Three things that turn out to be one thing: a session with people in it needs
to be able to say who they are, and a machine that only referees one should not
have to open a window to do it.
Names. A peer id is what the protocol addresses; "player 4" is no use to anybody
reading a lobby. CLIENT_HELLO carries a requested nickname now, and the server
decides what it actually is: trimmed of control characters and runs of spaces,
capped at 24, and made unique - two people called Marcin are two people and the
lobby has to say which is which. The result goes out as PEER_ROSTER whenever
somebody joins or leaves, and to a joining peer just before its snapshot, so the
crew section resolves to names rather than numbers. The name comes from
multiplayer.nickname in the ini or --nick on the command line, falling back to
the account name and, failing that, to the old numbering. The host is a peer
like any other here too, and registers its own name at startup.
Chat, on the key left of 1. Going up a line carries only its text: who said it
is decided by the connection it arrived on, never by the field in the packet,
which is what stops anybody putting words in anybody's mouth. The server tidies
and caps it, says it, and passes it on - a client waits to hear itself back, so
that everybody sees the conversation in the same order. While the box has the
keyboard imgui swallows the input before the driver mode sees it, so typing "d"
types "d" instead of opening the doors of whatever you are driving; that also
means the key that opened the panel cannot close it, hence escape.
And --nogui. No glfw, no window, no imgui, the null renderer, sound off, log to
the console it was started from, and nobody in a cab - the scenario is left to
its own drivers and this process is a referee rather than a participant. Every
window-dependent path is guarded on an empty window list rather than on a flag
scattered through the code, and the loop is paced at fifty steps a second
because nothing is waiting on a swapchain to slow it down. Chat lines go to the
log, which is what a machine with no window has instead of a chat panel.
eu07.exe -s scenario.scn --host 0.0.0.0:7420 --nogui --nick Dispatcher
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
Every peer used to evaluate event conditions for itself and put the results
straight into its own queue, so the same event could fire twice, or on one
machine only. Worse, the launchers were polled around Global.pCamera, which in
a session means "wherever one particular player happens to be looking".
Condition evaluation is now separated from execution:
authority: evaluate -> queueevent -> execute -> replicate
client: receive queueevent -> execute
- AddToQuery() takes the origin of the request. A locally evaluated launch on a
client is dropped, on the host it is handed to the session as the ordinary
queueevent command and comes back to be carried out by everybody, itself
included, so it happens exactly once per peer. Scenario init (onstart,
negative delay) and the children of an already replicated event are marked as
authority decisions and run in place, since they are identical everywhere.
With no session running, both branches collapse to the old behaviour
- queueevent now carries the delay as well
- event launchers are polled around every crewed vehicle instead of around the
local camera, and a cell is polled at most once per frame no matter how many
players stand near it - launcher activation counters are stateful
- Events.update() still services the queue on a client, with its delays and its
execution; only the condition checking is skipped there
An event whose request is still travelling is not requested again, with a
timeout so that a lost request cannot silence it for good. Instant (AddValues)
events are exempt from that guard, because every one of their additions has to
get through.
A vehicle is not owned by one player: two people share a cab, so the session
needs a crew relation rather than an owner field. The server keeps it and is
the only one allowed to change it.
- network/session: vehicle_crew { entity_id, crew, ai_before_claim } and a
crew_registry holding the roster. the host goes through exactly the same
path as a remote peer (claim -> crew -> grant), only without transport
- CLAIM_VEHICLE / CLAIM_GRANTED / CLAIM_DENIED / LEAVE_VEHICLE / CREW_UPDATE;
the server validates the vehicle exists, is present in the scenario and has
room in the crew, and answers a refusal with a reason the lobby shows
- the cab of a claimed vehicle is created by the authority posting the
ordinary replicated entervehicle, so every peer builds it and the vehicle is
never moved. the claiming peer only walks in once the cab appears locally
- AI transitions follow the crew size: 0 -> 1 remembers whether an AI was
really driving and takes the vehicle over, 1 -> 2 and 2 -> 1 change nothing,
1 -> 0 hands the vehicle back but only if there was an AI driver to begin
with. driver mode no longer fires aidriverdisable of its own in multiplayer
- losing a connection removes that peer from every crew it was on; a vehicle
with somebody still on board carries on untouched
- the lobby lists who is on board, offers "join crew" for a vehicle another
player already occupies, and lets a player step off
The AI and cab handling runs as a reconciliation pass rather than as edge
triggered actions, so a request that has not taken effect yet (the cab needs a
frame to appear) is simply repeated, with a cooldown to keep it off the wire.
TTrain::id() numbers cabs with a local counter, in the order each peer happens
to create them, so it cannot address a vehicle across the session. Neither was
there any way for a player to see what is available to drive.
- network/entities: PeerId (the host is peer 1, remote peers are numbered from
2, so it is a participant rather than a special case) and NetworkEntityId,
plus a registry that numbers the drivable vehicles of the loaded scenario in
name order and resolves ids back to TDynamicObject
- the server owns the numbering and publishes it as VEHICLE_LIST - name, crew
occupancy, crew capacity, AI state, availability - on every change and at
least every 120 frames, out of band so it stays out of the replay backbuffer;
a client binds the received ids to its own copy of the scenario by name
- SERVER_HELLO hands the joining peer its PeerId
- a multiplayer lobby listing that roster, with vehicles taken by other players
shown as such rather than hidden; entering one goes through the existing
entervehicle command, the vehicle is never moved
The lobby lives inside the driver mode instead of a mode of its own: the world
has to keep running while somebody is choosing (a client stops consuming the
server's frames the moment its mode is not a command processor), and the same
screen is what a player needs to change vehicles later on.
Crew occupancy is still derived from the presence of a cab instance; the real
crew model, claim handshake and AI takeover follow in stage 3.