17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-18 16:09:19 +02:00

multiplayer: stream the authoritative state, and let a client walk into a cab

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.
This commit is contained in:
2026-09-06 22:19:28 +00:00
parent 65159b9546
commit 9dc78a65dc
16 changed files with 859 additions and 185 deletions

View File

@@ -463,10 +463,9 @@ void eu07_application::request_vehicle_leave(network::NetworkEntityId const Enti
namespace {
// how many steps in a row may disagree with the authority before we ask to be corrected,
// and how long to leave the correction alone before asking again
uint64_t const NETWORK_RESYNC_THRESHOLD = 120;
uint64_t const NETWORK_RESYNC_COOLDOWN = 600;
// the state digest is logged when it starts differing and then only this often, so that
// a persistent difference does not bury the rest of the log
uint64_t const NETWORK_DIGEST_LOG_INTERVAL = 1800;
} // namespace
@@ -581,46 +580,26 @@ int eu07_application::run()
// if we're slave
if (m_network && m_network->client)
{
if (authoritative.valid)
// the digest is a diagnostic, not a control input: a client runs its
// own physics, so the two will never agree bit for bit and demanding
// that they do only produces noise. what actually keeps a client in
// line is the authoritative state the server streams to it
if (authoritative.valid && authoritative.state_hash_version == network::STATE_HASH_VERSION)
{
if (authoritative.state_hash_version != network::STATE_HASH_VERSION)
{
// nothing sensible to compare; say so once and stop pretending
if (m_statemismatches == 0)
{
ErrorLog("net: state digest version mismatch, desync detection is off", logtype::net);
m_statemismatches = 1;
}
}
else if (statehash != authoritative.state_hash)
if (statehash != authoritative.state_hash)
{
++m_statemismatches;
// a drift usually persists, so this is reported when it starts
// and then only now and then, instead of every single step
if (m_statemismatches == 1 || (m_statemismatches % 300) == 0)
if (m_statemismatches == 1 || (m_statemismatches % NETWORK_DIGEST_LOG_INTERVAL) == 0)
{
WriteLog("net: state mismatch at tick " + std::to_string(authoritative.tick) + " (" + std::to_string(m_statemismatches) +
WriteLog("net: state digest differs at tick " + std::to_string(authoritative.tick) + " (" + std::to_string(m_statemismatches) +
" steps): local " + std::to_string(statehash) + ", authoritative " + std::to_string(authoritative.state_hash),
logtype::net);
}
Global.desync = (float)m_statemismatches;
// a short disagreement rides itself out; a lasting one will not,
// and calls for the authoritative state rather than a restart
if ((m_statemismatches >= NETWORK_RESYNC_THRESHOLD) &&
(m_lastresynctick == 0 || authoritative.tick > m_lastresynctick + NETWORK_RESYNC_COOLDOWN))
{
m_lastresynctick = authoritative.tick;
WriteLog("net: resync requested at tick " + std::to_string(authoritative.tick), logtype::net);
m_network->request_resync(authoritative.tick, statehash);
}
}
else
else if (m_statemismatches != 0)
{
if (m_statemismatches != 0)
WriteLog("net: state back in step with the authority at tick " + std::to_string(authoritative.tick), logtype::net);
WriteLog("net: state digest back in step at tick " + std::to_string(authoritative.tick), logtype::net);
m_statemismatches = 0;
Global.desync = 0.0f;
}
}

View File

@@ -123,10 +123,9 @@ private:
// members
bool m_screenshot_queued = false;
// consecutive authoritative steps whose state digest did not match ours
// consecutive authoritative steps whose state digest did not match ours. purely a
// diagnostic: a client runs its own physics, so it is never expected to match exactly
std::uint64_t m_statemismatches { 0 };
// tick at which we last asked the server to put us back in line
std::uint64_t m_lastresynctick { 0 };
modeptr_array m_modes { nullptr }; // collection of available application behaviour modes
mode_stack m_modestack; // current behaviour mode

View File

@@ -54,6 +54,10 @@ driver_ui::driver_ui()
m_transcriptspanel.size_min = {435, 85};
m_transcriptspanel.size_max = {Global.fb_size.x * 0.95, Global.fb_size.y * 0.95};
m_multiplayerlobby.title = STR("Multiplayer lobby");
m_multiplayerlobby.size_min = {560, 320};
m_multiplayerlobby.size_max = {Global.fb_size.x * 0.95f, Global.fb_size.y * 0.95f};
if (Global.gui_defaultwindows)
{
m_aidpanel.is_open = true;
@@ -278,11 +282,11 @@ void driver_ui::render_()
ImGui::OpenPopup(popupheader);
}
if (Global.desync != 0.0f)
if (Global.network_position_error > 1.0f)
{
ImGui::SetNextWindowSize(ImVec2(-1, -1));
if (ImGui::Begin("network", nullptr, ImGuiWindowFlags_NoCollapse))
ImGui::Text("out of step with the server for %d ticks (at %llu)", (int)Global.desync, (unsigned long long)Global.simulation_tick);
ImGui::Text("%.1f m behind the server (tick %llu)", Global.network_position_error, (unsigned long long)Global.simulation_tick);
ImGui::End();
}
}

View File

@@ -806,17 +806,37 @@ bool is_vehicle_recipient(uint32_t const Recipient) {
return (command_target)(Recipient & ~0xffff) == command_target::vehicle;
}
// says a thing once instead of once per frame; a command that cannot be addressed keeps
// failing for as long as the player holds the key
bool report_once(std::string const &Message) {
static std::unordered_set<std::string> reported;
if (!reported.emplace(Message).second)
return false;
ErrorLog(Message, logtype::net);
return true;
}
uint32_t to_network_recipient(uint32_t const Recipient) {
if (!is_vehicle_recipient(Recipient) || network::Entities.empty())
if (!is_vehicle_recipient(Recipient))
return Recipient;
if (network::Entities.empty()) {
if (network::is_multiplayer())
report_once("net: no vehicle roster yet, vehicle commands cannot be addressed");
return Recipient;
}
TTrain *train = simulation::Trains.find_id((uint16_t)(Recipient & 0xffff));
if (train == nullptr || train->Dynamic() == nullptr)
if (train == nullptr || train->Dynamic() == nullptr) {
report_once("net: no cab behind local id " + std::to_string(Recipient & 0xffff) + ", its commands cannot be addressed");
return Recipient;
}
auto const id = network::Entities.id_of(train->Dynamic()->name());
if (id == network::ENTITY_NONE || id > 0xffff)
if (id == network::ENTITY_NONE || id > 0xffff) {
report_once("net: \"" + train->Dynamic()->name() + "\" has no network id, its commands cannot leave this peer");
return Recipient;
}
return (uint32_t)command_target::vehicle | id;
}
@@ -833,8 +853,10 @@ bool from_network_recipient(uint32_t const Recipient, uint32_t &Local) {
return true;
TTrain *train = simulation::Trains.find(name);
if (train == nullptr)
if (train == nullptr) {
report_once("net: \"" + name + "\" has no cab here yet, commands for it are dropped");
return false;
}
Local = (uint32_t)command_target::vehicle | train->id();
return true;

View File

@@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/.
#include "utilities/Logs.h"
#include "utilities/Globals.h"
#include "network/statehash.h"
#include "network/snapshot.h"
network::server_manager::server_manager()
{
@@ -158,6 +159,30 @@ void network::manager::notify_scenario_loaded()
client->send_ready();
}
void network::server_manager::publish_state()
{
if (--state_countdown > 0)
return;
state_countdown = STATE_INTERVAL_FRAMES;
// most of the time only what changed goes out; every so often the whole thing does,
// so that anything a peer missed heals by itself
bool const full = ((state_updates++ % STATE_FULL_EVERY) == 0);
auto const blob = take_snapshot(full);
if (blob.empty())
return;
snapshot msg;
msg.tick = Global.simulation_tick;
msg.mode = 1; // correction on top of a running simulation
msg.blob = blob;
for (auto srv : servers)
srv->push_message(msg);
}
void network::manager::request_resync(uint64_t tick, uint64_t state_hash)
{
if (client)
@@ -172,6 +197,7 @@ void network::manager::update()
if (servers && simulation::is_ready) {
servers->update_crews();
servers->publish_vehicle_list();
servers->publish_state();
}
if (client)

View File

@@ -21,6 +21,9 @@ namespace network
void publish_vehicle_list();
// brings the world in line with the crew roster and announces every change
void update_crews();
// sends the peers what the world actually looks like. this is what keeps a client
// in line - the command stream alone cannot, because a client runs its own physics
void publish_state();
// crew request coming from the local participant, which needs no transport
void apply_local_claim(NetworkEntityId entity_id);
void apply_local_leave(NetworkEntityId entity_id);
@@ -29,9 +32,16 @@ namespace network
// how many frames may pass between two unconditional broadcasts of the roster
static const int PUBLISH_INTERVAL_FRAMES = 120;
// how often the authoritative state goes out, and how often it goes out whole
// rather than as what changed since the last one
static const int STATE_INTERVAL_FRAMES = 10;
static const int STATE_FULL_EVERY = 60;
std::vector<vehicle_entry> last_published_list;
int publish_countdown = 0;
int crew_publish_countdown = 0;
int state_countdown = 0;
int state_updates = 0;
};
class manager

View File

@@ -97,6 +97,7 @@ void network::client_ready::deserialize(std::istream &stream)
void network::snapshot::serialize(std::ostream &stream) const
{
sn_utils::ls_uint64(stream, tick);
sn_utils::s_uint8(stream, mode);
sn_utils::ls_uint32(stream, (uint32_t)blob.size());
stream.write(blob.data(), blob.size());
}
@@ -104,6 +105,7 @@ void network::snapshot::serialize(std::ostream &stream) const
void network::snapshot::deserialize(std::istream &stream)
{
tick = sn_utils::ld_uint64(stream);
mode = sn_utils::d_uint8(stream);
uint32_t const size = sn_utils::ld_uint32(stream);
blob.assign(size, '\0');

View File

@@ -113,6 +113,9 @@ struct snapshot : public message
snapshot() : message(SNAPSHOT) {}
uint64_t tick{ 0 };
// 0 - the peer is joining and takes the world as given
// 1 - routine correction on top of a simulation that is already running
uint8_t mode{ 0 };
std::string blob;
virtual void serialize(std::ostream &stream) const override;

View File

@@ -205,7 +205,8 @@ void network::server::handle_message(std::shared_ptr<connection> conn, const mes
snapshot reply;
reply.tick = Global.simulation_tick;
reply.blob = take_snapshot();
reply.mode = 0; // joining: take the world as it is
reply.blob = take_snapshot(true);
conn->send_message(reply);
// from here on the peer receives the live stream; there is no backlog to catch up on
@@ -225,7 +226,8 @@ void network::server::handle_message(std::shared_ptr<connection> conn, const mes
snapshot reply;
reply.tick = Global.simulation_tick;
reply.blob = take_snapshot();
reply.mode = 0;
reply.blob = take_snapshot(true);
conn->send_message(reply);
}
else if (msg.type == message::CLAIM_VEHICLE) {
@@ -256,6 +258,29 @@ void network::server::handle_message(std::shared_ptr<connection> conn, const mes
for (auto const &kv : cmd.commands) {
for (command_data const &data : kv.second) {
if (data.command == user_command::entervehicle) {
// the peer walked into a cab; that is a request for a seat, exactly
// like the lobby button, and the crew registry is what decides on it
NetworkEntityId entity { ENTITY_NONE };
auto const result = claim_by_name(conn->peer_id, data.payload, entity);
if (result == claim_result::granted || result == claim_result::already_member) {
claim_granted reply;
reply.entity_id = entity;
conn->send_message(reply);
}
else {
WriteLog("net: peer " + std::to_string(conn->peer_id) + " cannot take "
+ data.payload + ": " + describe(result), logtype::net);
claim_denied reply;
reply.entity_id = entity;
reply.reason = describe(result);
conn->send_message(reply);
}
continue;
}
auto const verdict = validate_command(conn->peer_id, data.command, kv.first);
if (verdict != command_verdict::accepted) {
report_rejected_command(conn->peer_id, data.command, verdict);
@@ -393,6 +418,8 @@ void network::client::send_claim(NetworkEntityId entity_id)
claim_vehicle msg;
msg.entity_id = entity_id;
conn->send_message(msg);
WriteLog("net: asking for a seat on vehicle " + std::to_string(entity_id) + " (" + Entities.name_of(entity_id) + ")", logtype::net);
}
void network::client::send_leave(NetworkEntityId entity_id)
@@ -471,8 +498,16 @@ void network::client::handle_message(std::shared_ptr<connection> conn, const mes
if (msg.type == message::SNAPSHOT) {
const auto& cmd = dynamic_cast<const snapshot&>(msg);
if (apply_snapshot(cmd.blob)) {
auto const mode = (cmd.mode == 0 ? snapshot_mode::join : snapshot_mode::correction);
auto const result = apply_snapshot(cmd.blob, mode);
if (!result.ok) {
ErrorLog("net: could not apply the state handed to us", logtype::net);
Global.network_status = "The server sent a snapshot this build cannot read";
}
else if (mode == snapshot_mode::join) {
Global.network_snapshot_applied = true;
Global.network_position_error = (float)result.worst_position_error;
// the roster may say we are on a crew already - after a reconnect, or because
// the claim was granted while we were still loading. walk into that cab
@@ -484,13 +519,30 @@ void network::client::handle_message(std::shared_ptr<connection> conn, const mes
}
}
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";
Global.network_position_error = (float)result.worst_position_error;
// the routine correction is what keeps us in line; a full resync is only worth
// asking for when even that is not catching up
if (result.worst_position_error > RESYNC_POSITION_ERROR) {
if (++bad_corrections >= RESYNC_BAD_CORRECTIONS) {
bad_corrections = 0;
WriteLog("net: " + std::to_string((int)result.worst_position_error)
+ " m out of place after a correction, asking for a full resync", logtype::net);
send_resync_request(Global.simulation_tick, 0);
}
}
else {
bad_corrections = 0;
}
}
}
if (msg.type == message::VEHICLE_LIST) {
const auto& cmd = dynamic_cast<const vehicle_list&>(msg);
if (Entities.empty() && !cmd.vehicles.empty())
WriteLog("net: vehicle roster received, " + std::to_string(cmd.vehicles.size()) + " vehicles to choose from", logtype::net);
Entities.adopt(cmd.vehicles);
}

View File

@@ -110,6 +110,12 @@ namespace network
std::queue<frame_info> delta_queue;
// how far out of place a correction found us, in a row. a full resync is only
// worth asking for when the routine stream is not catching up on its own
int bad_corrections = 0;
static constexpr double RESYNC_POSITION_ERROR = 25.0;
static constexpr int RESYNC_BAD_CORRECTIONS = 5;
float last_target = 20.0f;
float jitteriness = 1.0f;
float consume_counter = 0.0f;

View File

@@ -86,6 +86,24 @@ command_verdict validate_command(PeerId Peer, user_command Command, uint32_t Rec
return command_verdict::unsupported_target;
}
claim_result claim_by_name(PeerId Peer, std::string const &Vehicle, NetworkEntityId &Entity)
{
Entity = Entities.id_of(Vehicle);
if (Entity == ENTITY_NONE)
return claim_result::unknown_vehicle;
auto const previous = Crews.vehicle_of(Peer);
auto const result = Crews.claim(Peer, Entity);
if ((result == claim_result::granted) && (previous != ENTITY_NONE) && (previous != Entity))
{
// nobody works two vehicles at once
Crews.leave(Peer, previous);
}
return result;
}
void filter_commands(PeerId Peer, command_queue::commands_map &Commands)
{
for (auto it = Commands.begin(); it != Commands.end();)
@@ -94,6 +112,27 @@ void filter_commands(PeerId Peer, command_queue::commands_map &Commands)
for (auto command = sequence.begin(); command != sequence.end();)
{
if (command->source == PEER_NONE)
{
// posted by the authority itself rather than by a person; it is already
// the decision, not a request for one
++command;
continue;
}
if (command->command == user_command::entervehicle)
{
// the local player walked into a cab. that is a crew request; the cab
// itself is built by the authority once the seat is granted
NetworkEntityId entity{ENTITY_NONE};
auto const result = claim_by_name(Peer, command->payload, entity);
if (result != claim_result::granted && result != claim_result::already_member)
WriteLog("net: cannot take " + command->payload + ": " + describe(result), logtype::net);
command = sequence.erase(command);
continue;
}
if (validate_command(Peer, command->command, it->first) != command_verdict::accepted)
{
command = sequence.erase(command);

View File

@@ -138,6 +138,11 @@ std::string describe(command_verdict Verdict);
// simply holds every right - there is no separate path for the local participant
command_verdict validate_command(PeerId Peer, user_command Command, uint32_t Recipient);
// walking into a cab with the in-game key is the same request as pressing the button in
// the lobby, so it goes through the crew registry rather than being treated as a command
// that reaches the whole world. returns the vehicle it resolved to, if any
claim_result claim_by_name(PeerId Peer, std::string const &Vehicle, NetworkEntityId &Entity);
// drops everything the peer is not allowed to ask for and stamps the rest with its
// identity. used for the local participant as well, so that host input goes through the
// very same authority layer as input arriving over the wire, only without the transport

View File

@@ -20,6 +20,8 @@ http://mozilla.org/MPL/2.0/.
#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
@@ -27,9 +29,29 @@ 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};
// 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)
{
@@ -41,7 +63,7 @@ void write_chunk(std::ostream &Stream, network::snapshot_chunk const Id, std::st
// ---------------------------------------------------------------------------
// session
std::string write_session()
std::string write_session(bool const Full)
{
std::ostringstream body;
@@ -57,15 +79,20 @@ std::string write_session()
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());
// 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)
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);
@@ -78,7 +105,10 @@ void read_session(std::istream &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);
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;
@@ -96,14 +126,282 @@ void read_session(std::istream &Stream)
std::istringstream engine(enginestate);
engine >> Global.random_engine;
}
(void)Mode;
}
// ---------------------------------------------------------------------------
// vehicles
std::string write_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 body;
std::ostringstream entries;
uint32_t count{0};
@@ -112,33 +410,35 @@ std::string write_vehicles()
if (vehicle == nullptr || vehicle->MoverParameters == nullptr)
continue;
auto const &mover = *vehicle->MoverParameters;
auto const *track = vehicle->RaTrackGet();
auto const state = read_from(*vehicle);
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());
std::ostringstream packed;
serialize_vehicle(packed, state);
auto const record = packed.str();
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);
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());
@@ -146,87 +446,98 @@ std::string write_vehicles()
return body.str();
}
void read_vehicles(std::istream &Stream)
void read_vehicles(std::istream &Stream, network::snapshot_mode const Mode, network::snapshot_result &Result)
{
auto const count = sn_utils::ld_uint32(Stream);
uint32_t repositioned{0};
uint32_t missing{0};
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 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 state = deserialize_vehicle(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);
TDynamicObject *vehicle = simulation::Vehicles.find(state.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;
apply_controls(*vehicle, state);
states.emplace(vehicle, state);
vehicle->place_on_track(track, nose, false);
++repositioned;
Result.worst_position_error = std::max(Result.worst_position_error, glm::length(vehicle->GetPosition() - state.position));
}
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);
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;
}
}
}
// ---------------------------------------------------------------------------
@@ -234,7 +545,6 @@ void read_vehicles(std::istream &Stream)
std::string write_crews()
{
std::ostringstream body;
std::ostringstream entries;
uint32_t count{0};
@@ -252,6 +562,7 @@ std::string write_crews()
++count;
}
std::ostringstream body;
sn_utils::ls_uint32(body, count);
auto const packed = entries.str();
body.write(packed.data(), packed.size());
@@ -277,42 +588,204 @@ void read_crews(std::istream &Stream)
}
}
// ---------------------------------------------------------------------------
// 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
std::string network::take_snapshot()
void network::reset_snapshot_history()
{
std::ostringstream stream;
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, 3); // number of chunks that follow
sn_utils::ls_uint32(stream, chunks);
write_chunk(stream, SNAPSHOT_SESSION, write_session());
write_chunk(stream, SNAPSHOT_VEHICLES, write_vehicles());
write_chunk(stream, SNAPSHOT_CREWS, write_crews());
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();
WriteLog("net: snapshot taken at tick " + std::to_string(Global.simulation_tick) + ", " + std::to_string(blob.size()) + " bytes", logtype::net);
if (Full)
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)
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 false;
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 false;
return result;
}
auto const tick = sn_utils::ld_uint64(stream);
@@ -323,7 +796,7 @@ bool network::apply_snapshot(std::string const &Blob)
if (!stream.good())
{
ErrorLog("net: snapshot truncated", logtype::net);
return false;
return result;
}
auto const id = sn_utils::ld_uint16(stream);
@@ -337,23 +810,35 @@ bool network::apply_snapshot(std::string const &Blob)
switch (id)
{
case SNAPSHOT_SESSION:
read_session(chunk);
read_session(chunk, Mode);
break;
case SNAPSHOT_VEHICLES:
read_vehicles(chunk);
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
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);
result.ok = true;
return 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;
}

