diff options
Diffstat (limited to 'game/server/sessions.cc')
| -rw-r--r-- | game/server/sessions.cc | 402 |
1 files changed, 402 insertions, 0 deletions
diff --git a/game/server/sessions.cc b/game/server/sessions.cc new file mode 100644 index 0000000..f2e643d --- /dev/null +++ b/game/server/sessions.cc @@ -0,0 +1,402 @@ +#include "server/pch.hh" +#include "server/sessions.hh" + +#include "core/config.hh" +#include "core/constexpr.hh" +#include "core/crc64.hh" +#include "core/strtools.hh" + +#include "shared/chunk.hh" +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/factory.hh" +#include "shared/head.hh" +#include "shared/player.hh" +#include "shared/protocol.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" +#include "shared/voxel_registry.hh" + +#include "server/game.hh" +#include "server/globals.hh" +#include "server/whitelist.hh" + +class DimensionListener final { +public: + explicit DimensionListener(Dimension *dimension); + void on_destroy_entity(const entt::registry ®istry, entt::entity entity); + +private: + Dimension *dimension; +}; + +ConfigUnsigned sessions::max_players(8U, 1U, 128U); +unsigned int sessions::num_players = 0U; + +static emhash8::HashMap<std::string, Session *> username_map; +static emhash8::HashMap<std::uint64_t, Session *> identity_map; +static std::vector<DimensionListener> dimension_listeners; +static std::vector<Session> sessions_vector; + +static void on_login_request_packet(const protocol::LoginRequest &packet) +{ + if(packet.version > protocol::VERSION) { + protocol::Disconnect response; + response.reason = "protocol.outdated_server"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + + if(packet.version < protocol::VERSION) { + protocol::Disconnect response; + response.reason = "protocol.outdated_client"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + + // FIXME: calculate voxel registry checksum ahead of time + // instead of figuring it out every time a new player connects + if(packet.voxel_def_checksum != voxel_registry::checksum()) { + protocol::Disconnect response; + response.reason = "protocol.voxel_registry_checksum"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + + // Don't assign new usernames and just kick the player if + // an another client using the same username is already connected + // and playing; since we have a whitelist, adding "(1)" isn't feasible anymore + if(username_map.contains(packet.username)) { + protocol::Disconnect response; + response.reason = "protocol.username_taken"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + + if(whitelist::enabled.get_value()) { + if(!whitelist::contains(packet.username.c_str())) { + protocol::Disconnect response; + response.reason = "protocol.not_whitelisted"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + + if(!whitelist::matches(packet.username.c_str(), packet.password_hash)) { + protocol::Disconnect response; + response.reason = "protocol.password_incorrect"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + } + else if(packet.password_hash != server_game::password_hash) { + protocol::Disconnect response; + response.reason = "protocol.password_incorrect"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + + if(Session *session = sessions::create(packet.peer, packet.username.c_str())) { + protocol::LoginResponse response; + response.client_index = session->client_index; + response.client_identity = session->client_identity; + response.server_tickrate = globals::tickrate; + protocol::send(packet.peer, protocol::encode(response)); + + protocol::DimensionInfo dim_info; + dim_info.name = globals::spawn_dimension->get_name(); + dim_info.gravity = globals::spawn_dimension->get_gravity(); + protocol::send(packet.peer, protocol::encode(dim_info)); + + spdlog::info("sessions: {} [{}] logged in with client_index={} in {}", session->client_username, + session->client_identity, session->client_index, globals::spawn_dimension->get_name()); + + // FIXME: only send entities that are present within the current + // player's view bounding box; this also would mean we're not sending + // anything here and just straight up spawing the player and await them + // to receive all the chunks and entites they feel like requesting + for(auto entity : globals::spawn_dimension->entities.view<entt::entity>()) { + if(const auto head = globals::spawn_dimension->entities.try_get<HeadComponent>(entity)) { + protocol::EntityHead head_packet; + head_packet.entity = entity; + head_packet.angles = head->angles; + protocol::send(session->peer, protocol::encode(head_packet)); + } + + if(const auto transform = globals::spawn_dimension->entities.try_get<TransformComponent>(entity)) { + protocol::EntityTransform transform_packet; + transform_packet.entity = entity; + transform_packet.angles = transform->angles; + transform_packet.chunk = transform->chunk; + transform_packet.local = transform->local; + protocol::send(session->peer, protocol::encode(transform_packet)); + } + + if(const auto velocity = globals::spawn_dimension->entities.try_get<VelocityComponent>(entity)) { + protocol::EntityVelocity velocity_packet; + velocity_packet.entity = entity; + velocity_packet.value = velocity->value; + protocol::send(session->peer, protocol::encode(velocity_packet)); + } + + if(globals::spawn_dimension->entities.all_of<PlayerComponent>(entity)) { + protocol::EntityPlayer player_packet; + player_packet.entity = entity; + protocol::send(session->peer, protocol::encode(player_packet)); + } + } + + session->dimension = globals::spawn_dimension; + session->player_entity = globals::spawn_dimension->entities.create(); + shared_factory::create_player(globals::spawn_dimension, session->player_entity); + + const auto &head = globals::spawn_dimension->entities.get<HeadComponent>(session->player_entity); + const auto &transform = globals::spawn_dimension->entities.get<TransformComponent>(session->player_entity); + const auto &velocity = globals::spawn_dimension->entities.get<VelocityComponent>(session->player_entity); + + protocol::EntityHead head_packet; + head_packet.entity = session->player_entity; + head_packet.angles = head.angles; + + protocol::EntityTransform transform_packet; + transform_packet.entity = session->player_entity; + transform_packet.angles = transform.angles; + transform_packet.chunk = transform.chunk; + transform_packet.local = transform.local; + + protocol::EntityVelocity velocity_packet; + velocity_packet.entity = session->player_entity; + velocity_packet.value = velocity.value; + + protocol::EntityPlayer player_packet; + player_packet.entity = session->player_entity; + + protocol::broadcast(globals::server_host, protocol::encode(head_packet)); + protocol::broadcast(globals::server_host, protocol::encode(transform_packet)); + protocol::broadcast(globals::server_host, protocol::encode(velocity_packet)); + protocol::broadcast(globals::server_host, protocol::encode(player_packet)); + + protocol::SpawnPlayer spawn_packet; + spawn_packet.entity = session->player_entity; + + // SpawnPlayer serves a different purpose compared to EntityPlayer + // The latter is used to construct entities (as in "attach a component") + // whilst the SpawnPlayer packet is used to notify client-side that the + // entity identifier in the packet is to be treated as the local player entity + protocol::send(session->peer, protocol::encode(spawn_packet)); + + protocol::ChatMessage message; + message.type = protocol::ChatMessage::PLAYER_JOIN; + message.sender = session->client_username; + message.message = std::string(); + + protocol::broadcast(globals::server_host, protocol::encode(message)); + + sessions::refresh_scoreboard(); + + return; + } + + protocol::Disconnect response; + response.reason = "protocol.server_full"; + protocol::send(packet.peer, protocol::encode(response)); +} + +static void on_disconnect_packet(const protocol::Disconnect &packet) +{ + if(Session *session = sessions::find(packet.peer)) { + protocol::ChatMessage message; + message.type = protocol::ChatMessage::PLAYER_LEAVE; + message.sender = session->client_username; + message.message = packet.reason; + + protocol::broadcast(globals::server_host, protocol::encode(message), session->peer); + + spdlog::info("{} disconnected ({})", session->client_username, packet.reason); + + sessions::destroy(session); + sessions::refresh_scoreboard(); + } +} + +// NOTE: [sessions] is a good place for this since [receive] +// handles entity data sent by players and [sessions] handles +// everything else network related that is not player movement +static void on_voxel_set(const VoxelSetEvent &event) +{ + protocol::SetVoxel packet; + packet.vpos = coord::to_voxel(event.cpos, event.lpos); + packet.voxel = event.voxel; + packet.flags = 0U; // UNDONE + protocol::broadcast(globals::server_host, protocol::encode(packet)); +} + +DimensionListener::DimensionListener(Dimension *dimension) +{ + this->dimension = dimension; +} + +void DimensionListener::on_destroy_entity(const entt::registry ®istry, entt::entity entity) +{ + protocol::RemoveEntity packet; + packet.entity = entity; + sessions::broadcast(dimension, protocol::encode(packet)); +} + +void sessions::init(void) +{ + globals::server_config.add_value("sessions.max_players", sessions::max_players); + + globals::dispatcher.sink<protocol::LoginRequest>().connect<&on_login_request_packet>(); + globals::dispatcher.sink<protocol::Disconnect>().connect<&on_disconnect_packet>(); + + globals::dispatcher.sink<VoxelSetEvent>().connect<&on_voxel_set>(); +} + +void sessions::init_late(void) +{ + sessions::num_players = 0U; + + username_map.clear(); + identity_map.clear(); + sessions_vector.resize(sessions::max_players.get_value(), Session()); + + for(unsigned int i = 0U; i < sessions::max_players.get_value(); ++i) { + sessions_vector[i].client_index = UINT16_MAX; + sessions_vector[i].client_identity = UINT64_MAX; + sessions_vector[i].client_username = std::string(); + sessions_vector[i].player_entity = entt::null; + sessions_vector[i].peer = nullptr; + } +} + +void sessions::init_post_universe(void) +{ + for(auto &dimension : globals::dimensions) { + dimension_listeners.push_back(DimensionListener(dimension.second)); + dimension.second->entities.on_destroy<entt::entity>().connect<&DimensionListener::on_destroy_entity>(dimension_listeners.back()); + } +} + +void sessions::deinit(void) +{ + username_map.clear(); + identity_map.clear(); + sessions_vector.clear(); + dimension_listeners.clear(); +} + +Session *sessions::create(ENetPeer *peer, const char *client_username) +{ + for(unsigned int i = 0U; i < sessions::max_players.get_value(); ++i) { + if(!sessions_vector[i].peer) { + std::uint64_t client_identity = crc64::get(client_username); + + sessions_vector[i].client_index = i; + sessions_vector[i].client_identity = client_identity; + sessions_vector[i].client_username = client_username; + sessions_vector[i].player_entity = entt::null; + sessions_vector[i].peer = peer; + + username_map[client_username] = &sessions_vector[i]; + identity_map[client_identity] = &sessions_vector[i]; + + peer->data = &sessions_vector[i]; + + sessions::num_players += 1U; + + return &sessions_vector[i]; + } + } + + return nullptr; +} + +Session *sessions::find(const char *client_username) +{ + const auto it = username_map.find(client_username); + if(it != username_map.cend()) + return it->second; + return nullptr; +} + +Session *sessions::find(std::uint16_t client_index) +{ + if(client_index < sessions_vector.size()) { + if(!sessions_vector[client_index].peer) + return nullptr; + return &sessions_vector[client_index]; + } + + return nullptr; +} + +Session *sessions::find(std::uint64_t client_identity) +{ + const auto it = identity_map.find(client_identity); + if(it != identity_map.cend()) + return it->second; + return nullptr; +} + +Session *sessions::find(ENetPeer *peer) +{ + if(peer != nullptr) + return reinterpret_cast<Session *>(peer->data); + return nullptr; +} + +void sessions::destroy(Session *session) +{ + if(session) { + if(session->peer) { + // Make sure we don't leave a mark + session->peer->data = nullptr; + } + + if(session->dimension) { + session->dimension->entities.destroy(session->player_entity); + } + + username_map.erase(session->client_username); + identity_map.erase(session->client_identity); + + session->client_index = UINT16_MAX; + session->client_identity = UINT64_MAX; + session->client_username = std::string(); + session->player_entity = entt::null; + session->peer = nullptr; + + sessions::num_players -= 1U; + } +} + +void sessions::broadcast(const Dimension *dimension, ENetPacket *packet) +{ + for(const auto &session : sessions_vector) { + if(session.peer && (session.dimension == dimension)) { + enet_peer_send(session.peer, protocol::CHANNEL, packet); + } + } +} + +void sessions::broadcast(const Dimension *dimension, ENetPacket *packet, ENetPeer *except) +{ + for(const auto &session : sessions_vector) { + if(session.peer && (session.peer != except)) { + enet_peer_send(session.peer, protocol::CHANNEL, packet); + } + } +} + +void sessions::refresh_scoreboard(void) +{ + protocol::ScoreboardUpdate packet; + + for(std::size_t i = 0; i < sessions::max_players.get_value(); ++i) { + if(!sessions_vector[i].peer) + continue; + packet.names.push_back(sessions_vector[i].client_username); + } + + protocol::broadcast(globals::server_host, protocol::encode(packet)); +} |
