diff --git a/docs/L2-predictions.md b/docs/L2-predictions.md new file mode 100644 index 0000000..2d481a6 --- /dev/null +++ b/docs/L2-predictions.md @@ -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 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.