View File

@@ -16,27 +16,54 @@ namespace network
{
// layout of the snapshot container. a reader refuses a blob it does not understand
constexpr uint32_t SNAPSHOT_VERSION = 1;
constexpr uint32_t SNAPSHOT_VERSION = 2;
// 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
// tick, clock, weather and (in a full snapshot) the state of the random engine
SNAPSHOT_SESSION = 1,
// where every vehicle is and what its controls and brakes are doing
// where every vehicle is, what its controls and appliances are set to, and what its
// 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
SNAPSHOT_CREWS = 3,
// contents of the memory cells - this is what signalling reads
SNAPSHOT_MEMCELLS = 4,
// which way the switches are thrown
SNAPSHOT_SWITCHES = 5
};
// packs the current state of the world. only ever called on the authority
std::string take_snapshot();
// what a peer is meant to do with the blob it received
enum class snapshot_mode
{
// the peer is coming in cold and takes the world as it is given
join,
// routine correction on top of a simulation that is already running
correction
};
// 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);
struct snapshot_result
{
bool ok{false};
// how far the worst vehicle was from where the authority says it is
double worst_position_error{0.0};
uint32_t vehicles{0};
uint32_t repositioned{0};
};
// packs the state of the world. only ever called on the authority.
// a full snapshot carries everything and is what a joining peer gets; otherwise only what
// has changed since the last one goes out, which is what keeps the routine correction
// stream small
std::string take_snapshot(bool Full);
// restores state handed to us by the authority
snapshot_result apply_snapshot(std::string const &Blob, snapshot_mode Mode);
// forgets what was last sent, so that the next delta comes out complete
void reset_snapshot_history();
} // namespace network

