summaryrefslogtreecommitdiffstats
path: root/game/shared
diff options
context:
space:
mode:
authoruntodesu <kirill@untode.su>2025-03-15 16:22:09 +0500
committeruntodesu <kirill@untode.su>2025-03-15 16:22:09 +0500
commit3bf42c6ff3805a0d42bbc661794a95ff31bedc26 (patch)
tree05049955847504808d6bed2bb7b155f8b03807bb /game/shared
parent02294547dcde0d4ad76e229106702261e9f10a51 (diff)
downloadvoxelius-3bf42c6ff3805a0d42bbc661794a95ff31bedc26.tar.bz2
voxelius-3bf42c6ff3805a0d42bbc661794a95ff31bedc26.zip
Add whatever I was working on for the last month
Diffstat (limited to 'game/shared')
-rw-r--r--game/shared/CMakeLists.txt55
-rw-r--r--game/shared/chunk.cc55
-rw-r--r--game/shared/chunk.hh33
-rw-r--r--game/shared/chunk_aabb.cc57
-rw-r--r--game/shared/chunk_aabb.hh30
-rw-r--r--game/shared/collision.cc160
-rw-r--r--game/shared/collision.hh19
-rw-r--r--game/shared/const.hh47
-rw-r--r--game/shared/coord.hh148
-rw-r--r--game/shared/dimension.cc163
-rw-r--r--game/shared/dimension.hh81
-rw-r--r--game/shared/factory.cc36
-rw-r--r--game/shared/factory.hh12
-rw-r--r--game/shared/game.cc118
-rw-r--r--game/shared/game.hh11
-rw-r--r--game/shared/game_items.cc78
-rw-r--r--game/shared/game_items.hh26
-rw-r--r--game/shared/game_voxels.cc113
-rw-r--r--game/shared/game_voxels.hh28
-rw-r--r--game/shared/globals.cc11
-rw-r--r--game/shared/globals.hh23
-rw-r--r--game/shared/gravity.cc17
-rw-r--r--game/shared/gravity.hh12
-rw-r--r--game/shared/grounded.hh13
-rw-r--r--game/shared/head.hh14
-rw-r--r--game/shared/item_registry.cc80
-rw-r--r--game/shared/item_registry.hh57
-rw-r--r--game/shared/pch.hh23
-rw-r--r--game/shared/player.hh7
-rw-r--r--game/shared/protocol.cc490
-rw-r--r--game/shared/protocol.hh213
-rw-r--r--game/shared/ray_dda.cc102
-rw-r--r--game/shared/ray_dda.hh35
-rw-r--r--game/shared/splash.cc34
-rw-r--r--game/shared/splash.hh11
-rw-r--r--game/shared/stasis.cc16
-rw-r--r--game/shared/stasis.hh14
-rw-r--r--game/shared/threading.cc91
-rw-r--r--game/shared/threading.hh46
-rw-r--r--game/shared/transform.cc31
-rw-r--r--game/shared/transform.hh25
-rw-r--r--game/shared/types.hh44
-rw-r--r--game/shared/velocity.cc16
-rw-r--r--game/shared/velocity.hh17
-rw-r--r--game/shared/voxel_registry.cc184
-rw-r--r--game/shared/voxel_registry.hh144
-rw-r--r--game/shared/voxel_storage.cc43
-rw-r--r--game/shared/voxel_storage.hh18
48 files changed, 3101 insertions, 0 deletions
diff --git a/game/shared/CMakeLists.txt b/game/shared/CMakeLists.txt
new file mode 100644
index 0000000..cd3eed4
--- /dev/null
+++ b/game/shared/CMakeLists.txt
@@ -0,0 +1,55 @@
+add_library(shared STATIC
+ "${CMAKE_CURRENT_LIST_DIR}/chunk_aabb.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/chunk_aabb.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/chunk.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/chunk.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt"
+ "${CMAKE_CURRENT_LIST_DIR}/collision.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/collision.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/const.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/coord.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/dimension.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/dimension.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/factory.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/factory.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}/game.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/game.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/globals.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/globals.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}/item_registry.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/item_registry.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/pch.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/player.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/protocol.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/protocol.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/ray_dda.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/ray_dda.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/splash.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/splash.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/stasis.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/stasis.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/threading.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/threading.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/transform.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/transform.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/types.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/velocity.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/velocity.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")
+target_compile_features(shared PUBLIC cxx_std_17)
+target_include_directories(shared PUBLIC "${DEPS_INCLUDE_DIR}")
+target_include_directories(shared PRIVATE "${PROJECT_SOURCE_DIR}")
+target_include_directories(shared PRIVATE "${PROJECT_SOURCE_DIR}/game")
+target_precompile_headers(shared PRIVATE "${CMAKE_CURRENT_LIST_DIR}/pch.hh")
+target_link_libraries(shared PUBLIC core enet FNL miniz parson)
diff --git a/game/shared/chunk.cc b/game/shared/chunk.cc
new file mode 100644
index 0000000..c996973
--- /dev/null
+++ b/game/shared/chunk.cc
@@ -0,0 +1,55 @@
+#include "shared/pch.hh"
+#include "shared/chunk.hh"
+
+#include "shared/coord.hh"
+
+Chunk::Chunk(entt::entity entity, Dimension *dimension)
+{
+ m_entity = entity;
+ m_dimension = dimension;
+ m_voxels.fill(NULL_VOXEL_ID);
+}
+
+voxel_id Chunk::get_voxel(const local_pos &lpos) const
+{
+ return get_voxel(coord::to_index(lpos));
+}
+
+voxel_id Chunk::get_voxel(const std::size_t index) const
+{
+ if(index >= CHUNK_VOLUME)
+ return NULL_VOXEL_ID;
+ return m_voxels[index];
+}
+
+void Chunk::set_voxel(voxel_id voxel, const local_pos &lpos)
+{
+ set_voxel(voxel, coord::to_index(lpos));
+}
+
+void Chunk::set_voxel(voxel_id voxel, const std::size_t index)
+{
+ if(index >= CHUNK_VOLUME)
+ return;
+ m_voxels[index] = voxel;
+}
+
+const VoxelStorage &Chunk::get_voxels(void) const
+{
+ return m_voxels;
+}
+
+void Chunk::set_voxels(const VoxelStorage &voxels)
+{
+ m_voxels = voxels;
+}
+
+entt::entity Chunk::get_entity(void) const
+{
+ return m_entity;
+}
+
+Dimension *Chunk::get_dimension(void) const
+{
+ return m_dimension;
+}
diff --git a/game/shared/chunk.hh b/game/shared/chunk.hh
new file mode 100644
index 0000000..506d867
--- /dev/null
+++ b/game/shared/chunk.hh
@@ -0,0 +1,33 @@
+#ifndef SHARED_CHUNK_HH
+#define SHARED_CHUNK_HH 1
+#pragma once
+
+#include "shared/types.hh"
+#include "shared/voxel_storage.hh"
+
+class Dimension;
+
+class Chunk final {
+public:
+ explicit Chunk(entt::entity entity, Dimension *dimension);
+ virtual ~Chunk(void) = default;
+
+ voxel_id get_voxel(const local_pos &lpos) const;
+ voxel_id get_voxel(const std::size_t index) const;
+
+ void set_voxel(voxel_id voxel, const local_pos &lpos);
+ void set_voxel(voxel_id voxel, const std::size_t index);
+
+ const VoxelStorage &get_voxels(void) const;
+ void set_voxels(const VoxelStorage &voxels);
+
+ entt::entity get_entity(void) const;
+ Dimension *get_dimension(void) const;
+
+private:
+ entt::entity m_entity;
+ Dimension *m_dimension;
+ VoxelStorage m_voxels;
+};
+
+#endif /* SHARED_CHUNK_HH */
diff --git a/game/shared/chunk_aabb.cc b/game/shared/chunk_aabb.cc
new file mode 100644
index 0000000..b4e5b82
--- /dev/null
+++ b/game/shared/chunk_aabb.cc
@@ -0,0 +1,57 @@
+#include "shared/pch.hh"
+#include "shared/chunk_aabb.hh"
+
+void ChunkAABB::set_bounds(const chunk_pos &min, const chunk_pos &max)
+{
+ this->min = min;
+ this->max = max;
+}
+
+void ChunkAABB::set_offset(const chunk_pos &base, const chunk_pos &size)
+{
+ this->min = base;
+ this->max = base + size;
+}
+
+bool ChunkAABB::contains(const chunk_pos &point) const
+{
+ if((point.x < min.x) || (point.x > max.x))
+ return false;
+ if((point.y < min.y) || (point.y > max.y))
+ return false;
+ if((point.z < min.z) || (point.z > max.z))
+ return false;
+ return true;
+}
+
+bool ChunkAABB::intersect(const ChunkAABB &other_box) const
+{
+ if((min.x >= other_box.max.x) || (max.x <= other_box.min.x))
+ return false;
+ if((min.y >= other_box.max.y) || (max.y <= other_box.min.y))
+ return false;
+ if((min.z >= other_box.max.z) || (max.z <= other_box.min.z))
+ return false;
+ return true;
+}
+
+ChunkAABB ChunkAABB::combine_with(const ChunkAABB &other_box) const
+{
+ ChunkAABB result;
+ result.set_bounds(min, other_box.max);
+ return result;
+}
+
+ChunkAABB ChunkAABB::multiply_with(const ChunkAABB &other_box) const
+{
+ ChunkAABB result;
+ result.set_bounds(other_box.min, max);
+ return result;
+}
+
+ChunkAABB ChunkAABB::push(const chunk_pos &vector) const
+{
+ ChunkAABB result;
+ result.set_bounds(min + vector, max + vector);
+ return result;
+}
diff --git a/game/shared/chunk_aabb.hh b/game/shared/chunk_aabb.hh
new file mode 100644
index 0000000..68b8701
--- /dev/null
+++ b/game/shared/chunk_aabb.hh
@@ -0,0 +1,30 @@
+#ifndef SHARED_CHUNK_AABB
+#define SHARED_CHUNK_AABB 1
+#pragma once
+
+#include "shared/types.hh"
+
+class ChunkAABB final {
+public:
+ explicit ChunkAABB(void) = default;
+ virtual ~ChunkAABB(void) = default;
+
+ void set_bounds(const chunk_pos &min, const chunk_pos &max);
+ void set_offset(const chunk_pos &base, const chunk_pos &size);
+
+ const chunk_pos &get_min(void) const;
+ const chunk_pos &get_max(void) const;
+
+ bool contains(const chunk_pos &point) const;
+ bool intersect(const ChunkAABB &other_box) const;
+
+ ChunkAABB combine_with(const ChunkAABB &other_box) const;
+ ChunkAABB multiply_with(const ChunkAABB &other_box) const;
+ ChunkAABB push(const chunk_pos &vector) const;
+
+public:
+ chunk_pos min;
+ chunk_pos max;
+};
+
+#endif /* SHARED_CHUNK_AABB */
diff --git a/game/shared/collision.cc b/game/shared/collision.cc
new file mode 100644
index 0000000..126d3bb
--- /dev/null
+++ b/game/shared/collision.cc
@@ -0,0 +1,160 @@
+#include "shared/pch.hh"
+#include "shared/collision.hh"
+
+#include "core/constexpr.hh"
+
+#include "shared/coord.hh"
+#include "shared/dimension.hh"
+#include "shared/globals.hh"
+#include "shared/grounded.hh"
+#include "shared/transform.hh"
+#include "shared/velocity.hh"
+#include "shared/voxel_registry.hh"
+
+static int vgrid_collide(const Dimension *dimension, int d, CollisionComponent &collision, TransformComponent &transform, VelocityComponent &velocity, voxel_surface &touch_surface)
+{
+ const auto move = globals::fixed_frametime * velocity.value[d];
+ const auto move_sign = cxpr::sign<int>(move);
+
+ const auto &ref_aabb = collision.aabb;
+ const auto current_aabb = ref_aabb.push(transform.local);
+
+ auto next_aabb = AABB(current_aabb);
+ next_aabb.min[d] += move;
+ next_aabb.max[d] += move;
+
+ local_pos lpos_min;
+ lpos_min.x = cxpr::floor<local_pos::value_type>(next_aabb.min.x);
+ lpos_min.y = cxpr::floor<local_pos::value_type>(next_aabb.min.y);
+ lpos_min.z = cxpr::floor<local_pos::value_type>(next_aabb.min.z);
+
+ local_pos lpos_max;
+ lpos_max.x = cxpr::ceil<local_pos::value_type>(next_aabb.max.x);
+ lpos_max.y = cxpr::ceil<local_pos::value_type>(next_aabb.max.y);
+ lpos_max.z = cxpr::ceil<local_pos::value_type>(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];
+ }
+
+ voxel_touch latch_touch = voxel_touch::NOTHING;
+ glm::fvec3 latch_values = glm::fvec3(0.0f, 0.0f, 0.0f);
+ voxel_surface latch_surface = voxel_surface::UNKNOWN;
+ AABB 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;
+
+ const auto vpos = coord::to_voxel(transform.chunk, lpos);
+ const auto info = voxel_registry::find(dimension->get_voxel(vpos));
+
+ if(info == nullptr) {
+ // Don't collide with something
+ // that we assume to be nothing
+ continue;
+ }
+
+ AABB vbox;
+ vbox.min = glm::fvec3(lpos);
+ vbox.max = glm::fvec3(lpos) + 1.0f;
+
+ if(!next_aabb.intersect(vbox)) {
+ // No intersection between the voxel
+ // and the entity's collision hull
+ continue;
+ }
+
+ if(info->touch_type == voxel_touch::SOLID) {
+ // Solid touch type makes a collision
+ // response whenever it is encountered
+ velocity.value[d] = 0.0f;
+ touch_surface = info->surface;
+ return move_sign;
+ }
+
+ // In case of other touch types, they
+ // are latched and the last ever touch
+ // type is then responded to
+ if(info->touch_type != voxel_touch::NOTHING) {
+ latch_touch = info->touch_type;
+ latch_values = info->touch_values;
+ latch_surface = info->surface;
+ latch_vbox = vbox;
+ continue;
+ }
+ }
+ }
+
+ if(latch_touch != voxel_touch::NOTHING) {
+ if(latch_touch == voxel_touch::BOUNCE) {
+ const auto move_distance = cxpr::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 == voxel_touch::SINK) {
+ velocity.value[d] *= latch_values[d];
+ touch_surface = latch_surface;
+ return move_sign;
+ }
+ }
+
+ return 0;
+}
+
+void CollisionComponent::fixed_update(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<CollisionComponent>(entt::get<TransformComponent, VelocityComponent>);
+
+ for(auto [entity, collision, transform, velocity] : group.each()) {
+ auto surface = voxel_surface::UNKNOWN;
+ auto vertical_move = vgrid_collide(dimension, 1, collision, transform, velocity, surface);
+
+ if(vertical_move == cxpr::sign<int>(dimension->get_gravity())) {
+ auto &component = dimension->entities.get_or_emplace<GroundedComponent>(entity);
+ component.surface = surface;
+ }
+ else {
+ dimension->entities.remove<GroundedComponent>(entity);
+ }
+
+ vgrid_collide(dimension, 0, collision, transform, velocity, surface);
+ vgrid_collide(dimension, 2, collision, transform, velocity, surface);
+ }
+}
diff --git a/game/shared/collision.hh b/game/shared/collision.hh
new file mode 100644
index 0000000..3e3643f
--- /dev/null
+++ b/game/shared/collision.hh
@@ -0,0 +1,19 @@
+#ifndef SHARED_COLLISION_HH
+#define SHARED_COLLISION_HH 1
+#pragma once
+
+#include "core/aabb.hh"
+
+class Dimension;
+
+struct CollisionComponent final {
+ AABB aabb;
+
+public:
+ // NOTE: CollisionComponent::fixed_update must be called
+ // before TransformComponent::fixed_update and VelocityComponent::fixed_update
+ // because both transform and velocity may be updated internally
+ static void fixed_update(Dimension *dimension);
+};
+
+#endif /* SHARED_COLLISION_HH */
diff --git a/game/shared/const.hh b/game/shared/const.hh
new file mode 100644
index 0000000..8f8f0f9
--- /dev/null
+++ b/game/shared/const.hh
@@ -0,0 +1,47 @@
+#ifndef SHARED_CONST_HH
+#define SHARED_CONST_HH 1
+#pragma once
+
+#include "core/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 = cxpr::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);
+
+#endif /* SHARED_CONST_HH */
diff --git a/game/shared/coord.hh b/game/shared/coord.hh
new file mode 100644
index 0000000..e51624f
--- /dev/null
+++ b/game/shared/coord.hh
@@ -0,0 +1,148 @@
+#ifndef SHARED_COORD_HH
+#define SHARED_COORD_HH 1
+#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>(cxpr::mod_signed<voxel_pos::value_type>(vpos.x, CHUNK_SIZE)),
+ static_cast<local_pos::value_type>(cxpr::mod_signed<voxel_pos::value_type>(vpos.y, CHUNK_SIZE)),
+ static_cast<local_pos::value_type>(cxpr::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),
+ };
+}
+
+#endif /* SHARED_COORD_HH */
diff --git a/game/shared/dimension.cc b/game/shared/dimension.cc
new file mode 100644
index 0000000..378779c
--- /dev/null
+++ b/game/shared/dimension.cc
@@ -0,0 +1,163 @@
+#include "shared/pch.hh"
+#include "shared/dimension.hh"
+
+#include "shared/coord.hh"
+#include "shared/globals.hh"
+#include "shared/chunk.hh"
+
+Dimension::Dimension(const char *name, float gravity)
+{
+ m_name = name;
+ m_gravity = gravity;
+}
+
+Dimension::~Dimension(void)
+{
+ for(const auto it : m_hashmap)
+ delete it.second;
+ entities.clear();
+ chunks.clear();
+}
+
+const char *Dimension::get_name(void) const
+{
+ return m_name.c_str();
+}
+
+float Dimension::get_gravity(void) const
+{
+ return m_gravity;
+}
+
+Chunk *Dimension::create_chunk(const chunk_pos &cpos)
+{
+ auto it = m_hashmap.find(cpos);
+
+ if(it != m_hashmap.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_hashmap.insert_or_assign(cpos, std::move(chunk)).first->second;
+}
+
+Chunk *Dimension::find_chunk(entt::entity entity) const
+{
+ if(chunks.valid(entity))
+ return chunks.get<ChunkComponent>(entity).chunk;
+ return nullptr;
+}
+
+Chunk *Dimension::find_chunk(const chunk_pos &cpos) const
+{
+ auto it = m_hashmap.find(cpos);
+ if(it != m_hashmap.cend())
+ return it->second;
+ return nullptr;
+}
+
+void Dimension::remove_chunk(entt::entity entity)
+{
+ if(chunks.valid(entity)) {
+ auto &component = chunks.get<ChunkComponent>(entity);
+ m_hashmap.erase(component.cpos);
+ chunks.destroy(entity);
+ }
+}
+
+void Dimension::remove_chunk(const chunk_pos &cpos)
+{
+ auto it = m_hashmap.find(cpos);
+
+ if(it != m_hashmap.cend()) {
+ chunks.destroy(it->second->get_entity());
+ m_hashmap.erase(it);
+ }
+}
+
+void Dimension::remove_chunk(Chunk *chunk)
+{
+ if(chunk) {
+ const auto &component = chunks.get<ChunkComponent>(chunk->get_entity());
+ m_hashmap.erase(component.cpos);
+ chunks.destroy(chunk->get_entity());
+ }
+}
+
+voxel_id 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 NULL_VOXEL_ID;
+}
+
+voxel_id 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 Dimension::set_voxel(voxel_id voxel, const voxel_pos &vpos)
+{
+ auto cpos = coord::to_chunk(vpos);
+ auto lpos = coord::to_local(vpos);
+
+ if(auto chunk = find_chunk(cpos)) {
+ chunk->set_voxel(voxel, lpos);
+
+ VoxelSetEvent event;
+ event.dimension = this;
+ event.cpos = cpos;
+ event.lpos = lpos;
+ event.voxel = voxel;
+ event.chunk = chunk;
+
+ globals::dispatcher.trigger(event);
+
+ return true;
+ }
+
+ return false;
+}
+
+bool Dimension::set_voxel(voxel_id 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 Dimension::init(Config &config)
+{
+
+}
+
+void Dimension::init_late(std::uint64_t global_seed)
+{
+
+}
+
+bool Dimension::generate(const chunk_pos &cpos, VoxelStorage &voxels)
+{
+ return false;
+}
diff --git a/game/shared/dimension.hh b/game/shared/dimension.hh
new file mode 100644
index 0000000..eb6f896
--- /dev/null
+++ b/game/shared/dimension.hh
@@ -0,0 +1,81 @@
+#ifndef SHARED_DIMENSION_HH
+#define SHARED_DIMENSION_HH 1
+#pragma once
+
+#include "shared/types.hh"
+
+class Chunk;
+class Config;
+class VoxelStorage;
+
+class Dimension {
+public:
+ explicit Dimension(const char *name, float gravity);
+ virtual ~Dimension(void);
+
+ const char *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:
+ voxel_id get_voxel(const voxel_pos &vpos) const;
+ voxel_id get_voxel(const chunk_pos &cpos, const local_pos &lpos) const;
+
+ bool set_voxel(voxel_id voxel, const voxel_pos &vpos);
+ bool set_voxel(voxel_id voxel, const chunk_pos &cpos, const local_pos &lpos);
+
+public:
+ virtual void init(Config &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_hashmap;
+ float m_gravity;
+};
+
+struct ChunkComponent final {
+ chunk_pos cpos;
+ Chunk *chunk;
+};
+
+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;
+ chunk_pos cpos;
+ local_pos lpos;
+ voxel_id voxel;
+ Chunk *chunk;
+};
+
+#endif /* SHARED_DIMENSION_HH */
diff --git a/game/shared/factory.cc b/game/shared/factory.cc
new file mode 100644
index 0000000..ee09a24
--- /dev/null
+++ b/game/shared/factory.cc
@@ -0,0 +1,36 @@
+#include "shared/pch.hh"
+#include "shared/factory.hh"
+
+#include "shared/collision.hh"
+#include "shared/dimension.hh"
+#include "shared/globals.hh"
+#include "shared/gravity.hh"
+#include "shared/head.hh"
+#include "shared/player.hh"
+#include "shared/transform.hh"
+#include "shared/velocity.hh"
+
+void shared_factory::create_player(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<CollisionComponent>(entity);
+ collision.aabb.min = glm::fvec3(-0.4f, -1.6f, -0.4f);
+ collision.aabb.max = glm::fvec3(+0.4f, +0.2f, +0.4f);
+
+ dimension->entities.emplace_or_replace<GravityComponent>(entity);
+
+ auto &head = dimension->entities.emplace_or_replace<HeadComponent>(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<PlayerComponent>(entity);
+
+ auto &transform = dimension->entities.emplace_or_replace<TransformComponent>(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<VelocityComponent>(entity);
+ velocity.value = glm::fvec3(0.0f, 0.0f, 0.0f);
+}
diff --git a/game/shared/factory.hh b/game/shared/factory.hh
new file mode 100644
index 0000000..79df276
--- /dev/null
+++ b/game/shared/factory.hh
@@ -0,0 +1,12 @@
+#ifndef SHARED_FACTORY
+#define SHARED_FACTORY 1
+#pragma once
+
+class Dimension;
+
+namespace shared_factory
+{
+void create_player(Dimension *dimension, entt::entity entity);
+} // namespace shared_factory
+
+#endif /* SHARED_FACTORY */
diff --git a/game/shared/game.cc b/game/shared/game.cc
new file mode 100644
index 0000000..22693eb
--- /dev/null
+++ b/game/shared/game.cc
@@ -0,0 +1,118 @@
+#include "shared/pch.hh"
+#include "shared/game.hh"
+
+#include "core/cmdline.hh"
+
+static std::filesystem::path get_gamepath(void)
+{
+ if(auto gamepath = cmdline::get("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() / "data";
+}
+
+static std::filesystem::path get_userpath(void)
+{
+ if(auto userpath = cmdline::get("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)
+{
+ 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>());
+ logger_sinks.push_back(std::make_shared<spdlog::sinks::basic_file_sink_mt>("voxelius.log", false));
+
+#if defined(NDEBUG)
+ constexpr auto default_loglevel = spdlog::level::info;
+#else
+ constexpr auto default_loglevel = spdlog::level::trace;
+#endif
+
+ if(cmdline::contains("quiet"))
+ logger->set_level(spdlog::level::warn);
+ else if(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: {}", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
+ 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(), PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
+ std::terminate();
+ }
+
+ if(!PHYSFS_mount(userpath.string().c_str(), nullptr, false)) {
+ spdlog::critical("physfs: mount {} failed: {}", userpath.string(), PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
+ std::terminate();
+ }
+
+ if(!PHYSFS_setWriteDir(userpath.string().c_str())) {
+ spdlog::critical("physfs: setwritedir {} failed: {}", userpath.string(), PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
+ std::terminate();
+ }
+
+ if(enet_initialize()) {
+ spdlog::critical("enet: init failed");
+ std::terminate();
+ }
+}
+
+void shared_game::deinit(void)
+{
+ enet_deinitialize();
+
+ if(!PHYSFS_deinit()) {
+ spdlog::critical("physfs: deinit failed: {}", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
+ std::terminate();
+ }
+}
diff --git a/game/shared/game.hh b/game/shared/game.hh
new file mode 100644
index 0000000..f274d25
--- /dev/null
+++ b/game/shared/game.hh
@@ -0,0 +1,11 @@
+#ifndef SHARED_GAME
+#define SHARED_GAME 1
+#pragma once
+
+namespace shared_game
+{
+void init(int argc, char **argv);
+void deinit(void);
+} // namespace shared_game
+
+#endif /* SHARED_GAME */
diff --git a/game/shared/game_items.cc b/game/shared/game_items.cc
new file mode 100644
index 0000000..dae286f
--- /dev/null
+++ b/game/shared/game_items.cc
@@ -0,0 +1,78 @@
+#include "shared/pch.hh"
+#include "shared/game_items.hh"
+
+#include "shared/game_voxels.hh"
+#include "shared/item_registry.hh"
+
+item_id game_items::stone = NULL_ITEM_ID;
+item_id game_items::cobblestone = NULL_ITEM_ID;
+item_id game_items::dirt = NULL_ITEM_ID;
+item_id game_items::grass = NULL_ITEM_ID;
+item_id game_items::oak_leaves = NULL_ITEM_ID;
+item_id game_items::oak_planks = NULL_ITEM_ID;
+item_id game_items::oak_log = NULL_ITEM_ID;
+item_id game_items::glass = NULL_ITEM_ID;
+item_id game_items::slime = NULL_ITEM_ID;
+item_id game_items::mud = NULL_ITEM_ID;
+
+void game_items::populate(void)
+{
+ // Stone; a hardened slate rock
+ game_items::stone = item_registry::construct("stone")
+ .set_texture("textures/item/stone.png")
+ .set_place_voxel(game_voxels::stone)
+ .build();
+
+ // Cobblestone; a bunch of small stones
+ game_items::cobblestone = item_registry::construct("cobblestone")
+ .set_texture("textures/item/cobblestone.png")
+ .set_place_voxel(game_voxels::cobblestone)
+ .build();
+
+ // Dirt; it's very dirty
+ game_items::dirt = item_registry::construct("dirt")
+ .set_texture("textures/item/dirt.png")
+ .set_place_voxel(game_voxels::dirt)
+ .build();
+
+ // Grass; literally just grassy dirt
+ game_items::grass = item_registry::construct("grass")
+ .set_texture("textures/item/grass.png")
+ .set_place_voxel(game_voxels::grass)
+ .build();
+
+ // Oak leaves; they're bushy!
+ game_items::oak_leaves = item_registry::construct("oak_leaves")
+ .set_texture("textures/item/oak_leaves.png")
+ .set_place_voxel(game_voxels::oak_leaves)
+ .build();
+
+ // Oak planks; watch for splinters!
+ game_items::oak_planks = item_registry::construct("oak_planks")
+ .set_texture("textures/item/oak_planks.png")
+ .set_place_voxel(game_voxels::oak_planks)
+ .build();
+
+ // Oak log; a big wad of wood
+ game_items::oak_log = item_registry::construct("oak_log")
+ .set_texture("textures/item/oak_log.png")
+ .set_place_voxel(game_voxels::oak_log)
+ .build();
+
+ // Glass; used for windowing
+ game_items::glass = item_registry::construct("glass")
+ .set_texture("textures/item/glass.png")
+ .set_place_voxel(game_voxels::glass)
+ .build();
+
+ // Slime; it's bouncy!
+ game_items::slime = item_registry::construct("slime")
+ .set_texture("textures/item/slime.png")
+ .set_place_voxel(game_voxels::slime)
+ .build();
+
+ // Mud; you sink in it!
+ game_items::mud = item_registry::construct("mud")
+ .set_texture("textures/item/mud.png")
+ .build();
+}
diff --git a/game/shared/game_items.hh b/game/shared/game_items.hh
new file mode 100644
index 0000000..4e4eb81
--- /dev/null
+++ b/game/shared/game_items.hh
@@ -0,0 +1,26 @@
+#ifndef SHARED_GAME_ITEMS
+#define SHARED_GAME_ITEMS 1
+#pragma once
+
+#include "shared/types.hh"
+
+namespace game_items
+{
+extern item_id stone;
+extern item_id cobblestone;
+extern item_id dirt;
+extern item_id grass;
+extern item_id oak_leaves;
+extern item_id oak_planks;
+extern item_id oak_log;
+extern item_id glass;
+extern item_id slime;
+extern item_id mud;
+} // namespace game_items
+
+namespace game_items
+{
+void populate(void);
+} // namespace game_items
+
+#endif /* SHARED_GAME_ITEMS */
diff --git a/game/shared/game_voxels.cc b/game/shared/game_voxels.cc
new file mode 100644
index 0000000..b72ded4
--- /dev/null
+++ b/game/shared/game_voxels.cc
@@ -0,0 +1,113 @@
+#include "shared/pch.hh"
+#include "shared/game_voxels.hh"
+
+#include "shared/voxel_registry.hh"
+
+voxel_id game_voxels::cobblestone = NULL_VOXEL_ID;
+voxel_id game_voxels::dirt = NULL_VOXEL_ID;
+voxel_id game_voxels::grass = NULL_VOXEL_ID;
+voxel_id game_voxels::stone = NULL_VOXEL_ID;
+voxel_id game_voxels::vtest = NULL_VOXEL_ID;
+voxel_id game_voxels::vtest_ck = NULL_VOXEL_ID;
+voxel_id game_voxels::oak_leaves = NULL_VOXEL_ID;
+voxel_id game_voxels::oak_planks = NULL_VOXEL_ID;
+voxel_id game_voxels::oak_log = NULL_VOXEL_ID;
+voxel_id game_voxels::glass = NULL_VOXEL_ID;
+voxel_id game_voxels::slime = NULL_VOXEL_ID;
+voxel_id game_voxels::mud = NULL_VOXEL_ID;
+
+void game_voxels::populate(void)
+{
+ // Stone; the backbone of the generated world
+ game_voxels::stone = voxel_registry::construct("stone", voxel_type::CUBE, false, false)
+ .add_texture_default("textures/voxel/stone_01.png")
+ .add_texture_default("textures/voxel/stone_02.png")
+ .add_texture_default("textures/voxel/stone_03.png")
+ .add_texture_default("textures/voxel/stone_04.png")
+ .set_surface(voxel_surface::STONE)
+ .build();
+
+ // Cobblestone; should drop when a stone is broken, might also be present in surface features
+ game_voxels::cobblestone = voxel_registry::construct("cobblestone", voxel_type::CUBE, false, false)
+ .add_texture_default("textures/voxel/cobblestone_01.png")
+ .add_texture_default("textures/voxel/cobblestone_02.png")
+ .set_surface(voxel_surface::STONE)
+ .build();
+
+ // Dirt with a grass layer on top; the top layer of plains biome
+ game_voxels::grass = voxel_registry::construct("grass", voxel_type::CUBE, false, false)
+ .add_texture_default("textures/voxel/grass_side_01.png")
+ .add_texture_default("textures/voxel/grass_side_02.png")
+ .add_texture(voxel_face::CUBE_BOTTOM, "textures/voxel/dirt_01.png")
+ .add_texture(voxel_face::CUBE_BOTTOM, "textures/voxel/dirt_02.png")
+ .add_texture(voxel_face::CUBE_BOTTOM, "textures/voxel/dirt_03.png")
+ .add_texture(voxel_face::CUBE_BOTTOM, "textures/voxel/dirt_04.png")
+ .add_texture(voxel_face::CUBE_TOP, "textures/voxel/grass_01.png")
+ .add_texture(voxel_face::CUBE_TOP, "textures/voxel/grass_02.png")
+ .set_surface(voxel_surface::GRASS)
+ .build();
+
+ // Dirt; the under-surface layer of some biomes
+ game_voxels::dirt = voxel_registry::construct("dirt", voxel_type::CUBE, false, false)
+ .add_texture_default("textures/voxel/dirt_01.png")
+ .add_texture_default("textures/voxel/dirt_02.png")
+ .add_texture_default("textures/voxel/dirt_03.png")
+ .add_texture_default("textures/voxel/dirt_04.png")
+ .set_surface(voxel_surface::DIRT)
+ .build();
+
+ // VTest; a test voxel to ensure animations work
+ game_voxels::vtest = voxel_registry::construct("vtest", voxel_type::CUBE, true, false)
+ .add_texture_default("textures/voxel/vtest_F1.png")
+ .add_texture_default("textures/voxel/vtest_F2.png")
+ .add_texture_default("textures/voxel/vtest_F3.png")
+ .add_texture_default("textures/voxel/vtest_F4.png")
+ .build();
+
+ // VTest-CK; a pure blue chromakey I used to make the game's logo
+ game_voxels::vtest_ck = voxel_registry::construct("vtest_ck", voxel_type::CUBE, false, false)
+ .add_texture_default("textures/voxel/chromakey.png")
+ .build();
+
+ // Oak leaves; greenery. TODO: add trees as surface features
+ game_voxels::oak_leaves = voxel_registry::construct("oak_leaves", voxel_type::CUBE, false, false)
+ .add_texture_default("textures/voxel/oak_leaves.png")
+ .set_surface(voxel_surface::GRASS)
+ .build();
+
+ // Oak planks; the thing that comes out of oak logs
+ game_voxels::oak_planks = voxel_registry::construct("oak_planks", voxel_type::CUBE, false, false)
+ .add_texture_default("textures/voxel/oak_planks_01.png")
+ .add_texture_default("textures/voxel/oak_planks_02.png")
+ .set_surface(voxel_surface::WOOD)
+ .build();
+
+ // Oak logs; greenery. TODO: add trees as surface features
+ game_voxels::oak_log = voxel_registry::construct("oak_log", voxel_type::CUBE, false, false)
+ .add_texture_default("textures/voxel/oak_wood_01.png")
+ .add_texture_default("textures/voxel/oak_wood_02.png")
+ .add_texture(voxel_face::CUBE_BOTTOM, "textures/voxel/oak_wood_top.png")
+ .add_texture(voxel_face::CUBE_TOP, "textures/voxel/oak_wood_top.png")
+ .set_surface(voxel_surface::WOOD)
+ .build();
+
+ // Glass; blend rendering test
+ game_voxels::glass = voxel_registry::construct("glass", voxel_type::CUBE, false, true)
+ .add_texture_default("textures/voxel/glass_01.png")
+ .set_surface(voxel_surface::GLASS)
+ .build();
+
+ // Slime; it's bouncy!
+ game_voxels::slime = voxel_registry::construct("slime", voxel_type::CUBE, false, true)
+ .set_touch(voxel_touch::BOUNCE, glm::fvec3(0.00f, 0.60f, 0.00f))
+ .add_texture_default("textures/voxel/slime_01.png")
+ .build();
+
+ // Mud; you sink in it
+ game_voxels::mud = voxel_registry::construct("mud", voxel_type::CUBE, false, false)
+ .set_touch(voxel_touch::SINK, glm::fvec3(0.50f, 0.75f, 0.50f))
+ .add_texture_default("textures/voxel/mud_01.png")
+ .add_texture_default("textures/voxel/mud_02.png")
+ .set_surface(voxel_surface::DIRT)
+ .build();
+}
diff --git a/game/shared/game_voxels.hh b/game/shared/game_voxels.hh
new file mode 100644
index 0000000..1b344bf
--- /dev/null
+++ b/game/shared/game_voxels.hh
@@ -0,0 +1,28 @@
+#ifndef SHARED_GAME_VOXELS
+#define SHARED_GAME_VOXELS 1
+#pragma once
+
+#include "shared/types.hh"
+
+namespace game_voxels
+{
+extern voxel_id cobblestone;
+extern voxel_id dirt;
+extern voxel_id grass;
+extern voxel_id stone;
+extern voxel_id vtest;
+extern voxel_id vtest_ck;
+extern voxel_id oak_leaves;
+extern voxel_id oak_planks;
+extern voxel_id oak_log;
+extern voxel_id glass;
+extern voxel_id slime;
+extern voxel_id mud;
+} // namespace game_voxels
+
+namespace game_voxels
+{
+void populate(void);
+} // namespace game_voxels
+
+#endif /* SHARED_GAME_VOXELS */
diff --git a/game/shared/globals.cc b/game/shared/globals.cc
new file mode 100644
index 0000000..466398f
--- /dev/null
+++ b/game/shared/globals.cc
@@ -0,0 +1,11 @@
+#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/game/shared/globals.hh b/game/shared/globals.hh
new file mode 100644
index 0000000..216d341
--- /dev/null
+++ b/game/shared/globals.hh
@@ -0,0 +1,23 @@
+#ifndef SHARED_GLOBALS_HH
+#define SHARED_GLOBALS_HH 1
+#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
+
+#endif /* SHARED_GLOBALS_HH */
diff --git a/game/shared/gravity.cc b/game/shared/gravity.cc
new file mode 100644
index 0000000..66b6589
--- /dev/null
+++ b/game/shared/gravity.cc
@@ -0,0 +1,17 @@
+#include "shared/pch.hh"
+#include "shared/gravity.hh"
+
+#include "shared/dimension.hh"
+#include "shared/globals.hh"
+#include "shared/stasis.hh"
+#include "shared/velocity.hh"
+
+void GravityComponent::fixed_update(Dimension *dimension)
+{
+ auto fixed_acceleration = globals::fixed_frametime * dimension->get_gravity();
+ auto group = dimension->entities.group<GravityComponent>(entt::get<VelocityComponent>, entt::exclude<StasisComponent>);
+
+ for(auto [entity, velocity] : group.each()) {
+ velocity.value.y += fixed_acceleration;
+ }
+}
diff --git a/game/shared/gravity.hh b/game/shared/gravity.hh
new file mode 100644
index 0000000..06eeae7
--- /dev/null
+++ b/game/shared/gravity.hh
@@ -0,0 +1,12 @@
+#ifndef SHARED_GRAVITY_HH
+#define SHARED_GRAVITY_HH 1
+#pragma once
+
+class Dimension;
+
+struct GravityComponent final {
+public:
+ static void fixed_update(Dimension *dimension);
+};
+
+#endif /* SHARED_GRAVITY_HH */
diff --git a/game/shared/grounded.hh b/game/shared/grounded.hh
new file mode 100644
index 0000000..869cca0
--- /dev/null
+++ b/game/shared/grounded.hh
@@ -0,0 +1,13 @@
+#ifndef SHARED_GROUNDED
+#define SHARED_GROUNDED 1
+#pragma once
+
+#include "shared/voxel_registry.hh"
+
+// Assigned to entities which are grounded
+// according to the collision and gravity system
+struct GroundedComponent final {
+ voxel_surface surface;
+};
+
+#endif /* SHARED_GROUNDED */
diff --git a/game/shared/head.hh b/game/shared/head.hh
new file mode 100644
index 0000000..57dd445
--- /dev/null
+++ b/game/shared/head.hh
@@ -0,0 +1,14 @@
+#ifndef SHARED_HEAD_HH
+#define SHARED_HEAD_HH 1
+#pragma once
+
+struct HeadComponent {
+ glm::fvec3 angles;
+ glm::fvec3 offset;
+};
+
+// Client-side only - interpolated and previous head
+struct HeadComponentIntr final : public HeadComponent {};
+struct HeadComponentPrev final : public HeadComponent {};
+
+#endif /* SHARED_HEAD_HH */
diff --git a/game/shared/item_registry.cc b/game/shared/item_registry.cc
new file mode 100644
index 0000000..02164bd
--- /dev/null
+++ b/game/shared/item_registry.cc
@@ -0,0 +1,80 @@
+#include "shared/pch.hh"
+#include "shared/item_registry.hh"
+
+#include "shared/voxel_registry.hh"
+
+std::unordered_map<std::string, ItemInfoBuilder> item_registry::builders = {};
+std::unordered_map<std::string, item_id> item_registry::names = {};
+std::vector<std::shared_ptr<ItemInfo>> item_registry::items = {};
+
+ItemInfoBuilder::ItemInfoBuilder(const char *name)
+{
+ prototype.name = name;
+ prototype.texture = std::string();
+ prototype.place_voxel = NULL_VOXEL_ID;
+ prototype.cached_texture = nullptr;
+}
+
+ItemInfoBuilder &ItemInfoBuilder::set_texture(const char *texture)
+{
+ prototype.texture = texture;
+ prototype.cached_texture = nullptr;
+ return *this;
+}
+
+ItemInfoBuilder &ItemInfoBuilder::set_place_voxel(voxel_id place_voxel)
+{
+ prototype.place_voxel = place_voxel;
+ return *this;
+}
+
+item_id ItemInfoBuilder::build(void) const
+{
+ const auto it = item_registry::names.find(prototype.name);
+
+ if(it != item_registry::names.cend()) {
+ spdlog::warn("item_registry: cannot build {}: name already present", prototype.name);
+ return it->second;
+ }
+
+ auto new_info = std::make_shared<ItemInfo>();
+ new_info->name = prototype.name;
+ new_info->texture = prototype.texture;
+ new_info->place_voxel = prototype.place_voxel;
+ new_info->cached_texture = nullptr;
+
+ item_registry::items.push_back(new_info);
+ item_registry::names.insert_or_assign(prototype.name, static_cast<item_id>(item_registry::items.size()));
+
+ return static_cast<item_id>(item_registry::items.size());
+}
+
+ItemInfoBuilder &item_registry::construct(const char *name)
+{
+ const auto it = item_registry::builders.find(name);
+ if(it != item_registry::builders.cend())
+ return it->second;
+ return item_registry::builders.emplace(name, ItemInfoBuilder(name)).first->second;
+}
+
+ItemInfo *item_registry::find(const char *name)
+{
+ const auto it = item_registry::names.find(name);
+ if(it != item_registry::names.cend())
+ return item_registry::find(it->second);
+ return nullptr;
+}
+
+ItemInfo *item_registry::find(const item_id item)
+{
+ if((item != NULL_ITEM_ID) && (item <= item_registry::items.size()))
+ return item_registry::items[item - 1].get();
+ return nullptr;
+}
+
+void item_registry::purge(void)
+{
+ item_registry::builders.clear();
+ item_registry::names.clear();
+ item_registry::items.clear();
+}
diff --git a/game/shared/item_registry.hh b/game/shared/item_registry.hh
new file mode 100644
index 0000000..7160b0b
--- /dev/null
+++ b/game/shared/item_registry.hh
@@ -0,0 +1,57 @@
+#ifndef SHARED_ITEM_REGISTRY_HH
+#define SHARED_ITEM_REGISTRY_HH 1
+#pragma once
+
+#include "core/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;
+
+struct ItemInfo final {
+ std::string name;
+ std::string texture;
+ voxel_id place_voxel;
+
+ resource_ptr<TextureGUI> cached_texture; // Client-side only
+};
+
+class ItemInfoBuilder final {
+public:
+ explicit ItemInfoBuilder(const char *name);
+ virtual ~ItemInfoBuilder(void) = default;
+
+public:
+ ItemInfoBuilder &set_texture(const char *texture);
+ ItemInfoBuilder &set_place_voxel(voxel_id place_voxel);
+
+public:
+ item_id build(void) const;
+
+private:
+ ItemInfo prototype;
+};
+
+namespace item_registry
+{
+extern std::unordered_map<std::string, ItemInfoBuilder> builders;
+extern std::unordered_map<std::string, item_id> names;
+extern std::vector<std::shared_ptr<ItemInfo>> items;
+} // namespace item_registry
+
+namespace item_registry
+{
+ItemInfoBuilder &construct(const char *name);
+ItemInfo *find(const char *name);
+ItemInfo *find(const item_id item);
+} // namespace item_registry
+
+namespace item_registry
+{
+void purge(void);
+} // namespace item_registry
+
+#endif /* SHARED_ITEM_REGISTRY_HH */
diff --git a/game/shared/pch.hh b/game/shared/pch.hh
new file mode 100644
index 0000000..3588709
--- /dev/null
+++ b/game/shared/pch.hh
@@ -0,0 +1,23 @@
+#ifndef SHARED_PCH_HH
+#define SHARED_PCH_HH 1
+#pragma once
+
+#include <core/pch.hh>
+
+#include <BS_thread_pool.hpp>
+
+#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>
+
+#endif /* SHARED_PCH_HH */
diff --git a/game/shared/player.hh b/game/shared/player.hh
new file mode 100644
index 0000000..a01b4f2
--- /dev/null
+++ b/game/shared/player.hh
@@ -0,0 +1,7 @@
+#ifndef SHARED_PLAYER_HH
+#define SHARED_PLAYER_HH 1
+#pragma once
+
+struct PlayerComponent final {};
+
+#endif /* SHARED_PLAYER_HH */
diff --git a/game/shared/protocol.cc b/game/shared/protocol.cc
new file mode 100644
index 0000000..e2c79b5
--- /dev/null
+++ b/game/shared/protocol.cc
@@ -0,0 +1,490 @@
+#include "shared/pch.hh"
+#include "shared/protocol.hh"
+
+#include "core/buffer.hh"
+#include "core/floathacks.hh"
+
+#include "shared/chunk.hh"
+#include "shared/dimension.hh"
+#include "shared/globals.hh"
+#include "shared/head.hh"
+#include "shared/player.hh"
+#include "shared/transform.hh"
+#include "shared/velocity.hh"
+
+static ReadBuffer read_buffer;
+static WriteBuffer write_buffer;
+
+ENetPacket *protocol::encode(const protocol::StatusRequest &packet, enet_uint32 flags)
+{
+ write_buffer.reset();
+ write_buffer.write_UI16(protocol::StatusRequest::ID);
+ write_buffer.write_UI32(packet.version);
+ return write_buffer.to_packet(flags);
+}
+
+ENetPacket *protocol::encode(const protocol::StatusResponse &packet, enet_uint32 flags)
+{
+ write_buffer.reset();
+ write_buffer.write_UI16(protocol::StatusResponse::ID);
+ write_buffer.write_UI32(packet.version);
+ write_buffer.write_UI16(packet.max_players);
+ write_buffer.write_UI16(packet.num_players);
+ write_buffer.write_string(packet.motd);
+ return write_buffer.to_packet(flags);
+}
+
+ENetPacket *protocol::encode(const protocol::LoginRequest &packet, enet_uint32 flags)
+{
+ write_buffer.reset();
+ write_buffer.write_UI16(protocol::LoginRequest::ID);
+ write_buffer.write_UI32(packet.version);
+ write_buffer.write_UI64(packet.voxel_def_checksum);
+ write_buffer.write_UI64(packet.item_def_checksum);
+ write_buffer.write_UI64(packet.password_hash);
+ write_buffer.write_string(packet.username.substr(0, protocol::MAX_USERNAME));
+ return write_buffer.to_packet(flags);
+}
+
+ENetPacket *protocol::encode(const protocol::LoginResponse &packet, enet_uint32 flags)
+{
+ write_buffer.reset();
+ write_buffer.write_UI16(protocol::LoginResponse::ID);
+ write_buffer.write_UI16(packet.client_index);
+ write_buffer.write_UI64(packet.client_identity);
+ write_buffer.write_UI16(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_UI16(protocol::Disconnect::ID);
+ write_buffer.write_string(packet.reason);
+ return write_buffer.to_packet(flags);
+}
+
+ENetPacket *protocol::encode(const protocol::ChunkVoxels &packet, enet_uint32 flags)
+{
+ write_buffer.reset();
+ write_buffer.write_UI16(protocol::ChunkVoxels::ID);
+ write_buffer.write_I32(packet.chunk.x);
+ write_buffer.write_I32(packet.chunk.y);
+ write_buffer.write_I32(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_UI16(protocol::EntityTransform::ID);
+ write_buffer.write_UI64(static_cast<std::uint64_t>(packet.entity));
+ write_buffer.write_I32(packet.chunk.x);
+ write_buffer.write_I32(packet.chunk.y);
+ write_buffer.write_I32(packet.chunk.z);
+ write_buffer.write_FP32(packet.local.x);
+ write_buffer.write_FP32(packet.local.y);
+ write_buffer.write_FP32(packet.local.z);
+ write_buffer.write_FP32(packet.angles.x);
+ write_buffer.write_FP32(packet.angles.y);
+ write_buffer.write_FP32(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_UI16(protocol::EntityHead::ID);
+ write_buffer.write_UI64(static_cast<std::uint64_t>(packet.entity));
+ write_buffer.write_FP32(packet.angles.x);
+ write_buffer.write_FP32(packet.angles.y);
+ write_buffer.write_FP32(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_UI16(protocol::EntityVelocity::ID);
+ write_buffer.write_UI64(static_cast<std::uint64_t>(packet.entity));
+ write_buffer.write_FP32(packet.value.x);
+ write_buffer.write_FP32(packet.value.y);
+ write_buffer.write_FP32(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_UI16(protocol::SpawnPlayer::ID);
+ write_buffer.write_UI64(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_UI16(protocol::ChatMessage::ID);
+ write_buffer.write_UI16(packet.type);
+ write_buffer.write_string(packet.sender.substr(0, protocol::MAX_USERNAME));
+ write_buffer.write_string(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_UI16(protocol::SetVoxel::ID);
+ write_buffer.write_I64(packet.vpos.x);
+ write_buffer.write_I64(packet.vpos.y);
+ write_buffer.write_I64(packet.vpos.z);
+ write_buffer.write_UI16(packet.voxel);
+ write_buffer.write_UI16(packet.flags);
+ return write_buffer.to_packet(flags);
+}
+
+ENetPacket *protocol::encode(const protocol::RemoveEntity &packet, enet_uint32 flags)
+{
+ write_buffer.reset();
+ write_buffer.write_UI16(protocol::RemoveEntity::ID);
+ write_buffer.write_UI64(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_UI16(protocol::EntityPlayer::ID);
+ write_buffer.write_UI64(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_UI16(protocol::ScoreboardUpdate::ID);
+ write_buffer.write_UI16(static_cast<std::uint16_t>(packet.names.size()));
+ for(const std::string &username : packet.names)
+ write_buffer.write_string(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_UI16(protocol::RequestChunk::ID);
+ write_buffer.write_I32(packet.cpos.x);
+ write_buffer.write_I32(packet.cpos.y);
+ write_buffer.write_I32(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_UI16(protocol::GenericSound::ID);
+ write_buffer.write_string(packet.sound.substr(0, protocol::MAX_SOUNDNAME));
+ write_buffer.write_UI8(packet.looping);
+ write_buffer.write_FP32(packet.pitch);
+ return write_buffer.to_packet(flags);
+}
+
+ENetPacket *protocol::encode(const protocol::EntitySound &packet, enet_uint32 flags)
+{
+ write_buffer.reset();
+ write_buffer.write_UI16(protocol::EntitySound::ID);
+ write_buffer.write_UI64(static_cast<std::uint64_t>(packet.entity));
+ write_buffer.write_string(packet.sound.substr(0, protocol::MAX_SOUNDNAME));
+ write_buffer.write_UI8(packet.looping);
+ write_buffer.write_FP32(packet.pitch);
+ return write_buffer.to_packet(flags);
+}
+
+ENetPacket *protocol::encode(const protocol::DimensionInfo &packet, enet_uint32 flags)
+{
+ write_buffer.reset();
+ write_buffer.write_UI16(protocol::DimensionInfo::ID);
+ write_buffer.write_string(packet.name);
+ write_buffer.write_FP32(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)
+ continue;
+ 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_UI16();
+
+ switch(id) {
+ case protocol::StatusRequest::ID:
+ status_request.peer = peer;
+ status_request.version = read_buffer.read_UI32();
+ dispatcher.trigger(status_request);
+ break;
+ case protocol::StatusResponse::ID:
+ status_response.peer = peer;
+ status_response.version = read_buffer.read_UI32();
+ status_response.max_players = read_buffer.read_UI16();
+ status_response.num_players = read_buffer.read_UI16();
+ status_response.motd = read_buffer.read_string();
+ dispatcher.trigger(status_response);
+ break;
+ case protocol::LoginRequest::ID:
+ login_request.peer = peer;
+ login_request.version = read_buffer.read_UI32();
+ login_request.voxel_def_checksum = read_buffer.read_UI64();
+ login_request.item_def_checksum = read_buffer.read_UI64();
+ login_request.password_hash = read_buffer.read_UI64();
+ login_request.username = read_buffer.read_string();
+ dispatcher.trigger(login_request);
+ break;
+ case protocol::LoginResponse::ID:
+ login_response.peer = peer;
+ login_response.client_index = read_buffer.read_UI16();
+ login_response.client_identity = read_buffer.read_UI64();
+ login_response.server_tickrate = read_buffer.read_UI16();
+ dispatcher.trigger(login_response);
+ break;
+ case protocol::Disconnect::ID:
+ disconnect.peer = peer;
+ disconnect.reason = read_buffer.read_string();
+ dispatcher.trigger(disconnect);
+ break;
+ case protocol::ChunkVoxels::ID:
+ chunk_voxels.peer = peer;
+ chunk_voxels.chunk.x = read_buffer.read_I32();
+ chunk_voxels.chunk.y = read_buffer.read_I32();
+ chunk_voxels.chunk.z = read_buffer.read_I32();
+ 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_UI64());
+ entity_transform.chunk.x = read_buffer.read_I32();
+ entity_transform.chunk.y = read_buffer.read_I32();
+ entity_transform.chunk.z = read_buffer.read_I32();
+ entity_transform.local.x = read_buffer.read_FP32();
+ entity_transform.local.y = read_buffer.read_FP32();
+ entity_transform.local.z = read_buffer.read_FP32();
+ entity_transform.angles.x = read_buffer.read_FP32();
+ entity_transform.angles.y = read_buffer.read_FP32();
+ entity_transform.angles.z = read_buffer.read_FP32();
+ dispatcher.trigger(entity_transform);
+ break;
+ case protocol::EntityHead::ID:
+ entity_head.peer = peer;
+ entity_head.entity = static_cast<entt::entity>(read_buffer.read_UI64());
+ entity_head.angles[0] = read_buffer.read_FP32();
+ entity_head.angles[1] = read_buffer.read_FP32();
+ entity_head.angles[2] = read_buffer.read_FP32();
+ dispatcher.trigger(entity_head);
+ break;
+ case protocol::EntityVelocity::ID:
+ entity_velocity.peer = peer;
+ entity_velocity.entity = static_cast<entt::entity>(read_buffer.read_UI64());
+ entity_velocity.value.x = read_buffer.read_FP32();
+ entity_velocity.value.y = read_buffer.read_FP32();
+ entity_velocity.value.z = read_buffer.read_FP32();
+ dispatcher.trigger(entity_velocity);
+ break;
+ case protocol::SpawnPlayer::ID:
+ spawn_player.peer = peer;
+ spawn_player.entity = static_cast<entt::entity>(read_buffer.read_UI64());
+ dispatcher.trigger(spawn_player);
+ break;
+ case protocol::ChatMessage::ID:
+ chat_message.peer = peer;
+ chat_message.type = read_buffer.read_UI16();
+ chat_message.sender = read_buffer.read_string();
+ chat_message.message = read_buffer.read_string();
+ dispatcher.trigger(chat_message);
+ break;
+ case protocol::SetVoxel::ID:
+ set_voxel.peer = peer;
+ set_voxel.vpos.x = read_buffer.read_I64();
+ set_voxel.vpos.y = read_buffer.read_I64();
+ set_voxel.vpos.z = read_buffer.read_I64();
+ set_voxel.voxel = read_buffer.read_UI16();
+ set_voxel.flags = read_buffer.read_UI16();
+ dispatcher.trigger(set_voxel);
+ break;
+ case protocol::RemoveEntity::ID:
+ remove_entity.peer = peer;
+ remove_entity.entity = static_cast<entt::entity>(read_buffer.read_UI64());
+ dispatcher.trigger(remove_entity);
+ break;
+ case protocol::EntityPlayer::ID:
+ entity_player.peer = peer;
+ entity_player.entity = static_cast<entt::entity>(read_buffer.read_UI64());
+ dispatcher.trigger(entity_player);
+ break;
+ case protocol::ScoreboardUpdate::ID:
+ scoreboard_update.peer = peer;
+ scoreboard_update.names.resize(read_buffer.read_UI16());
+ for(std::size_t i = 0; i < scoreboard_update.names.size(); ++i)
+ scoreboard_update.names[i] = read_buffer.read_string();
+ dispatcher.trigger(scoreboard_update);
+ break;
+ case protocol::RequestChunk::ID:
+ request_chunk.peer = peer;
+ request_chunk.cpos.x = read_buffer.read_UI32();
+ request_chunk.cpos.y = read_buffer.read_UI32();
+ request_chunk.cpos.z = read_buffer.read_UI32();
+ dispatcher.trigger(request_chunk);
+ break;
+ case protocol::GenericSound::ID:
+ generic_sound.peer = peer;
+ generic_sound.sound = read_buffer.read_string();
+ generic_sound.looping = read_buffer.read_UI8();
+ generic_sound.pitch = read_buffer.read_FP32();
+ dispatcher.trigger(generic_sound);
+ break;
+ case protocol::EntitySound::ID:
+ entity_sound.peer = peer;
+ entity_sound.entity = static_cast<entt::entity>(read_buffer.read_UI64());
+ entity_sound.sound = read_buffer.read_string();
+ entity_sound.looping = read_buffer.read_UI8();
+ entity_sound.pitch = read_buffer.read_FP32();
+ dispatcher.trigger(entity_sound);
+ break;
+ case protocol::DimensionInfo::ID:
+ dimension_info.peer = peer;
+ dimension_info.name = read_buffer.read_string();
+ dimension_info.gravity = read_buffer.read_FP32();
+ dispatcher.trigger(dimension_info);
+ break;
+ }
+}
+
+ENetPacket *protocol::utils::make_disconnect(const char *reason, enet_uint32 flags)
+{
+ protocol::Disconnect packet;
+ packet.reason = std::string(reason);
+ return protocol::encode(packet, flags);
+}
+
+ENetPacket *protocol::utils::make_chat_message(const char *message, enet_uint32 flags)
+{
+ protocol::ChatMessage packet;
+ packet.type = protocol::ChatMessage::TEXT_MESSAGE;
+ packet.message = std::string(message);
+ return protocol::encode(packet, flags);
+}
+
+ENetPacket *protocol::utils::make_chunk_voxels(Dimension *dimension, entt::entity entity, enet_uint32 flags)
+{
+ if(auto component = dimension->chunks.try_get<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(Dimension *dimension, entt::entity entity, enet_uint32 flags)
+{
+ if(auto component = dimension->entities.try_get<HeadComponent>(entity)) {
+ protocol::EntityHead packet;
+ packet.entity = entity;
+ packet.angles = component->angles;
+ return protocol::encode(packet, flags);
+ }
+
+ return nullptr;
+}
+
+ENetPacket *protocol::utils::make_entity_transform(Dimension *dimension, entt::entity entity, enet_uint32 flags)
+{
+ if(auto component = dimension->entities.try_get<TransformComponent>(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(Dimension *dimension, entt::entity entity, enet_uint32 flags)
+{
+ if(auto component = dimension->entities.try_get<VelocityComponent>(entity)) {
+ protocol::EntityVelocity packet;
+ packet.entity = entity;
+ packet.value = component->value;
+ return protocol::encode(packet, flags);
+ }
+
+ return nullptr;
+}
+
+ENetPacket *protocol::utils::make_entity_player(Dimension *dimension, entt::entity entity, enet_uint32 flags)
+{
+ if(dimension->entities.any_of<PlayerComponent>(entity)) {
+ protocol::EntityPlayer packet;
+ packet.entity = entity;
+ return protocol::encode(packet, flags);
+ }
+
+ return nullptr;
+}
+
+ENetPacket *protocol::utils::make_dimension_info(const 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/game/shared/protocol.hh b/game/shared/protocol.hh
new file mode 100644
index 0000000..0b8b0f1
--- /dev/null
+++ b/game/shared/protocol.hh
@@ -0,0 +1,213 @@
+#ifndef SHARED_PROTOCOL_HH
+#define SHARED_PROTOCOL_HH 1
+#pragma once
+
+#include "shared/chunk.hh"
+
+class Dimension;
+
+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::uint32_t VERSION = 15;
+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(const char *reason, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE);
+ENetPacket *make_chat_message(const char *message, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE);
+} // namespace protocol::utils
+
+namespace protocol::utils
+{
+ENetPacket *make_chunk_voxels(Dimension *dimension, entt::entity entity, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE);
+} // namespace protocol::utils
+
+namespace protocol::utils
+{
+ENetPacket *make_entity_head(Dimension *dimension, entt::entity entity, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE);
+ENetPacket *make_entity_transform(Dimension *dimension, entt::entity entity, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE);
+ENetPacket *make_entity_velocity(Dimension *dimension, entt::entity entity, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE);
+ENetPacket *make_entity_player(Dimension *dimension, entt::entity entity, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE);
+ENetPacket *make_dimension_info(const Dimension *dimension);
+} // namespace protocol::utils
+
+struct protocol::StatusRequest final : public protocol::Base<0x0000> {
+ std::uint32_t version;
+};
+
+struct protocol::StatusResponse final : public protocol::Base<0x0001> {
+ std::uint32_t version;
+ std::uint16_t max_players;
+ std::uint16_t num_players;
+ std::string motd;
+};
+
+struct protocol::LoginRequest final : public protocol::Base<0x0002> {
+ std::uint32_t version;
+ std::uint64_t voxel_def_checksum;
+ std::uint64_t item_def_checksum;
+ std::uint64_t password_hash;
+ std::string username;
+};
+
+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;
+ 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;
+};
+
+#endif /* SHARED_PROTOCOL_HH */
diff --git a/game/shared/ray_dda.cc b/game/shared/ray_dda.cc
new file mode 100644
index 0000000..132d05a
--- /dev/null
+++ b/game/shared/ray_dda.cc
@@ -0,0 +1,102 @@
+#include "shared/pch.hh"
+#include "shared/ray_dda.hh"
+
+#include "shared/coord.hh"
+#include "shared/dimension.hh"
+
+RayDDA::RayDDA(const Dimension *dimension, const chunk_pos &start_chunk, const glm::fvec3 &start_fpos, const glm::fvec3 &direction)
+{
+ reset(dimension, start_chunk, start_fpos, direction);
+}
+
+RayDDA::RayDDA(const Dimension &dimension, const chunk_pos &start_chunk, const glm::fvec3 &start_fpos, const glm::fvec3 &direction)
+{
+ reset(dimension, start_chunk, start_fpos, direction);
+}
+
+void RayDDA::reset(const 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 ? cxpr::abs(1.0f / direction.x) : std::numeric_limits<float>::max();
+ this->delta_dist.y = direction.y ? cxpr::abs(1.0f / direction.y) : std::numeric_limits<float>::max();
+ this->delta_dist.z = direction.z ? cxpr::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 RayDDA::reset(const Dimension &dimension, const chunk_pos &start_chunk, const glm::fvec3 &start_fpos, const glm::fvec3 &direction)
+{
+ reset(&dimension, start_chunk, start_fpos, direction);
+}
+
+voxel_id 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/game/shared/ray_dda.hh b/game/shared/ray_dda.hh
new file mode 100644
index 0000000..9b4374e
--- /dev/null
+++ b/game/shared/ray_dda.hh
@@ -0,0 +1,35 @@
+#ifndef SHARED_RAY_DDA
+#define SHARED_RAY_DDA 1
+#pragma once
+
+#include "shared/types.hh"
+
+class Dimension;
+
+class RayDDA final {
+public:
+ explicit 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);
+
+ voxel_id 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;
+};
+
+#endif /* SHARED_RAY_DDA */
diff --git a/game/shared/splash.cc b/game/shared/splash.cc
new file mode 100644
index 0000000..ec7b3c8
--- /dev/null
+++ b/game/shared/splash.cc
@@ -0,0 +1,34 @@
+#include "shared/pch.hh"
+#include "shared/splash.hh"
+
+constexpr static const char *SPLASHES_FILENAME = "misc/splashes.txt";
+
+static std::mt19937_64 splash_random;
+static std::vector<std::string> splash_lines;
+
+void splash::init(void)
+{
+ if(auto file = PHYSFS_openRead(SPLASHES_FILENAME)) {
+ 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(line);
+ }
+
+ splash_random.seed(std::random_device()());
+ }
+ else {
+ splash_lines.push_back(fmt::format("{}: {}", SPLASHES_FILENAME, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())));
+ }
+}
+
+const char* splash::get(void)
+{
+ std::uniform_int_distribution<std::size_t> dist(0, splash_lines.size() - 1);
+ return splash_lines.at(dist(splash_random)).c_str();
+}
diff --git a/game/shared/splash.hh b/game/shared/splash.hh
new file mode 100644
index 0000000..2a370f9
--- /dev/null
+++ b/game/shared/splash.hh
@@ -0,0 +1,11 @@
+#ifndef SHARED_SPLASH_HH
+#define SHARED_SPLASH_HH 1
+#pragma once
+
+namespace splash
+{
+void init(void);
+const char* get(void);
+} // namespace splash
+
+#endif /* SHARED_SPLASH_HH */
diff --git a/game/shared/stasis.cc b/game/shared/stasis.cc
new file mode 100644
index 0000000..4e474af
--- /dev/null
+++ b/game/shared/stasis.cc
@@ -0,0 +1,16 @@
+#include "shared/pch.hh"
+#include "shared/stasis.hh"
+
+#include "shared/dimension.hh"
+#include "shared/transform.hh"
+
+void StasisComponent::fixed_update(Dimension *dimension)
+{
+ auto view = dimension->entities.view<TransformComponent>();
+
+ for(auto [entity, transform] : view.each()) {
+ if(dimension->find_chunk(transform.chunk))
+ dimension->entities.remove<StasisComponent>(entity);
+ else dimension->entities.emplace_or_replace<StasisComponent>(entity);
+ }
+}
diff --git a/game/shared/stasis.hh b/game/shared/stasis.hh
new file mode 100644
index 0000000..6a3e280
--- /dev/null
+++ b/game/shared/stasis.hh
@@ -0,0 +1,14 @@
+#ifndef SHARED_STASIS_HH
+#define SHARED_STASIS_HH 1
+#pragma once
+
+class Dimension;
+
+// Attached to entities with transform values
+// out of bounds in a specific dimension
+struct StasisComponent final {
+public:
+ static void fixed_update(Dimension *dimension);
+};
+
+#endif /* SHARED_STASIS_HH */
diff --git a/game/shared/threading.cc b/game/shared/threading.cc
new file mode 100644
index 0000000..06a9411
--- /dev/null
+++ b/game/shared/threading.cc
@@ -0,0 +1,91 @@
+#include "shared/pch.hh"
+#include "shared/threading.hh"
+
+#include "core/cmdline.hh"
+#include "core/constexpr.hh"
+
+static BS::light_thread_pool *thread_pool;
+static std::deque<Task *> task_deque;
+
+static void task_process(Task *task)
+{
+ task->set_status(task_status::PROCESSING);
+ task->process();
+
+ if(task->get_status() == task_status::PROCESSING) {
+ // If the task status is still PROCESSING
+ // it can be deduced it hasn't been cancelled
+ task->set_status(task_status::COMPLETED);
+ }
+}
+
+task_status Task::get_status(void) const
+{
+ return m_status;
+}
+
+void Task::set_status(task_status status)
+{
+ m_status = status;
+}
+
+void threading::init(void)
+{
+ auto threads_arg = cmdline::get("threads", "4");
+ auto threads_num = cxpr::clamp<unsigned long>(std::strtoul(threads_arg, nullptr, 10), 2U, 4U);
+ thread_pool = new BS::light_thread_pool(threads_num);
+ task_deque.clear();
+}
+
+void threading::deinit(void)
+{
+ for(auto task : task_deque) {
+ auto status = task->get_status();
+ if((status != task_status::CANCELLED) || (status != task_status::COMPLETED)) {
+ task->set_status(task_status::CANCELLED);
+ }
+ }
+
+ thread_pool->purge();
+ thread_pool->wait();
+
+ for(auto task : task_deque)
+ delete task;
+ task_deque.clear();
+
+ delete thread_pool;
+}
+
+void threading::update(void)
+{
+ auto task_iter = task_deque.cbegin();
+
+ while(task_iter != task_deque.cend()) {
+ auto task_ptr = *task_iter;
+ auto status = task_ptr->get_status();
+
+ if(status == task_status::CANCELLED) {
+ delete task_ptr;
+ task_iter = task_deque.erase(task_iter);
+ continue;
+ }
+
+ if(status == task_status::COMPLETED) {
+ task_ptr->finalize();
+ delete task_ptr;
+ task_iter = task_deque.erase(task_iter);
+ continue;
+ }
+
+ task_iter = std::next(task_iter);
+ }
+}
+
+void threading::submit(Task *task)
+{
+ task->set_status(task_status::ENQUEUED);
+
+ static_cast<void>(thread_pool->submit_task(std::bind(&task_process, task)));
+
+ task_deque.push_back(task);
+}
diff --git a/game/shared/threading.hh b/game/shared/threading.hh
new file mode 100644
index 0000000..e143f4f
--- /dev/null
+++ b/game/shared/threading.hh
@@ -0,0 +1,46 @@
+#ifndef SHARED_THREADING_HH
+#define SHARED_THREADING_HH 1
+#pragma once
+
+enum class task_status : unsigned int {
+ ENQUEUED = 0x0000U,
+ PROCESSING = 0x0001U,
+ COMPLETED = 0x0002U,
+ CANCELLED = 0x0004U,
+};
+
+class Task {
+public:
+ virtual ~Task(void) = default;
+ virtual void process(void) = 0;
+ virtual void finalize(void) = 0;
+
+ task_status get_status(void) const;
+ void set_status(task_status status);
+
+protected:
+ std::atomic<task_status> m_status;
+ std::future<void> m_future;
+};
+
+namespace threading
+{
+void init(void);
+void deinit(void);
+void update(void);
+void submit(Task *task);
+} // namespace threading
+
+namespace threading
+{
+template<typename T, typename... AT>
+void submit(AT &&... args);
+} // namespace threading
+
+template<typename T, typename... AT>
+inline void threading::submit(AT &&... args)
+{
+ threading::submit(new T(args...));
+}
+
+#endif /* SHARED_THREADING_HH */
diff --git a/game/shared/transform.cc b/game/shared/transform.cc
new file mode 100644
index 0000000..6dc6126
--- /dev/null
+++ b/game/shared/transform.cc
@@ -0,0 +1,31 @@
+#include "shared/pch.hh"
+#include "shared/transform.hh"
+
+#include "shared/const.hh"
+#include "shared/dimension.hh"
+
+constexpr inline static void update_component(unsigned int dim, TransformComponent &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 TransformComponent::fixed_update(Dimension *dimension)
+{
+ auto view = dimension->entities.view<TransformComponent>();
+
+ for(auto [entity, transform] : view.each()) {
+ update_component(0U, transform);
+ update_component(1U, transform);
+ update_component(2U, transform);
+ }
+}
diff --git a/game/shared/transform.hh b/game/shared/transform.hh
new file mode 100644
index 0000000..0c0cc51
--- /dev/null
+++ b/game/shared/transform.hh
@@ -0,0 +1,25 @@
+#ifndef SHARED_TRANSFORM_HH
+#define SHARED_TRANSFORM_HH 1
+#pragma once
+
+#include "shared/types.hh"
+
+class Dimension;
+
+struct TransformComponent {
+ chunk_pos chunk;
+ glm::fvec3 local;
+ glm::fvec3 angles;
+
+public:
+ // Updates TransformComponent values so that
+ // the local translation field is always within
+ // local coodrinates; [floating-point precision]
+ static void fixed_update(Dimension *dimension);
+};
+
+// Client-side only - interpolated and previous transform
+struct TransformComponentIntr final : public TransformComponent {};
+struct TransformComponentPrev final : public TransformComponent {};
+
+#endif /* SHARED_TRANSFORM_HH */
diff --git a/game/shared/types.hh b/game/shared/types.hh
new file mode 100644
index 0000000..dace85e
--- /dev/null
+++ b/game/shared/types.hh
@@ -0,0 +1,44 @@
+#ifndef SHARED_TYPES_HH
+#define SHARED_TYPES_HH 1
+#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>;
+
+// A special 2D chunk coordinate used by world generation code
+// to cache things like 2D noise and terrain heightmap for performance reasons
+using worldgen_chunk_pos = glm::vec<2, chunk_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<worldgen_chunk_pos> final {
+ constexpr inline std::size_t operator()(const worldgen_chunk_pos &cwpos) const
+ {
+ std::size_t value = 0;
+ value ^= cwpos.x * 73856093;
+ value ^= cwpos.y * 19349663;
+ return value;
+ }
+};
+
+#endif /* SHARED_TYPES_HH */
diff --git a/game/shared/velocity.cc b/game/shared/velocity.cc
new file mode 100644
index 0000000..6305363
--- /dev/null
+++ b/game/shared/velocity.cc
@@ -0,0 +1,16 @@
+#include "shared/pch.hh"
+#include "shared/velocity.hh"
+
+#include "shared/dimension.hh"
+#include "shared/globals.hh"
+#include "shared/stasis.hh"
+#include "shared/transform.hh"
+
+void VelocityComponent::fixed_update(Dimension *dimension)
+{
+ auto group = dimension->entities.group<VelocityComponent>(entt::get<TransformComponent>, entt::exclude<StasisComponent>);
+
+ for(auto [entity, velocity, transform] : group.each()) {
+ transform.local += velocity.value * globals::fixed_frametime;
+ }
+}
diff --git a/game/shared/velocity.hh b/game/shared/velocity.hh
new file mode 100644
index 0000000..9aafed1
--- /dev/null
+++ b/game/shared/velocity.hh
@@ -0,0 +1,17 @@
+#ifndef SHARED_VELOCITY_HH
+#define SHARED_VELOCITY_HH 1
+#pragma once
+
+class Dimension;
+
+struct VelocityComponent final {
+ glm::fvec3 value;
+
+public:
+ // Updates entities TransformComponent values
+ // according to velocities multiplied by fixed_frametime.
+ // NOTE: This system was previously called inertial
+ static void fixed_update(Dimension *dimension);
+};
+
+#endif /* SHARED_VELOCITY_HH */
diff --git a/game/shared/voxel_registry.cc b/game/shared/voxel_registry.cc
new file mode 100644
index 0000000..dea7179
--- /dev/null
+++ b/game/shared/voxel_registry.cc
@@ -0,0 +1,184 @@
+#include "shared/pch.hh"
+#include "shared/voxel_registry.hh"
+
+#include "core/crc64.hh"
+
+std::unordered_map<std::string, VoxelInfoBuilder> voxel_registry::builders = {};
+std::unordered_map<std::string, voxel_id> voxel_registry::names = {};
+std::vector<std::shared_ptr<VoxelInfo>> voxel_registry::voxels = {};
+
+VoxelInfoBuilder::VoxelInfoBuilder(const char *name, voxel_type type, bool animated, bool blending)
+{
+ prototype.name = name;
+ prototype.type = type;
+ prototype.animated = animated;
+ prototype.blending = blending;
+
+ switch(type) {
+ case voxel_type::CUBE:
+ prototype.textures.resize(static_cast<std::size_t>(voxel_face::CUBE__NR));
+ break;
+ case voxel_type::CROSS:
+ prototype.textures.resize(static_cast<std::size_t>(voxel_face::CROSS__NR));
+ break;
+ case voxel_type::MODEL:
+ // Custom models should use a different texture
+ // resource management that is not a voxel atlas
+ // TODO: actually implement custom models lol
+ prototype.textures.resize(0);
+ break;
+ default:
+ // Something really bad should happen if we end up here.
+ // The outside code would static_cast an int to VoxelType
+ // and possibly fuck a lot of things up to cause this
+ spdlog::critical("voxel_registry: {}: unknown voxel type {}", name, static_cast<int>(type));
+ std::terminate();
+ }
+
+ // Physics properties
+ prototype.touch_type = voxel_touch::SOLID;
+ prototype.touch_values = glm::fvec3(0.0f, 0.0f, 0.0f);
+ prototype.surface = voxel_surface::DEFAULT;
+
+ // Things set in future by item_def
+ prototype.item_pick = NULL_ITEM_ID;
+}
+
+VoxelInfoBuilder &VoxelInfoBuilder::add_texture_default(const char *texture)
+{
+ default_texture.paths.push_back(texture);
+ return *this;
+}
+
+VoxelInfoBuilder &VoxelInfoBuilder::add_texture(voxel_face face, const char *texture)
+{
+ const auto index = static_cast<std::size_t>(face);
+ prototype.textures[index].paths.push_back(texture);
+ return *this;
+}
+
+VoxelInfoBuilder &VoxelInfoBuilder::set_touch(voxel_touch type, const glm::fvec3 &values)
+{
+ prototype.touch_type = type;
+ prototype.touch_values = values;
+ return *this;
+}
+
+VoxelInfoBuilder &VoxelInfoBuilder::set_surface(voxel_surface surface)
+{
+ prototype.surface = surface;
+ return *this;
+}
+
+voxel_id VoxelInfoBuilder::build(void) const
+{
+ const auto it = voxel_registry::names.find(prototype.name);
+
+ if(it != voxel_registry::names.cend()) {
+ spdlog::warn("voxel_registry: cannot build {}: name already present", prototype.name);
+ return it->second;
+ }
+
+ std::size_t state_count;
+
+ switch(prototype.type) {
+ case voxel_type::CUBE:
+ case voxel_type::CROSS:
+ case voxel_type::MODEL:
+ state_count = 1;
+ break;
+ default:
+ // Something really bad should happen if we end up here.
+ // The outside code would static_cast an int to VoxelType
+ // and possibly fuck a lot of things up to cause this
+ spdlog::critical("voxel_registry: {}: unknown voxel type {}", prototype.name, static_cast<int>(prototype.type));
+ std::terminate();
+ }
+
+ if((voxel_registry::voxels.size() + state_count) >= MAX_VOXEL_ID) {
+ spdlog::critical("voxel_registry: voxel registry overflow");
+ std::terminate();
+ }
+
+ auto new_info = std::make_shared<VoxelInfo>();
+ new_info->name = prototype.name;
+ new_info->type = prototype.type;
+ new_info->animated = prototype.animated;
+ new_info->blending = prototype.blending;
+
+ new_info->textures.resize(prototype.textures.size());
+
+ for(std::size_t i = 0; i < prototype.textures.size(); ++i) {
+ if(prototype.textures[i].paths.empty()) {
+ new_info->textures[i].paths = default_texture.paths;
+ new_info->textures[i].cached_offset = SIZE_MAX;
+ new_info->textures[i].cached_plane = SIZE_MAX;
+ }
+ else {
+ new_info->textures[i].paths = prototype.textures[i].paths;
+ new_info->textures[i].cached_offset = SIZE_MAX;
+ new_info->textures[i].cached_plane = SIZE_MAX;
+ }
+ }
+
+ // Physics properties
+ new_info->touch_type = prototype.touch_type;
+ new_info->touch_values = prototype.touch_values;
+ new_info->surface = prototype.surface;
+
+ // Things set in future by item_def
+ new_info->item_pick = prototype.item_pick;
+
+ // Base voxel identifier offset
+ new_info->base_voxel = voxel_registry::voxels.size() + 1;
+
+ for(std::size_t i = 0; i < state_count; ++i)
+ voxel_registry::voxels.push_back(new_info);
+ voxel_registry::names.insert_or_assign(new_info->name, new_info->base_voxel);
+
+ return new_info->base_voxel;
+}
+
+VoxelInfoBuilder &voxel_registry::construct(const char *name, voxel_type type, bool animated, bool blending)
+{
+ const auto it = voxel_registry::builders.find(name);
+ if(it != voxel_registry::builders.cend())
+ return it->second;
+ return voxel_registry::builders.emplace(name, VoxelInfoBuilder(name, type, animated, blending)).first->second;
+}
+
+VoxelInfo *voxel_registry::find(const char *name)
+{
+ const auto it = voxel_registry::names.find(name);
+ if(it != voxel_registry::names.cend())
+ return voxel_registry::find(it->second);
+ return nullptr;
+}
+
+VoxelInfo *voxel_registry::find(const voxel_id voxel)
+{
+ if((voxel != NULL_VOXEL_ID) && (voxel <= voxel_registry::voxels.size()))
+ return voxel_registry::voxels[voxel - 1].get();
+ return nullptr;
+}
+
+void voxel_registry::purge(void)
+{
+ voxel_registry::builders.clear();
+ voxel_registry::names.clear();
+ voxel_registry::voxels.clear();
+}
+
+std::uint64_t voxel_registry::checksum(void)
+{
+ std::uint64_t result = 0;
+
+ for(const std::shared_ptr<VoxelInfo> &info : voxel_registry::voxels) {
+ result = crc64::get(info->name, result);
+ result += static_cast<std::uint64_t>(info->type);
+ result += static_cast<std::uint64_t>(info->base_voxel);
+ result += info->blending ? 256 : 1;
+ }
+
+ return result;
+}
diff --git a/game/shared/voxel_registry.hh b/game/shared/voxel_registry.hh
new file mode 100644
index 0000000..722cec1
--- /dev/null
+++ b/game/shared/voxel_registry.hh
@@ -0,0 +1,144 @@
+#ifndef SHARED_VOXEL_REGISTRY_HH
+#define SHARED_VOXEL_REGISTRY_HH 1
+#pragma once
+
+#include "shared/types.hh"
+
+enum class voxel_face : unsigned short {
+ CUBE_NORTH = 0x0000,
+ CUBE_SOUTH = 0x0001,
+ CUBE_EAST = 0x0002,
+ CUBE_WEST = 0x0003,
+ CUBE_TOP = 0x0004,
+ CUBE_BOTTOM = 0x0005,
+ CUBE__NR = 0x0006,
+
+ CROSS_NESW = 0x0000,
+ CROSS_NWSE = 0x0001,
+ CROSS__NR = 0x0002,
+};
+
+enum class voxel_type : unsigned short {
+ CUBE = 0x0000,
+ CROSS = 0x0001, // TODO
+ MODEL = 0x0002, // TODO
+};
+
+enum class voxel_facing : unsigned short {
+ NORTH = 0x0000,
+ SOUTH = 0x0001,
+ EAST = 0x0002,
+ WEST = 0x0003,
+ UP = 0x0004,
+ DOWN = 0x0005,
+ NESW = 0x0006,
+ NWSE = 0x0007,
+};
+
+enum class voxel_touch : unsigned short {
+ SOLID = 0x0000, // The entity is stopped in its tracks
+ BOUNCE = 0x0001, // The entity bounces back with some energy loss
+ SINK = 0x0002, // The entity phases/sinks through the voxel
+ NOTHING = 0xFFFF,
+};
+
+enum class voxel_surface : unsigned short {
+ DEFAULT = 0x0000,
+ STONE = 0x0001,
+ DIRT = 0x0002,
+ GLASS = 0x0003,
+ GRASS = 0x0004,
+ GRAVEL = 0x0005,
+ METAL = 0x0006,
+ SAND = 0x0007,
+ WOOD = 0x0008,
+ SLOSH = 0x0009,
+ COUNT = 0x000A,
+ UNKNOWN = 0xFFFF,
+};
+
+using voxel_vis = unsigned short;
+constexpr static voxel_vis VIS_NORTH = 1 << static_cast<unsigned int>(voxel_facing::NORTH);
+constexpr static voxel_vis VIS_SOUTH = 1 << static_cast<unsigned int>(voxel_facing::SOUTH);
+constexpr static voxel_vis VIS_EAST = 1 << static_cast<unsigned int>(voxel_facing::EAST);
+constexpr static voxel_vis VIS_WEST = 1 << static_cast<unsigned int>(voxel_facing::WEST);
+constexpr static voxel_vis VIS_UP = 1 << static_cast<unsigned int>(voxel_facing::UP);
+constexpr static voxel_vis VIS_DOWN = 1 << static_cast<unsigned int>(voxel_facing::DOWN);
+
+struct VoxelTexture final {
+ std::vector<std::string> paths;
+ std::size_t cached_offset; // client-side only
+ std::size_t cached_plane; // client-side only
+};
+
+struct VoxelInfo final {
+ std::string name;
+ voxel_type type;
+ bool animated;
+ bool blending;
+
+ std::vector<VoxelTexture> textures;
+
+ // Physics properties go here
+ // TODO: player_move friction modifiers
+ // that would make the voxel very sticky or
+ // very slippery to walk on
+ voxel_touch touch_type;
+ glm::fvec3 touch_values;
+ voxel_surface surface;
+
+ // Some voxel types might occupy multiple voxel_id
+ // values that reference to the exact same VoxelInfo
+ // structure; the actual numeric state is figured out by
+ // subtracting base_voxel from the checking voxel_id
+ voxel_id base_voxel;
+
+ // These will be set by item_registry
+ // and by default set to NULL_ITEM_ID
+ item_id item_pick;
+};
+
+class VoxelInfoBuilder final {
+public:
+ explicit VoxelInfoBuilder(const char *name, voxel_type type, bool animated, bool blending);
+ virtual ~VoxelInfoBuilder(void) = default;
+
+public:
+ VoxelInfoBuilder &add_texture_default(const char *texture);
+ VoxelInfoBuilder &add_texture(voxel_face face, const char *texture);
+ VoxelInfoBuilder &set_touch(voxel_touch type, const glm::fvec3 &values);
+ VoxelInfoBuilder &set_surface(voxel_surface surface);
+
+public:
+ voxel_id build(void) const;
+
+private:
+ VoxelTexture default_texture;
+ VoxelInfo prototype;
+};
+
+namespace voxel_registry
+{
+extern std::unordered_map<std::string, VoxelInfoBuilder> builders;
+extern std::unordered_map<std::string, voxel_id> names;
+extern std::vector<std::shared_ptr<VoxelInfo>> voxels;
+} // namespace voxel_registry
+
+namespace voxel_registry
+{
+VoxelInfoBuilder &construct(const char *name, voxel_type type, bool animated, bool blending);
+VoxelInfo *find(const char *name);
+VoxelInfo *find(const voxel_id voxel);
+} // namespace voxel_registry
+
+namespace voxel_registry
+{
+void purge(void);
+} // namespace voxel_registry
+
+namespace voxel_registry
+{
+std::uint64_t checksum(void);
+} // namespace voxel_registry
+
+#endif /* SHARED_VOXEL_REGISTRY_HH */
diff --git a/game/shared/voxel_storage.cc b/game/shared/voxel_storage.cc
new file mode 100644
index 0000000..eb51347
--- /dev/null
+++ b/game/shared/voxel_storage.cc
@@ -0,0 +1,43 @@
+#include "shared/pch.hh"
+#include "shared/voxel_storage.hh"
+
+#include "core/buffer.hh"
+
+void VoxelStorage::serialize(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_UI64(bound);
+
+ // Write all the compressed data into the buffer
+ for(std::size_t i = 0; i < bound; buffer.write_UI8(zdata[i++]));
+}
+
+void VoxelStorage::deserialize(ReadBuffer &buffer)
+{
+ auto size = static_cast<mz_ulong>(sizeof(VoxelStorage));
+ auto bound = static_cast<mz_ulong>(buffer.read_UI64());
+ 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_UI8());
+
+ 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/game/shared/voxel_storage.hh b/game/shared/voxel_storage.hh
new file mode 100644
index 0000000..08d234d
--- /dev/null
+++ b/game/shared/voxel_storage.hh
@@ -0,0 +1,18 @@
+#ifndef SHARED_VOXEL_STORAGE_HH
+#define SHARED_VOXEL_STORAGE_HH 1
+#pragma once
+
+#include "shared/const.hh"
+#include "shared/types.hh"
+
+class ReadBuffer;
+class WriteBuffer;
+
+class VoxelStorage final : public std::array<voxel_id, CHUNK_VOLUME> {
+public:
+ using std::array<voxel_id, CHUNK_VOLUME>::array;
+ void serialize(WriteBuffer &buffer) const;
+ void deserialize(ReadBuffer &buffer);
+};
+
+#endif /* SHARED_VOXEL_STORAGE_HH */