summaryrefslogtreecommitdiffstats
path: root/src/game/shared/world
diff options
context:
space:
mode:
authoruntodesu <kirill@untode.su>2025-12-11 15:14:26 +0500
committeruntodesu <kirill@untode.su>2025-12-11 15:14:26 +0500
commitf40d09cb8f712e87691af4912f3630d92d692779 (patch)
tree7ac3a4168ff722689372fd489c6f94d0a2546e8f /src/game/shared/world
parent8bcbd2729388edc63c82d77d314b583af1447c49 (diff)
downloadvoxelius-f40d09cb8f712e87691af4912f3630d92d692779.tar.bz2
voxelius-f40d09cb8f712e87691af4912f3630d92d692779.zip
Shuffle stuff around
- Use the new and improved hierarchy I figured out when making Prospero chat - Re-add NSIS scripts, again from Prospero - Update most build and utility scripts with their most recent versions
Diffstat (limited to 'src/game/shared/world')
-rw-r--r--src/game/shared/world/CMakeLists.txt20
-rw-r--r--src/game/shared/world/chunk.cc72
-rw-r--r--src/game/shared/world/chunk.hh43
-rw-r--r--src/game/shared/world/chunk_aabb.hh10
-rw-r--r--src/game/shared/world/dimension.cc187
-rw-r--r--src/game/shared/world/dimension.hh101
-rw-r--r--src/game/shared/world/feature.cc75
-rw-r--r--src/game/shared/world/feature.hh25
-rw-r--r--src/game/shared/world/item.cc55
-rw-r--r--src/game/shared/world/item.hh84
-rw-r--r--src/game/shared/world/item_registry.cc68
-rw-r--r--src/game/shared/world/item_registry.hh26
-rw-r--r--src/game/shared/world/ray_dda.cc107
-rw-r--r--src/game/shared/world/ray_dda.hh38
-rw-r--r--src/game/shared/world/voxel.cc149
-rw-r--r--src/game/shared/world/voxel.hh286
-rw-r--r--src/game/shared/world/voxel_registry.cc64
-rw-r--r--src/game/shared/world/voxel_registry.hh26
-rw-r--r--src/game/shared/world/voxel_storage.cc48
-rw-r--r--src/game/shared/world/voxel_storage.hh20
20 files changed, 1504 insertions, 0 deletions
diff --git a/src/game/shared/world/CMakeLists.txt b/src/game/shared/world/CMakeLists.txt
new file mode 100644
index 0000000..db3f370
--- /dev/null
+++ b/src/game/shared/world/CMakeLists.txt
@@ -0,0 +1,20 @@
+target_sources(shared PRIVATE
+ "${CMAKE_CURRENT_LIST_DIR}/chunk_aabb.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/chunk.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/chunk.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/dimension.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/dimension.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/feature.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/feature.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/item_registry.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/item_registry.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/item.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/item.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/ray_dda.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/ray_dda.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/voxel_registry.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/voxel_registry.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/voxel_storage.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/voxel_storage.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/voxel.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/voxel.hh")
diff --git a/src/game/shared/world/chunk.cc b/src/game/shared/world/chunk.cc
new file mode 100644
index 0000000..e2d60cb
--- /dev/null
+++ b/src/game/shared/world/chunk.cc
@@ -0,0 +1,72 @@
+#include "shared/pch.hh"
+
+#include "shared/world/chunk.hh"
+
+#include "shared/world/voxel_registry.hh"
+
+#include "shared/coord.hh"
+
+world::Chunk::Chunk(entt::entity entity, Dimension* dimension)
+{
+ m_entity = entity;
+ m_dimension = dimension;
+ m_voxels.fill(NULL_VOXEL_ID);
+ m_biome = BIOME_VOID;
+}
+
+const world::Voxel* world::Chunk::get_voxel(const local_pos& lpos) const
+{
+ return get_voxel(coord::to_index(lpos));
+}
+
+const world::Voxel* world::Chunk::get_voxel(const std::size_t index) const
+{
+ if(index >= CHUNK_VOLUME) {
+ return nullptr;
+ }
+
+ return voxel_registry::find(m_voxels[index]);
+}
+
+void world::Chunk::set_voxel(const Voxel* voxel, const local_pos& lpos)
+{
+ set_voxel(voxel, coord::to_index(lpos));
+}
+
+void world::Chunk::set_voxel(const Voxel* voxel, const std::size_t index)
+{
+ if(index < CHUNK_VOLUME) {
+ m_voxels[index] = voxel ? voxel->get_id() : NULL_VOXEL_ID;
+ return;
+ }
+}
+
+const world::VoxelStorage& world::Chunk::get_voxels(void) const
+{
+ return m_voxels;
+}
+
+void world::Chunk::set_voxels(const VoxelStorage& voxels)
+{
+ m_voxels = voxels;
+}
+
+unsigned int world::Chunk::get_biome(void) const
+{
+ return m_biome;
+}
+
+void world::Chunk::set_biome(unsigned int biome)
+{
+ m_biome = biome;
+}
+
+entt::entity world::Chunk::get_entity(void) const
+{
+ return m_entity;
+}
+
+world::Dimension* world::Chunk::get_dimension(void) const
+{
+ return m_dimension;
+}
diff --git a/src/game/shared/world/chunk.hh b/src/game/shared/world/chunk.hh
new file mode 100644
index 0000000..7518127
--- /dev/null
+++ b/src/game/shared/world/chunk.hh
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "shared/world/voxel_storage.hh"
+
+#include "shared/types.hh"
+
+constexpr static unsigned int BIOME_VOID = 0U;
+
+namespace world
+{
+class Dimension;
+class Voxel;
+} // namespace world
+
+namespace world
+{
+class Chunk final {
+public:
+ explicit Chunk(entt::entity entity, Dimension* dimension);
+ virtual ~Chunk(void) = default;
+
+ const Voxel* get_voxel(const local_pos& lpos) const;
+ const Voxel* get_voxel(const std::size_t index) const;
+
+ void set_voxel(const Voxel* voxel, const local_pos& lpos);
+ void set_voxel(const Voxel* voxel, const std::size_t index);
+
+ const VoxelStorage& get_voxels(void) const;
+ void set_voxels(const VoxelStorage& voxels);
+
+ unsigned int get_biome(void) const;
+ void set_biome(unsigned int biome);
+
+ entt::entity get_entity(void) const;
+ Dimension* get_dimension(void) const;
+
+private:
+ entt::entity m_entity;
+ Dimension* m_dimension;
+ VoxelStorage m_voxels;
+ unsigned int m_biome;
+};
+} // namespace world
diff --git a/src/game/shared/world/chunk_aabb.hh b/src/game/shared/world/chunk_aabb.hh
new file mode 100644
index 0000000..f07d3e1
--- /dev/null
+++ b/src/game/shared/world/chunk_aabb.hh
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "core/math/aabb.hh"
+
+#include "shared/types.hh"
+
+namespace world
+{
+using ChunkAABB = math::AABB<chunk_pos::value_type>;
+} // namespace world
diff --git a/src/game/shared/world/dimension.cc b/src/game/shared/world/dimension.cc
new file mode 100644
index 0000000..0088753
--- /dev/null
+++ b/src/game/shared/world/dimension.cc
@@ -0,0 +1,187 @@
+#include "shared/pch.hh"
+
+#include "shared/world/dimension.hh"
+
+#include "shared/world/chunk.hh"
+#include "shared/world/voxel_registry.hh"
+
+#include "shared/coord.hh"
+#include "shared/globals.hh"
+
+world::Dimension::Dimension(std::string_view name, float gravity)
+{
+ m_name = name;
+ m_gravity = gravity;
+}
+
+world::Dimension::~Dimension(void)
+{
+ for(const auto it : m_chunkmap)
+ delete it.second;
+ entities.clear();
+ chunks.clear();
+}
+
+std::string_view world::Dimension::get_name(void) const
+{
+ return m_name;
+}
+
+float world::Dimension::get_gravity(void) const
+{
+ return m_gravity;
+}
+
+world::Chunk* world::Dimension::create_chunk(const chunk_pos& cpos)
+{
+ auto it = m_chunkmap.find(cpos);
+
+ if(it != m_chunkmap.cend()) {
+ // Chunk already exists
+ return it->second;
+ }
+
+ auto entity = chunks.create();
+ auto chunk = new Chunk(entity, this);
+
+ auto& component = chunks.emplace<ChunkComponent>(entity);
+ component.chunk = chunk;
+ component.cpos = cpos;
+
+ ChunkCreateEvent event;
+ event.dimension = this;
+ event.chunk = chunk;
+ event.cpos = cpos;
+
+ globals::dispatcher.trigger(event);
+
+ return m_chunkmap.insert_or_assign(cpos, std::move(chunk)).first->second;
+}
+
+world::Chunk* world::Dimension::find_chunk(entt::entity entity) const
+{
+ if(chunks.valid(entity)) {
+ return chunks.get<ChunkComponent>(entity).chunk;
+ }
+ else {
+ return nullptr;
+ }
+}
+
+world::Chunk* world::Dimension::find_chunk(const chunk_pos& cpos) const
+{
+ auto it = m_chunkmap.find(cpos);
+
+ if(it != m_chunkmap.cend()) {
+ return it->second;
+ }
+ else {
+ return nullptr;
+ }
+}
+
+void world::Dimension::remove_chunk(entt::entity entity)
+{
+ if(chunks.valid(entity)) {
+ auto& component = chunks.get<ChunkComponent>(entity);
+ m_chunkmap.erase(component.cpos);
+ chunks.destroy(entity);
+ }
+}
+
+void world::Dimension::remove_chunk(const chunk_pos& cpos)
+{
+ auto it = m_chunkmap.find(cpos);
+
+ if(it != m_chunkmap.cend()) {
+ chunks.destroy(it->second->get_entity());
+ m_chunkmap.erase(it);
+ }
+}
+
+void world::Dimension::remove_chunk(Chunk* chunk)
+{
+ if(chunk) {
+ const auto& component = chunks.get<ChunkComponent>(chunk->get_entity());
+ m_chunkmap.erase(component.cpos);
+ chunks.destroy(chunk->get_entity());
+ }
+}
+
+const world::Voxel* world::Dimension::get_voxel(const voxel_pos& vpos) const
+{
+ auto cpos = coord::to_chunk(vpos);
+ auto lpos = coord::to_local(vpos);
+
+ if(auto chunk = find_chunk(cpos)) {
+ return chunk->get_voxel(lpos);
+ }
+
+ return nullptr;
+}
+
+const world::Voxel* world::Dimension::get_voxel(const chunk_pos& cpos, const local_pos& lpos) const
+{
+ // This allows accessing get_voxel with negative
+ // local coordinates that usually would result in an
+ // out-of-range values; this is useful for per-voxel update logic
+ return get_voxel(coord::to_voxel(cpos, lpos));
+}
+
+bool world::Dimension::set_voxel(const Voxel* voxel, const voxel_pos& vpos)
+{
+ auto cpos = coord::to_chunk(vpos);
+ auto lpos = coord::to_local(vpos);
+
+ if(auto chunk = find_chunk(cpos)) {
+ if(auto old_voxel = chunk->get_voxel(lpos)) {
+ if(old_voxel != voxel) {
+ // Notify the old voxel that it is
+ // being replaced with a different voxel
+ old_voxel->on_remove(this, vpos);
+ }
+ }
+
+ chunk->set_voxel(voxel, lpos);
+
+ if(voxel) {
+ // If we're not placing air, notify the
+ // new voxel that it has been placed
+ voxel->on_place(this, vpos);
+ }
+
+ VoxelSetEvent event;
+ event.dimension = this;
+ event.voxel = voxel;
+ event.cpos = cpos;
+ event.lpos = lpos;
+ event.chunk = chunk;
+
+ globals::dispatcher.trigger(event);
+
+ return true;
+ }
+
+ return false;
+}
+
+bool world::Dimension::set_voxel(const Voxel* voxel, const chunk_pos& cpos, const local_pos& lpos)
+{
+ // This allows accessing set_voxel with negative
+ // local coordinates that usually would result in an
+ // out-of-range values; this is useful for per-voxel update logic
+ return set_voxel(voxel, coord::to_voxel(cpos, lpos));
+}
+
+void world::Dimension::init(io::ConfigMap& config)
+{
+}
+
+void world::Dimension::init_late(std::uint64_t global_seed)
+{
+}
+
+bool world::Dimension::generate(const chunk_pos& cpos, VoxelStorage& voxels)
+{
+ return false;
+}
diff --git a/src/game/shared/world/dimension.hh b/src/game/shared/world/dimension.hh
new file mode 100644
index 0000000..6549bf6
--- /dev/null
+++ b/src/game/shared/world/dimension.hh
@@ -0,0 +1,101 @@
+#pragma once
+
+#include "shared/const.hh"
+#include "shared/types.hh"
+
+namespace io
+{
+class ConfigMap;
+} // namespace io
+
+namespace world
+{
+class Chunk;
+class Voxel;
+class VoxelStorage;
+} // namespace world
+
+namespace world
+{
+using dimension_entropy_map = std::array<std::uint64_t, CHUNK_AREA>;
+using dimension_height_map = std::array<voxel_pos::value_type, CHUNK_AREA>;
+} // namespace world
+
+namespace world
+{
+class Dimension {
+public:
+ explicit Dimension(std::string_view name, float gravity);
+ virtual ~Dimension(void);
+
+ std::string_view get_name(void) const;
+ float get_gravity(void) const;
+
+public:
+ Chunk* create_chunk(const chunk_pos& cpos);
+ Chunk* find_chunk(entt::entity entity) const;
+ Chunk* find_chunk(const chunk_pos& cpos) const;
+
+ void remove_chunk(entt::entity entity);
+ void remove_chunk(const chunk_pos& cpos);
+ void remove_chunk(Chunk* chunk);
+
+public:
+ const Voxel* get_voxel(const voxel_pos& vpos) const;
+ const Voxel* get_voxel(const chunk_pos& cpos, const local_pos& lpos) const;
+
+ bool set_voxel(const Voxel* voxel, const voxel_pos& vpos);
+ bool set_voxel(const Voxel* voxel, const chunk_pos& cpos, const local_pos& lpos);
+
+public:
+ virtual void init(io::ConfigMap& config);
+ virtual void init_late(std::uint64_t global_seed);
+ virtual bool generate(const chunk_pos& cpos, VoxelStorage& voxels);
+
+public:
+ entt::registry chunks;
+ entt::registry entities;
+
+private:
+ std::string m_name;
+ emhash8::HashMap<chunk_pos, Chunk*> m_chunkmap;
+ float m_gravity;
+};
+} // namespace world
+
+namespace world
+{
+struct ChunkComponent final {
+ chunk_pos cpos;
+ Chunk* chunk;
+};
+} // namespace world
+
+namespace world
+{
+struct ChunkCreateEvent final {
+ Dimension* dimension;
+ chunk_pos cpos;
+ Chunk* chunk;
+};
+
+struct ChunkDestroyEvent final {
+ Dimension* dimension;
+ chunk_pos cpos;
+ Chunk* chunk;
+};
+
+struct ChunkUpdateEvent final {
+ Dimension* dimension;
+ chunk_pos cpos;
+ Chunk* chunk;
+};
+
+struct VoxelSetEvent final {
+ Dimension* dimension;
+ const Voxel* voxel;
+ chunk_pos cpos;
+ local_pos lpos;
+ Chunk* chunk;
+};
+} // namespace world
diff --git a/src/game/shared/world/feature.cc b/src/game/shared/world/feature.cc
new file mode 100644
index 0000000..2468473
--- /dev/null
+++ b/src/game/shared/world/feature.cc
@@ -0,0 +1,75 @@
+#include "shared/pch.hh"
+
+#include "shared/world/feature.hh"
+
+#include "shared/world/chunk.hh"
+#include "shared/world/dimension.hh"
+#include "shared/world/voxel.hh"
+
+#include "shared/coord.hh"
+
+void world::Feature::place(const voxel_pos& vpos, Dimension* dimension) const
+{
+ for(const auto [rpos, voxel, overwrite] : (*this)) {
+ auto it_vpos = vpos + rpos;
+ auto it_cpos = coord::to_chunk(it_vpos);
+
+ if(auto chunk = dimension->create_chunk(it_cpos)) {
+ auto it_lpos = coord::to_local(it_vpos);
+ auto it_index = coord::to_index(it_lpos);
+
+ if(chunk->get_voxel(it_index) && !overwrite) {
+ // There is something in the way
+ // and the called intentionally requested
+ // we do not force feature to overwrite voxels
+ continue;
+ }
+
+ chunk->set_voxel(voxel, it_index);
+ }
+ }
+}
+
+void world::Feature::place(const voxel_pos& vpos, const chunk_pos& cpos, Chunk& chunk) const
+{
+ for(const auto [rpos, voxel, overwrite] : (*this)) {
+ auto it_vpos = vpos + rpos;
+ auto it_cpos = coord::to_chunk(it_vpos);
+
+ if(it_cpos == cpos) {
+ auto it_lpos = coord::to_local(it_vpos);
+ auto it_index = coord::to_index(it_lpos);
+
+ if(chunk.get_voxel(it_index) && !overwrite) {
+ // There is something in the way
+ // and the called intentionally requested
+ // we do not force feature to overwrite voxels
+ continue;
+ }
+
+ chunk.set_voxel(voxel, it_index);
+ }
+ }
+}
+
+void world::Feature::place(const voxel_pos& vpos, const chunk_pos& cpos, VoxelStorage& voxels) const
+{
+ for(const auto [rpos, voxel, overwrite] : (*this)) {
+ auto it_vpos = vpos + rpos;
+ auto it_cpos = coord::to_chunk(it_vpos);
+
+ if(it_cpos == cpos) {
+ auto it_lpos = coord::to_local(it_vpos);
+ auto it_index = coord::to_index(it_lpos);
+
+ if(voxels[it_index] && !overwrite) {
+ // There is something in the way
+ // and the called intentionally requested
+ // we do not force feature to overwrite voxels
+ continue;
+ }
+
+ voxels[it_index] = voxel ? voxel->get_id() : NULL_VOXEL_ID;
+ }
+ }
+}
diff --git a/src/game/shared/world/feature.hh b/src/game/shared/world/feature.hh
new file mode 100644
index 0000000..0d28b20
--- /dev/null
+++ b/src/game/shared/world/feature.hh
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "shared/types.hh"
+
+namespace world
+{
+class Chunk;
+class Dimension;
+class Voxel;
+class VoxelStorage;
+} // namespace world
+
+namespace world
+{
+class Feature final : public std::vector<std::tuple<voxel_pos, const Voxel*, bool>> {
+public:
+ Feature(void) = default;
+ virtual ~Feature(void) = default;
+
+public:
+ void place(const voxel_pos& vpos, Dimension* dimension) const;
+ void place(const voxel_pos& vpos, const chunk_pos& cpos, Chunk& chunk) const;
+ void place(const voxel_pos& vpos, const chunk_pos& cpos, VoxelStorage& voxels) const;
+};
+} // namespace world
diff --git a/src/game/shared/world/item.cc b/src/game/shared/world/item.cc
new file mode 100644
index 0000000..5e60609
--- /dev/null
+++ b/src/game/shared/world/item.cc
@@ -0,0 +1,55 @@
+#include "shared/pch.hh"
+
+#include "shared/world/item.hh"
+
+#include "core/math/crc64.hh"
+
+#include "shared/world/voxel.hh"
+
+world::Item::Item(const Item& source, item_id id) noexcept : Item(source)
+{
+ m_id = id;
+}
+
+void world::Item::set_cached_texture(resource_ptr<TextureGUI> texture) const noexcept
+{
+ m_cached_texture = std::move(texture);
+}
+
+std::uint64_t world::Item::get_checksum(std::uint64_t combine) const
+{
+ combine = math::crc64(m_name.data(), m_name.size(), combine);
+ combine = math::crc64(m_texture.data(), m_texture.size(), combine);
+
+ std::uint32_t id = m_place_voxel ? m_place_voxel->get_id() : NULL_VOXEL_ID;
+ combine = math::crc64(&id, sizeof(id), combine);
+
+ return combine;
+}
+
+world::ItemBuilder::ItemBuilder(std::string_view name)
+{
+ set_name(name);
+}
+
+void world::ItemBuilder::set_name(std::string_view name)
+{
+ assert(name.size());
+
+ m_name = name;
+}
+
+void world::ItemBuilder::set_texture(std::string_view texture)
+{
+ m_texture = texture;
+}
+
+void world::ItemBuilder::set_place_voxel(const Voxel* place_voxel)
+{
+ m_place_voxel = place_voxel;
+}
+
+std::unique_ptr<world::Item> world::ItemBuilder::build(item_id id) const
+{
+ return std::make_unique<Item>(*this, id);
+}
diff --git a/src/game/shared/world/item.hh b/src/game/shared/world/item.hh
new file mode 100644
index 0000000..ffa7f5c
--- /dev/null
+++ b/src/game/shared/world/item.hh
@@ -0,0 +1,84 @@
+#pragma once
+
+#include "core/resource/resource.hh"
+
+#include "shared/types.hh"
+
+// This resource is only defined client-side and
+// resource_ptr<TextureGUI> should remain set to null
+// anywhere else in the shared and server code
+struct TextureGUI;
+
+namespace world
+{
+class Voxel;
+} // namespace world
+
+namespace world
+{
+class Item {
+public:
+ Item(void) = default;
+ explicit Item(const Item& source, item_id id) noexcept;
+
+ constexpr std::string_view get_name(void) const noexcept;
+ constexpr item_id get_id(void) const noexcept;
+
+ constexpr std::string_view get_texture(void) const noexcept;
+ constexpr const Voxel* get_place_voxel(void) const noexcept;
+
+ constexpr resource_ptr<TextureGUI>& get_cached_texture(void) const noexcept;
+ void set_cached_texture(resource_ptr<TextureGUI> texture) const noexcept;
+
+ std::uint64_t get_checksum(std::uint64_t combine = 0U) const;
+
+protected:
+ std::string m_name;
+ item_id m_id { NULL_ITEM_ID };
+
+ std::string m_texture;
+ const Voxel* m_place_voxel { nullptr };
+
+ mutable resource_ptr<TextureGUI> m_cached_texture; // Client-side only
+};
+} // namespace world
+
+namespace world
+{
+class ItemBuilder final : public Item {
+public:
+ explicit ItemBuilder(std::string_view name);
+
+ void set_name(std::string_view name);
+
+ void set_texture(std::string_view texture);
+ void set_place_voxel(const Voxel* place_voxel);
+
+ std::unique_ptr<Item> build(item_id id) const;
+};
+} // namespace world
+
+constexpr std::string_view world::Item::get_name(void) const noexcept
+{
+ return m_name;
+}
+
+constexpr item_id world::Item::get_id(void) const noexcept
+{
+ return m_id;
+}
+
+constexpr std::string_view world::Item::get_texture(void) const noexcept
+{
+ return m_texture;
+}
+
+constexpr const world::Voxel* world::Item::get_place_voxel(void) const noexcept
+{
+ return m_place_voxel;
+}
+
+constexpr resource_ptr<TextureGUI>& world::Item::get_cached_texture(void) const noexcept
+{
+ return m_cached_texture;
+}
diff --git a/src/game/shared/world/item_registry.cc b/src/game/shared/world/item_registry.cc
new file mode 100644
index 0000000..d89abe9
--- /dev/null
+++ b/src/game/shared/world/item_registry.cc
@@ -0,0 +1,68 @@
+#include "shared/pch.hh"
+
+#include "shared/world/item_registry.hh"
+
+#include "core/math/crc64.hh"
+
+#include "shared/world/voxel_registry.hh"
+
+static std::uint64_t registry_checksum = 0U;
+std::unordered_map<std::string, item_id> world::item_registry::names = {};
+std::vector<std::unique_ptr<world::Item>> world::item_registry::items = {};
+
+static void recalculate_checksum(void)
+{
+ registry_checksum = 0U;
+
+ for(const auto& item : world::item_registry::items) {
+ registry_checksum = item->get_checksum(registry_checksum);
+ }
+}
+
+world::Item* world::item_registry::register_item(const ItemBuilder& builder)
+{
+ assert(builder.get_name().size());
+ assert(nullptr == find(builder.get_name()));
+
+ const auto id = static_cast<item_id>(1 + items.size());
+
+ std::unique_ptr<Item> item(builder.build(id));
+ names.emplace(std::string(builder.get_name()), id);
+ items.push_back(std::move(item));
+
+ recalculate_checksum();
+
+ return items.back().get();
+}
+
+world::Item* world::item_registry::find(std::string_view name)
+{
+ const auto it = names.find(std::string(name));
+
+ if(it == names.end()) {
+ return nullptr;
+ }
+
+ return items[it->second - 1].get();
+}
+
+world::Item* world::item_registry::find(const item_id item)
+{
+ if(item == NULL_ITEM_ID || item > items.size()) {
+ return nullptr;
+ }
+
+ return items[item - 1].get();
+}
+
+void world::item_registry::purge(void)
+{
+ registry_checksum = 0U;
+ items.clear();
+ names.clear();
+}
+
+std::uint64_t world::item_registry::get_checksum(void)
+{
+ return registry_checksum;
+}
diff --git a/src/game/shared/world/item_registry.hh b/src/game/shared/world/item_registry.hh
new file mode 100644
index 0000000..a841a2d
--- /dev/null
+++ b/src/game/shared/world/item_registry.hh
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "shared/world/item.hh"
+
+namespace world::item_registry
+{
+extern std::unordered_map<std::string, item_id> names;
+extern std::vector<std::unique_ptr<Item>> items;
+} // namespace world::item_registry
+
+namespace world::item_registry
+{
+Item* register_item(const ItemBuilder& builder);
+Item* find(std::string_view name);
+Item* find(const item_id item);
+} // namespace world::item_registry
+
+namespace world::item_registry
+{
+void purge(void);
+} // namespace world::item_registry
+
+namespace world::item_registry
+{
+std::uint64_t get_checksum(void);
+} // namespace world::item_registry
diff --git a/src/game/shared/world/ray_dda.cc b/src/game/shared/world/ray_dda.cc
new file mode 100644
index 0000000..d6383b9
--- /dev/null
+++ b/src/game/shared/world/ray_dda.cc
@@ -0,0 +1,107 @@
+#include "shared/pch.hh"
+
+#include "shared/world/ray_dda.hh"
+
+#include "shared/world/dimension.hh"
+
+#include "shared/coord.hh"
+
+world::RayDDA::RayDDA(const world::Dimension* dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos,
+ const glm::fvec3& direction)
+{
+ reset(dimension, start_chunk, start_fpos, direction);
+}
+
+world::RayDDA::RayDDA(const world::Dimension& dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos,
+ const glm::fvec3& direction)
+{
+ reset(dimension, start_chunk, start_fpos, direction);
+}
+
+void world::RayDDA::reset(const world::Dimension* dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos,
+ const glm::fvec3& direction)
+{
+ this->dimension = dimension;
+ this->start_chunk = start_chunk;
+ this->start_fpos = start_fpos;
+ this->direction = direction;
+
+ this->delta_dist.x = direction.x ? glm::abs(1.0f / direction.x) : std::numeric_limits<float>::max();
+ this->delta_dist.y = direction.y ? glm::abs(1.0f / direction.y) : std::numeric_limits<float>::max();
+ this->delta_dist.z = direction.z ? glm::abs(1.0f / direction.z) : std::numeric_limits<float>::max();
+
+ this->distance = 0.0f;
+ this->vpos = coord::to_voxel(start_chunk, start_fpos);
+ this->vnormal = voxel_pos(0, 0, 0);
+
+ // Need this for initial direction calculations
+ auto lpos = coord::to_local(start_fpos);
+
+ if(direction.x < 0.0f) {
+ this->side_dist.x = this->delta_dist.x * (start_fpos.x - lpos.x);
+ this->vstep.x = voxel_pos::value_type(-1);
+ }
+ else {
+ this->side_dist.x = this->delta_dist.x * (lpos.x + 1.0f - start_fpos.x);
+ this->vstep.x = voxel_pos::value_type(+1);
+ }
+
+ if(direction.y < 0.0f) {
+ this->side_dist.y = this->delta_dist.y * (start_fpos.y - lpos.y);
+ this->vstep.y = voxel_pos::value_type(-1);
+ }
+ else {
+ this->side_dist.y = this->delta_dist.y * (lpos.y + 1.0f - start_fpos.y);
+ this->vstep.y = voxel_pos::value_type(+1);
+ }
+
+ if(direction.z < 0.0f) {
+ this->side_dist.z = this->delta_dist.z * (start_fpos.z - lpos.z);
+ this->vstep.z = voxel_pos::value_type(-1);
+ }
+ else {
+ this->side_dist.z = this->delta_dist.z * (lpos.z + 1.0f - start_fpos.z);
+ this->vstep.z = voxel_pos::value_type(+1);
+ }
+}
+
+void world::RayDDA::reset(const world::Dimension& dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos,
+ const glm::fvec3& direction)
+{
+ reset(&dimension, start_chunk, start_fpos, direction);
+}
+
+const world::Voxel* world::RayDDA::step(void)
+{
+ if(side_dist.x < side_dist.z) {
+ if(side_dist.x < side_dist.y) {
+ vnormal = voxel_pos(-vstep.x, 0, 0);
+ distance = side_dist.x;
+ side_dist.x += delta_dist.x;
+ vpos.x += vstep.x;
+ }
+ else {
+ vnormal = voxel_pos(0, -vstep.y, 0);
+ distance = side_dist.y;
+ side_dist.y += delta_dist.y;
+ vpos.y += vstep.y;
+ }
+ }
+ else {
+ if(side_dist.z < side_dist.y) {
+ vnormal = voxel_pos(0, 0, -vstep.z);
+ distance = side_dist.z;
+ side_dist.z += delta_dist.z;
+ vpos.z += vstep.z;
+ }
+ else {
+ vnormal = voxel_pos(0, -vstep.y, 0);
+ distance = side_dist.y;
+ side_dist.y += delta_dist.y;
+ vpos.y += vstep.y;
+ }
+ }
+
+ // This is slower than I want it to be
+ return dimension->get_voxel(vpos);
+}
diff --git a/src/game/shared/world/ray_dda.hh b/src/game/shared/world/ray_dda.hh
new file mode 100644
index 0000000..0f548ba
--- /dev/null
+++ b/src/game/shared/world/ray_dda.hh
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "shared/types.hh"
+
+namespace world
+{
+class Dimension;
+class Voxel;
+} // namespace world
+
+namespace world
+{
+class RayDDA final {
+public:
+ RayDDA(void) = default;
+ explicit RayDDA(const Dimension* dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction);
+ explicit RayDDA(const Dimension& dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction);
+
+ void reset(const Dimension* dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction);
+ void reset(const Dimension& dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction);
+
+ const Voxel* step(void);
+
+public:
+ const Dimension* dimension;
+ chunk_pos start_chunk;
+ glm::fvec3 start_fpos;
+ glm::fvec3 direction;
+
+ glm::fvec3 delta_dist;
+ glm::fvec3 side_dist;
+ voxel_pos vstep;
+
+ double distance;
+ voxel_pos vnormal;
+ voxel_pos vpos;
+};
+} // namespace world
diff --git a/src/game/shared/world/voxel.cc b/src/game/shared/world/voxel.cc
new file mode 100644
index 0000000..21fe62c
--- /dev/null
+++ b/src/game/shared/world/voxel.cc
@@ -0,0 +1,149 @@
+#include "shared/pch.hh"
+
+#include "shared/world/voxel.hh"
+
+#include "core/math/crc64.hh"
+
+#include "shared/world/dimension.hh"
+
+world::Voxel::Voxel(const Voxel& source, voxel_id id) noexcept : Voxel(source)
+{
+ m_id = id;
+}
+
+void world::Voxel::on_place(Dimension* dimension, const voxel_pos& vpos) const
+{
+ if(m_on_place) {
+ m_on_place(dimension, vpos);
+ }
+}
+
+void world::Voxel::on_remove(Dimension* dimension, const voxel_pos& vpos) const
+{
+ if(m_on_remove) {
+ m_on_remove(dimension, vpos);
+ }
+}
+
+void world::Voxel::on_tick(Dimension* dimension, const voxel_pos& vpos) const
+{
+ if(m_on_tick) {
+ m_on_tick(dimension, vpos);
+ }
+}
+
+std::size_t world::Voxel::get_random_texture_index(VoxelFace face, const voxel_pos& vpos) const
+{
+ const auto& textures = get_face_textures(face);
+
+ assert(textures.size());
+
+ std::uint64_t hash = 0U;
+ hash = math::crc64(&vpos.x, sizeof(vpos.x), hash);
+ hash = math::crc64(&vpos.y, sizeof(vpos.y), hash);
+ hash = math::crc64(&vpos.z, sizeof(vpos.z), hash);
+
+ return static_cast<std::size_t>(hash % textures.size());
+}
+
+void world::Voxel::set_face_cache(VoxelFace face, std::size_t offset, std::size_t plane)
+{
+ assert(face < m_cached_face_offsets.size());
+ assert(face < m_cached_face_planes.size());
+
+ m_cached_face_offsets[face] = offset;
+ m_cached_face_planes[face] = plane;
+}
+
+std::uint64_t world::Voxel::get_checksum(std::uint64_t combine) const
+{
+ combine = math::crc64(m_name.data(), m_name.size(), combine);
+ combine += static_cast<std::uint64_t>(m_shape);
+ combine += static_cast<std::uint64_t>(m_render_mode);
+ return combine;
+}
+
+world::VoxelBuilder::VoxelBuilder(std::string_view name)
+{
+ set_name(name);
+}
+
+void world::VoxelBuilder::set_on_place(VoxelOnPlaceFunc func) noexcept
+{
+ m_on_place = std::move(func);
+}
+
+void world::VoxelBuilder::set_on_remove(VoxelOnRemoveFunc func) noexcept
+{
+ m_on_remove = std::move(func);
+}
+
+void world::VoxelBuilder::set_on_tick(VoxelOnTickFunc func) noexcept
+{
+ m_on_tick = std::move(func);
+}
+
+void world::VoxelBuilder::set_name(std::string_view name) noexcept
+{
+ assert(name.size());
+
+ m_name = name;
+}
+
+void world::VoxelBuilder::set_render_mode(VoxelRender mode) noexcept
+{
+ m_render_mode = mode;
+}
+
+void world::VoxelBuilder::set_shape(VoxelShape shape) noexcept
+{
+ m_shape = shape;
+}
+
+void world::VoxelBuilder::set_animated(bool animated) noexcept
+{
+ m_animated = animated;
+}
+
+void world::VoxelBuilder::set_touch_type(VoxelTouch type) noexcept
+{
+ m_touch_type = type;
+}
+
+void world::VoxelBuilder::set_touch_values(const glm::fvec3& values) noexcept
+{
+ m_touch_values = values;
+}
+
+void world::VoxelBuilder::set_surface_material(VoxelMaterial material) noexcept
+{
+ m_surface_material = material;
+}
+
+void world::VoxelBuilder::set_collision(const math::AABBf& box) noexcept
+{
+ m_collision = box;
+}
+
+void world::VoxelBuilder::add_default_texture(std::string_view path)
+{
+ assert(path.size());
+
+ m_default_textures.emplace_back(path);
+}
+
+void world::VoxelBuilder::add_face_texture(VoxelFace face, std::string_view path)
+{
+ assert(face < m_face_textures.size());
+ assert(path.size());
+
+ m_face_textures[face].emplace_back(path);
+}
+
+std::unique_ptr<world::Voxel> world::VoxelBuilder::build(voxel_id id) const
+{
+ assert(m_name.size());
+ assert(id);
+
+ return std::make_unique<Voxel>(*this, id);
+}
diff --git a/src/game/shared/world/voxel.hh b/src/game/shared/world/voxel.hh
new file mode 100644
index 0000000..6013962
--- /dev/null
+++ b/src/game/shared/world/voxel.hh
@@ -0,0 +1,286 @@
+#pragma once
+
+#include "core/math/aabb.hh"
+
+#include "shared/types.hh"
+
+namespace world
+{
+class Dimension;
+} // namespace world
+
+namespace world
+{
+enum VoxelRender : unsigned int {
+ VRENDER_NONE = 0U, ///< The voxel is not rendered at all
+ VRENDER_OPAQUE, ///< The voxel is fully opaque
+ VRENDER_BLEND, ///< The voxel is blended (e.g. water, glass)
+};
+
+enum VoxelShape : unsigned int {
+ VSHAPE_CUBE = 0U, ///< Full cube shape
+ VSHAPE_CROSS, ///< TODO: Cross shape
+ VSHAPE_MODEL, ///< TODO: Custom model shape
+};
+
+enum VoxelFace : unsigned int {
+ VFACE_NORTH = 0U, ///< Positive Z face
+ VFACE_SOUTH, ///< Negative Z face
+ VFACE_EAST, ///< Positive X face
+ VFACE_WEST, ///< Negative X face
+ VFACE_TOP, ///< Positive Y face
+ VFACE_BOTTOM, ///< Negative Y face
+ VFACE_CROSS_NWSE, ///< Diagonal cross face northwest-southeast
+ VFACE_CROSS_NESW, ///< Diagonal cross face northeast-southwest
+ VFACE_COUNT
+};
+
+enum VoxelTouch : unsigned int {
+ VTOUCH_NONE = 0xFFFFU,
+ VTOUCH_SOLID = 0U, ///< The entity is stopped in its tracks
+ VTOUCH_BOUNCE, ///< The entity bounces back with some energy loss
+ VTOUCH_SINK, ///< The entity phases/sinks through the voxel
+};
+
+enum VoxelMaterial : unsigned int {
+ VMAT_UNKNOWN = 0xFFFFU,
+ VMAT_DEFAULT = 0U,
+ VMAT_STONE,
+ VMAT_DIRT,
+ VMAT_GLASS,
+ VMAT_GRASS,
+ VMAT_GRAVEL,
+ VMAT_METAL,
+ VMAT_SAND,
+ VMAT_WOOD,
+ VMAT_SLOSH,
+ VMAT_COUNT
+};
+
+enum VoxelVisBits : unsigned int {
+ VVIS_NORTH = 1U << VFACE_NORTH, ///< Positive Z
+ VVIS_SOUTH = 1U << VFACE_SOUTH, ///< Negative Z
+ VVIS_EAST = 1U << VFACE_EAST, ///< Positive X
+ VVIS_WEST = 1U << VFACE_WEST, ///< Negative X
+ VVIS_UP = 1U << VFACE_TOP, ///< Positive Y
+ VVIS_DOWN = 1U << VFACE_BOTTOM, ///< Negative Y
+};
+} // namespace world
+
+namespace world
+{
+using VoxelOnPlaceFunc = std::function<void(Dimension*, const voxel_pos&)>;
+using VoxelOnRemoveFunc = std::function<void(Dimension*, const voxel_pos&)>;
+using VoxelOnTickFunc = std::function<void(Dimension*, const voxel_pos&)>;
+} // namespace world
+
+namespace world
+{
+class Voxel {
+public:
+ Voxel(void) = default;
+ explicit Voxel(const Voxel& source, voxel_id id) noexcept;
+
+ void on_place(Dimension* dimension, const voxel_pos& vpos) const;
+ void on_remove(Dimension* dimension, const voxel_pos& vpos) const;
+ void on_tick(Dimension* dimension, const voxel_pos& vpos) const;
+
+ constexpr std::string_view get_name(void) const noexcept;
+ constexpr voxel_id get_id(void) const noexcept;
+
+ constexpr VoxelRender get_render_mode(void) const noexcept;
+ constexpr VoxelShape get_shape(void) const noexcept;
+ constexpr bool is_animated(void) const noexcept;
+
+ constexpr VoxelTouch get_touch_type(void) const noexcept;
+ constexpr const glm::fvec3& get_touch_values(void) const noexcept;
+ constexpr VoxelMaterial get_surface_material(void) const noexcept;
+
+ constexpr const math::AABBf& get_collision(void) const noexcept;
+
+ constexpr const std::vector<std::string>& get_default_textures(void) const noexcept;
+ constexpr const std::vector<std::string>& get_face_textures(VoxelFace face) const noexcept;
+ constexpr std::size_t get_cached_face_offset(VoxelFace face) const noexcept;
+ constexpr std::size_t get_cached_face_plane(VoxelFace face) const noexcept;
+
+ template<VoxelRender RenderMode>
+ constexpr bool is_render_mode(void) const noexcept;
+ template<VoxelShape Shape>
+ constexpr bool is_shape(void) const noexcept;
+ template<VoxelTouch TouchType>
+ constexpr bool is_touch_type(void) const noexcept;
+ template<VoxelMaterial Material>
+ constexpr bool is_surface_material(void) const noexcept;
+
+ /// Non-model voxel shapes support texture variation based on the
+ /// voxel position on the world; this method handles the math behind this
+ /// @param face The face of the voxel to get the texture index for
+ /// @param vpos The absolute voxel position to get the texture index for
+ /// @return The index of the texture to use for the given face at the given position
+ /// @remarks On client-side: plane[get_cached_face_plane][get_cached_face_offset + thisFunctionResult]
+ std::size_t get_random_texture_index(VoxelFace face, const voxel_pos& vpos) const;
+
+ /// Assign cached plane index and plane offset for a given face
+ /// @param face The face to assign the cache for
+ /// @param offset The offset to assign to the face
+ /// @param plane The plane index to assign to the face
+ void set_face_cache(VoxelFace face, std::size_t offset, std::size_t plane);
+
+ /// Calculate a checksum for the voxel's properties
+ /// @param combine An optional initial checksum to combine with
+ /// @return The calculated checksum
+ std::uint64_t get_checksum(std::uint64_t combine = 0U) const;
+
+protected:
+ std::string m_name;
+ voxel_id m_id { NULL_VOXEL_ID };
+
+ VoxelRender m_render_mode { VRENDER_OPAQUE };
+ VoxelShape m_shape { VSHAPE_CUBE };
+ bool m_animated { false };
+
+ VoxelTouch m_touch_type { VTOUCH_SOLID };
+ glm::fvec3 m_touch_values { 0.0f };
+ VoxelMaterial m_surface_material { VMAT_DEFAULT };
+
+ math::AABBf m_collision { { 0.0f, 0.0f, 0.0f }, { 1.0f, 1.0f, 1.0f } };
+
+ std::vector<std::string> m_default_textures;
+ std::array<std::vector<std::string>, VFACE_COUNT> m_face_textures;
+ std::array<std::size_t, VFACE_COUNT> m_cached_face_offsets;
+ std::array<std::size_t, VFACE_COUNT> m_cached_face_planes;
+
+ VoxelOnPlaceFunc m_on_place;
+ VoxelOnRemoveFunc m_on_remove;
+ VoxelOnTickFunc m_on_tick;
+};
+} // namespace world
+
+namespace world
+{
+class VoxelBuilder final : public Voxel {
+public:
+ VoxelBuilder(void) = default;
+ explicit VoxelBuilder(std::string_view name);
+
+ void set_on_place(VoxelOnPlaceFunc func) noexcept;
+ void set_on_remove(VoxelOnRemoveFunc func) noexcept;
+ void set_on_tick(VoxelOnTickFunc func) noexcept;
+
+ void set_name(std::string_view name) noexcept;
+
+ void set_render_mode(VoxelRender mode) noexcept;
+ void set_shape(VoxelShape shape) noexcept;
+ void set_animated(bool animated) noexcept;
+
+ void set_touch_type(VoxelTouch type) noexcept;
+ void set_touch_values(const glm::fvec3& values) noexcept;
+ void set_surface_material(VoxelMaterial material) noexcept;
+
+ void set_collision(const math::AABBf& box) noexcept;
+
+ void add_default_texture(std::string_view path);
+ void add_face_texture(VoxelFace face, std::string_view path);
+
+ std::unique_ptr<Voxel> build(voxel_id id) const;
+};
+} // namespace world
+
+constexpr std::string_view world::Voxel::get_name(void) const noexcept
+{
+ return m_name;
+}
+
+constexpr voxel_id world::Voxel::get_id(void) const noexcept
+{
+ return m_id;
+}
+
+constexpr world::VoxelRender world::Voxel::get_render_mode(void) const noexcept
+{
+ return m_render_mode;
+}
+
+constexpr world::VoxelShape world::Voxel::get_shape(void) const noexcept
+{
+ return m_shape;
+}
+
+constexpr bool world::Voxel::is_animated(void) const noexcept
+{
+ return m_animated;
+}
+
+constexpr world::VoxelTouch world::Voxel::get_touch_type(void) const noexcept
+{
+ return m_touch_type;
+}
+
+constexpr const glm::fvec3& world::Voxel::get_touch_values(void) const noexcept
+{
+ return m_touch_values;
+}
+
+constexpr world::VoxelMaterial world::Voxel::get_surface_material(void) const noexcept
+{
+ return m_surface_material;
+}
+
+constexpr const math::AABBf& world::Voxel::get_collision(void) const noexcept
+{
+ return m_collision;
+}
+
+constexpr const std::vector<std::string>& world::Voxel::get_default_textures(void) const noexcept
+{
+ return m_default_textures;
+}
+
+constexpr const std::vector<std::string>& world::Voxel::get_face_textures(VoxelFace face) const noexcept
+{
+ assert(face <= m_face_textures.size());
+
+ if(m_face_textures[face].empty()) {
+ return m_default_textures;
+ }
+
+ return m_face_textures[face];
+}
+
+constexpr std::size_t world::Voxel::get_cached_face_offset(VoxelFace face) const noexcept
+{
+ assert(face <= m_cached_face_offsets.size());
+
+ return m_cached_face_offsets[face];
+}
+
+constexpr std::size_t world::Voxel::get_cached_face_plane(VoxelFace face) const noexcept
+{
+ assert(face <= m_cached_face_planes.size());
+
+ return m_cached_face_planes[face];
+}
+
+template<world::VoxelRender RenderMode>
+constexpr bool world::Voxel::is_render_mode(void) const noexcept
+{
+ return m_render_mode == RenderMode;
+}
+
+template<world::VoxelShape Shape>
+constexpr bool world::Voxel::is_shape(void) const noexcept
+{
+ return m_shape == Shape;
+}
+
+template<world::VoxelTouch TouchType>
+constexpr bool world::Voxel::is_touch_type(void) const noexcept
+{
+ return m_touch_type == TouchType;
+}
+
+template<world::VoxelMaterial Material>
+constexpr bool world::Voxel::is_surface_material(void) const noexcept
+{
+ return m_surface_material == Material;
+}
diff --git a/src/game/shared/world/voxel_registry.cc b/src/game/shared/world/voxel_registry.cc
new file mode 100644
index 0000000..fae83fa
--- /dev/null
+++ b/src/game/shared/world/voxel_registry.cc
@@ -0,0 +1,64 @@
+#include "shared/pch.hh"
+
+#include "shared/world/voxel_registry.hh"
+
+static std::uint64_t registry_checksum = 0U;
+emhash8::HashMap<std::string, voxel_id> world::voxel_registry::names;
+std::vector<std::unique_ptr<world::Voxel>> world::voxel_registry::voxels;
+
+static void recalculate_checksum(void)
+{
+ registry_checksum = 0U;
+
+ for(const auto& voxel : world::voxel_registry::voxels) {
+ registry_checksum = voxel->get_checksum(registry_checksum);
+ }
+}
+
+world::Voxel* world::voxel_registry::register_voxel(const VoxelBuilder& builder)
+{
+ assert(builder.get_name().size());
+ assert(nullptr == find(builder.get_name()));
+
+ const auto id = static_cast<voxel_id>(1 + voxels.size());
+
+ std::unique_ptr<Voxel> voxel(builder.build(id));
+ names.emplace(std::string(builder.get_name()), id);
+ voxels.push_back(std::move(voxel));
+
+ recalculate_checksum();
+
+ return voxels.back().get();
+}
+
+world::Voxel* world::voxel_registry::find(std::string_view name)
+{
+ const auto it = names.find(std::string(name));
+
+ if(it == names.end()) {
+ return nullptr;
+ }
+
+ return voxels[it->second - 1].get();
+}
+
+world::Voxel* world::voxel_registry::find(voxel_id id)
+{
+ if(id == NULL_VOXEL_ID || id > voxels.size()) {
+ return nullptr;
+ }
+
+ return voxels[id - 1].get();
+}
+
+void world::voxel_registry::purge(void)
+{
+ registry_checksum = 0U;
+ voxels.clear();
+ names.clear();
+}
+
+std::uint64_t world::voxel_registry::get_checksum(void)
+{
+ return registry_checksum;
+}
diff --git a/src/game/shared/world/voxel_registry.hh b/src/game/shared/world/voxel_registry.hh
new file mode 100644
index 0000000..a1e0eee
--- /dev/null
+++ b/src/game/shared/world/voxel_registry.hh
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "shared/world/voxel.hh"
+
+namespace world::voxel_registry
+{
+extern emhash8::HashMap<std::string, voxel_id> names;
+extern std::vector<std::unique_ptr<Voxel>> voxels;
+} // namespace world::voxel_registry
+
+namespace world::voxel_registry
+{
+Voxel* register_voxel(const VoxelBuilder& builder);
+Voxel* find(std::string_view name);
+Voxel* find(voxel_id id);
+} // namespace world::voxel_registry
+
+namespace world::voxel_registry
+{
+void purge(void);
+} // namespace world::voxel_registry
+
+namespace world::voxel_registry
+{
+std::uint64_t get_checksum(void);
+} // namespace world::voxel_registry
diff --git a/src/game/shared/world/voxel_storage.cc b/src/game/shared/world/voxel_storage.cc
new file mode 100644
index 0000000..68e261c
--- /dev/null
+++ b/src/game/shared/world/voxel_storage.cc
@@ -0,0 +1,48 @@
+#include "shared/pch.hh"
+
+#include "shared/world/voxel_storage.hh"
+
+#include "core/io/buffer.hh"
+
+void world::VoxelStorage::serialize(io::WriteBuffer& buffer) const
+{
+ auto bound = mz_compressBound(sizeof(VoxelStorage));
+ auto zdata = std::vector<unsigned char>(bound);
+
+ VoxelStorage net_storage;
+
+ for(std::size_t i = 0; i < CHUNK_VOLUME; ++i) {
+ // Convert voxel indices into network byte order;
+ // We're going to compress them but we still want
+ // the order to be consistent across all the platforms
+ net_storage[i] = ENET_HOST_TO_NET_16(at(i));
+ }
+
+ mz_compress(zdata.data(), &bound, reinterpret_cast<unsigned char*>(net_storage.data()), sizeof(VoxelStorage));
+
+ buffer.write<std::uint64_t>(bound);
+
+ // Write all the compressed data into the buffer
+ for(std::size_t i = 0; i < bound; buffer.write<std::uint8_t>(zdata[i++])) {
+ // empty
+ }
+}
+
+void world::VoxelStorage::deserialize(io::ReadBuffer& buffer)
+{
+ auto size = static_cast<mz_ulong>(sizeof(VoxelStorage));
+ auto bound = static_cast<mz_ulong>(buffer.read<std::uint64_t>());
+ auto zdata = std::vector<unsigned char>(bound);
+
+ // Read all the compressed data from the buffer
+ for(std::size_t i = 0; i < bound; zdata[i++] = buffer.read<std::uint8_t>()) {
+ // empty
+ }
+
+ mz_uncompress(reinterpret_cast<unsigned char*>(data()), &size, zdata.data(), bound);
+
+ for(std::size_t i = 0; i < CHUNK_VOLUME; ++i) {
+ // Convert voxel indices back into the host byte order
+ at(i) = ENET_NET_TO_HOST_16(at(i));
+ }
+}
diff --git a/src/game/shared/world/voxel_storage.hh b/src/game/shared/world/voxel_storage.hh
new file mode 100644
index 0000000..ac7f03d
--- /dev/null
+++ b/src/game/shared/world/voxel_storage.hh
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "shared/const.hh"
+#include "shared/types.hh"
+
+namespace io
+{
+class ReadBuffer;
+class WriteBuffer;
+} // namespace io
+
+namespace world
+{
+class VoxelStorage final : public std::array<voxel_id, CHUNK_VOLUME> {
+public:
+ using std::array<voxel_id, CHUNK_VOLUME>::array;
+ void serialize(io::WriteBuffer& buffer) const;
+ void deserialize(io::ReadBuffer& buffer);
+};
+} // namespace world