diff --git a/application/application.cpp b/application/application.cpp index ddc2c4a9..90ae44d0 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -463,10 +463,9 @@ void eu07_application::request_vehicle_leave(network::NetworkEntityId const Enti namespace { -// how many steps in a row may disagree with the authority before we ask to be corrected, -// and how long to leave the correction alone before asking again -uint64_t const NETWORK_RESYNC_THRESHOLD = 120; -uint64_t const NETWORK_RESYNC_COOLDOWN = 600; +// the state digest is logged when it starts differing and then only this often, so that +// a persistent difference does not bury the rest of the log +uint64_t const NETWORK_DIGEST_LOG_INTERVAL = 1800; } // namespace @@ -581,46 +580,26 @@ int eu07_application::run() // if we're slave if (m_network && m_network->client) { - if (authoritative.valid) + // the digest is a diagnostic, not a control input: a client runs its + // own physics, so the two will never agree bit for bit and demanding + // that they do only produces noise. what actually keeps a client in + // line is the authoritative state the server streams to it + if (authoritative.valid && authoritative.state_hash_version == network::STATE_HASH_VERSION) { - if (authoritative.state_hash_version != network::STATE_HASH_VERSION) - { - // nothing sensible to compare; say so once and stop pretending - if (m_statemismatches == 0) - { - ErrorLog("net: state digest version mismatch, desync detection is off", logtype::net); - m_statemismatches = 1; - } - } - else if (statehash != authoritative.state_hash) + if (statehash != authoritative.state_hash) { ++m_statemismatches; - // a drift usually persists, so this is reported when it starts - // and then only now and then, instead of every single step - if (m_statemismatches == 1 || (m_statemismatches % 300) == 0) + if (m_statemismatches == 1 || (m_statemismatches % NETWORK_DIGEST_LOG_INTERVAL) == 0) { - WriteLog("net: state mismatch at tick " + std::to_string(authoritative.tick) + " (" + std::to_string(m_statemismatches) + + WriteLog("net: state digest differs at tick " + std::to_string(authoritative.tick) + " (" + std::to_string(m_statemismatches) + " steps): local " + std::to_string(statehash) + ", authoritative " + std::to_string(authoritative.state_hash), logtype::net); } - Global.desync = (float)m_statemismatches; - - // a short disagreement rides itself out; a lasting one will not, - // and calls for the authoritative state rather than a restart - if ((m_statemismatches >= NETWORK_RESYNC_THRESHOLD) && - (m_lastresynctick == 0 || authoritative.tick > m_lastresynctick + NETWORK_RESYNC_COOLDOWN)) - { - m_lastresynctick = authoritative.tick; - WriteLog("net: resync requested at tick " + std::to_string(authoritative.tick), logtype::net); - m_network->request_resync(authoritative.tick, statehash); - } } - else + else if (m_statemismatches != 0) { - if (m_statemismatches != 0) - WriteLog("net: state back in step with the authority at tick " + std::to_string(authoritative.tick), logtype::net); + WriteLog("net: state digest back in step at tick " + std::to_string(authoritative.tick), logtype::net); m_statemismatches = 0; - Global.desync = 0.0f; } } diff --git a/application/application.h b/application/application.h index bf6d69ad..98c76bf6 100644 --- a/application/application.h +++ b/application/application.h @@ -123,10 +123,9 @@ private: // members bool m_screenshot_queued = false; - // consecutive authoritative steps whose state digest did not match ours + // consecutive authoritative steps whose state digest did not match ours. purely a + // diagnostic: a client runs its own physics, so it is never expected to match exactly std::uint64_t m_statemismatches { 0 }; - // tick at which we last asked the server to put us back in line - std::uint64_t m_lastresynctick { 0 }; modeptr_array m_modes { nullptr }; // collection of available application behaviour modes mode_stack m_modestack; // current behaviour mode diff --git a/application/driveruilayer.cpp b/application/driveruilayer.cpp index 48d7d871..70de30ee 100644 --- a/application/driveruilayer.cpp +++ b/application/driveruilayer.cpp @@ -54,6 +54,10 @@ driver_ui::driver_ui() m_transcriptspanel.size_min = {435, 85}; m_transcriptspanel.size_max = {Global.fb_size.x * 0.95, Global.fb_size.y * 0.95}; + m_multiplayerlobby.title = STR("Multiplayer lobby"); + m_multiplayerlobby.size_min = {560, 320}; + m_multiplayerlobby.size_max = {Global.fb_size.x * 0.95f, Global.fb_size.y * 0.95f}; + if (Global.gui_defaultwindows) { m_aidpanel.is_open = true; @@ -278,11 +282,11 @@ void driver_ui::render_() ImGui::OpenPopup(popupheader); } - if (Global.desync != 0.0f) + if (Global.network_position_error > 1.0f) { ImGui::SetNextWindowSize(ImVec2(-1, -1)); if (ImGui::Begin("network", nullptr, ImGuiWindowFlags_NoCollapse)) - ImGui::Text("out of step with the server for %d ticks (at %llu)", (int)Global.desync, (unsigned long long)Global.simulation_tick); + ImGui::Text("%.1f m behind the server (tick %llu)", Global.network_position_error, (unsigned long long)Global.simulation_tick); ImGui::End(); } } diff --git a/input/command.cpp b/input/command.cpp index 9fbf72f9..103b5710 100644 --- a/input/command.cpp +++ b/input/command.cpp @@ -806,17 +806,37 @@ bool is_vehicle_recipient(uint32_t const Recipient) { return (command_target)(Recipient & ~0xffff) == command_target::vehicle; } +// says a thing once instead of once per frame; a command that cannot be addressed keeps +// failing for as long as the player holds the key +bool report_once(std::string const &Message) { + static std::unordered_set reported; + if (!reported.emplace(Message).second) + return false; + ErrorLog(Message, logtype::net); + return true; +} + uint32_t to_network_recipient(uint32_t const Recipient) { - if (!is_vehicle_recipient(Recipient) || network::Entities.empty()) + if (!is_vehicle_recipient(Recipient)) return Recipient; + if (network::Entities.empty()) { + if (network::is_multiplayer()) + report_once("net: no vehicle roster yet, vehicle commands cannot be addressed"); + return Recipient; + } + TTrain *train = simulation::Trains.find_id((uint16_t)(Recipient & 0xffff)); - if (train == nullptr || train->Dynamic() == nullptr) + if (train == nullptr || train->Dynamic() == nullptr) { + report_once("net: no cab behind local id " + std::to_string(Recipient & 0xffff) + ", its commands cannot be addressed"); return Recipient; + } auto const id = network::Entities.id_of(train->Dynamic()->name()); - if (id == network::ENTITY_NONE || id > 0xffff) + if (id == network::ENTITY_NONE || id > 0xffff) { + report_once("net: \"" + train->Dynamic()->name() + "\" has no network id, its commands cannot leave this peer"); return Recipient; + } return (uint32_t)command_target::vehicle | id; } @@ -833,8 +853,10 @@ bool from_network_recipient(uint32_t const Recipient, uint32_t &Local) { return true; TTrain *train = simulation::Trains.find(name); - if (train == nullptr) + if (train == nullptr) { + report_once("net: \"" + name + "\" has no cab here yet, commands for it are dropped"); return false; + } Local = (uint32_t)command_target::vehicle | train->id(); return true; diff --git a/network/manager.cpp b/network/manager.cpp index f4da38a9..89c50c38 100644 --- a/network/manager.cpp +++ b/network/manager.cpp @@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Logs.h" #include "utilities/Globals.h" #include "network/statehash.h" +#include "network/snapshot.h" network::server_manager::server_manager() { @@ -158,6 +159,30 @@ void network::manager::notify_scenario_loaded() client->send_ready(); } +void network::server_manager::publish_state() +{ + if (--state_countdown > 0) + return; + + state_countdown = STATE_INTERVAL_FRAMES; + + // most of the time only what changed goes out; every so often the whole thing does, + // so that anything a peer missed heals by itself + bool const full = ((state_updates++ % STATE_FULL_EVERY) == 0); + + auto const blob = take_snapshot(full); + if (blob.empty()) + return; + + snapshot msg; + msg.tick = Global.simulation_tick; + msg.mode = 1; // correction on top of a running simulation + msg.blob = blob; + + for (auto srv : servers) + srv->push_message(msg); +} + void network::manager::request_resync(uint64_t tick, uint64_t state_hash) { if (client) @@ -172,6 +197,7 @@ void network::manager::update() if (servers && simulation::is_ready) { servers->update_crews(); servers->publish_vehicle_list(); + servers->publish_state(); } if (client) diff --git a/network/manager.h b/network/manager.h index 414153bb..4ce057ed 100644 --- a/network/manager.h +++ b/network/manager.h @@ -21,6 +21,9 @@ namespace network void publish_vehicle_list(); // brings the world in line with the crew roster and announces every change void update_crews(); + // sends the peers what the world actually looks like. this is what keeps a client + // in line - the command stream alone cannot, because a client runs its own physics + void publish_state(); // 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); @@ -29,9 +32,16 @@ namespace network // how many frames may pass between two unconditional broadcasts of the roster static const int PUBLISH_INTERVAL_FRAMES = 120; + // how often the authoritative state goes out, and how often it goes out whole + // rather than as what changed since the last one + static const int STATE_INTERVAL_FRAMES = 10; + static const int STATE_FULL_EVERY = 60; + std::vector last_published_list; int publish_countdown = 0; int crew_publish_countdown = 0; + int state_countdown = 0; + int state_updates = 0; }; class manager diff --git a/network/message.cpp b/network/message.cpp index 98c78170..b525b80d 100644 --- a/network/message.cpp +++ b/network/message.cpp @@ -97,6 +97,7 @@ void network::client_ready::deserialize(std::istream &stream) void network::snapshot::serialize(std::ostream &stream) const { sn_utils::ls_uint64(stream, tick); + sn_utils::s_uint8(stream, mode); sn_utils::ls_uint32(stream, (uint32_t)blob.size()); stream.write(blob.data(), blob.size()); } @@ -104,6 +105,7 @@ void network::snapshot::serialize(std::ostream &stream) const void network::snapshot::deserialize(std::istream &stream) { tick = sn_utils::ld_uint64(stream); + mode = sn_utils::d_uint8(stream); uint32_t const size = sn_utils::ld_uint32(stream); blob.assign(size, '\0'); diff --git a/network/message.h b/network/message.h index 315218f4..97ee58ed 100644 --- a/network/message.h +++ b/network/message.h @@ -113,6 +113,9 @@ struct snapshot : public message snapshot() : message(SNAPSHOT) {} uint64_t tick{ 0 }; + // 0 - the peer is joining and takes the world as given + // 1 - routine correction on top of a simulation that is already running + uint8_t mode{ 0 }; std::string blob; virtual void serialize(std::ostream &stream) const override; diff --git a/network/network.cpp b/network/network.cpp index e6bcbf7a..266c7be5 100644 --- a/network/network.cpp +++ b/network/network.cpp @@ -205,7 +205,8 @@ void network::server::handle_message(std::shared_ptr conn, const mes snapshot reply; reply.tick = Global.simulation_tick; - reply.blob = take_snapshot(); + reply.mode = 0; // joining: take the world as it is + reply.blob = take_snapshot(true); conn->send_message(reply); // from here on the peer receives the live stream; there is no backlog to catch up on @@ -225,7 +226,8 @@ void network::server::handle_message(std::shared_ptr conn, const mes snapshot reply; reply.tick = Global.simulation_tick; - reply.blob = take_snapshot(); + reply.mode = 0; + reply.blob = take_snapshot(true); conn->send_message(reply); } else if (msg.type == message::CLAIM_VEHICLE) { @@ -256,6 +258,29 @@ void network::server::handle_message(std::shared_ptr conn, const mes for (auto const &kv : cmd.commands) { for (command_data const &data : kv.second) { + if (data.command == user_command::entervehicle) { + // the peer walked into a cab; that is a request for a seat, exactly + // like the lobby button, and the crew registry is what decides on it + NetworkEntityId entity { ENTITY_NONE }; + auto const result = claim_by_name(conn->peer_id, data.payload, entity); + + if (result == claim_result::granted || result == claim_result::already_member) { + claim_granted reply; + reply.entity_id = entity; + conn->send_message(reply); + } + else { + WriteLog("net: peer " + std::to_string(conn->peer_id) + " cannot take " + + data.payload + ": " + describe(result), logtype::net); + + claim_denied reply; + reply.entity_id = entity; + reply.reason = describe(result); + conn->send_message(reply); + } + continue; + } + auto const verdict = validate_command(conn->peer_id, data.command, kv.first); if (verdict != command_verdict::accepted) { report_rejected_command(conn->peer_id, data.command, verdict); @@ -393,6 +418,8 @@ void network::client::send_claim(NetworkEntityId entity_id) claim_vehicle msg; msg.entity_id = entity_id; conn->send_message(msg); + + WriteLog("net: asking for a seat on vehicle " + std::to_string(entity_id) + " (" + Entities.name_of(entity_id) + ")", logtype::net); } void network::client::send_leave(NetworkEntityId entity_id) @@ -471,8 +498,16 @@ void network::client::handle_message(std::shared_ptr conn, const mes if (msg.type == message::SNAPSHOT) { const auto& cmd = dynamic_cast(msg); - if (apply_snapshot(cmd.blob)) { + auto const mode = (cmd.mode == 0 ? snapshot_mode::join : snapshot_mode::correction); + auto const result = apply_snapshot(cmd.blob, mode); + + if (!result.ok) { + ErrorLog("net: could not apply the state handed to us", logtype::net); + Global.network_status = "The server sent a snapshot this build cannot read"; + } + else if (mode == snapshot_mode::join) { Global.network_snapshot_applied = true; + Global.network_position_error = (float)result.worst_position_error; // the roster may say we are on a crew already - after a reconnect, or because // the claim was granted while we were still loading. walk into that cab @@ -484,13 +519,30 @@ void network::client::handle_message(std::shared_ptr conn, const mes } } else { - ErrorLog("net: could not apply the snapshot handed to us", logtype::net); - Global.network_status = "The server sent a snapshot this build cannot read"; + Global.network_position_error = (float)result.worst_position_error; + + // the routine correction is what keeps us in line; a full resync is only worth + // asking for when even that is not catching up + if (result.worst_position_error > RESYNC_POSITION_ERROR) { + if (++bad_corrections >= RESYNC_BAD_CORRECTIONS) { + bad_corrections = 0; + WriteLog("net: " + std::to_string((int)result.worst_position_error) + + " m out of place after a correction, asking for a full resync", logtype::net); + send_resync_request(Global.simulation_tick, 0); + } + } + else { + bad_corrections = 0; + } } } if (msg.type == message::VEHICLE_LIST) { const auto& cmd = dynamic_cast(msg); + + if (Entities.empty() && !cmd.vehicles.empty()) + WriteLog("net: vehicle roster received, " + std::to_string(cmd.vehicles.size()) + " vehicles to choose from", logtype::net); + Entities.adopt(cmd.vehicles); } diff --git a/network/network.h b/network/network.h index 1d3603dc..e29c9360 100644 --- a/network/network.h +++ b/network/network.h @@ -110,6 +110,12 @@ namespace network std::queue delta_queue; + // how far out of place a correction found us, in a row. a full resync is only + // worth asking for when the routine stream is not catching up on its own + int bad_corrections = 0; + static constexpr double RESYNC_POSITION_ERROR = 25.0; + static constexpr int RESYNC_BAD_CORRECTIONS = 5; + float last_target = 20.0f; float jitteriness = 1.0f; float consume_counter = 0.0f; diff --git a/network/session.cpp b/network/session.cpp index 98e89d85..1fe15c82 100644 --- a/network/session.cpp +++ b/network/session.cpp @@ -86,6 +86,24 @@ command_verdict validate_command(PeerId Peer, user_command Command, uint32_t Rec return command_verdict::unsupported_target; } +claim_result claim_by_name(PeerId Peer, std::string const &Vehicle, NetworkEntityId &Entity) +{ + Entity = Entities.id_of(Vehicle); + if (Entity == ENTITY_NONE) + return claim_result::unknown_vehicle; + + auto const previous = Crews.vehicle_of(Peer); + auto const result = Crews.claim(Peer, Entity); + + if ((result == claim_result::granted) && (previous != ENTITY_NONE) && (previous != Entity)) + { + // nobody works two vehicles at once + Crews.leave(Peer, previous); + } + + return result; +} + void filter_commands(PeerId Peer, command_queue::commands_map &Commands) { for (auto it = Commands.begin(); it != Commands.end();) @@ -94,6 +112,27 @@ void filter_commands(PeerId Peer, command_queue::commands_map &Commands) for (auto command = sequence.begin(); command != sequence.end();) { + if (command->source == PEER_NONE) + { + // posted by the authority itself rather than by a person; it is already + // the decision, not a request for one + ++command; + continue; + } + + if (command->command == user_command::entervehicle) + { + // the local player walked into a cab. that is a crew request; the cab + // itself is built by the authority once the seat is granted + NetworkEntityId entity{ENTITY_NONE}; + auto const result = claim_by_name(Peer, command->payload, entity); + if (result != claim_result::granted && result != claim_result::already_member) + WriteLog("net: cannot take " + command->payload + ": " + describe(result), logtype::net); + + command = sequence.erase(command); + continue; + } + if (validate_command(Peer, command->command, it->first) != command_verdict::accepted) { command = sequence.erase(command); diff --git a/network/session.h b/network/session.h index 6b5ebee8..937aeb33 100644 --- a/network/session.h +++ b/network/session.h @@ -138,6 +138,11 @@ std::string describe(command_verdict Verdict); // simply holds every right - there is no separate path for the local participant command_verdict validate_command(PeerId Peer, user_command Command, uint32_t Recipient); +// walking into a cab with the in-game key is the same request as pressing the button in +// the lobby, so it goes through the crew registry rather than being treated as a command +// that reaches the whole world. returns the vehicle it resolved to, if any +claim_result claim_by_name(PeerId Peer, std::string const &Vehicle, NetworkEntityId &Entity); + // drops everything the peer is not allowed to ask for and stamps the rest with its // identity. used for the local participant as well, so that host input goes through the // very same authority layer as input arriving over the wire, only without the transport diff --git a/network/snapshot.cpp b/network/snapshot.cpp index d3268520..dbc4d65a 100644 --- a/network/snapshot.cpp +++ b/network/snapshot.cpp @@ -20,6 +20,8 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Logs.h" #include "vehicle/Driver.h" #include "vehicle/DynObj.h" +#include "world/Event.h" +#include "world/MemCell.h" #include "world/Track.h" namespace @@ -27,9 +29,29 @@ namespace uint32_t const SNAPSHOT_MAGIC{0x4e535545}; // 'EUSN' -// a vehicle is only put back on the rails when it really is somewhere else; nudging one -// that is already in place would only shake it about -double const REPOSITION_THRESHOLD{0.5}; +// how far a vehicle may sit from where the authority says it is before it gets put back. +// a joining peer is placed exactly; a running one is left to its own physics for small +// errors, because nudging a vehicle every fraction of a second is worse than the error +double const REPOSITION_TOLERANCE_JOIN{0.5}; +double const REPOSITION_TOLERANCE_CORRECTION{2.5}; + +// --------------------------------------------------------------------------- +// what the authority last sent, so that a routine correction only carries changes + +std::unordered_map g_lastvehicles; +std::unordered_map g_lastmemcells; +std::unordered_map g_lastswitches; + +uint64_t digest_of(std::string const &Text) +{ + uint64_t hash{14695981039346656037ull}; + for (char const character : Text) + { + hash ^= (uint64_t)(unsigned char)character; + hash *= 1099511628211ull; + } + return hash; +} void write_chunk(std::ostream &Stream, network::snapshot_chunk const Id, std::string const &Body) { @@ -41,7 +63,7 @@ void write_chunk(std::ostream &Stream, network::snapshot_chunk const Id, std::st // --------------------------------------------------------------------------- // session -std::string write_session() +std::string write_session(bool const Full) { std::ostringstream body; @@ -57,15 +79,20 @@ std::string write_session() sn_utils::ls_float64(body, Global.Overcast); sn_utils::ls_float64(body, Global.AirTemperature); - // the engine drives gameplay randomness, so a joining peer has to pick it up as it is - std::ostringstream engine; - engine << Global.random_engine; - sn_utils::s_str(body, engine.str()); + // the engine drives gameplay randomness, so a joining peer has to pick it up as it is. + // it is several kilobytes though, far too much to repeat in every routine correction + sn_utils::s_bool(body, Full); + if (Full) + { + std::ostringstream engine; + engine << Global.random_engine; + sn_utils::s_str(body, engine.str()); + } return body.str(); } -void read_session(std::istream &Stream) +void read_session(std::istream &Stream, network::snapshot_mode const Mode) { auto const tick = sn_utils::ld_uint64(Stream); auto const timestamp = sn_utils::ld_int64(Stream); @@ -78,7 +105,10 @@ void read_session(std::istream &Stream) auto const overcast = sn_utils::ld_float64(Stream); auto const temperature = sn_utils::ld_float64(Stream); - auto const enginestate = sn_utils::d_str(Stream); + bool const hasengine = sn_utils::d_bool(Stream); + std::string enginestate; + if (hasengine) + enginestate = sn_utils::d_str(Stream); Global.simulation_tick = tick; Global.starting_timestamp = timestamp; @@ -96,14 +126,282 @@ void read_session(std::istream &Stream) std::istringstream engine(enginestate); engine >> Global.random_engine; } + + (void)Mode; } // --------------------------------------------------------------------------- // vehicles -std::string write_vehicles() +// everything about one vehicle the rest of the session needs to agree on +struct vehicle_state +{ + std::string name; + std::string track; + double translation{0.0}; + uint8_t axlefirst{0}; + uint8_t direction{0}; + glm::dvec3 position{}; + + double velocity{0.0}; + double velocitykmh{0.0}; + int32_t diractive{0}; + int32_t mainctrl{0}; + int32_t scndctrl{0}; + int32_t brakectrl{0}; + double localbrake{0.0}; + + double pipepress{0.0}; + double brakepress{0.0}; + double scndpipepress{0.0}; + double eqvtpipepress{0.0}; + double compressor{0.0}; + double compressedvolume{0.0}; + + // switch positions and permissions - the inputs of the vehicle, not its outputs. + // getting these right lets the receiving peer derive the rest for itself + bool mains{false}; + bool battery{false}; + bool converterallow{false}; + bool compressorallow{false}; + int32_t cabactive{0}; + int32_t caboccupied{0}; + std::array pantenabled{{false, false}}; + std::array pantdisabled{{false, false}}; + std::array pantactive{{false, false}}; + int32_t lights[2]{0, 0}; + // door open/close intent, one entry per side; the local model derives the rest + std::array dooropen{{false, false}}; + std::array doorpermit{{false, false}}; + bool aiactive{false}; +}; + +void serialize_vehicle(std::ostream &Stream, vehicle_state const &State) +{ + sn_utils::s_str(Stream, State.name); + sn_utils::s_str(Stream, State.track); + sn_utils::ls_float64(Stream, State.translation); + sn_utils::s_uint8(Stream, State.axlefirst); + sn_utils::s_uint8(Stream, State.direction); + sn_utils::s_dvec3(Stream, State.position); + + sn_utils::ls_float64(Stream, State.velocity); + sn_utils::ls_float64(Stream, State.velocitykmh); + sn_utils::ls_int32(Stream, State.diractive); + sn_utils::ls_int32(Stream, State.mainctrl); + sn_utils::ls_int32(Stream, State.scndctrl); + sn_utils::ls_int32(Stream, State.brakectrl); + sn_utils::ls_float64(Stream, State.localbrake); + + sn_utils::ls_float64(Stream, State.pipepress); + sn_utils::ls_float64(Stream, State.brakepress); + sn_utils::ls_float64(Stream, State.scndpipepress); + sn_utils::ls_float64(Stream, State.eqvtpipepress); + sn_utils::ls_float64(Stream, State.compressor); + sn_utils::ls_float64(Stream, State.compressedvolume); + + sn_utils::s_bool(Stream, State.mains); + sn_utils::s_bool(Stream, State.battery); + sn_utils::s_bool(Stream, State.converterallow); + sn_utils::s_bool(Stream, State.compressorallow); + sn_utils::ls_int32(Stream, State.cabactive); + sn_utils::ls_int32(Stream, State.caboccupied); + for (int i = 0; i < 2; ++i) + { + sn_utils::s_bool(Stream, State.pantenabled[i]); + sn_utils::s_bool(Stream, State.pantdisabled[i]); + sn_utils::s_bool(Stream, State.pantactive[i]); + } + sn_utils::ls_int32(Stream, State.lights[0]); + sn_utils::ls_int32(Stream, State.lights[1]); + for (int i = 0; i < 2; ++i) + { + sn_utils::s_bool(Stream, State.dooropen[i]); + sn_utils::s_bool(Stream, State.doorpermit[i]); + } + sn_utils::s_bool(Stream, State.aiactive); +} + +vehicle_state deserialize_vehicle(std::istream &Stream) +{ + vehicle_state state; + + state.name = sn_utils::d_str(Stream); + state.track = sn_utils::d_str(Stream); + state.translation = sn_utils::ld_float64(Stream); + state.axlefirst = sn_utils::d_uint8(Stream); + state.direction = sn_utils::d_uint8(Stream); + state.position = sn_utils::d_dvec3(Stream); + + state.velocity = sn_utils::ld_float64(Stream); + state.velocitykmh = sn_utils::ld_float64(Stream); + state.diractive = sn_utils::ld_int32(Stream); + state.mainctrl = sn_utils::ld_int32(Stream); + state.scndctrl = sn_utils::ld_int32(Stream); + state.brakectrl = sn_utils::ld_int32(Stream); + state.localbrake = sn_utils::ld_float64(Stream); + + state.pipepress = sn_utils::ld_float64(Stream); + state.brakepress = sn_utils::ld_float64(Stream); + state.scndpipepress = sn_utils::ld_float64(Stream); + state.eqvtpipepress = sn_utils::ld_float64(Stream); + state.compressor = sn_utils::ld_float64(Stream); + state.compressedvolume = sn_utils::ld_float64(Stream); + + state.mains = sn_utils::d_bool(Stream); + state.battery = sn_utils::d_bool(Stream); + state.converterallow = sn_utils::d_bool(Stream); + state.compressorallow = sn_utils::d_bool(Stream); + state.cabactive = sn_utils::ld_int32(Stream); + state.caboccupied = sn_utils::ld_int32(Stream); + for (int i = 0; i < 2; ++i) + { + state.pantenabled[i] = sn_utils::d_bool(Stream); + state.pantdisabled[i] = sn_utils::d_bool(Stream); + state.pantactive[i] = sn_utils::d_bool(Stream); + } + state.lights[0] = sn_utils::ld_int32(Stream); + state.lights[1] = sn_utils::ld_int32(Stream); + for (int i = 0; i < 2; ++i) + { + state.dooropen[i] = sn_utils::d_bool(Stream); + state.doorpermit[i] = sn_utils::d_bool(Stream); + } + state.aiactive = sn_utils::d_bool(Stream); + + return state; +} + +vehicle_state read_from(TDynamicObject const &Vehicle) +{ + auto const &mover = *Vehicle.MoverParameters; + auto const *track = Vehicle.RaTrackGet(); + + vehicle_state state; + state.name = Vehicle.name(); + state.track = (track != nullptr ? track->name() : std::string()); + state.translation = Vehicle.RaTranslationGet(); + state.axlefirst = (uint8_t)(Vehicle.iAxleFirst ? 1 : 0); + state.direction = (uint8_t)(Vehicle.iDirection ? 1 : 0); + state.position = Vehicle.GetPosition(); + + state.velocity = mover.V; + state.velocitykmh = mover.Vel; + state.diractive = mover.DirActive; + state.mainctrl = mover.MainCtrlPos; + state.scndctrl = mover.ScndCtrlPos; + state.brakectrl = mover.BrakeCtrlPos; + state.localbrake = mover.LocalBrakePosA; + + state.pipepress = mover.PipePress; + state.brakepress = mover.BrakePress; + state.scndpipepress = mover.ScndPipePress; + state.eqvtpipepress = mover.EqvtPipePress; + state.compressor = mover.Compressor; + state.compressedvolume = mover.CompressedVolume; + + state.mains = mover.Mains; + state.battery = mover.Battery; + state.converterallow = mover.ConverterAllow; + state.compressorallow = mover.CompressorAllow; + state.cabactive = mover.CabActive; + state.caboccupied = mover.CabOccupied; + for (int i = 0; i < 2; ++i) + { + state.pantenabled[i] = mover.Pantographs[i].valve.is_enabled; + state.pantdisabled[i] = mover.Pantographs[i].valve.is_disabled; + state.pantactive[i] = mover.Pantographs[i].is_active; + } + state.lights[0] = mover.iLights[0]; + state.lights[1] = mover.iLights[1]; + for (int i = 0; i < 2; ++i) + { + state.dooropen[i] = mover.Doors.instances[i].is_open; + state.doorpermit[i] = mover.Doors.instances[i].open_permit; + } + state.aiactive = (Vehicle.Mechanik != nullptr && Vehicle.Mechanik->AIControllFlag); + + return state; +} + +// applies everything except where the vehicle is; position is dealt with per consist, +// because moving one vehicle of a coupled set on its own tears the couplers apart +void apply_controls(TDynamicObject &Vehicle, vehicle_state const &State) +{ + auto &mover = *Vehicle.MoverParameters; + + mover.V = State.velocity; + mover.Vel = State.velocitykmh; + mover.DirActive = State.diractive; + mover.MainCtrlPos = State.mainctrl; + mover.ScndCtrlPos = State.scndctrl; + mover.BrakeCtrlPos = State.brakectrl; + mover.LocalBrakePosA = State.localbrake; + + mover.PipePress = State.pipepress; + mover.BrakePress = State.brakepress; + mover.ScndPipePress = State.scndpipepress; + mover.EqvtPipePress = State.eqvtpipepress; + mover.Compressor = State.compressor; + mover.CompressedVolume = State.compressedvolume; + + // the appliances go through the vehicle's own switches wherever it has them, so that + // whatever they drag along stays consistent. range local: every vehicle carries its + // own state in the update, there is no need to push it down the consist twice + if (mover.Battery != State.battery) + mover.BatterySwitch(State.battery, range_t::local); + if (mover.Mains != State.mains) + mover.MainSwitch(State.mains, range_t::local); + if (mover.ConverterAllow != State.converterallow) + mover.ConverterSwitch(State.converterallow, range_t::local); + if (mover.CompressorAllow != State.compressorallow) + mover.CompressorSwitch(State.compressorallow, range_t::local); + + mover.CabActive = State.cabactive; + mover.CabOccupied = State.caboccupied; + + for (int i = 0; i < 2; ++i) + { + mover.Pantographs[i].valve.is_enabled = State.pantenabled[i]; + mover.Pantographs[i].valve.is_disabled = State.pantdisabled[i]; + mover.Pantographs[i].is_active = State.pantactive[i]; + } + + mover.iLights[0] = State.lights[0]; + mover.iLights[1] = State.lights[1]; + + for (int i = 0; i < 2; ++i) + { + auto const side = (i == 0 ? side::right : side::left); + if (mover.Doors.instances[i].open_permit != State.doorpermit[i]) + mover.PermitDoors(side, State.doorpermit[i], range_t::local); + if (mover.Doors.instances[i].is_open != State.dooropen[i]) + mover.OperateDoors(side, State.dooropen[i], range_t::local); + } + + if (Vehicle.Mechanik != nullptr && Vehicle.Mechanik->AIControllFlag != State.aiactive) + Vehicle.Mechanik->TakeControl(State.aiactive); +} + +// puts a vehicle back where the authority says it is. place_on_track() wants the distance +// of the vehicle's nose along the track, while what travels in the update is the offset of +// its leading bogie; this inverts the arithmetic that function itself does +void reposition(TDynamicObject &Vehicle, vehicle_state const &State) +{ + TTrack *track = (State.track.empty() ? nullptr : simulation::Paths.find(State.track)); + if (track == nullptr) + return; + + double const half = Vehicle.fAxleDist * 0.5; + double const axleoffset = (State.axlefirst ? -half : half); + double const sign = (State.direction ? 1.0 : -1.0); + double const nose = (State.translation - axleoffset) * sign + 0.5 * Vehicle.MoverParameters->Dim.L; + + Vehicle.place_on_track(track, nose, false); +} + +std::string write_vehicles(bool const Full) { - std::ostringstream body; std::ostringstream entries; uint32_t count{0}; @@ -112,33 +410,35 @@ std::string write_vehicles() if (vehicle == nullptr || vehicle->MoverParameters == nullptr) continue; - auto const &mover = *vehicle->MoverParameters; - auto const *track = vehicle->RaTrackGet(); + auto const state = read_from(*vehicle); - sn_utils::s_str(entries, vehicle->name()); - sn_utils::s_str(entries, track != nullptr ? track->name() : std::string()); - sn_utils::ls_float64(entries, vehicle->RaTranslationGet()); - sn_utils::s_uint8(entries, (uint8_t)(vehicle->iAxleFirst ? 1 : 0)); - sn_utils::s_uint8(entries, (uint8_t)(vehicle->iDirection ? 1 : 0)); - sn_utils::s_dvec3(entries, vehicle->GetPosition()); + std::ostringstream packed; + serialize_vehicle(packed, state); + auto const record = packed.str(); - sn_utils::ls_float64(entries, mover.V); - sn_utils::ls_float64(entries, mover.Vel); - sn_utils::ls_int32(entries, mover.DirActive); - sn_utils::ls_int32(entries, mover.MainCtrlPos); - sn_utils::ls_int32(entries, mover.ScndCtrlPos); - sn_utils::ls_int32(entries, mover.BrakeCtrlPos); - sn_utils::ls_float64(entries, mover.LocalBrakePosA); - sn_utils::ls_float64(entries, mover.PipePress); - sn_utils::ls_float64(entries, mover.BrakePress); - sn_utils::ls_float64(entries, mover.ScndPipePress); - sn_utils::ls_float64(entries, mover.Compressor); - sn_utils::ls_float64(entries, mover.CompressedVolume); - sn_utils::s_bool(entries, vehicle->Mechanik != nullptr && vehicle->Mechanik->AIControllFlag); + if (!Full) + { + // a vehicle that is standing still and has not been touched needs no update + auto const digest = digest_of(record); + auto const previous = g_lastvehicles.find(state.name); + if (previous != g_lastvehicles.end() && previous->second == digest) + continue; + g_lastvehicles[state.name] = digest; + } + else + { + g_lastvehicles[state.name] = digest_of(record); + } + + entries.write(record.data(), record.size()); ++count; } + if (count == 0) + return std::string(); + + std::ostringstream body; sn_utils::ls_uint32(body, count); auto const packed = entries.str(); body.write(packed.data(), packed.size()); @@ -146,87 +446,98 @@ std::string write_vehicles() return body.str(); } -void read_vehicles(std::istream &Stream) +void read_vehicles(std::istream &Stream, network::snapshot_mode const Mode, network::snapshot_result &Result) { auto const count = sn_utils::ld_uint32(Stream); - uint32_t repositioned{0}; - uint32_t missing{0}; + double const tolerance = (Mode == network::snapshot_mode::join ? REPOSITION_TOLERANCE_JOIN : REPOSITION_TOLERANCE_CORRECTION); + + std::unordered_map states; + states.reserve(count); for (uint32_t i = 0; i < count; ++i) { - auto const name = sn_utils::d_str(Stream); - auto const trackname = sn_utils::d_str(Stream); - auto const translation = sn_utils::ld_float64(Stream); - auto const axlefirst = sn_utils::d_uint8(Stream); - auto const direction = sn_utils::d_uint8(Stream); - auto const position = sn_utils::d_dvec3(Stream); + auto const state = deserialize_vehicle(Stream); - auto const velocity = sn_utils::ld_float64(Stream); - auto const velocitykmh = sn_utils::ld_float64(Stream); - auto const diractive = sn_utils::ld_int32(Stream); - auto const mainctrl = sn_utils::ld_int32(Stream); - auto const scndctrl = sn_utils::ld_int32(Stream); - auto const brakectrl = sn_utils::ld_int32(Stream); - auto const localbrake = sn_utils::ld_float64(Stream); - auto const pipepress = sn_utils::ld_float64(Stream); - auto const brakepress = sn_utils::ld_float64(Stream); - auto const scndpipepress = sn_utils::ld_float64(Stream); - auto const compressor = sn_utils::ld_float64(Stream); - auto const compressedvolume = sn_utils::ld_float64(Stream); - auto const aiactive = sn_utils::d_bool(Stream); - - TDynamicObject *vehicle = simulation::Vehicles.find(name); + TDynamicObject *vehicle = simulation::Vehicles.find(state.name); if (vehicle == nullptr || vehicle->MoverParameters == nullptr) - { - ++missing; - continue; - } - - auto &mover = *vehicle->MoverParameters; - - mover.V = velocity; - mover.Vel = velocitykmh; - mover.DirActive = diractive; - mover.MainCtrlPos = mainctrl; - mover.ScndCtrlPos = scndctrl; - mover.BrakeCtrlPos = brakectrl; - mover.LocalBrakePosA = localbrake; - mover.PipePress = pipepress; - mover.BrakePress = brakepress; - mover.ScndPipePress = scndpipepress; - mover.Compressor = compressor; - mover.CompressedVolume = compressedvolume; - - if (vehicle->Mechanik != nullptr && vehicle->Mechanik->AIControllFlag != aiactive) - { - vehicle->Mechanik->TakeControl(aiactive); - } - - if (glm::length(vehicle->GetPosition() - position) <= REPOSITION_THRESHOLD) - { - // close enough already; leaving it alone avoids shaking a vehicle that is fine - continue; - } - - TTrack *track = (trackname.empty() ? nullptr : simulation::Paths.find(trackname)); - if (track == nullptr) continue; - // place_on_track() takes the distance of the vehicle's nose, while what we recorded - // is the offset of its leading bogie. this inverts the arithmetic the placement - // itself does, so the two stay in step if that code ever changes - double const half = vehicle->fAxleDist * 0.5; - double const axleoffset = (axlefirst ? -half : half); - double const sign = (direction ? 1.0 : -1.0); - double const nose = (translation - axleoffset) * sign + 0.5 * mover.Dim.L; + apply_controls(*vehicle, state); + states.emplace(vehicle, state); - vehicle->place_on_track(track, nose, false); - ++repositioned; + Result.worst_position_error = std::max(Result.worst_position_error, glm::length(vehicle->GetPosition() - state.position)); } - WriteLog("net: snapshot restored " + std::to_string(count) + " vehicles, " + std::to_string(repositioned) + " of them put back on the rails" + - (missing > 0 ? ", " + std::to_string(missing) + " unknown here" : ""), - logtype::net); + Result.vehicles = (uint32_t)states.size(); + + // a coupled set is put back as a whole or not at all: correcting one vehicle of a + // consist while its neighbour stays put is what stretches a coupler until it breaks + std::unordered_set visited; + + for (auto const &pair : states) + { + if (visited.count(pair.first) > 0) + continue; + + // the cap is only there so that a malformed consist cannot spin us forever + int const CONSIST_LIMIT{256}; + + std::vector group; + TDynamicObject *front = pair.first; + for (int step = 0; step < CONSIST_LIMIT; ++step) + { + TDynamicObject *previous = front->Prev(); + if (previous == nullptr || previous == pair.first || visited.count(previous) > 0) + break; + front = previous; + } + + TDynamicObject *member = front; + for (int step = 0; step < CONSIST_LIMIT && member != nullptr; ++step, member = member->Next()) + { + if (visited.count(member) > 0) + break; + visited.emplace(member); + group.emplace_back(member); + } + + bool wanted{false}; + for (TDynamicObject *member : group) + { + auto const known = states.find(member); + if (known == states.end()) + continue; + if (glm::length(member->GetPosition() - known->second.position) > tolerance) + { + wanted = true; + break; + } + } + + if (!wanted) + continue; + + for (TDynamicObject *member : group) + { + auto const known = states.find(member); + if (known == states.end()) + { + // part of the consist is not in this update, so the set cannot be placed + // as a whole; leaving it alone beats tearing it in half + wanted = false; + break; + } + } + + if (!wanted) + continue; + + for (TDynamicObject *member : group) + { + reposition(*member, states.at(member)); + ++Result.repositioned; + } + } } // --------------------------------------------------------------------------- @@ -234,7 +545,6 @@ void read_vehicles(std::istream &Stream) std::string write_crews() { - std::ostringstream body; std::ostringstream entries; uint32_t count{0}; @@ -252,6 +562,7 @@ std::string write_crews() ++count; } + std::ostringstream body; sn_utils::ls_uint32(body, count); auto const packed = entries.str(); body.write(packed.data(), packed.size()); @@ -277,42 +588,204 @@ void read_crews(std::istream &Stream) } } +// --------------------------------------------------------------------------- +// memory cells - this is what the signalling reads, so without them a client sees +// semaphores frozen at whatever the scenario started with + +std::string write_memcells(bool const Full) +{ + std::ostringstream entries; + uint32_t count{0}; + + for (TMemCell const *cell : simulation::Memory.sequence()) + { + if (cell == nullptr || cell->name().empty()) + continue; + + std::ostringstream packed; + sn_utils::s_str(packed, cell->name()); + sn_utils::s_str(packed, cell->Text()); + sn_utils::ls_float64(packed, cell->Value1()); + sn_utils::ls_float64(packed, cell->Value2()); + auto const record = packed.str(); + + auto const digest = digest_of(record); + if (!Full) + { + auto const previous = g_lastmemcells.find(cell->name()); + if (previous != g_lastmemcells.end() && previous->second == digest) + continue; + } + g_lastmemcells[cell->name()] = digest; + + entries.write(record.data(), record.size()); + ++count; + } + + if (count == 0) + return std::string(); + + std::ostringstream body; + sn_utils::ls_uint32(body, count); + auto const packed = entries.str(); + body.write(packed.data(), packed.size()); + + return body.str(); +} + +void read_memcells(std::istream &Stream) +{ + auto const count = sn_utils::ld_uint32(Stream); + + for (uint32_t i = 0; i < count; ++i) + { + auto const name = sn_utils::d_str(Stream); + auto const text = sn_utils::d_str(Stream); + auto const value1 = sn_utils::ld_float64(Stream); + auto const value2 = sn_utils::ld_float64(Stream); + + TMemCell *cell = simulation::Memory.find(name); + if (cell == nullptr) + continue; + + if (cell->Text() == text && cell->Value1() == value1 && cell->Value2() == value2) + continue; + + cell->UpdateValues(text, value1, value2, basic_event::flags::text | basic_event::flags::value1 | basic_event::flags::value2); + } +} + +// --------------------------------------------------------------------------- +// switches + +std::string write_switches(bool const Full) +{ + std::ostringstream entries; + uint32_t count{0}; + + for (TTrack *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->name().empty()) + continue; + + int const state = track->GetSwitchState(); + if (state < 0) + continue; + + if (!Full) + { + auto const previous = g_lastswitches.find(track->name()); + if (previous != g_lastswitches.end() && previous->second == state) + continue; + } + g_lastswitches[track->name()] = state; + + sn_utils::s_str(entries, track->name()); + sn_utils::ls_int32(entries, state); + ++count; + } + + if (count == 0) + return std::string(); + + std::ostringstream body; + sn_utils::ls_uint32(body, count); + auto const packed = entries.str(); + body.write(packed.data(), packed.size()); + + return body.str(); +} + +void read_switches(std::istream &Stream) +{ + auto const count = sn_utils::ld_uint32(Stream); + + for (uint32_t i = 0; i < count; ++i) + { + auto const name = sn_utils::d_str(Stream); + auto const state = sn_utils::ld_int32(Stream); + + TTrack *track = simulation::Paths.find(name); + if (track == nullptr) + continue; + + if (track->GetSwitchState() != state) + track->Switch(state); + } +} + } // namespace -std::string network::take_snapshot() +void network::reset_snapshot_history() { - std::ostringstream stream; + g_lastvehicles.clear(); + g_lastmemcells.clear(); + g_lastswitches.clear(); +} +std::string network::take_snapshot(bool const Full) +{ + auto const session = write_session(Full); + auto const vehicles = write_vehicles(Full); + auto const crews = write_crews(); + auto const memcells = write_memcells(Full); + auto const switches = write_switches(Full); + + if (!Full && vehicles.empty() && memcells.empty() && switches.empty()) + { + // nothing moved and nobody touched anything; the clock alone is not worth a packet + return std::string(); + } + + uint32_t chunks{1}; // the session section always goes along + if (!vehicles.empty()) + ++chunks; + ++chunks; // crews + if (!memcells.empty()) + ++chunks; + if (!switches.empty()) + ++chunks; + + std::ostringstream stream; sn_utils::ls_uint32(stream, SNAPSHOT_MAGIC); sn_utils::ls_uint32(stream, SNAPSHOT_VERSION); sn_utils::ls_uint64(stream, Global.simulation_tick); - sn_utils::ls_uint32(stream, 3); // number of chunks that follow + sn_utils::ls_uint32(stream, chunks); - write_chunk(stream, SNAPSHOT_SESSION, write_session()); - write_chunk(stream, SNAPSHOT_VEHICLES, write_vehicles()); - write_chunk(stream, SNAPSHOT_CREWS, write_crews()); + write_chunk(stream, SNAPSHOT_SESSION, session); + if (!vehicles.empty()) + write_chunk(stream, SNAPSHOT_VEHICLES, vehicles); + write_chunk(stream, SNAPSHOT_CREWS, crews); + if (!memcells.empty()) + write_chunk(stream, SNAPSHOT_MEMCELLS, memcells); + if (!switches.empty()) + write_chunk(stream, SNAPSHOT_SWITCHES, switches); auto const blob = stream.str(); - WriteLog("net: snapshot taken at tick " + std::to_string(Global.simulation_tick) + ", " + std::to_string(blob.size()) + " bytes", logtype::net); + + if (Full) + WriteLog("net: snapshot taken at tick " + std::to_string(Global.simulation_tick) + ", " + std::to_string(blob.size()) + " bytes", logtype::net); return blob; } -bool network::apply_snapshot(std::string const &Blob) +network::snapshot_result network::apply_snapshot(std::string const &Blob, snapshot_mode const Mode) { + snapshot_result result; + std::istringstream stream(Blob); if (sn_utils::ld_uint32(stream) != SNAPSHOT_MAGIC) { ErrorLog("net: snapshot rejected, not a snapshot", logtype::net); - return false; + return result; } auto const version = sn_utils::ld_uint32(stream); if (version != SNAPSHOT_VERSION) { ErrorLog("net: snapshot rejected, version " + std::to_string(version) + " but this build speaks " + std::to_string(SNAPSHOT_VERSION), logtype::net); - return false; + return result; } auto const tick = sn_utils::ld_uint64(stream); @@ -323,7 +796,7 @@ bool network::apply_snapshot(std::string const &Blob) if (!stream.good()) { ErrorLog("net: snapshot truncated", logtype::net); - return false; + return result; } auto const id = sn_utils::ld_uint16(stream); @@ -337,23 +810,35 @@ bool network::apply_snapshot(std::string const &Blob) switch (id) { case SNAPSHOT_SESSION: - read_session(chunk); + read_session(chunk, Mode); break; case SNAPSHOT_VEHICLES: - read_vehicles(chunk); + read_vehicles(chunk, Mode, result); break; case SNAPSHOT_CREWS: read_crews(chunk); break; + case SNAPSHOT_MEMCELLS: + read_memcells(chunk); + break; + case SNAPSHOT_SWITCHES: + read_switches(chunk); + break; default: // a section this build knows nothing about; its length is right there, so it // costs nothing to walk past it - WriteLog("net: snapshot section " + std::to_string(id) + " skipped, unknown to this build", logtype::net); break; } } - WriteLog("net: snapshot applied, now at tick " + std::to_string(tick), logtype::net); + result.ok = true; - return true; + if (Mode == snapshot_mode::join) + { + WriteLog("net: snapshot applied at tick " + std::to_string(tick) + ", " + std::to_string(result.vehicles) + " vehicles, " + + std::to_string(result.repositioned) + " put back on the rails", + logtype::net); + } + + return result; } diff --git a/network/snapshot.h b/network/snapshot.h index 07776a4f..1d7b0f3e 100644 --- a/network/snapshot.h +++ b/network/snapshot.h @@ -16,27 +16,54 @@ namespace network { // layout of the snapshot container. a reader refuses a blob it does not understand -constexpr uint32_t SNAPSHOT_VERSION = 1; +constexpr uint32_t SNAPSHOT_VERSION = 2; // 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 // still read a newer snapshot and simply ignores what it has no use for enum snapshot_chunk : uint16_t { - // tick, clock, weather and the state of the random engine + // tick, clock, weather and (in a full snapshot) the state of the random engine SNAPSHOT_SESSION = 1, - // where every vehicle is and what its controls and brakes are doing + // where every vehicle is, what its controls and appliances are set to, and what its + // brakes are doing SNAPSHOT_VEHICLES = 2, // who is working which vehicle - SNAPSHOT_CREWS = 3 - // candidates for the next versions: switches, memcells, the event queue, signals + SNAPSHOT_CREWS = 3, + // contents of the memory cells - this is what signalling reads + SNAPSHOT_MEMCELLS = 4, + // which way the switches are thrown + SNAPSHOT_SWITCHES = 5 }; -// packs the current state of the world. only ever called on the authority -std::string take_snapshot(); +// what a peer is meant to do with the blob it received +enum class snapshot_mode +{ + // the peer is coming in cold and takes the world as it is given + join, + // routine correction on top of a simulation that is already running + correction +}; -// restores a snapshot handed to us by the authority. returns false when the blob cannot -// be used at all - a version we do not know, or damaged content -bool apply_snapshot(std::string const &Blob); +struct snapshot_result +{ + bool ok{false}; + // how far the worst vehicle was from where the authority says it is + double worst_position_error{0.0}; + uint32_t vehicles{0}; + uint32_t repositioned{0}; +}; + +// packs the state of the world. only ever called on the authority. +// a full snapshot carries everything and is what a joining peer gets; otherwise only what +// has changed since the last one goes out, which is what keeps the routine correction +// stream small +std::string take_snapshot(bool Full); + +// restores state handed to us by the authority +snapshot_result apply_snapshot(std::string const &Blob, snapshot_mode Mode); + +// forgets what was last sent, so that the next delta comes out complete +void reset_snapshot_history(); } // namespace network diff --git a/utilities/Globals.h b/utilities/Globals.h index 6e8927fe..1da6f56b 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -370,6 +370,8 @@ struct global_settings { bool network_snapshot_applied = false; // identity handed out by the server, presented again when reconnecting uint64_t network_session_token = 0; + // how far the worst vehicle was out of place when the last correction arrived, metres + float network_position_error = 0.0f; std::unordered_map trainset_overrides; diff --git a/widgets/multiplayer_lobby.cpp b/widgets/multiplayer_lobby.cpp index 2feac4a6..08ae6fc2 100644 --- a/widgets/multiplayer_lobby.cpp +++ b/widgets/multiplayer_lobby.cpp @@ -24,7 +24,10 @@ http://mozilla.org/MPL/2.0/. ui::multiplayer_lobby_panel::multiplayer_lobby_panel() : ui_panel(STR_C("Multiplayer lobby"), false) { - size_min = {520, 280}; + // both bounds have to be set: ui_panel feeds them straight to imgui, and a maximum + // left at its -1 default is smaller than the minimum, which collapses the window + size_min = {560, 320}; + size_max = {1920, 1080}; } std::string ui::multiplayer_lobby_panel::describe_crew(network::NetworkEntityId const Entity) const @@ -62,7 +65,8 @@ 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.SceneryFile.empty()) + ImGui::TextDisabled("%s", 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()); @@ -72,16 +76,22 @@ void ui::multiplayer_lobby_panel::render_contents() auto const &entries = network::Entities.entries(); if (entries.empty()) { - ImGui::TextUnformatted(STR_C("Waiting for the vehicle list from the server...")); + ImGui::TextUnformatted(client ? STR_C("Waiting for the vehicle list from the server...") : STR_C("This scenario has no vehicles anybody could drive.")); return; } network::NetworkEntityId const ownvehicle{network::Crews.vehicle_of(Global.network_peer_id)}; + float const scale = std::max(1.0f, Global.ui_scale); + float const crewwidth = 60.0f * scale; + float const statewidth = 150.0f * scale; + float const actionwidth = 110.0f * scale; + ImGui::Columns(4, "mp_vehicles", false); - 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::SetColumnWidth(0, std::max(120.0f * scale, ImGui::GetWindowWidth() - crewwidth - statewidth - actionwidth - 30.0f * scale)); + ImGui::SetColumnWidth(1, crewwidth); + ImGui::SetColumnWidth(2, statewidth); + ImGui::SetColumnWidth(3, actionwidth); ImGui::TextDisabled("%s", STR_C("Vehicle")); ImGui::NextColumn(); @@ -111,14 +121,14 @@ void ui::multiplayer_lobby_panel::render_contents() if (entry.id == ownvehicle) { - if (ImGui::Button(STR_C("Leave"), ImVec2(-1, 0))) + if (ImGui::Button(STR_C("Leave"))) Application.request_vehicle_leave(entry.id); } else if (entry.claimable) { // 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))) + if (ImGui::Button(entry.crew_count > 0 ? STR_C("Join crew") : STR_C("Enter"))) claim(entry.id, ownvehicle); } else @@ -131,6 +141,9 @@ void ui::multiplayer_lobby_panel::render_contents() } ImGui::Columns(1); + + ImGui::Separator(); + ImGui::TextDisabled("%s", STR_C("You can also walk up to a vehicle and use the enter-vehicle key.")); } void ui::multiplayer_lobby_panel::claim(network::NetworkEntityId const Entity, network::NetworkEntityId const Current)