17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 15:39:20 +02:00

multiplayer: a player works a train, not a car - and put the corrections right

Second live test. Four things, three of them the same mistake seen from
different angles: the session was reasoning about single vehicles where it
should have been reasoning about trains.

- a peer may now operate anything coupled into the set it is sitting in. This
  is what "cabchangebackward rejected: not a member of the crew of that
  vehicle" was: the crew was recorded against one car of a four-car unit, and
  the cab switch addressed another
- every vehicle gets a network id, not only the ones worth driving. Commands
  are addressed by that id, and a player has to be able to walk into any car
  of their train. "peer 3 cannot take en71-017rb_4: no such vehicle in this
  session" was the driving trailer of an EN71 - a full desk, no engine, and
  nobody assigned to it, so the old test missed it. Having a driving position
  is what counts now, engine or not
- the roster groups vehicles into the trains they are coupled into and the
  lobby lists one row per train. Coupling is worked out afresh every time, so
  a train that comes apart becomes two, and because a crew entry sits on the
  car its player occupies, each half keeps the people who were riding in it
- walking to another car of your own train is not a new claim and asks nothing
  of the crew capacity, which counts people per train

Positional correction never actually did anything: it identified a track by
name, and most track in a scenery has none, so the lookup returned nothing and
place_on_track() was never reached. The client sat 178 m out of place for
minutes, reporting four vehicles "put back on the rails" every time while
moving none of them. Tracks now travel as their index in the path table, which
comes from the scenery file and is therefore the same on every peer.

Also from the test:

- the camera jumped in and out of the cab on the client whenever the host
  reversed, because the correction was forcing the server's CabActive and
  CabOccupied onto it. Which cab somebody sits in is theirs alone - two people
  sharing a vehicle each have their own - so it is no longer replicated
- the correction no longer touches the control positions of the train the local
  player is working. Their crew's commands reach every peer on their own, and
  snapping a lever back a fraction of a second after it was moved is what made
  the reverser look dead
- the frame buffer was sized for the old lockstep, up to sixty frames deep.
  Every frame in it is input lag, so it is down to twelve with a target of one
- asking the AI of a vehicle to step aside is given five tries and then said
  once, instead of every thirty frames forever; and a resync request now has a
  real cooldown
This commit is contained in:
2026-09-06 23:39:13 +00:00
parent a72c3292ad
commit 0b75e701fc
9 changed files with 357 additions and 52 deletions

View File

