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;