diff options
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/CMakeLists.txt | 50 | ||||
| -rw-r--r-- | src/server/chunk_data.hh | 6 | ||||
| -rw-r--r-- | src/server/client.cc | 14 | ||||
| -rw-r--r-- | src/server/client.hh | 15 | ||||
| -rw-r--r-- | src/server/database.cc | 77 | ||||
| -rw-r--r-- | src/server/database.hh | 2 | ||||
| -rw-r--r-- | src/server/init.cc | 45 | ||||
| -rw-r--r-- | src/server/init.hh | 15 | ||||
| -rw-r--r-- | src/server/movement.cc | 87 | ||||
| -rw-r--r-- | src/server/movement/movement.cc | 98 | ||||
| -rw-r--r-- | src/server/movement/movement.hh (renamed from src/server/movement.hh) | 8 | ||||
| -rw-r--r-- | src/server/resources.cc | 1 | ||||
| -rw-r--r-- | src/server/resources.hh | 5 | ||||
| -rw-r--r-- | src/server/server.cc | 470 | ||||
| -rw-r--r-- | src/server/server.hh | 6 | ||||
| -rw-r--r-- | src/server/shared.hh | 2 | ||||
| -rw-r--r-- | src/server/world.cc | 53 | ||||
| -rw-r--r-- | src/server/world.hh | 59 | ||||
| -rw-r--r-- | src/server/world/chunk.cc | 26 | ||||
| -rw-r--r-- | src/server/world/chunk.hh | 46 |
20 files changed, 688 insertions, 397 deletions
diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt new file mode 100644 index 0000000..7ac16c2 --- /dev/null +++ b/src/server/CMakeLists.txt @@ -0,0 +1,50 @@ +cmake_minimum_required(VERSION 3.18) + +project(server) + +file (GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS + "*.cc" +) +file (GLOB_RECURSE HEADER_FILES CONFIGURE_DEPENDS + "*.hh" + "../shared/*.hh" +) +add_library(${PROJECT_NAME} STATIC + ${SOURCE_FILES} +) + +find_library(LIB_SQLITE3 sqlite3 SQLITE3 REQUIRED) +find_library(LIB_PROTOBUF protobuf libprotobuf REQUIRED) +find_package(Boost COMPONENTS iostreams REQUIRED) +find_package(Threads REQUIRED) +find_package(Backtrace REQUIRED) + +target_compile_options(${PROJECT_NAME} PRIVATE + -Wall -Wextra -Wshadow -Wdouble-promotion -Wformat=2 -Wundef -fno-common + -Wconversion -Wpedantic -std=c++20 -O2 + -Wno-exceptions + -Wno-missing-field-initializers -Wno-unknown-pragmas +) +if (${IS_DEBUG}) + target_compile_options(${PROJECT_NAME} PRIVATE + ¦ -fstack-protector-strong -fno-omit-frame-pointer -fsanitize=undefined + ) + target_link_options(${PROJECT_NAME} PRIVATE + ¦ -fstack-protector-strong -fsanitize=undefined + ) +endif() +target_include_directories(${PROJECT_NAME} PRIVATE + "${PROJECT_SOURCE_DIR}/../../src" +) +target_link_libraries(${PROJECT_NAME} PRIVATE + ${LIB_SQLITE3} + ${LIB_PROTOBUF} + ${Backtrace_LIBRARIES} + ${Threads_LIBRARIES} + ${Boost_LIBRARIES} + ${FREETYPE_LIBRARIES} + shared +) +target_precompile_headers(${PROJECT_NAME} PRIVATE + ${HEADER_FILES} +) diff --git a/src/server/chunk_data.hh b/src/server/chunk_data.hh index eea5cef..349d4dc 100644 --- a/src/server/chunk_data.hh +++ b/src/server/chunk_data.hh @@ -5,8 +5,8 @@ #include <optional> #include <unordered_set> -#include "server/world.hh" -#include "shared/player.hh" +#include "server/world/chunk.hh" +#include "shared/entity/player.hh" namespace server { @@ -15,7 +15,7 @@ public: // nullopt = constructing/destructing via future operations in thread pool // we use shared_ptr here to avoid complex moves in boost::asio::post. // There is no good reason to use shared_ptr over unique_ptr, other than - // boost::asio::post requiring copy_constructable args in std::bind. + // boost::asio::post requiring copy_constructable args in std::bind. std::optional<std::shared_ptr<server::world::chunk>> chunk; // players associated with the chunk std::unordered_set<shared::player::index_t> players; diff --git a/src/server/client.cc b/src/server/client.cc index 0197f90..6d3b5b0 100644 --- a/src/server/client.cc +++ b/src/server/client.cc @@ -1 +1,15 @@ #include "server/client.hh" + +namespace server { + +bool client::is_in_pvs(const client& other) const noexcept { + if (!other.has_initialised()) { + return false; + } + if (!this->chunks.contains(other.get_player().get_chunk_pos())) { + return false; + } + return true; +} + +} // namespace server diff --git a/src/server/client.hh b/src/server/client.hh index 5866035..4a2c1fd 100644 --- a/src/server/client.hh +++ b/src/server/client.hh @@ -9,10 +9,10 @@ #include <unordered_set> #include "server/chunk_data.hh" -#include "server/world.hh" +#include "server/world/chunk.hh" +#include "shared/entity/player.hh" #include "shared/net/connection.hh" -#include "shared/player.hh" -#include "shared/world.hh" +#include "shared/world/chunk.hh" namespace server { @@ -44,14 +44,21 @@ public: decltype(&shared::world::chunk::equal)> chunks{4096, shared::world::chunk::hash, shared::world::chunk::equal}; + // sequence of the client's last commands, used for prediction + shared::tick_t sequence = 0u; + public: client(shared::net::connection&& con, const shared::player::index_t& index) : connection(std::move(con)), index(index) {} + bool is_in_pvs(const client& other) const noexcept; + + // Not safe to use getter functions without checking has_initalised. bool has_initialised() const noexcept { return this->player_info.has_value(); } - shared::player& get_player() noexcept { + + shared::player& get_player() const noexcept { return (*this->player_info)->player; } const std::string& get_username() const noexcept { diff --git a/src/server/database.cc b/src/server/database.cc index 077c86c..37f5757 100644 --- a/src/server/database.cc +++ b/src/server/database.cc @@ -3,14 +3,25 @@ namespace server { namespace database { +// because of all the things not to work, bit cast doesn't +template <typename T, typename U> +static T bit_cast(const U& src) noexcept { + static_assert(sizeof(T) == sizeof(U)); + + T dest; + std::memcpy(&dest, &src, sizeof(U)); + return dest; +} + sqlite3* get_database() noexcept { static sqlite3* const database = []() -> sqlite3* { std::filesystem::create_directory(server::state.directory); const std::string path = server::state.directory + "world.dat"; if (!std::filesystem::exists(path)) { - shared::print::warn( - "server: regenerating non-existent world data\n"); + shared::print::warn + << shared::print::time + << "server: regenerating non-existent world data\n"; } sqlite3* database = nullptr; @@ -59,8 +70,8 @@ static void finalise(sqlite3_stmt*& statement) noexcept { static void bind_int64(sqlite3_stmt*& statement, const int index, const std::uint64_t& key) noexcept { - if (const int status = sqlite3_bind_int64(statement, index, - std::bit_cast<std::int64_t>(key)); + if (const int status = + sqlite3_bind_int64(statement, index, bit_cast<std::int64_t>(key)); status != SQLITE_OK) { throw std::runtime_error(std::string{"sqlite bind int error \""} + sqlite3_errstr(status) + '\"'); @@ -121,8 +132,7 @@ void quit() noexcept { // We have to store our key in network byte order! static std::uint64_t make_key(const shared::math::coords& coords) noexcept { const auto to_64 = [](const std::int32_t& val, const int lshift) { - return static_cast<std::uint64_t>( - htonl(std::bit_cast<std::uint32_t>(val))) + return static_cast<std::uint64_t>(htonl(bit_cast<std::uint32_t>(val))) << lshift; }; @@ -145,14 +155,21 @@ maybe_read_chunk(const shared::math::coords& pos) noexcept { std::string blocks{addr, addr + sqlite3_column_bytes(statement, 0)}; finalise(statement); - shared::decompress_string(blocks); - proto::chunk chunk; - if (!chunk.ParseFromString(blocks)) { + std::optional<proto::chunk> chunk = [&]() -> std::optional<proto::chunk> { + const auto decompress = shared::maybe_decompress_string(blocks); + if (!decompress.has_value()) { + return std::nullopt; + } - shared::print::fault("server: chunk [" + std::to_string(pos.x) + ", " + - std::to_string(pos.z) + - "] failed to parse and was evicted\n"); + proto::chunk ret; + ret.ParseFromString(*decompress); + return ret; + }(); + if (!chunk.has_value()) { + shared::print::fault << shared::print::time << "server: chunk [" + << pos.x << ", " << pos.z + << "] failed to parse and was evicted\n"; statement = nullptr; prepare(statement, "DELETE FROM Chunks WHERE Chunks.coords = ?;"); bind_int64(statement, 1, make_key(pos)); @@ -160,14 +177,14 @@ maybe_read_chunk(const shared::math::coords& pos) noexcept { finalise(statement); return std::nullopt; } - return chunk; + return *chunk; } void write_chunk(const shared::math::coords& pos, const proto::chunk& chunk) noexcept { std::string blocks; chunk.SerializeToString(&blocks); - shared::compress_string(blocks); + blocks = shared::compress_string(blocks); sqlite3_stmt* statement = nullptr; @@ -195,17 +212,30 @@ maybe_read_player(const std::string& username) noexcept { const char* plr_adr = static_cast<const char*>(sqlite3_column_blob(statement, 0)); - std::string player_bytes{plr_adr, plr_adr + sqlite3_column_bytes(statement, 0)}; + std::string player_bytes{plr_adr, + plr_adr + sqlite3_column_bytes(statement, 0)}; const unsigned char* pass_adr = sqlite3_column_text(statement, 1); - std::string pass_bytes{pass_adr, pass_adr + sqlite3_column_bytes(statement, 1)}; + std::string pass_bytes{pass_adr, + pass_adr + sqlite3_column_bytes(statement, 1)}; finalise(statement); - shared::decompress_string(player_bytes); - proto::player player; - if (!player.ParseFromString(player_bytes)) { - shared::print::fault("server: player \"" + username + - "\" failed to parse and was evicted\n"); + const std::optional<proto::player> player = + [&]() -> std::optional<proto::player> { + const auto decompress = shared::maybe_decompress_string(player_bytes); + if (!decompress.has_value()) { + return std::nullopt; + } + + proto::player ret; + ret.ParseFromString(*decompress); + return ret; + }(); + + if (!player.has_value()) { + shared::print::fault << shared::print::time << "server: player \"" + << username + << "\" failed to parse and was evicted\n"; statement = nullptr; prepare(statement, "DELETE FROM Players WHERE Players.username = ?;"); @@ -214,14 +244,15 @@ maybe_read_player(const std::string& username) noexcept { finalise(statement); return std::nullopt; } - return std::make_pair(player, pass_bytes); + + return std::make_pair(*player, pass_bytes); } void write_player(const std::string& username, const std::string& password, const proto::player& player) noexcept { std::string player_bytes; player.SerializeToString(&player_bytes); - shared::compress_string(player_bytes); + player_bytes = shared::compress_string(player_bytes); sqlite3_stmt* statement = nullptr; prepare(statement, "INSERT OR REPLACE INTO Players VALUES(?, ?, ?);"); diff --git a/src/server/database.hh b/src/server/database.hh index 154cdf2..13798a1 100644 --- a/src/server/database.hh +++ b/src/server/database.hh @@ -13,7 +13,7 @@ #include "server/shared.hh" #include "shared/net/net.hh" #include "shared/shared.hh" -#include "shared/world.hh" +#include "shared/world/chunk.hh" namespace server { namespace database { diff --git a/src/server/init.cc b/src/server/init.cc new file mode 100644 index 0000000..31dca46 --- /dev/null +++ b/src/server/init.cc @@ -0,0 +1,45 @@ +#include "server/init.hh" + +namespace server { + +const shared::args_t& get_options() { + static shared::args_t ret{ + {.name = "seed", .desc = "manually set the worldseed", .val = "S:"}, + {.name = "directory", + .desc = "manually set the directory", + .val = "d:"}, + {.name = "tickrate", + .desc = "override the default tickrate", + .val = "t:"}, + {.name = "distance", + .desc = "limit the max chunk distance the server provides", + .val = "D:"}, + }; + return ret; +} + +bool parse_arg(const int& c, const char* const arg) { + switch (c) { + case 'S': + server::state.seed = + boost::lexical_cast<decltype(server::state.seed)>(arg); + break; + case 'd': + server::state.directory = + boost::lexical_cast<decltype(server::state.directory)>(arg); + break; + case 't': + server::state.tickrate = + boost::lexical_cast<decltype(server::state.tickrate)>(arg); + break; + case 'D': + server::state.draw_distance = + boost::lexical_cast<decltype(server::state.draw_distance)>(arg); + break; + default: + return false; + } + return true; +} + +} // namespace server diff --git a/src/server/init.hh b/src/server/init.hh new file mode 100644 index 0000000..2dcd2b0 --- /dev/null +++ b/src/server/init.hh @@ -0,0 +1,15 @@ +#ifndef SERVER_INIT_HH_ +#define SERVER_INIT_HH_ + +#include "server/shared.hh" +#include "shared/init.hh" + +namespace server { + +const shared::args_t& get_options(); + +bool parse_arg(const int& c, const char* const arg); + +} // namespace server + +#endif diff --git a/src/server/movement.cc b/src/server/movement.cc deleted file mode 100644 index 42b17f1..0000000 --- a/src/server/movement.cc +++ /dev/null @@ -1,87 +0,0 @@ -#include "server/movement.hh" - -// Gets blocks from chunks, returning nullopt if it doesn't exist. -static std::optional<shared::world::block> -get_block(const shared::math::coords& pos, server::resources::chunk_map& chunks, - const glm::ivec3& block_pos) noexcept { - - const auto find_it = chunks.find(pos); - if (find_it == std::end(chunks)) { - return std::nullopt; - } - auto& chunk_data = find_it->second; - if (!chunk_data->has_initialised()) { - return std::nullopt; - } - - return chunk_data->get_chunk().get_block(block_pos); -} - -static std::optional<std::vector<shared::movement::blockinfo>> -make_blockinfos(server::client& client, - server::resources::chunk_map& chunks) noexcept { - - std::vector<shared::movement::blockinfo> blockinfos; - - constexpr int width = shared::movement::move_width; - constexpr int height = shared::movement::move_height; - for (int x = -width; x <= width; ++x) { - for (int y = -height; y <= height; ++y) { - for (int z = -width; z <= width; ++z) { - - const glm::ivec3 rel_pos = - glm::ivec3{x, y, z} + - glm::ivec3{client.get_player().local_pos}; - - if (rel_pos.y < 0 || - rel_pos.y >= shared::world::chunk::HEIGHT) { - continue; - } - - const shared::math::coords norm_chunk_pos = - shared::world::chunk::get_normalised_chunk( - client.get_player().chunk_pos, rel_pos.x, rel_pos.z); - const glm::ivec3 norm_pos = - shared::world::chunk::get_normalised_coords(rel_pos); - - const auto block = get_block(norm_chunk_pos, chunks, norm_pos); - - if (!block.has_value()) { - return std::nullopt; - } - - const glm::vec3 pos = - glm::vec3{rel_pos} - client.get_player().local_pos; - - const shared::movement::aabb aabb = { - .min = glm::vec3{0.0f, 0.0f, 0.0f} + pos, - .max = glm::vec3{1.0f, 1.0f, 1.0f} + pos}; - blockinfos.push_back( - shared::movement::blockinfo{.block = *block, - .aabb = aabb, - .chunk_pos = norm_chunk_pos, - .pos = norm_pos}); - } - } - } - - return blockinfos; -} - -void server::movement::move(server::client& client, - server::resources::chunk_map& chunks) noexcept { - - if (!client.has_initialised()) { - return; - } - - const auto blockinfos = make_blockinfos(client, chunks); - - if (!blockinfos.has_value()) { - return; - } - - const float deltatime = 1.0f / static_cast<float>(server::state.tickrate); - - shared::movement::move(client.get_player(), *blockinfos, deltatime); -} diff --git a/src/server/movement/movement.cc b/src/server/movement/movement.cc new file mode 100644 index 0000000..0dd3f28 --- /dev/null +++ b/src/server/movement/movement.cc @@ -0,0 +1,98 @@ +#include "server/movement/movement.hh" + +namespace server { +namespace movement { + +// Gets blocks from chunks, returning nullopt if it doesn't exist. +static std::optional<shared::world::block> +maybe_get_block(const shared::math::coords& pos, + server::resources::chunk_map& chunks, + const glm::ivec3& block_pos) noexcept { + + const auto find_it = chunks.find(pos); + if (find_it == std::end(chunks)) { + return std::nullopt; + } + auto& chunk_data = find_it->second; + if (!chunk_data->has_initialised()) { + return std::nullopt; + } + + return chunk_data->get_chunk().get_block(block_pos); +} + +static std::optional<shared::movement::blocks> +maybe_make_blocks(server::client& client, + server::resources::chunk_map& chunks) noexcept { + + shared::movement::blocks blocks; + + const auto xy = shared::movement::get_move_xy(server::state.tickrate, + client.get_player()); + for (int x = -xy.x; x <= xy.x; ++x) { + for (int y = -xy.y; y <= xy.y; ++y) { + for (int z = -xy.x; z <= xy.x; ++z) { + + const glm::ivec3 rel_pos = + glm::ivec3{x, y, z} + + glm::ivec3{client.get_player().get_local_pos()}; + + if (rel_pos.y < 0 || + rel_pos.y >= shared::world::chunk::HEIGHT) { + continue; + } + + const shared::math::coords norm_chunk_pos = + shared::world::chunk::get_normalised_chunk( + client.get_player().get_chunk_pos(), rel_pos.x, + rel_pos.z); + const glm::ivec3 norm_pos = + shared::world::chunk::get_normalised_coords(rel_pos); + + const auto block = + maybe_get_block(norm_chunk_pos, chunks, norm_pos); + if (!block.has_value()) { + return std::nullopt; + } + + const glm::vec3 pos = + glm::vec3{rel_pos} - client.get_player().get_local_pos(); + + const shared::movement::aabb aabb = { + .min = glm::vec3{0.0f, 0.0f, 0.0f} + pos, + .max = glm::vec3{1.0f, 1.0f, 1.0f} + pos}; + blocks.push_back( + shared::movement::block{.block = *block, + .aabb = aabb, + .chunk_pos = norm_chunk_pos, + .pos = norm_pos}); + } + } + } + + return blocks; +} + +void move(client& client, resources::chunk_map& chunks) noexcept { + + if (!client.has_initialised()) { + return; + } + + const auto blocks = maybe_make_blocks(client, chunks); + if (!blocks.has_value()) { + return; + } + + auto& player = client.get_player(); + + const shared::animate result = + shared::movement::move(player, *blocks, state.tickrate); + + player.get_mutable_velocity() = result.get_velocity(); + player.get_mutable_local_pos() = result.get_local_pos(); + player.get_mutable_chunk_pos() = result.get_chunk_pos(); +} + +} // namespace movement +} // namespace server diff --git a/src/server/movement.hh b/src/server/movement/movement.hh index 2c647b3..0b09a24 100644 --- a/src/server/movement.hh +++ b/src/server/movement/movement.hh @@ -1,10 +1,10 @@ -#ifndef SERVER_MOVEMENT_HH_ -#define SERVER_MOVEMENT_HH_ +#ifndef SERVER_MOVEMENT_MOVEMENT_HH_ +#define SERVER_MOVEMENT_MOVEMENT_HH_ #include "server/client.hh" #include "server/resources.hh" -#include "server/world.hh" -#include "shared/movement.hh" +#include "server/world/chunk.hh" +#include "shared/movement/movement.hh" namespace server { diff --git a/src/server/resources.cc b/src/server/resources.cc index 7eb3d8b..9b4a1b8 100644 --- a/src/server/resources.cc +++ b/src/server/resources.cc @@ -46,6 +46,7 @@ void quit() noexcept { sleep(); continue; } + while (!get_resources_lock()->chunks.empty()) { sleep(); continue; diff --git a/src/server/resources.hh b/src/server/resources.hh index 578a248..a7d2faf 100644 --- a/src/server/resources.hh +++ b/src/server/resources.hh @@ -12,9 +12,8 @@ #include "server/chunk_data.hh" #include "server/client.hh" #include "server/database.hh" -#include "server/world.hh" -#include "shared/player.hh" -#include "shared/world.hh" +#include "shared/entity/player.hh" +#include "shared/world/chunk.hh" namespace server { namespace resources { diff --git a/src/server/server.cc b/src/server/server.cc index 72faa03..4592334 100644 --- a/src/server/server.cc +++ b/src/server/server.cc @@ -2,25 +2,39 @@ namespace server { -static proto::packet -make_init_packet(const resources::client_map_value& client) noexcept { - proto::packet packet; - - const auto init_packet = packet.mutable_init_packet(); - init_packet->set_seed(state.seed); - init_packet->set_draw_distance(state.draw_distance); - shared::net::set_player(*init_packet->mutable_localplayer(), - client->get_player()); +static std::uint32_t& get_tick() noexcept { + static std::uint32_t ret = 0; + return ret; +} +static proto::player make_player_packet(const shared::player& player) noexcept { + proto::player packet; + player.pack(&packet); return packet; } -static proto::packet make_player_packet(const shared::player& player) noexcept { +static proto::packet +make_animate_update_packet(const shared::animate& animate, const std::optional<shared::tick_t>& sequence) noexcept { proto::packet packet; + const auto animate_update = packet.mutable_animate_update_packet(); + animate.pack(animate_update->mutable_animate()); + animate_update->set_tick(get_tick()); + if (sequence.has_value()) { + animate_update->set_sequence(*sequence); + } + return packet; +} - const auto player_packet = packet.mutable_player_packet(); - shared::net::set_player(*player_packet, player); +static proto::packet +make_init_packet(const resources::client_map_value& client) noexcept { + proto::packet packet; + const auto init_packet = packet.mutable_init_packet(); + init_packet->set_seed(state.seed); + init_packet->set_draw_distance(state.draw_distance); + init_packet->set_tickrate(state.tickrate); + init_packet->set_tick(get_tick()); + (*client->player_info)->player.pack(init_packet->mutable_localplayer()); return packet; } @@ -39,7 +53,7 @@ static proto::packet make_remove_packet(const resources::client_map_value& client) noexcept { proto::packet packet; - const auto remove_packet = packet.mutable_remove_player_packet(); + const auto remove_packet = packet.mutable_remove_entity_packet(); remove_packet->set_index(client->index); return packet; @@ -62,12 +76,11 @@ static proto::packet make_chunk_packet(const world::chunk& chunk) noexcept { static void block_by_tickrate() noexcept { const auto tickrate = server::state.tickrate; - static const auto ratetime = std::chrono::milliseconds( - static_cast<int>((1.0 / static_cast<double>(tickrate)) * 1000.0)); - static auto prev = std::chrono::steady_clock::now(); - const auto now = std::chrono::steady_clock::now(); - std::this_thread::sleep_for(ratetime - (now - prev)); - prev = std::chrono::steady_clock::now(); + static const auto ratetime = std::chrono::microseconds( + static_cast<int>((1.0 / static_cast<double>(tickrate)) * 1'000'000)); + static auto wanted = std::chrono::steady_clock::now(); + std::this_thread::sleep_until(wanted); + wanted += ratetime; } // Creates, binds, listens on new nonblocking socket. @@ -89,14 +102,52 @@ static std::optional<shared::net::connection> make_connection(const int sock) { if (!accept.has_value()) { return std::nullopt; } - return shared::net::connection(accept->socket); + + try { + return shared::net::connection(accept->socket); + } catch (const std::runtime_error& e) { +#ifndef NDEBUG + shared::print::debug + << shared::print::time + << "server: constructor for client connection failed; what(): " + << e.what() << '\n'; +#endif + } + + return std::nullopt; +} + +static void move_client(resources::client_map_value& client, + resources::chunk_map& chunks) noexcept { + movement::move(*client, chunks); } static void handle_move_packet(const proto::move& packet, - resources::client_map_value& client) noexcept { - client->get_player().viewangles = {.pitch = packet.viewangles().pitch(), - .yaw = packet.viewangles().yaw()}; - client->get_player().commands = packet.commands(); + resources::client_map_value& client, + resources::chunk_map& chunks) noexcept { + if (!client->has_initialised()) { + return; + } + + const shared::tick_t sequence = packet.sequence(); + if (sequence <= client->sequence) { // Packet is late, drop it. +#ifndef NDEBUG + shared::print::debug << shared::print::time + << "server: client sent late tick " << sequence + << " <= " << client->sequence << '\n'; +#endif + return; + } + client->sequence = sequence; + + auto& player = client->get_player(); + player.get_mutable_angles() = {.pitch = packet.viewangles().pitch(), + .yaw = packet.viewangles().yaw()}; + player.get_mutable_angles().clamp(); + player.get_mutable_commands() = packet.commands(); + player.get_mutable_active_item() = packet.active_item(); + + move_client(client, chunks); } static void handle_say_packet(const proto::say& packet, @@ -104,18 +155,24 @@ static void handle_say_packet(const proto::say& packet, server::resources::client_map& clients) noexcept { if (std::size(packet.text()) > shared::MAX_SAY_LENGTH) { #ifndef NDEBUG - shared::print::warn( - "server: client tried to say a message that was too long, size: " + - std::to_string(std::size(packet.text())) + '\n'); + shared::print::debug + << shared::print::time + << "server: client tried to say a message that was too long, size: " + << std::size(packet.text()) << '\n'; #endif return; } - shared::print::message("server: player " + std::to_string(client->index) + - " said \"" + packet.text() + "\"\n"); - const auto hear_packet = make_hear_packet(packet.text(), client->index); + shared::print::message << shared::print::time << "server: player " + << client->index << " said \"" << packet.text() + << "\"\n"; + const auto hear_packet = std::make_shared<shared::net::rpacket>( + make_hear_packet(packet.text(), client->index)); for (auto& [index, client_ptr] : clients) { + if (!client_ptr->is_in_pvs(*client)) { + continue; + } client_ptr->connection.rsend_packet(hear_packet); } } @@ -145,9 +202,9 @@ static void send_chunk_associated(resources::chunk_map_value& chunk_data, // correctly. if (find_it == std::end(clients)) { #ifndef NDEBUG - shared::print::debug( - "client index " + std::to_string(client_index) + - " was associated with a chunk, but not found\n"); + shared::print::debug + << shared::print::time << "client index " << client_index + << " was associated with a chunk, but not found\n"; #endif continue; } @@ -209,33 +266,45 @@ static void handle_request_chunk_packet(const proto::request_chunk& packet, resources::client_map_value& client, resources::chunk_map& chunks, resources::pool_t& pool) noexcept { + if (!client->has_initialised()) { + return; + } const shared::math::coords coords{packet.chunk_pos().x(), packet.chunk_pos().z()}; if (const auto find_it = chunks.find(coords); find_it != std::end(chunks)) { auto& chunk_data = find_it->second; + + // Associate client, then post sending of chunk to client. resources::associate_client_chunk(coords, client, chunk_data); boost::asio::post( - pool, - std::bind( - [](const shared::math::coords coords, - const shared::player::index_t index) { - auto res_lock = resources::get_resources_lock(); - auto& chunk_data = res_lock->chunks.find(coords)->second; - - if (!chunk_data->has_initialised()) { - return; // will be sent on construction - } - - const auto& client_it = res_lock->clients.find(index); - if (client_it == std::end(res_lock->clients)) { - return; - } - auto& client = client_it->second; - send_chunk(chunk_data->get_chunk(), client); - maybe_post_chunk_rm(coords, chunk_data, res_lock->pool); - }, - coords, client->index)); + pool, std::bind( + [](const shared::math::coords coords, + const shared::player::index_t index) { + auto res_lock = resources::get_resources_lock(); + + // Client has requested a chunk removed before + // receiving it? + const auto find_it = res_lock->chunks.find(coords); + if (find_it == std::end(res_lock->chunks)) { + return; + } + auto& chunk_data = find_it->second; + + if (!chunk_data->has_initialised()) { + return; // will be sent on construction + } + + const auto& client_it = res_lock->clients.find(index); + if (client_it == std::end(res_lock->clients)) { + return; + } + auto& client = client_it->second; + send_chunk(chunk_data->get_chunk(), client); + maybe_post_chunk_rm(coords, chunk_data, + res_lock->pool); + }, + coords, client->index)); return; } @@ -247,19 +316,26 @@ static void handle_request_chunk_packet(const proto::request_chunk& packet, resources::associate_client_chunk(coords, client, data); boost::asio::post( - pool, - std::bind( - [](const shared::math::coords coords) { - server::world::chunk chunk{server::state.seed, coords}; + pool, std::bind( + [](const shared::math::coords coords) { + auto chunk = + [&coords]() -> std::shared_ptr<server::world::chunk> { + auto maybe_chunk = database::maybe_read_chunk(coords); + if (maybe_chunk.has_value()) { + return std::make_shared<server::world::chunk>( + server::state.seed, *maybe_chunk); + } + return std::make_shared<server::world::chunk>( + server::state.seed, coords); + }(); - auto res_lock = resources::get_resources_lock(); - auto& chunk_data = res_lock->chunks.find(coords)->second; - chunk_data->chunk.emplace( - std::make_unique<server::world::chunk>(std::move(chunk))); - send_chunk_associated(chunk_data, res_lock->clients); - maybe_post_chunk_rm(coords, chunk_data, res_lock->pool); - }, - coords)); + auto res_lock = resources::get_resources_lock(); + auto& chunk_data = res_lock->chunks.find(coords)->second; + chunk_data->chunk.emplace(std::move(chunk)); + send_chunk_associated(chunk_data, res_lock->clients); + maybe_post_chunk_rm(coords, chunk_data, res_lock->pool); + }, + coords)); } // Disassociates a client with a chunk, while performing necessary cleanups. @@ -310,17 +386,21 @@ static void post_chunk_update(const shared::math::coords& coords, coords)); } -static void modify_block(const enum shared::world::block::type block_type, - const glm::ivec3& block_pos, - const shared::math::coords& coords, - resources::chunk_map& chunks) noexcept { +static void +handle_add_block_packet(const proto::add_block& packet, + [[maybe_unused]] resources::client_map_value& client, + server::resources::chunk_map& chunks, + resources::pool_t& pool) noexcept { + + const auto coords = shared::net::get_coords(packet.chunk_pos()); + const auto pos = shared::net::get_ivec3(packet.block_pos()); + const auto active_item = packet.active_item(); const auto find_it = chunks.find(coords); if (find_it == std::end(chunks)) { return; } - - if (shared::world::chunk::is_outside_chunk(block_pos)) { + if (shared::world::chunk::is_outside_chunk(pos)) { return; } @@ -329,22 +409,24 @@ static void modify_block(const enum shared::world::block::type block_type, return; } - chunk_data->get_chunk().get_block(block_pos) = block_type; - chunk_data->get_chunk().arm_should_update(); -} + auto& inventory = client->get_player().inventory; + if (active_item < 0 || active_item >= std::size(inventory.contents)) { + return; + } -static void -handle_add_block_packet(const proto::add_block& packet, - [[maybe_unused]] resources::client_map_value& client, - server::resources::chunk_map& chunks, - resources::pool_t& pool) noexcept { - const shared::math::coords coords{packet.chunk_pos().x(), - packet.chunk_pos().z()}; - const glm::ivec3 block_pos{packet.block_pos().x(), packet.block_pos().y(), - packet.block_pos().z()}; - const auto block = - static_cast<enum shared::world::block::type>(packet.block()); - modify_block(block, block_pos, coords, chunks); + const auto& item = inventory.contents[active_item]; + if (item == nullptr) { + return; + } + const auto block_ptr = dynamic_cast<shared::item::block*>(&*item); + if (block_ptr == nullptr) { + return; + } + + chunk_data->get_chunk().get_block(pos) = block_ptr->type; + inventory.decrement(active_item); + + chunk_data->get_chunk().arm_should_update(); post_chunk_update(coords, pool); } @@ -353,11 +435,30 @@ handle_remove_block_packet(const proto::remove_block& packet, [[maybe_unused]] resources::client_map_value& client, resources::chunk_map& chunks, resources::pool_t& pool) noexcept { - const shared::math::coords coords{packet.chunk_pos().x(), - packet.chunk_pos().z()}; - const glm::ivec3 block_pos{packet.block_pos().x(), packet.block_pos().y(), - packet.block_pos().z()}; - modify_block(shared::world::block::type::air, block_pos, coords, chunks); + const auto coords = shared::net::get_coords(packet.chunk_pos()); + const auto pos = shared::net::get_ivec3(packet.block_pos()); + + const auto find_it = chunks.find(coords); + if (find_it == std::end(chunks)) { + return; + } + if (shared::world::chunk::is_outside_chunk(pos)) { + return; + } + + auto& chunk_data = find_it->second; + if (!chunk_data->has_initialised()) { + return; + } + + auto& block = chunk_data->get_chunk().get_block(pos); + + auto& inventory = client->get_player().inventory; + inventory.maybe_add(shared::item::block::get_type(block.type), 1); + + block = shared::world::block::type::air; + + chunk_data->get_chunk().arm_should_update(); post_chunk_update(coords, pool); } @@ -379,47 +480,58 @@ static void handle_auth_packet(const proto::auth& packet, auto& client = find_it->second; // find if we're already associated, eventually kick our client - // it would be nice if this wasn't a find_if - if (std::find_if(std::begin(res_lock->clients), - std::end(res_lock->clients), - [&](const auto& it) { - auto& client = it.second; - if (!client->has_initialised()) { - return false; - } - return client->index != client_index && - client->get_username() == user; - }) != std::end(res_lock->clients)) { - + if (std::ranges::any_of(res_lock->clients, + [&client_index, &user](const auto& it) { + const auto& [idx, c] = it; + if (idx == client_index) { + return false; + } + if (!c->has_initialised()) { + return false; + } + if (c->get_username() != user) { + return false; + } + return true; + })) { client->disconnect_reason.emplace("user already in server"); return; } - struct client::player_info player_info { - .username = user, .password = pass - }; - if (db_plr.has_value()) { - auto& [player, db_password] = *db_plr; + const auto in_db = db_plr.has_value(); + if (in_db && db_plr->second != pass) { + client->disconnect_reason.emplace("bad password"); + return; + } - if (db_password != pass) { - client->disconnect_reason.emplace("bad password"); - return; + shared::player player = [&]() -> shared::player { + if (in_db) { + // update to new client_index before returning + shared::player player{db_plr->first}; + player.get_mutable_index() = client_index; + return player; } - - player_info.player = shared::net::get_player(player); - player_info.player.index = client_index; - } else { // TODO: Find a random spawn chunk and put the player // on the highest block. Because we don't want to gen chunks // while blocking, we can't do this here. For now, we'll // just put the player at a high spot. - player_info.player = - shared::player{.index = client_index, - .local_pos = {0.0f, 140.0f, 0.0f}}; - } + return shared::player{ + shared::item::items{}, + 0u, + shared::math::angles{0.0f, 0.0f}, + glm::vec3{0.0f, 0.0f, 0.0f}, + 0u, + client_index, + shared::math::coords{0, 0}, + glm::vec3{0.0f, 120.0f, 0.0f}, + }; + }(); + + using pi = struct client::player_info; client->player_info.emplace( - std::make_shared<struct client::player_info>( - std::move(player_info))); + std::make_shared<pi>(pi{.username = user, + .password = pass, + .player = std::move(player)})); client->connection.rsend_packet(make_init_packet(client)); }, @@ -427,6 +539,30 @@ static void handle_auth_packet(const proto::auth& packet, std::move(packet.password()))); } +static void +handle_item_swap_packet(const proto::item_swap& proto, + const resources::client_map_value& client) noexcept { + auto& inventory = client->get_player().inventory; + + const auto a = proto.index_a(); + const auto b = proto.index_b(); + + if (a == b) { + return; + } + const auto in_range = [&](const auto val) { + const auto MAX_SIZE = + static_cast<std::uint32_t>(std::size(inventory.contents)); + return std::clamp(val, 0u, MAX_SIZE) == val; + }; + + if (!in_range(a) || !in_range(b)) { + return; + } + + std::swap(inventory.contents[a], inventory.contents[b]); +} + // Get new packets from clients, this will change client data and worldata. static void parse_client_packets(resources::client_map_value& client, resources::resources& res) noexcept { @@ -434,7 +570,7 @@ static void parse_client_packets(resources::client_map_value& client, if (packet->has_auth_packet()) { handle_auth_packet(packet->auth_packet(), client, res.pool); } else if (packet->has_move_packet()) { - handle_move_packet(packet->move_packet(), client); + handle_move_packet(packet->move_packet(), client, res.chunks); } else if (packet->has_say_packet()) { handle_say_packet(packet->say_packet(), client, res.clients); } else if (packet->has_request_chunk_packet()) { @@ -449,10 +585,13 @@ static void parse_client_packets(resources::client_map_value& client, } else if (packet->has_remove_block_packet()) { handle_remove_block_packet(packet->remove_block_packet(), client, res.chunks, res.pool); + } else if (packet->has_item_swap_packet()) { + handle_item_swap_packet(packet->item_swap_packet(), client); } #ifndef NDEBUG else { - shared::print::warn("server: unhandled packet type\n"); + shared::print::debug << shared::print::time + << "server: unhandled packet type\n"; } #endif } @@ -462,7 +601,8 @@ static void parse_client_packets(resources::client_map_value& client, static void send_remove_packets(server::resources::client_map_value& remove_client, server::resources::client_map& clients) noexcept { - const auto remove_packet = make_remove_packet(remove_client); + const auto remove_packet = std::make_shared<shared::net::rpacket>( + make_remove_packet(remove_client)); for (auto& [index, client_ptr] : clients) { client_ptr->connection.rsend_packet(remove_packet); } @@ -470,8 +610,9 @@ send_remove_packets(server::resources::client_map_value& remove_client, static void handle_new_connections(shared::net::connection& connection, resources::resources& res) noexcept { - shared::print::message("server: got connection from " + - connection.get_address() + '\n'); + shared::print::message << shared::print::time + << "server: got connection from " + << connection.get_address() << '\n'; // Add the client, waiting for an auth packet. static uint32_t index = 0; @@ -481,25 +622,25 @@ static void handle_new_connections(shared::net::connection& connection, ++index; } -static void move_client(resources::client_map_value& client, - resources::chunk_map& chunks) noexcept { - // shared::movement::fly(client.player, client.commands); - movement::move(*client, chunks); -} - static void send_client_packets(resources::client_map_value& client, resources::resources& res) noexcept { - // TODO PVS, as simple as checking if a client has our chunk pos in their - // associated chunks list + + if (!client->has_initialised()) { + return; + } + const auto non_local_packet = std::make_shared<shared::net::upacket>( + make_animate_update_packet(client->get_player(), std::nullopt)); for (auto& [index, c] : res.clients) { - if (!c->has_initialised()) { - continue; - } - if (!client->chunks.contains(c->get_player().chunk_pos)) { + if (!c->is_in_pvs(*client)) { continue; } - client->connection.usend_packet(make_player_packet(c->get_player())); + // only network the last received sequence if sending an animate packet for the client's local player + if (c->index == client->index) { + c->connection.usend_packet( make_animate_update_packet(client->get_player(), client->sequence) ); + } else { + c->connection.usend_packet(non_local_packet); + } } } @@ -510,15 +651,17 @@ get_sent_disconnect_reason(const resources::client_map_value& client) noexcept { return client->connection.get_bad_reason(); } - if (client->chunks.size() > - unsigned(state.draw_distance * state.draw_distance * 4)) { + if (static const unsigned max = static_cast<unsigned>( + state.draw_distance * state.draw_distance * 4); + client->chunks.size() > max) { return "too many chunks associated with client"; } return client->disconnect_reason; } -static void remove_bad_clients(resources::resources& res) noexcept { +static void remove_bad_clients(resources::resources& res, + const bool send_remove = true) noexcept { for (auto& [index, client] : res.clients) { const auto reason = get_sent_disconnect_reason(client); if (reason == std::nullopt) { @@ -530,11 +673,18 @@ static void remove_bad_clients(resources::resources& res) noexcept { } client->disconnecting = true; - shared::print::message("server: dropped " + - client->connection.get_address() + " for \"" + - *reason + "\"\n"); - send_message("disconnected for " + *reason, client, true); - send_remove_packets(client, res.clients); + shared::print::message << shared::print::time << "server: dropped " + << client->connection.get_address() << " for \"" + << *reason << "\"\n"; + send_message(*reason, client, true); + if (send_remove) { + send_remove_packets(client, res.clients); + } + + // Close early so the client may reconnect and receive a better message + // than a generic errno error if they connect too fast later. + client->connection.poll(); + client->connection.close(); boost::asio::post( res.pool, @@ -546,8 +696,7 @@ static void remove_bad_clients(resources::resources& res) noexcept { if (plr_info.has_value()) { database::write_player( (*plr_info)->username, (*plr_info)->password, - make_player_packet((*plr_info)->player) - .player_packet()); + make_player_packet((*plr_info)->player)); } // cleanup associated chunks @@ -583,9 +732,10 @@ static void process_resources(resources::resources& res) noexcept { } // Move clients via their (hopefully updated) command. - for (auto& [index, client] : res.clients) { + // CHANGED: we now do this when we get move packets + /*for (auto& [index, client] : res.clients) { move_client(client, res.chunks); - } + }*/ // Send packets which are sent once per tick, as of now the player // struct of other clients. @@ -593,9 +743,15 @@ static void process_resources(resources::resources& res) noexcept { send_client_packets(client, res); } + // Send queued packets. + for (auto& [index, client] : res.clients) { + client->connection.poll(); + } + // Delete bad connections, print if it happens. Also sends a remove // to all clients per client removed. remove_bad_clients(res); + ++get_tick(); } void main(const std::string_view address, const std::string_view port) { @@ -603,8 +759,8 @@ void main(const std::string_view address, const std::string_view port) { server::resources::init(); has_initialised = true; - shared::print::notify("server: started at " + std::string{address} + ':' + - std::string{port} + '\n'); + shared::print::notify << shared::print::time << "server: started at " + << address << ':' << port << '\n'; // Server has a tickrate, we will use non-blocking polling at this // tickrate. @@ -629,12 +785,14 @@ void main(const std::string_view address, const std::string_view port) { const auto& client = it.second; client->disconnect_reason.emplace("server shutting down"); }); - remove_bad_clients(*res_lock); + remove_bad_clients(*res_lock, false); } - shared::print::notify("server: writing world data\n"); + shared::print::notify << shared::print::time + << "server: writing world data\n"; server::resources::quit(); - shared::print::notify("server: gracefully exited\n"); + shared::print::notify << shared::print::time + << "server: gracefully exited\n"; } } // namespace server diff --git a/src/server/server.hh b/src/server/server.hh index bd2d5fd..d40827e 100644 --- a/src/server/server.hh +++ b/src/server/server.hh @@ -20,12 +20,12 @@ #include "server/client.hh" #include "server/database.hh" -#include "server/movement.hh" +#include "server/movement/movement.hh" #include "server/resources.hh" -#include "server/world.hh" +#include "server/world/chunk.hh" +#include "shared/entity/player.hh" #include "shared/net/net.hh" #include "shared/net/proto.hh" -#include "shared/player.hh" #include "shared/shared.hh" namespace server { diff --git a/src/server/shared.hh b/src/server/shared.hh index ffbf0f9..608e25e 100644 --- a/src/server/shared.hh +++ b/src/server/shared.hh @@ -10,7 +10,7 @@ struct state { int draw_distance = 32; std::uint64_t seed = 123456789; std::string directory = "world/"; - std::uint32_t tickrate = 144; + std::uint32_t tickrate = 20; }; inline state state; diff --git a/src/server/world.cc b/src/server/world.cc deleted file mode 100644 index 6527d6c..0000000 --- a/src/server/world.cc +++ /dev/null @@ -1,53 +0,0 @@ -#include "server/world.hh" - -namespace server { -namespace world { - -proto::packet chunk::make_chunk_packet() const noexcept { - - proto::packet ret_packet; - - auto chunk_packet = ret_packet.mutable_chunk_packet(); - - auto chunk_pos = chunk_packet->mutable_chunk_pos(); - const shared::math::coords this_chunk_pos = this->get_pos(); - chunk_pos->set_x(this_chunk_pos.x); - chunk_pos->set_z(this_chunk_pos.z); - - // Since protobuf can store at minimum uint32, we mash four of our - // uint_8 chunk blocks into a single uint32. - static_assert(shared::world::chunk::VOLUME % 4 == 0); - for (unsigned i = 0u; i < shared::world::chunk::VOLUME / 4u; ++i) { - std::uint32_t packed_blocks = 0u; - - for (unsigned j = 0; j < 4; ++j) { - const auto block = - static_cast<std::uint8_t>(this->blocks[i * 4 + j].type); - packed_blocks |= static_cast<unsigned>(block << j * 8); - } - - chunk_packet->add_blocks(packed_blocks); - } - - return ret_packet; -} - -static std::optional<shared::world::chunk::block_array> -maybe_get_blocks(const shared::math::coords& coords) { - const auto chunk = database::maybe_read_chunk(coords); - if (!chunk.has_value()) { - return std::nullopt; - } - return shared::world::chunk::make_blocks_from_chunk(chunk.value()); -} - -chunk::chunk(const std::uint64_t& seed, - const shared::math::coords& coords) noexcept - : shared::world::chunk(seed, coords, maybe_get_blocks(coords)) { - this->update(); -} - -chunk::~chunk() noexcept { this->write(); } - -} // namespace world -} // namespace server diff --git a/src/server/world.hh b/src/server/world.hh deleted file mode 100644 index 134f63b..0000000 --- a/src/server/world.hh +++ /dev/null @@ -1,59 +0,0 @@ -#ifndef SERVER_WORLD_HH_ -#define SERVER_WORLD_HH_ - -#include <cstdint> - -#include "server/database.hh" -#include "server/shared.hh" -#include "shared/math.hh" -#include "shared/net/net.hh" -#include "shared/net/proto.hh" -#include "shared/player.hh" -#include "shared/world.hh" - -namespace server { -namespace world { - -class chunk : public shared::world::chunk { -private: - bool should_write = false; - bool should_update = true; - -public: - proto::packet packet; // Packet ready for sending, updated in update(). - void arm_should_update() noexcept { - this->should_update = this->should_write = true; - } - bool get_should_update() noexcept { return this->should_update; } - -private: - proto::packet make_chunk_packet() const noexcept; - -public: - // Attempt to read the file using protobuf, otherwise create a new chunk. - // chunk(const chunk&) = delete; - chunk(const uint64_t& seed, const shared::math::coords& coords) noexcept; - ~chunk() noexcept; - - // Update the chunk_packet associated with the chunk if necessary. - void update() noexcept { - if (!this->should_update) { - return; - } - this->packet = make_chunk_packet(); - this->should_update = false; - } - // calling .write before the destrutor will not result in a double write - void write() noexcept { - if (!this->should_write) { - return; - } - server::database::write_chunk(this->pos, this->packet.chunk_packet()); - this->should_write = false; - } -}; - -} // namespace world -} // namespace server - -#endif diff --git a/src/server/world/chunk.cc b/src/server/world/chunk.cc new file mode 100644 index 0000000..80a3da9 --- /dev/null +++ b/src/server/world/chunk.cc @@ -0,0 +1,26 @@ +#include "server/world/chunk.hh" + +namespace server { +namespace world { + +void chunk::update() noexcept { + if (!this->should_update) { + return; + } + this->packet.clear_chunk_packet(); + this->pack(packet.mutable_chunk_packet()); + this->should_update = false; +} + +void chunk::write() noexcept { + if (!this->should_write) { + return; + } + server::database::write_chunk(this->pos, this->packet.chunk_packet()); + this->should_write = false; +} + +chunk::~chunk() noexcept { this->write(); } + +} // namespace world +} // namespace server diff --git a/src/server/world/chunk.hh b/src/server/world/chunk.hh new file mode 100644 index 0000000..b8fc73c --- /dev/null +++ b/src/server/world/chunk.hh @@ -0,0 +1,46 @@ +#ifndef SERVER_WORLD_CHUNK_HH_ +#define SERVER_WORLD_CHUNK_HH_ + +#include <cstdint> + +#include "server/database.hh" +#include "server/shared.hh" +#include "shared/entity/player.hh" +#include "shared/math/math.hh" +#include "shared/net/net.hh" +#include "shared/net/proto.hh" +#include "shared/world/chunk.hh" + +namespace server { +namespace world { + +class chunk : public shared::world::chunk { +private: + bool should_write = false; + bool should_update = true; + +public: + proto::packet packet; // Packet ready for sending, updated in update(). + void arm_should_update() noexcept { + this->should_update = this->should_write = true; + } + +public: + template <typename... Args> + chunk(Args&&... args) noexcept + : shared::world::chunk(std::forward<Args>(args)...) { + this->pack(packet.mutable_chunk_packet()); + } + virtual ~chunk() noexcept; + +public: + // Update the chunk_packet associated with the chunk if necessary. + void update() noexcept; + // calling .write before the destrutor will not result in a double write + void write() noexcept; +}; + +} // namespace world +} // namespace server + +#endif |