@@ -44,10 +44,11 @@ bool is_drivable(TDynamicObject const *Vehicle)
if (Vehicle == nullptr || Vehicle->MoverParameters == nullptr)
return false;
// anything with its own drive, plus anything the scenario already put a driver in
// (this covers driving trailers of multiple units, which carry no engine of their own)
// TODO: a cab count exposed by TMoverParameters would be a more precise test
return Vehicle->MoverParameters->EngineType != TEngineType::None || Vehicle->Mechanik != nullptr;
auto const &mover = *Vehicle->MoverParameters;
// a driving position is what counts, not an engine: the driving trailer of a multiple
// unit has a full desk and no traction of its own, and the scenario puts nobody in it
return mover.EngineType != TEngineType::None || Vehicle->Mechanik != nullptr || mover.MainCtrlPosNo > 0 || mover.BrakeCtrlPosNo > 0;
}
void entity_registry::clear()
@@ -61,10 +62,12 @@ void entity_registry::build()
{
clear();
// every vehicle gets an id, not only the ones worth driving. commands are addressed by
// it, and a player working a train may walk into any of its cars
std::vector<std::string> names;
for (TDynamicObject *vehicle : simulation::Vehicles.sequence())
{
if (!is_drivable(vehicle))
if (vehicle == nullptr)
continue;
names.emplace_back(vehicle->name());
}
@@ -88,10 +91,22 @@ void entity_registry::build()
m_byname.emplace(name, entry.id);
}
if (m_entries.size() > 0xffff)
{
// the command channel packs the recipient into sixteen bits
ErrorLog("net: scenario holds more vehicles than the command channel can address", logtype::net);
}
reindex();
refresh();
WriteLog("net: assigned network ids to " + std::to_string(m_entries.size()) + " drivable vehicles", logtype::net);
uint32_t drivable{0};
for (auto const &entry : m_entries)
if (entry.drivable)
++drivable;
WriteLog("net: assigned network ids to " + std::to_string(m_entries.size()) + " vehicles, " + std::to_string(drivable) + " of them with a driving position",
logtype::net);
}
void entity_registry::adopt(std::vector<vehicle_entry> const &Entries)
@@ -112,15 +127,110 @@ void entity_registry::reindex()
void entity_registry::refresh()
{
// only there so that a malformed consist cannot spin us forever
int const CONSIST_LIMIT{256};
for (auto &entry : m_entries)
{
TDynamicObject const *vehicle = resolve(entry.id);
entry.consist_id = entry.id;
entry.consist_size = 1;
entry.consist_lead = false;
entry.drivable = is_drivable(vehicle);
entry.ai_active = (vehicle != nullptr && vehicle->Mechanik != nullptr && vehicle->Mechanik->AIControllFlag);
entry.crew_count = Crews.count(entry.id);
entry.crew_capacity = CREW_CAPACITY;
entry.claimable = (vehicle != nullptr && entry.crew_count < entry.crew_capacity);
}
// group the vehicles into the trains they are currently coupled into. this is redone
// every time, so a train that comes apart turns into two trains here, and the crews
// follow whichever half each player happens to be sitting in
std::unordered_set<TDynamicObject const *> visited;
for (auto const &entry : m_entries)
{
TDynamicObject *vehicle = resolve(entry.id);
if (vehicle == nullptr || visited.count(vehicle) > 0)
continue;
TDynamicObject *front = vehicle;
for (int step = 0; step < CONSIST_LIMIT; ++step)
{
TDynamicObject *previous = front->Prev();
if (previous == nullptr || previous == vehicle || visited.count(previous) > 0)
break;
front = previous;
}
std::vector<NetworkEntityId> members;
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);
auto const id = id_of(member->name());
if (id != ENTITY_NONE)
members.emplace_back(id);
}
if (members.empty())
continue;
// the lowest id in the set names the train, and the lowest one that can actually
// be driven stands for it in the lobby
NetworkEntityId consist{members.front()};
NetworkEntityId lead{ENTITY_NONE};
uint8_t crew{0};
for (auto const id : members)
{
consist = std::min(consist, id);
crew = (uint8_t)std::min<int>(255, crew + Crews.count(id));
auto const *candidate = find(id);
if (candidate != nullptr && candidate->drivable && (lead == ENTITY_NONE || id < lead))
lead = id;
}
if (lead == ENTITY_NONE)
lead = consist;
for (auto const id : members)
{
auto const lookup = m_byid.find(id);
if (lookup == m_byid.end())
continue;
auto &target = m_entries[lookup->second];
target.consist_id = consist;
target.consist_size = (uint8_t)std::min<size_t>(255, members.size());
target.consist_lead = (id == lead);
target.crew_count = crew;
target.claimable = (crew < target.crew_capacity);
}
}
}
NetworkEntityId entity_registry::consist_of(NetworkEntityId Id) const
{
auto const *entry = find(Id);
return entry != nullptr ? entry->consist_id : ENTITY_NONE;
}
std::vector<NetworkEntityId> entity_registry::consist_members(NetworkEntityId Consist) const
{
std::vector<NetworkEntityId> members;
if (Consist == ENTITY_NONE)
return members;
for (auto const &entry : m_entries)
{
if (entry.consist_id == Consist)
members.emplace_back(entry.id);
}
return members;
}
NetworkEntityId entity_registry::id_of(std::string const &Name) const

View File

