mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-18 09:09:19 +02:00
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
360 lines
11 KiB
C++
360 lines
11 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/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<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);
|
|
}
|
|
}
|
|
|
|
} // 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;
|
|
}
|