diff --git a/application/application.cpp b/application/application.cpp index 44dfd58d..96679109 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -894,6 +894,17 @@ eu07_application::get_input_hint( user_command const Command ) const { void eu07_application::on_key(int const Key, int const Scancode, int const Action, int const Mods) { + // the session chat opens on the key left of 1, and it is checked before imgui gets a + // look at the input: a lobby or any other window that happens to have the keyboard + // would otherwise swallow the key and the chat could never be opened at all. the one + // case where imgui does get it is when something is already taking typed text - the + // chat box itself, most of the time, which closes on escape instead + if ((Key == GLFW_KEY_GRAVE_ACCENT) && (Action == GLFW_PRESS) && (Mods == 0) && !m_modestack.empty() && network::is_multiplayer() && + !ui_layer::wants_keyboard()) + { + if (m_modes[m_modestack.top()]->toggle_chat()) + return; + } if (ui_layer::key_callback(Key, Scancode, Action, Mods)) return; diff --git a/application/applicationmode.h b/application/applicationmode.h index feea3bdb..ab2addde 100644 --- a/application/applicationmode.h +++ b/application/applicationmode.h @@ -51,6 +51,8 @@ public: virtual void on_scroll( double Xoffset, double Yoffset ) = 0; virtual void on_window_resize( int w, int h ) = 0; virtual void on_event_poll() = 0; + // opens or closes the session chat; true when this mode has one to open + virtual bool toggle_chat() { return false; } virtual bool is_command_processor() const = 0; protected: diff --git a/application/drivermode.cpp b/application/drivermode.cpp index 9c5f0134..3e1a00e9 100644 --- a/application/drivermode.cpp +++ b/application/drivermode.cpp @@ -31,6 +31,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Logs.h" #include "network/entities.h" #include "network/snapshot.h" +#include "network/session.h" /* namespace input { @@ -213,6 +214,17 @@ bool driver_mode::update() if (!change_train.empty()) { TTrain *train = simulation::Trains.find(change_train); + + if ((train == nullptr) && network::is_multiplayer()) + { + // a cab belongs to the peer it is on: each player has their own, with their own + // camera and their own view of it. waiting for a replicated command to build it + // only ever worked for the first person aboard a train - the second finds the + // cab already standing on the peer that got there first, so nothing is posted + // and the lobby button appears to do nothing at all + train = network::ensure_local_cab(simulation::Vehicles.find(change_train)); + } + if (train) { Global.local_start_vehicle = change_train; @@ -540,20 +552,6 @@ void driver_mode::on_key(int const Key, int const Scancode, int const Action, in bool anyModifier = Mods & (GLFW_MOD_SHIFT | GLFW_MOD_CONTROL | GLFW_MOD_ALT); - // the key left of 1 opens and closes the session chat. it is only bound in a session, - // so single player keeps it for whatever else may want it later. while the chat has - // the keyboard this is never reached: imgui swallows the input before it gets here, - // which is exactly what stops typed letters from working the cab - if ((Key == GLFW_KEY_GRAVE_ACCENT) && (Action == GLFW_PRESS) && !anyModifier && network::is_multiplayer()) - { - auto ui = std::dynamic_pointer_cast(m_userinterface); - if (ui != nullptr) - { - ui->toggle_chat(); - return; - } - } - // give the ui first shot at the input processing... if (!anyModifier && true == m_userinterface->on_key(Key, Action)) { @@ -629,6 +627,16 @@ void driver_mode::on_event_poll() m_input.poll(); } +bool driver_mode::toggle_chat() +{ + auto ui = std::dynamic_pointer_cast(m_userinterface); + if (ui == nullptr) + return false; + + ui->toggle_chat(); + return true; +} + bool driver_mode::is_command_processor() const { diff --git a/application/drivermode.h b/application/drivermode.h index 17edc575..679e98d4 100644 --- a/application/drivermode.h +++ b/application/drivermode.h @@ -49,6 +49,8 @@ public: void on_scroll( double Xoffset, double Yoffset ) override; void on_window_resize( int w, int h ) override { ; } void on_event_poll() override; + // opens or closes the session chat + bool toggle_chat() override; bool is_command_processor() const override; private: diff --git a/application/uilayer.cpp b/application/uilayer.cpp index 00ff8273..5a1efd90 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -112,6 +112,11 @@ bool ui_layer::key_callback(int key, int scancode, int action, int mods) return m_imguiio->WantCaptureKeyboard; } +bool ui_layer::wants_keyboard() +{ + return (m_imguiio != nullptr) && m_imguiio->WantCaptureKeyboard; +} + bool ui_layer::char_callback(unsigned int c) { ImGui_ImplGlfw_CharCallback(m_window, c); diff --git a/application/uilayer.h b/application/uilayer.h index 6d4019c6..61b36121 100644 --- a/application/uilayer.h +++ b/application/uilayer.h @@ -115,6 +115,9 @@ public: // callback functions for imgui input // returns true if input is consumed static bool key_callback(int key, int scancode, int action, int mods); + // true when something on screen is taking typed input and the game must keep its + // hands off the keyboard + static bool wants_keyboard(); static bool char_callback(unsigned int c); static bool scroll_callback(double xoffset, double yoffset); static bool mouse_button_callback(int button, int action, int mods); diff --git a/network/manager.cpp b/network/manager.cpp index 94398a2c..30b92617 100644 --- a/network/manager.cpp +++ b/network/manager.cpp @@ -112,6 +112,7 @@ void network::server_manager::update_crews() crew_update msg; msg.entity_id = id; msg.crew = Crews.crew_of(id); + msg.since = Crews.crew_since_of(id); for (auto srv : servers) srv->push_message(msg); @@ -226,6 +227,7 @@ void network::manager::say(const std::string &text) { // a client waits to hear itself back from the server, so that everybody sees the // conversation in the same order + WriteLog("net: saying \"" + line + "\"", logtype::net); client->send_chat(line); } } diff --git a/network/message.cpp b/network/message.cpp index 0781cabe..675fe3be 100644 --- a/network/message.cpp +++ b/network/message.cpp @@ -220,6 +220,9 @@ void network::crew_update::serialize(std::ostream &stream) const sn_utils::ls_uint32(stream, (uint32_t)crew.size()); for (PeerId const peer : crew) sn_utils::ls_uint32(stream, peer); + sn_utils::ls_uint32(stream, (uint32_t)since.size()); + for (uint64_t const stamp : since) + sn_utils::ls_uint64(stream, stamp); } void network::crew_update::deserialize(std::istream &stream) @@ -231,6 +234,12 @@ void network::crew_update::deserialize(std::istream &stream) crew.reserve(count); for (uint32_t i = 0; i < count; i++) crew.emplace_back(sn_utils::ld_uint32(stream)); + + since.clear(); + uint32_t const stamps = sn_utils::ld_uint32(stream); + since.reserve(stamps); + for (uint32_t i = 0; i < stamps; i++) + since.emplace_back(sn_utils::ld_uint64(stream)); } void ::network::request_command::serialize(std::ostream &stream) const diff --git a/network/message.h b/network/message.h index 2e7f0d8c..522fc0c3 100644 --- a/network/message.h +++ b/network/message.h @@ -222,6 +222,9 @@ struct crew_update : public message uint32_t entity_id{ ENTITY_NONE }; std::vector crew; + // when each of them boarded, on a session-wide counter. it decides which peer runs the + // train's physics, so it has to travel with the crew rather than be guessed at + std::vector since; virtual void serialize(std::ostream &stream) const override; virtual void deserialize(std::istream &stream) override; diff --git a/network/network.cpp b/network/network.cpp index 50ae0248..852086e7 100644 --- a/network/network.cpp +++ b/network/network.cpp @@ -17,7 +17,7 @@ // 3 - handshake carries build identification and an explicit rejection message // 4 - peer identity, vehicle roster and crew handshake, command source, // logical simulation tick and a versioned state digest instead of the position sum -std::uint32_t const EU07_NETWORK_VERSION = 5; +std::uint32_t const EU07_NETWORK_VERSION = 6; namespace network { @@ -668,7 +668,7 @@ void network::client::handle_message(std::shared_ptr conn, const mes if (msg.type == message::CREW_UPDATE) { const auto& cmd = dynamic_cast(msg); - Crews.mirror(cmd.entity_id, cmd.crew); + Crews.mirror(cmd.entity_id, cmd.crew, cmd.since); } if (msg.type == message::CLAIM_GRANTED) { @@ -676,8 +676,7 @@ void network::client::handle_message(std::shared_ptr conn, const mes 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 + // the driver mode builds this peer's own cab for the vehicle and moves in Global.network_pending_vehicle = Entities.name_of(cmd.entity_id); } diff --git a/network/session.cpp b/network/session.cpp index f22eab61..46c56db3 100644 --- a/network/session.cpp +++ b/network/session.cpp @@ -24,6 +24,19 @@ namespace network crew_registry Crews; +namespace +{ + +// counts up for as long as the session lasts; the number a crew member is stamped with is +// the whole of their claim to running the train +uint64_t next_join_stamp() +{ + static uint64_t stamp{0}; + return ++stamp; +} + +} // namespace + std::string describe(claim_result Result) { switch (Result) @@ -185,21 +198,29 @@ PeerId simulation_owner(NetworkEntityId Entity) if (train == ENTITY_NONE) return PEER_HOST; - // the crew of the lowest numbered car decides, so that every peer works it out the - // same way without having to be told + // whoever boarded this train first runs it. not the lowest numbered car, not the + // newest arrival: the physics has to stay with one person for as long as they are + // aboard, because every handover is a stutter in somebody's ride - and the person + // who has been driving is the one who would notice it PeerId owner{PEER_NONE}; - NetworkEntityId lowest{ENTITY_NONE}; + uint64_t earliest{0}; for (auto const id : Entities.consist_members(train)) { auto const crew = Crews.crew_of(id); - if (crew.empty()) - continue; + auto const since = Crews.crew_since_of(id); - if (lowest == ENTITY_NONE || id < lowest) + for (size_t i = 0; i < crew.size(); ++i) { - lowest = id; - owner = crew.front(); + // a stamp of zero is a crew we were told about by an older peer, or one that + // somehow lost its order; it sorts behind anything stamped properly + uint64_t const stamp = (i < since.size() && since[i] != 0 ? since[i] : ~0ull - crew[i]); + + if (owner == PEER_NONE || stamp < earliest) + { + earliest = stamp; + owner = crew[i]; + } } } @@ -274,6 +295,7 @@ claim_result crew_registry::claim(PeerId Peer, NetworkEntityId Id) } vehicle.crew.emplace_back(Peer); + vehicle.crew_since.emplace_back(next_join_stamp()); m_pending.emplace_back(Id); WriteLog("net: peer " + std::to_string(Peer) + " joined the crew of " + published->name + " (" + std::to_string(vehicle.crew.size()) + "/" + @@ -294,7 +316,10 @@ bool crew_registry::leave(PeerId Peer, NetworkEntityId Id) if (member == crew.end()) return false; + auto const index = (size_t)std::distance(crew.begin(), member); crew.erase(member); + if (index < lookup->second.crew_since.size()) + lookup->second.crew_since.erase(lookup->second.crew_since.begin() + index); 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); @@ -334,6 +359,28 @@ void apply_session_config(int64_t Config) Global.RealisticControlMode = realisticcontrol; } +TTrain *ensure_local_cab(TDynamicObject *Vehicle) +{ + if (Vehicle == nullptr) + return nullptr; + + TTrain *train = simulation::Trains.find(Vehicle->name()); + if (train != nullptr) + return train; + + train = new TTrain(); + if (false == train->Init(Vehicle)) + { + delete train; + ErrorLog("net: could not build a cab for " + Vehicle->name(), logtype::net); + return nullptr; + } + + simulation::Trains.insert(train); + + return train; +} + void enforce_session_settings() { if (Global.trainThreads != 0) @@ -451,9 +498,18 @@ NetworkEntityId crew_registry::vehicle_of(PeerId Peer) const return ENTITY_NONE; } -void crew_registry::mirror(NetworkEntityId Id, std::vector const &Crew) +void crew_registry::mirror(NetworkEntityId Id, std::vector const &Crew, std::vector const &Since) { - entry(Id).crew = Crew; + auto &vehicle = entry(Id); + vehicle.crew = Crew; + vehicle.crew_since = Since; + vehicle.crew_since.resize(Crew.size(), 0); +} + +std::vector crew_registry::crew_since_of(NetworkEntityId Id) const +{ + auto const lookup = m_vehicles.find(Id); + return lookup != m_vehicles.end() ? lookup->second.crew_since : std::vector(); } void crew_registry::take_seat(PeerId Peer, NetworkEntityId Id) @@ -470,6 +526,7 @@ void crew_registry::take_seat(PeerId Peer, NetworkEntityId Id) } vehicle.crew.emplace_back(Peer); + vehicle.crew_since.emplace_back(next_join_stamp()); m_pending.emplace_back(Id); } @@ -550,11 +607,15 @@ void crew_registry::reconcile() { 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; + // the authority needs a cab of its own to address the AI commands to. it + // is built here and not replicated: a cab belongs to the peer it is on, + // and every peer that is actually aboard builds its own + train = ensure_local_cab(dynamic); + if (train == nullptr) + { + vehicle.action_cooldown = ACTION_COOLDOWN_FRAMES; + continue; + } } if (dynamic->Mechanik != nullptr && dynamic->Mechanik->AIControllFlag) diff --git a/network/session.h b/network/session.h index 42e5b17a..ca55e711 100644 --- a/network/session.h +++ b/network/session.h @@ -16,6 +16,9 @@ http://mozilla.org/MPL/2.0/. #include #include +class TDynamicObject; +class TTrain; + namespace network { @@ -26,6 +29,11 @@ struct vehicle_crew { NetworkEntityId entity_id{ENTITY_NONE}; std::vector crew; + // when each of them sat down, on a counter that runs across the whole session. this + // is what settles who runs the physics of a shared train: the one who got there first + // keeps it, so their ride stays smooth and nobody's authority changes under them when + // somebody else joins or the consist is renumbered + std::vector crew_since; // 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}; @@ -76,7 +84,9 @@ public: 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); + void mirror(NetworkEntityId Id, std::vector const &Crew, std::vector const &Since); + // the join stamps of a crew, in the same order as crew_of() + std::vector crew_since_of(NetworkEntityId Id) const; // puts a peer on a vehicle without asking about capacity; used when somebody walks to // another car of the train they are already working void take_seat(PeerId Peer, NetworkEntityId Id); @@ -171,4 +181,11 @@ claim_result claim_by_name(PeerId Peer, std::string const &Vehicle, NetworkEntit // very same authority layer as input arriving over the wire, only without the transport void filter_commands(PeerId Peer, command_queue::commands_map &Commands); +// makes sure this peer has a cab instance for a vehicle, building one if it has none. +// the cab is a local object - each participant has their own, with their own camera and +// their own view - so it is never replicated: the second person to board a train finds +// the cab already built on the peer that got there first and would otherwise wait for a +// command that is never coming +TTrain *ensure_local_cab(TDynamicObject *Vehicle); + } // namespace network diff --git a/network/snapshot.cpp b/network/snapshot.cpp index fc340045..7b7e4efb 100644 --- a/network/snapshot.cpp +++ b/network/snapshot.cpp @@ -1045,10 +1045,15 @@ std::string write_crews() if (crew.empty()) continue; + auto since = network::Crews.crew_since_of(entry.id); + since.resize(crew.size(), 0); + sn_utils::ls_uint32(entries, entry.id); sn_utils::ls_uint32(entries, (uint32_t)crew.size()); for (network::PeerId const peer : crew) sn_utils::ls_uint32(entries, peer); + for (uint64_t const stamp : since) + sn_utils::ls_uint64(entries, stamp); ++count; } @@ -1075,7 +1080,12 @@ void read_crews(std::istream &Stream) for (uint32_t p = 0; p < size; ++p) crew.emplace_back(sn_utils::ld_uint32(Stream)); - network::Crews.mirror(id, crew); + std::vector since; + since.reserve(size); + for (uint32_t p = 0; p < size; ++p) + since.emplace_back(sn_utils::ld_uint64(Stream)); + + network::Crews.mirror(id, crew, since); } } diff --git a/network/snapshot.h b/network/snapshot.h index 947478d9..5b8950c7 100644 --- a/network/snapshot.h +++ b/network/snapshot.h @@ -18,7 +18,7 @@ namespace network { // layout of the snapshot container. a reader refuses a blob it does not understand -constexpr uint32_t SNAPSHOT_VERSION = 6; +constexpr uint32_t SNAPSHOT_VERSION = 7; // the sections a snapshot is built from. a reader skips over any id it does not know, // which is what makes the format extensible: a peer built before a section existed can