17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 08:39:19 +02:00
Files
maszyna/network/snapshot.cpp
Hirek193 1bfa1644c9 multiplayer: a cab belongs to the peer it is on, and the first one aboard drives
Joining a crew did nothing. The cab was being built by a replicated entervehicle
that the authority posted only when it had no cab of its own - which is exactly
the case that never happens for the second person to board. The host claims a
vehicle, the server builds a cab, and from then on every further claim finds one
already standing over there, posts nothing, and the joining peer waits forever
for a command that is not coming.

A cab is not a shared object. Each participant has their own, with their own
camera and their own view of it, and nothing about it needs to travel. So it is
built locally by whoever needs one: the driver mode does it for the player when
their claim is granted, and the authority does it for itself, because it needs a
cab to address the AI handover commands to. That also stops a cab being created
on every peer for every claimed vehicle, which is what the entervehicle
broadcast was doing.

Who runs a shared train is no longer decided by car numbering. It is whoever
boarded first, and it stays with them: every crew member is stamped with a
session-wide counter when they sit down, and the smallest stamp on the whole
consist owns the physics. Coupling, uncoupling and renumbering do not move it,
and neither does somebody else joining - each handover is a stutter in the ride
of the person who has been driving, and they are the one who would notice it.
The stamps travel with CREW_UPDATE and in the snapshot, because a client works
out ownership for itself and has to arrive at the same answer.

Chat: the key that opens it is now read before imgui gets a look at the input.
Any window holding the keyboard - the lobby, most likely, which is precisely
where a client sits - would swallow it, so on that side the panel could not be
opened at all. The exception is text actually being typed, which is the chat box
itself; escape closes that. A client also logs what it says, so if a line goes
missing it is clear which half of the trip lost it.

Protocol version 6, snapshot version 7.
2026-09-07 16:00:41 +00:00

