mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 23:49:19 +02:00
First live test showed three things wrong at once.
A client could not take a vehicle. Walking up to a locomotive and pressing the
enter-vehicle key posts entervehicle, which stage 4 classed as a world-wide
command and refused outright ("reserved for the session host"), so the client
never got a cab and every later control command died locally for want of one.
Walking into a cab is a request for a seat, not a world command: the authority
now turns it into a crew claim, for the host through the same path as for a
remote peer, and answers it the way the lobby button is answered.
Nothing but commands was ever replicated. The server sent dt and the commands
it ran, and the client simulated the rest for itself - so a pantograph the host
raised before the client joined, the contents of the memory cells the signalling
reads, brake pressures and switch positions simply never crossed the wire. The
server now streams the authoritative state a few times a second: whatever has
changed since the last one, and the whole of it every so often so a peer heals
from anything it missed. The snapshot carries the vehicle's inputs - line
breaker, battery, converter and compressor permits, pantograph valves, cab,
lights, doors - and lets the receiving peer derive the rest, plus the memory
cells and the switches.
Desync detection demanded that two state digests be equal, on a design where
the client deliberately runs its own physics. They never can be, so it reported
a desync permanently and asked for a resync every five seconds forever, which
fixed nothing because the next step disagreed again. The digest is a log line
now; what is measured instead is how far out of place the routine correction
found us, and a full resync is only asked for when even that is not catching up.
Also: a consist is put back on the rails as a whole or not at all. Correcting
one vehicle while its neighbour stayed put is what was stretching couplers
until they tore - the last test moved six vehicles out of eight, every time.
And the lobby window collapsed to nothing, because its size_max was left at the
-1 default while size_min was set, so imgui was handed a maximum smaller than
the minimum.
845 lines
24 KiB
C++
845 lines
24 KiB
C++
/*
|
|
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/Event.h"
|
|
#include "world/MemCell.h"
|
|
#include "world/Track.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
uint32_t const SNAPSHOT_MAGIC{0x4e535545}; // 'EUSN'
|
|
|
|
// 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<std::string, uint64_t> g_lastvehicles;
|
|
std::unordered_map<std::string, uint64_t> g_lastmemcells;
|
|
std::unordered_map<std::string, int> 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)
|
|
{
|
|
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(bool const Full)
|
|
{
|
|
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.
|
|
// 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, network::snapshot_mode const Mode)
|
|
{
|
|
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);
|
|
|
|
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;
|
|
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;
|
|
}
|
|
|
|
(void)Mode;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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<bool, 2> pantenabled{{false, false}};
|
|
std::array<bool, 2> pantdisabled{{false, false}};
|
|
std::array<bool, 2> 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<bool, 2> dooropen{{false, false}};
|
|
std::array<bool, 2> 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 entries;
|
|
uint32_t count{0};
|
|
|
|
for (TDynamicObject const *vehicle : simulation::Vehicles.sequence())
|
|
{
|
|
if (vehicle == nullptr || vehicle->MoverParameters == nullptr)
|
|
continue;
|
|
|
|
auto const state = read_from(*vehicle);
|
|
|
|
std::ostringstream packed;
|
|
serialize_vehicle(packed, state);
|
|
auto const record = packed.str();
|
|
|
|
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());
|
|
|
|
return body.str();
|
|
}
|
|
|
|
void read_vehicles(std::istream &Stream, network::snapshot_mode const Mode, network::snapshot_result &Result)
|
|
{
|
|
auto const count = sn_utils::ld_uint32(Stream);
|
|
double const tolerance = (Mode == network::snapshot_mode::join ? REPOSITION_TOLERANCE_JOIN : REPOSITION_TOLERANCE_CORRECTION);
|
|
|
|
std::unordered_map<TDynamicObject *, vehicle_state> states;
|
|
states.reserve(count);
|
|
|
|
for (uint32_t i = 0; i < count; ++i)
|
|
{
|
|
auto const state = deserialize_vehicle(Stream);
|
|
|
|
TDynamicObject *vehicle = simulation::Vehicles.find(state.name);
|
|
if (vehicle == nullptr || vehicle->MoverParameters == nullptr)
|
|
continue;
|
|
|
|
apply_controls(*vehicle, state);
|
|
states.emplace(vehicle, state);
|
|
|
|
Result.worst_position_error = std::max(Result.worst_position_error, glm::length(vehicle->GetPosition() - state.position));
|
|
}
|
|
|
|
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<TDynamicObject *> 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<TDynamicObject *> 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// crews
|
|
|
|
std::string write_crews()
|
|
{
|
|
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;
|
|
}
|
|
|
|
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_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<network::PeerId> 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);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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
|
|
|
|
void network::reset_snapshot_history()
|
|
{
|
|
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, chunks);
|
|
|
|
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();
|
|
|
|
if (Full)
|
|
WriteLog("net: snapshot taken at tick " + std::to_string(Global.simulation_tick) + ", " + std::to_string(blob.size()) + " bytes", logtype::net);
|
|
|
|
return 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 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 result;
|
|
}
|
|
|
|
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 result;
|
|
}
|
|
|
|
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, Mode);
|
|
break;
|
|
case SNAPSHOT_VEHICLES:
|
|
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
|
|
break;
|
|
}
|
|
}
|
|
|
|
result.ok = 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;
|
|
}
|