@@ -36,20 +36,35 @@ constexpr NetworkEntityId ENTITY_NONE = 0;
// so raising this number does not call for a structural change
constexpr uint8_t CREW_CAPACITY = 2;
// authoritative description of a single drivable vehicle, as published by the server
// authoritative description of a single vehicle, as published by the server.
// every vehicle is listed, not only the ones worth driving: commands are addressed by
// this id, and a player may walk into any member of the train they are working
struct vehicle_entry
{
NetworkEntityId id{ENTITY_NONE};
std::string name;
// the train this vehicle is currently coupled into, named by the lowest id in the set.
// a player belongs to a train, not to a single car, so this is what the crew, the
// permissions and the lobby all work with. it is recomputed as vehicles couple and
// uncouple, which is what splits a crew between the halves of a train that came apart
NetworkEntityId consist_id{ENTITY_NONE};
// how many vehicles that train has, and whether this entry is the one that stands for
// it in the lobby
uint8_t consist_size{1};
bool consist_lead{false};
// crew of the whole train, not of this vehicle alone
uint8_t crew_count{0};
uint8_t crew_capacity{CREW_CAPACITY};
bool ai_active{false};
bool claimable{false};
// has a driving position of its own
bool drivable{false};
bool operator==(vehicle_entry const &Other) const
{
return id == Other.id && name == Other.name && crew_count == Other.crew_count && crew_capacity == Other.crew_capacity && ai_active == Other.ai_active &&
claimable == Other.claimable;
return id == Other.id && name == Other.name && consist_id == Other.consist_id && consist_size == Other.consist_size && consist_lead == Other.consist_lead &&
crew_count == Other.crew_count && crew_capacity == Other.crew_capacity && ai_active == Other.ai_active && claimable == Other.claimable &&
drivable == Other.drivable;
}
bool operator!=(vehicle_entry const &Other) const { return !(*this == Other); }
};
@@ -74,6 +89,9 @@ public:
NetworkEntityId id_of(std::string const &Name) const;
NetworkEntityId id_of(TDynamicObject const *Vehicle) const;
// the train a vehicle belongs to, and everything coupled into it
NetworkEntityId consist_of(NetworkEntityId Id) const;
std::vector<NetworkEntityId> consist_members(NetworkEntityId Consist) const;
std::string name_of(NetworkEntityId Id) const;
TDynamicObject *resolve(NetworkEntityId Id) const;
vehicle_entry const *find(NetworkEntityId Id) const;
@@ -98,7 +116,8 @@ bool is_multiplayer();
// of a session do; a client does not, it only carries out what the server hands it
bool is_authority();
// true for vehicles a player can take over
// true for vehicles that carry a driving position of their own. note that a driving
// trailer has one without having an engine, and without the scenario putting a driver in
bool is_drivable(TDynamicObject const *Vehicle);
} // namespace network

View File

@@ -57,9 +57,11 @@ void network::vehicle_list::serialize(std::ostream &stream) const
{
sn_utils::ls_uint32(stream, entry.id);
sn_utils::s_str(stream, entry.name);
sn_utils::ls_uint32(stream, entry.consist_id);
sn_utils::s_uint8(stream, entry.consist_size);
sn_utils::s_uint8(stream, entry.crew_count);
sn_utils::s_uint8(stream, entry.crew_capacity);
sn_utils::s_uint8(stream, (uint8_t)((entry.ai_active ? 1 : 0) | (entry.claimable ? 2 : 0)));
sn_utils::s_uint8(stream, (uint8_t)((entry.ai_active ? 1 : 0) | (entry.claimable ? 2 : 0) | (entry.drivable ? 4 : 0) | (entry.consist_lead ? 8 : 0)));
}
}
@@ -75,11 +77,15 @@ void network::vehicle_list::deserialize(std::istream &stream)
vehicle_entry entry;
entry.id = sn_utils::ld_uint32(stream);
entry.name = sn_utils::d_str(stream);
entry.consist_id = sn_utils::ld_uint32(stream);
entry.consist_size = sn_utils::d_uint8(stream);
entry.crew_count = sn_utils::d_uint8(stream);
entry.crew_capacity = sn_utils::d_uint8(stream);
uint8_t const flags = sn_utils::d_uint8(stream);
entry.ai_active = (flags & 1) != 0;
entry.claimable = (flags & 2) != 0;
entry.drivable = (flags & 4) != 0;
entry.consist_lead = (flags & 8) != 0;
vehicles.emplace_back(entry);
}
}

View File

