summaryrefslogtreecommitdiffstats
path: root/game/server
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/server
parent02294547dcde0d4ad76e229106702261e9f10a51 (diff)
downloadvoxelius-3bf42c6ff3805a0d42bbc661794a95ff31bedc26.tar.bz2
voxelius-3bf42c6ff3805a0d42bbc661794a95ff31bedc26.zip
Add whatever I was working on for the last month
Diffstat (limited to 'game/server')
-rw-r--r--game/server/CMakeLists.txt39
-rw-r--r--game/server/chat.cc51
-rw-r--r--game/server/chat.hh16
-rw-r--r--game/server/game.cc145
-rw-r--r--game/server/game.hh26
-rw-r--r--game/server/globals.cc17
-rw-r--r--game/server/globals.hh25
-rw-r--r--game/server/inhabited.hh7
-rw-r--r--game/server/main.cc103
-rw-r--r--game/server/overworld.cc266
-rw-r--r--game/server/overworld.hh51
-rw-r--r--game/server/pch.hh9
-rw-r--r--game/server/receive.cc184
-rw-r--r--game/server/receive.hh10
-rw-r--r--game/server/sessions.cc402
-rw-r--r--game/server/sessions.hh53
-rw-r--r--game/server/status.cc26
-rw-r--r--game/server/status.hh10
-rw-r--r--game/server/universe.cc214
-rw-r--r--game/server/universe.hh25
-rw-r--r--game/server/unloader.cc77
-rw-r--r--game/server/unloader.hh14
-rw-r--r--game/server/vserver.icobin0 -> 60201 bytes
-rw-r--r--game/server/vserver.rc1
-rw-r--r--game/server/whitelist.cc97
-rw-r--r--game/server/whitelist.hh27
-rw-r--r--game/server/worldgen.cc128
-rw-r--r--game/server/worldgen.hh16
28 files changed, 2039 insertions, 0 deletions
diff --git a/game/server/CMakeLists.txt b/game/server/CMakeLists.txt
new file mode 100644
index 0000000..c2b6c06
--- /dev/null
+++ b/game/server/CMakeLists.txt
@@ -0,0 +1,39 @@
+add_executable(vserver
+ "${CMAKE_CURRENT_LIST_DIR}/chat.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/chat.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}/inhabited.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/main.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/overworld.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/overworld.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/pch.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/receive.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/receive.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/sessions.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/sessions.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/status.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/status.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/universe.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/universe.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/unloader.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/unloader.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/whitelist.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/whitelist.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/worldgen.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/worldgen.hh")
+target_compile_features(vserver PUBLIC cxx_std_17)
+target_include_directories(vserver PUBLIC "${DEPS_INCLUDE_DIR}")
+target_include_directories(vserver PRIVATE "${PROJECT_SOURCE_DIR}")
+target_include_directories(vserver PRIVATE "${PROJECT_SOURCE_DIR}/game")
+target_precompile_headers(vserver PRIVATE "${CMAKE_CURRENT_LIST_DIR}/pch.hh")
+target_link_libraries(vserver PUBLIC shared)
+
+if(WIN32)
+ enable_language(RC)
+ target_sources(vserver PRIVATE "${CMAKE_CURRENT_LIST_DIR}/vserver.rc")
+endif()
+
+install(TARGETS vserver RUNTIME DESTINATION ".")
diff --git a/game/server/chat.cc b/game/server/chat.cc
new file mode 100644
index 0000000..709d21b
--- /dev/null
+++ b/game/server/chat.cc
@@ -0,0 +1,51 @@
+#include "server/pch.hh"
+#include "server/chat.hh"
+
+#include "server/globals.hh"
+#include "server/sessions.hh"
+#include "shared/protocol.hh"
+
+static void on_chat_message_packet(const protocol::ChatMessage &packet)
+{
+ if(packet.type == protocol::ChatMessage::TEXT_MESSAGE) {
+ if(auto session = sessions::find(packet.peer))
+ server_chat::broadcast(packet.message.c_str(), session->client_username.c_str());
+ else server_chat::broadcast(packet.message.c_str(), packet.sender.c_str());
+ }
+}
+
+void server_chat::init(void)
+{
+ globals::dispatcher.sink<protocol::ChatMessage>().connect<&on_chat_message_packet>();
+}
+
+void server_chat::broadcast(const char *message)
+{
+ server_chat::broadcast(message, "server");
+}
+
+void server_chat::broadcast(const char *message, const char *sender)
+{
+ protocol::ChatMessage packet;
+ packet.type = protocol::ChatMessage::TEXT_MESSAGE;
+ packet.message = std::string(message);
+ packet.sender = std::string(sender);
+
+ protocol::broadcast(globals::server_host, protocol::encode(packet));
+
+ spdlog::info("<{}> {}", sender, message);
+}
+
+void server_chat::send(Session *session, const char *message)
+{
+ server_chat::send(session, message, "server");
+}
+
+void server_chat::send(Session *session, const char *message, const char *sender)
+{
+ protocol::ChatMessage packet;
+ packet.type = protocol::ChatMessage::TEXT_MESSAGE;
+ packet.message = std::string(message);
+ packet.sender = std::string(sender);
+ protocol::broadcast(globals::server_host, protocol::encode(packet));
+}
diff --git a/game/server/chat.hh b/game/server/chat.hh
new file mode 100644
index 0000000..7efc7ff
--- /dev/null
+++ b/game/server/chat.hh
@@ -0,0 +1,16 @@
+#ifndef SERVER_CHAT_HH
+#define SERVER_CHAT_HH 1
+#pragma once
+
+struct Session;
+
+namespace server_chat
+{
+void init(void);
+void broadcast(const char *message);
+void broadcast(const char *message, const char *sender);
+void send(Session *session, const char *message);
+void send(Session *session, const char *message, const char *sender);
+} // namespace server_chat
+
+#endif /* SERVER_CHAT_HH */
diff --git a/game/server/game.cc b/game/server/game.cc
new file mode 100644
index 0000000..9d690f2
--- /dev/null
+++ b/game/server/game.cc
@@ -0,0 +1,145 @@
+#include "server/pch.hh"
+#include "server/game.hh"
+
+#include "core/cmdline.hh"
+#include "core/config.hh"
+#include "core/constexpr.hh"
+#include "core/crc64.hh"
+#include "core/epoch.hh"
+
+#include "shared/collision.hh"
+#include "shared/dimension.hh"
+#include "shared/game_items.hh"
+#include "shared/game_voxels.hh"
+#include "shared/gravity.hh"
+#include "shared/head.hh"
+#include "shared/player.hh"
+#include "shared/protocol.hh"
+#include "shared/splash.hh"
+#include "shared/stasis.hh"
+#include "shared/transform.hh"
+#include "shared/velocity.hh"
+
+#include "server/chat.hh"
+#include "server/globals.hh"
+#include "server/receive.hh"
+#include "server/sessions.hh"
+#include "server/status.hh"
+#include "server/universe.hh"
+#include "server/unloader.hh"
+#include "server/whitelist.hh"
+
+ConfigUnsigned server_game::view_distance(4U, 4U, 32U);
+
+std::uint64_t server_game::password_hash = UINT64_MAX;
+
+static ConfigNumber<enet_uint16> listen_port(protocol::PORT, 1024U, UINT16_MAX);
+static ConfigUnsigned status_peers(2U, 1U, 16U);
+static ConfigString password_string("");
+
+void server_game::init(void)
+{
+ globals::server_config.add_value("game.listen_port", listen_port);
+ globals::server_config.add_value("game.status_peers", status_peers);
+ globals::server_config.add_value("game.password", password_string);
+ globals::server_config.add_value("game.view_distance", server_game::view_distance);
+
+ sessions::init();
+
+ whitelist::init();
+
+ splash::init();
+
+ status::init();
+
+ server_chat::init();
+ server_recieve::init();
+
+ unloader::init();
+ universe::init();
+}
+
+void server_game::init_late(void)
+{
+ server_game::password_hash = crc64::get(password_string.get());
+
+ sessions::init_late();
+
+ whitelist::init_late();
+
+ ENetAddress address;
+ address.host = ENET_HOST_ANY;
+ address.port = listen_port.get_value();
+
+ globals::server_host = enet_host_create(&address, sessions::max_players.get_value() + status_peers.get_value(), 1, 0, 0);
+ globals::server_host->checksum = &enet_crc32;
+
+ if(!globals::server_host) {
+ spdlog::critical("game: unable to setup an ENet host");
+ std::terminate();
+ }
+
+ spdlog::info("game: host: {} player + {} status peers", sessions::max_players.get_value(), status_peers.get_value());
+ spdlog::info("game: host: listening on UDP port {}", address.port);
+
+ game_voxels::populate();
+ game_items::populate();
+
+ unloader::init_late();
+ universe::init_late();
+
+ sessions::init_post_universe();
+}
+
+void server_game::deinit(void)
+{
+ protocol::Disconnect packet;
+ packet.reason = "protocol.server_shutdown";
+ protocol::broadcast(globals::server_host, protocol::encode(packet));
+
+ whitelist::deinit();
+
+ sessions::deinit();
+
+ enet_host_flush(globals::server_host);
+ enet_host_service(globals::server_host, nullptr, 500);
+ enet_host_destroy(globals::server_host);
+
+ universe::deinit();
+}
+
+void server_game::fixed_update(void)
+{
+ // FIXME: threading
+ for(auto dimension : globals::dimensions) {
+ CollisionComponent::fixed_update(dimension.second);
+ VelocityComponent::fixed_update(dimension.second);
+ TransformComponent::fixed_update(dimension.second);
+ GravityComponent::fixed_update(dimension.second);
+ StasisComponent::fixed_update(dimension.second);
+ }
+}
+
+void server_game::fixed_update_late(void)
+{
+ ENetEvent enet_event;
+
+ while(0 < enet_host_service(globals::server_host, &enet_event, 0)) {
+ if(enet_event.type == ENET_EVENT_TYPE_DISCONNECT) {
+ sessions::destroy(sessions::find(enet_event.peer));
+ sessions::refresh_scoreboard();
+ continue;
+ }
+
+ if(enet_event.type == ENET_EVENT_TYPE_RECEIVE) {
+ protocol::decode(globals::dispatcher, enet_event.packet, enet_event.peer);
+ enet_packet_destroy(enet_event.packet);
+ continue;
+ }
+ }
+
+ // FIXME: threading
+ for(auto dimension : globals::dimensions) {
+ unloader::fixed_update_late(dimension.second);
+ }
+}
diff --git a/game/server/game.hh b/game/server/game.hh
new file mode 100644
index 0000000..98c6bf3
--- /dev/null
+++ b/game/server/game.hh
@@ -0,0 +1,26 @@
+#ifndef SERVER_GAME_HH
+#define SERVER_GAME_HH 1
+#pragma once
+
+class ConfigUnsigned;
+
+namespace server_game
+{
+extern ConfigUnsigned view_distance;
+} // namespace server_game
+
+namespace server_game
+{
+extern std::uint64_t password_hash;
+} // namespace server_game
+
+namespace server_game
+{
+void init(void);
+void init_late(void);
+void deinit(void);
+void fixed_update(void);
+void fixed_update_late(void);
+} // namespace server_game
+
+#endif /* SERVER_GAME_HH */
diff --git a/game/server/globals.cc b/game/server/globals.cc
new file mode 100644
index 0000000..e0f50da
--- /dev/null
+++ b/game/server/globals.cc
@@ -0,0 +1,17 @@
+#include "server/pch.hh"
+#include "server/globals.hh"
+
+#include "core/config.hh"
+
+#include "shared/protocol.hh"
+
+Config globals::server_config;
+
+ENetHost *globals::server_host;
+
+bool globals::is_running;
+unsigned int globals::tickrate;
+std::uint64_t globals::tickrate_dt;
+
+Dimension *globals::spawn_dimension;
+std::unordered_map<std::string, Dimension *> globals::dimensions;
diff --git a/game/server/globals.hh b/game/server/globals.hh
new file mode 100644
index 0000000..f2fc256
--- /dev/null
+++ b/game/server/globals.hh
@@ -0,0 +1,25 @@
+#ifndef SERVER_GLOBALS_HH
+#define SERVER_GLOBALS_HH 1
+#pragma once
+
+#include "shared/globals.hh"
+
+class Config;
+
+class Dimension;
+
+namespace globals
+{
+extern Config server_config;
+
+extern ENetHost *server_host;
+
+extern bool is_running;
+extern unsigned int tickrate;
+extern std::uint64_t tickrate_dt;
+
+extern Dimension *spawn_dimension;
+extern std::unordered_map<std::string, Dimension *> dimensions;
+} // namespace globals
+
+#endif /* SERVER_GLOBALS_HH */
diff --git a/game/server/inhabited.hh b/game/server/inhabited.hh
new file mode 100644
index 0000000..c68ddaa
--- /dev/null
+++ b/game/server/inhabited.hh
@@ -0,0 +1,7 @@
+#ifndef SERVER_INHABITED_HH
+#define SERVER_INHABITED_HH 1
+#pragma once
+
+struct InhabitedComponent final {};
+
+#endif /* SERVER_INHABITED_HH */
diff --git a/game/server/main.cc b/game/server/main.cc
new file mode 100644
index 0000000..64bca10
--- /dev/null
+++ b/game/server/main.cc
@@ -0,0 +1,103 @@
+#include "server/pch.hh"
+
+#include "core/binfile.hh"
+#include "core/cmdline.hh"
+#include "core/config.hh"
+#include "core/constexpr.hh"
+#include "core/epoch.hh"
+#include "core/image.hh"
+#include "core/resource.hh"
+#include "core/version.hh"
+
+#include "shared/game.hh"
+#include "shared/protocol.hh"
+#include "shared/threading.hh"
+
+#include "server/game.hh"
+#include "server/globals.hh"
+
+static ConfigUnsigned server_tickrate(protocol::TICKRATE, 10U, 300U);
+
+static void on_termination_signal(int)
+{
+ spdlog::warn("server: received termination signal");
+ globals::is_running = false;
+}
+
+int main(int argc, char **argv)
+{
+ cmdline::create(argc, argv);
+
+ shared_game::init(argc, argv);
+
+ spdlog::info("Voxelius {} server", PROJECT_VERSION_STRING);
+
+ globals::fixed_frametime = 0.0f;
+ globals::fixed_frametime_avg = 0.0f;
+ globals::fixed_frametime_us = 0;
+ globals::fixed_framecount = 0;
+
+ globals::curtime = epoch::microseconds();
+
+ globals::is_running = true;
+
+ std::signal(SIGINT, &on_termination_signal);
+ std::signal(SIGTERM, &on_termination_signal);
+
+ server_game::init();
+
+ threading::init();
+
+ globals::server_config.add_value("server.tickrate", server_tickrate);
+ globals::server_config.load_file("server.conf");
+ globals::server_config.load_cmdline();
+
+ globals::tickrate = server_tickrate.get_value();
+ globals::tickrate_dt = static_cast<std::uint64_t>(1000000.0f / static_cast<float>(globals::tickrate));
+
+ server_game::init_late();
+
+ std::uint64_t last_curtime = globals::curtime;
+
+ while(globals::is_running) {
+ globals::curtime = epoch::microseconds();
+
+ globals::fixed_frametime_us = globals::curtime - last_curtime;
+ globals::fixed_frametime = static_cast<float>(globals::fixed_frametime_us) / 1000000.0f;
+ globals::fixed_frametime_avg += globals::fixed_frametime;
+ globals::fixed_frametime_avg *= 0.5f;
+
+ last_curtime = globals::curtime;
+
+ server_game::fixed_update();
+ server_game::fixed_update_late();
+
+ globals::dispatcher.update();
+
+ globals::fixed_framecount += 1;
+
+ std::this_thread::sleep_for(std::chrono::microseconds(globals::tickrate_dt));
+
+ resource::soft_cleanup<BinFile>();
+ resource::soft_cleanup<Image>();
+
+ threading::update();
+ }
+
+ server_game::deinit();
+
+ resource::hard_cleanup<BinFile>();
+ resource::hard_cleanup<Image>();
+
+ threading::deinit();
+
+ spdlog::info("server: shutdown after {} frames", globals::fixed_framecount);
+ spdlog::info("server: average framerate: {:.03f} TPS", 1.0f / globals::fixed_frametime_avg);
+ spdlog::info("server: average frametime: {:.03f} MSPT", 1000.0f * globals::fixed_frametime_avg);
+
+ globals::server_config.save_file("server.conf");
+
+ shared_game::deinit();
+
+ return EXIT_SUCCESS;
+}
diff --git a/game/server/overworld.cc b/game/server/overworld.cc
new file mode 100644
index 0000000..87d8dd6
--- /dev/null
+++ b/game/server/overworld.cc
@@ -0,0 +1,266 @@
+#include "server/pch.hh"
+#include "server/overworld.hh"
+
+#include "shared/coord.hh"
+#include "shared/game_voxels.hh"
+#include "shared/voxel_storage.hh"
+
+Overworld::Overworld(const char *name) : Dimension(name, -30.0f)
+{
+
+}
+
+void Overworld::init(Config &config)
+{
+ m_terrain_variation.set_value(64);
+ m_bottommost_chunk.set_value(-4);
+ m_enable_surface.set_value(true);
+ m_enable_carvers.set_value(true);
+ m_enable_features.set_value(true);
+
+ config.add_value("overworld.terrain_variation", m_terrain_variation);
+ config.add_value("overworld.bottommost_chunk", m_bottommost_chunk);
+ config.add_value("overworld.enable_surface", m_enable_surface);
+ config.add_value("overworld.enable_carvers", m_enable_carvers);
+ config.add_value("overworld.enable_features", m_enable_features);
+}
+
+void Overworld::init_late(std::uint64_t global_seed)
+{
+ m_twister.seed(global_seed);
+
+ m_fnl_terrain = fnlCreateState();
+ m_fnl_terrain.seed = static_cast<int>(m_twister());
+ m_fnl_terrain.noise_type = FNL_NOISE_OPENSIMPLEX2S;
+ m_fnl_terrain.fractal_type = FNL_FRACTAL_FBM;
+ m_fnl_terrain.frequency = 0.005f;
+ m_fnl_terrain.octaves = 4;
+
+ m_fnl_caves_a = fnlCreateState();
+ m_fnl_caves_a.seed = static_cast<int>(m_twister());
+ m_fnl_caves_a.noise_type = FNL_NOISE_PERLIN;
+ m_fnl_caves_a.frequency = 0.0075f;
+
+ m_fnl_caves_b = fnlCreateState();
+ m_fnl_caves_b.seed = static_cast<int>(m_twister());
+ m_fnl_caves_b.noise_type = FNL_NOISE_PERLIN;
+ m_fnl_caves_b.frequency = 0.0075f;
+
+ // This ensures the metadata is cleaned
+ // between different world loads that happen
+ // on singleplayer; this should fix retained
+ // entropy bug we've just found out this morning
+ m_metadata.clear();
+}
+
+bool Overworld::generate(const chunk_pos &cpos, VoxelStorage &voxels)
+{
+ if(cpos.y < m_bottommost_chunk.get_value()) {
+ // If the player asks the generator
+ // to generate a lot of stuff below
+ // the surface, it will happily chew
+ // through all the server threads
+ return false;
+ }
+
+ voxels.fill(NULL_VOXEL_ID);
+
+ m_mutex.lock();
+ generate_terrain(cpos, voxels);
+ m_mutex.unlock();
+
+ if(m_enable_surface.get_value()) {
+ m_mutex.lock();
+ generate_surface(cpos, voxels);
+ m_mutex.unlock();
+ }
+
+ if(m_enable_carvers.get_value()) {
+ m_mutex.lock();
+ generate_carvers(cpos, voxels);
+ m_mutex.unlock();
+ }
+
+ if(m_enable_features.get_value()) {
+ m_mutex.lock();
+ generate_features(cpos, voxels);
+ m_mutex.unlock();
+ }
+
+ return true;
+}
+
+float Overworld::get_noise(const voxel_pos &vpos, std::int64_t variation)
+{
+ // Terrain noise is also sampled when we're placing
+ // surface voxels; this is needed becuase chunks don't
+ // know if they have generated neighbours or not.
+ return variation * fnlGetNoise3D(&m_fnl_terrain, vpos.x, vpos.y, vpos.z) - vpos.y;
+}
+
+Metadata_2501 &Overworld::get_metadata(const worldgen_chunk_pos &cpos)
+{
+ const auto it = m_metadata.find(cpos);
+
+ if(it == m_metadata.cend()) {
+
+ auto &metadata = m_metadata.insert_or_assign(cpos, Metadata_2501()).first->second;
+ for(std::size_t i = 0; i < CHUNK_AREA; metadata.entropy[i++] = m_twister());
+ metadata.heightmap.fill(INT64_MIN);
+
+ return metadata;
+ }
+
+ return it->second;
+}
+
+void Overworld::generate_terrain(const chunk_pos &cpos, VoxelStorage &voxels)
+{
+ auto &metadata = get_metadata(worldgen_chunk_pos(cpos.x, cpos.z));
+
+ for(std::size_t index = 0; index < CHUNK_VOLUME; index += 1) {
+ auto lpos = coord::to_local(index);
+ auto vpos = coord::to_voxel(cpos, lpos);
+ auto hdx = static_cast<std::size_t>(lpos.x + lpos.z * CHUNK_SIZE);
+
+ // Sampling 3D noise like that is expensive; to
+ // avoid unnecessary noise sampling we can speculate
+ // where the terrain would be guaranteed to be solid or air
+ if(cxpr::abs(vpos.y) >= (m_terrain_variation.get_value() + 1)) {
+ if(vpos.y < INT64_C(0)) {
+ if(vpos.y > metadata.heightmap[hdx])
+ metadata.heightmap[hdx] = vpos.y;
+ voxels[index] = game_voxels::stone;
+ }
+
+ continue;
+ }
+
+ if(get_noise(vpos, m_terrain_variation.get_value()) > 0.0f) {
+ if(vpos.y > metadata.heightmap[hdx])
+ metadata.heightmap[hdx] = vpos.y;
+ voxels[index] = game_voxels::stone;
+ continue;
+ }
+ }
+}
+
+void Overworld::generate_surface(const chunk_pos &cpos, VoxelStorage &voxels)
+{
+ auto &metadata = get_metadata(worldgen_chunk_pos(cpos.x, cpos.z));
+
+ for(std::size_t index = 0; index < CHUNK_VOLUME; index += 1) {
+ auto lpos = coord::to_local(index);
+ auto vpos = coord::to_voxel(cpos, lpos);
+
+ // Same speculation check applies here albeit
+ // a little differently - there's no surface to
+ // place voxels on above variation range
+ if(cxpr::abs(vpos.y) >= (m_terrain_variation.get_value() + 1)) {
+ continue;
+ }
+
+ // Surface voxel checks only apply for solid voxels;
+ // it's kind of obvious you can't replace air with grass
+ if(voxels[index] == NULL_VOXEL_ID) {
+ continue;
+ }
+
+ std::size_t depth = 0;
+
+ for(local_pos::value_type dy = 0; dy < 5; dy += 1) {
+ auto dlpos = local_pos(lpos.x, lpos.y + dy + 1, lpos.z);
+ auto dvpos = coord::to_voxel(cpos, dlpos);
+ auto didx = coord::to_index(dlpos);
+
+ if(dlpos.y >= CHUNK_SIZE) {
+ if(get_noise(dvpos, m_terrain_variation.get_value()) <= 0.0f)
+ break;
+ depth += 1;
+ }
+ else {
+ if(voxels[didx] == NULL_VOXEL_ID)
+ break;
+ depth += 1;
+ }
+ }
+
+ if(depth < 5) {
+ if(depth == 0)
+ voxels[index] = game_voxels::grass;
+ else voxels[index] = game_voxels::dirt;
+ }
+ }
+}
+
+void Overworld::generate_carvers(const chunk_pos &cpos, VoxelStorage &voxels)
+{
+ auto &metadata = get_metadata(worldgen_chunk_pos(cpos.x, cpos.z));
+
+ for(std::size_t index = 0; index < CHUNK_VOLUME; index += 1) {
+ auto lpos = coord::to_local(index);
+ auto vpos = coord::to_voxel(cpos, lpos);
+ auto hdx = static_cast<std::size_t>(lpos.x + lpos.z * CHUNK_SIZE);
+
+ // Speculative optimization - there's no solid
+ // terrain above variation to carve caves out from
+ if(vpos[1] > (m_terrain_variation.get_value() + 1)) {
+ continue;
+ }
+
+ const float na = fnlGetNoise3D(&m_fnl_caves_a, vpos.x, 1.5f * vpos.y, vpos.z);
+ const float nb = fnlGetNoise3D(&m_fnl_caves_b, vpos.x, 1.5f * vpos.y, vpos.z);
+
+ if((na * na + nb * nb) <= (1.0f / 1024.0f)) {
+ if(vpos[1] == metadata.heightmap[hdx]) {
+ metadata.heightmap[hdx] = INT64_MIN;
+ }
+
+ voxels[index] = NULL_VOXEL_ID;
+ continue;
+ }
+ }
+}
+
+void Overworld::generate_features(const chunk_pos &cpos, VoxelStorage &voxels)
+{
+ auto &metadata = get_metadata(worldgen_chunk_pos(cpos.x, cpos.z));
+
+#if 1
+ constexpr static std::size_t COUNT = 5;
+ std::array<std::int16_t, COUNT> lxa = {};
+ std::array<std::int16_t, COUNT> lza = {};
+ std::array<std::int64_t, COUNT> heights = {};
+
+ for(std::size_t tc = 0; tc < COUNT; tc += 1) {
+ lxa[tc] = static_cast<std::int16_t>(metadata.entropy[tc * 3 + 0] % CHUNK_SIZE);
+ lza[tc] = static_cast<std::int16_t>(metadata.entropy[tc * 3 + 1] % CHUNK_SIZE);
+ heights[tc] = 3 + static_cast<std::int64_t>(metadata.entropy[tc * 3 + 2] % 4);
+ }
+
+ for(std::size_t index = 0; index < CHUNK_VOLUME; index += 1) {
+ auto lpos = coord::to_local(index);
+ auto vpos = coord::to_voxel(cpos, lpos);
+ auto hdx = static_cast<std::size_t>(lpos.x + lpos.z * CHUNK_SIZE);
+
+ for(std::size_t tc = 0; tc < COUNT; tc += 1) {
+ if((lpos.x == lxa[tc]) && (lpos.z == lza[tc])) {
+ if(cxpr::range<std::int64_t>(vpos.y - metadata.heightmap[hdx], 1, heights[tc]))
+ voxels[index] = game_voxels::cobblestone;
+ break;
+ }
+ }
+ }
+#else
+ for(std::size_t index = 0; index < CHUNK_VOLUME; index += 1) {
+ auto lpos = coord::to_local(index);
+ auto vpos = coord::to_voxel(cpos, lpos);
+ auto hdx = static_cast<std::size_t>(lpos.x + lpos.z * CHUNK_SIZE);
+
+ if(vpos.y == (metadata.heightmap[hdx] + 1)) {
+ voxels[index] = game_voxels::vtest;
+ continue;
+ }
+ }
+#endif
+}
diff --git a/game/server/overworld.hh b/game/server/overworld.hh
new file mode 100644
index 0000000..dbe66d0
--- /dev/null
+++ b/game/server/overworld.hh
@@ -0,0 +1,51 @@
+#ifndef SERVER_OVERWORLD_HH
+#define SERVER_OVERWORLD_HH 1
+#pragma once
+
+#include "core/config.hh"
+
+#include "shared/const.hh"
+#include "shared/dimension.hh"
+
+struct Metadata_2501 final {
+ std::array<std::uint64_t, CHUNK_AREA> entropy;
+ std::array<voxel_pos::value_type, CHUNK_AREA> heightmap;
+};
+
+class Overworld final : public Dimension {
+public:
+ explicit Overworld(const char *name);
+ virtual ~Overworld(void) = default;
+
+public:
+ virtual void init(Config &config) override;
+ virtual void init_late(std::uint64_t global_seed) override;
+ virtual bool generate(const chunk_pos &cpos, VoxelStorage &voxels) override;
+
+private:
+ float get_noise(const voxel_pos &vpos, std::int64_t variation);
+ Metadata_2501 &get_metadata(const worldgen_chunk_pos &cpos);
+ void generate_terrain(const chunk_pos &cpos, VoxelStorage &voxels);
+ void generate_surface(const chunk_pos &cpos, VoxelStorage &voxels);
+ void generate_carvers(const chunk_pos &cpos, VoxelStorage &voxels);
+ void generate_features(const chunk_pos &cpos, VoxelStorage &voxels);
+
+private:
+ ConfigInt m_terrain_variation;
+ ConfigInt m_bottommost_chunk;
+ ConfigBoolean m_enable_surface;
+ ConfigBoolean m_enable_carvers;
+ ConfigBoolean m_enable_features;
+
+private:
+ emhash8::HashMap<worldgen_chunk_pos, Metadata_2501> m_metadata;
+ std::mt19937_64 m_twister;
+ fnl_state m_fnl_terrain;
+ fnl_state m_fnl_caves_a;
+ fnl_state m_fnl_caves_b;
+
+private:
+ std::mutex m_mutex;
+};
+
+#endif /* SERVER_OVERWORLD_HH */
diff --git a/game/server/pch.hh b/game/server/pch.hh
new file mode 100644
index 0000000..5a4f5f2
--- /dev/null
+++ b/game/server/pch.hh
@@ -0,0 +1,9 @@
+#ifndef SERVER_PCH_HH
+#define SERVER_PCH_HH 1
+#pragma once
+
+#include <shared/pch.hh>
+
+#include <csignal>
+
+#endif /* SERVER_PCH_HH */
diff --git a/game/server/receive.cc b/game/server/receive.cc
new file mode 100644
index 0000000..d2862e2
--- /dev/null
+++ b/game/server/receive.cc
@@ -0,0 +1,184 @@
+#include "server/pch.hh"
+#include "server/receive.hh"
+
+#include "core/config.hh"
+
+#include "shared/chunk_aabb.hh"
+#include "shared/coord.hh"
+#include "shared/dimension.hh"
+#include "shared/head.hh"
+#include "shared/protocol.hh"
+#include "shared/transform.hh"
+#include "shared/velocity.hh"
+
+#include "server/game.hh"
+#include "server/globals.hh"
+#include "server/inhabited.hh"
+#include "server/sessions.hh"
+#include "server/universe.hh"
+#include "server/worldgen.hh"
+
+static void on_entity_transform_packet(const protocol::EntityTransform &packet)
+{
+ if(auto session = sessions::find(packet.peer)) {
+ if(session->dimension && session->dimension->entities.valid(session->player_entity)) {
+ auto &component = session->dimension->entities.emplace_or_replace<TransformComponent>(session->player_entity);
+ component.angles = packet.angles;
+ component.chunk = packet.chunk;
+ component.local = packet.local;
+
+ protocol::EntityTransform response;
+ response.entity = session->player_entity;
+ response.angles = component.angles;
+ response.chunk = component.chunk;
+ response.local = component.local;
+
+ // Propagate changes to the rest of the world
+ // except the peer that has sent the packet in the first place
+ sessions::broadcast(session->dimension, protocol::encode(response), session->peer);
+ }
+ }
+}
+
+static void on_entity_velocity_packet(const protocol::EntityVelocity &packet)
+{
+ if(auto session = sessions::find(packet.peer)) {
+ if(session->dimension && session->dimension->entities.valid(session->player_entity)) {
+ auto &component = session->dimension->entities.emplace_or_replace<VelocityComponent>(session->player_entity);
+ component.value = packet.value;
+
+ protocol::EntityVelocity response;
+ response.entity = session->player_entity;
+ response.value = component.value;
+
+ // Propagate changes to the rest of the world
+ // except the peer that has sent the packet in the first place
+ sessions::broadcast(session->dimension, protocol::encode(response), session->peer);
+ }
+ }
+}
+
+static void on_entity_head_packet(const protocol::EntityHead &packet)
+{
+ if(auto session = sessions::find(packet.peer)) {
+ if(session->dimension && session->dimension->entities.valid(session->player_entity)) {
+ auto &component = session->dimension->entities.emplace_or_replace<HeadComponent>(session->player_entity);
+ component.angles = packet.angles;
+
+ protocol::EntityHead response;
+ response.entity = session->player_entity;
+ response.angles = component.angles;
+
+ // Propagate changes to the rest of the world
+ // except the peer that has sent the packet in the first place
+ sessions::broadcast(session->dimension, protocol::encode(response), session->peer);
+ }
+ }
+}
+
+static void on_set_voxel_packet(const protocol::SetVoxel &packet)
+{
+ if(auto session = sessions::find(packet.peer)) {
+ if(session->dimension && !session->dimension->set_voxel(packet.voxel, packet.vpos)) {
+ auto cpos = coord::to_chunk(packet.vpos);
+ auto lpos = coord::to_local(packet.vpos);
+ auto index = coord::to_index(lpos);
+
+ if(worldgen::is_generating(session->dimension, cpos)) {
+ // The chunk is currently being
+ // generated, so we must ignore any
+ // requests from players to build there
+ return;
+ }
+
+ auto chunk = session->dimension->find_chunk(cpos);
+ auto created = false;
+
+ if(chunk == nullptr) {
+ chunk = universe::load_chunk(session->dimension, cpos);
+ created = true;
+ }
+
+ if(chunk == nullptr) {
+ chunk = session->dimension->create_chunk(cpos);
+ created = true;
+ }
+
+ chunk->set_voxel(packet.voxel, index);
+
+ if(created) {
+ session->dimension->chunks.emplace_or_replace<InhabitedComponent>(chunk->get_entity());
+
+ protocol::ChunkVoxels response;
+ response.voxels = chunk->get_voxels();
+ response.chunk = cpos;
+ sessions::broadcast(session->dimension, protocol::encode(response));
+ }
+ else {
+ protocol::SetVoxel response;
+ response.vpos = packet.vpos;
+ response.voxel = packet.voxel;
+ sessions::broadcast(session->dimension, protocol::encode(response), session->peer);
+ }
+
+ return;
+ }
+ }
+}
+
+static void on_request_chunk_packet(const protocol::RequestChunk &packet)
+{
+ if(auto session = sessions::find(packet.peer)) {
+ if(!session->dimension || !session->dimension->entities.valid(session->player_entity)) {
+ // De-spawned sessions cannot request
+ // chunks from the server; that's cheating!!!
+ return;
+ }
+
+ if(auto transform = session->dimension->entities.try_get<TransformComponent>(session->player_entity)) {
+ ChunkAABB view_box;
+ view_box.min = transform->chunk - static_cast<chunk_pos::value_type>(server_game::view_distance.get_value());
+ view_box.max = transform->chunk + static_cast<chunk_pos::value_type>(server_game::view_distance.get_value());
+
+ if(view_box.contains(packet.cpos)) {
+ if(auto chunk = universe::load_chunk(session->dimension, packet.cpos)) {
+ protocol::ChunkVoxels response;
+ response.chunk = packet.cpos;
+ response.voxels = chunk->get_voxels();
+ protocol::send(packet.peer, protocol::encode(response));
+ }
+ else {
+ worldgen::request_chunk(session, packet.cpos);
+ }
+ }
+ }
+ }
+}
+
+static void on_entity_sound_packet(const protocol::EntitySound &packet)
+{
+ if(auto session = sessions::find(packet.peer)) {
+ if(!session->dimension || !session->dimension->entities.valid(session->player_entity)) {
+ // De-spawned sessions cannot play sounds
+ return;
+ }
+
+ protocol::EntitySound response;
+ response.entity = session->player_entity;
+ response.sound = packet.sound;
+ response.looping = packet.looping;
+ response.pitch = packet.pitch;
+
+ sessions::broadcast(session->dimension, protocol::encode(response), packet.peer);
+ }
+}
+
+void server_recieve::init(void)
+{
+ globals::dispatcher.sink<protocol::EntityTransform>().connect<&on_entity_transform_packet>();
+ globals::dispatcher.sink<protocol::EntityVelocity>().connect<&on_entity_velocity_packet>();
+ globals::dispatcher.sink<protocol::EntityHead>().connect<&on_entity_head_packet>();
+ globals::dispatcher.sink<protocol::SetVoxel>().connect<&on_set_voxel_packet>();
+ globals::dispatcher.sink<protocol::RequestChunk>().connect<&on_request_chunk_packet>();
+ globals::dispatcher.sink<protocol::EntitySound>().connect<&on_entity_sound_packet>();
+}
diff --git a/game/server/receive.hh b/game/server/receive.hh
new file mode 100644
index 0000000..57090b5
--- /dev/null
+++ b/game/server/receive.hh
@@ -0,0 +1,10 @@
+#ifndef SERVER_RECEIVE_HH
+#define SERVER_RECEIVE_HH 1
+#pragma once
+
+namespace server_recieve
+{
+void init(void);
+} // namespace server_recieve
+
+#endif /* SERVER_RECEIVE_HH */
diff --git a/game/server/sessions.cc b/game/server/sessions.cc
new file mode 100644
index 0000000..f2e643d
--- /dev/null
+++ b/game/server/sessions.cc
@@ -0,0 +1,402 @@
+#include "server/pch.hh"
+#include "server/sessions.hh"
+
+#include "core/config.hh"
+#include "core/constexpr.hh"
+#include "core/crc64.hh"
+#include "core/strtools.hh"
+
+#include "shared/chunk.hh"
+#include "shared/coord.hh"
+#include "shared/dimension.hh"
+#include "shared/factory.hh"
+#include "shared/head.hh"
+#include "shared/player.hh"
+#include "shared/protocol.hh"
+#include "shared/transform.hh"
+#include "shared/velocity.hh"
+#include "shared/voxel_registry.hh"
+
+#include "server/game.hh"
+#include "server/globals.hh"
+#include "server/whitelist.hh"
+
+class DimensionListener final {
+public:
+ explicit DimensionListener(Dimension *dimension);
+ void on_destroy_entity(const entt::registry &registry, entt::entity entity);
+
+private:
+ Dimension *dimension;
+};
+
+ConfigUnsigned sessions::max_players(8U, 1U, 128U);
+unsigned int sessions::num_players = 0U;
+
+static emhash8::HashMap<std::string, Session *> username_map;
+static emhash8::HashMap<std::uint64_t, Session *> identity_map;
+static std::vector<DimensionListener> dimension_listeners;
+static std::vector<Session> sessions_vector;
+
+static void on_login_request_packet(const protocol::LoginRequest &packet)
+{
+ if(packet.version > protocol::VERSION) {
+ protocol::Disconnect response;
+ response.reason = "protocol.outdated_server";
+ protocol::send(packet.peer, protocol::encode(response));
+ return;
+ }
+
+ if(packet.version < protocol::VERSION) {
+ protocol::Disconnect response;
+ response.reason = "protocol.outdated_client";
+ protocol::send(packet.peer, protocol::encode(response));
+ return;
+ }
+
+ // FIXME: calculate voxel registry checksum ahead of time
+ // instead of figuring it out every time a new player connects
+ if(packet.voxel_def_checksum != voxel_registry::checksum()) {
+ protocol::Disconnect response;
+ response.reason = "protocol.voxel_registry_checksum";
+ protocol::send(packet.peer, protocol::encode(response));
+ return;
+ }
+
+ // Don't assign new usernames and just kick the player if
+ // an another client using the same username is already connected
+ // and playing; since we have a whitelist, adding "(1)" isn't feasible anymore
+ if(username_map.contains(packet.username)) {
+ protocol::Disconnect response;
+ response.reason = "protocol.username_taken";
+ protocol::send(packet.peer, protocol::encode(response));
+ return;
+ }
+
+ if(whitelist::enabled.get_value()) {
+ if(!whitelist::contains(packet.username.c_str())) {
+ protocol::Disconnect response;
+ response.reason = "protocol.not_whitelisted";
+ protocol::send(packet.peer, protocol::encode(response));
+ return;
+ }
+
+ if(!whitelist::matches(packet.username.c_str(), packet.password_hash)) {
+ protocol::Disconnect response;
+ response.reason = "protocol.password_incorrect";
+ protocol::send(packet.peer, protocol::encode(response));
+ return;
+ }
+ }
+ else if(packet.password_hash != server_game::password_hash) {
+ protocol::Disconnect response;
+ response.reason = "protocol.password_incorrect";
+ protocol::send(packet.peer, protocol::encode(response));
+ return;
+ }
+
+ if(Session *session = sessions::create(packet.peer, packet.username.c_str())) {
+ protocol::LoginResponse response;
+ response.client_index = session->client_index;
+ response.client_identity = session->client_identity;
+ response.server_tickrate = globals::tickrate;
+ protocol::send(packet.peer, protocol::encode(response));
+
+ protocol::DimensionInfo dim_info;
+ dim_info.name = globals::spawn_dimension->get_name();
+ dim_info.gravity = globals::spawn_dimension->get_gravity();
+ protocol::send(packet.peer, protocol::encode(dim_info));
+
+ spdlog::info("sessions: {} [{}] logged in with client_index={} in {}", session->client_username,
+ session->client_identity, session->client_index, globals::spawn_dimension->get_name());
+
+ // FIXME: only send entities that are present within the current
+ // player's view bounding box; this also would mean we're not sending
+ // anything here and just straight up spawing the player and await them
+ // to receive all the chunks and entites they feel like requesting
+ for(auto entity : globals::spawn_dimension->entities.view<entt::entity>()) {
+ if(const auto head = globals::spawn_dimension->entities.try_get<HeadComponent>(entity)) {
+ protocol::EntityHead head_packet;
+ head_packet.entity = entity;
+ head_packet.angles = head->angles;
+ protocol::send(session->peer, protocol::encode(head_packet));
+ }
+
+ if(const auto transform = globals::spawn_dimension->entities.try_get<TransformComponent>(entity)) {
+ protocol::EntityTransform transform_packet;
+ transform_packet.entity = entity;
+ transform_packet.angles = transform->angles;
+ transform_packet.chunk = transform->chunk;
+ transform_packet.local = transform->local;
+ protocol::send(session->peer, protocol::encode(transform_packet));
+ }
+
+ if(const auto velocity = globals::spawn_dimension->entities.try_get<VelocityComponent>(entity)) {
+ protocol::EntityVelocity velocity_packet;
+ velocity_packet.entity = entity;
+ velocity_packet.value = velocity->value;
+ protocol::send(session->peer, protocol::encode(velocity_packet));
+ }
+
+ if(globals::spawn_dimension->entities.all_of<PlayerComponent>(entity)) {
+ protocol::EntityPlayer player_packet;
+ player_packet.entity = entity;
+ protocol::send(session->peer, protocol::encode(player_packet));
+ }
+ }
+
+ session->dimension = globals::spawn_dimension;
+ session->player_entity = globals::spawn_dimension->entities.create();
+ shared_factory::create_player(globals::spawn_dimension, session->player_entity);
+
+ const auto &head = globals::spawn_dimension->entities.get<HeadComponent>(session->player_entity);
+ const auto &transform = globals::spawn_dimension->entities.get<TransformComponent>(session->player_entity);
+ const auto &velocity = globals::spawn_dimension->entities.get<VelocityComponent>(session->player_entity);
+
+ protocol::EntityHead head_packet;
+ head_packet.entity = session->player_entity;
+ head_packet.angles = head.angles;
+
+ protocol::EntityTransform transform_packet;
+ transform_packet.entity = session->player_entity;
+ transform_packet.angles = transform.angles;
+ transform_packet.chunk = transform.chunk;
+ transform_packet.local = transform.local;
+
+ protocol::EntityVelocity velocity_packet;
+ velocity_packet.entity = session->player_entity;
+ velocity_packet.value = velocity.value;
+
+ protocol::EntityPlayer player_packet;
+ player_packet.entity = session->player_entity;
+
+ protocol::broadcast(globals::server_host, protocol::encode(head_packet));
+ protocol::broadcast(globals::server_host, protocol::encode(transform_packet));
+ protocol::broadcast(globals::server_host, protocol::encode(velocity_packet));
+ protocol::broadcast(globals::server_host, protocol::encode(player_packet));
+
+ protocol::SpawnPlayer spawn_packet;
+ spawn_packet.entity = session->player_entity;
+
+ // SpawnPlayer serves a different purpose compared to EntityPlayer
+ // The latter is used to construct entities (as in "attach a component")
+ // whilst the SpawnPlayer packet is used to notify client-side that the
+ // entity identifier in the packet is to be treated as the local player entity
+ protocol::send(session->peer, protocol::encode(spawn_packet));
+
+ protocol::ChatMessage message;
+ message.type = protocol::ChatMessage::PLAYER_JOIN;
+ message.sender = session->client_username;
+ message.message = std::string();
+
+ protocol::broadcast(globals::server_host, protocol::encode(message));
+
+ sessions::refresh_scoreboard();
+
+ return;
+ }
+
+ protocol::Disconnect response;
+ response.reason = "protocol.server_full";
+ protocol::send(packet.peer, protocol::encode(response));
+}
+
+static void on_disconnect_packet(const protocol::Disconnect &packet)
+{
+ if(Session *session = sessions::find(packet.peer)) {
+ protocol::ChatMessage message;
+ message.type = protocol::ChatMessage::PLAYER_LEAVE;
+ message.sender = session->client_username;
+ message.message = packet.reason;
+
+ protocol::broadcast(globals::server_host, protocol::encode(message), session->peer);
+
+ spdlog::info("{} disconnected ({})", session->client_username, packet.reason);
+
+ sessions::destroy(session);
+ sessions::refresh_scoreboard();
+ }
+}
+
+// NOTE: [sessions] is a good place for this since [receive]
+// handles entity data sent by players and [sessions] handles
+// everything else network related that is not player movement
+static void on_voxel_set(const VoxelSetEvent &event)
+{
+ protocol::SetVoxel packet;
+ packet.vpos = coord::to_voxel(event.cpos, event.lpos);
+ packet.voxel = event.voxel;
+ packet.flags = 0U; // UNDONE
+ protocol::broadcast(globals::server_host, protocol::encode(packet));
+}
+
+DimensionListener::DimensionListener(Dimension *dimension)
+{
+ this->dimension = dimension;
+}
+
+void DimensionListener::on_destroy_entity(const entt::registry &registry, entt::entity entity)
+{
+ protocol::RemoveEntity packet;
+ packet.entity = entity;
+ sessions::broadcast(dimension, protocol::encode(packet));
+}
+
+void sessions::init(void)
+{
+ globals::server_config.add_value("sessions.max_players", sessions::max_players);
+
+ globals::dispatcher.sink<protocol::LoginRequest>().connect<&on_login_request_packet>();
+ globals::dispatcher.sink<protocol::Disconnect>().connect<&on_disconnect_packet>();
+
+ globals::dispatcher.sink<VoxelSetEvent>().connect<&on_voxel_set>();
+}
+
+void sessions::init_late(void)
+{
+ sessions::num_players = 0U;
+
+ username_map.clear();
+ identity_map.clear();
+ sessions_vector.resize(sessions::max_players.get_value(), Session());
+
+ for(unsigned int i = 0U; i < sessions::max_players.get_value(); ++i) {
+ sessions_vector[i].client_index = UINT16_MAX;
+ sessions_vector[i].client_identity = UINT64_MAX;
+ sessions_vector[i].client_username = std::string();
+ sessions_vector[i].player_entity = entt::null;
+ sessions_vector[i].peer = nullptr;
+ }
+}
+
+void sessions::init_post_universe(void)
+{
+ for(auto &dimension : globals::dimensions) {
+ dimension_listeners.push_back(DimensionListener(dimension.second));
+ dimension.second->entities.on_destroy<entt::entity>().connect<&DimensionListener::on_destroy_entity>(dimension_listeners.back());
+ }
+}
+
+void sessions::deinit(void)
+{
+ username_map.clear();
+ identity_map.clear();
+ sessions_vector.clear();
+ dimension_listeners.clear();
+}
+
+Session *sessions::create(ENetPeer *peer, const char *client_username)
+{
+ for(unsigned int i = 0U; i < sessions::max_players.get_value(); ++i) {
+ if(!sessions_vector[i].peer) {
+ std::uint64_t client_identity = crc64::get(client_username);
+
+ sessions_vector[i].client_index = i;
+ sessions_vector[i].client_identity = client_identity;
+ sessions_vector[i].client_username = client_username;
+ sessions_vector[i].player_entity = entt::null;
+ sessions_vector[i].peer = peer;
+
+ username_map[client_username] = &sessions_vector[i];
+ identity_map[client_identity] = &sessions_vector[i];
+
+ peer->data = &sessions_vector[i];
+
+ sessions::num_players += 1U;
+
+ return &sessions_vector[i];
+ }
+ }
+
+ return nullptr;
+}
+
+Session *sessions::find(const char *client_username)
+{
+ const auto it = username_map.find(client_username);
+ if(it != username_map.cend())
+ return it->second;
+ return nullptr;
+}
+
+Session *sessions::find(std::uint16_t client_index)
+{
+ if(client_index < sessions_vector.size()) {
+ if(!sessions_vector[client_index].peer)
+ return nullptr;
+ return &sessions_vector[client_index];
+ }
+
+ return nullptr;
+}
+
+Session *sessions::find(std::uint64_t client_identity)
+{
+ const auto it = identity_map.find(client_identity);
+ if(it != identity_map.cend())
+ return it->second;
+ return nullptr;
+}
+
+Session *sessions::find(ENetPeer *peer)
+{
+ if(peer != nullptr)
+ return reinterpret_cast<Session *>(peer->data);
+ return nullptr;
+}
+
+void sessions::destroy(Session *session)
+{
+ if(session) {
+ if(session->peer) {
+ // Make sure we don't leave a mark
+ session->peer->data = nullptr;
+ }
+
+ if(session->dimension) {
+ session->dimension->entities.destroy(session->player_entity);
+ }
+
+ username_map.erase(session->client_username);
+ identity_map.erase(session->client_identity);
+
+ session->client_index = UINT16_MAX;
+ session->client_identity = UINT64_MAX;
+ session->client_username = std::string();
+ session->player_entity = entt::null;
+ session->peer = nullptr;
+
+ sessions::num_players -= 1U;
+ }
+}
+
+void sessions::broadcast(const Dimension *dimension, ENetPacket *packet)
+{
+ for(const auto &session : sessions_vector) {
+ if(session.peer && (session.dimension == dimension)) {
+ enet_peer_send(session.peer, protocol::CHANNEL, packet);
+ }
+ }
+}
+
+void sessions::broadcast(const Dimension *dimension, ENetPacket *packet, ENetPeer *except)
+{
+ for(const auto &session : sessions_vector) {
+ if(session.peer && (session.peer != except)) {
+ enet_peer_send(session.peer, protocol::CHANNEL, packet);
+ }
+ }
+}
+
+void sessions::refresh_scoreboard(void)
+{
+ protocol::ScoreboardUpdate packet;
+
+ for(std::size_t i = 0; i < sessions::max_players.get_value(); ++i) {
+ if(!sessions_vector[i].peer)
+ continue;
+ packet.names.push_back(sessions_vector[i].client_username);
+ }
+
+ protocol::broadcast(globals::server_host, protocol::encode(packet));
+}
diff --git a/game/server/sessions.hh b/game/server/sessions.hh
new file mode 100644
index 0000000..cb31bd9
--- /dev/null
+++ b/game/server/sessions.hh
@@ -0,0 +1,53 @@
+#ifndef SERVER_SESSIONS_HH
+#define SERVER_SESSIONS_HH 1
+#pragma once
+
+class Dimension;
+
+class ConfigUnsigned;
+
+struct Session final {
+ std::uint16_t client_index;
+ std::uint64_t client_identity;
+ std::string client_username;
+ entt::entity player_entity;
+ Dimension *dimension;
+ ENetPeer *peer;
+};
+
+namespace sessions
+{
+extern ConfigUnsigned max_players;
+extern unsigned int num_players;
+} // namespace sessions
+
+namespace sessions
+{
+void init(void);
+void init_late(void);
+void init_post_universe(void);
+void deinit(void);
+} // namespace sessions
+
+namespace sessions
+{
+Session *create(ENetPeer *peer, const char *client_username);
+Session *find(const char *client_username);
+Session *find(std::uint16_t client_index);
+Session *find(std::uint64_t client_identity);
+Session *find(ENetPeer *peer);
+void destroy(Session *session);
+} // namespace sessions
+
+namespace sessions
+{
+void broadcast(const Dimension *dimension, ENetPacket *packet);
+void broadcast(const Dimension *dimension, ENetPacket *packet, ENetPeer *except);
+} // namespace sessions
+
+namespace sessions
+{
+void refresh_scoreboard(void);
+} // namespace sessions
+
+#endif /* SERVER_SESSIONS_HH */
diff --git a/game/server/status.cc b/game/server/status.cc
new file mode 100644
index 0000000..0b8660b
--- /dev/null
+++ b/game/server/status.cc
@@ -0,0 +1,26 @@
+#include "server/pch.hh"
+#include "server/status.hh"
+
+#include "core/config.hh"
+
+#include "shared/protocol.hh"
+#include "shared/splash.hh"
+
+#include "server/globals.hh"
+#include "server/sessions.hh"
+
+static void on_status_request_packet(const protocol::StatusRequest &packet)
+{
+ spdlog::info("STATUS REQUEST");
+ protocol::StatusResponse response;
+ response.version = protocol::VERSION;
+ response.max_players = sessions::max_players.get_value();
+ response.num_players = sessions::num_players;
+ response.motd = splash::get();
+ protocol::send(packet.peer, protocol::encode(response));
+}
+
+void status::init(void)
+{
+ globals::dispatcher.sink<protocol::StatusRequest>().connect<&on_status_request_packet>();
+}
diff --git a/game/server/status.hh b/game/server/status.hh
new file mode 100644
index 0000000..5f939f7
--- /dev/null
+++ b/game/server/status.hh
@@ -0,0 +1,10 @@
+#ifndef SERVER_STATUS_HH
+#define SERVER_STATUS_HH 1
+#pragma once
+
+namespace status
+{
+void init(void);
+} // namespace status
+
+#endif /* SERVER_STATUS_HH */
diff --git a/game/server/universe.cc b/game/server/universe.cc
new file mode 100644
index 0000000..22abaff
--- /dev/null
+++ b/game/server/universe.cc
@@ -0,0 +1,214 @@
+#include "server/pch.hh"
+#include "server/universe.hh"
+
+#include "core/config.hh"
+#include "core/epoch.hh"
+#include "core/buffer.hh"
+
+#include "shared/chunk.hh"
+#include "shared/dimension.hh"
+
+#include "server/globals.hh"
+#include "server/inhabited.hh"
+#include "server/overworld.hh"
+
+struct DimensionMetadata final {
+ std::string config_path;
+ std::string zvox_dir;
+ Config config;
+};
+
+static ConfigString universe_name("save");
+
+static Config universe_config;
+static ConfigUnsigned64 universe_config_seed;
+static ConfigString universe_spawn_dimension("world");
+
+static std::string universe_config_path;
+static std::unordered_map<Dimension *, DimensionMetadata *> metadata_map;
+
+static std::string make_chunk_filename(const DimensionMetadata *metadata, const chunk_pos &cpos)
+{
+ const auto unsigned_x = static_cast<std::uint32_t>(cpos.x);
+ const auto unsigned_y = static_cast<std::uint32_t>(cpos.y);
+ const auto unsigned_z = static_cast<std::uint32_t>(cpos.z);
+ return fmt::format("{}/{:08X}-{:08X}-{:08X}.zvox", metadata->zvox_dir, unsigned_x, unsigned_y, unsigned_z);
+}
+
+static void add_new_dimension(Dimension *dimension)
+{
+ if(globals::dimensions.count(dimension->get_name())) {
+ spdlog::critical("universe: dimension named {} already exists", dimension->get_name());
+ std::terminate();
+ }
+
+ auto dimension_dir = fmt::format("{}/{}", universe_name.get(), dimension->get_name());
+
+ if(!PHYSFS_mkdir(dimension_dir.c_str())) {
+ spdlog::critical("universe: {}: {}", dimension_dir, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
+ std::terminate();
+ }
+
+ auto metadata = new DimensionMetadata;
+ metadata->config_path = fmt::format("{}/dimension.conf", dimension_dir);
+ metadata->zvox_dir = fmt::format("{}/chunk", dimension_dir);
+
+ if(!PHYSFS_mkdir(metadata->zvox_dir.c_str())) {
+ spdlog::critical("universe: {}: {}", metadata->zvox_dir, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
+ std::terminate();
+ }
+
+ globals::dimensions.insert_or_assign(dimension->get_name(), dimension);
+
+ auto &mapped_metadata = metadata_map.insert_or_assign(dimension, metadata).first->second;
+
+ dimension->init(mapped_metadata->config);
+
+ mapped_metadata->config.load_file(mapped_metadata->config_path.c_str());
+
+ dimension->init_late(universe_config_seed.get_value());
+}
+
+static void internal_save_chunk(const DimensionMetadata *metadata, const Dimension *dimension, const chunk_pos &cpos, const Chunk *chunk)
+{
+ auto path = make_chunk_filename(metadata, cpos);
+
+ WriteBuffer buffer;
+ chunk->get_voxels().serialize(buffer);
+
+ if(auto file = buffer.to_file(path.c_str())) {
+ PHYSFS_close(file);
+ return;
+ }
+}
+
+void universe::init(void)
+{
+ // If the world is newly created, the seed will
+ // be chosed based on the current system's view on UNIX time
+ universe_config_seed.set_value(epoch::microseconds());
+
+ // We're going to read files from directory named with
+ // the value of this config value. Since config is also
+ // read from command line, the [--universe <name>] parameter still works
+ globals::server_config.add_value("universe", universe_name);
+
+ universe_config.add_value("global_seed", universe_config_seed);
+ universe_config.add_value("spawn_dimension", universe_spawn_dimension);
+}
+
+void universe::init_late(void)
+{
+ const auto universe_dir = std::string(universe_name.get());
+
+ if(!PHYSFS_mkdir(universe_dir.c_str())) {
+ spdlog::critical("universe: {}: {}", universe_dir, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
+ std::terminate();
+ }
+
+ universe_config_path = fmt::format("{}/universe.conf", universe_dir);
+ universe_config.load_file(universe_config_path.c_str());
+
+ add_new_dimension(new Overworld("world"));
+
+ // UNDONE: lua scripts to setup dimensions
+ if(globals::dimensions.empty()) {
+ spdlog::critical("universe: no dimensions");
+ std::terminate();
+ }
+
+ auto spawn_dimension = globals::dimensions.find(universe_spawn_dimension.get());
+
+ if(spawn_dimension == globals::dimensions.cend()) {
+ spdlog::critical("universe: {} is not a valid dimension name", universe_spawn_dimension.get());
+ std::terminate();
+ }
+
+ globals::spawn_dimension = spawn_dimension->second;
+}
+
+void universe::deinit(void)
+{
+ for(const auto metadata : metadata_map) {
+ metadata.second->config.save_file(metadata.second->config_path.c_str());
+ delete metadata.second;
+ }
+
+ metadata_map.clear();
+
+ for(const auto dimension : globals::dimensions) {
+ universe::save_all_chunks(dimension.second);
+ delete dimension.second;
+ }
+
+ globals::dimensions.clear();
+ globals::spawn_dimension = nullptr;
+
+ universe_config.save_file(universe_config_path.c_str());
+}
+
+Chunk *universe::load_chunk(Dimension *dimension, const chunk_pos &cpos)
+{
+ if(auto chunk = dimension->find_chunk(cpos)) {
+ // Just return the existing chunk which is
+ // most probable to be up to date compared to
+ // whatever the hell is currently stored on disk
+ return chunk;
+ }
+
+ auto metadata = metadata_map.find(dimension);
+
+ if(metadata == metadata_map.cend()) {
+ // The dimension is for sure a weird one
+ return nullptr;
+ }
+
+ if(auto file = PHYSFS_openRead(make_chunk_filename(metadata->second, cpos).c_str())) {
+ VoxelStorage voxels;
+ ReadBuffer buffer(file);
+ voxels.deserialize(buffer);
+
+ PHYSFS_close(file);
+
+ auto chunk = dimension->create_chunk(cpos);
+ chunk->set_voxels(voxels);
+
+ // Make sure we're going to save it later
+ dimension->chunks.emplace_or_replace<InhabitedComponent>(chunk->get_entity());
+
+ return chunk;
+ }
+
+ return nullptr;
+}
+
+void universe::save_chunk(Dimension *dimension, const chunk_pos &cpos)
+{
+ auto metadata = metadata_map.find(dimension);
+
+ if(metadata == metadata_map.cend()) {
+ // Cannot save a chunk in a dimension
+ // that doesn't have a metadata struct
+ return;
+ }
+
+ if(auto chunk = dimension->find_chunk(cpos)) {
+ internal_save_chunk(metadata->second, dimension, cpos, chunk);
+ }
+}
+
+void universe::save_all_chunks(Dimension *dimension)
+{
+ auto group = dimension->chunks.group(entt::get<ChunkComponent, InhabitedComponent>);
+ auto metadata = metadata_map.find(dimension);
+
+ if(metadata == metadata_map.cend()) {
+ // Cannot save a chunk in a dimension
+ // that doesn't have a metadata struct
+ return;
+ }
+
+ for(auto [entity, chunk] : group.each()) {
+ internal_save_chunk(metadata->second, dimension, chunk.cpos, chunk.chunk);
+ }
+}
diff --git a/game/server/universe.hh b/game/server/universe.hh
new file mode 100644
index 0000000..00c8b8d
--- /dev/null
+++ b/game/server/universe.hh
@@ -0,0 +1,25 @@
+#ifndef SERVER_UNIVERSE_HH
+#define SERVER_UNIVERSE_HH 1
+#pragma once
+
+#include "shared/types.hh"
+
+class Chunk;
+class Dimension;
+class Session;
+
+namespace universe
+{
+void init(void);
+void init_late(void);
+void deinit(void);
+} // namespace universe
+
+namespace universe
+{
+Chunk *load_chunk(Dimension *dimension, const chunk_pos &cpos);
+void save_chunk(Dimension *dimension, const chunk_pos &cpos);
+void save_all_chunks(Dimension *dimension);
+} // namespace universe
+
+#endif /* SERVER_UNIVERSE_HH */
diff --git a/game/server/unloader.cc b/game/server/unloader.cc
new file mode 100644
index 0000000..fd838d0
--- /dev/null
+++ b/game/server/unloader.cc
@@ -0,0 +1,77 @@
+#include "server/pch.hh"
+#include "server/unloader.hh"
+
+#include "core/config.hh"
+
+#include "shared/chunk_aabb.hh"
+#include "shared/chunk.hh"
+#include "shared/dimension.hh"
+#include "shared/player.hh"
+#include "shared/player.hh"
+#include "shared/transform.hh"
+
+#include "server/game.hh"
+#include "server/globals.hh"
+#include "server/inhabited.hh"
+#include "server/universe.hh"
+
+static void on_chunk_update(const ChunkUpdateEvent &event)
+{
+ event.dimension->chunks.emplace_or_replace<InhabitedComponent>(event.chunk->get_entity());
+}
+
+static void on_voxel_set(const VoxelSetEvent &event)
+{
+ event.dimension->chunks.emplace_or_replace<InhabitedComponent>(event.chunk->get_entity());
+}
+
+void unloader::init(void)
+{
+ globals::dispatcher.sink<ChunkUpdateEvent>().connect<&on_chunk_update>();
+ globals::dispatcher.sink<VoxelSetEvent>().connect<&on_voxel_set>();
+}
+
+void unloader::init_late(void)
+{
+
+}
+
+void unloader::fixed_update_late(Dimension *dimension)
+{
+ auto group = dimension->entities.group(entt::get<PlayerComponent, TransformComponent>);
+ auto boxes = std::vector<ChunkAABB>();
+
+ for(const auto [entity, transform] : group.each()) {
+ ChunkAABB aabb;
+ aabb.min = transform.chunk - static_cast<chunk_pos::value_type>(server_game::view_distance.get_value());
+ aabb.max = transform.chunk + static_cast<chunk_pos::value_type>(server_game::view_distance.get_value());
+ boxes.push_back(aabb);
+ }
+
+ auto view = dimension->chunks.view<ChunkComponent>();
+ auto chunk_in_view = false;
+
+ for(const auto [entity, chunk] : view.each()) {
+ chunk_in_view = false;
+
+ for(const auto &aabb : boxes) {
+ if(aabb.contains(chunk.cpos)) {
+ chunk_in_view = true;
+ break;
+ }
+ }
+
+ if(chunk_in_view) {
+ // The chunk is within view box of at least
+ // a single player; we shouldn't unload it now
+ continue;
+ }
+
+ if(dimension->chunks.any_of<InhabitedComponent>(entity)) {
+ // Only store inhabited chunks on disk
+ universe::save_chunk(dimension, chunk.cpos);
+ }
+
+ dimension->remove_chunk(entity);
+ }
+}
diff --git a/game/server/unloader.hh b/game/server/unloader.hh
new file mode 100644
index 0000000..d7a95da
--- /dev/null
+++ b/game/server/unloader.hh
@@ -0,0 +1,14 @@
+#ifndef SERVER_UNLOADER_HH
+#define SERVER_UNLOADER_HH 1
+#pragma once
+
+class Dimension;
+
+namespace unloader
+{
+void init(void);
+void init_late(void);
+void fixed_update_late(Dimension *dimension);
+} // namespace unloader
+
+#endif /* SERVER_UNLOADER_HH */
diff --git a/game/server/vserver.ico b/game/server/vserver.ico
new file mode 100644
index 0000000..02ff006
--- /dev/null
+++ b/game/server/vserver.ico
Binary files differ
diff --git a/game/server/vserver.rc b/game/server/vserver.rc
new file mode 100644
index 0000000..b6828bf
--- /dev/null
+++ b/game/server/vserver.rc
@@ -0,0 +1 @@
+IDI_ICON1 ICON DISCARDABLE "vserver.ico"
diff --git a/game/server/whitelist.cc b/game/server/whitelist.cc
new file mode 100644
index 0000000..4d9d394
--- /dev/null
+++ b/game/server/whitelist.cc
@@ -0,0 +1,97 @@
+#include "server/pch.hh"
+#include "server/whitelist.hh"
+
+#include "core/config.hh"
+#include "core/crc64.hh"
+#include "core/strtools.hh"
+
+#include "server/game.hh"
+#include "server/globals.hh"
+
+constexpr static const char *DEFAULT_FILENAME = "whitelist.txt";
+constexpr static char SEPARATOR_CHAR = ':';
+
+ConfigBoolean whitelist::enabled(false);
+ConfigString whitelist::filename(DEFAULT_FILENAME);
+
+static emhash8::HashMap<std::string, std::uint64_t> whitelist_map;
+
+void whitelist::init(void)
+{
+ globals::server_config.add_value("whitelist.enabled", whitelist::enabled);
+ globals::server_config.add_value("whitelist.filename", whitelist::filename);
+}
+
+void whitelist::init_late(void)
+{
+ whitelist_map.clear();
+
+ if(!whitelist::enabled.get_value()) {
+ // Not enabled, shouldn't
+ // even bother with parsing
+ // the whitelist file
+ return;
+ }
+
+ if(strtools::is_whitespace(whitelist::filename.get())) {
+ spdlog::warn("whitelist: enabled but filename is empty, using default ({})", DEFAULT_FILENAME);
+ whitelist::filename.set(DEFAULT_FILENAME);
+ }
+
+ PHYSFS_File *file = PHYSFS_openRead(whitelist::filename.get());
+
+ if(file == nullptr) {
+ spdlog::warn("whitelist: {}: {}", whitelist::filename.get(), PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
+ whitelist::enabled.set_value(false);
+ return;
+ }
+
+ auto source = std::string(PHYSFS_fileLength(file), char(0x00));
+ PHYSFS_readBytes(file, source.data(), source.size());
+ PHYSFS_close(file);
+
+ std::istringstream stream(source);
+ std::string line;
+
+ while(std::getline(stream, line)) {
+ const auto location = line.find_last_of(SEPARATOR_CHAR);
+
+ if(location == std::string::npos) {
+ // Entries that don't define a password field default
+ // to the global server password; this allows easier adding
+ // of guest accounts which can later be edited to use a better password
+ whitelist_map[line] = server_game::password_hash;
+ }
+ else {
+ const auto username = line.substr(0, location);
+ const auto password = line.substr(location + 1);
+ whitelist_map[username] = crc64::get(password);
+ }
+ }
+
+ PHYSFS_close(file);
+}
+
+void whitelist::deinit(void)
+{
+ // UNDONE: implement saving
+}
+
+bool whitelist::contains(const char *username)
+{
+ return whitelist_map.contains(username);
+}
+
+bool whitelist::matches(const char *username, std::uint64_t password_hash)
+{
+ const auto it = whitelist_map.find(username);
+
+ if(it == whitelist_map.cend()) {
+ // Not whitelisted, no match
+ return false;
+ }
+
+ if(it->second == password_hash)
+ return true;
+ return false;
+}
diff --git a/game/server/whitelist.hh b/game/server/whitelist.hh
new file mode 100644
index 0000000..6cbaa0b
--- /dev/null
+++ b/game/server/whitelist.hh
@@ -0,0 +1,27 @@
+#ifndef SERVER_WHITELIST_HH
+#define SERVER_WHITELIST_HH 1
+#pragma once
+
+class ConfigBoolean;
+class ConfigString;
+
+namespace whitelist
+{
+extern ConfigBoolean enabled;
+extern ConfigString filename;
+} // namespace whitelist
+
+namespace whitelist
+{
+void init(void);
+void init_late(void);
+void deinit(void);
+} // namespace whitelist
+
+namespace whitelist
+{
+bool contains(const char *username);
+bool matches(const char *username, std::uint64_t password_hash);
+} // namespace whitelist
+
+#endif /* SERVER_WHITELIST_HH */
diff --git a/game/server/worldgen.cc b/game/server/worldgen.cc
new file mode 100644
index 0000000..83fddbb
--- /dev/null
+++ b/game/server/worldgen.cc
@@ -0,0 +1,128 @@
+#include "server/pch.hh"
+#include "server/worldgen.hh"
+
+#include "shared/chunk.hh"
+#include "shared/dimension.hh"
+#include "shared/protocol.hh"
+#include "shared/threading.hh"
+
+#include "server/sessions.hh"
+
+static emhash8::HashMap<Dimension *, emhash8::HashMap<chunk_pos, std::unordered_set<Session *>>> active_tasks;
+
+class WorldgenTask final : public Task {
+public:
+ explicit WorldgenTask(Dimension *dimension, const chunk_pos &cpos);
+ virtual ~WorldgenTask(void) = default;
+ virtual void process(void) override;
+ virtual void finalize(void) override;
+
+private:
+ Dimension *m_dimension;
+ VoxelStorage m_voxels;
+ chunk_pos m_cpos;
+};
+
+WorldgenTask::WorldgenTask(Dimension *dimension, const chunk_pos &cpos)
+{
+ m_dimension = dimension;
+ m_voxels.fill(rand()); // trolling
+ m_cpos = cpos;
+}
+
+void WorldgenTask::process(void)
+{
+ if(!m_dimension->generate(m_cpos, m_voxels)) {
+ set_status(task_status::CANCELLED);
+ }
+}
+
+void WorldgenTask::finalize(void)
+{
+ auto dim_tasks = active_tasks.find(m_dimension);
+
+ if(dim_tasks == active_tasks.cend()) {
+ // Normally this should never happen but
+ // one can never be sure about anything
+ // when that anything is threaded out
+ return;
+ }
+
+ auto it = dim_tasks->second.find(m_cpos);
+
+ if(it == dim_tasks->second.cend()) {
+ // Normally this should never happen but
+ // one can never be sure about anything
+ // when that anything is threaded out
+ return;
+ }
+
+ auto chunk = m_dimension->create_chunk(m_cpos);
+ chunk->set_voxels(m_voxels);
+
+ protocol::ChunkVoxels response;
+ response.voxels = m_voxels;
+ response.chunk = m_cpos;
+
+ auto packet = protocol::encode(response);
+
+ for(auto session : it->second) {
+ if(session->peer) {
+ // Respond with the voxels to every session
+ // that has requested this specific chunk for this dimension
+ enet_peer_send(session->peer, protocol::CHANNEL, packet);
+ }
+ }
+
+ dim_tasks->second.erase(it);
+
+ if(dim_tasks->second.empty()) {
+ // There are no more requests
+ // to generate a chunk for that
+ // dimension, at least for now
+ active_tasks.erase(dim_tasks);
+ }
+}
+
+bool worldgen::is_generating(Dimension *dimension, const chunk_pos &cpos)
+{
+ auto dim_tasks = active_tasks.find(dimension);
+
+ if(dim_tasks == active_tasks.cend()) {
+ // No tasks for this dimension
+ return false;
+ }
+
+ auto it = dim_tasks->second.find(cpos);
+
+ if(it == dim_tasks->second.cend()) {
+ // Not generating this chunk
+ return false;
+ }
+
+ return true;
+}
+
+void worldgen::request_chunk(Session *session, const chunk_pos &cpos)
+{
+ if(session->dimension) {
+ auto dim_tasks = active_tasks.find(session->dimension);
+
+ if(dim_tasks == active_tasks.cend()) {
+ dim_tasks = active_tasks.emplace(session->dimension, emhash8::HashMap<chunk_pos, std::unordered_set<Session *>>()).first;
+ }
+
+ auto it = dim_tasks->second.find(cpos);
+
+ if(it == dim_tasks->second.cend()) {
+ auto &sessions = dim_tasks->second.insert_or_assign(cpos, std::unordered_set<Session *>()).first->second;
+ sessions.insert(session);
+
+ threading::submit<WorldgenTask>(session->dimension, cpos);
+
+ return;
+ }
+
+ it->second.insert(session);
+ }
+}
diff --git a/game/server/worldgen.hh b/game/server/worldgen.hh
new file mode 100644
index 0000000..3153071
--- /dev/null
+++ b/game/server/worldgen.hh
@@ -0,0 +1,16 @@
+#ifndef SERVER_WORLDGEN_HH
+#define SERVER_WORLDGEN_HH 1
+#pragma once
+
+#include "shared/types.hh"
+
+class Dimension;
+class Session;
+
+namespace worldgen
+{
+bool is_generating(Dimension *dimension, const chunk_pos &cpos);
+void request_chunk(Session *session, const chunk_pos &cpos);
+} // namespace worldgen
+
+#endif /* SERVER_WORLDGEN_HH */