1430 lines
45 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 train may sit from where the session says it is before it gets put back.
// two margins, both settable as multiplayer.sync.running / multiplayer.sync.stop and both
// handed to every client by the server on connect: a generous one while it rolls, because
// nudging a moving vehicle several times a second is worse than the error itself, and a
// tight one for the moment it comes to a stand, applied once and then left alone
double running_tolerance()
{
return std::max(0.05, (double)Global.multiplayer_sync_running);
}
double stop_tolerance()
{
return std::max(0.01, (double)Global.multiplayer_sync_stop);
}
// anything slower than this counts as standing still
double const STANDSTILL_VELOCITY{0.05};
// consists that have had their one precise correction since they last moved
std::unordered_set<std::string> g_settled;
// ---------------------------------------------------------------------------
// what the authority last sent, so that a routine correction only carries changes
// most tracks in a scenery carry no name at all, so a name is useless as an identity.
// their order in the path table comes straight from the scenery file and is therefore the
// same on every peer, which makes the index a usable one
std::vector<TTrack *> g_trackindex;
std::unordered_map<TTrack const *, uint32_t> g_trackids;
void refresh_track_index()
{
auto const &paths = simulation::Paths.sequence();
if (g_trackindex.size() == paths.size())
return;
g_trackindex.assign(paths.begin(), paths.end());
g_trackids.clear();
for (uint32_t i = 0; i < g_trackindex.size(); ++i)
{
if (g_trackindex[i] != nullptr)
g_trackids.emplace(g_trackindex[i], i);
}
}
uint32_t track_id_of(TTrack const *Track)
{
if (Track == nullptr)
return 0xffffffff;
refresh_track_index();
auto const lookup = g_trackids.find(Track);
return lookup != g_trackids.end() ? lookup->second : 0xffffffff;
}
TTrack *track_by_id(uint32_t const Id)
{
refresh_track_index();
return (Id < g_trackindex.size() ? g_trackindex[Id] : nullptr);
}
// one-shot sound events seen since the last update went out, per vehicle
std::unordered_map<std::string, int> g_soundevents;
// the same for the brake unit's own events, plus what was on its bitfield the last time
// we looked - the flags are only cleared when the vehicle has an accelerator sound to
// play, so a bit that stays lit is one event, not one per frame
std::unordered_map<std::string, int> g_brakesoundevents;
std::unordered_map<std::string, int> g_brakesoundseen;
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;
uint32_t track{0xffffffff};
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};
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};
// what the vehicle is heard and seen doing. a peer that is not in this cab has no
// TTrain for it, so the commands that set these never reach it - only the result does
int32_t warningsignal{0};
// devices whose sound runs for as long as they do, and the one-shot events - relays
// clicking, air hissing - that happen too fast for an update to catch by sampling
bool converterrunning{false};
bool compressorrunning{false};
bool pantcompressorrunning{false};
bool compressorlock{false};
bool releaser{false};
int32_t soundevents{0};
// the brake unit keeps its own event bitfield, and it drives the accelerator and the
// cylinder venting
int32_t brakesoundevents{0};
// outputs of the physics, not settings. a peer that does not run this train computes
// its own and gets them wrong, so the authority's values are what it is told to use:
// these are what the braking, squealing and relay sounds are built from
double unitbrakeforce{0.0};
int32_t mainctrlactual{0};
int32_t scndctrlactual{0};
bool linecontactors{false};
bool slippingwheels{false};
bool sanddose{false};
double enginepower{0.0};
int32_t emergencywarningsignal{0};
bool alarmchain{false};
double wheelrevolutions{0.0};
double enginerevolutions{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};
};
// the authoritative reading for every train somebody else runs, held so that it can be
// put back after each local physics step
std::unordered_map<std::string, vehicle_state> g_remotestate;
void serialize_vehicle(std::ostream &Stream, vehicle_state const &State)
{
sn_utils::s_str(Stream, State.name);
sn_utils::ls_uint32(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);
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]);
sn_utils::s_bool(Stream, State.converterrunning);
sn_utils::s_bool(Stream, State.compressorrunning);
sn_utils::s_bool(Stream, State.pantcompressorrunning);
sn_utils::s_bool(Stream, State.compressorlock);
sn_utils::s_bool(Stream, State.releaser);
sn_utils::ls_int32(Stream, State.soundevents);
sn_utils::ls_int32(Stream, State.brakesoundevents);
sn_utils::ls_float64(Stream, State.unitbrakeforce);
sn_utils::ls_int32(Stream, State.mainctrlactual);
sn_utils::ls_int32(Stream, State.scndctrlactual);
sn_utils::s_bool(Stream, State.linecontactors);
sn_utils::s_bool(Stream, State.slippingwheels);
sn_utils::s_bool(Stream, State.sanddose);
sn_utils::ls_float64(Stream, State.enginepower);
sn_utils::ls_int32(Stream, State.warningsignal);
sn_utils::ls_int32(Stream, State.emergencywarningsignal);
sn_utils::s_bool(Stream, State.alarmchain);
sn_utils::ls_float64(Stream, State.wheelrevolutions);
sn_utils::ls_float64(Stream, State.enginerevolutions);
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::ld_uint32(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);
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);
state.converterrunning = sn_utils::d_bool(Stream);
state.compressorrunning = sn_utils::d_bool(Stream);
state.pantcompressorrunning = sn_utils::d_bool(Stream);
state.compressorlock = sn_utils::d_bool(Stream);
state.releaser = sn_utils::d_bool(Stream);
state.soundevents = sn_utils::ld_int32(Stream);
state.brakesoundevents = sn_utils::ld_int32(Stream);
state.unitbrakeforce = sn_utils::ld_float64(Stream);
state.mainctrlactual = sn_utils::ld_int32(Stream);
state.scndctrlactual = sn_utils::ld_int32(Stream);
state.linecontactors = sn_utils::d_bool(Stream);
state.slippingwheels = sn_utils::d_bool(Stream);
state.sanddose = sn_utils::d_bool(Stream);
state.enginepower = sn_utils::ld_float64(Stream);
state.warningsignal = sn_utils::ld_int32(Stream);
state.emergencywarningsignal = sn_utils::ld_int32(Stream);
state.alarmchain = sn_utils::d_bool(Stream);
state.wheelrevolutions = sn_utils::ld_float64(Stream);
state.enginerevolutions = sn_utils::ld_float64(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_id_of(track);
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;
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];
state.converterrunning = mover.ConverterFlag;
state.compressorrunning = mover.CompressorFlag;
state.pantcompressorrunning = mover.PantCompFlag;
state.compressorlock = mover.CompressorGovernorLock;
state.releaser = (mover.Hamulec != nullptr && mover.Hamulec->Releaser());
auto const events = g_soundevents.find(Vehicle.name());
state.soundevents = (events != g_soundevents.end() ? events->second : 0);
auto const brakeevents = g_brakesoundevents.find(Vehicle.name());
state.brakesoundevents = (brakeevents != g_brakesoundevents.end() ? brakeevents->second : 0);
state.unitbrakeforce = mover.UnitBrakeForce;
state.mainctrlactual = mover.MainCtrlActualPos;
state.scndctrlactual = mover.ScndCtrlActualPos;
state.linecontactors = mover.StLinFlag;
state.slippingwheels = mover.SlippingWheels;
state.sanddose = mover.SandDose;
state.enginepower = mover.EnginePower;
state.warningsignal = mover.WarningSignal;
state.emergencywarningsignal = mover.EmergencyBrakeWarningSignal;
state.alarmchain = mover.AlarmChainFlag;
state.wheelrevolutions = mover.nrot;
state.enginerevolutions = mover.enrot;
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;
}
// the readings the sound is built from. these are results of a physics step, and a peer
// that is not running this train produces its own, wrong, version of them every frame -
// which is exactly what made the brakes and the squeal come out in shreds, the pitch
// chasing a speed that fell away between updates and snapped back on the next one.
// so they are taken from the authority and put back after every local step
void apply_outputs(TDynamicObject &Vehicle, vehicle_state const &State)
{
auto &mover = *Vehicle.MoverParameters;
mover.V = State.velocity;
mover.Vel = State.velocitykmh;
mover.UnitBrakeForce = State.unitbrakeforce;
mover.nrot = State.wheelrevolutions;
mover.enrot = State.enginerevolutions;
mover.EnginePower = State.enginepower;
mover.SlippingWheels = State.slippingwheels;
mover.SandDose = State.sanddose;
// the notch the resistors are actually on, as opposed to the one the handle is at.
// with this coming from the authority the local automatic notching stays out of it,
// and every relay that clicks here is one that clicked over there
mover.MainCtrlActualPos = State.mainctrlactual;
mover.ScndCtrlActualPos = State.scndctrlactual;
mover.StLinFlag = State.linecontactors;
// the machinery whose sound runs for as long as it does. left to itself the local
// physics keeps switching these off, because the conditions it evaluates are not the
// ones the owner of the train is evaluating
mover.ConverterFlag = State.converterrunning;
mover.CompressorFlag = State.compressorrunning;
mover.PantCompFlag = State.pantcompressorrunning;
mover.CompressorGovernorLock = State.compressorlock;
mover.WarningSignal = State.warningsignal;
}
// 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.
// only ever called for vehicles somebody else is running
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);
// CabActive and CabOccupied are deliberately not replicated: which cab a player is
// sitting in is theirs alone, and two people sharing a vehicle each have their own.
// forcing the server's value on them is what made the camera jump in and out
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];
if (mover.Hamulec != nullptr && mover.Hamulec->Releaser() != State.releaser)
mover.Hamulec->Releaser(State.releaser ? 1 : 0);
// the one-shot events are added to whatever this peer's own physics produced; the
// vehicle's sound handling plays them once and wipes the lot, as it always does
mover.SoundFlag |= State.soundevents;
if (mover.Hamulec != nullptr)
mover.Hamulec->RaiseSoundFlag(State.brakesoundevents);
// and the physics outputs the sounds are shaped from
apply_outputs(Vehicle, State);
mover.EmergencyBrakeWarningSignal = State.emergencywarningsignal;
mover.AlarmChainFlag = State.alarmchain;
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 where the authority says it is.
//
// there are two ways to do that and they are not interchangeable. place_on_track() sets
// the axles from scratch, which is right for a peer that is joining and has nothing to go
// on - but doing it to a running train turns the whole set around, because the placement
// works from the vehicle's own idea of which way it faces and that is not what a moving
// consist looks like from outside. So a routine correction slides the vehicle along the
// track it is already on, by the difference between the two bogie offsets. That is the
// very same operation the physics performs on every step, so nothing gets reoriented and
// nothing ends up beside the rails
void correct_position(TDynamicObject &Vehicle, vehicle_state const &State, bool const Absolute)
{
if (Absolute)
{
TTrack *track = track_by_id(State.track);
if (track == nullptr)
{
static bool reported{false};
if (!reported)
{
reported = true;
ErrorLog("net: cannot place " + Vehicle.name() + ", track " + std::to_string(State.track) + " is unknown here", logtype::net);
}
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);
return;
}
// both sides have to be talking about the same piece of track for the difference of
// the offsets to mean anything. when they are not, the vehicle is left alone until it
// gets there - a wrong guess here is what puts a locomotive next to the rails
if (track_id_of(Vehicle.RaTrackGet()) != State.track)
return;
double const delta = State.translation - Vehicle.RaTranslationGet();
if (std::abs(delta) < 0.01)
return;
// the offset runs from the track's first point, the move runs along the vehicle, and
// the axle's own sense of direction is what converts between the two
Vehicle.Move(delta * Vehicle.RaDirectionGet());
}
// ---------------------------------------------------------------------------
// how the trains are made up.
//
// every peer loads its own copy of the scenery, which is the only reason joining a
// session takes seconds rather than minutes - but the copy on disk is not the session.
// The host may have shunted a set apart, put one together, or simply be running a
// scenery whose trainsets were edited since. So the composition travels: which coupler
// of which vehicle is joined to which coupler of which other vehicle, and with what.
// The vehicles themselves do not - a peer that is missing one cannot be sent a model
// over the wire, and says so in the log instead
struct coupling_end
{
int32_t flag{0};
std::string other;
uint8_t otherend{0};
};
struct consist_entry
{
std::string name;
coupling_end ends[2];
};
std::string write_consists()
{
std::ostringstream entries;
uint32_t count{0};
for (TDynamicObject const *vehicle : simulation::Vehicles.sequence())
{
if (vehicle == nullptr || vehicle->MoverParameters == nullptr)
continue;
sn_utils::s_str(entries, vehicle->name());
for (int end = 0; end < 2; ++end)
{
auto const &coupler = vehicle->MoverParameters->Couplers[end];
bool const linked = (coupler.Connected != nullptr) && (coupler.CouplingFlag != coupling::faux) && (coupler.ConnectedNr >= 0) && (coupler.ConnectedNr < 2);
sn_utils::ls_int32(entries, linked ? (int32_t)coupler.CouplingFlag : 0);
sn_utils::s_str(entries, linked ? coupler.Connected->Name : std::string());
sn_utils::s_uint8(entries, linked ? (uint8_t)coupler.ConnectedNr : (uint8_t)0);
}
++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();
}
// breaks a coupling whatever state the buffers are in. TDynamicObject::Dettach() leaves
// the mechanical coupler in place unless the vehicles are pressed together, which is the
// right thing for a player pulling a lever and the wrong thing for putting a peer's world
// in order
void force_detach(TDynamicObject &Vehicle, int const End)
{
auto &coupler = Vehicle.MoverParameters->Couplers[End];
if (coupler.Connected == nullptr)
{
coupler.CouplingFlag = coupling::faux;
coupler.ConnectedNr = -1;
return;
}
// hands the consist's driver back, among other housekeeping
Vehicle.Dettach(End);
if ((coupler.Connected != nullptr) && (coupler.ConnectedNr >= 0) && (coupler.ConnectedNr < 2))
{
auto &othercoupler = coupler.Connected->Couplers[coupler.ConnectedNr];
othercoupler.Connected = nullptr;
othercoupler.ConnectedNr = -1;
othercoupler.CouplingFlag = coupling::faux;
}
coupler.Connected = nullptr;
coupler.ConnectedNr = -1;
coupler.CouplingFlag = coupling::faux;
}
void read_consists(std::istream &Stream)
{
auto const count = sn_utils::ld_uint32(Stream);
std::vector<consist_entry> manifest;
manifest.reserve(count);
for (uint32_t i = 0; i < count; ++i)
{
consist_entry entry;
entry.name = sn_utils::d_str(Stream);
for (int end = 0; end < 2; ++end)
{
entry.ends[end].flag = sn_utils::ld_int32(Stream);
entry.ends[end].other = sn_utils::d_str(Stream);
entry.ends[end].otherend = sn_utils::d_uint8(Stream);
}
manifest.emplace_back(std::move(entry));
}
int detached{0};
int attached{0};
int missing{0};
// first everything that is coupled here and should not be, or is coupled to the wrong
// vehicle. doing this before making the new connections keeps a coupler from being
// claimed twice
for (auto const &entry : manifest)
{
TDynamicObject *vehicle = simulation::Vehicles.find(entry.name);
if (vehicle == nullptr || vehicle->MoverParameters == nullptr)
{
++missing;
continue;
}
for (int end = 0; end < 2; ++end)
{
auto const &coupler = vehicle->MoverParameters->Couplers[end];
if (coupler.Connected == nullptr)
continue;
auto const &wanted = entry.ends[end];
bool const same = (!wanted.other.empty()) && (coupler.Connected->Name == wanted.other) && (coupler.ConnectedNr == (int)wanted.otherend);
if (same)
continue;
force_detach(*vehicle, end);
++detached;
}
}
// then everything that should be coupled and is not, or is coupled with the wrong set
// of hoses and cables
for (auto const &entry : manifest)
{
TDynamicObject *vehicle = simulation::Vehicles.find(entry.name);
if (vehicle == nullptr || vehicle->MoverParameters == nullptr)
continue;
for (int end = 0; end < 2; ++end)
{
auto const &wanted = entry.ends[end];
if (wanted.other.empty() || wanted.flag == coupling::faux)
continue;
TDynamicObject *other = simulation::Vehicles.find(wanted.other);
if (other == nullptr || other->MoverParameters == nullptr)
{
++missing;
continue;
}
auto const &coupler = vehicle->MoverParameters->Couplers[end];
if ((coupler.Connected == other->MoverParameters) && (coupler.ConnectedNr == (int)wanted.otherend) && (coupler.CouplingFlag == wanted.flag))
continue;
// Enforce, because the two may not be pressed together yet on this peer - the
// positions in the same snapshot are what will settle that. Silent, because
// nobody here pulled a lever
if (false == vehicle->MoverParameters->Attach(end, (int)wanted.otherend, other->MoverParameters, wanted.flag, true, false))
continue;
vehicle->update_neighbours();
other->update_neighbours();
++attached;
}
}
if (detached != 0 || attached != 0)
{
WriteLog("net: trainsets reconciled with the session: " + std::to_string(detached) + " uncoupled, " + std::to_string(attached) + " coupled", logtype::net);
}
if (missing != 0)
{
ErrorLog("net: " + std::to_string(missing) + " vehicle(s) the session runs are not in the scenery loaded here", logtype::net);
}
}
std::string write_vehicles(bool const Full, bool const OwnedOnly)
{
std::ostringstream entries;
uint32_t count{0};
for (TDynamicObject const *vehicle : simulation::Vehicles.sequence())
{
if (vehicle == nullptr || vehicle->MoverParameters == nullptr)
continue;
if (OwnedOnly && !network::is_locally_simulated(network::Entities.id_of(vehicle->name())))
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;
// the one-shot events have been handed over; they must not go out twice
g_soundevents.erase(state.name);
g_brakesoundevents.erase(state.name);
}
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::PeerId const Owner, network::snapshot_result &Result)
{
auto const count = sn_utils::ld_uint32(Stream);
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;
auto const entity = network::Entities.id_of(state.name);
// a train this peer runs is not corrected by anybody: its own physics is what the
// rest of the session is being told about, and overwriting it would be the thing
// that makes the owner's ride stutter
if ((Mode == network::snapshot_mode::correction) && network::is_locally_simulated(entity))
continue;
// and a peer may only move what it is entitled to move
if ((Owner != network::PEER_NONE) && (network::simulation_owner(entity) != Owner))
continue;
apply_controls(*vehicle, state);
states.emplace(vehicle, state);
// kept so that the local physics can be overruled again on every frame until the
// next update arrives, not only on the frame it lands
g_remotestate[state.name] = 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);
}
// a rolling train and a standing one are held to different margins. the moving one
// is left to its own physics unless it is well out; the standing one is worth
// getting right to the centimetre, and is put right once rather than on every
// update that arrives while it sits there
bool moving{false};
for (TDynamicObject *member : group)
{
auto const known = states.find(member);
if ((known != states.end()) && (std::abs(known->second.velocity) > STANDSTILL_VELOCITY))
{
moving = true;
break;
}
}
double tolerance{stop_tolerance()};
if (Mode != network::snapshot_mode::join)
{
if (moving)
{
// on the move again: the next stand earns another precise correction
for (TDynamicObject *member : group)
g_settled.erase(member->name());
tolerance = running_tolerance();
}
else
{
bool settled{true};
for (TDynamicObject *member : group)
{
if (g_settled.count(member->name()) == 0)
{
settled = false;
break;
}
}
if (settled)
continue;
}
}
bool complete{true};
for (TDynamicObject *member : group)
{
if (states.find(member) == 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
complete = false;
break;
}
}
if (!complete)
continue;
bool wanted{false};
for (TDynamicObject *member : group)
{
if (glm::length(member->GetPosition() - states.at(member).position) > tolerance)
{
wanted = true;
break;
}
}
if (wanted)
{
for (TDynamicObject *member : group)
{
correct_position(*member, states.at(member), Mode == network::snapshot_mode::join);
++Result.repositioned;
}
}
if (!moving && (Mode != network::snapshot_mode::join))
{
// dealt with; nothing more happens to this train until it moves again
for (TDynamicObject *member : group)
g_settled.emplace(member->name());
}
}
}
// ---------------------------------------------------------------------------
// 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;
auto since = network::Crews.crew_since_of(entry.id);
since.resize(crew.size(), 0);
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);
for (uint64_t const stamp : since)
sn_utils::ls_uint64(entries, stamp);
++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));
std::vector<uint64_t> since;
since.reserve(size);
for (uint32_t p = 0; p < size; ++p)
since.emplace_back(sn_utils::ld_uint64(Stream));
network::Crews.mirror(id, crew, since);
}
}
// ---------------------------------------------------------------------------
// 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::note_sound_events(std::string const &Vehicle, int const Events)
{
if (Events == 0)
return;
g_soundevents[Vehicle] |= Events;
}
void network::note_brake_sound_events(std::string const &Vehicle, int const Events)
{
// what comes in here was peeked, not consumed, so a flag the vehicle never gets round
// to clearing would otherwise be reported over and over. only what has just come up
// counts as an event
auto &seen = g_brakesoundseen[Vehicle];
int const fresh = Events & ~seen;
seen = Events;
if (fresh == 0)
return;
g_brakesoundevents[Vehicle] |= fresh;
}
void network::hold_remote_state()
{
if (g_remotestate.empty())
return;
for (auto &entry : g_remotestate)
{
TDynamicObject *vehicle = simulation::Vehicles.find(entry.first);
if (vehicle == nullptr || vehicle->MoverParameters == nullptr)
continue;
// a train that has come under this peer's own control runs its own physics again
if (network::is_locally_simulated(network::Entities.id_of(entry.first)))
continue;
apply_outputs(*vehicle, entry.second);
}
}
void network::reset_snapshot_history()
{
g_soundevents.clear();
g_brakesoundevents.clear();
g_brakesoundseen.clear();
g_remotestate.clear();
g_settled.clear();
g_lastvehicles.clear();
g_lastmemcells.clear();
g_lastswitches.clear();
}
std::string network::take_snapshot(bool const Full, bool const OwnedOnly)
{
auto const vehicles = write_vehicles(Full, OwnedOnly);
if (OwnedOnly)
{
// what a peer says about its own trains, and nothing else: the clock, the crews,
// the memory cells and the switches all belong to the authority
if (vehicles.empty())
return std::string();
std::ostringstream owned;
sn_utils::ls_uint32(owned, SNAPSHOT_MAGIC);
sn_utils::ls_uint32(owned, SNAPSHOT_VERSION);
sn_utils::ls_uint64(owned, Global.simulation_tick);
sn_utils::ls_uint32(owned, 1);
write_chunk(owned, SNAPSHOT_VEHICLES, vehicles);
return owned.str();
}
auto const session = write_session(Full);
// the make-up of the trains only travels with a full snapshot: it is what a joining
// peer needs, and after that it is a slow-moving thing that costs nothing to repeat
auto const consists = (Full ? write_consists() : std::string());
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 (!consists.empty())
++chunks;
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);
// before the positions: how the trains are made up decides which vehicles get put
// back on the rails together
if (!consists.empty())
write_chunk(stream, SNAPSHOT_CONSISTS, consists);
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, PeerId const Owner)
{
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:
if (Owner == PEER_NONE)
read_session(chunk, Mode);
break;
case SNAPSHOT_CONSISTS:
// only the authority says what is coupled to what; an update coming the other
// way is one peer talking about its own trains
if (Owner == PEER_NONE)
read_consists(chunk);
break;
case SNAPSHOT_VEHICLES:
read_vehicles(chunk, Mode, Owner, result);
break;
case SNAPSHOT_CREWS:
if (Owner == PEER_NONE)
read_crews(chunk);
break;
case SNAPSHOT_MEMCELLS:
if (Owner == PEER_NONE)
read_memcells(chunk);
break;
case SNAPSHOT_SWITCHES:
if (Owner == PEER_NONE)
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;
}