View File

@@ -370,6 +370,8 @@ struct global_settings {
bool network_snapshot_applied = false;
// identity handed out by the server, presented again when reconnecting
uint64_t network_session_token = 0;
// how far the worst vehicle was out of place when the last correction arrived, metres
float network_position_error = 0.0f;
std::unordered_map<int, std::string> trainset_overrides;

View File

@@ -24,7 +24,10 @@ http://mozilla.org/MPL/2.0/.
ui::multiplayer_lobby_panel::multiplayer_lobby_panel()
: ui_panel(STR_C("Multiplayer lobby"), false)
{
size_min = {520, 280};
// both bounds have to be set: ui_panel feeds them straight to imgui, and a maximum
// left at its -1 default is smaller than the minimum, which collapses the window
size_min = {560, 320};
size_max = {1920, 1080};
}
std::string ui::multiplayer_lobby_panel::describe_crew(network::NetworkEntityId const Entity) const
@@ -62,7 +65,8 @@ void ui::multiplayer_lobby_panel::render_contents()
}
ImGui::Text("%s: %s, peer %u", STR_C("Session"), host ? (client ? "host + client" : "host") : "client", Global.network_peer_id);
ImGui::TextUnformatted(Global.SceneryFile.c_str());
if (!Global.SceneryFile.empty())
ImGui::TextDisabled("%s", Global.SceneryFile.c_str());
if (!Global.network_lobby_message.empty())
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.3f, 1.0f), "%s", Global.network_lobby_message.c_str());
@@ -72,16 +76,22 @@ void ui::multiplayer_lobby_panel::render_contents()
auto const &entries = network::Entities.entries();
if (entries.empty())
{
ImGui::TextUnformatted(STR_C("Waiting for the vehicle list from the server..."));
ImGui::TextUnformatted(client ? STR_C("Waiting for the vehicle list from the server...") : STR_C("This scenario has no vehicles anybody could drive."));
return;
}
network::NetworkEntityId const ownvehicle{network::Crews.vehicle_of(Global.network_peer_id)};
float const scale = std::max(1.0f, Global.ui_scale);
float const crewwidth = 60.0f * scale;
float const statewidth = 150.0f * scale;
float const actionwidth = 110.0f * scale;
ImGui::Columns(4, "mp_vehicles", false);
ImGui::SetColumnWidth(1, 70.0f * Global.ui_scale);
ImGui::SetColumnWidth(2, 200.0f * Global.ui_scale);
ImGui::SetColumnWidth(3, 130.0f * Global.ui_scale);
ImGui::SetColumnWidth(0, std::max(120.0f * scale, ImGui::GetWindowWidth() - crewwidth - statewidth - actionwidth - 30.0f * scale));
ImGui::SetColumnWidth(1, crewwidth);
ImGui::SetColumnWidth(2, statewidth);
ImGui::SetColumnWidth(3, actionwidth);
ImGui::TextDisabled("%s", STR_C("Vehicle"));
ImGui::NextColumn();
@@ -111,14 +121,14 @@ void ui::multiplayer_lobby_panel::render_contents()
if (entry.id == ownvehicle)
{
if (ImGui::Button(STR_C("Leave"), ImVec2(-1, 0)))
if (ImGui::Button(STR_C("Leave")))
Application.request_vehicle_leave(entry.id);
}
else if (entry.claimable)
{
// a vehicle somebody else is already working on can still be joined, that is
// what the crew capacity is for
if (ImGui::Button(entry.crew_count > 0 ? STR_C("Join crew") : STR_C("Enter"), ImVec2(-1, 0)))
if (ImGui::Button(entry.crew_count > 0 ? STR_C("Join crew") : STR_C("Enter")))
claim(entry.id, ownvehicle);
}
else
@@ -131,6 +141,9 @@ void ui::multiplayer_lobby_panel::render_contents()
}
ImGui::Columns(1);
ImGui::Separator();
ImGui::TextDisabled("%s", STR_C("You can also walk up to a vehicle and use the enter-vehicle key."));
}
void ui::multiplayer_lobby_panel::claim(network::NetworkEntityId const Entity, network::NetworkEntityId const Current)