# 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.