7.2 KiB
Lane L2 — combat multiplayer: predictions, written before the run
Lane L2, 2026-09-08. Guest VM141 (sots-re-win10-b, ZFS clone of VM140, adopted this lane,
192.168.10.143). Committed before the game was launched for the multiplayer combat run.
Background: findings/subsystems/multiplayer-tier0-verified.md (lane W2 — Tier 0 verified: two
players, one guest, two turns, 44,319 packets and zero to any GameSpy port) and
findings/subsystems/multiplayer-gamespy.md (lane G2 — the service map and the direct-join arm).
W2 §8 named the gap: no combat. CombatHostPort 3370 was never bound because no battle ever
happened; the combat lockstep, with its 1000 ms CombatLatency and its own SyncCheckCombat, is
entirely untested. This lane attacks that.
Instrumentation, and why it cannot perturb (rule 19)
Three instruments, all outside the game process:
tcpdump -i tap141i0on spicy — the host side of the guest's NIC. Outside the guest entirely.Get-NetUDPEndpoint/Get-NetTCPConnectionpolled over SSH from a separate process. This is what W2 used to establish that 3369 binds and 3370 does not; it reads kernel tables and never touches the game.qm monitor <id> screendumpfor the screen — reads the QEMU framebuffer, no guest agent, no guest software.
shim.cfg is set to hooks=off, matching W2. The guest was cloned with lane W3's hooks=trace
config in place and it is removed before the run: a hook changed the game's behaviour earlier this
session, and the question here is what the shipped game does.
Known blind spot, stated up front, same as W2's: both instances live on one guest and talk over
127.0.0.1, so the game's own 3369/3370 exchange never reaches tap141i0. The tap answers "is there
GameSpy traffic"; the socket table answers "does 3370 bind".
P1 — CombatHostPort 3370 binds, and only when a battle launches
Game_LoadNetworkConfig (0x005a0610) defaults CombatHostPort to 3370 and there is no [Network]
section in this install's sots.ini, so 3370 is the compiled-in value in force. W2 showed it is
not bound at lobby time. ui-screen-map.md C2 puts the combat network session behind
SNMHostCombat → SNMHostCombatReply → SNMLaunchCombat / SNMJoinCombat, served by
CombatNetworkServer / CombatNetworkClient.
Predict: polling the UDP socket table across a battle shows 0.0.0.0:3370 (or 127.0.0.1:3370)
appear, owned by exactly one process — the combat host — no earlier than the combat-loading screen
(C2), and disappear again after the combat report (C4).
Falsified if: 3370 never appears at any point in a battle; or it appears at lobby time or at strategic-turn time (i.e. W2's observation that it is unbound at lobby was a timing artefact); or both processes bind it.
P2 — combat resolves in lockstep and both clients survive it
Predict: with both human players in the same battle, both instances load the combat screen (C3), run the simulation, reach the combat report (C4), and then both advance to the same next turn number on the strategy screen.
Falsified if: either instance hangs at COMBAT_WAITINGFORPLAYER / COMBAT_WAITINGFORSERVER
indefinitely; or either crashes; or a @SYNC ERROR appears (the string ui-screen-map.md C3 records
in CombatSim); or the two instances end on different turn numbers or different combat reports.
This is the headline. A failure here is the deliverable just as much as a success, and it must be characterised: which side stalls, at which screen, with what in the logs.
P3 — no GameSpy traffic at combat time
W2 proved zero GameSpy packets at lobby and strategic-turn time. Combat is a different code path.
G2 established that GameSpy_NNBeginNegotiationWithSocket (0x00412590) has exactly two call sites,
the browser-mediated join (0x00770e70) and the host reporting side (0x00787cb0), and that the
direct-join arm reaches neither — but neither call site was checked against the combat host/join
path, which is a distinct pair of messages.
Predict: across the whole combat window the tap shows zero packets to UDP 27900, UDP 27901,
TCP 28910, TCP 28900, TCP 6667, TCP 6500, and zero DNS A queries for any *.gamespy.com name —
in particular none for natneg1/natneg2, which W2 also never saw on the direct-join arm.
Falsified if: any packet to any of those ports, or any *.gamespy.com lookup, appears between
the encounter query and the combat report.
P4 — combat traffic stays on loopback and never reaches the tap (control)
Predict: the tap shows no UDP 3369 and no UDP 3370 traffic at all during the battle, because both
endpoints are on the same host. This is the control that tells P3's zero apart from "the capture was
not looking at the right thing": DNS, the MOTD fetch to www.kerberos-productions.com, and Windows
telemetry will be visible on the same capture, so the instrument is demonstrably live.
Falsified if: 3369/3370 packets appear on the tap. That would mean the combat host announces or
broadcasts to the LAN — itself worth knowing, since LanScanPortRange covers 3369..3370 and the
LAN browser scans both.
P5 — the encounter query reaches both players
ui-screen-map.md §2.2 step 4: each encounter raises C1 for every involved player
(SNMDoEncounterQuery), and results come back as SNMEncounterQueryResults.
Predict: for a battle involving both humans, both instances show the C1 combat-setup card with the per-player action choice (Fight Manually / Auto Resolve / Observe / …) and a query timer, and both proceed once both have chosen.
Falsified if: only the host sees the query, or the client's choice is not honoured, or the query timer expires without either side being able to answer.
How a battle is provoked
Two routes, cheapest first, because SOTS fleets are slow and a lane is finite:
- Route A — random encounter. Custom Game Setup exposes Random Encounters %
(
ui-screen-map.mdF9). Set to maximum. An encounter fires against one player's fleet or system within a few turns and exercisesSNMDoEncounterQuery→SNMHostCombat→SNMLaunchCombat→ C2/C3/C4 →SNMAllCombatDone. This settles P1, P3, P4 and half of P5 without needing the two fleets to meet. - Route B — player-versus-player. A hand-authored two-player map in
C:\SOTS\Maps\*.csv(the format is plain CSV:system,x,y,z,name[,flag]rows,nodeline,a,b,colony,playerN,system, per the shippedexample.csv) with the two homeworlds a few light years apart, so one fleet can reach the other in one or two turns. This is the only route that settles P2 properly, because lockstep only matters when both humans are simulating the same battle.
Route A is not a substitute for Route B on P2, and the report must say which route produced which evidence.
What this run will NOT cover, stated in advance (rule 15)
- One guest, loopback only — same limitation as W2. No two-machine combat, no NAT, no latency.
- No
sots_server.exe; the dedicated-server bug stays unreproduced. - No long battle, no reinforcement waves, no host migration, no reconnection.
SyncLogCombatstays at its defaultFalse, so no sync log is produced; a desync would be observed as a symptom, not read out of a log, unless the symptom warrants turning it on.