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