@@ -524,8 +524,12 @@ void network::client::handle_message(std::shared_ptr<connection> conn, const mes
// 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;
if ((bad_corrections >= RESYNC_BAD_CORRECTIONS)
&& (last_resync_tick == 0 || Global.simulation_tick > last_resync_tick + RESYNC_COOLDOWN_TICKS)) {
bad_corrections = 0;
last_resync_tick = Global.simulation_tick;
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);

View File

@@ -101,9 +101,12 @@ namespace network
size_t reconnect_delay = 0;
const size_t RECONNECT_DELAY_FRAMES = 60;
const float MAX_BUFFER_SIZE = 60.0f;
// the queue of authoritative frames waiting to be played out. every frame sitting
// in here is a frame of delay between pressing a key and seeing it happen, so it
// is kept as short as the connection allows rather than a comfortable second deep
const float MAX_BUFFER_SIZE = 12.0f;
const float JITTERINESS_MIX = 0.998f;
const float TARGET_MIN = 2.0f;
const float TARGET_MIN = 1.0f;
const float TARGET_MIX = 0.98f;
const float JITTERINESS_MULTIPIER = 2.0f;
const float CONSUME_MULTIPIER = 0.05f;
@@ -113,10 +116,13 @@ namespace network
// 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;
uint64_t last_resync_tick = 0;
static constexpr double RESYNC_POSITION_ERROR = 25.0;
static constexpr int RESYNC_BAD_CORRECTIONS = 5;
// a correction needs time to take hold; asking again before it has is pointless
static constexpr uint64_t RESYNC_COOLDOWN_TICKS = 600;
float last_target = 20.0f;
float last_target = 2.0f;
float jitteriness = 1.0f;
float consume_counter = 0.0f;

View File

@@ -70,8 +70,7 @@ command_verdict validate_command(PeerId Peer, user_command Command, uint32_t Rec
if (target == command_target::vehicle)
{
// a player works the controls of the vehicle they are on, and of no other
return Crews.is_member(Peer, (NetworkEntityId)(Recipient & 0xffff)) ? command_verdict::accepted : command_verdict::not_in_crew;
return may_control(Peer, (NetworkEntityId)(Recipient & 0xffff)) ? command_verdict::accepted : command_verdict::not_in_crew;
}
if (target == command_target::simulation)
@@ -93,11 +92,24 @@ claim_result claim_by_name(PeerId Peer, std::string const &Vehicle, NetworkEntit
return claim_result::unknown_vehicle;
auto const previous = Crews.vehicle_of(Peer);
if (previous == Entity)
return claim_result::already_member;
if ((previous != ENTITY_NONE) && (Entities.consist_of(previous) == Entities.consist_of(Entity)))
{
// walking to another car of the train you are already working is not a new claim,
// so it asks nothing of the capacity. the seat still moves, because which car a
// player sits in decides which half they keep when the train comes apart
Crews.leave(Peer, previous);
Crews.take_seat(Peer, Entity);
return claim_result::granted;
}
auto const result = Crews.claim(Peer, Entity);
if ((result == claim_result::granted) && (previous != ENTITY_NONE) && (previous != Entity))
if ((result == claim_result::granted) && (previous != ENTITY_NONE))
{
// nobody works two vehicles at once
// nobody works two trains at once
Crews.leave(Peer, previous);
}
@@ -152,6 +164,21 @@ void filter_commands(PeerId Peer, command_queue::commands_map &Commands)
}
}
bool may_control(PeerId Peer, NetworkEntityId Entity)
{
auto const seat = Crews.vehicle_of(Peer);
if (seat == ENTITY_NONE)
return false;
auto const train = Entities.consist_of(Entity);
if (train == ENTITY_NONE)
return false;
// the whole set the player is sitting in, so that the desk of a driving trailer works
// on the motor cars behind it and the cab switches of a multiple unit go through
return Entities.consist_of(seat) == train;
}
vehicle_crew &crew_registry::entry(NetworkEntityId Id)
{
auto lookup = m_vehicles.find(Id);
@@ -178,7 +205,8 @@ claim_result crew_registry::claim(PeerId Peer, NetworkEntityId Id)
if (std::find(vehicle.crew.begin(), vehicle.crew.end(), Peer) != vehicle.crew.end())
return claim_result::already_member;
if (vehicle.crew.size() >= published->crew_capacity)
// capacity counts the people on the whole train, not on this one car
if (published->crew_count >= published->crew_capacity)
return claim_result::crew_full;
if (vehicle.crew.empty())
@@ -373,6 +401,23 @@ void crew_registry::mirror(NetworkEntityId Id, std::vector<PeerId> const &Crew)
entry(Id).crew = Crew;
}
void crew_registry::take_seat(PeerId Peer, NetworkEntityId Id)
{
auto &vehicle = entry(Id);
if (std::find(vehicle.crew.begin(), vehicle.crew.end(), Peer) != vehicle.crew.end())
return;
if (vehicle.crew.empty())
{
TDynamicObject const *dynamic = Entities.resolve(Id);
vehicle.ai_before_claim = (dynamic != nullptr && dynamic->Mechanik != nullptr && dynamic->Mechanik->AIControllFlag);
vehicle.ai_restore_pending = false;
}
vehicle.crew.emplace_back(Peer);
m_pending.emplace_back(Id);
}
std::vector<NetworkEntityId> crew_registry::take_pending_updates()
{
std::vector<NetworkEntityId> pending;
@@ -425,6 +470,8 @@ void post_authority_command(user_command Command, uint32_t Recipient, glm::vec3
// frames to wait before repeating an authority request that has not taken effect yet
static int const ACTION_COOLDOWN_FRAMES = 30;
// how many times to ask a vehicle's AI driver to step aside before giving up on it
static int const AI_HANDOVER_ATTEMPTS = 5;
void crew_registry::reconcile()
{
@@ -457,10 +504,26 @@ void crew_registry::reconcile()
if (dynamic->Mechanik != nullptr && dynamic->Mechanik->AIControllFlag)
{
// first human on board takes over from the AI
WriteLog("net: " + dynamic->name() + " taken over from the AI driver", logtype::net);
post_authority_command(user_command::aidriverdisable, (uint32_t)command_target::vehicle | train->id(), dynamic->GetPosition(), std::string());
vehicle.action_cooldown = ACTION_COOLDOWN_FRAMES;
if (vehicle.ai_attempts < AI_HANDOVER_ATTEMPTS)
{
// first human on board takes over from the AI
if (vehicle.ai_attempts == 0)
WriteLog("net: " + dynamic->name() + " taken over from the AI driver", logtype::net);
++vehicle.ai_attempts;
post_authority_command(user_command::aidriverdisable, (uint32_t)command_target::vehicle | train->id(), dynamic->GetPosition(), std::string());
vehicle.action_cooldown = ACTION_COOLDOWN_FRAMES;
}
else if (vehicle.ai_attempts == AI_HANDOVER_ATTEMPTS)
{
// asking again would only fill the log; say so once and leave it
++vehicle.ai_attempts;
ErrorLog("net: " + dynamic->name() + " will not let go of its AI driver", logtype::net);
}
}
else
{
vehicle.ai_attempts = 0;
}
continue;
}

View File

@@ -34,6 +34,9 @@ struct vehicle_crew
// keeps reconcile() from re-posting the same request every single frame while the
// previous one is still on its way through the command queue
int action_cooldown{0};
// how many times we have asked the AI to hand this vehicle over. if it will not, that
// is worth saying once rather than asking forever
int ai_attempts{0};
};
enum class claim_result
@@ -74,6 +77,9 @@ public:
// client side: takes over the crew of one vehicle as published by the server
void mirror(NetworkEntityId Id, std::vector<PeerId> const &Crew);
// puts a peer on a vehicle without asking about capacity; used when somebody walks to
// another car of the train they are already working
void take_seat(PeerId Peer, NetworkEntityId Id);
// server side: brings the world in line with the crew roster - creates the cab of a
// claimed vehicle, hands the vehicle over from the AI and hands it back when the last
@@ -138,6 +144,10 @@ 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);
// a player works a train, not a single car: they may operate anything coupled into the
// set they are sitting in, which is what makes the cab switches of a multiple unit work
bool may_control(PeerId Peer, NetworkEntityId Entity);
// 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

View File

@@ -38,6 +38,45 @@ double const REPOSITION_TOLERANCE_CORRECTION{2.5};
// ---------------------------------------------------------------------------
// 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);
}
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;
@@ -137,7 +176,7 @@ void read_session(std::istream &Stream, network::snapshot_mode const Mode)
struct vehicle_state
{
std::string name;
std::string track;
uint32_t track{0xffffffff};
double translation{0.0};
uint8_t axlefirst{0};
uint8_t direction{0};
@@ -164,8 +203,6 @@ struct vehicle_state
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}};
@@ -179,7 +216,7 @@ struct vehicle_state
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_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);
@@ -204,8 +241,6 @@ void serialize_vehicle(std::ostream &Stream, vehicle_state const &State)
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]);
@@ -227,7 +262,7 @@ 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.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);
@@ -252,8 +287,6 @@ vehicle_state deserialize_vehicle(std::istream &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);
@@ -279,7 +312,7 @@ vehicle_state read_from(TDynamicObject const &Vehicle)
vehicle_state state;
state.name = Vehicle.name();
state.track = (track != nullptr ? track->name() : std::string());
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);
@@ -304,8 +337,6 @@ vehicle_state read_from(TDynamicObject const &Vehicle)
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;
@@ -324,19 +355,41 @@ vehicle_state read_from(TDynamicObject const &Vehicle)
return state;
}
// true for the train the local player is working. its controls are driven by the commands
// its crew issue, which every peer already receives, so the correction must keep its hands
// off them - otherwise it keeps snapping a lever back a fraction of a second after the
// player moved it, and the control looks broken
bool is_own_train(std::string const &Name)
{
auto const seat = network::Crews.vehicle_of(Global.network_peer_id);
if (seat == network::ENTITY_NONE)
return false;
auto const train = network::Entities.consist_of(network::Entities.id_of(Name));
return (train != network::ENTITY_NONE) && (network::Entities.consist_of(seat) == train);
}
// 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;
// what the physics produced is always taken from the authority; what a person set is
// not, on the train that person is working
bool const ours = is_own_train(State.name);
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;
if (!ours)
{
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;
@@ -345,6 +398,14 @@ void apply_controls(TDynamicObject &Vehicle, vehicle_state const &State)
mover.Compressor = State.compressor;
mover.CompressedVolume = State.compressedvolume;
if (ours)
{
// the rest of it is what the crew set, and their commands reach us on their own
if (Vehicle.Mechanik != nullptr && Vehicle.Mechanik->AIControllFlag != State.aiactive)
Vehicle.Mechanik->TakeControl(State.aiactive);
return;
}
// 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
@@ -357,8 +418,9 @@ void apply_controls(TDynamicObject &Vehicle, vehicle_state const &State)
if (mover.CompressorAllow != State.compressorallow)
mover.CompressorSwitch(State.compressorallow, range_t::local);
mover.CabActive = State.cabactive;
mover.CabOccupied = State.caboccupied;
// 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)
{
@@ -388,9 +450,17 @@ void apply_controls(TDynamicObject &Vehicle, vehicle_state const &State)
// 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));
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);

View File

@@ -32,7 +32,14 @@ ui::multiplayer_lobby_panel::multiplayer_lobby_panel()
std::string ui::multiplayer_lobby_panel::describe_crew(network::NetworkEntityId const Entity) const
{
auto const crew = network::Crews.crew_of(Entity);
// the people on the whole train, wherever in it they happen to be sitting
std::vector<network::PeerId> crew;
for (auto const member : network::Entities.consist_members(network::Entities.consist_of(Entity)))
{
for (auto const peer : network::Crews.crew_of(member))
crew.emplace_back(peer);
}
if (crew.empty())
return std::string();
@@ -93,7 +100,7 @@ void ui::multiplayer_lobby_panel::render_contents()
ImGui::SetColumnWidth(2, statewidth);
ImGui::SetColumnWidth(3, actionwidth);
ImGui::TextDisabled("%s", STR_C("Vehicle"));
ImGui::TextDisabled("%s", STR_C("Train"));
ImGui::NextColumn();
ImGui::TextDisabled("%s", STR_C("Crew"));
ImGui::NextColumn();
@@ -102,11 +109,21 @@ void ui::multiplayer_lobby_panel::render_contents()
ImGui::NextColumn();
ImGui::Separator();
network::NetworkEntityId const owntrain{network::Entities.consist_of(ownvehicle)};
for (auto const &entry : entries)
{
// one row per train. a player takes the whole set, not a single car, and moves
// between its cars freely once they are on it
if (!entry.consist_lead)
continue;
ImGui::PushID(static_cast<int>(entry.id));
ImGui::TextUnformatted(entry.name.c_str());
if (entry.consist_size > 1)
ImGui::Text("%s +%u", entry.name.c_str(), (unsigned)(entry.consist_size - 1));
else
ImGui::TextUnformatted(entry.name.c_str());
ImGui::NextColumn();
ImGui::Text("%u/%u", (unsigned)entry.crew_count, (unsigned)entry.crew_capacity);
@@ -119,10 +136,10 @@ void ui::multiplayer_lobby_panel::render_contents()
ImGui::TextDisabled("%s", entry.ai_active ? STR_C("AI") : STR_C("free"));
ImGui::NextColumn();
if (entry.id == ownvehicle)
if (owntrain != network::ENTITY_NONE && entry.consist_id == owntrain)
{
if (ImGui::Button(STR_C("Leave")))
Application.request_vehicle_leave(entry.id);
Application.request_vehicle_leave(ownvehicle);
}
else if (entry.claimable)
{
@@ -143,7 +160,7 @@ 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."));
ImGui::TextDisabled("%s", STR_C("You can also walk up to a train and use the enter-vehicle key."));
}
void ui::multiplayer_lobby_panel::claim(network::NetworkEntityId const Entity, network::NetworkEntityId const Current)