#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((1.0 / static_cast(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 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 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 = 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(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(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( 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( 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 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