aboutsummaryrefslogtreecommitdiff
path: root/src/server/server.cc
diff options
context:
space:
mode:
authorNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-12 18:05:18 +1100
committerNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-12 18:05:18 +1100
commit1cc08c51eb4b0f95c30c0a98ad1fc5ad3459b2df (patch)
tree222dfcd07a1e40716127a347bbfd7119ce3d0984 /src/server/server.cc
initial commit
Diffstat (limited to 'src/server/server.cc')
-rw-r--r--src/server/server.cc640
1 files changed, 640 insertions, 0 deletions
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