diff --git a/CMakeLists.txt b/CMakeLists.txt index b223f699..d128ed2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -194,6 +194,7 @@ set(SOURCES "network/message.cpp" "network/manager.cpp" "network/entities.cpp" +"network/session.cpp" "network/backend/asio.cpp" "widgets/vehiclelist.cpp" diff --git a/application/application.cpp b/application/application.cpp index 714a02bc..0c4768e8 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -460,6 +460,18 @@ bool eu07_application::is_client() const return m_network && m_network->client; } +void eu07_application::request_vehicle_claim(network::NetworkEntityId const Entity) +{ + if (m_network) + m_network->request_claim(Entity); +} + +void eu07_application::request_vehicle_leave(network::NetworkEntityId const Entity) +{ + if (m_network) + m_network->request_leave(Entity); +} + int eu07_application::run() { auto frame{0}; diff --git a/application/application.h b/application/application.h index 11415603..99cc6565 100644 --- a/application/application.h +++ b/application/application.h @@ -93,6 +93,11 @@ public: is_server() const; bool is_client() const; + // asks the session authority to put us on the crew of given vehicle, or take us off it + void + request_vehicle_claim( network::NetworkEntityId const Entity ); + void + request_vehicle_leave( network::NetworkEntityId const Entity ); private: // types diff --git a/application/drivermode.cpp b/application/drivermode.cpp index 1ca75c37..e41eb077 100644 --- a/application/drivermode.cpp +++ b/application/drivermode.cpp @@ -266,6 +266,22 @@ bool driver_mode::update() // variable step simulation time routines + if (Global.network_leave_pending) + { + // we were taken off the crew (by our own request or by the server), so the + // camera goes back to observing. the cab instance itself stays put, it may + // still be occupied by somebody else or handed back to the AI + Global.network_leave_pending = false; + if (simulation::Train != nullptr) + { + if (!FreeFlyModeFlag) + InOutKey(); + simulation::Train = nullptr; + Camera.m_owner = nullptr; + Global.local_start_vehicle = "ghostview"; + } + } + if (change_train.empty() && !Global.network_pending_vehicle.empty()) { // vehicle picked in the multiplayer lobby; the cab itself is built by the @@ -282,8 +298,17 @@ bool driver_mode::update() Global.local_start_vehicle = change_train; simulation::Train = train; InOutKey(); - m_relay.post(user_command::aidriverdisable, 0.0, 0.0, GLFW_PRESS, 0); + if (!Application.is_server() && !Application.is_client()) + { + // in a multiplayer session the AI handover belongs to the server, which + // also knows whether the vehicle had an AI driver in the first place + m_relay.post(user_command::aidriverdisable, 0.0, 0.0, GLFW_PRESS, 0); + } change_train.clear(); + + auto ui = std::dynamic_pointer_cast(m_userinterface); + if (ui != nullptr) + ui->show_multiplayer_lobby(false); } } diff --git a/network/entities.cpp b/network/entities.cpp index 3bdd08af..d8c78b74 100644 --- a/network/entities.cpp +++ b/network/entities.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "network/entities.h" +#include "network/session.h" #include "simulation/simulation.h" #include "vehicle/DynObj.h" @@ -105,9 +106,7 @@ void entity_registry::refresh() TDynamicObject const *vehicle = resolve(entry.id); entry.ai_active = (vehicle != nullptr && vehicle->Mechanik != nullptr && vehicle->Mechanik->AIControllFlag); - // until the crew model of stage 3 is in place, an existing cab instance is the - // only evidence that somebody took the vehicle over - entry.crew_count = (simulation::Trains.find(entry.name) != nullptr ? 1 : 0); + entry.crew_count = Crews.count(entry.id); entry.crew_capacity = CREW_CAPACITY; entry.claimable = (vehicle != nullptr && entry.crew_count < entry.crew_capacity); } diff --git a/network/manager.cpp b/network/manager.cpp index 37900d14..6a64f97b 100644 --- a/network/manager.cpp +++ b/network/manager.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "network/manager.h" #include "simulation/simulation.h" #include "utilities/Logs.h" +#include "utilities/Globals.h" network::server_manager::server_manager() { @@ -82,13 +83,80 @@ void network::server_manager::publish_vehicle_list() srv->push_message(msg); } +void network::server_manager::update_crews() +{ + Crews.reconcile(); + + if (--crew_publish_countdown <= 0) { + // peers that joined after somebody took a vehicle over have to learn about it too + crew_publish_countdown = PUBLISH_INTERVAL_FRAMES; + Crews.mark_all_pending(); + } + + auto const changed = Crews.take_pending_updates(); + for (NetworkEntityId const id : changed) { + crew_update msg; + msg.entity_id = id; + msg.crew = Crews.crew_of(id); + + for (auto srv : servers) + srv->push_message(msg); + } +} + +void network::server_manager::apply_local_claim(NetworkEntityId entity_id) +{ + auto const result = Crews.claim(PEER_HOST, entity_id); + + if (result == claim_result::granted || result == claim_result::already_member) { + Global.network_lobby_message.clear(); + Global.network_pending_vehicle = Entities.name_of(entity_id); + } + else { + WriteLog("net: refused local claim of vehicle " + std::to_string(entity_id) + ": " + describe(result), logtype::net); + Global.network_lobby_message = describe(result); + } +} + +void network::server_manager::apply_local_leave(NetworkEntityId entity_id) +{ + if (Crews.leave(PEER_HOST, entity_id)) + Global.network_leave_pending = true; +} + +void network::manager::request_claim(NetworkEntityId entity_id) +{ + if (client) { + client->send_claim(entity_id); + return; + } + + if (servers) + servers->apply_local_claim(entity_id); +} + +void network::manager::request_leave(NetworkEntityId entity_id) +{ + if (client) { + client->send_leave(entity_id); + // the server owns the roster, but stepping out of our own cab is a local matter + Global.network_leave_pending = true; + return; + } + + if (servers) + servers->apply_local_leave(entity_id); +} + void network::manager::update() { for (auto &backend : backend_list()) backend.second->update(); - if (servers && simulation::is_ready) + if (servers && simulation::is_ready) { + servers->update_crews(); servers->publish_vehicle_list(); + } if (client) client->update(); diff --git a/network/manager.h b/network/manager.h index e5aa01ea..a530fa64 100644 --- a/network/manager.h +++ b/network/manager.h @@ -19,6 +19,11 @@ namespace network void create_server(const std::string &backend, const std::string &conf); // refreshes the authoritative vehicle roster and publishes it when it changed void publish_vehicle_list(); + // brings the world in line with the crew roster and announces every change + void update_crews(); + // crew request coming from the local participant, which needs no transport + void apply_local_claim(NetworkEntityId entity_id); + void apply_local_leave(NetworkEntityId entity_id); private: // how many frames may pass between two unconditional broadcasts of the roster @@ -26,6 +31,7 @@ namespace network std::vector last_published_list; int publish_countdown = 0; + int crew_publish_countdown = 0; }; class manager @@ -39,5 +45,10 @@ namespace network void create_server(const std::string &backend, const std::string &conf); void connect(const std::string &backend, const std::string &conf); void update(); + + // routes a lobby request to whoever holds the authority: straight into the crew + // registry when we are the server, over the wire when we are a client + void request_claim(NetworkEntityId entity_id); + void request_leave(NetworkEntityId entity_id); }; } diff --git a/network/message.cpp b/network/message.cpp index 801c8423..3a5b0eea 100644 --- a/network/message.cpp +++ b/network/message.cpp @@ -80,6 +80,67 @@ void network::vehicle_list::deserialize(std::istream &stream) } } +void network::claim_vehicle::serialize(std::ostream &stream) const +{ + sn_utils::ls_uint32(stream, entity_id); +} + +void network::claim_vehicle::deserialize(std::istream &stream) +{ + entity_id = sn_utils::ld_uint32(stream); +} + +void network::claim_granted::serialize(std::ostream &stream) const +{ + sn_utils::ls_uint32(stream, entity_id); +} + +void network::claim_granted::deserialize(std::istream &stream) +{ + entity_id = sn_utils::ld_uint32(stream); +} + +void network::claim_denied::serialize(std::ostream &stream) const +{ + sn_utils::ls_uint32(stream, entity_id); + sn_utils::s_str(stream, reason); +} + +void network::claim_denied::deserialize(std::istream &stream) +{ + entity_id = sn_utils::ld_uint32(stream); + reason = sn_utils::d_str(stream); +} + +void network::leave_vehicle::serialize(std::ostream &stream) const +{ + sn_utils::ls_uint32(stream, entity_id); +} + +void network::leave_vehicle::deserialize(std::istream &stream) +{ + entity_id = sn_utils::ld_uint32(stream); +} + +void network::crew_update::serialize(std::ostream &stream) const +{ + sn_utils::ls_uint32(stream, entity_id); + sn_utils::ls_uint32(stream, (uint32_t)crew.size()); + for (PeerId const peer : crew) + sn_utils::ls_uint32(stream, peer); +} + +void network::crew_update::deserialize(std::istream &stream) +{ + entity_id = sn_utils::ld_uint32(stream); + + crew.clear(); + uint32_t const count = sn_utils::ld_uint32(stream); + crew.reserve(count); + for (uint32_t i = 0; i < count; i++) + crew.emplace_back(sn_utils::ld_uint32(stream)); +} + void ::network::request_command::serialize(std::ostream &stream) const { sn_utils::ls_uint32(stream, commands.size()); @@ -169,6 +230,16 @@ std::shared_ptr network::deserialize_message(std::istream &str msg = std::make_shared(); else if (type == message::VEHICLE_LIST) msg = std::make_shared(); + else if (type == message::CLAIM_VEHICLE) + msg = std::make_shared(); + else if (type == message::CLAIM_GRANTED) + msg = std::make_shared(); + else if (type == message::CLAIM_DENIED) + msg = std::make_shared(); + else if (type == message::LEAVE_VEHICLE) + msg = std::make_shared(); + else if (type == message::CREW_UPDATE) + msg = std::make_shared(); if (!msg) { // unknown message type; hand back a marker the peer handlers treat as a protocol error diff --git a/network/message.h b/network/message.h index 42d62ebb..61602162 100644 --- a/network/message.h +++ b/network/message.h @@ -16,6 +16,11 @@ struct message REQUEST_COMMAND, SERVER_REJECT, VEHICLE_LIST, + CLAIM_VEHICLE, + CLAIM_GRANTED, + CLAIM_DENIED, + LEAVE_VEHICLE, + CREW_UPDATE, TYPE_MAX }; @@ -80,6 +85,63 @@ struct vehicle_list : public message virtual void deserialize(std::istream &stream) override; }; +// a peer asking the server to be put on the crew of a vehicle +struct claim_vehicle : public message +{ + claim_vehicle() : message(CLAIM_VEHICLE) {} + + uint32_t entity_id{ ENTITY_NONE }; + + virtual void serialize(std::ostream &stream) const override; + virtual void deserialize(std::istream &stream) override; +}; + +// answer to a claim the server accepted +struct claim_granted : public message +{ + claim_granted() : message(CLAIM_GRANTED) {} + + uint32_t entity_id{ ENTITY_NONE }; + + virtual void serialize(std::ostream &stream) const override; + virtual void deserialize(std::istream &stream) override; +}; + +// answer to a claim the server refused, with a reason the lobby can show +struct claim_denied : public message +{ + claim_denied() : message(CLAIM_DENIED) {} + + uint32_t entity_id{ ENTITY_NONE }; + std::string reason; + + virtual void serialize(std::ostream &stream) const override; + virtual void deserialize(std::istream &stream) override; +}; + +// a peer stepping off the crew of a vehicle +struct leave_vehicle : public message +{ + leave_vehicle() : message(LEAVE_VEHICLE) {} + + uint32_t entity_id{ ENTITY_NONE }; + + virtual void serialize(std::ostream &stream) const override; + virtual void deserialize(std::istream &stream) override; +}; + +// authoritative crew of one vehicle, broadcast whenever it changes +struct crew_update : public message +{ + crew_update() : message(CREW_UPDATE) {} + + uint32_t entity_id{ ENTITY_NONE }; + std::vector crew; + + virtual void serialize(std::ostream &stream) const override; + virtual void deserialize(std::istream &stream) override; +}; + struct request_command : public message { request_command(type_e type) : message(type) {} diff --git a/network/network.cpp b/network/network.cpp index 7764ff99..608bec54 100644 --- a/network/network.cpp +++ b/network/network.cpp @@ -97,23 +97,37 @@ network::server::server(std::shared_ptr buf) : backbuffer(buf) } -void network::server::push_delta(const frame_info &msg) +void network::server::prune_clients() { for (auto it = clients.begin(); it != clients.end(); ) { if ((*it)->state == connection::DEAD) { + if ((*it)->peer_id != PEER_NONE) { + WriteLog("net: peer " + std::to_string((*it)->peer_id) + " disconnected", logtype::net); + // a vehicle keeps running for whoever is left on board; it only falls back + // to the AI when the crew becomes empty, which Crews decides on its own + Crews.drop_peer((*it)->peer_id); + } it = clients.erase(it); continue; } - - if ((*it)->state == connection::ACTIVE) - (*it)->send_message(msg); - it++; } } +void network::server::push_delta(const frame_info &msg) +{ + prune_clients(); + + for (auto const &client : clients) { + if (client->state == connection::ACTIVE) + client->send_message(msg); + } +} + void network::server::push_message(const message &msg) { + prune_clients(); + for (auto const &client : clients) { if (client->state == connection::ACTIVE) client->send_message(msg); @@ -177,6 +191,29 @@ void network::server::handle_message(std::shared_ptr conn, const mes WriteLog("net: peer " + std::to_string(conn->peer_id) + " connected, build \"" + cmd.app_version + "\", scenario \"" + Global.SceneryFile + "\"", logtype::net); } + else if (msg.type == message::CLAIM_VEHICLE) { + const auto& cmd = dynamic_cast(msg); + + auto const result = Crews.claim(conn->peer_id, cmd.entity_id); + if (result == claim_result::granted || result == claim_result::already_member) { + claim_granted reply; + reply.entity_id = cmd.entity_id; + conn->send_message(reply); + } + else { + WriteLog("net: refused claim of vehicle " + std::to_string(cmd.entity_id) + " by peer " + + std::to_string(conn->peer_id) + ": " + describe(result), logtype::net); + + claim_denied reply; + reply.entity_id = cmd.entity_id; + reply.reason = describe(result); + conn->send_message(reply); + } + } + else if (msg.type == message::LEAVE_VEHICLE) { + const auto& cmd = dynamic_cast(msg); + Crews.leave(conn->peer_id, cmd.entity_id); + } else if (msg.type == message::REQUEST_COMMAND) { const auto& cmd = dynamic_cast(msg); @@ -267,6 +304,26 @@ void network::client::send_commands(command_queue::commands_map commands) conn->send_message(msg); } +void network::client::send_claim(NetworkEntityId entity_id) +{ + if (!conn || conn->state != connection::ACTIVE) + return; + + claim_vehicle msg; + msg.entity_id = entity_id; + conn->send_message(msg); +} + +void network::client::send_leave(NetworkEntityId entity_id) +{ + if (!conn || conn->state != connection::ACTIVE) + return; + + leave_vehicle msg; + msg.entity_id = entity_id; + conn->send_message(msg); +} + void network::client::handle_message(std::shared_ptr conn, const message &msg) { if (msg.type >= message::TYPE_MAX) @@ -322,6 +379,28 @@ void network::client::handle_message(std::shared_ptr conn, const mes Entities.adopt(cmd.vehicles); } + if (msg.type == message::CREW_UPDATE) { + const auto& cmd = dynamic_cast(msg); + Crews.mirror(cmd.entity_id, cmd.crew); + } + + if (msg.type == message::CLAIM_GRANTED) { + const auto& cmd = dynamic_cast(msg); + + WriteLog("net: claim of vehicle " + std::to_string(cmd.entity_id) + " granted", logtype::net); + Global.network_lobby_message.clear(); + // the cab itself is built by the replicated entervehicle the server posts; + // we only have to walk into it once it shows up + Global.network_pending_vehicle = Entities.name_of(cmd.entity_id); + } + + if (msg.type == message::CLAIM_DENIED) { + const auto& cmd = dynamic_cast(msg); + + WriteLog("net: claim of vehicle " + std::to_string(cmd.entity_id) + " denied: " + cmd.reason, logtype::net); + Global.network_lobby_message = cmd.reason; + } + if (msg.type == message::FRAME_INFO) { resume_frame_counter++; diff --git a/network/network.h b/network/network.h index a737d926..c681e6fd 100644 --- a/network/network.h +++ b/network/network.h @@ -6,6 +6,7 @@ #include #include "network/message.h" #include "network/entities.h" +#include "network/session.h" #include "input/command.h" namespace network @@ -65,6 +66,10 @@ namespace network command_queue::commands_map client_commands_queue; + protected: + // drops peers whose socket is gone, together with their crew memberships + void prune_clients(); + public: server(std::shared_ptr buf); void push_delta(const frame_info &msg); @@ -105,6 +110,9 @@ namespace network void update(); std::tuple get_next_delta(int counter); void send_commands(command_queue::commands_map commands); + // lobby requests; the server is the one that decides + void send_claim(NetworkEntityId entity_id); + void send_leave(NetworkEntityId entity_id); int get_frame_counter() { return resume_frame_counter; } diff --git a/network/session.cpp b/network/session.cpp new file mode 100644 index 00000000..824a8b6a --- /dev/null +++ b/network/session.cpp @@ -0,0 +1,266 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "network/session.h" + +#include "input/command.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/Timer.h" +#include "vehicle/Driver.h" +#include "vehicle/DynObj.h" +#include "vehicle/Train.h" + +namespace network +{ + +crew_registry Crews; + +std::string describe(claim_result Result) +{ + switch (Result) + { + case claim_result::granted: + return "granted"; + case claim_result::already_member: + return "already a crew member"; + case claim_result::unknown_vehicle: + return "no such vehicle in this session"; + case claim_result::vehicle_missing: + return "the vehicle is not present in the scenario"; + case claim_result::crew_full: + return "the crew of this vehicle is full"; + } + return "unknown"; +} + +vehicle_crew &crew_registry::entry(NetworkEntityId Id) +{ + auto lookup = m_vehicles.find(Id); + if (lookup == m_vehicles.end()) + { + vehicle_crew fresh; + fresh.entity_id = Id; + lookup = m_vehicles.emplace(Id, fresh).first; + } + return lookup->second; +} + +claim_result crew_registry::claim(PeerId Peer, NetworkEntityId Id) +{ + vehicle_entry const *published = Entities.find(Id); + if (published == nullptr) + return claim_result::unknown_vehicle; + + if (Entities.resolve(Id) == nullptr) + return claim_result::vehicle_missing; + + vehicle_crew &vehicle = entry(Id); + + if (std::find(vehicle.crew.begin(), vehicle.crew.end(), Peer) != vehicle.crew.end()) + return claim_result::already_member; + + if (vehicle.crew.size() >= published->crew_capacity) + return claim_result::crew_full; + + if (vehicle.crew.empty()) + { + // remember whether this vehicle was actually being driven by the AI, so that it is + // handed back only to a driver that was really there before + TDynamicObject const *dynamic = Entities.resolve(Id); + vehicle.ai_before_claim = (dynamic != nullptr && dynamic->Mechanik != nullptr && dynamic->Mechanik->AIControllFlag); + vehicle.ai_restore_pending = false; + } + + vehicle.crew.emplace_back(Peer); + m_pending.emplace_back(Id); + + WriteLog("net: peer " + std::to_string(Peer) + " joined the crew of " + published->name + " (" + std::to_string(vehicle.crew.size()) + "/" + + std::to_string((int)published->crew_capacity) + ")", + logtype::net); + + return claim_result::granted; +} + +bool crew_registry::leave(PeerId Peer, NetworkEntityId Id) +{ + auto lookup = m_vehicles.find(Id); + if (lookup == m_vehicles.end()) + return false; + + auto &crew = lookup->second.crew; + auto member = std::find(crew.begin(), crew.end(), Peer); + if (member == crew.end()) + return false; + + crew.erase(member); + m_pending.emplace_back(Id); + + WriteLog("net: peer " + std::to_string(Peer) + " left the crew of " + Entities.name_of(Id) + " (" + std::to_string(crew.size()) + " left)", logtype::net); + + if (crew.empty() && lookup->second.ai_before_claim) + { + // the vehicle came with an AI driver, so it gets one back + lookup->second.ai_restore_pending = true; + } + + return true; +} + +void crew_registry::drop_peer(PeerId Peer) +{ + for (auto &pair : m_vehicles) + { + if (std::find(pair.second.crew.begin(), pair.second.crew.end(), Peer) != pair.second.crew.end()) + leave(Peer, pair.first); + } +} + +std::vector crew_registry::crew_of(NetworkEntityId Id) const +{ + auto const lookup = m_vehicles.find(Id); + return lookup != m_vehicles.end() ? lookup->second.crew : std::vector(); +} + +bool crew_registry::is_member(PeerId Peer, NetworkEntityId Id) const +{ + auto const lookup = m_vehicles.find(Id); + if (lookup == m_vehicles.end()) + return false; + return std::find(lookup->second.crew.begin(), lookup->second.crew.end(), Peer) != lookup->second.crew.end(); +} + +uint8_t crew_registry::count(NetworkEntityId Id) const +{ + auto const lookup = m_vehicles.find(Id); + return lookup != m_vehicles.end() ? (uint8_t)lookup->second.crew.size() : (uint8_t)0; +} + +NetworkEntityId crew_registry::vehicle_of(PeerId Peer) const +{ + for (auto const &pair : m_vehicles) + { + if (std::find(pair.second.crew.begin(), pair.second.crew.end(), Peer) != pair.second.crew.end()) + return pair.first; + } + return ENTITY_NONE; +} + +void crew_registry::mirror(NetworkEntityId Id, std::vector const &Crew) +{ + entry(Id).crew = Crew; +} + +std::vector crew_registry::take_pending_updates() +{ + std::vector pending; + pending.swap(m_pending); + + // the same vehicle can change more than once between two publications + std::sort(pending.begin(), pending.end()); + pending.erase(std::unique(pending.begin(), pending.end()), pending.end()); + + return pending; +} + +void crew_registry::mark_all_pending() +{ + for (auto const &pair : m_vehicles) + { + if (!pair.second.crew.empty()) + m_pending.emplace_back(pair.first); + } +} + +void crew_registry::clear() +{ + m_vehicles.clear(); + m_pending.clear(); +} + +namespace +{ + +// posts a command the way an input source would, but without going through command_relay: +// the authority acts on behalf of the session, not on behalf of the local camera, so it +// must not be filtered by the local free fly state +void post_authority_command(user_command Command, uint32_t Recipient, glm::vec3 const &Location, std::string const &Payload) +{ + command_data data; + data.command = Command; + data.action = GLFW_PRESS; + data.param1 = 0.0; + data.param2 = 0.0; + data.time_delta = Timer::GetDeltaTime(); + data.freefly = true; + data.location = Location; + data.payload = Payload; + + simulation::Commands.push(data, Recipient); +} + +} // namespace + +// frames to wait before repeating an authority request that has not taken effect yet +static int const ACTION_COOLDOWN_FRAMES = 30; + +void crew_registry::reconcile() +{ + for (auto &pair : m_vehicles) + { + vehicle_crew &vehicle = pair.second; + + TDynamicObject *dynamic = Entities.resolve(vehicle.entity_id); + if (dynamic == nullptr) + continue; + + if (vehicle.action_cooldown > 0) + { + --vehicle.action_cooldown; + continue; + } + + TTrain *train = simulation::Trains.find(dynamic->name()); + + if (!vehicle.crew.empty()) + { + if (train == nullptr) + { + // the cab has to exist on every peer, so the authority asks for it with the + // ordinary replicated command instead of building it locally + post_authority_command(user_command::entervehicle, (uint32_t)command_target::simulation, dynamic->GetPosition(), dynamic->name()); + vehicle.action_cooldown = ACTION_COOLDOWN_FRAMES; + continue; + } + + if (dynamic->Mechanik != nullptr && dynamic->Mechanik->AIControllFlag) + { + // first human on board takes over from the AI + WriteLog("net: " + dynamic->name() + " taken over from the AI driver", logtype::net); + post_authority_command(user_command::aidriverdisable, (uint32_t)command_target::vehicle | train->id(), dynamic->GetPosition(), std::string()); + vehicle.action_cooldown = ACTION_COOLDOWN_FRAMES; + } + continue; + } + + if (vehicle.ai_restore_pending && train != nullptr) + { + // aidriverenable resets the driver, so it must happen exactly once + vehicle.ai_restore_pending = false; + vehicle.ai_before_claim = false; + WriteLog("net: handing " + dynamic->name() + " back to the AI driver", logtype::net); + post_authority_command(user_command::aidriverenable, (uint32_t)command_target::vehicle | train->id(), dynamic->GetPosition(), std::string()); + vehicle.action_cooldown = ACTION_COOLDOWN_FRAMES; + } + } +} + +} // namespace network diff --git a/network/session.h b/network/session.h new file mode 100644 index 00000000..e2026e74 --- /dev/null +++ b/network/session.h @@ -0,0 +1,90 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "network/entities.h" + +#include +#include +#include + +namespace network +{ + +// who is working on a given vehicle. a vehicle can hold more than one person, which is +// the whole point of the crew model - the container is not limited by design, only by +// the capacity the server advertises +struct vehicle_crew +{ + NetworkEntityId entity_id{ENTITY_NONE}; + std::vector crew; + // whether the AI was actually driving before the first human arrived. a vehicle that + // nobody was driving must not suddenly acquire an AI driver when its crew leaves + bool ai_before_claim{false}; + // set on the 1 -> 0 transition, cleared once the AI has been handed the vehicle back + bool ai_restore_pending{false}; + // keeps reconcile() from re-posting the same request every single frame while the + // previous one is still on its way through the command queue + int action_cooldown{0}; +}; + +enum class claim_result +{ + granted, + already_member, + unknown_vehicle, + vehicle_missing, + crew_full +}; + +std::string describe(claim_result Result); + +// crew bookkeeping. authoritative on the server; on a client this is a read only mirror +// fed by CREW_UPDATE, used by the lobby +class crew_registry +{ +public: + claim_result claim(PeerId Peer, NetworkEntityId Id); + // returns true when the peer really was part of the crew + bool leave(PeerId Peer, NetworkEntityId Id); + // removes a peer from every crew it belonged to, e.g. after a disconnect + void drop_peer(PeerId Peer); + + std::vector crew_of(NetworkEntityId Id) const; + bool is_member(PeerId Peer, NetworkEntityId Id) const; + uint8_t count(NetworkEntityId Id) const; + NetworkEntityId vehicle_of(PeerId Peer) const; + + // client side: takes over the crew of one vehicle as published by the server + void mirror(NetworkEntityId Id, std::vector const &Crew); + + // server side: brings the world in line with the crew roster - creates the cab of a + // claimed vehicle, hands the vehicle over from the AI and hands it back when the last + // person leaves. safe to call every frame, it only acts when something is out of place + void reconcile(); + + // ids whose crew changed since the last call + std::vector take_pending_updates(); + // queues every crew for republication, so that a peer which just went active learns + // about the vehicles that were taken over before it arrived + void mark_all_pending(); + + void clear(); + +private: + vehicle_crew &entry(NetworkEntityId Id); + + std::unordered_map m_vehicles; + std::vector m_pending; +}; + +extern crew_registry Crews; + +} // namespace network diff --git a/utilities/Globals.h b/utilities/Globals.h index 03e34290..23f02b47 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -358,6 +358,10 @@ struct global_settings { uint32_t network_peer_id = 0; // vehicle the local player asked to enter from the multiplayer lobby std::string network_pending_vehicle; + // set when the local player left the crew and should go back to observing + bool network_leave_pending = false; + // last answer of the server to a lobby request, shown in the lobby + std::string network_lobby_message; std::unordered_map trainset_overrides; diff --git a/widgets/multiplayer_lobby.cpp b/widgets/multiplayer_lobby.cpp index 9c6420fe..2feac4a6 100644 --- a/widgets/multiplayer_lobby.cpp +++ b/widgets/multiplayer_lobby.cpp @@ -12,10 +12,11 @@ http://mozilla.org/MPL/2.0/. #include "application/application.h" #include "network/entities.h" +#include "network/session.h" #include "simulation/simulation.h" #include "utilities/Globals.h" -#include "utilities/utilities.h" #include "utilities/translation.h" +#include "utilities/utilities.h" #include "vehicle/Driver.h" #include "vehicle/DynObj.h" #include "vehicle/Train.h" @@ -23,21 +24,30 @@ http://mozilla.org/MPL/2.0/. ui::multiplayer_lobby_panel::multiplayer_lobby_panel() : ui_panel(STR_C("Multiplayer lobby"), false) { - size_min = {480, 260}; + size_min = {520, 280}; } -void ui::multiplayer_lobby_panel::enter_vehicle(std::string const &Name) +std::string ui::multiplayer_lobby_panel::describe_crew(network::NetworkEntityId const Entity) const { - TDynamicObject *vehicle = simulation::Vehicles.find(Name); - if (vehicle == nullptr) - return; + auto const crew = network::Crews.crew_of(Entity); + if (crew.empty()) + return std::string(); - std::string payload{Name}; - // the request travels as an ordinary simulation command, so every peer builds the cab - m_relay.post(user_command::entervehicle, 0.0, simulation::Train ? simulation::Train->id() : 0, GLFW_PRESS, 0, vehicle->GetPosition(), &payload); - // ...and the driver mode moves our own camera in once the cab shows up locally - Global.network_pending_vehicle = Name; - is_open = false; + std::string description; + for (network::PeerId const peer : crew) + { + if (!description.empty()) + description += ", "; + + if (peer == Global.network_peer_id) + description += STR("you"); + else if (peer == network::PEER_HOST) + description += STR("host"); + else + description += STR("player") + " " + std::to_string(peer); + } + + return description; } void ui::multiplayer_lobby_panel::render_contents() @@ -53,6 +63,10 @@ void ui::multiplayer_lobby_panel::render_contents() ImGui::Text("%s: %s, peer %u", STR_C("Session"), host ? (client ? "host + client" : "host") : "client", Global.network_peer_id); ImGui::TextUnformatted(Global.SceneryFile.c_str()); + + if (!Global.network_lobby_message.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.3f, 1.0f), "%s", Global.network_lobby_message.c_str()); + ImGui::Separator(); auto const &entries = network::Entities.entries(); @@ -62,24 +76,18 @@ void ui::multiplayer_lobby_panel::render_contents() return; } - std::string const currentvehicle{simulation::Train != nullptr && simulation::Train->Dynamic() != nullptr ? simulation::Train->Dynamic()->name() : std::string()}; - - if (!FreeFlyModeFlag) - { - ImGui::TextUnformatted(STR_C("Leave the cab to change vehicles.")); - ImGui::Separator(); - } + network::NetworkEntityId const ownvehicle{network::Crews.vehicle_of(Global.network_peer_id)}; ImGui::Columns(4, "mp_vehicles", false); - ImGui::SetColumnWidth(1, 80.0f * Global.ui_scale); - ImGui::SetColumnWidth(2, 90.0f * Global.ui_scale); + ImGui::SetColumnWidth(1, 70.0f * Global.ui_scale); + ImGui::SetColumnWidth(2, 200.0f * Global.ui_scale); ImGui::SetColumnWidth(3, 130.0f * Global.ui_scale); ImGui::TextDisabled("%s", STR_C("Vehicle")); ImGui::NextColumn(); ImGui::TextDisabled("%s", STR_C("Crew")); ImGui::NextColumn(); - ImGui::TextDisabled("%s", STR_C("Driver")); + ImGui::TextDisabled("%s", STR_C("On board")); ImGui::NextColumn(); ImGui::NextColumn(); ImGui::Separator(); @@ -94,21 +102,28 @@ void ui::multiplayer_lobby_panel::render_contents() ImGui::Text("%u/%u", (unsigned)entry.crew_count, (unsigned)entry.crew_capacity); ImGui::NextColumn(); - ImGui::TextUnformatted(entry.crew_count > 0 ? STR_C("player") : entry.ai_active ? STR_C("AI") : STR_C("free")); + std::string const crew{describe_crew(entry.id)}; + if (!crew.empty()) + ImGui::TextUnformatted(crew.c_str()); + else + ImGui::TextDisabled("%s", entry.ai_active ? STR_C("AI") : STR_C("free")); ImGui::NextColumn(); - if (entry.name == currentvehicle) + if (entry.id == ownvehicle) { - ImGui::TextUnformatted(STR_C("you")); + if (ImGui::Button(STR_C("Leave"), ImVec2(-1, 0))) + Application.request_vehicle_leave(entry.id); } - else if (entry.claimable && FreeFlyModeFlag && network::Entities.resolve(entry.id) != nullptr) + else if (entry.claimable) { - if (ImGui::Button(STR_C("Enter"), ImVec2(-1, 0))) - enter_vehicle(entry.name); + // a vehicle somebody else is already working on can still be joined, that is + // what the crew capacity is for + if (ImGui::Button(entry.crew_count > 0 ? STR_C("Join crew") : STR_C("Enter"), ImVec2(-1, 0))) + claim(entry.id, ownvehicle); } else { - ImGui::TextDisabled("%s", entry.claimable ? "-" : STR_C("occupied")); + ImGui::TextDisabled("%s", STR_C("full")); } ImGui::NextColumn(); @@ -117,3 +132,14 @@ void ui::multiplayer_lobby_panel::render_contents() ImGui::Columns(1); } + +void ui::multiplayer_lobby_panel::claim(network::NetworkEntityId const Entity, network::NetworkEntityId const Current) +{ + if (Current != network::ENTITY_NONE) + { + // one person cannot work two vehicles at once + Application.request_vehicle_leave(Current); + } + + Application.request_vehicle_claim(Entity); +} diff --git a/widgets/multiplayer_lobby.h b/widgets/multiplayer_lobby.h index a57e58e5..4a09f188 100644 --- a/widgets/multiplayer_lobby.h +++ b/widgets/multiplayer_lobby.h @@ -10,7 +10,7 @@ http://mozilla.org/MPL/2.0/. #pragma once #include "application/uilayer.h" -#include "input/command.h" +#include "network/entities.h" namespace ui { @@ -27,9 +27,8 @@ public: void render_contents() override; private: - void enter_vehicle(std::string const &Name); - - command_relay m_relay; + void claim(network::NetworkEntityId const Entity, network::NetworkEntityId const Current); + std::string describe_crew(network::NetworkEntityId const Entity) const; }; } // namespace ui