Round four reached one step from the race: eggs delivered, both
members ACKed with complete meshes, one member entered LoadingMission.
Two failures remained, both now fixed.
One: Steam reports incoming callers under locally-allocated ALIAS
FakeIPs, not their global ones - the owner accepted both mesh legs
but its identity check compared the alias against the egg address and
never counted the connections (no Connected to GameMachineHost on the
owner). The peer table now carries each member SteamID (lobby go
roster gained a field) and Accept resolves the caller identity back
to the global FakeIP the egg promised.
Two: mission load stalls the game thread for 10-30s with nothing
pumping, and Steam default 10s connected-timeout sheared every
connection mid-load (end reason 4001, rx ages 11.5-20.5s - right at
load duration). Connected timeout is now 90s; TCP never timed out an
idle arcade link and races pump every frame once running.
Self-test still green (ping, survives listener close).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round-three diagnosis from the three-machine logs: the console
channel connected first try on SDR both ways - then died seconds
later with end reason 5010 (PeerSentNoConnection). Cause: the arcade
engine closes its console LISTENER the moment the console connects
(correct under TCP, where accepted sockets outlive the listener), but
Steam''s CloseListenSocket closes all accepted connections
ungracefully. The members silently killed their console connection at
accept; the owner''s next packet got no-connection back, no eggs were
ever fed, and all three pods sat waiting.
The transport now bridges the semantics: an engine close only marks
the listener (new callers are rejected, pending queue dropped); the
Steam socket is destroyed in Cleanup at mission teardown. Re-listening
on the same engine port reopens the marked listener.
The loopback self-test now covers exactly this: accept, close the
listener, then push data both ways over the accepted connection -
PASSED (ping 1, survives listener close 1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of the three-machine failure found and fixed. The
connection-status callback was registered as a per-connection config
value pointer - which the steam_api flavor of the library never
dispatches (only the standalone lib does). So members never saw the
incoming connection request, never accepted, and the owner timed out
after 120s of state-5 retries. Valve''s own SpaceWar example registers
SteamNetConnectionStatusChangedCallback_t with STEAM_CALLBACK; the
transport now does the same through a CCallback listener constructed
after SteamAPI_Init.
Proven with the new RP412STEAMSELFTEST=1 loopback: one machine
listens on its fake console port and dials its own FakeIP - the log
now shows incoming/accept/connected on both ends and a verified data
round trip, connect succeeded on attempt 1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The three-machine test failed at exactly one step: the owner''s
ConnectByIPAddress to a member''s fake console port retried silently
for 120s while the member WAS listening on that port. The transport
threw away Steam''s reason for killing each attempt.
Now every connection state change logs the connection description,
and drops log the end reason plus Steam''s debug string - the next
run will say exactly which subsystem refused (cert, FakeIP directory,
routing, accept). Connect also polls GetConnectionInfo directly
instead of trusting only the callback flags, logs each attempt''s
final state, no longer holds a pointer into the connection table
across mutations, and paces retries at 1s.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RPL4LOBBY implements the multiplayer front door on ISteamMatchmaking.
The setup menu grows HOST STEAM RACE / JOIN STEAM RACE buttons when
the Steam wire is live; hosting creates a tagged public lobby, joining
finds one. Every member publishes FakeIP + fake ports + persona +
loadout as member data; the room screen lists members (host marked)
and gives the owner a launch button.
Launching writes a nonced go-roster into lobby data. Each pod
registers every peer with the Steam transport (two-port peer table:
engine console/game ports map to Steam fake ports on connect) and
enters the race: the owner through the hosted-race path - it builds
the multi-pilot egg from real personas and loadouts and its console
marshals everyone - and members as network pods that boot straight
into WaitingForEgg for the owner to feed over the wire.
The lobby outlives races: members loop back through WinMain into the
room (no local console needed - MissionCompleted is waived for member
races), and the owner returns to the room after its results screen.
Leaving the lobby clears the hosted-race priming.
Verified on this box: menu buttons appear under RP412STEAM=1, hosting
creates a lobby on the Steam backend, the room runs and leaves back to
the menu; single-player cycling and the LAN hosted race both still
pass. Full three-account mesh test is next, on real hardware.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Steamworks SDK 1.64 vendored at extern/steamworks_sdk_164 (headers +
win32 redistributables only; .gitignore trims the rest). Both projects
build with RP412_STEAM; activation stays behind the RP412STEAM=1
environment switch, so plain desktop runs never touch Steam.
L4STEAMTRANSPORT.cpp implements NetTransport on ISteamNetworkingSockets
with FakeIP: SteamNetTransport_Install brings up SteamAPI, relay
network access, and a two-port FakeIP identity (fake port 0 = console
channel, 1 = game mesh), then swaps the process wire; any failure logs
the reason and the game carries on over TCP. Addressing keeps the
engine untouched: all pods share the -net port convention, eggs carry
fakeip:engineport, and the transport alone translates engine ports to
Steam fake ports via the lobby-fed peer table (RegisterPeer). Connect
mirrors the TCP retry-while-refused loop; Receive normalizes message
lanes back into the stream semantics CheckBuffers expects.
Runtime verified on this box: RP412STEAM=1 under AppID 480 came up as
169.254.59.52 (fake ports 32256/32257); without Steam credentials it
falls back to TCP cleanly; default boot logs no Steam lines at all.
steam_api.dll ships in the dist.
Next: the lobby layer (ISteamMatchmaking member data -> RegisterPeer +
egg build + RPL4CONSOLE marshal), which needs a second account to test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>