diff options
Diffstat (limited to 'src/server/server.cc')
| -rw-r--r-- | src/server/server.cc | 640 |
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 |
