124 lines
7.2 KiB
Markdown
124 lines
7.2 KiB
Markdown
# 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.
|