diff options
Diffstat (limited to 'src/game/server')
28 files changed, 2196 insertions, 0 deletions
diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt new file mode 100644 index 0000000..bd0eff3 --- /dev/null +++ b/src/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_20) +target_include_directories(vserver PUBLIC "${DEPS_INCLUDE_DIR}") +target_include_directories(vserver PRIVATE "${PROJECT_SOURCE_DIR}/src") +target_include_directories(vserver PRIVATE "${PROJECT_SOURCE_DIR}/src/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/src/game/server/chat.cc b/src/game/server/chat.cc new file mode 100644 index 0000000..1634c59 --- /dev/null +++ b/src/game/server/chat.cc @@ -0,0 +1,54 @@ +#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/src/game/server/chat.hh b/src/game/server/chat.hh new file mode 100644 index 0000000..2557290 --- /dev/null +++ b/src/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/src/game/server/game.cc b/src/game/server/game.cc new file mode 100644 index 0000000..978474c --- /dev/null +++ b/src/game/server/game.cc @@ -0,0 +1,148 @@ +#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" +#include "server/worldgen.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_server(); + + status::init(); + + server_chat::init(); + server_recieve::init(); + + worldgen::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); + + 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/src/game/server/game.hh b/src/game/server/game.hh new file mode 100644 index 0000000..98c6bf3 --- /dev/null +++ b/src/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/src/game/server/globals.cc b/src/game/server/globals.cc new file mode 100644 index 0000000..883588b --- /dev/null +++ b/src/game/server/globals.cc @@ -0,0 +1,18 @@ +#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/src/game/server/globals.hh b/src/game/server/globals.hh new file mode 100644 index 0000000..54f025a --- /dev/null +++ b/src/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/src/game/server/inhabited.hh b/src/game/server/inhabited.hh new file mode 100644 index 0000000..c68ddaa --- /dev/null +++ b/src/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/src/game/server/main.cc b/src/game/server/main.cc new file mode 100644 index 0000000..734ae6a --- /dev/null +++ b/src/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/src/game/server/overworld.cc b/src/game/server/overworld.cc new file mode 100644 index 0000000..a280e06 --- /dev/null +++ b/src/game/server/overworld.cc @@ -0,0 +1,380 @@ +#include "server/pch.hh" + +#include "server/overworld.hh" + +#include "core/vectors.hh" + +#include "shared/coord.hh" +#include "shared/game_voxels.hh" +#include "shared/voxel_storage.hh" + +// FIXME: load these from a file +static void compute_tree_feature(unsigned int height, Feature& feature, voxel_id log_voxel, voxel_id leaves_voxel) +{ + // Ensure the tree height is too small + height = vx::max<unsigned int>(height, 4U); + + // Put down a single piece of dirt + feature.push_back({ voxel_pos(0, -1, 0), game_voxels::dirt, true }); + + // Generate tree stem + for(unsigned int i = 0; i < height; ++i) { + feature.push_back({ voxel_pos(0, i, 0), log_voxel, true }); + } + + auto leaves_start = height - 3U; + auto leaves_thick_end = height - 2U; + auto leaves_thin_end = height - 1U; + + // Generate the thin 3x3 layer of leaves that + // starts from leaves_start and ends at leaves_thin_end + for(unsigned int i = leaves_start; i <= leaves_thin_end; ++i) { + feature.push_back({ local_pos(-1, i, -1), leaves_voxel, false }); + feature.push_back({ local_pos(-1, i, +0), leaves_voxel, false }); + feature.push_back({ local_pos(-1, i, +1), leaves_voxel, false }); + feature.push_back({ local_pos(+0, i, -1), leaves_voxel, false }); + feature.push_back({ local_pos(+0, i, +1), leaves_voxel, false }); + feature.push_back({ local_pos(+1, i, -1), leaves_voxel, false }); + feature.push_back({ local_pos(+1, i, +0), leaves_voxel, false }); + feature.push_back({ local_pos(+1, i, +1), leaves_voxel, false }); + } + + // Generate the tree cap; a 3x3 patch of leaves + // that is slapped right on top of the thin 3x3 layer + feature.push_back({ local_pos(-1, height, +0), leaves_voxel, false }); + feature.push_back({ local_pos(+0, height, -1), leaves_voxel, false }); + feature.push_back({ local_pos(+0, height, +0), leaves_voxel, false }); + feature.push_back({ local_pos(+0, height, +1), leaves_voxel, false }); + feature.push_back({ local_pos(+1, height, +0), leaves_voxel, false }); + + // Generate the thin 5x5 layer of leaves that + // starts from leaves_start and ends at leaves_thin_end + for(unsigned int i = leaves_start; i <= leaves_thick_end; ++i) { + feature.push_back({ local_pos(-1, i, -2), leaves_voxel, false }); + feature.push_back({ local_pos(-1, i, +2), leaves_voxel, false }); + feature.push_back({ local_pos(-2, i, -1), leaves_voxel, false }); + feature.push_back({ local_pos(-2, i, -2), leaves_voxel, false }); + feature.push_back({ local_pos(-2, i, +0), leaves_voxel, false }); + feature.push_back({ local_pos(-2, i, +1), leaves_voxel, false }); + feature.push_back({ local_pos(-2, i, +2), leaves_voxel, false }); + feature.push_back({ local_pos(+0, i, -2), leaves_voxel, false }); + feature.push_back({ local_pos(+0, i, +2), leaves_voxel, false }); + feature.push_back({ local_pos(+1, i, -2), leaves_voxel, false }); + feature.push_back({ local_pos(+1, i, +2), leaves_voxel, false }); + feature.push_back({ local_pos(+2, i, -1), leaves_voxel, false }); + feature.push_back({ local_pos(+2, i, -2), leaves_voxel, false }); + feature.push_back({ local_pos(+2, i, +0), leaves_voxel, false }); + feature.push_back({ local_pos(+2, i, +1), leaves_voxel, false }); + feature.push_back({ local_pos(+2, i, +2), leaves_voxel, false }); + } +} + +Overworld::Overworld(const char* name) : Dimension(name, -30.0f) +{ + m_bottommost_chunk.set_limits(-64, -4); + m_terrain_variation.set_limits(16, 256); + + compute_tree_feature(4U, m_feat_tree[0], game_voxels::oak_log, game_voxels::oak_leaves); + compute_tree_feature(5U, m_feat_tree[1], game_voxels::oak_log, game_voxels::oak_leaves); + compute_tree_feature(6U, m_feat_tree[2], game_voxels::oak_log, game_voxels::oak_leaves); + compute_tree_feature(8U, m_feat_tree[3], game_voxels::oak_log, game_voxels::oak_leaves); +} + +void Overworld::init(Config& config) +{ + m_terrain_variation.set_value(64); + m_bottommost_chunk.set_value(-4); + + config.add_value("overworld.terrain_variation", m_terrain_variation); + config.add_value("overworld.bottommost_chunk", m_bottommost_chunk); +} + +void Overworld::init_late(std::uint64_t global_seed) +{ + std::mt19937 twister(global_seed); + + m_fnl_variation = fnlCreateState(); + m_fnl_variation.seed = static_cast<int>(twister()); + m_fnl_variation.noise_type = FNL_NOISE_PERLIN; + m_fnl_variation.frequency = 0.001f; + + m_fnl_terrain = fnlCreateState(); + m_fnl_terrain.seed = static_cast<int>(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>(twister()); + m_fnl_caves_a.noise_type = FNL_NOISE_PERLIN; + m_fnl_caves_a.fractal_type = FNL_FRACTAL_RIDGED; + m_fnl_caves_a.frequency = 0.0125f; + m_fnl_caves_a.octaves = 1; + + m_fnl_caves_b = fnlCreateState(); + m_fnl_caves_b.seed = static_cast<int>(twister()); + m_fnl_caves_b.noise_type = FNL_NOISE_OPENSIMPLEX2S; + m_fnl_caves_b.fractal_type = FNL_FRACTAL_RIDGED; + m_fnl_caves_b.frequency = 0.0125f; + m_fnl_caves_b.octaves = 1; + + m_fnl_nvdi = fnlCreateState(); + m_fnl_nvdi.seed = static_cast<int>(twister()); + m_fnl_nvdi.noise_type = FNL_NOISE_OPENSIMPLEX2S; + m_fnl_nvdi.frequency = 1.0f; + + m_metamap.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(); + + m_mutex.lock(); + generate_surface(cpos, voxels); + m_mutex.unlock(); + + m_mutex.lock(); + generate_caves(cpos, voxels); + m_mutex.unlock(); + + m_mutex.lock(); + generate_features(cpos, voxels); + m_mutex.unlock(); + + return true; +} + +bool Overworld::is_inside_cave(const voxel_pos& vpos) +{ + auto noise_a = fnlGetNoise3D(&m_fnl_caves_a, vpos.x, vpos.y * 2.0f, vpos.z); + auto noise_b = fnlGetNoise3D(&m_fnl_caves_b, vpos.x, vpos.y * 2.0f, vpos.z); + return (noise_a > 0.95f) && (noise_b > 0.85f); +} + +bool Overworld::is_inside_terrain(const voxel_pos& vpos) +{ + auto variation_noise = fnlGetNoise3D(&m_fnl_terrain, vpos.x, vpos.y, vpos.z); + auto variation = m_terrain_variation.get_value() * (1.0f - (variation_noise * variation_noise)); + auto noise = variation * fnlGetNoise3D(&m_fnl_terrain, vpos.x, vpos.y, vpos.z) - vpos.y; + return noise > 0.0f; +} + +const Overworld_Metadata& Overworld::get_or_create_metadata(const chunk_pos_xz& cpos) +{ + auto it = m_metamap.find(cpos); + + if(it != m_metamap.cend()) { + // Metadata is present + return it->second; + } + + auto& metadata = m_metamap.insert_or_assign(cpos, Overworld_Metadata()).first->second; + metadata.entropy.fill(std::numeric_limits<std::uint64_t>::max()); + metadata.heightmap.fill(std::numeric_limits<voxel_pos::value_type>::min()); + + auto twister = std::mt19937_64(std::hash<chunk_pos_xz>()(cpos)); + auto variation = m_terrain_variation.get_value(); + + // Generator might need some randomness + // that depends on 2D coordinates, so we + // generate this entropy ahead of time + for(int i = 0; i < CHUNK_AREA; ++i) { + metadata.entropy[i] = twister(); + } + + // Generate speculative heightmap; + // Cave generation might have issues with placing + // surface features such as trees but I genuinely don't give a shit + for(int lx = 0; lx < CHUNK_SIZE; lx += 1) { + for(int lz = 0; lz < CHUNK_SIZE; lz += 1) { + auto hdx = static_cast<std::size_t>(lx + lz * CHUNK_SIZE); + auto vpos = coord::to_voxel(chunk_pos(cpos.x, 0, cpos.y), local_pos(lx, 0, lz)); + + for(vpos.y = variation; vpos.y >= -variation; vpos.y -= 1) { + if(is_inside_terrain(vpos)) { + metadata.heightmap[hdx] = vpos.y; + break; + } + } + } + } + + auto nvdi_value = 0.5f + 0.5f * fnlGetNoise2D(&m_fnl_nvdi, cpos.x, cpos.y); + auto tree_density = (nvdi_value >= 0.33f) ? vx::floor<unsigned int>(nvdi_value * 4.0f) : 0U; + + for(unsigned int i = 0U; i < tree_density; ++i) { + auto lpos = local_pos((twister() % CHUNK_SIZE), (twister() % OW_NUM_TREES), (twister() % CHUNK_SIZE)); + auto is_unique = true; + + for(const auto& check_lpos : metadata.trees) { + if(vx::distance2(check_lpos, lpos) <= 9) { + is_unique = false; + break; + } + } + + if(is_unique) { + metadata.trees.push_back(lpos); + } + } + + return metadata; +} + +void Overworld::generate_terrain(const chunk_pos& cpos, VoxelStorage& voxels) +{ + auto& metadata = get_or_create_metadata(chunk_pos_xz(cpos.x, cpos.z)); + auto variation = m_terrain_variation.get_value(); + + for(unsigned long i = 0; i < CHUNK_VOLUME; ++i) { + auto lpos = coord::to_local(i); + auto vpos = coord::to_voxel(cpos, lpos); + + if(vpos.y > variation) { + voxels[i] = NULL_VOXEL_ID; + continue; + } + + if(vpos.y < -variation) { + voxels[i] = game_voxels::stone; + continue; + } + + if(is_inside_terrain(vpos)) { + voxels[i] = game_voxels::stone; + continue; + } + } +} + +void Overworld::generate_surface(const chunk_pos& cpos, VoxelStorage& voxels) +{ + auto& metadata = get_or_create_metadata(chunk_pos_xz(cpos.x, cpos.z)); + auto variation = m_terrain_variation.get_value(); + + for(unsigned long i = 0; i < CHUNK_VOLUME; ++i) { + auto lpos = coord::to_local(i); + auto vpos = coord::to_voxel(cpos, lpos); + auto hdx = static_cast<std::size_t>(lpos.x + lpos.z * CHUNK_SIZE); + + if((vpos.y > variation) || (vpos.y < -variation)) { + // Speculative optimization + continue; + } + + if(voxels[i] == NULL_VOXEL_ID) { + // Surface voxel checks only apply for solid voxels; + // it's kind of obvious you can't replace air with grass + continue; + } + + unsigned int depth = 0U; + + for(unsigned int dy = 0U; dy < 5U; dy += 1U) { + auto d_lpos = local_pos(lpos.x, lpos.y + dy + 1, lpos.z); + auto d_vpos = coord::to_voxel(cpos, d_lpos); + auto d_index = coord::to_index(d_lpos); + + if(d_lpos.y >= CHUNK_SIZE) { + if(!is_inside_terrain(d_vpos)) { + break; + } + + depth += 1U; + } else { + if(voxels[d_index] == NULL_VOXEL_ID) { + break; + } + + depth += 1U; + } + } + + if(depth < 5U) { + if(depth == 0U) { + voxels[i] = game_voxels::grass; + } else { + voxels[i] = game_voxels::dirt; + } + } + } +} + +void Overworld::generate_caves(const chunk_pos& cpos, VoxelStorage& voxels) +{ + auto& metadata = get_or_create_metadata(chunk_pos_xz(cpos.x, cpos.z)); + auto variation = m_terrain_variation.get_value(); + + for(unsigned long i = 0U; i < CHUNK_VOLUME; ++i) { + auto lpos = coord::to_local(i); + auto vpos = coord::to_voxel(cpos, lpos); + + if(vpos.y > variation) { + // Speculative optimization - there's no solid + // terrain above variation to carve caves out from + continue; + } + + if(is_inside_cave(vpos)) { + voxels[i] = NULL_VOXEL_ID; + continue; + } + } +} + +void Overworld::generate_features(const chunk_pos& cpos, VoxelStorage& voxels) +{ + const chunk_pos_xz tree_chunks[] = { + chunk_pos_xz(cpos.x - 0, cpos.z - 1), + chunk_pos_xz(cpos.x - 1, cpos.z - 1), + chunk_pos_xz(cpos.x - 1, cpos.z + 0), + chunk_pos_xz(cpos.x - 1, cpos.z + 1), + chunk_pos_xz(cpos.x + 0, cpos.z + 0), + chunk_pos_xz(cpos.x + 0, cpos.z + 1), + chunk_pos_xz(cpos.x + 1, cpos.z - 1), + chunk_pos_xz(cpos.x + 1, cpos.z + 0), + chunk_pos_xz(cpos.x + 1, cpos.z + 1), + }; + + for(unsigned int i = 0U; i < vx::array_size(tree_chunks); ++i) { + const auto& cpos_xz = tree_chunks[i]; + const auto& metadata = get_or_create_metadata(cpos_xz); + + for(const auto& tree_info : metadata.trees) { + auto hdx = static_cast<std::size_t>(tree_info.x + tree_info.z * CHUNK_SIZE); + auto height = metadata.heightmap[hdx]; + + if(height == std::numeric_limits<voxel_pos::value_type>::min()) { + // What happened? Cave happened + continue; + } + + auto cpos_xyz = chunk_pos(cpos_xz.x, 0, cpos_xz.y); + auto lpos_xyz = local_pos(tree_info.x, 0, tree_info.z); + + auto vpos = coord::to_voxel(cpos_xyz, lpos_xyz); + vpos.y = height; + + if(is_inside_cave(vpos)) { + // Cave is in the way + continue; + } + + m_feat_tree[tree_info.y].place(vpos + DIR_UP<voxel_pos::value_type>, cpos, voxels); + } + } +} diff --git a/src/game/server/overworld.hh b/src/game/server/overworld.hh new file mode 100644 index 0000000..972a91d --- /dev/null +++ b/src/game/server/overworld.hh @@ -0,0 +1,63 @@ +#ifndef SERVER_OVERWORLD_HH +#define SERVER_OVERWORLD_HH 1 +#pragma once + +#include "core/config.hh" + +#include "shared/const.hh" +#include "shared/dimension.hh" +#include "shared/feature.hh" + +constexpr static unsigned int OW_NUM_TREES = 4U; + +struct Overworld_Metadata final { + dimension_entropy_map entropy; + dimension_height_map heightmap; + std::vector<local_pos> trees; +}; + +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: + bool is_inside_cave(const voxel_pos& vpos); + bool is_inside_terrain(const voxel_pos& vpos); + +private: + const Overworld_Metadata& get_or_create_metadata(const chunk_pos_xz& cpos); + +private: + void generate_terrain(const chunk_pos& cpos, VoxelStorage& voxels); + void generate_surface(const chunk_pos& cpos, VoxelStorage& voxels); + void generate_caves(const chunk_pos& cpos, VoxelStorage& voxels); + void generate_features(const chunk_pos& cpos, VoxelStorage& voxels); + +private: + ConfigInt m_terrain_variation; + ConfigInt m_bottommost_chunk; + +private: + emhash8::HashMap<chunk_pos_xz, Overworld_Metadata> m_metamap; + +private: + fnl_state m_fnl_variation; + fnl_state m_fnl_terrain; + fnl_state m_fnl_caves_a; + fnl_state m_fnl_caves_b; + fnl_state m_fnl_nvdi; + +private: + Feature m_feat_tree[OW_NUM_TREES]; + +private: + std::mutex m_mutex; +}; + +#endif /* SERVER_OVERWORLD_HH */ diff --git a/src/game/server/pch.hh b/src/game/server/pch.hh new file mode 100644 index 0000000..89b396b --- /dev/null +++ b/src/game/server/pch.hh @@ -0,0 +1,7 @@ +#ifndef SERVER_PCH_HH +#define SERVER_PCH_HH 1 +#pragma once + +#include <shared/pch.hh> + +#endif /* SERVER_PCH_HH */ diff --git a/src/game/server/receive.cc b/src/game/server/receive.cc new file mode 100644 index 0000000..7674122 --- /dev/null +++ b/src/game/server/receive.cc @@ -0,0 +1,170 @@ +#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; + // ignore all requests from players to build there + return; + } + + auto chunk = session->dimension->find_chunk(cpos); + + if(chunk == nullptr) { + // The chunk is not loaded, so we must + // ignore any requests from players to build there + return; + } + + chunk->set_voxel(packet.voxel, index); + + session->dimension->chunks.emplace_or_replace<InhabitedComponent>(chunk->get_entity()); + + 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/src/game/server/receive.hh b/src/game/server/receive.hh new file mode 100644 index 0000000..57090b5 --- /dev/null +++ b/src/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/src/game/server/sessions.cc b/src/game/server/sessions.cc new file mode 100644 index 0000000..43abbb7 --- /dev/null +++ b/src/game/server/sessions.cc @@ -0,0 +1,419 @@ +#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/item_registry.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_registry_checksum != voxel_registry::calcualte_checksum()) { + protocol::Disconnect response; + response.reason = "protocol.voxel_registry_checksum"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + + if(packet.item_registry_checksum != item_registry::calcualte_checksum()) { + protocol::Disconnect response; + response.reason = "protocol.item_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; + } else { + return nullptr; + } +} + +Session* sessions::find(std::uint16_t client_index) +{ + if(client_index < sessions_vector.size()) { + if(!sessions_vector[client_index].peer) { + return nullptr; + } else { + 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; + } else { + return nullptr; + } +} + +Session* sessions::find(ENetPeer* peer) +{ + if(peer != nullptr) { + return reinterpret_cast<Session*>(peer->data); + } else { + 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) { + packet.names.push_back(sessions_vector[i].client_username); + } + } + + protocol::broadcast(globals::server_host, protocol::encode(packet)); +} diff --git a/src/game/server/sessions.hh b/src/game/server/sessions.hh new file mode 100644 index 0000000..b9a6348 --- /dev/null +++ b/src/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/src/game/server/status.cc b/src/game/server/status.cc new file mode 100644 index 0000000..6b64719 --- /dev/null +++ b/src/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) +{ + 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/src/game/server/status.hh b/src/game/server/status.hh new file mode 100644 index 0000000..5f939f7 --- /dev/null +++ b/src/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/src/game/server/universe.cc b/src/game/server/universe.cc new file mode 100644 index 0000000..96884cf --- /dev/null +++ b/src/game/server/universe.cc @@ -0,0 +1,215 @@ +#include "server/pch.hh" + +#include "server/universe.hh" + +#include "core/buffer.hh" +#include "core/config.hh" +#include "core/epoch.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 std::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 = std::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 = std::format("{}/dimension.conf", dimension_dir); + metadata->zvox_dir = std::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 = std::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/src/game/server/universe.hh b/src/game/server/universe.hh new file mode 100644 index 0000000..2a16806 --- /dev/null +++ b/src/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/src/game/server/unloader.cc b/src/game/server/unloader.cc new file mode 100644 index 0000000..f986a61 --- /dev/null +++ b/src/game/server/unloader.cc @@ -0,0 +1,76 @@ +#include "server/pch.hh" + +#include "server/unloader.hh" + +#include "core/config.hh" + +#include "shared/chunk.hh" +#include "shared/chunk_aabb.hh" +#include "shared/dimension.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/src/game/server/unloader.hh b/src/game/server/unloader.hh new file mode 100644 index 0000000..414cdc4 --- /dev/null +++ b/src/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/src/game/server/vserver.ico b/src/game/server/vserver.ico Binary files differnew file mode 100644 index 0000000..02ff006 --- /dev/null +++ b/src/game/server/vserver.ico diff --git a/src/game/server/vserver.rc b/src/game/server/vserver.rc new file mode 100644 index 0000000..b6828bf --- /dev/null +++ b/src/game/server/vserver.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON DISCARDABLE "vserver.ico" diff --git a/src/game/server/whitelist.cc b/src/game/server/whitelist.cc new file mode 100644 index 0000000..6635122 --- /dev/null +++ b/src/game/server/whitelist.cc @@ -0,0 +1,95 @@ +#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; + } + + return it->second == password_hash; +} diff --git a/src/game/server/whitelist.hh b/src/game/server/whitelist.hh new file mode 100644 index 0000000..fe7656b --- /dev/null +++ b/src/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/src/game/server/worldgen.cc b/src/game/server/worldgen.cc new file mode 100644 index 0000000..5c74d47 --- /dev/null +++ b/src/game/server/worldgen.cc @@ -0,0 +1,148 @@ +#include "server/pch.hh" + +#include "server/worldgen.hh" + +#include "core/cmdline.hh" + +#include "shared/chunk.hh" +#include "shared/dimension.hh" +#include "shared/protocol.hh" +#include "shared/threading.hh" + +#include "server/globals.hh" +#include "server/inhabited.hh" +#include "server/sessions.hh" + +static bool aggressive_caching; + +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); + + if(aggressive_caching) { + // Marking the chunk with InhabitedComponent makes + // it so that it is saved regardles of whether it was + // modified by players or not. This isn't particularly + // good for server-side disk usage but it might improve performance + m_dimension->chunks.emplace<InhabitedComponent>(chunk->get_entity()); + } + + 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); + } +} + +void worldgen::init(void) +{ + aggressive_caching = cmdline::contains("aggressive-caching"); +} + +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/src/game/server/worldgen.hh b/src/game/server/worldgen.hh new file mode 100644 index 0000000..b0127e5 --- /dev/null +++ b/src/game/server/worldgen.hh @@ -0,0 +1,21 @@ +#ifndef SERVER_WORLDGEN_HH +#define SERVER_WORLDGEN_HH 1 +#pragma once + +#include "shared/types.hh" + +class Dimension; +class Session; + +namespace worldgen +{ +void init(void); +} // namespace worldgen + +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 */ |
