From 54d8b112b899b6c142c0ed11a50745219a4c85e0 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sun, 6 Sep 2026 19:37:28 +0000 Subject: [PATCH] multiplayer stage 7: versioned snapshots instead of replaying the session Joining meant replaying backbuffer.bin from the very first frame of the session, so the longer a session ran, the longer it took to get in - and the whole history had to be kept for it. - network/snapshot: a chunked container with a version of its own. Every section carries its length, so a peer built before a section existed reads a newer snapshot and simply walks past what it does not know. Version 1 carries the session (tick, clock, weather, the state of the random engine), the vehicles (position on the track, speed, direction, controls, brake pressures, whether the AI is driving) and the crew roster. Switches, memcells, signals and the event queue are the next sections - joining is now: handshake -> load the scenario -> CLIENT_READY -> the server takes a snapshot on the spot and sends it -> the peer goes live. Because the snapshot is taken at the moment the peer becomes active there is no backlog to replay at all; the frame by frame catch-up is gone - the server refuses a peer that loaded a different scenario, with a readable reason instead of a silent desync - reconnecting into a session that is already loaded asks for a fresh snapshot straight away rather than waiting for a loader that will not run again - a vehicle is only put back on the rails when it really is somewhere else; the arithmetic that turns the recorded bogie offset back into the nose distance place_on_track() expects mirrors what that function itself does - the session log is still written out - it is useful when picking a desync apart - but nothing depends on replaying it any more --- CMakeLists.txt | 1 + application/application.cpp | 6 + application/application.h | 3 + application/scenarioloadermode.cpp | 4 + network/backend/asio.h | 3 +- network/manager.cpp | 6 + network/manager.h | 4 + network/message.cpp | 30 +++ network/message.h | 26 +++ network/network.cpp | 102 +++++--- network/network.h | 14 +- network/snapshot.cpp | 359 +++++++++++++++++++++++++++++ network/snapshot.h | 42 ++++ utilities/Globals.h | 2 + 14 files changed, 558 insertions(+), 44 deletions(-) create mode 100644 network/snapshot.cpp create mode 100644 network/snapshot.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 552bc40d..8603f93b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -196,6 +196,7 @@ set(SOURCES "network/entities.cpp" "network/session.cpp" "network/statehash.cpp" +"network/snapshot.cpp" "network/backend/asio.cpp" "widgets/vehiclelist.cpp" diff --git a/application/application.cpp b/application/application.cpp index 05babab6..a99a4662 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -461,6 +461,12 @@ void eu07_application::request_vehicle_leave(network::NetworkEntityId const Enti m_network->request_leave(Entity); } +void eu07_application::network_scenario_loaded() +{ + if (m_network) + m_network->notify_scenario_loaded(); +} + int eu07_application::run() { auto frame{0}; diff --git a/application/application.h b/application/application.h index cedf5753..5010a83b 100644 --- a/application/application.h +++ b/application/application.h @@ -95,6 +95,9 @@ public: request_vehicle_claim( network::NetworkEntityId const Entity ); void request_vehicle_leave( network::NetworkEntityId const Entity ); + // tells the network layer the scenario finished loading + void + network_scenario_loaded(); private: // types diff --git a/application/scenarioloadermode.cpp b/application/scenarioloadermode.cpp index 098f60d5..05af181d 100644 --- a/application/scenarioloadermode.cpp +++ b/application/scenarioloadermode.cpp @@ -67,6 +67,10 @@ bool scenarioloader_mode::update() { network::Entities.build(); } + // a client can take the state of the world over now; the server answers with a + // snapshot instead of making us replay the session from its first frame + Application.network_scenario_loaded(); + WriteLog( "Scenario loading time: " + std::to_string( std::chrono::duration_cast( std::chrono::system_clock::now() - timestart ).count() ) + " seconds" ); // TODO: implement and use next mode cue diff --git a/network/backend/asio.h b/network/backend/asio.h index 47e731c7..6a401310 100644 --- a/network/backend/asio.h +++ b/network/backend/asio.h @@ -10,7 +10,8 @@ namespace network::tcp { const uint32_t NETWORK_MAGIC = 0x37305545; - const uint32_t MAX_MSG_SIZE = 100000; + // a snapshot of a busy scenario is comfortably larger than a command frame + const uint32_t MAX_MSG_SIZE = 8 * 1024 * 1024; class connection : public network::connection { diff --git a/network/manager.cpp b/network/manager.cpp index 07b0a610..662f5ab3 100644 --- a/network/manager.cpp +++ b/network/manager.cpp @@ -151,6 +151,12 @@ void network::manager::request_leave(NetworkEntityId entity_id) servers->apply_local_leave(entity_id); } +void network::manager::notify_scenario_loaded() +{ + if (client) + client->send_ready(); +} + void network::manager::update() { for (auto &backend : backend_list()) diff --git a/network/manager.h b/network/manager.h index 551c24c7..ab91affa 100644 --- a/network/manager.h +++ b/network/manager.h @@ -50,5 +50,9 @@ namespace network // 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); + + // the local scenario is loaded; a client asks the server for a snapshot of the + // world as it stands right now + void notify_scenario_loaded(); }; } diff --git a/network/message.cpp b/network/message.cpp index 2b00f314..d7975611 100644 --- a/network/message.cpp +++ b/network/message.cpp @@ -80,6 +80,32 @@ void network::vehicle_list::deserialize(std::istream &stream) } } +void network::client_ready::serialize(std::ostream &stream) const +{ + sn_utils::s_str(stream, scenario); +} + +void network::client_ready::deserialize(std::istream &stream) +{ + scenario = sn_utils::d_str(stream); +} + +void network::snapshot::serialize(std::ostream &stream) const +{ + sn_utils::ls_uint64(stream, tick); + sn_utils::ls_uint32(stream, (uint32_t)blob.size()); + stream.write(blob.data(), blob.size()); +} + +void network::snapshot::deserialize(std::istream &stream) +{ + tick = sn_utils::ld_uint64(stream); + + uint32_t const size = sn_utils::ld_uint32(stream); + blob.assign(size, '\0'); + stream.read(blob.data(), size); +} + void network::claim_vehicle::serialize(std::ostream &stream) const { sn_utils::ls_uint32(stream, entity_id); @@ -247,6 +273,10 @@ std::shared_ptr network::deserialize_message(std::istream &str msg = std::make_shared(); else if (type == message::CREW_UPDATE) msg = std::make_shared(); + else if (type == message::CLIENT_READY) + msg = std::make_shared(); + else if (type == message::SNAPSHOT) + 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 fc304b07..cca3bdc7 100644 --- a/network/message.h +++ b/network/message.h @@ -21,6 +21,8 @@ struct message CLAIM_DENIED, LEAVE_VEHICLE, CREW_UPDATE, + CLIENT_READY, + SNAPSHOT, TYPE_MAX }; @@ -85,6 +87,30 @@ struct vehicle_list : public message virtual void deserialize(std::istream &stream) override; }; +// sent once the joining peer has the scenario loaded and can take a snapshot +struct client_ready : public message +{ + client_ready() : message(CLIENT_READY) {} + + std::string scenario; + + virtual void serialize(std::ostream &stream) const override; + virtual void deserialize(std::istream &stream) override; +}; + +// the state of the world as of one particular tick, handed to a peer that is joining. +// this is what replaces replaying the whole session from its first frame +struct snapshot : public message +{ + snapshot() : message(SNAPSHOT) {} + + uint64_t tick{ 0 }; + std::string blob; + + virtual void serialize(std::ostream &stream) const override; + 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 { diff --git a/network/network.cpp b/network/network.cpp index e7ee6182..6ae3886d 100644 --- a/network/network.cpp +++ b/network/network.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "network/network.h" +#include "network/snapshot.h" #include "network/message.h" #include "utilities/Logs.h" #include "scene/sn_utils.h" @@ -56,42 +57,9 @@ void network::connection::connected() } } -void network::connection::catch_up() -{ - backbuffer->seekg(backbuffer_pos); - - std::vector> messages; - - for (int i = 0; i < CATCHUP_PACKETS; i++) { - if (backbuffer->peek() == EOF) { - send_messages(messages); - state = ACTIVE; - backbuffer->seekg(0, std::ios_base::end); - return; - } - - auto msg = deserialize_message(*backbuffer.get()); - - if (packet_counter) { - packet_counter--; - i--; // TODO: it would be better to skip frames in chunks - continue; - } - - messages.push_back(msg); - } - - backbuffer_pos = backbuffer->tellg(); - backbuffer->seekg(0, std::ios_base::end); - - send_messages(messages); -} - void network::connection::send_complete(std::shared_ptr buf) { - if (!is_client && state == CATCHING_UP) { - catch_up(); - } + // the shared buffer had to stay alive until asio finished writing it out; now it can go } // -------------- @@ -203,9 +171,9 @@ void network::server::handle_message(std::shared_ptr conn, const mes reply.app_version = Global.asVersion; reply.peer_id = allocate_peer_id(); conn->peer_id = reply.peer_id; - conn->state = connection::CATCHING_UP; - conn->backbuffer = backbuffer; - conn->backbuffer_pos = 0; + // the peer now goes and loads the scenario. it gets the state of the world as a + // snapshot when it reports back, instead of replaying the session frame by frame + conn->state = connection::AWAITING_READY; conn->packet_counter = cmd.start_packet; conn->send_message(reply); @@ -213,6 +181,34 @@ 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::CLIENT_READY) { + const auto& cmd = dynamic_cast(msg); + + if (cmd.scenario != Global.SceneryFile) { + WriteLog("net: peer " + std::to_string(conn->peer_id) + " loaded \"" + cmd.scenario + + "\" but the session runs \"" + Global.SceneryFile + "\"", logtype::net); + + server_reject reply; + reply.reason = "scenario mismatch: the session runs \"" + Global.SceneryFile + "\""; + conn->send_message(reply); + conn->disconnect(); + return; + } + + snapshot reply; + reply.tick = Global.simulation_tick; + reply.blob = take_snapshot(); + conn->send_message(reply); + + // from here on the peer receives the live stream; there is no backlog to catch up on + conn->state = connection::ACTIVE; + + WriteLog("net: snapshot sent to peer " + std::to_string(conn->peer_id) + " at tick " + + std::to_string(reply.tick), logtype::net); + + // and it still has to learn who is where + Crews.mark_all_pending(); + } else if (msg.type == message::CLAIM_VEHICLE) { const auto& cmd = dynamic_cast(msg); @@ -347,6 +343,18 @@ void network::client::send_commands(command_queue::commands_map commands) conn->send_message(msg); } +void network::client::send_ready() +{ + if (!conn || conn->state == connection::DEAD) + return; + + client_ready msg; + msg.scenario = Global.SceneryFile; + conn->send_message(msg); + + WriteLog("net: scenario loaded, asking the server for a snapshot", logtype::net); +} + void network::client::send_claim(NetworkEntityId entity_id) { if (!conn || conn->state != connection::ACTIVE) @@ -389,6 +397,8 @@ void network::client::handle_message(std::shared_ptr conn, const mes const auto& cmd = dynamic_cast(msg); conn->state = connection::ACTIVE; + bool const firsthandshake { !Global.ready_to_load }; + if (!Global.ready_to_load) { Global.random_seed = cmd.seed; Global.random_engine.seed(Global.random_seed); @@ -412,11 +422,29 @@ void network::client::handle_message(std::shared_ptr conn, const mes Global.network_status.clear(); WriteLog("net: accept received", logtype::net); + + if (!firsthandshake && simulation::is_ready) { + // we are coming back to a session we already have loaded, so there is nobody + // to wait for: ask for a fresh snapshot straight away + send_ready(); + } } if (conn->state != connection::ACTIVE) return; + if (msg.type == message::SNAPSHOT) { + const auto& cmd = dynamic_cast(msg); + + if (apply_snapshot(cmd.blob)) { + Global.network_snapshot_applied = true; + } + 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"; + } + } + if (msg.type == message::VEHICLE_LIST) { const auto& cmd = dynamic_cast(msg); Entities.adopt(cmd.vehicles); diff --git a/network/network.h b/network/network.h index 28479cd0..f302f98c 100644 --- a/network/network.h +++ b/network/network.h @@ -18,17 +18,13 @@ namespace network friend class client; private: - const int CATCHUP_PACKETS = 300; - bool is_client; protected: - std::shared_ptr backbuffer; - size_t backbuffer_pos; size_t packet_counter; + // exists only to keep the send buffer alive until asio is done with it void send_complete(std::shared_ptr buf); - void catch_up(); public: std::function message_handler; @@ -44,7 +40,9 @@ namespace network enum peer_state { AWAITING_HELLO, - CATCHING_UP, + // handshake done, the peer is loading the scenario; it gets a snapshot and + // goes live the moment it says it is ready + AWAITING_READY, ACTIVE, DEAD }; @@ -57,6 +55,8 @@ namespace network class server { private: + // the session log is still written out, it is handy when picking a desync apart, + // but joining no longer means replaying it std::shared_ptr backbuffer; protected: @@ -122,6 +122,8 @@ namespace network void update(); frame_delta get_next_delta(int counter); void send_commands(command_queue::commands_map commands); + // tells the server the scenario is loaded and a snapshot can be applied + void send_ready(); // lobby requests; the server is the one that decides void send_claim(NetworkEntityId entity_id); void send_leave(NetworkEntityId entity_id); diff --git a/network/snapshot.cpp b/network/snapshot.cpp new file mode 100644 index 00000000..d3268520 --- /dev/null +++ b/network/snapshot.cpp @@ -0,0 +1,359 @@ +/* +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/snapshot.h" + +#include "McZapkie/MOVER.h" +#include "network/entities.h" +#include "network/session.h" +#include "scene/sn_utils.h" +#include "simulation/simulation.h" +#include "simulation/simulationtime.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "vehicle/Driver.h" +#include "vehicle/DynObj.h" +#include "world/Track.h" + +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}; + +void write_chunk(std::ostream &Stream, network::snapshot_chunk const Id, std::string const &Body) +{ + sn_utils::ls_uint16(Stream, (uint16_t)Id); + sn_utils::ls_uint32(Stream, (uint32_t)Body.size()); + Stream.write(Body.data(), Body.size()); +} + +// --------------------------------------------------------------------------- +// session + +std::string write_session() +{ + std::ostringstream body; + + sn_utils::ls_uint64(body, Global.simulation_tick); + sn_utils::ls_int64(body, Global.starting_timestamp); + sn_utils::ls_uint32(body, Global.random_seed); + + auto const &time = simulation::Time.data(); + sn_utils::ls_uint32(body, (uint32_t)simulation::Time.year_day()); + sn_utils::ls_uint32(body, (uint32_t)(time.wHour * 60 + time.wMinute)); + sn_utils::ls_float64(body, simulation::Time.second()); + + 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()); + + return body.str(); +} + +void read_session(std::istream &Stream) +{ + auto const tick = sn_utils::ld_uint64(Stream); + auto const timestamp = sn_utils::ld_int64(Stream); + auto const seed = sn_utils::ld_uint32(Stream); + + auto const yearday = sn_utils::ld_uint32(Stream); + auto const minuteofday = sn_utils::ld_uint32(Stream); + auto const second = sn_utils::ld_float64(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); + + Global.simulation_tick = tick; + Global.starting_timestamp = timestamp; + Global.random_seed = seed; + + simulation::Time.set_time((int)yearday, (int)minuteofday); + simulation::Time.data().wSecond = (WORD)std::floor(second); + simulation::Time.data().wMilliseconds = (WORD)((second - std::floor(second)) * 1000.0); + + Global.Overcast = (float)overcast; + Global.AirTemperature = (float)temperature; + + if (!enginestate.empty()) + { + std::istringstream engine(enginestate); + engine >> Global.random_engine; + } +} + +// --------------------------------------------------------------------------- +// vehicles + +std::string write_vehicles() +{ + std::ostringstream body; + std::ostringstream entries; + uint32_t count{0}; + + for (TDynamicObject const *vehicle : simulation::Vehicles.sequence()) + { + if (vehicle == nullptr || vehicle->MoverParameters == nullptr) + continue; + + auto const &mover = *vehicle->MoverParameters; + auto const *track = vehicle->RaTrackGet(); + + 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()); + + 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); + + ++count; + } + + sn_utils::ls_uint32(body, count); + auto const packed = entries.str(); + body.write(packed.data(), packed.size()); + + return body.str(); +} + +void read_vehicles(std::istream &Stream) +{ + auto const count = sn_utils::ld_uint32(Stream); + uint32_t repositioned{0}; + uint32_t missing{0}; + + 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 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); + 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; + + vehicle->place_on_track(track, nose, false); + ++repositioned; + } + + 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); +} + +// --------------------------------------------------------------------------- +// crews + +std::string write_crews() +{ + std::ostringstream body; + std::ostringstream entries; + uint32_t count{0}; + + for (auto const &entry : network::Entities.entries()) + { + auto const crew = network::Crews.crew_of(entry.id); + if (crew.empty()) + continue; + + 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); + + ++count; + } + + sn_utils::ls_uint32(body, count); + auto const packed = entries.str(); + body.write(packed.data(), packed.size()); + + return body.str(); +} + +void read_crews(std::istream &Stream) +{ + auto const count = sn_utils::ld_uint32(Stream); + + for (uint32_t i = 0; i < count; ++i) + { + auto const id = sn_utils::ld_uint32(Stream); + auto const size = sn_utils::ld_uint32(Stream); + + std::vector crew; + crew.reserve(size); + for (uint32_t p = 0; p < size; ++p) + crew.emplace_back(sn_utils::ld_uint32(Stream)); + + network::Crews.mirror(id, crew); + } +} + +} // namespace + +std::string network::take_snapshot() +{ + 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 + + write_chunk(stream, SNAPSHOT_SESSION, write_session()); + write_chunk(stream, SNAPSHOT_VEHICLES, write_vehicles()); + write_chunk(stream, SNAPSHOT_CREWS, write_crews()); + + 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); + + return blob; +} + +bool network::apply_snapshot(std::string const &Blob) +{ + std::istringstream stream(Blob); + + if (sn_utils::ld_uint32(stream) != SNAPSHOT_MAGIC) + { + ErrorLog("net: snapshot rejected, not a snapshot", logtype::net); + return false; + } + + 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; + } + + auto const tick = sn_utils::ld_uint64(stream); + auto const chunks = sn_utils::ld_uint32(stream); + + for (uint32_t i = 0; i < chunks; ++i) + { + if (!stream.good()) + { + ErrorLog("net: snapshot truncated", logtype::net); + return false; + } + + auto const id = sn_utils::ld_uint16(stream); + auto const length = sn_utils::ld_uint32(stream); + + std::string body(length, '\0'); + stream.read(body.data(), length); + + std::istringstream chunk(body); + + switch (id) + { + case SNAPSHOT_SESSION: + read_session(chunk); + break; + case SNAPSHOT_VEHICLES: + read_vehicles(chunk); + break; + case SNAPSHOT_CREWS: + read_crews(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); + + return true; +} diff --git a/network/snapshot.h b/network/snapshot.h new file mode 100644 index 00000000..07776a4f --- /dev/null +++ b/network/snapshot.h @@ -0,0 +1,42 @@ +/* +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 +#include + +namespace network +{ + +// layout of the snapshot container. a reader refuses a blob it does not understand +constexpr uint32_t SNAPSHOT_VERSION = 1; + +// 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 + SNAPSHOT_SESSION = 1, + // where every vehicle is and what its controls and 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 +}; + +// packs the current state of the world. only ever called on the authority +std::string take_snapshot(); + +// 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); + +} // namespace network diff --git a/utilities/Globals.h b/utilities/Globals.h index 96b627f4..22e87639 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -366,6 +366,8 @@ struct global_settings { // authoritative frame it is executing; the network timeline hangs off this, not off // the number of frames the renderer happened to draw uint64_t simulation_tick = 0; + // set once a joining client has taken over the state of the world from the server + bool network_snapshot_applied = false; std::unordered_map trainset_overrides;