L2: predictions for the multiplayer combat run, before the run

This commit is contained in:
alex 2026-09-08 17:01:56 -04:00
parent 011749571b
commit 1e7517e382

124
docs/L2-predictions.md Normal file
View file

@ -0,0 +1,124 @@
# 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:
1. `tcpdump -i tap141i0` on **spicy** — the host side of the guest's NIC. Outside the guest entirely.
2. `Get-NetUDPEndpoint` / `Get-NetTCPConnection` polled 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.
3. `qm monitor <id> screendump` for 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.md` F9). Set to maximum. An encounter fires against one player's fleet or system
within a few turns and exercises `SNMDoEncounterQuery` → `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 shipped `example.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.
- `SyncLogCombat` stays at its default `False`, 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.