diff options
Diffstat (limited to 'src/game/shared')
53 files changed, 3480 insertions, 0 deletions
diff --git a/src/game/shared/CMakeLists.txt b/src/game/shared/CMakeLists.txt new file mode 100644 index 0000000..73da95f --- /dev/null +++ b/src/game/shared/CMakeLists.txt @@ -0,0 +1,25 @@ +add_library(shared STATIC + "${CMAKE_CURRENT_LIST_DIR}/const.hh" + "${CMAKE_CURRENT_LIST_DIR}/coord.hh" + "${CMAKE_CURRENT_LIST_DIR}/game.cc" + "${CMAKE_CURRENT_LIST_DIR}/game.hh" + "${CMAKE_CURRENT_LIST_DIR}/game_items.cc" + "${CMAKE_CURRENT_LIST_DIR}/game_items.hh" + "${CMAKE_CURRENT_LIST_DIR}/game_voxels.cc" + "${CMAKE_CURRENT_LIST_DIR}/game_voxels.hh" + "${CMAKE_CURRENT_LIST_DIR}/globals.cc" + "${CMAKE_CURRENT_LIST_DIR}/globals.hh" + "${CMAKE_CURRENT_LIST_DIR}/pch.hh" + "${CMAKE_CURRENT_LIST_DIR}/protocol.cc" + "${CMAKE_CURRENT_LIST_DIR}/protocol.hh" + "${CMAKE_CURRENT_LIST_DIR}/splash.cc" + "${CMAKE_CURRENT_LIST_DIR}/splash.hh" + "${CMAKE_CURRENT_LIST_DIR}/types.hh") +target_compile_features(shared PUBLIC cxx_std_20) +target_include_directories(shared PRIVATE "${PROJECT_SOURCE_DIR}/src") +target_include_directories(shared PRIVATE "${PROJECT_SOURCE_DIR}/src/game") +target_precompile_headers(shared PRIVATE "${CMAKE_CURRENT_LIST_DIR}/pch.hh") +target_link_libraries(shared PUBLIC core enet entt FNL miniz parson) + +add_subdirectory(entity) +add_subdirectory(world) diff --git a/src/game/shared/const.hh b/src/game/shared/const.hh new file mode 100644 index 0000000..187962a --- /dev/null +++ b/src/game/shared/const.hh @@ -0,0 +1,43 @@ +#pragma once + +#include "core/math/constexpr.hh" + +constexpr static unsigned int CHUNK_SIZE = 16; +constexpr static unsigned int CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE; +constexpr static unsigned int CHUNK_VOLUME = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE; +constexpr static unsigned int CHUNK_BITSHIFT = math::log2(CHUNK_SIZE); + +template<typename T> +constexpr static glm::vec<3, T> DIR_NORTH = glm::vec<3, T>(0, 0, +1); +template<typename T> +constexpr static glm::vec<3, T> DIR_SOUTH = glm::vec<3, T>(0, 0, -1); +template<typename T> +constexpr static glm::vec<3, T> DIR_EAST = glm::vec<3, T>(-1, 0, 0); +template<typename T> +constexpr static glm::vec<3, T> DIR_WEST = glm::vec<3, T>(+1, 0, 0); +template<typename T> +constexpr static glm::vec<3, T> DIR_DOWN = glm::vec<3, T>(0, -1, 0); +template<typename T> +constexpr static glm::vec<3, T> DIR_UP = glm::vec<3, T>(0, +1, 0); + +template<typename T> +constexpr static glm::vec<3, T> DIR_FORWARD = glm::vec<3, T>(0, 0, +1); +template<typename T> +constexpr static glm::vec<3, T> DIR_BACK = glm::vec<3, T>(0, 0, -1); +template<typename T> +constexpr static glm::vec<3, T> DIR_LEFT = glm::vec<3, T>(-1, 0, 0); +template<typename T> +constexpr static glm::vec<3, T> DIR_RIGHT = glm::vec<3, T>(+1, 0, 0); + +template<typename T> +constexpr static glm::vec<3, T> UNIT_X = glm::vec<3, T>(1, 0, 0); +template<typename T> +constexpr static glm::vec<3, T> UNIT_Y = glm::vec<3, T>(0, 1, 0); +template<typename T> +constexpr static glm::vec<3, T> UNIT_Z = glm::vec<3, T>(0, 0, 1); + +template<typename T> +constexpr static glm::vec<2, T> ZERO_VEC2 = glm::vec<2, T>(0, 0); + +template<typename T> +constexpr static glm::vec<3, T> ZERO_VEC3 = glm::vec<3, T>(0, 0, 0); diff --git a/src/game/shared/coord.hh b/src/game/shared/coord.hh new file mode 100644 index 0000000..9d9be18 --- /dev/null +++ b/src/game/shared/coord.hh @@ -0,0 +1,145 @@ +#pragma once + +#include "shared/const.hh" +#include "shared/types.hh" + +namespace coord +{ +constexpr chunk_pos to_chunk(const voxel_pos& vpos); +} // namespace coord + +namespace coord +{ +constexpr local_pos to_local(const voxel_pos& vpos); +constexpr local_pos to_local(const glm::fvec3& fvec); +constexpr local_pos to_local(std::size_t index); +} // namespace coord + +namespace coord +{ +constexpr voxel_pos to_voxel(const chunk_pos& cpos, const local_pos& lpos); +constexpr voxel_pos to_voxel(const chunk_pos& cpos, const glm::fvec3& fvec); +} // namespace coord + +namespace coord +{ +constexpr std::size_t to_index(const local_pos& lpos); +} // namespace coord + +namespace coord +{ +constexpr glm::fvec3 to_relative(const chunk_pos& pivot_cpos, const chunk_pos& cpos, const glm::fvec3& fvec); +constexpr glm::fvec3 to_relative(const chunk_pos& pivot_cpos, const glm::fvec3& pivot_fvec, const chunk_pos& cpos); +constexpr glm::fvec3 to_relative(const chunk_pos& pivot_cpos, const glm::fvec3& pivot_fvec, const chunk_pos& cpos, const glm::fvec3& fvec); +} // namespace coord + +namespace coord +{ +constexpr glm::fvec3 to_fvec3(const chunk_pos& cpos); +constexpr glm::fvec3 to_fvec3(const chunk_pos& cpos, const glm::fvec3& fpos); +} // namespace coord + +inline constexpr chunk_pos coord::to_chunk(const voxel_pos& vpos) +{ + return chunk_pos { + static_cast<chunk_pos::value_type>(vpos.x >> CHUNK_BITSHIFT), + static_cast<chunk_pos::value_type>(vpos.y >> CHUNK_BITSHIFT), + static_cast<chunk_pos::value_type>(vpos.z >> CHUNK_BITSHIFT), + }; +} + +inline constexpr local_pos coord::to_local(const voxel_pos& vpos) +{ + return local_pos { + static_cast<local_pos::value_type>(math::mod_signed<voxel_pos::value_type>(vpos.x, CHUNK_SIZE)), + static_cast<local_pos::value_type>(math::mod_signed<voxel_pos::value_type>(vpos.y, CHUNK_SIZE)), + static_cast<local_pos::value_type>(math::mod_signed<voxel_pos::value_type>(vpos.z, CHUNK_SIZE)), + }; +} + +inline constexpr local_pos coord::to_local(const glm::fvec3& fvec) +{ + return local_pos { + static_cast<local_pos::value_type>(fvec.x), + static_cast<local_pos::value_type>(fvec.y), + static_cast<local_pos::value_type>(fvec.z), + }; +} + +inline constexpr local_pos coord::to_local(std::size_t index) +{ + return local_pos { + static_cast<local_pos::value_type>((index % CHUNK_SIZE)), + static_cast<local_pos::value_type>((index / CHUNK_SIZE) / CHUNK_SIZE), + static_cast<local_pos::value_type>((index / CHUNK_SIZE) % CHUNK_SIZE), + }; +} + +inline constexpr voxel_pos coord::to_voxel(const chunk_pos& cpos, const local_pos& lpos) +{ + return voxel_pos { + lpos.x + (static_cast<voxel_pos::value_type>(cpos.x) << CHUNK_BITSHIFT), + lpos.y + (static_cast<voxel_pos::value_type>(cpos.y) << CHUNK_BITSHIFT), + lpos.z + (static_cast<voxel_pos::value_type>(cpos.z) << CHUNK_BITSHIFT), + }; +} + +inline constexpr voxel_pos coord::to_voxel(const chunk_pos& cpos, const glm::fvec3& fvec) +{ + return voxel_pos { + static_cast<voxel_pos::value_type>(fvec.x) + (static_cast<voxel_pos::value_type>(cpos.x) << CHUNK_BITSHIFT), + static_cast<voxel_pos::value_type>(fvec.y) + (static_cast<voxel_pos::value_type>(cpos.y) << CHUNK_BITSHIFT), + static_cast<voxel_pos::value_type>(fvec.z) + (static_cast<voxel_pos::value_type>(cpos.z) << CHUNK_BITSHIFT), + }; +} + +inline constexpr std::size_t coord::to_index(const local_pos& lpos) +{ + return static_cast<std::size_t>((lpos.y * CHUNK_SIZE + lpos.z) * CHUNK_SIZE + lpos.x); +} + +inline constexpr glm::fvec3 coord::to_relative(const chunk_pos& pivot_cpos, const chunk_pos& cpos, const glm::fvec3& fvec) +{ + return glm::fvec3 { + static_cast<float>((cpos.x - pivot_cpos.x) << CHUNK_BITSHIFT) + fvec.x, + static_cast<float>((cpos.y - pivot_cpos.y) << CHUNK_BITSHIFT) + fvec.y, + static_cast<float>((cpos.z - pivot_cpos.z) << CHUNK_BITSHIFT) + fvec.z, + }; +} + +inline constexpr glm::fvec3 coord::to_relative(const chunk_pos& pivot_cpos, const glm::fvec3& pivot_fvec, const chunk_pos& cpos) +{ + return glm::fvec3 { + static_cast<float>((cpos.x - pivot_cpos.x) << CHUNK_BITSHIFT) - pivot_fvec.x, + static_cast<float>((cpos.y - pivot_cpos.y) << CHUNK_BITSHIFT) - pivot_fvec.y, + static_cast<float>((cpos.z - pivot_cpos.z) << CHUNK_BITSHIFT) - pivot_fvec.z, + }; +} + +inline constexpr glm::fvec3 coord::to_relative(const chunk_pos& pivot_cpos, const glm::fvec3& pivot_fvec, const chunk_pos& cpos, + const glm::fvec3& fvec) +{ + return glm::fvec3 { + static_cast<float>((cpos.x - pivot_cpos.x) << CHUNK_BITSHIFT) + (fvec.x - pivot_fvec.x), + static_cast<float>((cpos.y - pivot_cpos.y) << CHUNK_BITSHIFT) + (fvec.y - pivot_fvec.y), + static_cast<float>((cpos.z - pivot_cpos.z) << CHUNK_BITSHIFT) + (fvec.z - pivot_fvec.z), + }; +} + +inline constexpr glm::fvec3 coord::to_fvec3(const chunk_pos& cpos) +{ + return glm::fvec3 { + static_cast<float>(cpos.x << CHUNK_BITSHIFT), + static_cast<float>(cpos.y << CHUNK_BITSHIFT), + static_cast<float>(cpos.z << CHUNK_BITSHIFT), + }; +} + +inline constexpr glm::fvec3 coord::to_fvec3(const chunk_pos& cpos, const glm::fvec3& fpos) +{ + return glm::fvec3 { + fpos.x + static_cast<float>(cpos.x << CHUNK_BITSHIFT), + fpos.y + static_cast<float>(cpos.y << CHUNK_BITSHIFT), + fpos.z + static_cast<float>(cpos.z << CHUNK_BITSHIFT), + }; +} diff --git a/src/game/shared/entity/CMakeLists.txt b/src/game/shared/entity/CMakeLists.txt new file mode 100644 index 0000000..36e45b6 --- /dev/null +++ b/src/game/shared/entity/CMakeLists.txt @@ -0,0 +1,16 @@ +target_sources(shared PRIVATE + "${CMAKE_CURRENT_LIST_DIR}/collision.cc" + "${CMAKE_CURRENT_LIST_DIR}/collision.hh" + "${CMAKE_CURRENT_LIST_DIR}/factory.cc" + "${CMAKE_CURRENT_LIST_DIR}/factory.hh" + "${CMAKE_CURRENT_LIST_DIR}/gravity.cc" + "${CMAKE_CURRENT_LIST_DIR}/gravity.hh" + "${CMAKE_CURRENT_LIST_DIR}/grounded.hh" + "${CMAKE_CURRENT_LIST_DIR}/head.hh" + "${CMAKE_CURRENT_LIST_DIR}/player.hh" + "${CMAKE_CURRENT_LIST_DIR}/stasis.cc" + "${CMAKE_CURRENT_LIST_DIR}/stasis.hh" + "${CMAKE_CURRENT_LIST_DIR}/transform.cc" + "${CMAKE_CURRENT_LIST_DIR}/transform.hh" + "${CMAKE_CURRENT_LIST_DIR}/velocity.cc" + "${CMAKE_CURRENT_LIST_DIR}/velocity.hh") diff --git a/src/game/shared/entity/collision.cc b/src/game/shared/entity/collision.cc new file mode 100644 index 0000000..bc9209b --- /dev/null +++ b/src/game/shared/entity/collision.cc @@ -0,0 +1,173 @@ +#include "shared/pch.hh" + +#include "shared/entity/collision.hh" + +#include "core/math/constexpr.hh" + +#include "shared/entity/gravity.hh" +#include "shared/entity/grounded.hh" +#include "shared/entity/transform.hh" +#include "shared/entity/velocity.hh" + +#include "shared/world/dimension.hh" +#include "shared/world/voxel_registry.hh" + +#include "shared/coord.hh" +#include "shared/globals.hh" + +static int vgrid_collide(const world::Dimension* dimension, int d, entity::Collision& collision, entity::Transform& transform, + entity::Velocity& velocity, world::VoxelMaterial& touch_surface) +{ + const auto move = globals::fixed_frametime * velocity.value[d]; + const auto move_sign = math::sign<int>(move); + + const auto& ref_aabb = collision.aabb; + const auto current_aabb = ref_aabb.push(transform.local); + + auto next_aabb = math::AABBf(current_aabb); + next_aabb.min[d] += move; + next_aabb.max[d] += move; + + local_pos lpos_min; + lpos_min.x = static_cast<local_pos::value_type>(glm::floor(next_aabb.min.x)); + lpos_min.y = static_cast<local_pos::value_type>(glm::floor(next_aabb.min.y)); + lpos_min.z = static_cast<local_pos::value_type>(glm::floor(next_aabb.min.z)); + + local_pos lpos_max; + lpos_max.x = static_cast<local_pos::value_type>(glm::ceil(next_aabb.max.x)); + lpos_max.y = static_cast<local_pos::value_type>(glm::ceil(next_aabb.max.y)); + lpos_max.z = static_cast<local_pos::value_type>(glm::ceil(next_aabb.max.z)); + + // Other axes + const int u = (d + 1) % 3; + const int v = (d + 2) % 3; + + local_pos::value_type ddir; + local_pos::value_type dmin; + local_pos::value_type dmax; + + if(move < 0.0f) { + ddir = local_pos::value_type(+1); + dmin = lpos_min[d]; + dmax = lpos_max[d]; + } + else { + ddir = local_pos::value_type(-1); + dmin = lpos_max[d]; + dmax = lpos_min[d]; + } + + world::VoxelTouch latch_touch = world::VTOUCH_NONE; + glm::fvec3 latch_values = glm::fvec3(0.0f, 0.0f, 0.0f); + world::VoxelMaterial latch_surface = world::VMAT_UNKNOWN; + math::AABBf latch_vbox; + + for(auto i = dmin; i != dmax; i += ddir) { + for(auto j = lpos_min[u]; j < lpos_max[u]; ++j) + for(auto k = lpos_min[v]; k < lpos_max[v]; ++k) { + local_pos lpos; + lpos[d] = i; + lpos[u] = j; + lpos[v] = k; + + auto vpos = coord::to_voxel(transform.chunk, lpos); + auto voxel = dimension->get_voxel(vpos); + + if(voxel == nullptr) { + // Don't collide with something + // that we assume to be nothing + continue; + } + + math::AABBf vbox(voxel->get_collision().push(lpos)); + + if(!next_aabb.intersect(vbox)) { + // No intersection between the voxel + // and the entity's collision hull + continue; + } + + if(voxel->is_touch_type<world::VTOUCH_SOLID>()) { + // Solid touch type makes a collision + // response whenever it is encountered + velocity.value[d] = 0.0f; + touch_surface = voxel->get_surface_material(); + return move_sign; + } + + // In case of other touch types, they + // are latched and the last ever touch + // type is then responded to + if(voxel->get_touch_type() != world::VTOUCH_NONE) { + latch_touch = voxel->get_touch_type(); + latch_values = voxel->get_touch_values(); + latch_surface = voxel->get_surface_material(); + latch_vbox = vbox; + continue; + } + } + } + + if(latch_touch != world::VTOUCH_NONE) { + if(latch_touch == world::VTOUCH_BOUNCE) { + const auto move_distance = glm::abs(current_aabb.min[d] - next_aabb.min[d]); + const auto threshold = 2.0f * globals::fixed_frametime; + + if(move_distance > threshold) { + velocity.value[d] *= -latch_values[d]; + } + else { + velocity.value[d] = 0.0f; + } + + touch_surface = latch_surface; + + return move_sign; + } + + if(latch_touch == world::VTOUCH_SINK) { + velocity.value[d] *= latch_values[d]; + touch_surface = latch_surface; + return move_sign; + } + } + + return 0; +} + +void entity::Collision::fixed_update(world::Dimension* dimension) +{ + // FIXME: this isn't particularly accurate considering + // some voxels might be passable and some other voxels + // might apply some slowing factor; what I might do in the + // future is to add a specific value to the voxel registry + // entries that would specify the amount of force we apply + // to prevent player movement inside a specific voxel, plus + // we shouldn't treat all voxels as full cubes if we want + // to support slabs, stairs and non-full liquid voxels in the future + + auto group = dimension->entities.group<entity::Collision>(entt::get<entity::Transform, entity::Velocity>); + + for(auto [entity, collision, transform, velocity] : group.each()) { + auto surface = world::VMAT_UNKNOWN; + auto vertical_move = vgrid_collide(dimension, 1, collision, transform, velocity, surface); + + if(dimension->entities.any_of<entity::Gravity>(entity)) { + if(vertical_move == math::sign<int>(dimension->get_gravity())) { + dimension->entities.emplace_or_replace<entity::Grounded>(entity, entity::Grounded { surface }); + } + else { + dimension->entities.remove<entity::Grounded>(entity); + } + } + else { + // The entity cannot be grounded because the component + // setup of said entity should not let it comprehend the + // concept of resting on the ground (it flies around) + dimension->entities.remove<entity::Grounded>(entity); + } + + vgrid_collide(dimension, 0, collision, transform, velocity, surface); + vgrid_collide(dimension, 2, collision, transform, velocity, surface); + } +} diff --git a/src/game/shared/entity/collision.hh b/src/game/shared/entity/collision.hh new file mode 100644 index 0000000..b37f409 --- /dev/null +++ b/src/game/shared/entity/collision.hh @@ -0,0 +1,21 @@ +#pragma once + +#include "core/math/aabb.hh" + +namespace world +{ +class Dimension; +} // namespace world + +namespace entity +{ +struct Collision final { + math::AABBf aabb; + +public: + // NOTE: entity::Collision::fixed_update must be called + // before entity::Transform::fixed_update and entity::Velocity::fixed_update + // because both transform and velocity may be updated internally + static void fixed_update(world::Dimension* dimension); +}; +} // namespace entity diff --git a/src/game/shared/entity/factory.cc b/src/game/shared/entity/factory.cc new file mode 100644 index 0000000..619a418 --- /dev/null +++ b/src/game/shared/entity/factory.cc @@ -0,0 +1,37 @@ +#include "shared/pch.hh" + +#include "shared/entity/factory.hh" + +#include "shared/entity/collision.hh" +#include "shared/entity/gravity.hh" +#include "shared/entity/head.hh" +#include "shared/entity/player.hh" +#include "shared/entity/transform.hh" +#include "shared/entity/velocity.hh" + +#include "shared/world/dimension.hh" + +#include "shared/globals.hh" + +void entity::shared::create_player(world::Dimension* dimension, entt::entity entity) +{ + spdlog::debug("factory[{}]: assigning player components to {}", dimension->get_name(), static_cast<std::uint64_t>(entity)); + + auto& collision = dimension->entities.emplace_or_replace<entity::Collision>(entity); + collision.aabb.min = glm::fvec3(-0.4f, -1.6f, -0.4f); + collision.aabb.max = glm::fvec3(+0.4f, +0.2f, +0.4f); + + auto& head = dimension->entities.emplace_or_replace<entity::Head>(entity); + head.angles = glm::fvec3(0.0f, 0.0f, 0.0f); + head.offset = glm::fvec3(0.0f, 0.0f, 0.0f); + + dimension->entities.emplace_or_replace<entity::Player>(entity); + + auto& transform = dimension->entities.emplace_or_replace<entity::Transform>(entity); + transform.chunk = chunk_pos(0, 2, 0); + transform.local = glm::fvec3(0.0f, 0.0f, 0.0f); + transform.angles = glm::fvec3(0.0f, 0.0f, 0.0f); + + auto& velocity = dimension->entities.emplace_or_replace<entity::Velocity>(entity); + velocity.value = glm::fvec3(0.0f, 0.0f, 0.0f); +} diff --git a/src/game/shared/entity/factory.hh b/src/game/shared/entity/factory.hh new file mode 100644 index 0000000..e709060 --- /dev/null +++ b/src/game/shared/entity/factory.hh @@ -0,0 +1,11 @@ +#pragma once + +namespace world +{ +class Dimension; +} // namespace world + +namespace entity::shared +{ +void create_player(world::Dimension* dimension, entt::entity entity); +} // namespace entity::shared diff --git a/src/game/shared/entity/gravity.cc b/src/game/shared/entity/gravity.cc new file mode 100644 index 0000000..f1708aa --- /dev/null +++ b/src/game/shared/entity/gravity.cc @@ -0,0 +1,20 @@ +#include "shared/pch.hh" + +#include "shared/entity/gravity.hh" + +#include "shared/entity/stasis.hh" +#include "shared/entity/velocity.hh" + +#include "shared/world/dimension.hh" + +#include "shared/globals.hh" + +void entity::Gravity::fixed_update(world::Dimension* dimension) +{ + auto fixed_acceleration = globals::fixed_frametime * dimension->get_gravity(); + auto group = dimension->entities.group<entity::Gravity>(entt::get<entity::Velocity>, entt::exclude<entity::Stasis>); + + for(auto [entity, velocity] : group.each()) { + velocity.value.y += fixed_acceleration; + } +} diff --git a/src/game/shared/entity/gravity.hh b/src/game/shared/entity/gravity.hh new file mode 100644 index 0000000..21f4582 --- /dev/null +++ b/src/game/shared/entity/gravity.hh @@ -0,0 +1,14 @@ +#pragma once + +namespace world +{ +class Dimension; +} // namespace world + +namespace entity +{ +struct Gravity final { +public: + static void fixed_update(world::Dimension* dimension); +}; +} // namespace entity diff --git a/src/game/shared/entity/grounded.hh b/src/game/shared/entity/grounded.hh new file mode 100644 index 0000000..6c33d1d --- /dev/null +++ b/src/game/shared/entity/grounded.hh @@ -0,0 +1,12 @@ +#pragma once + +#include "shared/world/voxel.hh" + +namespace entity +{ +// Assigned to entities which are grounded +// according to the collision and gravity system +struct Grounded final { + world::VoxelMaterial surface; +}; +} // namespace entity diff --git a/src/game/shared/entity/head.hh b/src/game/shared/entity/head.hh new file mode 100644 index 0000000..3f5d6d1 --- /dev/null +++ b/src/game/shared/entity/head.hh @@ -0,0 +1,16 @@ +#pragma once + +namespace entity +{ +struct Head { + glm::fvec3 angles; + glm::fvec3 offset; +}; +} // namespace entity + +namespace entity::client +{ +// Client-side only - interpolated and previous head +struct HeadIntr final : public Head {}; +struct HeadPrev final : public Head {}; +} // namespace entity::client diff --git a/src/game/shared/entity/player.hh b/src/game/shared/entity/player.hh new file mode 100644 index 0000000..4bd0217 --- /dev/null +++ b/src/game/shared/entity/player.hh @@ -0,0 +1,6 @@ +#pragma once + +namespace entity +{ +struct Player final {}; +} // namespace entity diff --git a/src/game/shared/entity/stasis.cc b/src/game/shared/entity/stasis.cc new file mode 100644 index 0000000..3b86294 --- /dev/null +++ b/src/game/shared/entity/stasis.cc @@ -0,0 +1,21 @@ +#include "shared/pch.hh" + +#include "shared/entity/stasis.hh" + +#include "shared/entity/transform.hh" + +#include "shared/world/dimension.hh" + +void entity::Stasis::fixed_update(world::Dimension* dimension) +{ + auto view = dimension->entities.view<entity::Transform>(); + + for(auto [entity, transform] : view.each()) { + if(dimension->find_chunk(transform.chunk)) { + dimension->entities.remove<entity::Stasis>(entity); + } + else { + dimension->entities.emplace_or_replace<entity::Stasis>(entity); + } + } +} diff --git a/src/game/shared/entity/stasis.hh b/src/game/shared/entity/stasis.hh new file mode 100644 index 0000000..e6cb9e4 --- /dev/null +++ b/src/game/shared/entity/stasis.hh @@ -0,0 +1,16 @@ +#pragma once + +namespace world +{ +class Dimension; +} // namespace world + +namespace entity +{ +// Attached to entities with transform values +// out of bounds in a specific dimension +struct Stasis final { +public: + static void fixed_update(world::Dimension* dimension); +}; +} // namespace entity diff --git a/src/game/shared/entity/transform.cc b/src/game/shared/entity/transform.cc new file mode 100644 index 0000000..379ddd5 --- /dev/null +++ b/src/game/shared/entity/transform.cc @@ -0,0 +1,33 @@ +#include "shared/pch.hh" + +#include "shared/entity/transform.hh" + +#include "shared/world/dimension.hh" + +#include "shared/const.hh" + +constexpr inline static void update_component(unsigned int dim, entity::Transform& component) +{ + if(component.local[dim] >= CHUNK_SIZE) { + component.local[dim] -= CHUNK_SIZE; + component.chunk[dim] += 1; + return; + } + + if(component.local[dim] < 0.0f) { + component.local[dim] += CHUNK_SIZE; + component.chunk[dim] -= 1; + return; + } +} + +void entity::Transform::fixed_update(world::Dimension* dimension) +{ + auto view = dimension->entities.view<entity::Transform>(); + + for(auto [entity, transform] : view.each()) { + update_component(0U, transform); + update_component(1U, transform); + update_component(2U, transform); + } +} diff --git a/src/game/shared/entity/transform.hh b/src/game/shared/entity/transform.hh new file mode 100644 index 0000000..2b357f8 --- /dev/null +++ b/src/game/shared/entity/transform.hh @@ -0,0 +1,30 @@ +#pragma once + +#include "shared/types.hh" + +namespace world +{ +class Dimension; +} // namespace world + +namespace entity +{ +struct Transform { + chunk_pos chunk; + glm::fvec3 local; + glm::fvec3 angles; + +public: + // Updates entity::Transform values so that + // the local translation field is always within + // local coodrinates; [floating-point precision] + static void fixed_update(world::Dimension* dimension); +}; +} // namespace entity + +namespace entity::client +{ +// Client-side only - interpolated and previous transform +struct TransformIntr final : public Transform {}; +struct TransformPrev final : public Transform {}; +} // namespace entity::client diff --git a/src/game/shared/entity/velocity.cc b/src/game/shared/entity/velocity.cc new file mode 100644 index 0000000..86df445 --- /dev/null +++ b/src/game/shared/entity/velocity.cc @@ -0,0 +1,19 @@ +#include "shared/pch.hh" + +#include "shared/entity/velocity.hh" + +#include "shared/entity/stasis.hh" +#include "shared/entity/transform.hh" + +#include "shared/world/dimension.hh" + +#include "shared/globals.hh" + +void entity::Velocity::fixed_update(world::Dimension* dimension) +{ + auto group = dimension->entities.group<entity::Velocity>(entt::get<entity::Transform>, entt::exclude<entity::Stasis>); + + for(auto [entity, velocity, transform] : group.each()) { + transform.local += velocity.value * globals::fixed_frametime; + } +} diff --git a/src/game/shared/entity/velocity.hh b/src/game/shared/entity/velocity.hh new file mode 100644 index 0000000..c8a1c91 --- /dev/null +++ b/src/game/shared/entity/velocity.hh @@ -0,0 +1,19 @@ +#pragma once + +namespace world +{ +class Dimension; +} // namespace world + +namespace entity +{ +struct Velocity final { + glm::fvec3 value; + +public: + // Updates entities entity::Transform values + // according to velocities multiplied by fixed_frametime. + // NOTE: This system was previously called inertial + static void fixed_update(world::Dimension* dimension); +}; +} // namespace entity diff --git a/src/game/shared/game.cc b/src/game/shared/game.cc new file mode 100644 index 0000000..a3a724d --- /dev/null +++ b/src/game/shared/game.cc @@ -0,0 +1,127 @@ +#include "shared/pch.hh" + +#include "shared/game.hh" + +#include "core/io/cmdline.hh" +#include "core/io/physfs.hh" + +static std::filesystem::path get_gamepath(void) +{ + if(auto gamepath = io::cmdline::get_cstr("gamepath")) { + // Allow users and third-party launchers to override + // content location. Perhaps this would work to allow + // for a Minecraft-like versioning approach? + return std::filesystem::path(gamepath); + } + + return std::filesystem::current_path() / "assets"; +} + +static std::filesystem::path get_userpath(void) +{ + if(auto userpath = io::cmdline::get_cstr("userpath")) { + // Allow users and third-party launchers to override + // user data location. Perhaps this would work to allow + // for a Minecraft-like versioning approach? + return std::filesystem::path(userpath); + } + + if(auto win_appdata = std::getenv("APPDATA")) { + // On pre-seven Windows systems it's just AppData + // On post-seven Windows systems it's AppData/Roaming + return std::filesystem::path(win_appdata) / "voxelius"; + } + + if(auto xdg_home = std::getenv("XDG_DATA_HOME")) { + // Systems with an active X11/Wayland session + // theoretically should have this defined, and + // it can be different from ${HOME} (I think). + return std::filesystem::path(xdg_home) / ".voxelius"; + } + + if(auto unix_home = std::getenv("HOME")) { + // Any spherical UNIX/UNIX-like system in vacuum + // has this defined for every single user process. + return std::filesystem::path(unix_home) / ".voxelius"; + } + + // Give up and save stuff into CWD + return std::filesystem::current_path(); +} + +void shared_game::init(int argc, char** argv, std::string_view logfile) +{ + auto logger = spdlog::default_logger(); + auto& logger_sinks = logger->sinks(); + + logger_sinks.clear(); + logger_sinks.push_back(std::make_shared<spdlog::sinks::stderr_color_sink_mt>()); + + if(logfile.size()) { + logger_sinks.push_back(std::make_shared<spdlog::sinks::basic_file_sink_mt>(std::string(logfile), false)); + } + +#if defined(NDEBUG) + constexpr auto default_loglevel = spdlog::level::info; +#else + constexpr auto default_loglevel = spdlog::level::trace; +#endif + + if(io::cmdline::contains("quiet")) { + logger->set_level(spdlog::level::warn); + } + else if(io::cmdline::contains("verbose")) { + logger->set_level(spdlog::level::trace); + } + else { + logger->set_level(default_loglevel); + } + + logger->set_pattern("%H:%M:%S.%e %^[%L]%$ %v"); + logger->flush(); + + if(!PHYSFS_init(argv[0])) { + spdlog::critical("physfs: init failed: {}", io::physfs_error()); + std::terminate(); + } + + auto gamepath = get_gamepath(); + auto userpath = get_userpath(); + + spdlog::info("shared_game: set gamepath to {}", gamepath.string()); + spdlog::info("shared_game: set userpath to {}", userpath.string()); + + std::error_code ignore_error = {}; + std::filesystem::create_directories(gamepath, ignore_error); + std::filesystem::create_directories(userpath, ignore_error); + + if(!PHYSFS_mount(gamepath.string().c_str(), nullptr, false)) { + spdlog::critical("physfs: mount {} failed: {}", gamepath.string(), io::physfs_error()); + std::terminate(); + } + + if(!PHYSFS_mount(userpath.string().c_str(), nullptr, false)) { + spdlog::critical("physfs: mount {} failed: {}", userpath.string(), io::physfs_error()); + std::terminate(); + } + + if(!PHYSFS_setWriteDir(userpath.string().c_str())) { + spdlog::critical("physfs: setwritedir {} failed: {}", userpath.string(), io::physfs_error()); + std::terminate(); + } + + if(enet_initialize()) { + spdlog::critical("enet: init failed"); + std::terminate(); + } +} + +void shared_game::shutdown(void) +{ + enet_deinitialize(); + + if(!PHYSFS_deinit()) { + spdlog::critical("physfs: deinit failed: {}", io::physfs_error()); + std::terminate(); + } +} diff --git a/src/game/shared/game.hh b/src/game/shared/game.hh new file mode 100644 index 0000000..6e89a3c --- /dev/null +++ b/src/game/shared/game.hh @@ -0,0 +1,7 @@ +#pragma once + +namespace shared_game +{ +void init(int argc, char** argv, std::string_view logfile = {}); +void shutdown(void); +} // namespace shared_game diff --git a/src/game/shared/game_items.cc b/src/game/shared/game_items.cc new file mode 100644 index 0000000..e239063 --- /dev/null +++ b/src/game/shared/game_items.cc @@ -0,0 +1,65 @@ +#include "shared/pch.hh" + +#include "shared/game_items.hh" + +#include "shared/world/item_registry.hh" + +#include "shared/game_voxels.hh" + +const world::Item* game_items::stone = nullptr; +const world::Item* game_items::cobblestone = nullptr; +const world::Item* game_items::dirt = nullptr; +const world::Item* game_items::grass = nullptr; +const world::Item* game_items::oak_leaves = nullptr; +const world::Item* game_items::oak_planks = nullptr; +const world::Item* game_items::oak_log = nullptr; +const world::Item* game_items::glass = nullptr; +const world::Item* game_items::slime = nullptr; + +void game_items::populate(void) +{ + auto stone_builder = world::ItemBuilder("stone"); + stone_builder.set_texture("textures/item/stone.png"); + stone_builder.set_place_voxel(game_voxels::stone); + stone = world::item_registry::register_item(stone_builder); + + auto cobblestone_builder = world::ItemBuilder("cobblestone"); + cobblestone_builder.set_texture("textures/item/cobblestone.png"); + cobblestone_builder.set_place_voxel(game_voxels::cobblestone); + cobblestone = world::item_registry::register_item(cobblestone_builder); + + auto dirt_builder = world::ItemBuilder("dirt"); + dirt_builder.set_texture("textures/item/dirt.png"); + dirt_builder.set_place_voxel(game_voxels::dirt); + dirt = world::item_registry::register_item(dirt_builder); + + auto grass_builder = world::ItemBuilder("grass"); + grass_builder.set_texture("textures/item/grass.png"); + grass_builder.set_place_voxel(game_voxels::grass); + grass = world::item_registry::register_item(grass_builder); + + auto oak_leaves_builder = world::ItemBuilder("oak_leaves"); + oak_leaves_builder.set_texture("textures/item/oak_leaves.png"); + oak_leaves_builder.set_place_voxel(game_voxels::oak_leaves); + oak_leaves = world::item_registry::register_item(oak_leaves_builder); + + auto oak_planks_builder = world::ItemBuilder("oak_planks"); + oak_planks_builder.set_texture("textures/item/oak_planks.png"); + oak_planks_builder.set_place_voxel(game_voxels::oak_planks); + oak_planks = world::item_registry::register_item(oak_planks_builder); + + auto oak_log_builder = world::ItemBuilder("oak_log"); + oak_log_builder.set_texture("textures/item/oak_log.png"); + oak_log_builder.set_place_voxel(game_voxels::oak_log); + oak_log = world::item_registry::register_item(oak_log_builder); + + auto glass_builder = world::ItemBuilder("glass"); + glass_builder.set_texture("textures/item/glass.png"); + glass_builder.set_place_voxel(game_voxels::glass); + glass = world::item_registry::register_item(glass_builder); + + auto slime_builder = world::ItemBuilder("slime"); + slime_builder.set_texture("textures/item/slime.png"); + slime_builder.set_place_voxel(game_voxels::slime); + slime = world::item_registry::register_item(slime_builder); +} diff --git a/src/game/shared/game_items.hh b/src/game/shared/game_items.hh new file mode 100644 index 0000000..c9b3638 --- /dev/null +++ b/src/game/shared/game_items.hh @@ -0,0 +1,24 @@ +#pragma once + +namespace world +{ +class Item; +} // namespace world + +namespace game_items +{ +extern const world::Item* stone; +extern const world::Item* cobblestone; +extern const world::Item* dirt; +extern const world::Item* grass; +extern const world::Item* oak_leaves; +extern const world::Item* oak_planks; +extern const world::Item* oak_log; +extern const world::Item* glass; +extern const world::Item* slime; +} // namespace game_items + +namespace game_items +{ +void populate(void); +} // namespace game_items diff --git a/src/game/shared/game_voxels.cc b/src/game/shared/game_voxels.cc new file mode 100644 index 0000000..c4c4ec3 --- /dev/null +++ b/src/game/shared/game_voxels.cc @@ -0,0 +1,152 @@ +#include "shared/pch.hh" + +#include "shared/game_voxels.hh" + +#include "shared/world/dimension.hh" +#include "shared/world/voxel_registry.hh" + +#include "shared/const.hh" + +const world::Voxel* game_voxels::cobblestone = nullptr; +const world::Voxel* game_voxels::dirt = nullptr; +const world::Voxel* game_voxels::grass = nullptr; +const world::Voxel* game_voxels::stone = nullptr; +const world::Voxel* game_voxels::vtest = nullptr; +const world::Voxel* game_voxels::vtest_ck = nullptr; +const world::Voxel* game_voxels::oak_leaves = nullptr; +const world::Voxel* game_voxels::oak_planks = nullptr; +const world::Voxel* game_voxels::oak_log = nullptr; +const world::Voxel* game_voxels::glass = nullptr; +const world::Voxel* game_voxels::slime = nullptr; + +static void dirt_tick(world::Dimension* dimension, const voxel_pos& vpos) +{ + auto grass_found = false; + auto air_above = false; + + for(voxel_pos::value_type dx = -1; dx <= 1 && !grass_found; ++dx) { + for(voxel_pos::value_type dy = -1; dy <= 1 && !grass_found; ++dy) { + for(voxel_pos::value_type dz = -1; dz <= 1 && !grass_found; ++dz) { + if(dx == 0 && dy == 0 && dz == 0) { + // Skip self + continue; + } + + auto neighbour_vpos = vpos + voxel_pos(dx, dy, dz); + auto neighbour_voxel = dimension->get_voxel(neighbour_vpos); + + // Voxel pointers returned by get_voxel() are the exact same + // returned by the voxel registry, so we can compare pointers directly + // and not bother with voxel_id property comparisons + if(neighbour_voxel == game_voxels::grass) { + grass_found = true; + break; + } + } + } + } + + auto above_vpos = vpos + voxel_pos(0, 1, 0); + auto above_voxel = dimension->get_voxel(above_vpos); + + if(above_voxel == nullptr || above_voxel->is_surface_material<world::VMAT_GLASS>()) { + air_above = true; + } + + if(grass_found && air_above) { + dimension->set_voxel(game_voxels::grass, vpos); + } +} + +static void grass_tick(world::Dimension* dimension, const voxel_pos& vpos) +{ + auto above_vpos = vpos + voxel_pos(0, 1, 0); + auto above_voxel = dimension->get_voxel(above_vpos); + + if(above_voxel && !above_voxel->is_surface_material<world::VMAT_GLASS>()) { + // Decay into dirt if something is blocking airflow + dimension->set_voxel(game_voxels::dirt, vpos); + } +} + +void game_voxels::populate(void) +{ + auto stone_builder = world::VoxelBuilder("stone"); + stone_builder.add_default_texture("textures/voxel/stone_01.png"); + stone_builder.add_default_texture("textures/voxel/stone_02.png"); + stone_builder.add_default_texture("textures/voxel/stone_03.png"); + stone_builder.add_default_texture("textures/voxel/stone_04.png"); + stone = world::voxel_registry::register_voxel(stone_builder); + + auto cobblestone_builder = world::VoxelBuilder("cobblestone"); + cobblestone_builder.add_default_texture("textures/voxel/cobblestone_01.png"); + cobblestone_builder.add_default_texture("textures/voxel/cobblestone_02.png"); + cobblestone = world::voxel_registry::register_voxel(cobblestone_builder); + + auto dirt_builder = world::VoxelBuilder("dirt"); + dirt_builder.add_default_texture("textures/voxel/dirt_01.png"); + dirt_builder.add_default_texture("textures/voxel/dirt_02.png"); + dirt_builder.add_default_texture("textures/voxel/dirt_03.png"); + dirt_builder.add_default_texture("textures/voxel/dirt_04.png"); + dirt_builder.set_surface_material(world::VMAT_DIRT); + dirt_builder.set_on_tick(&dirt_tick); + dirt = world::voxel_registry::register_voxel(dirt_builder); + + auto grass_builder = world::VoxelBuilder("grass"); + grass_builder.add_default_texture("textures/voxel/grass_side_01.png"); + grass_builder.add_default_texture("textures/voxel/grass_side_02.png"); + grass_builder.add_face_texture(world::VFACE_BOTTOM, "textures/voxel/dirt_01.png"); + grass_builder.add_face_texture(world::VFACE_BOTTOM, "textures/voxel/dirt_02.png"); + grass_builder.add_face_texture(world::VFACE_BOTTOM, "textures/voxel/dirt_03.png"); + grass_builder.add_face_texture(world::VFACE_BOTTOM, "textures/voxel/dirt_04.png"); + grass_builder.add_face_texture(world::VFACE_TOP, "textures/voxel/grass_01.png"); + grass_builder.add_face_texture(world::VFACE_TOP, "textures/voxel/grass_02.png"); + grass_builder.set_surface_material(world::VMAT_GRASS); + grass_builder.set_on_tick(&grass_tick); + grass = world::voxel_registry::register_voxel(grass_builder); + + auto vtest_builder = world::VoxelBuilder("vtest"); + vtest_builder.add_default_texture("textures/voxel/vtest_F1.png"); + vtest_builder.add_default_texture("textures/voxel/vtest_F2.png"); + vtest_builder.add_default_texture("textures/voxel/vtest_F3.png"); + vtest_builder.add_default_texture("textures/voxel/vtest_F4.png"); + vtest_builder.set_animated(true); + vtest = world::voxel_registry::register_voxel(vtest_builder); + + auto vtest_ck_builder = world::VoxelBuilder("vtest_ck"); + vtest_ck_builder.add_default_texture("textures/voxel/chromakey.png"); + vtest_ck = world::voxel_registry::register_voxel(vtest_ck_builder); + + auto oak_leaves_builder = world::VoxelBuilder("oak_leaves"); + oak_leaves_builder.add_default_texture("textures/voxel/oak_leaves.png"); + oak_leaves_builder.set_surface_material(world::VMAT_GRASS); + oak_leaves = world::voxel_registry::register_voxel(oak_leaves_builder); + + auto oak_planks_builder = world::VoxelBuilder("oak_planks"); + oak_planks_builder.add_default_texture("textures/voxel/oak_planks_01.png"); + oak_planks_builder.add_default_texture("textures/voxel/oak_planks_02.png"); + oak_planks_builder.set_surface_material(world::VMAT_WOOD); + oak_planks = world::voxel_registry::register_voxel(oak_planks_builder); + + auto oak_log_builder = world::VoxelBuilder("oak_log"); + oak_log_builder.add_default_texture("textures/voxel/oak_wood_01.png"); + oak_log_builder.add_default_texture("textures/voxel/oak_wood_02.png"); + oak_log_builder.add_face_texture(world::VFACE_BOTTOM, "textures/voxel/oak_wood_top.png"); + oak_log_builder.add_face_texture(world::VFACE_TOP, "textures/voxel/oak_wood_top.png"); + oak_log_builder.set_surface_material(world::VMAT_WOOD); + oak_log = world::voxel_registry::register_voxel(oak_log_builder); + + auto glass_builder = world::VoxelBuilder("glass"); + glass_builder.add_default_texture("textures/voxel/glass_01.png"); + glass_builder.set_render_mode(world::VRENDER_BLEND); + glass_builder.set_surface_material(world::VMAT_GLASS); + glass = world::voxel_registry::register_voxel(glass_builder); + + auto slime_builder = world::VoxelBuilder("slime"); + slime_builder.add_default_texture("textures/voxel/slime_01.png"); + slime_builder.set_render_mode(world::VRENDER_BLEND); + slime_builder.set_surface_material(world::VMAT_SLOSH); + slime_builder.set_touch_type(world::VTOUCH_BOUNCE); + slime_builder.set_touch_values({ 0.00f, 0.60f, 0.00f }); + slime = world::voxel_registry::register_voxel(slime_builder); +} diff --git a/src/game/shared/game_voxels.hh b/src/game/shared/game_voxels.hh new file mode 100644 index 0000000..a8d155f --- /dev/null +++ b/src/game/shared/game_voxels.hh @@ -0,0 +1,26 @@ +#pragma once + +namespace world +{ +class Voxel; +} // namespace world + +namespace game_voxels +{ +extern const world::Voxel* cobblestone; +extern const world::Voxel* dirt; +extern const world::Voxel* grass; +extern const world::Voxel* stone; +extern const world::Voxel* vtest; +extern const world::Voxel* vtest_ck; +extern const world::Voxel* oak_leaves; +extern const world::Voxel* oak_planks; +extern const world::Voxel* oak_log; +extern const world::Voxel* glass; +extern const world::Voxel* slime; +} // namespace game_voxels + +namespace game_voxels +{ +void populate(void); +} // namespace game_voxels diff --git a/src/game/shared/globals.cc b/src/game/shared/globals.cc new file mode 100644 index 0000000..8552214 --- /dev/null +++ b/src/game/shared/globals.cc @@ -0,0 +1,12 @@ +#include "shared/pch.hh" + +#include "shared/globals.hh" + +entt::dispatcher globals::dispatcher; + +float globals::fixed_frametime; +float globals::fixed_frametime_avg; +std::uint64_t globals::fixed_frametime_us; +std::size_t globals::fixed_framecount; + +std::uint64_t globals::curtime; diff --git a/src/game/shared/globals.hh b/src/game/shared/globals.hh new file mode 100644 index 0000000..39a12a7 --- /dev/null +++ b/src/game/shared/globals.hh @@ -0,0 +1,19 @@ +#pragma once + +namespace globals +{ +extern entt::dispatcher dispatcher; +} // namespace globals + +namespace globals +{ +extern float fixed_frametime; +extern float fixed_frametime_avg; +extern std::uint64_t fixed_frametime_us; +extern std::size_t fixed_framecount; +} // namespace globals + +namespace globals +{ +extern std::uint64_t curtime; +} // namespace globals diff --git a/src/game/shared/pch.hh b/src/game/shared/pch.hh new file mode 100644 index 0000000..e978e0f --- /dev/null +++ b/src/game/shared/pch.hh @@ -0,0 +1,19 @@ +#pragma once + +#include <core/pch.hh> // inherit dependent includes from core.lib + +#include <csignal> + +#include <enet/enet.h> + +#include <entt/entity/registry.hpp> +#include <entt/signal/dispatcher.hpp> + +#include <fastnoiselite.h> + +#include <miniz.h> + +#include <parson.h> + +#include <spdlog/sinks/basic_file_sink.h> +#include <spdlog/sinks/stdout_color_sinks.h> diff --git a/src/game/shared/protocol.cc b/src/game/shared/protocol.cc new file mode 100644 index 0000000..4c0c894 --- /dev/null +++ b/src/game/shared/protocol.cc @@ -0,0 +1,518 @@ +#include "shared/pch.hh" + +#include "shared/protocol.hh" + +#include "core/io/buffer.hh" + +#include "shared/entity/head.hh" +#include "shared/entity/player.hh" +#include "shared/entity/transform.hh" +#include "shared/entity/velocity.hh" + +#include "shared/world/chunk.hh" +#include "shared/world/dimension.hh" + +#include "shared/globals.hh" + +static io::ReadBuffer read_buffer; +static io::WriteBuffer write_buffer; + +ENetPacket* protocol::encode(const protocol::StatusRequest& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::StatusRequest::ID); + write_buffer.write<std::uint32_t>(packet.game_version_major); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::StatusResponse& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::StatusResponse::ID); + write_buffer.write<std::uint32_t>(packet.game_version_major); + write_buffer.write<std::uint16_t>(packet.max_players); + write_buffer.write<std::uint16_t>(packet.num_players); + write_buffer.write<std::string_view>(packet.motd); + write_buffer.write<std::uint32_t>(packet.game_version_minor); + write_buffer.write<std::uint32_t>(packet.game_version_patch); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::LoginRequest& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::LoginRequest::ID); + write_buffer.write<std::uint32_t>(packet.game_version_major); + write_buffer.write<std::uint64_t>(packet.voxel_registry_checksum); + write_buffer.write<std::uint64_t>(packet.item_registry_checksum); + write_buffer.write<std::uint64_t>(packet.password_hash); + write_buffer.write<std::string_view>(packet.username.substr(0, protocol::MAX_USERNAME)); + write_buffer.write<std::uint32_t>(packet.game_version_minor); + write_buffer.write<std::uint32_t>(packet.game_version_patch); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::LoginResponse& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::LoginResponse::ID); + write_buffer.write<std::uint16_t>(packet.client_index); + write_buffer.write<std::uint64_t>(packet.client_identity); + write_buffer.write<std::uint16_t>(packet.server_tickrate); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::Disconnect& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::Disconnect::ID); + write_buffer.write<std::string_view>(packet.reason); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::ChunkVoxels& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::ChunkVoxels::ID); + write_buffer.write<std::int32_t>(packet.chunk.x); + write_buffer.write<std::int32_t>(packet.chunk.y); + write_buffer.write<std::int32_t>(packet.chunk.z); + packet.voxels.serialize(write_buffer); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::EntityTransform& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::EntityTransform::ID); + write_buffer.write<std::uint64_t>(static_cast<std::uint64_t>(packet.entity)); + write_buffer.write<std::int32_t>(packet.chunk.x); + write_buffer.write<std::int32_t>(packet.chunk.y); + write_buffer.write<std::int32_t>(packet.chunk.z); + write_buffer.write<float>(packet.local.x); + write_buffer.write<float>(packet.local.y); + write_buffer.write<float>(packet.local.z); + write_buffer.write<float>(packet.angles.x); + write_buffer.write<float>(packet.angles.y); + write_buffer.write<float>(packet.angles.z); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::EntityHead& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::EntityHead::ID); + write_buffer.write<std::uint64_t>(static_cast<std::uint64_t>(packet.entity)); + write_buffer.write<float>(packet.angles.x); + write_buffer.write<float>(packet.angles.y); + write_buffer.write<float>(packet.angles.z); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::EntityVelocity& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::EntityVelocity::ID); + write_buffer.write<std::uint64_t>(static_cast<std::uint64_t>(packet.entity)); + write_buffer.write<float>(packet.value.x); + write_buffer.write<float>(packet.value.y); + write_buffer.write<float>(packet.value.z); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::SpawnPlayer& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::SpawnPlayer::ID); + write_buffer.write<std::uint64_t>(static_cast<std::uint64_t>(packet.entity)); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::ChatMessage& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::ChatMessage::ID); + write_buffer.write<std::uint16_t>(packet.type); + write_buffer.write<std::string_view>(packet.sender.substr(0, protocol::MAX_USERNAME)); + write_buffer.write<std::string_view>(packet.message.substr(0, protocol::MAX_CHAT)); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::SetVoxel& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::SetVoxel::ID); + write_buffer.write<std::int64_t>(packet.vpos.x); + write_buffer.write<std::int64_t>(packet.vpos.y); + write_buffer.write<std::int64_t>(packet.vpos.z); + write_buffer.write<std::uint16_t>(packet.voxel); + write_buffer.write<std::uint16_t>(packet.flags); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::RemoveEntity& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::RemoveEntity::ID); + write_buffer.write<std::uint64_t>(static_cast<std::uint64_t>(packet.entity)); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::EntityPlayer& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::EntityPlayer::ID); + write_buffer.write<std::uint64_t>(static_cast<std::uint64_t>(packet.entity)); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::ScoreboardUpdate& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::ScoreboardUpdate::ID); + write_buffer.write<std::uint16_t>(static_cast<std::uint16_t>(packet.names.size())); + for(const std::string& username : packet.names) + write_buffer.write<std::string_view>(username.substr(0, protocol::MAX_USERNAME)); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::RequestChunk& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::RequestChunk::ID); + write_buffer.write<std::int32_t>(packet.cpos.x); + write_buffer.write<std::int32_t>(packet.cpos.y); + write_buffer.write<std::int32_t>(packet.cpos.z); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::GenericSound& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::GenericSound::ID); + write_buffer.write<std::string_view>(packet.sound.substr(0, protocol::MAX_SOUNDNAME)); + write_buffer.write<std::uint8_t>(packet.looping); + write_buffer.write<float>(packet.pitch); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::EntitySound& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::EntitySound::ID); + write_buffer.write<std::uint64_t>(static_cast<std::uint64_t>(packet.entity)); + write_buffer.write<std::string_view>(packet.sound.substr(0, protocol::MAX_SOUNDNAME)); + write_buffer.write<std::uint8_t>(packet.looping); + write_buffer.write<float>(packet.pitch); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::DimensionInfo& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write<std::uint16_t>(protocol::DimensionInfo::ID); + write_buffer.write<std::string_view>(packet.name); + write_buffer.write<float>(packet.gravity); + return write_buffer.to_packet(flags); +} + +void protocol::broadcast(ENetHost* host, ENetPacket* packet) +{ + if(packet) { + enet_host_broadcast(host, protocol::CHANNEL, packet); + } +} + +void protocol::broadcast(ENetHost* host, ENetPacket* packet, ENetPeer* except) +{ + if(packet) { + for(unsigned int i = 0U; i < host->peerCount; ++i) { + if(host->peers[i].state == ENET_PEER_STATE_CONNECTED) { + if(&host->peers[i] != except) { + enet_peer_send(&host->peers[i], protocol::CHANNEL, packet); + } + } + } + } +} + +void protocol::send(ENetPeer* peer, ENetPacket* packet) +{ + if(packet) { + enet_peer_send(peer, protocol::CHANNEL, packet); + } +} + +void protocol::decode(entt::dispatcher& dispatcher, const ENetPacket* packet, ENetPeer* peer) +{ + read_buffer.reset(packet); + + protocol::StatusRequest status_request; + protocol::StatusResponse status_response; + protocol::LoginRequest login_request; + protocol::LoginResponse login_response; + protocol::Disconnect disconnect; + protocol::ChunkVoxels chunk_voxels; + protocol::EntityTransform entity_transform; + protocol::EntityHead entity_head; + protocol::EntityVelocity entity_velocity; + protocol::SpawnPlayer spawn_player; + protocol::ChatMessage chat_message; + protocol::SetVoxel set_voxel; + protocol::RemoveEntity remove_entity; + protocol::EntityPlayer entity_player; + protocol::ScoreboardUpdate scoreboard_update; + protocol::RequestChunk request_chunk; + protocol::GenericSound generic_sound; + protocol::EntitySound entity_sound; + protocol::DimensionInfo dimension_info; + + auto id = read_buffer.read<std::uint16_t>(); + + switch(id) { + case protocol::StatusRequest::ID: + status_request.peer = peer; + status_request.game_version_major = read_buffer.read<std::uint32_t>(); + dispatcher.trigger(status_request); + break; + + case protocol::StatusResponse::ID: + status_response.peer = peer; + status_response.game_version_major = read_buffer.read<std::uint32_t>(); + status_response.max_players = read_buffer.read<std::uint16_t>(); + status_response.num_players = read_buffer.read<std::uint16_t>(); + status_response.motd = read_buffer.read<std::string>(); + status_response.game_version_minor = read_buffer.read<std::uint32_t>(); + status_response.game_version_patch = read_buffer.read<std::uint32_t>(); + dispatcher.trigger(status_response); + break; + + case protocol::LoginRequest::ID: + login_request.peer = peer; + login_request.game_version_major = read_buffer.read<std::uint32_t>(); + login_request.voxel_registry_checksum = read_buffer.read<std::uint64_t>(); + login_request.item_registry_checksum = read_buffer.read<std::uint64_t>(); + login_request.password_hash = read_buffer.read<std::uint64_t>(); + login_request.username = read_buffer.read<std::string>(); + login_request.game_version_minor = read_buffer.read<std::uint32_t>(); + login_request.game_version_patch = read_buffer.read<std::uint32_t>(); + dispatcher.trigger(login_request); + break; + + case protocol::LoginResponse::ID: + login_response.peer = peer; + login_response.client_index = read_buffer.read<std::uint16_t>(); + login_response.client_identity = read_buffer.read<std::uint64_t>(); + login_response.server_tickrate = read_buffer.read<std::uint16_t>(); + dispatcher.trigger(login_response); + break; + + case protocol::Disconnect::ID: + disconnect.peer = peer; + disconnect.reason = read_buffer.read<std::string>(); + dispatcher.trigger(disconnect); + break; + + case protocol::ChunkVoxels::ID: + chunk_voxels.peer = peer; + chunk_voxels.chunk.x = read_buffer.read<std::int32_t>(); + chunk_voxels.chunk.y = read_buffer.read<std::int32_t>(); + chunk_voxels.chunk.z = read_buffer.read<std::int32_t>(); + chunk_voxels.voxels.deserialize(read_buffer); + dispatcher.trigger(chunk_voxels); + break; + + case protocol::EntityTransform::ID: + entity_transform.peer = peer; + entity_transform.entity = static_cast<entt::entity>(read_buffer.read<std::uint64_t>()); + entity_transform.chunk.x = read_buffer.read<std::int32_t>(); + entity_transform.chunk.y = read_buffer.read<std::int32_t>(); + entity_transform.chunk.z = read_buffer.read<std::int32_t>(); + entity_transform.local.x = read_buffer.read<float>(); + entity_transform.local.y = read_buffer.read<float>(); + entity_transform.local.z = read_buffer.read<float>(); + entity_transform.angles.x = read_buffer.read<float>(); + entity_transform.angles.y = read_buffer.read<float>(); + entity_transform.angles.z = read_buffer.read<float>(); + dispatcher.trigger(entity_transform); + break; + + case protocol::EntityHead::ID: + entity_head.peer = peer; + entity_head.entity = static_cast<entt::entity>(read_buffer.read<std::uint64_t>()); + entity_head.angles[0] = read_buffer.read<float>(); + entity_head.angles[1] = read_buffer.read<float>(); + entity_head.angles[2] = read_buffer.read<float>(); + dispatcher.trigger(entity_head); + break; + + case protocol::EntityVelocity::ID: + entity_velocity.peer = peer; + entity_velocity.entity = static_cast<entt::entity>(read_buffer.read<std::uint64_t>()); + entity_velocity.value.x = read_buffer.read<float>(); + entity_velocity.value.y = read_buffer.read<float>(); + entity_velocity.value.z = read_buffer.read<float>(); + dispatcher.trigger(entity_velocity); + break; + + case protocol::SpawnPlayer::ID: + spawn_player.peer = peer; + spawn_player.entity = static_cast<entt::entity>(read_buffer.read<std::uint64_t>()); + dispatcher.trigger(spawn_player); + break; + + case protocol::ChatMessage::ID: + chat_message.peer = peer; + chat_message.type = read_buffer.read<std::uint16_t>(); + chat_message.sender = read_buffer.read<std::string>(); + chat_message.message = read_buffer.read<std::string>(); + dispatcher.trigger(chat_message); + break; + + case protocol::SetVoxel::ID: + set_voxel.peer = peer; + set_voxel.vpos.x = read_buffer.read<std::int64_t>(); + set_voxel.vpos.y = read_buffer.read<std::int64_t>(); + set_voxel.vpos.z = read_buffer.read<std::int64_t>(); + set_voxel.voxel = read_buffer.read<std::uint16_t>(); + set_voxel.flags = read_buffer.read<std::uint16_t>(); + dispatcher.trigger(set_voxel); + break; + + case protocol::RemoveEntity::ID: + remove_entity.peer = peer; + remove_entity.entity = static_cast<entt::entity>(read_buffer.read<std::uint64_t>()); + dispatcher.trigger(remove_entity); + break; + + case protocol::EntityPlayer::ID: + entity_player.peer = peer; + entity_player.entity = static_cast<entt::entity>(read_buffer.read<std::uint64_t>()); + dispatcher.trigger(entity_player); + break; + + case protocol::ScoreboardUpdate::ID: + scoreboard_update.peer = peer; + scoreboard_update.names.resize(read_buffer.read<std::uint16_t>()); + for(std::size_t i = 0; i < scoreboard_update.names.size(); ++i) + scoreboard_update.names[i] = read_buffer.read<std::string>(); + dispatcher.trigger(scoreboard_update); + break; + + case protocol::RequestChunk::ID: + request_chunk.peer = peer; + request_chunk.cpos.x = read_buffer.read<std::uint32_t>(); + request_chunk.cpos.y = read_buffer.read<std::uint32_t>(); + request_chunk.cpos.z = read_buffer.read<std::uint32_t>(); + dispatcher.trigger(request_chunk); + break; + + case protocol::GenericSound::ID: + generic_sound.peer = peer; + generic_sound.sound = read_buffer.read<std::string>(); + generic_sound.looping = read_buffer.read<std::uint8_t>(); + generic_sound.pitch = read_buffer.read<float>(); + dispatcher.trigger(generic_sound); + break; + + case protocol::EntitySound::ID: + entity_sound.peer = peer; + entity_sound.entity = static_cast<entt::entity>(read_buffer.read<std::uint64_t>()); + entity_sound.sound = read_buffer.read<std::string>(); + entity_sound.looping = read_buffer.read<std::uint8_t>(); + entity_sound.pitch = read_buffer.read<float>(); + dispatcher.trigger(entity_sound); + break; + + case protocol::DimensionInfo::ID: + dimension_info.peer = peer; + dimension_info.name = read_buffer.read<std::string>(); + dimension_info.gravity = read_buffer.read<float>(); + dispatcher.trigger(dimension_info); + break; + } +} + +ENetPacket* protocol::utils::make_disconnect(std::string_view reason, enet_uint32 flags) +{ + protocol::Disconnect packet; + packet.reason = reason; + return protocol::encode(packet, flags); +} + +ENetPacket* protocol::utils::make_chat_message(std::string_view message, enet_uint32 flags) +{ + protocol::ChatMessage packet; + packet.type = protocol::ChatMessage::TEXT_MESSAGE; + packet.message = message; + return protocol::encode(packet, flags); +} + +ENetPacket* protocol::utils::make_chunk_voxels(world::Dimension* dimension, entt::entity entity, enet_uint32 flags) +{ + if(auto component = dimension->chunks.try_get<world::ChunkComponent>(entity)) { + protocol::ChunkVoxels packet; + packet.chunk = component->cpos; + packet.voxels = component->chunk->get_voxels(); + return protocol::encode(packet, flags); + } + + return nullptr; +} + +ENetPacket* protocol::utils::make_entity_head(world::Dimension* dimension, entt::entity entity, enet_uint32 flags) +{ + if(auto component = dimension->entities.try_get<entity::Head>(entity)) { + protocol::EntityHead packet; + packet.entity = entity; + packet.angles = component->angles; + return protocol::encode(packet, flags); + } + + return nullptr; +} + +ENetPacket* protocol::utils::make_entity_transform(world::Dimension* dimension, entt::entity entity, enet_uint32 flags) +{ + if(auto component = dimension->entities.try_get<entity::Transform>(entity)) { + protocol::EntityTransform packet; + packet.entity = entity; + packet.chunk = component->chunk; + packet.local = component->local; + packet.angles = component->angles; + return protocol::encode(packet, flags); + } + + return nullptr; +} + +ENetPacket* protocol::utils::make_entity_velocity(world::Dimension* dimension, entt::entity entity, enet_uint32 flags) +{ + if(auto component = dimension->entities.try_get<entity::Velocity>(entity)) { + protocol::EntityVelocity packet; + packet.entity = entity; + packet.value = component->value; + return protocol::encode(packet, flags); + } + + return nullptr; +} + +ENetPacket* protocol::utils::make_entity_player(world::Dimension* dimension, entt::entity entity, enet_uint32 flags) +{ + if(dimension->entities.any_of<entity::Player>(entity)) { + protocol::EntityPlayer packet; + packet.entity = entity; + return protocol::encode(packet, flags); + } + + return nullptr; +} + +ENetPacket* protocol::utils::make_dimension_info(const world::Dimension* dimension) +{ + protocol::DimensionInfo packet; + packet.name = dimension->get_name(); + packet.gravity = dimension->get_gravity(); + return protocol::encode(packet, ENET_PACKET_FLAG_RELIABLE); +} diff --git a/src/game/shared/protocol.hh b/src/game/shared/protocol.hh new file mode 100644 index 0000000..b222342 --- /dev/null +++ b/src/game/shared/protocol.hh @@ -0,0 +1,215 @@ +#pragma once + +#include "shared/world/chunk.hh" + +namespace world +{ +class Dimension; +} // namespace world + +namespace protocol +{ +constexpr static std::size_t MAX_CHAT = 16384; +constexpr static std::size_t MAX_USERNAME = 64; +constexpr static std::size_t MAX_SOUNDNAME = 1024; +constexpr static std::uint16_t TICKRATE = 60; +constexpr static std::uint16_t PORT = 43103; +constexpr static std::uint8_t CHANNEL = 0; +} // namespace protocol + +namespace protocol +{ +template<std::uint16_t packet_id> +struct Base { + constexpr static std::uint16_t ID = packet_id; + virtual ~Base(void) = default; + ENetPeer* peer { nullptr }; +}; +} // namespace protocol + +namespace protocol +{ +struct StatusRequest; +struct StatusResponse; +struct LoginRequest; +struct LoginResponse; +struct Disconnect; +struct ChunkVoxels; +struct EntityTransform; +struct EntityHead; +struct EntityVelocity; +struct SpawnPlayer; +struct ChatMessage; +struct SetVoxel; +struct RemoveEntity; +struct EntityPlayer; +struct ScoreboardUpdate; +struct RequestChunk; +struct GenericSound; +struct EntitySound; +struct DimensionInfo; +} // namespace protocol + +namespace protocol +{ +ENetPacket* encode(const StatusRequest& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const StatusResponse& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const LoginRequest& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const LoginResponse& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const Disconnect& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const ChunkVoxels& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const EntityTransform& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const EntityHead& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const EntityVelocity& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const SpawnPlayer& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const ChatMessage& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const SetVoxel& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const RemoveEntity& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const EntityPlayer& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const ScoreboardUpdate& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const RequestChunk& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const GenericSound& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const EntitySound& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const DimensionInfo& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +} // namespace protocol + +namespace protocol +{ +void broadcast(ENetHost* host, ENetPacket* packet); +void broadcast(ENetHost* host, ENetPacket* packet, ENetPeer* except); +void send(ENetPeer* peer, ENetPacket* packet); +} // namespace protocol + +namespace protocol +{ +void decode(entt::dispatcher& dispatcher, const ENetPacket* packet, ENetPeer* peer); +} // namespace protocol + +namespace protocol::utils +{ +ENetPacket* make_disconnect(std::string_view reason, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* make_chat_message(std::string_view message, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +} // namespace protocol::utils + +namespace protocol::utils +{ +ENetPacket* make_chunk_voxels(world::Dimension* dimension, entt::entity entity, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +} // namespace protocol::utils + +namespace protocol::utils +{ +ENetPacket* make_entity_head(world::Dimension* dimension, entt::entity entity, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* make_entity_transform(world::Dimension* dimension, entt::entity entity, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* make_entity_velocity(world::Dimension* dimension, entt::entity entity, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* make_entity_player(world::Dimension* dimension, entt::entity entity, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* make_dimension_info(const world::Dimension* dimension); +} // namespace protocol::utils + +struct protocol::StatusRequest final : public protocol::Base<0x0000> { + std::uint32_t game_version_major; // renamed from 'version' in v16.x.x +}; + +struct protocol::StatusResponse final : public protocol::Base<0x0001> { + std::uint32_t game_version_major; // renamed from 'version' in v16.x.x + std::uint16_t max_players; + std::uint16_t num_players; + std::string motd; + std::uint32_t game_version_minor { UINT32_MAX }; // added in v16.x.x + std::uint32_t game_version_patch { UINT32_MAX }; +}; + +struct protocol::LoginRequest final : public protocol::Base<0x0002> { + std::uint32_t game_version_major; // renamed from 'version' in v16.x.x + std::uint64_t voxel_registry_checksum; + std::uint64_t item_registry_checksum; + std::uint64_t password_hash; + std::string username; + std::uint32_t game_version_minor; // added in v16.x.x + std::uint32_t game_version_patch; +}; + +struct protocol::LoginResponse final : public protocol::Base<0x0003> { + std::uint16_t client_index; + std::uint64_t client_identity; + std::uint16_t server_tickrate; +}; + +struct protocol::Disconnect final : public protocol::Base<0x0004> { + std::string reason; +}; + +struct protocol::ChunkVoxels final : public protocol::Base<0x0005> { + chunk_pos chunk; + world::VoxelStorage voxels; +}; + +struct protocol::EntityTransform final : public protocol::Base<0x0006> { + entt::entity entity; + chunk_pos chunk; + glm::fvec3 local; + glm::fvec3 angles; +}; + +struct protocol::EntityHead final : public protocol::Base<0x0007> { + entt::entity entity; + glm::fvec3 angles; +}; + +struct protocol::EntityVelocity final : public protocol::Base<0x0008> { + entt::entity entity; + glm::fvec3 value; +}; + +struct protocol::SpawnPlayer final : public protocol::Base<0x0009> { + entt::entity entity; +}; + +struct protocol::ChatMessage final : public protocol::Base<0x000A> { + constexpr static std::uint16_t TEXT_MESSAGE = 0x0000; + constexpr static std::uint16_t PLAYER_JOIN = 0x0001; + constexpr static std::uint16_t PLAYER_LEAVE = 0x0002; + + std::uint16_t type; + std::string sender; + std::string message; +}; + +struct protocol::SetVoxel final : public protocol::Base<0x000B> { + voxel_pos vpos; + voxel_id voxel; + std::uint16_t flags; +}; + +struct protocol::RemoveEntity final : public protocol::Base<0x000C> { + entt::entity entity; +}; + +struct protocol::EntityPlayer final : public protocol::Base<0x000D> { + entt::entity entity; +}; + +struct protocol::ScoreboardUpdate final : public protocol::Base<0x000E> { + std::vector<std::string> names; +}; + +struct protocol::RequestChunk final : public protocol::Base<0x000F> { + chunk_pos cpos; +}; + +struct protocol::GenericSound final : public protocol::Base<0x0010> { + std::string sound; + bool looping; + float pitch; +}; + +struct protocol::EntitySound final : public protocol::Base<0x0011> { + entt::entity entity; + std::string sound; + bool looping; + float pitch; +}; + +struct protocol::DimensionInfo final : public protocol::Base<0x0012> { + std::string name; + float gravity; +}; diff --git a/src/game/shared/splash.cc b/src/game/shared/splash.cc new file mode 100644 index 0000000..4568779 --- /dev/null +++ b/src/game/shared/splash.cc @@ -0,0 +1,67 @@ +#include "shared/pch.hh" + +#include "shared/splash.hh" + +#include "core/io/physfs.hh" + +constexpr static std::string_view SPLASHES_FILENAME_CLIENT = "misc/splashes_client.txt"; +constexpr static std::string_view SPLASHES_FILENAME_SERVER = "misc/splashes_server.txt"; +constexpr static std::size_t SPLASH_SERVER_MAX_LENGTH = 32; + +static std::mt19937_64 splash_random; +static std::vector<std::string> splash_lines; + +static std::string sanitize_line(const std::string& line) +{ + std::string result; + + for(auto chr : line) { + if(chr != '\r' && chr != '\n') { + result.push_back(chr); + } + } + + return result; +} + +static void splash_init_filename(std::string_view filename) +{ + if(auto file = PHYSFS_openRead(std::string(filename).c_str())) { + auto source = std::string(PHYSFS_fileLength(file), char(0x00)); + PHYSFS_readBytes(file, source.data(), source.size()); + PHYSFS_close(file); + + std::string line; + std::istringstream stream(source); + + while(std::getline(stream, line)) + splash_lines.push_back(sanitize_line(line)); + splash_random.seed(std::random_device()()); + } + else { + splash_lines.push_back(std::format("{}: {}", filename, io::physfs_error())); + splash_random.seed(std::random_device()()); + } +} + +void splash::init_client(void) +{ + splash_init_filename(SPLASHES_FILENAME_CLIENT); +} + +void splash::init_server(void) +{ + splash_init_filename(SPLASHES_FILENAME_SERVER); + + // Server browser GUI should be able to display + // these splash messages without text clipping over + for(int i = 0; i < splash_lines.size(); i++) { + splash_lines[i] = splash_lines[i].substr(0, SPLASH_SERVER_MAX_LENGTH); + } +} + +std::string_view splash::get(void) +{ + std::uniform_int_distribution<std::size_t> dist(0, splash_lines.size() - 1); + return splash_lines.at(dist(splash_random)); +} diff --git a/src/game/shared/splash.hh b/src/game/shared/splash.hh new file mode 100644 index 0000000..be80cd6 --- /dev/null +++ b/src/game/shared/splash.hh @@ -0,0 +1,8 @@ +#pragma once + +namespace splash +{ +void init_client(void); +void init_server(void); +std::string_view get(void); +} // namespace splash diff --git a/src/game/shared/types.hh b/src/game/shared/types.hh new file mode 100644 index 0000000..792ed0f --- /dev/null +++ b/src/game/shared/types.hh @@ -0,0 +1,40 @@ +#pragma once + +using item_id = std::uint32_t; +constexpr static item_id NULL_ITEM_ID = UINT32_C(0x00000000); +constexpr static item_id MAX_ITEM_ID = UINT32_C(0xFFFFFFFF); + +using voxel_id = std::uint16_t; +constexpr static voxel_id NULL_VOXEL_ID = UINT16_C(0x0000); +constexpr static voxel_id MAX_VOXEL_ID = UINT16_C(0xFFFF); + +using chunk_pos = glm::vec<3, std::int32_t>; +using local_pos = glm::vec<3, std::int16_t>; +using voxel_pos = glm::vec<3, std::int64_t>; + +using chunk_pos_xz = glm::vec<2, chunk_pos::value_type>; +using local_pos_xz = glm::vec<2, local_pos::value_type>; +using voxel_pos_xz = glm::vec<2, local_pos::value_type>; + +template<> +struct std::hash<chunk_pos> final { + constexpr inline std::size_t operator()(const chunk_pos& cpos) const + { + std::size_t value = 0; + value ^= cpos.x * 73856093; + value ^= cpos.y * 19349663; + value ^= cpos.z * 83492791; + return value; + } +}; + +template<> +struct std::hash<chunk_pos_xz> final { + constexpr inline std::size_t operator()(const chunk_pos_xz& cwpos) const + { + std::size_t value = 0; + value ^= cwpos.x * 73856093; + value ^= cwpos.y * 19349663; + return value; + } +}; diff --git a/src/game/shared/world/CMakeLists.txt b/src/game/shared/world/CMakeLists.txt new file mode 100644 index 0000000..db3f370 --- /dev/null +++ b/src/game/shared/world/CMakeLists.txt @@ -0,0 +1,20 @@ +target_sources(shared PRIVATE + "${CMAKE_CURRENT_LIST_DIR}/chunk_aabb.hh" + "${CMAKE_CURRENT_LIST_DIR}/chunk.cc" + "${CMAKE_CURRENT_LIST_DIR}/chunk.hh" + "${CMAKE_CURRENT_LIST_DIR}/dimension.cc" + "${CMAKE_CURRENT_LIST_DIR}/dimension.hh" + "${CMAKE_CURRENT_LIST_DIR}/feature.cc" + "${CMAKE_CURRENT_LIST_DIR}/feature.hh" + "${CMAKE_CURRENT_LIST_DIR}/item_registry.cc" + "${CMAKE_CURRENT_LIST_DIR}/item_registry.hh" + "${CMAKE_CURRENT_LIST_DIR}/item.cc" + "${CMAKE_CURRENT_LIST_DIR}/item.hh" + "${CMAKE_CURRENT_LIST_DIR}/ray_dda.cc" + "${CMAKE_CURRENT_LIST_DIR}/ray_dda.hh" + "${CMAKE_CURRENT_LIST_DIR}/voxel_registry.cc" + "${CMAKE_CURRENT_LIST_DIR}/voxel_registry.hh" + "${CMAKE_CURRENT_LIST_DIR}/voxel_storage.cc" + "${CMAKE_CURRENT_LIST_DIR}/voxel_storage.hh" + "${CMAKE_CURRENT_LIST_DIR}/voxel.cc" + "${CMAKE_CURRENT_LIST_DIR}/voxel.hh") diff --git a/src/game/shared/world/chunk.cc b/src/game/shared/world/chunk.cc new file mode 100644 index 0000000..e2d60cb --- /dev/null +++ b/src/game/shared/world/chunk.cc @@ -0,0 +1,72 @@ +#include "shared/pch.hh" + +#include "shared/world/chunk.hh" + +#include "shared/world/voxel_registry.hh" + +#include "shared/coord.hh" + +world::Chunk::Chunk(entt::entity entity, Dimension* dimension) +{ + m_entity = entity; + m_dimension = dimension; + m_voxels.fill(NULL_VOXEL_ID); + m_biome = BIOME_VOID; +} + +const world::Voxel* world::Chunk::get_voxel(const local_pos& lpos) const +{ + return get_voxel(coord::to_index(lpos)); +} + +const world::Voxel* world::Chunk::get_voxel(const std::size_t index) const +{ + if(index >= CHUNK_VOLUME) { + return nullptr; + } + + return voxel_registry::find(m_voxels[index]); +} + +void world::Chunk::set_voxel(const Voxel* voxel, const local_pos& lpos) +{ + set_voxel(voxel, coord::to_index(lpos)); +} + +void world::Chunk::set_voxel(const Voxel* voxel, const std::size_t index) +{ + if(index < CHUNK_VOLUME) { + m_voxels[index] = voxel ? voxel->get_id() : NULL_VOXEL_ID; + return; + } +} + +const world::VoxelStorage& world::Chunk::get_voxels(void) const +{ + return m_voxels; +} + +void world::Chunk::set_voxels(const VoxelStorage& voxels) +{ + m_voxels = voxels; +} + +unsigned int world::Chunk::get_biome(void) const +{ + return m_biome; +} + +void world::Chunk::set_biome(unsigned int biome) +{ + m_biome = biome; +} + +entt::entity world::Chunk::get_entity(void) const +{ + return m_entity; +} + +world::Dimension* world::Chunk::get_dimension(void) const +{ + return m_dimension; +} diff --git a/src/game/shared/world/chunk.hh b/src/game/shared/world/chunk.hh new file mode 100644 index 0000000..7518127 --- /dev/null +++ b/src/game/shared/world/chunk.hh @@ -0,0 +1,43 @@ +#pragma once + +#include "shared/world/voxel_storage.hh" + +#include "shared/types.hh" + +constexpr static unsigned int BIOME_VOID = 0U; + +namespace world +{ +class Dimension; +class Voxel; +} // namespace world + +namespace world +{ +class Chunk final { +public: + explicit Chunk(entt::entity entity, Dimension* dimension); + virtual ~Chunk(void) = default; + + const Voxel* get_voxel(const local_pos& lpos) const; + const Voxel* get_voxel(const std::size_t index) const; + + void set_voxel(const Voxel* voxel, const local_pos& lpos); + void set_voxel(const Voxel* voxel, const std::size_t index); + + const VoxelStorage& get_voxels(void) const; + void set_voxels(const VoxelStorage& voxels); + + unsigned int get_biome(void) const; + void set_biome(unsigned int biome); + + entt::entity get_entity(void) const; + Dimension* get_dimension(void) const; + +private: + entt::entity m_entity; + Dimension* m_dimension; + VoxelStorage m_voxels; + unsigned int m_biome; +}; +} // namespace world diff --git a/src/game/shared/world/chunk_aabb.hh b/src/game/shared/world/chunk_aabb.hh new file mode 100644 index 0000000..f07d3e1 --- /dev/null +++ b/src/game/shared/world/chunk_aabb.hh @@ -0,0 +1,10 @@ +#pragma once + +#include "core/math/aabb.hh" + +#include "shared/types.hh" + +namespace world +{ +using ChunkAABB = math::AABB<chunk_pos::value_type>; +} // namespace world diff --git a/src/game/shared/world/dimension.cc b/src/game/shared/world/dimension.cc new file mode 100644 index 0000000..0088753 --- /dev/null +++ b/src/game/shared/world/dimension.cc @@ -0,0 +1,187 @@ +#include "shared/pch.hh" + +#include "shared/world/dimension.hh" + +#include "shared/world/chunk.hh" +#include "shared/world/voxel_registry.hh" + +#include "shared/coord.hh" +#include "shared/globals.hh" + +world::Dimension::Dimension(std::string_view name, float gravity) +{ + m_name = name; + m_gravity = gravity; +} + +world::Dimension::~Dimension(void) +{ + for(const auto it : m_chunkmap) + delete it.second; + entities.clear(); + chunks.clear(); +} + +std::string_view world::Dimension::get_name(void) const +{ + return m_name; +} + +float world::Dimension::get_gravity(void) const +{ + return m_gravity; +} + +world::Chunk* world::Dimension::create_chunk(const chunk_pos& cpos) +{ + auto it = m_chunkmap.find(cpos); + + if(it != m_chunkmap.cend()) { + // Chunk already exists + return it->second; + } + + auto entity = chunks.create(); + auto chunk = new Chunk(entity, this); + + auto& component = chunks.emplace<ChunkComponent>(entity); + component.chunk = chunk; + component.cpos = cpos; + + ChunkCreateEvent event; + event.dimension = this; + event.chunk = chunk; + event.cpos = cpos; + + globals::dispatcher.trigger(event); + + return m_chunkmap.insert_or_assign(cpos, std::move(chunk)).first->second; +} + +world::Chunk* world::Dimension::find_chunk(entt::entity entity) const +{ + if(chunks.valid(entity)) { + return chunks.get<ChunkComponent>(entity).chunk; + } + else { + return nullptr; + } +} + +world::Chunk* world::Dimension::find_chunk(const chunk_pos& cpos) const +{ + auto it = m_chunkmap.find(cpos); + + if(it != m_chunkmap.cend()) { + return it->second; + } + else { + return nullptr; + } +} + +void world::Dimension::remove_chunk(entt::entity entity) +{ + if(chunks.valid(entity)) { + auto& component = chunks.get<ChunkComponent>(entity); + m_chunkmap.erase(component.cpos); + chunks.destroy(entity); + } +} + +void world::Dimension::remove_chunk(const chunk_pos& cpos) +{ + auto it = m_chunkmap.find(cpos); + + if(it != m_chunkmap.cend()) { + chunks.destroy(it->second->get_entity()); + m_chunkmap.erase(it); + } +} + +void world::Dimension::remove_chunk(Chunk* chunk) +{ + if(chunk) { + const auto& component = chunks.get<ChunkComponent>(chunk->get_entity()); + m_chunkmap.erase(component.cpos); + chunks.destroy(chunk->get_entity()); + } +} + +const world::Voxel* world::Dimension::get_voxel(const voxel_pos& vpos) const +{ + auto cpos = coord::to_chunk(vpos); + auto lpos = coord::to_local(vpos); + + if(auto chunk = find_chunk(cpos)) { + return chunk->get_voxel(lpos); + } + + return nullptr; +} + +const world::Voxel* world::Dimension::get_voxel(const chunk_pos& cpos, const local_pos& lpos) const +{ + // This allows accessing get_voxel with negative + // local coordinates that usually would result in an + // out-of-range values; this is useful for per-voxel update logic + return get_voxel(coord::to_voxel(cpos, lpos)); +} + +bool world::Dimension::set_voxel(const Voxel* voxel, const voxel_pos& vpos) +{ + auto cpos = coord::to_chunk(vpos); + auto lpos = coord::to_local(vpos); + + if(auto chunk = find_chunk(cpos)) { + if(auto old_voxel = chunk->get_voxel(lpos)) { + if(old_voxel != voxel) { + // Notify the old voxel that it is + // being replaced with a different voxel + old_voxel->on_remove(this, vpos); + } + } + + chunk->set_voxel(voxel, lpos); + + if(voxel) { + // If we're not placing air, notify the + // new voxel that it has been placed + voxel->on_place(this, vpos); + } + + VoxelSetEvent event; + event.dimension = this; + event.voxel = voxel; + event.cpos = cpos; + event.lpos = lpos; + event.chunk = chunk; + + globals::dispatcher.trigger(event); + + return true; + } + + return false; +} + +bool world::Dimension::set_voxel(const Voxel* voxel, const chunk_pos& cpos, const local_pos& lpos) +{ + // This allows accessing set_voxel with negative + // local coordinates that usually would result in an + // out-of-range values; this is useful for per-voxel update logic + return set_voxel(voxel, coord::to_voxel(cpos, lpos)); +} + +void world::Dimension::init(io::ConfigMap& config) +{ +} + +void world::Dimension::init_late(std::uint64_t global_seed) +{ +} + +bool world::Dimension::generate(const chunk_pos& cpos, VoxelStorage& voxels) +{ + return false; +} diff --git a/src/game/shared/world/dimension.hh b/src/game/shared/world/dimension.hh new file mode 100644 index 0000000..6549bf6 --- /dev/null +++ b/src/game/shared/world/dimension.hh @@ -0,0 +1,101 @@ +#pragma once + +#include "shared/const.hh" +#include "shared/types.hh" + +namespace io +{ +class ConfigMap; +} // namespace io + +namespace world +{ +class Chunk; +class Voxel; +class VoxelStorage; +} // namespace world + +namespace world +{ +using dimension_entropy_map = std::array<std::uint64_t, CHUNK_AREA>; +using dimension_height_map = std::array<voxel_pos::value_type, CHUNK_AREA>; +} // namespace world + +namespace world +{ +class Dimension { +public: + explicit Dimension(std::string_view name, float gravity); + virtual ~Dimension(void); + + std::string_view get_name(void) const; + float get_gravity(void) const; + +public: + Chunk* create_chunk(const chunk_pos& cpos); + Chunk* find_chunk(entt::entity entity) const; + Chunk* find_chunk(const chunk_pos& cpos) const; + + void remove_chunk(entt::entity entity); + void remove_chunk(const chunk_pos& cpos); + void remove_chunk(Chunk* chunk); + +public: + const Voxel* get_voxel(const voxel_pos& vpos) const; + const Voxel* get_voxel(const chunk_pos& cpos, const local_pos& lpos) const; + + bool set_voxel(const Voxel* voxel, const voxel_pos& vpos); + bool set_voxel(const Voxel* voxel, const chunk_pos& cpos, const local_pos& lpos); + +public: + virtual void init(io::ConfigMap& config); + virtual void init_late(std::uint64_t global_seed); + virtual bool generate(const chunk_pos& cpos, VoxelStorage& voxels); + +public: + entt::registry chunks; + entt::registry entities; + +private: + std::string m_name; + emhash8::HashMap<chunk_pos, Chunk*> m_chunkmap; + float m_gravity; +}; +} // namespace world + +namespace world +{ +struct ChunkComponent final { + chunk_pos cpos; + Chunk* chunk; +}; +} // namespace world + +namespace world +{ +struct ChunkCreateEvent final { + Dimension* dimension; + chunk_pos cpos; + Chunk* chunk; +}; + +struct ChunkDestroyEvent final { + Dimension* dimension; + chunk_pos cpos; + Chunk* chunk; +}; + +struct ChunkUpdateEvent final { + Dimension* dimension; + chunk_pos cpos; + Chunk* chunk; +}; + +struct VoxelSetEvent final { + Dimension* dimension; + const Voxel* voxel; + chunk_pos cpos; + local_pos lpos; + Chunk* chunk; +}; +} // namespace world diff --git a/src/game/shared/world/feature.cc b/src/game/shared/world/feature.cc new file mode 100644 index 0000000..2468473 --- /dev/null +++ b/src/game/shared/world/feature.cc @@ -0,0 +1,75 @@ +#include "shared/pch.hh" + +#include "shared/world/feature.hh" + +#include "shared/world/chunk.hh" +#include "shared/world/dimension.hh" +#include "shared/world/voxel.hh" + +#include "shared/coord.hh" + +void world::Feature::place(const voxel_pos& vpos, Dimension* dimension) const +{ + for(const auto [rpos, voxel, overwrite] : (*this)) { + auto it_vpos = vpos + rpos; + auto it_cpos = coord::to_chunk(it_vpos); + + if(auto chunk = dimension->create_chunk(it_cpos)) { + auto it_lpos = coord::to_local(it_vpos); + auto it_index = coord::to_index(it_lpos); + + if(chunk->get_voxel(it_index) && !overwrite) { + // There is something in the way + // and the called intentionally requested + // we do not force feature to overwrite voxels + continue; + } + + chunk->set_voxel(voxel, it_index); + } + } +} + +void world::Feature::place(const voxel_pos& vpos, const chunk_pos& cpos, Chunk& chunk) const +{ + for(const auto [rpos, voxel, overwrite] : (*this)) { + auto it_vpos = vpos + rpos; + auto it_cpos = coord::to_chunk(it_vpos); + + if(it_cpos == cpos) { + auto it_lpos = coord::to_local(it_vpos); + auto it_index = coord::to_index(it_lpos); + + if(chunk.get_voxel(it_index) && !overwrite) { + // There is something in the way + // and the called intentionally requested + // we do not force feature to overwrite voxels + continue; + } + + chunk.set_voxel(voxel, it_index); + } + } +} + +void world::Feature::place(const voxel_pos& vpos, const chunk_pos& cpos, VoxelStorage& voxels) const +{ + for(const auto [rpos, voxel, overwrite] : (*this)) { + auto it_vpos = vpos + rpos; + auto it_cpos = coord::to_chunk(it_vpos); + + if(it_cpos == cpos) { + auto it_lpos = coord::to_local(it_vpos); + auto it_index = coord::to_index(it_lpos); + + if(voxels[it_index] && !overwrite) { + // There is something in the way + // and the called intentionally requested + // we do not force feature to overwrite voxels + continue; + } + + voxels[it_index] = voxel ? voxel->get_id() : NULL_VOXEL_ID; + } + } +} diff --git a/src/game/shared/world/feature.hh b/src/game/shared/world/feature.hh new file mode 100644 index 0000000..0d28b20 --- /dev/null +++ b/src/game/shared/world/feature.hh @@ -0,0 +1,25 @@ +#pragma once + +#include "shared/types.hh" + +namespace world +{ +class Chunk; +class Dimension; +class Voxel; +class VoxelStorage; +} // namespace world + +namespace world +{ +class Feature final : public std::vector<std::tuple<voxel_pos, const Voxel*, bool>> { +public: + Feature(void) = default; + virtual ~Feature(void) = default; + +public: + void place(const voxel_pos& vpos, Dimension* dimension) const; + void place(const voxel_pos& vpos, const chunk_pos& cpos, Chunk& chunk) const; + void place(const voxel_pos& vpos, const chunk_pos& cpos, VoxelStorage& voxels) const; +}; +} // namespace world diff --git a/src/game/shared/world/item.cc b/src/game/shared/world/item.cc new file mode 100644 index 0000000..5e60609 --- /dev/null +++ b/src/game/shared/world/item.cc @@ -0,0 +1,55 @@ +#include "shared/pch.hh" + +#include "shared/world/item.hh" + +#include "core/math/crc64.hh" + +#include "shared/world/voxel.hh" + +world::Item::Item(const Item& source, item_id id) noexcept : Item(source) +{ + m_id = id; +} + +void world::Item::set_cached_texture(resource_ptr<TextureGUI> texture) const noexcept +{ + m_cached_texture = std::move(texture); +} + +std::uint64_t world::Item::get_checksum(std::uint64_t combine) const +{ + combine = math::crc64(m_name.data(), m_name.size(), combine); + combine = math::crc64(m_texture.data(), m_texture.size(), combine); + + std::uint32_t id = m_place_voxel ? m_place_voxel->get_id() : NULL_VOXEL_ID; + combine = math::crc64(&id, sizeof(id), combine); + + return combine; +} + +world::ItemBuilder::ItemBuilder(std::string_view name) +{ + set_name(name); +} + +void world::ItemBuilder::set_name(std::string_view name) +{ + assert(name.size()); + + m_name = name; +} + +void world::ItemBuilder::set_texture(std::string_view texture) +{ + m_texture = texture; +} + +void world::ItemBuilder::set_place_voxel(const Voxel* place_voxel) +{ + m_place_voxel = place_voxel; +} + +std::unique_ptr<world::Item> world::ItemBuilder::build(item_id id) const +{ + return std::make_unique<Item>(*this, id); +} diff --git a/src/game/shared/world/item.hh b/src/game/shared/world/item.hh new file mode 100644 index 0000000..ffa7f5c --- /dev/null +++ b/src/game/shared/world/item.hh @@ -0,0 +1,84 @@ +#pragma once + +#include "core/resource/resource.hh" + +#include "shared/types.hh" + +// This resource is only defined client-side and +// resource_ptr<TextureGUI> should remain set to null +// anywhere else in the shared and server code +struct TextureGUI; + +namespace world +{ +class Voxel; +} // namespace world + +namespace world +{ +class Item { +public: + Item(void) = default; + explicit Item(const Item& source, item_id id) noexcept; + + constexpr std::string_view get_name(void) const noexcept; + constexpr item_id get_id(void) const noexcept; + + constexpr std::string_view get_texture(void) const noexcept; + constexpr const Voxel* get_place_voxel(void) const noexcept; + + constexpr resource_ptr<TextureGUI>& get_cached_texture(void) const noexcept; + void set_cached_texture(resource_ptr<TextureGUI> texture) const noexcept; + + std::uint64_t get_checksum(std::uint64_t combine = 0U) const; + +protected: + std::string m_name; + item_id m_id { NULL_ITEM_ID }; + + std::string m_texture; + const Voxel* m_place_voxel { nullptr }; + + mutable resource_ptr<TextureGUI> m_cached_texture; // Client-side only +}; +} // namespace world + +namespace world +{ +class ItemBuilder final : public Item { +public: + explicit ItemBuilder(std::string_view name); + + void set_name(std::string_view name); + + void set_texture(std::string_view texture); + void set_place_voxel(const Voxel* place_voxel); + + std::unique_ptr<Item> build(item_id id) const; +}; +} // namespace world + +constexpr std::string_view world::Item::get_name(void) const noexcept +{ + return m_name; +} + +constexpr item_id world::Item::get_id(void) const noexcept +{ + return m_id; +} + +constexpr std::string_view world::Item::get_texture(void) const noexcept +{ + return m_texture; +} + +constexpr const world::Voxel* world::Item::get_place_voxel(void) const noexcept +{ + return m_place_voxel; +} + +constexpr resource_ptr<TextureGUI>& world::Item::get_cached_texture(void) const noexcept +{ + return m_cached_texture; +} diff --git a/src/game/shared/world/item_registry.cc b/src/game/shared/world/item_registry.cc new file mode 100644 index 0000000..d89abe9 --- /dev/null +++ b/src/game/shared/world/item_registry.cc @@ -0,0 +1,68 @@ +#include "shared/pch.hh" + +#include "shared/world/item_registry.hh" + +#include "core/math/crc64.hh" + +#include "shared/world/voxel_registry.hh" + +static std::uint64_t registry_checksum = 0U; +std::unordered_map<std::string, item_id> world::item_registry::names = {}; +std::vector<std::unique_ptr<world::Item>> world::item_registry::items = {}; + +static void recalculate_checksum(void) +{ + registry_checksum = 0U; + + for(const auto& item : world::item_registry::items) { + registry_checksum = item->get_checksum(registry_checksum); + } +} + +world::Item* world::item_registry::register_item(const ItemBuilder& builder) +{ + assert(builder.get_name().size()); + assert(nullptr == find(builder.get_name())); + + const auto id = static_cast<item_id>(1 + items.size()); + + std::unique_ptr<Item> item(builder.build(id)); + names.emplace(std::string(builder.get_name()), id); + items.push_back(std::move(item)); + + recalculate_checksum(); + + return items.back().get(); +} + +world::Item* world::item_registry::find(std::string_view name) +{ + const auto it = names.find(std::string(name)); + + if(it == names.end()) { + return nullptr; + } + + return items[it->second - 1].get(); +} + +world::Item* world::item_registry::find(const item_id item) +{ + if(item == NULL_ITEM_ID || item > items.size()) { + return nullptr; + } + + return items[item - 1].get(); +} + +void world::item_registry::purge(void) +{ + registry_checksum = 0U; + items.clear(); + names.clear(); +} + +std::uint64_t world::item_registry::get_checksum(void) +{ + return registry_checksum; +} diff --git a/src/game/shared/world/item_registry.hh b/src/game/shared/world/item_registry.hh new file mode 100644 index 0000000..a841a2d --- /dev/null +++ b/src/game/shared/world/item_registry.hh @@ -0,0 +1,26 @@ +#pragma once + +#include "shared/world/item.hh" + +namespace world::item_registry +{ +extern std::unordered_map<std::string, item_id> names; +extern std::vector<std::unique_ptr<Item>> items; +} // namespace world::item_registry + +namespace world::item_registry +{ +Item* register_item(const ItemBuilder& builder); +Item* find(std::string_view name); +Item* find(const item_id item); +} // namespace world::item_registry + +namespace world::item_registry +{ +void purge(void); +} // namespace world::item_registry + +namespace world::item_registry +{ +std::uint64_t get_checksum(void); +} // namespace world::item_registry diff --git a/src/game/shared/world/ray_dda.cc b/src/game/shared/world/ray_dda.cc new file mode 100644 index 0000000..d6383b9 --- /dev/null +++ b/src/game/shared/world/ray_dda.cc @@ -0,0 +1,107 @@ +#include "shared/pch.hh" + +#include "shared/world/ray_dda.hh" + +#include "shared/world/dimension.hh" + +#include "shared/coord.hh" + +world::RayDDA::RayDDA(const world::Dimension* dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, + const glm::fvec3& direction) +{ + reset(dimension, start_chunk, start_fpos, direction); +} + +world::RayDDA::RayDDA(const world::Dimension& dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, + const glm::fvec3& direction) +{ + reset(dimension, start_chunk, start_fpos, direction); +} + +void world::RayDDA::reset(const world::Dimension* dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, + const glm::fvec3& direction) +{ + this->dimension = dimension; + this->start_chunk = start_chunk; + this->start_fpos = start_fpos; + this->direction = direction; + + this->delta_dist.x = direction.x ? glm::abs(1.0f / direction.x) : std::numeric_limits<float>::max(); + this->delta_dist.y = direction.y ? glm::abs(1.0f / direction.y) : std::numeric_limits<float>::max(); + this->delta_dist.z = direction.z ? glm::abs(1.0f / direction.z) : std::numeric_limits<float>::max(); + + this->distance = 0.0f; + this->vpos = coord::to_voxel(start_chunk, start_fpos); + this->vnormal = voxel_pos(0, 0, 0); + + // Need this for initial direction calculations + auto lpos = coord::to_local(start_fpos); + + if(direction.x < 0.0f) { + this->side_dist.x = this->delta_dist.x * (start_fpos.x - lpos.x); + this->vstep.x = voxel_pos::value_type(-1); + } + else { + this->side_dist.x = this->delta_dist.x * (lpos.x + 1.0f - start_fpos.x); + this->vstep.x = voxel_pos::value_type(+1); + } + + if(direction.y < 0.0f) { + this->side_dist.y = this->delta_dist.y * (start_fpos.y - lpos.y); + this->vstep.y = voxel_pos::value_type(-1); + } + else { + this->side_dist.y = this->delta_dist.y * (lpos.y + 1.0f - start_fpos.y); + this->vstep.y = voxel_pos::value_type(+1); + } + + if(direction.z < 0.0f) { + this->side_dist.z = this->delta_dist.z * (start_fpos.z - lpos.z); + this->vstep.z = voxel_pos::value_type(-1); + } + else { + this->side_dist.z = this->delta_dist.z * (lpos.z + 1.0f - start_fpos.z); + this->vstep.z = voxel_pos::value_type(+1); + } +} + +void world::RayDDA::reset(const world::Dimension& dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, + const glm::fvec3& direction) +{ + reset(&dimension, start_chunk, start_fpos, direction); +} + +const world::Voxel* world::RayDDA::step(void) +{ + if(side_dist.x < side_dist.z) { + if(side_dist.x < side_dist.y) { + vnormal = voxel_pos(-vstep.x, 0, 0); + distance = side_dist.x; + side_dist.x += delta_dist.x; + vpos.x += vstep.x; + } + else { + vnormal = voxel_pos(0, -vstep.y, 0); + distance = side_dist.y; + side_dist.y += delta_dist.y; + vpos.y += vstep.y; + } + } + else { + if(side_dist.z < side_dist.y) { + vnormal = voxel_pos(0, 0, -vstep.z); + distance = side_dist.z; + side_dist.z += delta_dist.z; + vpos.z += vstep.z; + } + else { + vnormal = voxel_pos(0, -vstep.y, 0); + distance = side_dist.y; + side_dist.y += delta_dist.y; + vpos.y += vstep.y; + } + } + + // This is slower than I want it to be + return dimension->get_voxel(vpos); +} diff --git a/src/game/shared/world/ray_dda.hh b/src/game/shared/world/ray_dda.hh new file mode 100644 index 0000000..0f548ba --- /dev/null +++ b/src/game/shared/world/ray_dda.hh @@ -0,0 +1,38 @@ +#pragma once + +#include "shared/types.hh" + +namespace world +{ +class Dimension; +class Voxel; +} // namespace world + +namespace world +{ +class RayDDA final { +public: + RayDDA(void) = default; + explicit RayDDA(const Dimension* dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction); + explicit RayDDA(const Dimension& dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction); + + void reset(const Dimension* dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction); + void reset(const Dimension& dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction); + + const Voxel* step(void); + +public: + const Dimension* dimension; + chunk_pos start_chunk; + glm::fvec3 start_fpos; + glm::fvec3 direction; + + glm::fvec3 delta_dist; + glm::fvec3 side_dist; + voxel_pos vstep; + + double distance; + voxel_pos vnormal; + voxel_pos vpos; +}; +} // namespace world diff --git a/src/game/shared/world/voxel.cc b/src/game/shared/world/voxel.cc new file mode 100644 index 0000000..21fe62c --- /dev/null +++ b/src/game/shared/world/voxel.cc @@ -0,0 +1,149 @@ +#include "shared/pch.hh" + +#include "shared/world/voxel.hh" + +#include "core/math/crc64.hh" + +#include "shared/world/dimension.hh" + +world::Voxel::Voxel(const Voxel& source, voxel_id id) noexcept : Voxel(source) +{ + m_id = id; +} + +void world::Voxel::on_place(Dimension* dimension, const voxel_pos& vpos) const +{ + if(m_on_place) { + m_on_place(dimension, vpos); + } +} + +void world::Voxel::on_remove(Dimension* dimension, const voxel_pos& vpos) const +{ + if(m_on_remove) { + m_on_remove(dimension, vpos); + } +} + +void world::Voxel::on_tick(Dimension* dimension, const voxel_pos& vpos) const +{ + if(m_on_tick) { + m_on_tick(dimension, vpos); + } +} + +std::size_t world::Voxel::get_random_texture_index(VoxelFace face, const voxel_pos& vpos) const +{ + const auto& textures = get_face_textures(face); + + assert(textures.size()); + + std::uint64_t hash = 0U; + hash = math::crc64(&vpos.x, sizeof(vpos.x), hash); + hash = math::crc64(&vpos.y, sizeof(vpos.y), hash); + hash = math::crc64(&vpos.z, sizeof(vpos.z), hash); + + return static_cast<std::size_t>(hash % textures.size()); +} + +void world::Voxel::set_face_cache(VoxelFace face, std::size_t offset, std::size_t plane) +{ + assert(face < m_cached_face_offsets.size()); + assert(face < m_cached_face_planes.size()); + + m_cached_face_offsets[face] = offset; + m_cached_face_planes[face] = plane; +} + +std::uint64_t world::Voxel::get_checksum(std::uint64_t combine) const +{ + combine = math::crc64(m_name.data(), m_name.size(), combine); + combine += static_cast<std::uint64_t>(m_shape); + combine += static_cast<std::uint64_t>(m_render_mode); + return combine; +} + +world::VoxelBuilder::VoxelBuilder(std::string_view name) +{ + set_name(name); +} + +void world::VoxelBuilder::set_on_place(VoxelOnPlaceFunc func) noexcept +{ + m_on_place = std::move(func); +} + +void world::VoxelBuilder::set_on_remove(VoxelOnRemoveFunc func) noexcept +{ + m_on_remove = std::move(func); +} + +void world::VoxelBuilder::set_on_tick(VoxelOnTickFunc func) noexcept +{ + m_on_tick = std::move(func); +} + +void world::VoxelBuilder::set_name(std::string_view name) noexcept +{ + assert(name.size()); + + m_name = name; +} + +void world::VoxelBuilder::set_render_mode(VoxelRender mode) noexcept +{ + m_render_mode = mode; +} + +void world::VoxelBuilder::set_shape(VoxelShape shape) noexcept +{ + m_shape = shape; +} + +void world::VoxelBuilder::set_animated(bool animated) noexcept +{ + m_animated = animated; +} + +void world::VoxelBuilder::set_touch_type(VoxelTouch type) noexcept +{ + m_touch_type = type; +} + +void world::VoxelBuilder::set_touch_values(const glm::fvec3& values) noexcept +{ + m_touch_values = values; +} + +void world::VoxelBuilder::set_surface_material(VoxelMaterial material) noexcept +{ + m_surface_material = material; +} + +void world::VoxelBuilder::set_collision(const math::AABBf& box) noexcept +{ + m_collision = box; +} + +void world::VoxelBuilder::add_default_texture(std::string_view path) +{ + assert(path.size()); + + m_default_textures.emplace_back(path); +} + +void world::VoxelBuilder::add_face_texture(VoxelFace face, std::string_view path) +{ + assert(face < m_face_textures.size()); + assert(path.size()); + + m_face_textures[face].emplace_back(path); +} + +std::unique_ptr<world::Voxel> world::VoxelBuilder::build(voxel_id id) const +{ + assert(m_name.size()); + assert(id); + + return std::make_unique<Voxel>(*this, id); +} diff --git a/src/game/shared/world/voxel.hh b/src/game/shared/world/voxel.hh new file mode 100644 index 0000000..6013962 --- /dev/null +++ b/src/game/shared/world/voxel.hh @@ -0,0 +1,286 @@ +#pragma once + +#include "core/math/aabb.hh" + +#include "shared/types.hh" + +namespace world +{ +class Dimension; +} // namespace world + +namespace world +{ +enum VoxelRender : unsigned int { + VRENDER_NONE = 0U, ///< The voxel is not rendered at all + VRENDER_OPAQUE, ///< The voxel is fully opaque + VRENDER_BLEND, ///< The voxel is blended (e.g. water, glass) +}; + +enum VoxelShape : unsigned int { + VSHAPE_CUBE = 0U, ///< Full cube shape + VSHAPE_CROSS, ///< TODO: Cross shape + VSHAPE_MODEL, ///< TODO: Custom model shape +}; + +enum VoxelFace : unsigned int { + VFACE_NORTH = 0U, ///< Positive Z face + VFACE_SOUTH, ///< Negative Z face + VFACE_EAST, ///< Positive X face + VFACE_WEST, ///< Negative X face + VFACE_TOP, ///< Positive Y face + VFACE_BOTTOM, ///< Negative Y face + VFACE_CROSS_NWSE, ///< Diagonal cross face northwest-southeast + VFACE_CROSS_NESW, ///< Diagonal cross face northeast-southwest + VFACE_COUNT +}; + +enum VoxelTouch : unsigned int { + VTOUCH_NONE = 0xFFFFU, + VTOUCH_SOLID = 0U, ///< The entity is stopped in its tracks + VTOUCH_BOUNCE, ///< The entity bounces back with some energy loss + VTOUCH_SINK, ///< The entity phases/sinks through the voxel +}; + +enum VoxelMaterial : unsigned int { + VMAT_UNKNOWN = 0xFFFFU, + VMAT_DEFAULT = 0U, + VMAT_STONE, + VMAT_DIRT, + VMAT_GLASS, + VMAT_GRASS, + VMAT_GRAVEL, + VMAT_METAL, + VMAT_SAND, + VMAT_WOOD, + VMAT_SLOSH, + VMAT_COUNT +}; + +enum VoxelVisBits : unsigned int { + VVIS_NORTH = 1U << VFACE_NORTH, ///< Positive Z + VVIS_SOUTH = 1U << VFACE_SOUTH, ///< Negative Z + VVIS_EAST = 1U << VFACE_EAST, ///< Positive X + VVIS_WEST = 1U << VFACE_WEST, ///< Negative X + VVIS_UP = 1U << VFACE_TOP, ///< Positive Y + VVIS_DOWN = 1U << VFACE_BOTTOM, ///< Negative Y +}; +} // namespace world + +namespace world +{ +using VoxelOnPlaceFunc = std::function<void(Dimension*, const voxel_pos&)>; +using VoxelOnRemoveFunc = std::function<void(Dimension*, const voxel_pos&)>; +using VoxelOnTickFunc = std::function<void(Dimension*, const voxel_pos&)>; +} // namespace world + +namespace world +{ +class Voxel { +public: + Voxel(void) = default; + explicit Voxel(const Voxel& source, voxel_id id) noexcept; + + void on_place(Dimension* dimension, const voxel_pos& vpos) const; + void on_remove(Dimension* dimension, const voxel_pos& vpos) const; + void on_tick(Dimension* dimension, const voxel_pos& vpos) const; + + constexpr std::string_view get_name(void) const noexcept; + constexpr voxel_id get_id(void) const noexcept; + + constexpr VoxelRender get_render_mode(void) const noexcept; + constexpr VoxelShape get_shape(void) const noexcept; + constexpr bool is_animated(void) const noexcept; + + constexpr VoxelTouch get_touch_type(void) const noexcept; + constexpr const glm::fvec3& get_touch_values(void) const noexcept; + constexpr VoxelMaterial get_surface_material(void) const noexcept; + + constexpr const math::AABBf& get_collision(void) const noexcept; + + constexpr const std::vector<std::string>& get_default_textures(void) const noexcept; + constexpr const std::vector<std::string>& get_face_textures(VoxelFace face) const noexcept; + constexpr std::size_t get_cached_face_offset(VoxelFace face) const noexcept; + constexpr std::size_t get_cached_face_plane(VoxelFace face) const noexcept; + + template<VoxelRender RenderMode> + constexpr bool is_render_mode(void) const noexcept; + template<VoxelShape Shape> + constexpr bool is_shape(void) const noexcept; + template<VoxelTouch TouchType> + constexpr bool is_touch_type(void) const noexcept; + template<VoxelMaterial Material> + constexpr bool is_surface_material(void) const noexcept; + + /// Non-model voxel shapes support texture variation based on the + /// voxel position on the world; this method handles the math behind this + /// @param face The face of the voxel to get the texture index for + /// @param vpos The absolute voxel position to get the texture index for + /// @return The index of the texture to use for the given face at the given position + /// @remarks On client-side: plane[get_cached_face_plane][get_cached_face_offset + thisFunctionResult] + std::size_t get_random_texture_index(VoxelFace face, const voxel_pos& vpos) const; + + /// Assign cached plane index and plane offset for a given face + /// @param face The face to assign the cache for + /// @param offset The offset to assign to the face + /// @param plane The plane index to assign to the face + void set_face_cache(VoxelFace face, std::size_t offset, std::size_t plane); + + /// Calculate a checksum for the voxel's properties + /// @param combine An optional initial checksum to combine with + /// @return The calculated checksum + std::uint64_t get_checksum(std::uint64_t combine = 0U) const; + +protected: + std::string m_name; + voxel_id m_id { NULL_VOXEL_ID }; + + VoxelRender m_render_mode { VRENDER_OPAQUE }; + VoxelShape m_shape { VSHAPE_CUBE }; + bool m_animated { false }; + + VoxelTouch m_touch_type { VTOUCH_SOLID }; + glm::fvec3 m_touch_values { 0.0f }; + VoxelMaterial m_surface_material { VMAT_DEFAULT }; + + math::AABBf m_collision { { 0.0f, 0.0f, 0.0f }, { 1.0f, 1.0f, 1.0f } }; + + std::vector<std::string> m_default_textures; + std::array<std::vector<std::string>, VFACE_COUNT> m_face_textures; + std::array<std::size_t, VFACE_COUNT> m_cached_face_offsets; + std::array<std::size_t, VFACE_COUNT> m_cached_face_planes; + + VoxelOnPlaceFunc m_on_place; + VoxelOnRemoveFunc m_on_remove; + VoxelOnTickFunc m_on_tick; +}; +} // namespace world + +namespace world +{ +class VoxelBuilder final : public Voxel { +public: + VoxelBuilder(void) = default; + explicit VoxelBuilder(std::string_view name); + + void set_on_place(VoxelOnPlaceFunc func) noexcept; + void set_on_remove(VoxelOnRemoveFunc func) noexcept; + void set_on_tick(VoxelOnTickFunc func) noexcept; + + void set_name(std::string_view name) noexcept; + + void set_render_mode(VoxelRender mode) noexcept; + void set_shape(VoxelShape shape) noexcept; + void set_animated(bool animated) noexcept; + + void set_touch_type(VoxelTouch type) noexcept; + void set_touch_values(const glm::fvec3& values) noexcept; + void set_surface_material(VoxelMaterial material) noexcept; + + void set_collision(const math::AABBf& box) noexcept; + + void add_default_texture(std::string_view path); + void add_face_texture(VoxelFace face, std::string_view path); + + std::unique_ptr<Voxel> build(voxel_id id) const; +}; +} // namespace world + +constexpr std::string_view world::Voxel::get_name(void) const noexcept +{ + return m_name; +} + +constexpr voxel_id world::Voxel::get_id(void) const noexcept +{ + return m_id; +} + +constexpr world::VoxelRender world::Voxel::get_render_mode(void) const noexcept +{ + return m_render_mode; +} + +constexpr world::VoxelShape world::Voxel::get_shape(void) const noexcept +{ + return m_shape; +} + +constexpr bool world::Voxel::is_animated(void) const noexcept +{ + return m_animated; +} + +constexpr world::VoxelTouch world::Voxel::get_touch_type(void) const noexcept +{ + return m_touch_type; +} + +constexpr const glm::fvec3& world::Voxel::get_touch_values(void) const noexcept +{ + return m_touch_values; +} + +constexpr world::VoxelMaterial world::Voxel::get_surface_material(void) const noexcept +{ + return m_surface_material; +} + +constexpr const math::AABBf& world::Voxel::get_collision(void) const noexcept +{ + return m_collision; +} + +constexpr const std::vector<std::string>& world::Voxel::get_default_textures(void) const noexcept +{ + return m_default_textures; +} + +constexpr const std::vector<std::string>& world::Voxel::get_face_textures(VoxelFace face) const noexcept +{ + assert(face <= m_face_textures.size()); + + if(m_face_textures[face].empty()) { + return m_default_textures; + } + + return m_face_textures[face]; +} + +constexpr std::size_t world::Voxel::get_cached_face_offset(VoxelFace face) const noexcept +{ + assert(face <= m_cached_face_offsets.size()); + + return m_cached_face_offsets[face]; +} + +constexpr std::size_t world::Voxel::get_cached_face_plane(VoxelFace face) const noexcept +{ + assert(face <= m_cached_face_planes.size()); + + return m_cached_face_planes[face]; +} + +template<world::VoxelRender RenderMode> +constexpr bool world::Voxel::is_render_mode(void) const noexcept +{ + return m_render_mode == RenderMode; +} + +template<world::VoxelShape Shape> +constexpr bool world::Voxel::is_shape(void) const noexcept +{ + return m_shape == Shape; +} + +template<world::VoxelTouch TouchType> +constexpr bool world::Voxel::is_touch_type(void) const noexcept +{ + return m_touch_type == TouchType; +} + +template<world::VoxelMaterial Material> +constexpr bool world::Voxel::is_surface_material(void) const noexcept +{ + return m_surface_material == Material; +} diff --git a/src/game/shared/world/voxel_registry.cc b/src/game/shared/world/voxel_registry.cc new file mode 100644 index 0000000..fae83fa --- /dev/null +++ b/src/game/shared/world/voxel_registry.cc @@ -0,0 +1,64 @@ +#include "shared/pch.hh" + +#include "shared/world/voxel_registry.hh" + +static std::uint64_t registry_checksum = 0U; +emhash8::HashMap<std::string, voxel_id> world::voxel_registry::names; +std::vector<std::unique_ptr<world::Voxel>> world::voxel_registry::voxels; + +static void recalculate_checksum(void) +{ + registry_checksum = 0U; + + for(const auto& voxel : world::voxel_registry::voxels) { + registry_checksum = voxel->get_checksum(registry_checksum); + } +} + +world::Voxel* world::voxel_registry::register_voxel(const VoxelBuilder& builder) +{ + assert(builder.get_name().size()); + assert(nullptr == find(builder.get_name())); + + const auto id = static_cast<voxel_id>(1 + voxels.size()); + + std::unique_ptr<Voxel> voxel(builder.build(id)); + names.emplace(std::string(builder.get_name()), id); + voxels.push_back(std::move(voxel)); + + recalculate_checksum(); + + return voxels.back().get(); +} + +world::Voxel* world::voxel_registry::find(std::string_view name) +{ + const auto it = names.find(std::string(name)); + + if(it == names.end()) { + return nullptr; + } + + return voxels[it->second - 1].get(); +} + +world::Voxel* world::voxel_registry::find(voxel_id id) +{ + if(id == NULL_VOXEL_ID || id > voxels.size()) { + return nullptr; + } + + return voxels[id - 1].get(); +} + +void world::voxel_registry::purge(void) +{ + registry_checksum = 0U; + voxels.clear(); + names.clear(); +} + +std::uint64_t world::voxel_registry::get_checksum(void) +{ + return registry_checksum; +} diff --git a/src/game/shared/world/voxel_registry.hh b/src/game/shared/world/voxel_registry.hh new file mode 100644 index 0000000..a1e0eee --- /dev/null +++ b/src/game/shared/world/voxel_registry.hh @@ -0,0 +1,26 @@ +#pragma once + +#include "shared/world/voxel.hh" + +namespace world::voxel_registry +{ +extern emhash8::HashMap<std::string, voxel_id> names; +extern std::vector<std::unique_ptr<Voxel>> voxels; +} // namespace world::voxel_registry + +namespace world::voxel_registry +{ +Voxel* register_voxel(const VoxelBuilder& builder); +Voxel* find(std::string_view name); +Voxel* find(voxel_id id); +} // namespace world::voxel_registry + +namespace world::voxel_registry +{ +void purge(void); +} // namespace world::voxel_registry + +namespace world::voxel_registry +{ +std::uint64_t get_checksum(void); +} // namespace world::voxel_registry diff --git a/src/game/shared/world/voxel_storage.cc b/src/game/shared/world/voxel_storage.cc new file mode 100644 index 0000000..68e261c --- /dev/null +++ b/src/game/shared/world/voxel_storage.cc @@ -0,0 +1,48 @@ +#include "shared/pch.hh" + +#include "shared/world/voxel_storage.hh" + +#include "core/io/buffer.hh" + +void world::VoxelStorage::serialize(io::WriteBuffer& buffer) const +{ + auto bound = mz_compressBound(sizeof(VoxelStorage)); + auto zdata = std::vector<unsigned char>(bound); + + VoxelStorage net_storage; + + for(std::size_t i = 0; i < CHUNK_VOLUME; ++i) { + // Convert voxel indices into network byte order; + // We're going to compress them but we still want + // the order to be consistent across all the platforms + net_storage[i] = ENET_HOST_TO_NET_16(at(i)); + } + + mz_compress(zdata.data(), &bound, reinterpret_cast<unsigned char*>(net_storage.data()), sizeof(VoxelStorage)); + + buffer.write<std::uint64_t>(bound); + + // Write all the compressed data into the buffer + for(std::size_t i = 0; i < bound; buffer.write<std::uint8_t>(zdata[i++])) { + // empty + } +} + +void world::VoxelStorage::deserialize(io::ReadBuffer& buffer) +{ + auto size = static_cast<mz_ulong>(sizeof(VoxelStorage)); + auto bound = static_cast<mz_ulong>(buffer.read<std::uint64_t>()); + auto zdata = std::vector<unsigned char>(bound); + + // Read all the compressed data from the buffer + for(std::size_t i = 0; i < bound; zdata[i++] = buffer.read<std::uint8_t>()) { + // empty + } + + mz_uncompress(reinterpret_cast<unsigned char*>(data()), &size, zdata.data(), bound); + + for(std::size_t i = 0; i < CHUNK_VOLUME; ++i) { + // Convert voxel indices back into the host byte order + at(i) = ENET_NET_TO_HOST_16(at(i)); + } +} diff --git a/src/game/shared/world/voxel_storage.hh b/src/game/shared/world/voxel_storage.hh new file mode 100644 index 0000000..ac7f03d --- /dev/null +++ b/src/game/shared/world/voxel_storage.hh @@ -0,0 +1,20 @@ +#pragma once + +#include "shared/const.hh" +#include "shared/types.hh" + +namespace io +{ +class ReadBuffer; +class WriteBuffer; +} // namespace io + +namespace world +{ +class VoxelStorage final : public std::array<voxel_id, CHUNK_VOLUME> { +public: + using std::array<voxel_id, CHUNK_VOLUME>::array; + void serialize(io::WriteBuffer& buffer) const; + void deserialize(io::ReadBuffer& buffer); +}; +} // namespace world |
