#include "server.hh" namespace server { 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_animate_update_packet(const shared::animate& animate, const std::optional& 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; } 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; } 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_entity_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::microseconds( static_cast((1.0 / static_cast(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. 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; } 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, 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, resources::client_map_value& client, server::resources::client_map& clients) noexcept { if (std::size(packet.text()) > shared::MAX_SAY_LENGTH) { #ifndef NDEBUG 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 << shared::print::time << "server: player " << client->index << " said \"" << packet.text() << "\"\n"; const auto hear_packet = std::make_shared( 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); } } 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 << shared::print::time << "client index " << 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 { 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(); // 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; } 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) { auto chunk = [&coords]() -> std::shared_ptr { auto maybe_chunk = database::maybe_read_chunk(coords); if (maybe_chunk.has_value()) { return std::make_shared( server::state.seed, *maybe_chunk); } return std::make_shared( 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::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 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(pos)) { return; } auto& chunk_data = find_it->second; if (!chunk_data->has_initialised()) { return; } auto& inventory = client->get_player().inventory; if (active_item < 0 || active_item >= std::size(inventory.contents)) { return; } const auto& item = inventory.contents[active_item]; if (item == nullptr) { return; } const auto block_ptr = dynamic_cast(&*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); } 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 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); } 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 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; } const auto in_db = db_plr.has_value(); if (in_db && db_plr->second != 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; } // 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. 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(pi{.username = user, .password = pass, .player = std::move(player)})); client->connection.rsend_packet(make_init_packet(client)); }, client->index, std::move(packet.username()), 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::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 { 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, res.chunks); } 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); } else if (packet->has_item_swap_packet()) { handle_item_swap_packet(packet->item_swap_packet(), client); } #ifndef NDEBUG else { shared::print::debug << shared::print::time << "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 = std::make_shared( 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 << shared::print::time << "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 send_client_packets(resources::client_map_value& client, resources::resources& res) noexcept { if (!client->has_initialised()) { return; } const auto non_local_packet = std::make_shared( make_animate_update_packet(client->get_player(), std::nullopt)); for (auto& [index, c] : res.clients) { if (!c->is_in_pvs(*client)) { continue; } // 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); } } } // 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 (static const unsigned max = static_cast( 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, const bool send_remove = true) 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 << 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, 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)); } // 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. // 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. for (auto& [index, client] : res.clients) { 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) { const auto rsock = create_listen_socket(address, port); // reliable socket server::resources::init(); has_initialised = true; 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. 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, false); } shared::print::notify << shared::print::time << "server: writing world data\n"; server::resources::quit(); shared::print::notify << shared::print::time << "server: gracefully exited\n"; } } // namespace server