aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
authorNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-12 18:05:18 +1100
committerNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-12 18:05:18 +1100
commit1cc08c51eb4b0f95c30c0a98ad1fc5ad3459b2df (patch)
tree222dfcd07a1e40716127a347bbfd7119ce3d0984 /src/server
initial commit
Diffstat (limited to 'src/server')
-rw-r--r--src/server/chunk_data.cc1
-rw-r--r--src/server/chunk_data.hh30
-rw-r--r--src/server/client.cc1
-rw-r--r--src/server/client.hh67
-rw-r--r--src/server/database.cc238
-rw-r--r--src/server/database.hh39
-rw-r--r--src/server/movement.cc87
-rw-r--r--src/server/movement.hh19
-rw-r--r--src/server/resources.cc59
-rw-r--r--src/server/resources.hh119
-rw-r--r--src/server/server.cc640
-rw-r--r--src/server/server.hh37
-rw-r--r--src/server/shared.cc1
-rw-r--r--src/server/shared.hh19
-rw-r--r--src/server/world.cc53
-rw-r--r--src/server/world.hh59
16 files changed, 1469 insertions, 0 deletions
diff --git a/src/server/chunk_data.cc b/src/server/chunk_data.cc
new file mode 100644
index 0000000..722e9bb
--- /dev/null
+++ b/src/server/chunk_data.cc
@@ -0,0 +1 @@
+#include "server/chunk_data.hh"
diff --git a/src/server/chunk_data.hh b/src/server/chunk_data.hh
new file mode 100644
index 0000000..eea5cef
--- /dev/null
+++ b/src/server/chunk_data.hh
@@ -0,0 +1,30 @@
+#ifndef SERVER_CHUNK_DATA_HH_
+#define SERVER_CHUNK_DATA_HH_
+
+#include <memory>
+#include <optional>
+#include <unordered_set>
+
+#include "server/world.hh"
+#include "shared/player.hh"
+
+namespace server {
+
+struct chunk_data {
+public:
+ // nullopt = constructing/destructing via future operations in thread pool
+ // we use shared_ptr here to avoid complex moves in boost::asio::post.
+ // There is no good reason to use shared_ptr over unique_ptr, other than
+ // boost::asio::post requiring copy_constructable args in std::bind.
+ std::optional<std::shared_ptr<server::world::chunk>> chunk;
+ // players associated with the chunk
+ std::unordered_set<shared::player::index_t> players;
+
+public:
+ server::world::chunk& get_chunk() noexcept { return *(*this->chunk); }
+ bool has_initialised() const noexcept { return chunk.has_value(); }
+};
+
+} // namespace server
+
+#endif
diff --git a/src/server/client.cc b/src/server/client.cc
new file mode 100644
index 0000000..0197f90
--- /dev/null
+++ b/src/server/client.cc
@@ -0,0 +1 @@
+#include "server/client.hh"
diff --git a/src/server/client.hh b/src/server/client.hh
new file mode 100644
index 0000000..5866035
--- /dev/null
+++ b/src/server/client.hh
@@ -0,0 +1,67 @@
+#ifndef SERVER_CLIENT_HH_
+#define SERVER_CLIENT_HH_
+
+#include <deque>
+#include <memory>
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+
+#include "server/chunk_data.hh"
+#include "server/world.hh"
+#include "shared/net/connection.hh"
+#include "shared/player.hh"
+#include "shared/world.hh"
+
+namespace server {
+
+// Client represents the entire state of a player.
+class client {
+public:
+ shared::net::connection connection;
+ shared::player::index_t index;
+
+ // If this is set a disconnect packet should be set
+ std::optional<std::string> disconnect_reason;
+
+ struct player_info {
+ std::string username;
+ std::string password;
+ shared::player player;
+ };
+ // If this is nullopt, we haven't got our init packet yet which contained
+ // their user + pass.
+ std::optional<std::shared_ptr<player_info>> player_info;
+
+ // flag for disconnecting, the client must stay while the server is writing
+ // its contents to a db
+ bool disconnecting = false;
+
+ // Chunks for which the player is associated with.
+ std::unordered_set<shared::math::coords,
+ decltype(&shared::world::chunk::hash),
+ decltype(&shared::world::chunk::equal)>
+ chunks{4096, shared::world::chunk::hash, shared::world::chunk::equal};
+
+public:
+ client(shared::net::connection&& con, const shared::player::index_t& index)
+ : connection(std::move(con)), index(index) {}
+
+ bool has_initialised() const noexcept {
+ return this->player_info.has_value();
+ }
+ shared::player& get_player() noexcept {
+ return (*this->player_info)->player;
+ }
+ const std::string& get_username() const noexcept {
+ return (*this->player_info)->username;
+ }
+ const std::string& get_password() const noexcept {
+ return (*this->player_info)->password;
+ }
+};
+
+} // namespace server
+
+#endif
diff --git a/src/server/database.cc b/src/server/database.cc
new file mode 100644
index 0000000..077c86c
--- /dev/null
+++ b/src/server/database.cc
@@ -0,0 +1,238 @@
+#include "server/database.hh"
+
+namespace server {
+namespace database {
+
+sqlite3* get_database() noexcept {
+
+ static sqlite3* const database = []() -> sqlite3* {
+ std::filesystem::create_directory(server::state.directory);
+ const std::string path = server::state.directory + "world.dat";
+ if (!std::filesystem::exists(path)) {
+ shared::print::warn(
+ "server: regenerating non-existent world data\n");
+ }
+
+ sqlite3* database = nullptr;
+ if (const int status = sqlite3_open(path.c_str(), &database);
+ status != SQLITE_OK) {
+
+ throw std::runtime_error("failed to open chunk database at " +
+ path);
+ }
+
+ return database;
+ }();
+
+ return database;
+}
+
+static void prepare(sqlite3_stmt*& statement, const std::string& sql) noexcept {
+ sqlite3* const db = get_database();
+
+ if (const int status = sqlite3_prepare_v2(
+ db, sql.c_str(), static_cast<int>(std::size(sql) + 1), &statement,
+ nullptr);
+ status != SQLITE_OK) {
+
+ throw std::runtime_error(std::string{"sqlite prepare error \""} +
+ sqlite3_errmsg(db) + '\"');
+ }
+}
+
+static auto step(sqlite3_stmt*& statement) noexcept {
+ const int status = sqlite3_step(statement);
+ if (status == SQLITE_ERROR) {
+ sqlite3* const db = get_database();
+ throw std::runtime_error(std::string{"sqlite step error \""} +
+ sqlite3_errmsg(db) + '\"');
+ }
+ return status;
+}
+
+static void finalise(sqlite3_stmt*& statement) noexcept {
+ if (const int status = sqlite3_finalize(statement); status != SQLITE_OK) {
+ throw std::runtime_error(std::string{"sqlite bind blob error \""} +
+ sqlite3_errstr(status) + '\"');
+ }
+}
+
+static void bind_int64(sqlite3_stmt*& statement, const int index,
+ const std::uint64_t& key) noexcept {
+ if (const int status = sqlite3_bind_int64(statement, index,
+ std::bit_cast<std::int64_t>(key));
+ status != SQLITE_OK) {
+ throw std::runtime_error(std::string{"sqlite bind int error \""} +
+ sqlite3_errstr(status) + '\"');
+ }
+}
+
+static void bind_text(sqlite3_stmt*& statement, const int index,
+ const std::string& text) noexcept {
+ if (const int status =
+ sqlite3_bind_text(statement, index, std::data(text),
+ static_cast<int>(std::size(text)), SQLITE_STATIC);
+ status != SQLITE_OK) {
+ throw std::runtime_error(std::string{"sqlite bind text error \""} +
+ sqlite3_errstr(status) + '\"');
+ }
+}
+
+template <typename T>
+static void bind_blob(sqlite3_stmt*& statement, const int index,
+ const T& blob) noexcept {
+ if (const int status =
+ sqlite3_bind_blob(statement, index, std::data(blob),
+ static_cast<int>(std::size(blob)), SQLITE_STATIC);
+ status != SQLITE_OK) {
+ throw std::runtime_error(std::string{"sqlite bind blob error \""} +
+ sqlite3_errstr(status) + '\"');
+ }
+}
+
+static void exec_void_stmt(const std::string& sql) noexcept {
+ sqlite3_stmt* statement = nullptr;
+ prepare(statement, sql);
+ step(statement);
+ finalise(statement);
+}
+
+void init() noexcept {
+ exec_void_stmt("CREATE TABLE IF NOT EXISTS Chunks ("
+ " coords BIGINT PRIMARY KEY,"
+ " blocks BLOB NOT NULL"
+ ");");
+
+ exec_void_stmt("CREATE TABLE IF NOT EXISTS Players ("
+ " username TEXT PRIMARY KEY,"
+ " password TEXT NOT NULL,"
+ " player BLOB NOT NULL"
+ ");");
+}
+
+void quit() noexcept {
+ sqlite3* database = get_database();
+ if (const int status = sqlite3_close(database); status != SQLITE_OK) {
+ throw std::runtime_error(std::string{"failed to close chunk database"} +
+ sqlite3_errmsg(database));
+ };
+}
+
+// We have to store our key in network byte order!
+static std::uint64_t make_key(const shared::math::coords& coords) noexcept {
+ const auto to_64 = [](const std::int32_t& val, const int lshift) {
+ return static_cast<std::uint64_t>(
+ htonl(std::bit_cast<std::uint32_t>(val)))
+ << lshift;
+ };
+
+ return to_64(coords.x, 32) + to_64(coords.z, 0);
+}
+
+std::optional<proto::chunk>
+maybe_read_chunk(const shared::math::coords& pos) noexcept {
+ sqlite3_stmt* statement = nullptr;
+ prepare(statement, "SELECT blocks FROM Chunks WHERE Chunks.coords = ?;");
+ bind_int64(statement, 1, make_key(pos));
+
+ if (step(statement) != SQLITE_ROW) {
+ finalise(statement);
+ return std::nullopt;
+ }
+
+ const char* addr =
+ static_cast<const char*>(sqlite3_column_blob(statement, 0));
+ std::string blocks{addr, addr + sqlite3_column_bytes(statement, 0)};
+ finalise(statement);
+
+ shared::decompress_string(blocks);
+ proto::chunk chunk;
+ if (!chunk.ParseFromString(blocks)) {
+
+ shared::print::fault("server: chunk [" + std::to_string(pos.x) + ", " +
+ std::to_string(pos.z) +
+ "] failed to parse and was evicted\n");
+
+ statement = nullptr;
+ prepare(statement, "DELETE FROM Chunks WHERE Chunks.coords = ?;");
+ bind_int64(statement, 1, make_key(pos));
+ step(statement);
+ finalise(statement);
+ return std::nullopt;
+ }
+ return chunk;
+}
+
+void write_chunk(const shared::math::coords& pos,
+ const proto::chunk& chunk) noexcept {
+ std::string blocks;
+ chunk.SerializeToString(&blocks);
+ shared::compress_string(blocks);
+
+ sqlite3_stmt* statement = nullptr;
+
+ prepare(statement, "INSERT OR REPLACE INTO Chunks VALUES(?, ?);");
+ bind_int64(statement, 1, make_key(pos));
+ bind_blob(statement, 2, blocks);
+
+ step(statement);
+
+ finalise(statement);
+}
+
+std::optional<std::pair<proto::player, std::string>>
+maybe_read_player(const std::string& username) noexcept {
+ sqlite3_stmt* statement = nullptr;
+ prepare(statement, "SELECT player, password "
+ "FROM Players "
+ "WHERE Players.username = ?");
+ bind_text(statement, 1, username);
+
+ if (step(statement) != SQLITE_ROW) {
+ finalise(statement);
+ return std::nullopt;
+ }
+
+ const char* plr_adr =
+ static_cast<const char*>(sqlite3_column_blob(statement, 0));
+ std::string player_bytes{plr_adr, plr_adr + sqlite3_column_bytes(statement, 0)};
+
+ const unsigned char* pass_adr = sqlite3_column_text(statement, 1);
+ std::string pass_bytes{pass_adr, pass_adr + sqlite3_column_bytes(statement, 1)};
+ finalise(statement);
+
+ shared::decompress_string(player_bytes);
+ proto::player player;
+ if (!player.ParseFromString(player_bytes)) {
+ shared::print::fault("server: player \"" + username +
+ "\" failed to parse and was evicted\n");
+
+ statement = nullptr;
+ prepare(statement, "DELETE FROM Players WHERE Players.username = ?;");
+ bind_text(statement, 1, username);
+ step(statement);
+ finalise(statement);
+ return std::nullopt;
+ }
+ return std::make_pair(player, pass_bytes);
+}
+
+void write_player(const std::string& username, const std::string& password,
+ const proto::player& player) noexcept {
+ std::string player_bytes;
+ player.SerializeToString(&player_bytes);
+ shared::compress_string(player_bytes);
+
+ sqlite3_stmt* statement = nullptr;
+ prepare(statement, "INSERT OR REPLACE INTO Players VALUES(?, ?, ?);");
+ bind_text(statement, 1, username);
+ bind_text(statement, 2, password);
+ bind_blob(statement, 3, player_bytes);
+
+ step(statement);
+ finalise(statement);
+}
+
+} // namespace database
+} // namespace server
+
diff --git a/src/server/database.hh b/src/server/database.hh
new file mode 100644
index 0000000..154cdf2
--- /dev/null
+++ b/src/server/database.hh
@@ -0,0 +1,39 @@
+#ifndef SERVER_DATABASE_HH_
+#define SERVER_DATABASE_HH_
+
+#include <bit>
+#include <filesystem>
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <utility>
+
+#include <sqlite3.h>
+
+#include "server/shared.hh"
+#include "shared/net/net.hh"
+#include "shared/shared.hh"
+#include "shared/world.hh"
+
+namespace server {
+namespace database {
+
+void init() noexcept;
+void quit() noexcept;
+
+// chunks
+std::optional<proto::chunk>
+maybe_read_chunk(const shared::math::coords& c) noexcept;
+void write_chunk(const shared::math::coords& pos,
+ const proto::chunk& chunk) noexcept;
+
+// players
+std::optional<std::pair<proto::player, std::string>> // player, password
+maybe_read_player(const std::string& username) noexcept;
+void write_player(const std::string& username, const std::string& password,
+ const proto::player& player) noexcept;
+
+} // namespace database
+} // namespace server
+
+#endif
diff --git a/src/server/movement.cc b/src/server/movement.cc
new file mode 100644
index 0000000..42b17f1
--- /dev/null
+++ b/src/server/movement.cc
@@ -0,0 +1,87 @@
+#include "server/movement.hh"
+
+// Gets blocks from chunks, returning nullopt if it doesn't exist.
+static std::optional<shared::world::block>
+get_block(const shared::math::coords& pos, server::resources::chunk_map& chunks,
+ const glm::ivec3& block_pos) noexcept {
+
+ const auto find_it = chunks.find(pos);
+ if (find_it == std::end(chunks)) {
+ return std::nullopt;
+ }
+ auto& chunk_data = find_it->second;
+ if (!chunk_data->has_initialised()) {
+ return std::nullopt;
+ }
+
+ return chunk_data->get_chunk().get_block(block_pos);
+}
+
+static std::optional<std::vector<shared::movement::blockinfo>>
+make_blockinfos(server::client& client,
+ server::resources::chunk_map& chunks) noexcept {
+
+ std::vector<shared::movement::blockinfo> blockinfos;
+
+ constexpr int width = shared::movement::move_width;
+ constexpr int height = shared::movement::move_height;
+ for (int x = -width; x <= width; ++x) {
+ for (int y = -height; y <= height; ++y) {
+ for (int z = -width; z <= width; ++z) {
+
+ const glm::ivec3 rel_pos =
+ glm::ivec3{x, y, z} +
+ glm::ivec3{client.get_player().local_pos};
+
+ if (rel_pos.y < 0 ||
+ rel_pos.y >= shared::world::chunk::HEIGHT) {
+ continue;
+ }
+
+ const shared::math::coords norm_chunk_pos =
+ shared::world::chunk::get_normalised_chunk(
+ client.get_player().chunk_pos, rel_pos.x, rel_pos.z);
+ const glm::ivec3 norm_pos =
+ shared::world::chunk::get_normalised_coords(rel_pos);
+
+ const auto block = get_block(norm_chunk_pos, chunks, norm_pos);
+
+ if (!block.has_value()) {
+ return std::nullopt;
+ }
+
+ const glm::vec3 pos =
+ glm::vec3{rel_pos} - client.get_player().local_pos;
+
+ const shared::movement::aabb aabb = {
+ .min = glm::vec3{0.0f, 0.0f, 0.0f} + pos,
+ .max = glm::vec3{1.0f, 1.0f, 1.0f} + pos};
+ blockinfos.push_back(
+ shared::movement::blockinfo{.block = *block,
+ .aabb = aabb,
+ .chunk_pos = norm_chunk_pos,
+ .pos = norm_pos});
+ }
+ }
+ }
+
+ return blockinfos;
+}
+
+void server::movement::move(server::client& client,
+ server::resources::chunk_map& chunks) noexcept {
+
+ if (!client.has_initialised()) {
+ return;
+ }
+
+ const auto blockinfos = make_blockinfos(client, chunks);
+
+ if (!blockinfos.has_value()) {
+ return;
+ }
+
+ const float deltatime = 1.0f / static_cast<float>(server::state.tickrate);
+
+ shared::movement::move(client.get_player(), *blockinfos, deltatime);
+}
diff --git a/src/server/movement.hh b/src/server/movement.hh
new file mode 100644
index 0000000..2c647b3
--- /dev/null
+++ b/src/server/movement.hh
@@ -0,0 +1,19 @@
+#ifndef SERVER_MOVEMENT_HH_
+#define SERVER_MOVEMENT_HH_
+
+#include "server/client.hh"
+#include "server/resources.hh"
+#include "server/world.hh"
+#include "shared/movement.hh"
+
+namespace server {
+
+namespace movement {
+
+void move(server::client& client,
+ server::resources::chunk_map& chunks) noexcept;
+
+} // namespace movement
+} // namespace server
+
+#endif
diff --git a/src/server/resources.cc b/src/server/resources.cc
new file mode 100644
index 0000000..7eb3d8b
--- /dev/null
+++ b/src/server/resources.cc
@@ -0,0 +1,59 @@
+#include "resources.hh"
+
+namespace server {
+namespace resources {
+
+void init() noexcept { server::database::init(); }
+
+// NOT THREAD SAFE! Use get_resources_lock!
+static client_map& get_client_map() noexcept {
+ static client_map ret{};
+ return ret;
+}
+
+static chunk_map& get_chunk_map() noexcept {
+ static chunk_map ret{314'159, shared::world::chunk::hash,
+ shared::world::chunk::equal};
+ return ret;
+}
+
+static pool_t& get_pool() noexcept {
+ static pool_t ret{};
+ return ret;
+}
+
+static resources& get_resources() noexcept {
+ static struct resources ret = {.clients = get_client_map(),
+ .chunks = get_chunk_map(),
+ .pool = get_pool()};
+ return ret;
+}
+
+low_priority_lock<resources> get_resources_lock() noexcept {
+ return low_priority_lock<resources>{get_resources()};
+}
+high_priority_lock<resources> get_resources_lock_immediate() noexcept {
+ return high_priority_lock<resources>{get_resources()};
+}
+
+void quit() noexcept {
+ const auto sleep = []() {
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+ };
+
+ // we recursively post in some cases, so this check is necessary.
+ while (!get_resources_lock()->clients.empty()) {
+ sleep();
+ continue;
+ }
+ while (!get_resources_lock()->chunks.empty()) {
+ sleep();
+ continue;
+ }
+
+ get_resources_lock()->pool.join();
+ server::database::quit();
+}
+
+} // namespace resources
+} // namespace server
diff --git a/src/server/resources.hh b/src/server/resources.hh
new file mode 100644
index 0000000..578a248
--- /dev/null
+++ b/src/server/resources.hh
@@ -0,0 +1,119 @@
+#ifndef SERVER_RESOURCES_HH_
+#define SERVER_RESOURCES_HH_
+
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <unordered_map>
+
+#include <boost/asio.hpp>
+#include <boost/thread/thread_pool.hpp>
+
+#include "server/chunk_data.hh"
+#include "server/client.hh"
+#include "server/database.hh"
+#include "server/world.hh"
+#include "shared/player.hh"
+#include "shared/world.hh"
+
+namespace server {
+namespace resources {
+
+// Occasionally we need access to certain objects quickly.
+// These classes enable the server to take priority over the thread pool's work
+// when necessary. Construct a low_priority_lock when it can happen whenever,
+// and a high_priority_lock when it should happen ~ now.
+template <typename T>
+class lock_base {
+private:
+ T& obj;
+
+protected:
+ static inline std::mutex data_mutex;
+ static inline std::mutex next_mutex;
+ static inline std::mutex low_priority_mutex;
+
+protected:
+ lock_base(T& obj) noexcept : obj(obj) {}
+
+public:
+ lock_base(const lock_base&) = delete;
+ lock_base(lock_base&&) = default;
+ virtual ~lock_base(){};
+
+ T& get() noexcept { return this->obj; }
+ T& operator*() noexcept { return this->get(); }
+ T* const operator->() noexcept { return &this->get(); }
+};
+
+// https://stackoverflow.com/questions/11666610/how-to-give-priority-to-privileged-thread-in-mutex-locking
+// ty ecatmur!
+template <typename T>
+class low_priority_lock : public lock_base<T> {
+public:
+ low_priority_lock(T& t) noexcept : lock_base<T>(t) {
+ lock_base<T>::low_priority_mutex.lock();
+ lock_base<T>::next_mutex.lock();
+ lock_base<T>::data_mutex.lock();
+ lock_base<T>::next_mutex.unlock();
+ }
+ virtual ~low_priority_lock() noexcept {
+ lock_base<T>::data_mutex.unlock();
+ lock_base<T>::low_priority_mutex.unlock();
+ }
+};
+
+template <typename T>
+class high_priority_lock : public lock_base<T> {
+public:
+ high_priority_lock(T& t) noexcept : lock_base<T>(t) {
+ lock_base<T>::next_mutex.lock();
+ lock_base<T>::data_mutex.lock();
+ lock_base<T>::next_mutex.unlock();
+ }
+ virtual ~high_priority_lock() noexcept {
+ lock_base<T>::data_mutex.unlock();
+ }
+};
+
+using chunk_map_key = shared::math::coords;
+using chunk_map_value = std::unique_ptr<chunk_data>;
+using chunk_map = std::unordered_map<chunk_map_key, chunk_map_value,
+ decltype(&shared::world::chunk::hash),
+ decltype(&shared::world::chunk::equal)>;
+
+using client_map_key = shared::player::index_t;
+using client_map_value = std::unique_ptr<server::client>;
+using client_map = std::unordered_map<client_map_key, client_map_value>;
+
+using pool_t = boost::asio::thread_pool;
+
+struct resources {
+ client_map& clients;
+ chunk_map& chunks;
+ pool_t& pool;
+};
+
+void init() noexcept;
+void quit() noexcept;
+
+low_priority_lock<resources> get_resources_lock() noexcept;
+high_priority_lock<resources> get_resources_lock_immediate() noexcept;
+
+inline void associate_client_chunk(const shared::math::coords& coords,
+ client_map_value& client,
+ chunk_map_value& chunk) noexcept {
+ client->chunks.emplace(coords);
+ chunk->players.emplace(client->index);
+}
+inline void disassociate_client_chunk(const shared::math::coords& coords,
+ client_map_value& client,
+ chunk_map_value& chunk) noexcept {
+ client->chunks.erase(coords);
+ chunk->players.erase(client->index);
+}
+
+} // namespace resources
+} // namespace server
+
+#endif
diff --git a/src/server/server.cc b/src/server/server.cc
new file mode 100644
index 0000000..72faa03
--- /dev/null
+++ b/src/server/server.cc
@@ -0,0 +1,640 @@
+#include "server.hh"
+
+namespace server {
+
+static proto::packet
+make_init_packet(const resources::client_map_value& client) noexcept {
+ proto::packet packet;
+
+ const auto init_packet = packet.mutable_init_packet();
+ init_packet->set_seed(state.seed);
+ init_packet->set_draw_distance(state.draw_distance);
+ shared::net::set_player(*init_packet->mutable_localplayer(),
+ client->get_player());
+
+ return packet;
+}
+
+static proto::packet make_player_packet(const shared::player& player) noexcept {
+ proto::packet packet;
+
+ const auto player_packet = packet.mutable_player_packet();
+ shared::net::set_player(*player_packet, player);
+
+ return packet;
+}
+
+static proto::packet make_server_message_packet(const std::string& msg,
+ const bool fatal) noexcept {
+ proto::packet packet;
+
+ const auto server_message_packet = packet.mutable_server_message_packet();
+ server_message_packet->set_message(msg);
+ server_message_packet->set_fatal(fatal);
+
+ return packet;
+}
+
+static proto::packet
+make_remove_packet(const resources::client_map_value& client) noexcept {
+ proto::packet packet;
+
+ const auto remove_packet = packet.mutable_remove_player_packet();
+ remove_packet->set_index(client->index);
+
+ return packet;
+}
+
+static proto::packet make_hear_packet(const std::string& message,
+ const std::uint32_t index) noexcept {
+ proto::packet packet;
+
+ const auto hear_player_packet = packet.mutable_hear_player_packet();
+ hear_player_packet->set_index(index);
+ hear_player_packet->set_text(message);
+
+ return packet;
+}
+
+static proto::packet make_chunk_packet(const world::chunk& chunk) noexcept {
+ return chunk.packet;
+}
+
+static void block_by_tickrate() noexcept {
+ const auto tickrate = server::state.tickrate;
+ static const auto ratetime = std::chrono::milliseconds(
+ static_cast<int>((1.0 / static_cast<double>(tickrate)) * 1000.0));
+ static auto prev = std::chrono::steady_clock::now();
+ const auto now = std::chrono::steady_clock::now();
+ std::this_thread::sleep_for(ratetime - (now - prev));
+ prev = std::chrono::steady_clock::now();
+}
+
+// Creates, binds, listens on new nonblocking socket.
+static int create_listen_socket(const std::string_view address,
+ const std::string_view port) noexcept {
+ constexpr addrinfo hints = {.ai_flags = AI_PASSIVE,
+ .ai_family = AF_INET,
+ .ai_socktype = SOCK_STREAM};
+ const auto info = shared::net::get_addr_info(address, port, &hints);
+ const auto sock = shared::net::make_socket(info.get());
+ shared::net::bind_socket(sock, info.get());
+ shared::net::listen_socket(sock);
+ return sock;
+}
+
+// Gets new connections if possible based on non-blocking listener socket.
+static std::optional<shared::net::connection> make_connection(const int sock) {
+ const auto accept = shared::net::get_accept(sock);
+ if (!accept.has_value()) {
+ return std::nullopt;
+ }
+ return shared::net::connection(accept->socket);
+}
+
+static void handle_move_packet(const proto::move& packet,
+ resources::client_map_value& client) noexcept {
+ client->get_player().viewangles = {.pitch = packet.viewangles().pitch(),
+ .yaw = packet.viewangles().yaw()};
+ client->get_player().commands = packet.commands();
+}
+
+static void handle_say_packet(const proto::say& packet,
+ resources::client_map_value& client,
+ server::resources::client_map& clients) noexcept {
+ if (std::size(packet.text()) > shared::MAX_SAY_LENGTH) {
+#ifndef NDEBUG
+ shared::print::warn(
+ "server: client tried to say a message that was too long, size: " +
+ std::to_string(std::size(packet.text())) + '\n');
+
+#endif
+ return;
+ }
+
+ shared::print::message("server: player " + std::to_string(client->index) +
+ " said \"" + packet.text() + "\"\n");
+ const auto hear_packet = make_hear_packet(packet.text(), client->index);
+ for (auto& [index, client_ptr] : clients) {
+ client_ptr->connection.rsend_packet(hear_packet);
+ }
+}
+
+static void send_chunk(const server::world::chunk& chunk,
+ resources::client_map_value& client) noexcept {
+ client->connection.rsend_packet(make_chunk_packet(chunk));
+}
+
+static void send_message(const std::string& message,
+ resources::client_map_value& client,
+ const bool fatal = false) {
+ client->connection.rsend_packet(make_server_message_packet(message, fatal));
+}
+
+static void send_chunk_associated(resources::chunk_map_value& chunk_data,
+ resources::client_map& clients) noexcept {
+ if (!chunk_data->has_initialised()) {
+ return;
+ }
+
+ chunk_data->get_chunk().update();
+ for (auto& client_index : chunk_data->players) {
+ const auto find_it = clients.find(client_index);
+
+ // Should NEVER happen. If this occurs our clients are not cleaning up
+ // correctly.
+ if (find_it == std::end(clients)) {
+#ifndef NDEBUG
+ shared::print::debug(
+ "client index " + std::to_string(client_index) +
+ " was associated with a chunk, but not found\n");
+#endif
+ continue;
+ }
+
+ auto& client = find_it->second;
+ send_chunk(chunk_data->get_chunk(), client);
+ }
+}
+
+// Checks if a chunk should be removed, posting it's async writing and
+// possible removal if it should.
+static void maybe_post_chunk_rm(const shared::math::coords& coords,
+ resources::chunk_map_value& chunk_data,
+ resources::pool_t& pool) noexcept {
+ // Chunk is in a state of contructing/destructing, do not touch.
+ if (!chunk_data->has_initialised()) {
+ return;
+ }
+
+ // Players are still associated with the chunk, do not touch.
+ if (std::size(chunk_data->players) > 0) {
+ return;
+ }
+
+ // Nobody is associated with the chunk and it should be removed.
+ boost::asio::post(
+ pool, std::bind(
+ [](const shared::math::coords coords,
+ std::shared_ptr<server::world::chunk> chunk) {
+ chunk->write();
+
+ auto res_lock = resources::get_resources_lock();
+ const auto find_it = res_lock->chunks.find(coords);
+ if (find_it == std::end(res_lock->chunks)) {
+ return;
+ }
+ auto& chunk_data = find_it->second;
+
+ // Two possible states here!
+ // The chunk still has people associated, meaning someone
+ // has requested the chunk while we were writing it.
+ // Put the chunk back, and send out the old chunk to the
+ // new clients.
+ if (std::size(chunk_data->players) > 0) {
+ chunk_data->chunk.emplace(std::move(chunk));
+ send_chunk_associated(chunk_data, res_lock->clients);
+ return;
+ }
+
+ res_lock->chunks.erase(find_it);
+ },
+ coords, std::move(*chunk_data->chunk)));
+ chunk_data->chunk.reset(); // std::move is not an implicit .reset()
+}
+
+// We don't send off our chunk immediately here because generating chunks are
+// expensive - we defer it later as it gets added to a threadpool.
+static void handle_request_chunk_packet(const proto::request_chunk& packet,
+ resources::client_map_value& client,
+ resources::chunk_map& chunks,
+ resources::pool_t& pool) noexcept {
+ const shared::math::coords coords{packet.chunk_pos().x(),
+ packet.chunk_pos().z()};
+
+ if (const auto find_it = chunks.find(coords); find_it != std::end(chunks)) {
+ auto& chunk_data = find_it->second;
+ resources::associate_client_chunk(coords, client, chunk_data);
+ boost::asio::post(
+ pool,
+ std::bind(
+ [](const shared::math::coords coords,
+ const shared::player::index_t index) {
+ auto res_lock = resources::get_resources_lock();
+ auto& chunk_data = res_lock->chunks.find(coords)->second;
+
+ if (!chunk_data->has_initialised()) {
+ return; // will be sent on construction
+ }
+
+ const auto& client_it = res_lock->clients.find(index);
+ if (client_it == std::end(res_lock->clients)) {
+ return;
+ }
+ auto& client = client_it->second;
+ send_chunk(chunk_data->get_chunk(), client);
+ maybe_post_chunk_rm(coords, chunk_data, res_lock->pool);
+ },
+ coords, client->index));
+
+ return;
+ }
+
+ auto& data = chunks
+ .emplace(coords, std::make_unique<chunk_data>(
+ chunk_data{.chunk = std::nullopt}))
+ .first->second;
+ resources::associate_client_chunk(coords, client, data);
+
+ boost::asio::post(
+ pool,
+ std::bind(
+ [](const shared::math::coords coords) {
+ server::world::chunk chunk{server::state.seed, coords};
+
+ auto res_lock = resources::get_resources_lock();
+ auto& chunk_data = res_lock->chunks.find(coords)->second;
+ chunk_data->chunk.emplace(
+ std::make_unique<server::world::chunk>(std::move(chunk)));
+ send_chunk_associated(chunk_data, res_lock->clients);
+ maybe_post_chunk_rm(coords, chunk_data, res_lock->pool);
+ },
+ coords));
+}
+
+// Disassociates a client with a chunk, while performing necessary cleanups.
+static void disassociate_client_coords(const shared::math::coords& coords,
+ resources::client_map_value& client,
+ resources::chunk_map& chunks,
+ resources::pool_t& pool) noexcept {
+ // Disassociate client with chunk.
+ client->chunks.erase(coords);
+
+ // Disassociate chunk with client.
+ const auto find_it = chunks.find(coords);
+ if (find_it == std::end(chunks)) {
+ return;
+ }
+ auto& chunk_data = find_it->second;
+ chunk_data->players.erase(client->index);
+
+ // Potentially remove the chunk from the chunks (if necessary).
+ maybe_post_chunk_rm(coords, chunk_data, pool);
+}
+
+// Our client tells us if they removed a chunk.
+// This system could be abused to blow up our memory, so we set a hard
+// limit for the amount of chunks a player may have associated.
+static void handle_remove_chunk_packet(const proto::remove_chunk& packet,
+ resources::client_map_value& client,
+ resources::chunk_map& chunks,
+ resources::pool_t& pool) noexcept {
+ const shared::math::coords coords{packet.chunk_pos().x(),
+ packet.chunk_pos().z()};
+ disassociate_client_coords(coords, client, chunks, pool);
+}
+
+static void post_chunk_update(const shared::math::coords& coords,
+ resources::pool_t& pool) noexcept {
+ boost::asio::post(
+ pool, std::bind(
+ [](const shared::math::coords coords) {
+ auto res_lock = resources::get_resources_lock();
+ const auto find_it = res_lock->chunks.find(coords);
+ if (find_it == std::end(res_lock->chunks)) {
+ return;
+ }
+ auto& chunk_data = find_it->second;
+ send_chunk_associated(chunk_data, res_lock->clients);
+ },
+ coords));
+}
+
+static void modify_block(const enum shared::world::block::type block_type,
+ const glm::ivec3& block_pos,
+ const shared::math::coords& coords,
+ resources::chunk_map& chunks) noexcept {
+
+ const auto find_it = chunks.find(coords);
+ if (find_it == std::end(chunks)) {
+ return;
+ }
+
+ if (shared::world::chunk::is_outside_chunk(block_pos)) {
+ return;
+ }
+
+ auto& chunk_data = find_it->second;
+ if (!chunk_data->has_initialised()) {
+ return;
+ }
+
+ chunk_data->get_chunk().get_block(block_pos) = block_type;
+ chunk_data->get_chunk().arm_should_update();
+}
+
+static void
+handle_add_block_packet(const proto::add_block& packet,
+ [[maybe_unused]] resources::client_map_value& client,
+ server::resources::chunk_map& chunks,
+ resources::pool_t& pool) noexcept {
+ const shared::math::coords coords{packet.chunk_pos().x(),
+ packet.chunk_pos().z()};
+ const glm::ivec3 block_pos{packet.block_pos().x(), packet.block_pos().y(),
+ packet.block_pos().z()};
+ const auto block =
+ static_cast<enum shared::world::block::type>(packet.block());
+ modify_block(block, block_pos, coords, chunks);
+ post_chunk_update(coords, pool);
+}
+
+static void
+handle_remove_block_packet(const proto::remove_block& packet,
+ [[maybe_unused]] resources::client_map_value& client,
+ resources::chunk_map& chunks,
+ resources::pool_t& pool) noexcept {
+ const shared::math::coords coords{packet.chunk_pos().x(),
+ packet.chunk_pos().z()};
+ const glm::ivec3 block_pos{packet.block_pos().x(), packet.block_pos().y(),
+ packet.block_pos().z()};
+ modify_block(shared::world::block::type::air, block_pos, coords, chunks);
+ post_chunk_update(coords, pool);
+}
+
+static void handle_auth_packet(const proto::auth& packet,
+ resources::client_map_value& client,
+ resources::pool_t& pool) noexcept {
+ boost::asio::post(
+ pool,
+ std::bind(
+ [](const resources::client_map_key client_index,
+ const std::string user, const std::string pass) {
+ const auto db_plr = database::maybe_read_player(user);
+
+ auto res_lock = resources::get_resources_lock();
+ const auto find_it = res_lock->clients.find(client_index);
+ if (find_it == std::end(res_lock->clients)) {
+ return;
+ }
+ auto& client = find_it->second;
+
+ // find if we're already associated, eventually kick our client
+ // it would be nice if this wasn't a find_if
+ if (std::find_if(std::begin(res_lock->clients),
+ std::end(res_lock->clients),
+ [&](const auto& it) {
+ auto& client = it.second;
+ if (!client->has_initialised()) {
+ return false;
+ }
+ return client->index != client_index &&
+ client->get_username() == user;
+ }) != std::end(res_lock->clients)) {
+
+ client->disconnect_reason.emplace("user already in server");
+ return;
+ }
+
+ struct client::player_info player_info {
+ .username = user, .password = pass
+ };
+ if (db_plr.has_value()) {
+ auto& [player, db_password] = *db_plr;
+
+ if (db_password != pass) {
+ client->disconnect_reason.emplace("bad password");
+ return;
+ }
+
+ player_info.player = shared::net::get_player(player);
+ player_info.player.index = client_index;
+ } else {
+ // TODO: Find a random spawn chunk and put the player
+ // on the highest block. Because we don't want to gen chunks
+ // while blocking, we can't do this here. For now, we'll
+ // just put the player at a high spot.
+ player_info.player =
+ shared::player{.index = client_index,
+ .local_pos = {0.0f, 140.0f, 0.0f}};
+ }
+ client->player_info.emplace(
+ std::make_shared<struct client::player_info>(
+ std::move(player_info)));
+
+ client->connection.rsend_packet(make_init_packet(client));
+ },
+ client->index, std::move(packet.username()),
+ std::move(packet.password())));
+}
+
+// Get new packets from clients, this will change client data and worldata.
+static void parse_client_packets(resources::client_map_value& client,
+ resources::resources& res) noexcept {
+ while (auto packet = client->connection.recv_packet()) {
+ if (packet->has_auth_packet()) {
+ handle_auth_packet(packet->auth_packet(), client, res.pool);
+ } else if (packet->has_move_packet()) {
+ handle_move_packet(packet->move_packet(), client);
+ } else if (packet->has_say_packet()) {
+ handle_say_packet(packet->say_packet(), client, res.clients);
+ } else if (packet->has_request_chunk_packet()) {
+ handle_request_chunk_packet(packet->request_chunk_packet(), client,
+ res.chunks, res.pool);
+ } else if (packet->has_remove_chunk_packet()) {
+ handle_remove_chunk_packet(packet->remove_chunk_packet(), client,
+ res.chunks, res.pool);
+ } else if (packet->has_add_block_packet()) {
+ handle_add_block_packet(packet->add_block_packet(), client,
+ res.chunks, res.pool);
+ } else if (packet->has_remove_block_packet()) {
+ handle_remove_block_packet(packet->remove_block_packet(), client,
+ res.chunks, res.pool);
+ }
+#ifndef NDEBUG
+ else {
+ shared::print::warn("server: unhandled packet type\n");
+ }
+#endif
+ }
+}
+
+[[maybe_unused]] // [[actually_used]]
+static void
+send_remove_packets(server::resources::client_map_value& remove_client,
+ server::resources::client_map& clients) noexcept {
+ const auto remove_packet = make_remove_packet(remove_client);
+ for (auto& [index, client_ptr] : clients) {
+ client_ptr->connection.rsend_packet(remove_packet);
+ }
+}
+
+static void handle_new_connections(shared::net::connection& connection,
+ resources::resources& res) noexcept {
+ shared::print::message("server: got connection from " +
+ connection.get_address() + '\n');
+
+ // Add the client, waiting for an auth packet.
+ static uint32_t index = 0;
+ res.clients.insert_or_assign(index, server::resources::client_map_value{
+ std::make_unique<server::client>(
+ std::move(connection), index)});
+ ++index;
+}
+
+static void move_client(resources::client_map_value& client,
+ resources::chunk_map& chunks) noexcept {
+ // shared::movement::fly(client.player, client.commands);
+ movement::move(*client, chunks);
+}
+
+static void send_client_packets(resources::client_map_value& client,
+ resources::resources& res) noexcept {
+ // TODO PVS, as simple as checking if a client has our chunk pos in their
+ // associated chunks list
+ for (auto& [index, c] : res.clients) {
+ if (!c->has_initialised()) {
+ continue;
+ }
+ if (!client->chunks.contains(c->get_player().chunk_pos)) {
+ continue;
+ }
+
+ client->connection.usend_packet(make_player_packet(c->get_player()));
+ }
+}
+
+// returns a dc reason that will be printed and sent to the client
+[[maybe_unused]] std::optional<std::string>
+get_sent_disconnect_reason(const resources::client_map_value& client) noexcept {
+ if (!client->connection.good()) {
+ return client->connection.get_bad_reason();
+ }
+
+ if (client->chunks.size() >
+ unsigned(state.draw_distance * state.draw_distance * 4)) {
+ return "too many chunks associated with client";
+ }
+
+ return client->disconnect_reason;
+}
+
+static void remove_bad_clients(resources::resources& res) noexcept {
+ for (auto& [index, client] : res.clients) {
+ const auto reason = get_sent_disconnect_reason(client);
+ if (reason == std::nullopt) {
+ continue;
+ }
+
+ if (client->disconnecting) {
+ continue;
+ }
+ client->disconnecting = true;
+
+ shared::print::message("server: dropped " +
+ client->connection.get_address() + " for \"" +
+ *reason + "\"\n");
+ send_message("disconnected for " + *reason, client, true);
+ send_remove_packets(client, res.clients);
+
+ boost::asio::post(
+ res.pool,
+ std::bind(
+ [](const auto rm_coords,
+ const resources::client_map_key client_index,
+ const decltype(client::player_info) plr_info) {
+ // Write the player to the db before we do any locks.
+ if (plr_info.has_value()) {
+ database::write_player(
+ (*plr_info)->username, (*plr_info)->password,
+ make_player_packet((*plr_info)->player)
+ .player_packet());
+ }
+
+ // cleanup associated chunks
+ for (const auto& coords : rm_coords) {
+ auto res_lock = resources::get_resources_lock();
+
+ const auto find_it = res_lock->chunks.find(coords);
+ if (find_it == std::end(res_lock->chunks)) {
+ return;
+ }
+ auto& chunk_data = find_it->second;
+ chunk_data->players.erase(client_index);
+
+ maybe_post_chunk_rm(coords, chunk_data, res_lock->pool);
+ }
+
+ // final cleanup, now we may reconnect
+ resources::get_resources_lock()->clients.erase(
+ client_index);
+ },
+ std::move(client->chunks), client->index,
+ std::move(client->player_info)));
+ client->chunks.clear(); // just being explicit
+ client->player_info.reset();
+ }
+}
+
+static void process_resources(resources::resources& res) noexcept {
+ // Get new packets from clients. The contents of these packets will
+ // determine how the world + client data changes.
+ for (auto& [index, client] : res.clients) {
+ parse_client_packets(client, res);
+ }
+
+ // Move clients via their (hopefully updated) command.
+ for (auto& [index, client] : res.clients) {
+ move_client(client, res.chunks);
+ }
+
+ // Send packets which are sent once per tick, as of now the player
+ // struct of other clients.
+ for (auto& [index, client] : res.clients) {
+ send_client_packets(client, res);
+ }
+
+ // Delete bad connections, print if it happens. Also sends a remove
+ // to all clients per client removed.
+ remove_bad_clients(res);
+}
+
+void main(const std::string_view address, const std::string_view port) {
+ const auto rsock = create_listen_socket(address, port); // reliable socket
+
+ server::resources::init();
+ has_initialised = true;
+ shared::print::notify("server: started at " + std::string{address} + ':' +
+ std::string{port} + '\n');
+
+ // Server has a tickrate, we will use non-blocking polling at this
+ // tickrate.
+ for (; !shared::should_exit; block_by_tickrate()) {
+ // Lock resources with a higher priority than work on the pool
+ auto resources_lock = server::resources::get_resources_lock_immediate();
+
+ // Get new connections and add them to our clients map.
+ while (auto connection = make_connection(rsock)) {
+ handle_new_connections(connection.value(), *resources_lock);
+ }
+
+ // Process data from resources including client packets, chunk
+ // updates, etc.
+ process_resources(*resources_lock);
+ }
+
+ // Cleanup of clients.
+ {
+ auto res_lock = resources::get_resources_lock();
+ std::ranges::for_each(res_lock->clients, [](const auto& it) {
+ const auto& client = it.second;
+ client->disconnect_reason.emplace("server shutting down");
+ });
+ remove_bad_clients(*res_lock);
+ }
+
+ shared::print::notify("server: writing world data\n");
+ server::resources::quit();
+ shared::print::notify("server: gracefully exited\n");
+}
+
+} // namespace server
diff --git a/src/server/server.hh b/src/server/server.hh
new file mode 100644
index 0000000..bd2d5fd
--- /dev/null
+++ b/src/server/server.hh
@@ -0,0 +1,37 @@
+#ifndef SERVER_SERVER_HH_
+#define SERVER_SERVER_HH_
+
+#include <algorithm>
+#include <atomic>
+#include <chrono>
+#include <csignal>
+#include <deque>
+#include <iostream>
+#include <memory>
+#include <queue>
+#include <ranges>
+#include <string>
+#include <string_view>
+#include <thread>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include "server/client.hh"
+#include "server/database.hh"
+#include "server/movement.hh"
+#include "server/resources.hh"
+#include "server/world.hh"
+#include "shared/net/net.hh"
+#include "shared/net/proto.hh"
+#include "shared/player.hh"
+#include "shared/shared.hh"
+
+namespace server {
+inline std::atomic<bool> has_initialised;
+
+void main(const std::string_view address, const std::string_view port);
+} // namespace server
+
+#endif
diff --git a/src/server/shared.cc b/src/server/shared.cc
new file mode 100644
index 0000000..fbdbcaa
--- /dev/null
+++ b/src/server/shared.cc
@@ -0,0 +1 @@
+#include "server/shared.hh"
diff --git a/src/server/shared.hh b/src/server/shared.hh
new file mode 100644
index 0000000..ffbf0f9
--- /dev/null
+++ b/src/server/shared.hh
@@ -0,0 +1,19 @@
+#ifndef SERVER_SHARED_HH_
+#define SERVER_SHARED_HH_
+
+#include <cstdint>
+#include <string>
+
+namespace server {
+
+struct state {
+ int draw_distance = 32;
+ std::uint64_t seed = 123456789;
+ std::string directory = "world/";
+ std::uint32_t tickrate = 144;
+};
+inline state state;
+
+} // namespace server
+
+#endif
diff --git a/src/server/world.cc b/src/server/world.cc
new file mode 100644
index 0000000..6527d6c
--- /dev/null
+++ b/src/server/world.cc
@@ -0,0 +1,53 @@
+#include "server/world.hh"
+
+namespace server {
+namespace world {
+
+proto::packet chunk::make_chunk_packet() const noexcept {
+
+ proto::packet ret_packet;
+
+ auto chunk_packet = ret_packet.mutable_chunk_packet();
+
+ auto chunk_pos = chunk_packet->mutable_chunk_pos();
+ const shared::math::coords this_chunk_pos = this->get_pos();
+ chunk_pos->set_x(this_chunk_pos.x);
+ chunk_pos->set_z(this_chunk_pos.z);
+
+ // Since protobuf can store at minimum uint32, we mash four of our
+ // uint_8 chunk blocks into a single uint32.
+ static_assert(shared::world::chunk::VOLUME % 4 == 0);
+ for (unsigned i = 0u; i < shared::world::chunk::VOLUME / 4u; ++i) {
+ std::uint32_t packed_blocks = 0u;
+
+ for (unsigned j = 0; j < 4; ++j) {
+ const auto block =
+ static_cast<std::uint8_t>(this->blocks[i * 4 + j].type);
+ packed_blocks |= static_cast<unsigned>(block << j * 8);
+ }
+
+ chunk_packet->add_blocks(packed_blocks);
+ }
+
+ return ret_packet;
+}
+
+static std::optional<shared::world::chunk::block_array>
+maybe_get_blocks(const shared::math::coords& coords) {
+ const auto chunk = database::maybe_read_chunk(coords);
+ if (!chunk.has_value()) {
+ return std::nullopt;
+ }
+ return shared::world::chunk::make_blocks_from_chunk(chunk.value());
+}
+
+chunk::chunk(const std::uint64_t& seed,
+ const shared::math::coords& coords) noexcept
+ : shared::world::chunk(seed, coords, maybe_get_blocks(coords)) {
+ this->update();
+}
+
+chunk::~chunk() noexcept { this->write(); }
+
+} // namespace world
+} // namespace server
diff --git a/src/server/world.hh b/src/server/world.hh
new file mode 100644
index 0000000..134f63b
--- /dev/null
+++ b/src/server/world.hh
@@ -0,0 +1,59 @@
+#ifndef SERVER_WORLD_HH_
+#define SERVER_WORLD_HH_
+
+#include <cstdint>
+
+#include "server/database.hh"
+#include "server/shared.hh"
+#include "shared/math.hh"
+#include "shared/net/net.hh"
+#include "shared/net/proto.hh"
+#include "shared/player.hh"
+#include "shared/world.hh"
+
+namespace server {
+namespace world {
+
+class chunk : public shared::world::chunk {
+private:
+ bool should_write = false;
+ bool should_update = true;
+
+public:
+ proto::packet packet; // Packet ready for sending, updated in update().
+ void arm_should_update() noexcept {
+ this->should_update = this->should_write = true;
+ }
+ bool get_should_update() noexcept { return this->should_update; }
+
+private:
+ proto::packet make_chunk_packet() const noexcept;
+
+public:
+ // Attempt to read the file using protobuf, otherwise create a new chunk.
+ // chunk(const chunk&) = delete;
+ chunk(const uint64_t& seed, const shared::math::coords& coords) noexcept;
+ ~chunk() noexcept;
+
+ // Update the chunk_packet associated with the chunk if necessary.
+ void update() noexcept {
+ if (!this->should_update) {
+ return;
+ }
+ this->packet = make_chunk_packet();
+ this->should_update = false;
+ }
+ // calling .write before the destrutor will not result in a double write
+ void write() noexcept {
+ if (!this->should_write) {
+ return;
+ }
+ server::database::write_chunk(this->pos, this->packet.chunk_packet());
+ this->should_write = false;
+ }
+};
+
+} // namespace world
+} // namespace server
+
+#endif