lane W2: multiplayer Tier 0 played end to end with no server; ModCount/Frame/Status watchpoints; rcex explained

Multiplayer: two clients on one guest, joined by typed IP and by LAN browse, launched a
2-player game and played two turns in lockstep. 44,319 packets captured outside the guest,
ZERO to any GameSpy port. Availability check fails open exactly as lane G2 read it; Join
Manually is enabled with every gamespy.com name dead, falsifying G2's own caveat (a). The
shipped MOTD is fetched live from kerberos-productions.com and tells players to host in LAN
mode -- the developer's own statement of the same finding.

Watchpoints: a new shim module arms DR0-DR3 from StrategyServer::ApplyAllTurnCommands.
ModCount takes exactly 12 writes per End Turn (lane A2's prediction, confirmed twice, both
predicted addresses exact); Frame takes exactly one, from BeginProcessTurn+0x2a, settling the
ModCount-vs-Frame naming in A2's favour. The Player.Status writer between tail phase 31 and
the autosave EXISTS and is StrategyNetworkClient::OnMessage+0xa15 -- correcting lane T2.
Rule 19 control: the armed run reproduced the determinism oracle byte for byte.

rcex: closed from the corpus alone. It is sixteen 4-bit per-player counters; nibble p is set
to 1 on the turn the system enters player p's AFlags and ticked to 0 the next turn. 7/7
across two different games.
This commit is contained in:
alex 2026-09-08 15:23:52 -04:00
parent 3fe74bc6a5
commit 92ad44efb0
14 changed files with 788 additions and 0 deletions

View file

@ -0,0 +1,239 @@
# Three questions, one armed run: `ModCount`, `Frame`, and the `Player.Status` regression
- **Type:** control-flow (live measurement)
- **Status:** **verified** — hardware data-write watchpoints on the running game, with a
byte-identical oracle control
- **Confidence:** high. Twenty-four consecutive `ModCount` values with no gap; every predicted
address hit to the byte.
- **Owner / date:** lane W2 · 2026-09-08
- **Instrument:** `src/shim/hooks/watchpoints.{h,cpp}`, build `w2watch-3512c9a-20260908T1900Z`,
config `shim.cfg.w2watch`
- **Tests:** `alliance-mask-and-modcount.md` §3 (lane A2's probe 1 and its four falsifiers),
`treaty-turn-stamp.md` §3 (lane T2), `turn-driver.md` §0.1 (lane T)
---
## 0. The four results
1. **`ModCount` gets exactly 12 writes per End Turn on this save, and lane A2's prediction is
confirmed to the address.** Two turns measured, 12 and 12, values running 13→24 and 25→36 with
**no gap**. Ten of the twelve come from the command flush, one from `ProcessTurn`'s first
instruction, one from `OnAllCombatDone_Tail`. Zero from the abandon/chaos check. §2.
2. **The `ModCount` / `Frame` naming dispute is settled, and lane A2 was right.** `S+0x8` took 12
writes in the window; `S+0xc` took **exactly one**, from `BeginProcessTurn + 0x2a`, and its
value became the turn number. `addresses.json`'s `StrategyServer_off_ModCount` names the wrong
word and lane T's `StrategyServer_off_PhaseCounter` **is** `ModCount`. §3.
3. **The `Player.Status` regression has a writer, and it is not where lane T2 looked.**
`StrategyNetworkClient::OnMessage + 0xa15` writes `Status = 4` *after* `ProcessTurn` returns and
*before* the autosave, over the `1` the ProcessTurn tail had just written. T2's "there is NO
writer between tail phase 31 and the autosave" is falsified by watching it happen, twice. §4.
4. **The instrument is byte-neutral, and this was checked rather than assumed (rule 19).** With all
four watchpoints armed, one End Turn from `ref-turn2.sav` reproduced the determinism oracle
exactly: `(Autosave EndTurn).sav` `bb4fd9ac89f41e3b…`, `(Autosave).sav` `978041acd168b56e…`.
§1.
---
## 1. The instrument, and its control
**One MinHook detour in the whole module.** Debug registers are per-thread, so the watchpoints have
to be armed from a point that runs on the turn thread and precedes the writes.
`StrategyServer::ApplyAllTurnCommands` `0x0078f6a0` is both — lane A2 read it as the End-Turn
command flush, called from `StrategyNetworkClient::OnMessage` immediately before that handler calls
`ProcessTurn`, and its `this` is the `S` frame — so `S+0x8` and `S+0xc` are one add away. The
detour is the register-transparent asm stub the M0 hook already uses; the watchpoints themselves
modify **no code at all**.
```
watch: arm hook StrategyServer::ApplyAllTurnCommands rva=0x0038f6a0 va=0120f6a0 create=MH_OK enable=MH_OK
watch: players vector @0e0b267c begin=0e163528 end=0e163548 count=8
watch: SELFTEST canary writes=1 traps=1 dr7=0xdddd0055 PASS
watch: slot 0 -> S+0x8 = 0x0e0b2630 slot 2 -> player[0]+0x164 Status = 0x0e0cc2d4
watch: slot 1 -> S+0xc = 0x0e0b2634 slot 3 -> player[1]+0x164 Status = 0x0e0c5d44
watch: ARMED on tid 5928 dr7=0xdddd0055, S=0e0b2628
```
**The self-test is not decoration.** Method rule 1 says a green verdict is not evidence, and "the
arm silently did nothing" looks exactly like "nothing writes this". So before any game address is
believed, DR3 is pointed at a word the shim owns, that word is written once, and the handler is
required to report exactly one trap. It did. `dr7 = 0xdddd0055` is all four slots enabled, R/W = 01
(data write), LEN = 11 (4 bytes).
**The control (rule 19).** Lane H's finding is that a MinHook detour can change the game's output,
so a run taken with a new detour installed proves nothing until the same run is taken without it.
This lane's control is stronger than a same-day A/B: the armed run **is** the oracle run. One End
Turn from `ref-turn2.sav`, watchpoints live, produced
| file | size | sha256 prefix | historical |
|---|---|---|---|
| `(Autosave EndTurn).sav` | 66,732 | `bb4fd9ac89f41e3b` | **identical** |
| `(Autosave).sav` | 67,219 | `978041acd168b56e` | **identical** |
So the arming detour, four hardware watchpoints and ~36 exception deliveries changed **nothing** in
the turn. That is worth stating as its own small result: a data breakpoint is a trap taken after the
store retires, and unlike a code patch it has no relocation hazard — which is a point of contrast
with §2 of `tail-probes.md`, where a detour on a clean prologue boundary *did* perturb.
**What the instrument cannot see, stated up front.** Debug registers are per-thread and these were
armed on the turn thread (tid 5928) only. A write from another thread would be invisible. The
evidence that there was none is indirect but strong: the 24 recorded `ModCount` values are
**contiguous** — 13, 14, … 36, no gaps — so nothing incremented that word without being trapped.
## 2. `ModCount` — every writer, in order
Workload: `ref-turn2.sav` (turn 2, 8 players, `ModCount = 12`, `Frame = 2`), two End Turns. The
window below is the **second** one, which is fully covered — arming happens inside the first End
Turn, so the first window misses the three writes that precede the flush. Both windows agree on
everything they share.
Addresses are Ghidra VAs; the trap reports the instruction **after** the store, so a 3-byte `inc`
shows as site + 3.
| # | trap EIP | site | writer | value | return addresses |
|---|---|---|---|---|---|
| 1 | `0x0086c3e9` | `0x0086c3e6` | unresolved handler | 25 | `0x0088fce2` → `ApplyTurnCommandBatch+0x332` |
| 2-5 | `0x00821a87` | `0x00821a84` | unresolved handler, **×4** | 26-29 | `0x0088ffcb` → `ApplyTurnCommandBatch+0x61b` |
| 6 | `0x0084946e` | `0x0084946b` | unresolved handler | 30 | `0x008900a4` → `ApplyTurnCommandBatch+0x6f4` |
| 7 | `0x0088bf01` | `0x0088befe` | unresolved (near `DestroyFleet`) | 31 | `0x008902b4` → `ApplyTurnCommandBatch+0x904` |
| 8-9 | `0x0089046f` | `0x0089046c` | **inlined in `ApplyTurnCommandBatch`**, ×2 | 32-33 | `0x0078f6e6` → `ApplyAllTurnCommands+0x46` |
| 10 | `0x008657ad` | `0x008657aa` | **`StrategySim::MoveFleetCommand`** | 34 | `0x00890652` → `ApplyTurnCommandBatch+0xca2` |
| 11 | `0x007dc6f3` | `0x007dc6f0` | **`StrategyServer::ProcessTurn`, first instruction** | 35 | — |
| 12 | `0x007d92cd` | `0x007d92ca` | **`OnAllCombatDone_Tail + 0x2a`** | 36 | — |
Against lane A2's prediction, item by item:
- **"exactly 12 hits" — confirmed**, twice.
- **"two of them at `0x007dc6f0` and `0x007d92ca`" — confirmed, both, to the byte.** These are the
only two predicted *addresses* in A2's probe and both landed exactly.
- **"zero of them at `0x007b9e20`" — confirmed.** `Abdn` is false on every system of this save and
the abandon/chaos check's bump never fired. Note what kind of negative this is: rule 20's
distinction. This is "the site never trapped", which on a *watchpoint* is stronger than a word
count — but it still does not separate "the function was not called" from "it was called and the
gate held". A2's static reading says the gate is `sys->+0xc4`; that remains a static claim.
- **"the other 10 … with a return address in `ApplyTurnCommandBatch 0x0088f9b0` or its callers" —
confirmed.** Every one of the ten carries either an `ApplyTurnCommandBatch` return address or
`ApplyAllTurnCommands+0x46`, and the outer frame is `OnMessage+0x2c9` (`0x00784909`), which is
the return of the `ApplyAllTurnCommands` call A2 located at `0x00784904`. **A2's whole call chain
is confirmed live, address by address.**
- **A2's inlined site `0x0089046c` is confirmed** — the trap at `0x0089046f` is three bytes past it
— and it fires **twice** in this window, so the six inlined sites are not one-shot.
- **`0x008657aa` (`MoveFleetCommand`) confirmed** to the byte.
- **A2's falsifier (c) fires, and it is a refinement rather than a refutation.** A2 predicted all 12
would be "before `ProcessTurn` is entered". Ten are. The eleventh **is** `ProcessTurn`'s own first
instruction and the twelfth is *after* it. So the correct statement is: **all ten
command-application bumps precede `ProcessTurn`; the two driver bumps bracket the turn.** The
"queue is flushed first" reading survives intact.
- **Falsifier (b) fires and is the useful part.** Four of the ten handler EIPs are not attributable
to a named function — the nearest known symbol is between `0x581` and `0x10e9` away, which is not
containment. They are recorded as addresses in `ghidra/addresses.d/lane-w2.json` with status
`mapped` rather than dropped or guessed. **The one to disassemble first is `0x00821a84`: it fires
four times per turn, the most of any handler on this save**, and it sits immediately after
`StrategyServer::MarkPlayerTurnEnded` (`0x00821a40`, 60 bytes, ending `0x00821a7c`) — so it is
that function's neighbour, not that function.
- **Falsifier (d) does not fire**: no trap at `0x007850d5` / `0x0078514b` / `0x00785224`, so A2's
exclusion of `OnMessage`'s three `inc [reg+4]` sites was right. (`OnMessage` *does* appear in §4,
but for `Status`, not for `ModCount`.)
**12 is not a constant** (rule 20). It is this save's command count. The corpus disagrees with itself
usefully: `turn1-state → turn2-state → turn3-state` moves `ModCount` 0 → 12 → 24 (this game),
`human-turn2-orders.sav` sits at 25 on frame 2 (a different game), and the Zuul line runs
`63 @ f5 → 210 @ f15 → 412 @ f23`, i.e. ~15-25 per turn. The **structure** is what generalises: two
driver bumps plus one per applied command.
## 3. `ModCount` vs `Frame` — settled
Lane T (`StrategyServer_off_PhaseCounter`, `turn-driver.md` §0.1) and lane A2
(`StrategySim_off_ModCount`, `alliance-mask-and-modcount.md` §2.1) disagreed about which of `S+0x8`
and `S+0xc` is which, and `addresses.json`'s `StrategyServer_off_ModCount` sits on the third
opinion. One run separates them, because the two words behave completely differently:
| word | writes per End Turn | writer | value |
|---|---|---|---|
| `S+0x8` | **12** | ten command handlers + `ProcessTurn` + `OnAllCombatDone_Tail` | monotonic, no relation to the turn |
| `S+0xc` | **1** | `BeginProcessTurn + 0x2a` (trap `0x007d990d`) | 3 → **4** on the turn that produced frame 4 |
A word written once per turn whose value is the turn number is `Frame`. A word written once per
applied command is a modification counter. **Lane A2 is right on both, and its correction of
`addresses.json` stands.** Lane T's `StrategyServer_off_PhaseCounter` — "nobody has named this one"
— is `ModCount`, and the note that "both advance once per turn in different functions" is wrong
about `S+0x8`: it advanced twelve times.
Lane A2 also predicted `BeginProcessTurn`'s bump at `0x007d990a` from the instruction stream. Trap
`0x007d990d`. Confirmed.
## 4. `Player.Status` — the complete ordered story
DR2 and DR3 watched `players[0]+0x164` and `players[1]+0x164` for the whole window. Every write of
`Status` on those two players, in order, for one End Turn:
| order | site | writer | player | value |
|---|---|---|---|---|
| 1 | `0x00821a75` (store ends here) | **`MarkPlayerTurnEnded + 0x35`**, called from `OnPlayerEndTurn + 0x35` (return `0x007d9b2a`) | **both**, one call each | **4** |
| 2 | — | `BeginProcessTurn` (Frame), `ApplyAllTurnCommands` (10 × ModCount), `ProcessTurn` entry | — | — |
| 3 | `0x007dcc8a` (trap `0x007dcc94`) | **`ProcessTurn + 0x5ca`** — the phase-31 site | **player 0 only** | **1** |
| 4 | `0x00785055` (store ends here) | **`StrategyNetworkClient::OnMessage + 0xa15`** | **player 0 only** | **4** |
| 5 | — | `OnAllCombatDone_Tail` (ModCount) | — | — |
| — | *(the post-turn autosave is written somewhere here)* | | | |
| 6 | `0x007ddd41` (trap `0x007ddd49`) | **`ResumePlaying + 0xb1`** | **both** | **0** |
**Step 4 is the answer to the open item.** The regression was stated as "the phase writes 1, the
file carries 4, a load resets to 0, and the writer between tail phase 31 and the autosave is
unaccounted". The writer is `OnMessage + 0xa15` and it is a **second, direct store site** — not a
call into `MarkPlayerTurnEnded`, because the trap EIP is inside `OnMessage`'s body, not inside
`0x00821a40`. This **corrects `treaty-turn-stamp.md` §3** on two points: `MarkPlayerTurnEnded` is
not "THE ONLY WRITER OF `Player.Status` = 4 IN THE IMAGE", and "there is NO writer between tail
phase 31 and the autosave; backlog item 6 looks in the wrong place" is wrong — the backlog item was
looking in exactly the right place, it just could not see the site by reading.
Two further refinements the watchpoint gives for free:
- **The phase-31 write of 1 hits the local player only**, not every player. T2 read the site
correctly (`ProcessTurn + 0x5ca`, value 1, inside a `0x44`-stride loop); the loop selects one
player on this save.
- **`ResumePlaying`'s zeroing runs inside the End Turn**, after the autosave, not only on load. The
saved files confirm the ordering independently: `(Autosave EndTurn).sav` (pre-turn) carries
`Status = 0` for all eight players, `(Autosave).sav` (post-turn) carries `4, 4, 4, 4, 0, 0, 0, 0`.
Player 0's value in that file is the `4` from step 4, having been `4 → 1 → 4` inside one turn;
players 1-3's is the untouched `4` from step 1; players 4-7 never receive one.
## 5. Coverage — what this run did NOT establish (rule 15)
- **One save, one game, two turns.** `ref-turn2.sav` only. Nothing here is a claim about a turn with
combat, with an abandoned system, or with more than two active players.
- **Two players watched, not eight.** DR2/DR3 covered `players[0]` and `players[1]`. Players 2-7's
`Status` was inferred from the save files, not watched. In particular, *who* calls
`MarkPlayerTurnEnded` for players 2 and 3 was not observed.
- **Four of the twelve `ModCount` writers are unnamed** (§2). The lane located them; it did not
identify them.
- **`0x007b9e20` never fired**, which is a real negative for this save and says nothing about a save
where `Abdn` is true. Rule 6: that path is still a hypothesis.
- **Per-thread blind spot** (§1). A `ModCount` write from a thread other than the turn thread would
be invisible; the contiguity of the values argues there was none, but only for these two turns.
- **The autosave's exact position** between steps 5 and 6 of §4 is inferred from the file contents,
not watched. A fifth watchpoint is not available (four DRs), and the `StrategyHost::Autosave` hook
already exists in the harness if anyone wants to bracket it precisely.
## 6. Files
- Hit log, both turns: `verify/results/shim/watchpoints/w2-modcount-status-2turns.txt`
- Instrument log incl. the self-test: `verify/results/shim/watchpoints/w2-shim-log-excerpt.txt`
- Addresses: `ghidra/addresses.d/lane-w2.json` (10 entries)
- Instrument: `sots-engine` `src/shim/hooks/watchpoints.{h,cpp}`, configs `shim.cfg.w2watch` /
`shim.cfg.w2control`
- Predictions, committed before the run: `sots-engine/docs/W2-predictions.md`
## 7. The instrument is reusable, and here is what it costs
`watch=on` in `shim.cfg` arms four 4-byte write watchpoints from
`StrategyServer::ApplyAllTurnCommands`. To watch something else, change the four addresses computed
in `WatchOnApplyAll` — anything reachable from `S` at that moment is one line. The run above cost
one build and about six minutes of VM time for two turns, and it answered three questions that had
consumed parts of four lanes of static reading. **Method rule 18, again: `what writes this?` is a
watchpoint.**
Two things a future user should keep:
- **keep the canary self-test.** It is four lines and it is the difference between "nothing writes
this" and "the arm silently failed".
- **keep the oracle control.** Loading `ref-turn2.sav` and ending one turn costs nothing extra and
proves the run is measuring the un-instrumented game.

View file

@ -0,0 +1,253 @@
# Multiplayer Tier 0, run on the real game: two players, one guest, no server
- **Type:** subsystem (live verification)
- **Status:** **verified** — a two-player multiplayer game was started, joined and played on VM140
with no GameSpy service of any kind, and the whole run was captured outside the guest
- **Confidence:** high. Every claim below is a screenshot or a packet.
- **Owner / date:** lane W2 · 2026-09-08
- **Tests:** `findings/subsystems/multiplayer-gamespy.md` §7 (lane G2's five predictions, written
before any VM run) and `sots-engine/docs/W2-predictions.md` (this lane's restatement, committed
before the run)
---
## 0. The headline
**SOTS1 multiplayer is not dead, and restoring it needs no server.** Two clients on one Windows
guest, joined by typed IP, played two full turns in lockstep. Over the entire session the capture
on the host side of the guest's NIC shows **44,319 packets and exactly zero** to UDP 27900, UDP
27901, TCP 28910, TCP 28900, TCP 6667 or TCP 6500 — every GameSpy port the SDK in this binary
knows about.
Lane G2's Tier 0 was the cheapest item on the board and it landed exactly as G2 read it out of the
instruction stream. Four of its five predictions are confirmed; the fifth (Tier 3, the self-hosted
master server) was deliberately not attempted and is reported as not-run.
**And the game says so itself.** The Message of the Day panel on the main menu is fetched live, on
every launch, from `www.kerberos-productions.com/motd/motd_EN.txt` — the server still answers today,
`Last-Modified: Fri, 16 Jun 2017`, served by LiteSpeed at 106.0.62.78. Its text, in full, is
Kerberos's own 2012 notice:
> Attention SolForce personnel;
>
> It is with wide, astonished eyes that we have to report that Gamespy was recently purchased by a
> third-party. This party then began shutting off game servers and informing developers and
> publishers that they would have to pay tens of thousdands of dollars a month in upkeep, in order
> to keep the servers on. This despite the fact that SotS1 used Gamespy thanks to a contracted and
> not-cheap agreement between Lighthouse Interactive and Gamespy.
>
> We are looking into our options. In the meantime, you will not be able to play via Internet.
> However, you can still play the game via direct connect with your friends. **Game hosts please
> note they have to host the game in LAN mode, not Internet mode, or direct connect will not
> work.**
That is the `this+0x2c == 1 && this+0x30 != 0` predicate G2 recovered from `0x007c2dd0`, written by
the developer, and it is shipped inside the client we are testing. The fetch uses
`User-Agent: GameSpyHTTP/1.0` — the ghttp SDK component — which is the one piece of GameSpy code on
any live path, and it talks to Kerberos, not to GameSpy.
---
## 1. What was run
VM140, GOG 1.8.1, single Win10 guest, `hooks=off` — **the instrument was removed** (method rule 19),
because the question is what the shipped game does, and because two processes sharing one trace file
is not a measurement. Capture: `tcpdump -i tap140i0` on **spicy**, i.e. on the host side of the
guest's tap device. That is outside the guest, so it cannot perturb it, and it sees everything the
VM emits.
**The blind spot, stated up front:** loopback traffic between the two instances never reaches the
tap, so this capture cannot show the game's own UDP 3369 exchange. That is the right trade — the
predictions that matter are all about traffic that *leaves*, and the join working is evidence
enough that the loopback path carried it. The socket table is the corroboration (§3).
Established before the run, and it is what makes §4 a real test: from the guest,
`swordots.available.gamespy.com` returns **NXDOMAIN** while `www.google.com` resolves. There is no
stale wildcard record at `gamespy.com`; the SOA in the negative answers is
`ns-889.awsdns-47.net` / `awsdns-hostmaster.amazon.…`.
## 2. P5 — `/concurrent` — **confirmed**
Instance A launched normally. Instance B launched as
```
"C:\SOTS\Sword of the Stars.exe" /concurrent /join 127.0.0.1:3369
```
Both reached their own window and their own main loop: `Get-Process` showed **two** PIDs (4728 and
8780), and B's own stdout log records the full startup — VFS mount, D3D adapter, `ver.1.8.1 Wed Dec
13 03:38:31 2017`, profile switch — rather than the foreground-and-exit path. The
`CreateMutexA` / `ERROR_ALREADY_EXISTS` / argv-compare chain G2 read at `0x0089ddaf` behaves exactly
as read. **A second instance on one guest is a supported, shipped configuration**, and the
two-client test needs one VM, not two.
## 3. P1 — direct join with zero GameSpy traffic — **confirmed, both halves**
Instance A: *Host Multi-Player* → **Custom** + **LAN** → OK → Custom Game Setup (2 players, 28
stars, defaults) → *Create Game* → game name `MyGame` → lobby.
At that moment Windows Defender Firewall raised its "has blocked some features of this app" prompt
for `C:\sots\sword of the stars.exe` — the host had just opened its listening socket. Allowed for
private networks. **This is worth knowing for anyone reproducing the setup: the host, not the
joiner, triggers the firewall prompt, and it appears at *Create Game*, not at launch.**
Socket table with the host in the lobby (`Get-NetUDPEndpoint`, filtered by PID):
| PID | role | UDP bindings |
|---|---|---|
| 4728 | host | **0.0.0.0:3369**, 0.0.0.0:60924 |
| 8780 | joiner | 0.0.0.0:60925 |
Three facts fall out. The host binds `HostPort` **3369** exactly as `Game_LoadNetworkConfig`
defaults it (there is no `[Network]` section in `sots.ini` at all, so every value in G2 §3 is a
compiled-in default in this install). **No TCP anywhere**, confirming G2's import-table reading (no
`listen`, no `accept`). And `CombatHostPort` **3370 is not bound at lobby time** — it must be opened
later, when a combat starts, which no lane has yet observed.
The joiner appeared in slot 2 of the host's lobby within ~50 s of launch. Both instances then showed
two player cards, the client's view carrying *Ready* / *Leave Game* and the host's *Launch* /
*Cancel Game*. Client pressed Ready; host pressed Launch.
**The game started.** Turn 1, two distinct human players — the host is `Venkman`, the client is
`Hitomi`, different portraits, different homeworlds, different treasuries. Each pressed *End Turn*;
the turn resolved and **both** advanced to Turn 2 (host Imperial Savings 11,200,596; client
11,299,146 — different players, same turn). A second round took both to **Turn 3**.
Capture over the whole session: **zero** packets to any GameSpy port; **zero** DNS queries for any
`*.gamespy.com` name during the hosting and joining sequence. The only names resolved in that window
were Windows telemetry.
**This is the multiplayer revival, demonstrated end to end.** Not "the code path exists" — a game
was created, joined, launched and played.
## 4. P2 — the availability check fails open — **confirmed**
Fresh single instance, DNS cache flushed, *Join Multi-Player* → **Internet** → OK.
The Game Browser opened on the Internet tab with an empty *Available Games* list, the MOTD panel
below it, and **no `MATCHINGSERVICE_UNSUPPORTED` dialog** — no "Online support … is no longer
available", nothing. G2's fail-open reading of `GSIStartAvailableCheck` / `GSIAvailableCheckThink`
is correct on the running game.
Exactly **one** A query each, all answered NXDOMAIN, and **no retry**:
```
14:39:35.670 A? swordots.available.gamespy.com. -> NXDOMAIN
14:39:35.940 A? swordots.ms5.gamespy.com. -> NXDOMAIN
14:39:36.108 A? peerchat.gamespy.com. -> NXDOMAIN
```
Three refinements to G2 §7's P2. **(a)** One query, not one-or-two: the two-attempt/2000 ms retry at
`0x0040a2bb` is a *socket* retry, and it never runs because the socket was never created. **(b)**
The browser resolves the **server-list** and **peerchat** hostnames on the same screen entry, not
just the availability host — so a `hosts`-file redirect for Tier 1 must cover `ms5` and `peerchat`
as well or those two will still fail. **(c)** `swordots.master.gamespy.com` and the two `natneg`
names were **not** resolved: they belong to the host/reporting path, not the join path.
**Consequence for the revival plan: Tier 1 is confirmed unnecessary.** G2 hoped finding it a no-op
would be a good outcome; it is a no-op. Nothing has to answer the availability check for a client to
reach the browser, the manual-join dialog, or a game.
## 5. G2 §7 caveat (a) — falsified
G2 flagged, honestly, that it had not read the *Join Manually* button's enable predicate and that
the button might be disabled without a browser object. It is not. On the Internet page with an empty
list and every GameSpy name dead, **Join Manually is enabled**, and it opens a dialog with *Enter
Host's IP Address*, *Enter Password*, a *Favorites* list, and Join/Cancel. The GameSpy-free arm G2
traced from `Game_ManualJoin_OnAccept` is reachable in the shipped UI in the state the world is
actually in.
## 6. P3 — LAN discovery with no server — **confirmed, and my own prediction was wrong**
I rated this *lower* than G2 did, and wrote down why: both instances are on one host, so a broadcast
to `255.255.255.255:3369` and the host's QR2 socket might contend for one UDP port. Written before
the run, and wrong. The socket table in §3 shows why: **the client never binds 3369** — it
broadcasts from an ephemeral port (60925) — so there is no contention to have.
With A hosting in LAN mode, B's *Join Multi-Player* → **LAN** page listed the game **on first open,
with no manual Refresh**:
| Game Name | Players | Ping | Password | Status | Version |
|---|---|---|---|---|---|
| `re : MyGame` | 1/2 | 32 | No | Staging | 1.8.1 |
That row is the QR2 custom-key set of G2 §4.2 rendered by the client, answered by the host's own
socket, with no master server in existence. Selecting the row and pressing **Join** also worked: B
went straight into the lobby. So both join paths — manual address and browser-mediated — work
GameSpy-free.
One thing G2 could not settle and this run does: **the browser-mediated LAN join makes no NAT
negotiation attempt.** No `natneg1`/`natneg2` name was ever resolved and nothing was sent to UDP
27901. G2's reading that NatNeg hangs off the *browser* join is right about the code, but on the LAN
path (`queryVersion = 1`, `lanBrowse = 1`) it is not reached.
## 7. P4 — the self-hosted master server — **not run**
Deliberately. Tier 3 is a container deployment plus a DNS redirect; it is a lane's work on its own,
it is not on the path to *playing*, and running it before Tier 0 was proved would have been the
wrong order. Nothing here confirms or disconfirms the OpenSpy / UniSpyServer plan, and §4's finding
that Tier 1 is unnecessary does **not** generalise to Tier 3 — the Internet list stays empty because
nothing answers, and only a real SB v2 + QR2 backend can change that.
Reported as not-run rather than unknown: the experiment is fully specified in
`multiplayer-gamespy.md` §5 and §7, and the two extra hostnames in §4(b) above are a correction to
its `hosts`-file list.
## 8. What this run did NOT cover (rule 15)
Loudly, because the headline is a strong one and it was established on a narrow workload.
- **One guest, loopback only.** No two-machine LAN test, no VPN, no NAT, no port forwarding. The
claim "two boxes on a LAN work" remains G2's inference plus Kerberos's MOTD; what is *measured* is
two processes on one box over 127.0.0.1.
- **No combat.** Both turns resolved without a battle, so `CombatHostPort` 3370 was never bound and
the combat lockstep — the part with a 1000 ms `CombatLatency` and its own sync checking — is
entirely untested. This is the most likely place for the direct path to still be broken.
- **Two turns.** Long-run desync, the `SyncCheckStrategy` machinery, and reconnection are untested.
- **Two players, both Human, both on defaults.** No password, no teams, no alliances, no scenario,
no more than two slots.
- **`sots_server.exe` was not run.** It is present in `C:\SOTS` alongside
`Dedicated Server Launchpad.exe`. The community bug G2 quotes ("the dedicated server does not seem
to work") is still unreproduced and unexplained, and it is now the cheapest remaining multiplayer
item.
- **The shim was `hooks=off`**, so nothing internal was observed — no message types, no wire
formats, no sync-check payloads. Everything above is UI, sockets and packets.
## 9. Files
- Capture (filtered to DNS, every GameSpy port, the game's own ports and the MOTD host):
`verify/results/multiplayer/w2-tier0-filtered.pcap`
- Screenshots: `verify/results/multiplayer/w2-lobby-client.png`, `w2-mp-turn1.png`,
`w2-mp-turn2-host.png`, `w2-mp-turn2-client.png`, `w2-internet-browser-no-error.png`,
`w2-join-manually-dialog.png`, `w2-lan-browse-listing.png`
- Predictions, committed before the run: `sots-engine/docs/W2-predictions.md`
## 10. Recipe, so nobody has to re-derive it
At 1024x768, with the game at the main menu:
- *Host Multi-Player* (512,478) → **Custom** (446,287) → **LAN** (446,428) → OK (551,522)
- Custom Game Setup: *Create Game* (835,707) → name dialog OK (600,392)
- Firewall prompt: *Allow access* (623,550) — appears once, on the first host
- Host lobby: *Launch* (931,663). Client lobby: *Ready* (931,663), *Leave Game* (931,692)
- *Join Multi-Player* (512,506) → **LAN** (446,357) / **Internet** (577,357) → OK (551,451)
- Game Browser: *Join Manually* (781,245), *Refresh* (874,245), *Join* (965,443), first row
(200,159), *Exit* (989,716)
- Second instance: `schtasks` task running
`"C:\SOTS\Sword of the Stars.exe" /concurrent /join 127.0.0.1:3369`. It must be a scheduled task
with `LogonType=InteractiveToken`; a process started straight from the SSH session lands in a
different window station and cannot be seen or clicked.
- Clicking a *specific* instance needs a helper that resolves the window by **PID**
(`Get-Process -Id`), not `Select -First 1` — the existing `C:\SOTS\ui\click_helper.ps1` picks
whichever instance it finds first. Lane W2 left `C:\SOTS\w2\ui.ps1` (takes a PID, or `-1` for "do
not foreground anything", which is how the firewall dialog gets clicked).
## 11. Corrections filed
- Against `multiplayer-gamespy.md` §7: caveat (a) is falsified (§5); P2's "exactly one DNS query"
is right but for a different reason, and the browser resolves **three** names not one (§4); P3's
mechanism is confirmed but the single-guest port-contention worry (this lane's, not G2's) was
unfounded (§6).
- The MOTD is **live**, not canned: this corrects any assumption that a 2026 launch is offline. One
outbound HTTP GET to `www.kerberos-productions.com` happens on every launch and on every entry to
the Game Browser.

View file

@ -0,0 +1,90 @@
# `rcex` — a per-player nibble array, and the rule is one line
- **Type:** subsystem (field semantics)
- **Address / RVA:** `ServerSystem` field, streamed as `int64` (`struct-recovery.md` row `0xf8`);
ticked inside `ServerSystem::ProcessTurn` (the "`rcex` tick", already in `addresses.json`'s
body-order note for `0x0074…`)
- **Status:** **mapped** — rule holds 7/7 across the corpus, from the saves alone, no VM time
- **Confidence:** high on the rule, medium on the *name* of the event that sets it
- **Owner / date:** lane W2 · 2026-09-08
- **Closes:** `system-visibility-prediction.md` §"What the model deliberately does not write" —
"`rcex` (6 leaves per pair). **Unexplained.**"; `system-visibility-record.md` §8 row
"`rcex` | unassigned"
---
## The rule
`rcex` is a **64-bit array of sixteen 4-bit counters, one per player index**. Player *p*'s counter
is nibble *p*, i.e. bits `4p … 4p+3`.
On the turn where a system enters player *p*'s `AFlags` — the per-player acquisition/observation
bit — nibble *p* is set to **1**. On the next turn the tick in `ServerSystem::ProcessTurn`
decrements it to **0**. That is the whole of the observed `0 → 1<<16 → 0` behaviour, and it is
exactly the shape `strategic-turn-internals.md` guessed at ("`Bats2` / `rcex` 64-bit nibble arrays:
per-player 4-bit countdowns (battle / recon cooldown)") without being able to pin the index.
## The evidence
Every system in the 11-save corpus with a non-zero `rcex`, with its `AFlags` beside it:
| save | frame | system | `rcex` | set bit | ⇒ nibble | `AFlags` | ⇒ set bit | nibble value |
|---|---:|---|---:|---:|---:|---:|---:|---:|
| `turn2-state` | 2 | Hyperion | 65536 | 16 | **4** | 16 | **4** | 1 |
| `turn2-state` | 2 | Koa'Vo | 268435456 | 28 | **7** | 128 | **7** | 1 |
| `turn2-state` | 2 | Kaa'Vaalu | 65536 | 16 | **4** | 16 | **4** | 1 |
| `turn2-state` | 2 | Markab | 65536 | 16 | **4** | 16 | **4** | 1 |
| `turn2-state` | 2 | Kea'Pono | 65536 | 16 | **4** | 16 | **4** | 1 |
| `turn2-state` | 2 | Ko'Rorkor | 65536 | 16 | **4** | 16 | **4** | 1 |
| `human-turn2-orders` | 2 | Terra | 4096 | 12 | **3** | 8 | **3** | 1 |
**7 for 7.** `nibble index == the index of the single set `AFlags` bit`, and the nibble's value is
always 1. The seventh row is the strong one: `human-turn2-orders.sav` is a **different game** with a
different map, a different system and a different player index, and it obeys the same rule.
Every other save in the corpus — `turn1-state`, `turn3-state`, `human-turn3-noderoute`, and all six
Zuul saves from frame 5 to frame 23 — has `rcex == 0` on all 28 systems, which is what a counter
that decays to zero in one turn looks like when nothing was acquired that turn.
That also explains the `system-visibility-record.md` §7 observation directly: `rcex` moves
`0 → 65536` on `turn1 → turn2` and back `65536 → 0` on `turn2 → turn3` **on the same six systems**,
and Koa'Vo takes `1 << 28` rather than `1 << 16` — because Koa'Vo was acquired by player 7 and the
other five by player 4.
## Why this had to be six leaves per pair and not more
The visibility model's residual was "6 leaves per pair" for `rcex`. It is six because six systems
changed hands in the turn-1→turn-2 pair and each contributes one `int64` leaf. The count was never
about `rcex` being complicated; it was about nobody having connected the nibble index to the player
index.
## How this could be wrong, and the symptom of each way (rule 2)
- **The index is not the player index but something correlated with it on this corpus.** Every save
here has **single-bit** `AFlags` on every system — the corpus limitation
`system-visibility-prediction.md` already flags — so "nibble index == player index" and "nibble
index == index of the lowest set `AFlags` bit" are indistinguishable. *Falsifying workload:* a
save where **two** players acquire the same system on the same turn. The rule predicts **two**
nibbles set to 1; a lowest-bit rule predicts one.
- **The initial value is not always 1.** Every observed nibble is 1. A longer cooldown (2, 3) would
appear on some other event. *Symptom:* a nibble > 1, and a system whose `rcex` takes two turns to
reach zero. Nothing in the corpus shows one.
- **It counts something other than acquisition.** `AFlags`, `VFlags` and `EFlags` are all equal on
all seven rows, so this corpus cannot separate them. *Falsifying workload:* any save where a
system's `VFlags` and `AFlags` disagree.
- **The decrement is not in `ServerSystem::ProcessTurn`.** The "rcex tick" is named in that
function's body-order note in `addresses.json`, between the `Bats2` tick and the `haltv` clear;
the corpus is consistent with it but does not prove the site. *This is the one item that wants a
watchpoint* — and it is now cheap: `src/shim/hooks/watchpoints.cpp` arms four 4-byte write
watchpoints from a known `this`, and `rcex` is at a fixed offset in `ServerSystem`. See
`findings/control-flow/watchpoints-modcount-status.md` §7.
## Cross-refs
- `findings/objects/struct-recovery.md` (the `int64` typing, and the warning that "R2 int is wrong")
- `findings/subsystems/strategic-turn-internals.md` (the nibble-array reading this confirms)
- `findings/subsystems/system-visibility-record.md` §7-8 and
`findings/subsystems/system-visibility-prediction.md` (the open item this closes)
- **`Bats2` is the same shape and is still unassigned.** It is `0` on every system of every corpus
save, so it is a rule-6 hypothesis: the nibble-per-player reading is inherited from `rcex` and has
never been exercised. The workload is a save taken on the turn after a battle.

View file

@ -0,0 +1,84 @@
{
"entries": [
{
"name": "StrategyServer_ProcessTurn_ModCountBump",
"addr": "0x007dc6f0",
"convention": "site",
"prototype": "`inc [esi+0x8]` with esi = S -- the FIRST instruction of StrategyServer::ProcessTurn's body bumps ModCount. OBSERVED LIVE by a DR0 4-byte write watchpoint on S+0x8 (lane W2): the trap reports EIP 0x007dc6f3, i.e. the instruction after a 3-byte `inc`, on both measured End Turns. This is the ordering marker for the whole ModCount question -- every command-application bump precedes it and the OnAllCombatDone_Tail bump follows it",
"status": "verified",
"source": "findings/control-flow/watchpoints-modcount-status.md §2 (lane W2 2026-09-08, hardware watchpoint, 2 End Turns from ref-turn2.sav); address predicted by lane A2 alliance-mask-and-modcount.md §3 and by lane T turn-driver.md §0.1"
},
{
"name": "StrategyServer_OnAllCombatDone_Tail_ModCountBump",
"addr": "0x007d92ca",
"convention": "site",
"prototype": "ModCount bump at OnAllCombatDone_Tail + 0x2a (trap EIP 0x007d92cd). CORRECTS lane A2's prose, which called it 'OnAllCombatDone_Tail's first instruction': the address A2 predicted is exactly right, the offset is +0x2a and not +0. It is the LAST ModCount write of the turn and it lands AFTER StrategyServer::ProcessTurn has been entered, which refines A2's falsifier (c) -- 'hits after ProcessTurn is entered' is expected for this one site and only this one",
"status": "verified",
"source": "findings/control-flow/watchpoints-modcount-status.md §2 (lane W2 2026-09-08, hardware watchpoint)"
},
{
"name": "StrategyServer_BeginProcessTurn_FrameBump",
"addr": "0x007d990a",
"convention": "site",
"prototype": "The turn-number increment: `inc [reg+0xc]` with reg = S, at BeginProcessTurn + 0x2a (trap EIP 0x007d990d). OBSERVED LIVE on DR1 watching S+0xc: EXACTLY ONE write per End Turn, value 3 -> 4 on the second measured turn, against TWELVE writes to S+0x8 in the same window. THIS SETTLES THE NAMING DISPUTE: S+0x8 is a modification counter (lane A2's `StrategySim_off_ModCount`) and S+0xc is the frame/turn number (lane A2's `StrategySim_off_Frame`). addresses.json's `StrategyServer_off_ModCount` (0x8 in the raw frame == S+0xc) carries the name on the wrong word, and lane T's `StrategyServer_off_PhaseCounter` (S+0x8) is ModCount",
"status": "verified",
"source": "findings/control-flow/watchpoints-modcount-status.md §3 (lane W2 2026-09-08, hardware watchpoint)"
},
{
"name": "StrategyServer_ProcessTurnTail_PlayerStatusOne",
"addr": "0x007dcc8a",
"convention": "site",
"prototype": "Writes Player.Status(+0x164) = 1 for the LOCAL player only (not for every player), at StrategyServer::ProcessTurn + 0x5ca. Trap EIP 0x007dcc94, so the store is a 10-byte `mov dword ptr [reg+0x164], 1`. Confirms lane T2's static reading of the site AND narrows it: on a two-human-player save only player[0]'s Status moved; player[1]'s did not",
"status": "verified",
"source": "findings/control-flow/watchpoints-modcount-status.md §4 (lane W2 2026-09-08, DR2/DR3 on players[0]/[1] +0x164)"
},
{
"name": "StrategyNetworkClient_OnMessage_PlayerStatusFour",
"addr": "0x00785055",
"convention": "site",
"prototype": "THE MISSING WRITER OF Player.Status = 4 between tail phase 31 and the post-turn autosave. This address is the instruction AFTER the store (the trap address); the store itself ends here. It lives in StrategyNetworkClient::OnMessage at +0xa15 -- the End-Turn dispatcher -- and it fires AFTER StrategyServer::ProcessTurn has returned and BEFORE the autosave, writing 4 to the local player's Status over the 1 the ProcessTurn tail had just written. CORRECTS lane T2's treaty-turn-stamp.md §3, which read StrategyServer::MarkPlayerTurnEnded 0x00821a40 as 'THE ONLY WRITER OF Player.Status = 4 IN THE IMAGE' and concluded 'there is NO writer between tail phase 31 and the autosave'. There is, and it was watched happening twice on two consecutive End Turns",
"status": "verified",
"source": "findings/control-flow/watchpoints-modcount-status.md §4 (lane W2 2026-09-08, hardware watchpoint on ServerPlayer+0x164)"
},
{
"name": "StrategyServer_MarkPlayerTurnEnded_StatusStore",
"addr": "0x00821a75",
"convention": "site",
"prototype": "The `p->Status(+0x164) = 4` store inside StrategyServer::MarkPlayerTurnEnded, at +0x35 (trap EIP 0x00821a75, so the store ends here). Called ONCE PER PLAYER at the START of an End Turn, from OnPlayerEndTurn 0x007d9af0 (return address 0x007d9b2a, i.e. the call is at +0x35) -- confirming lane T2's caller list live. It runs BEFORE the pre-turn autosave's successor and before ApplyAllTurnCommands, which is why the `(Autosave EndTurn)` file still carries Status 0",
"status": "verified",
"source": "findings/control-flow/watchpoints-modcount-status.md §4 (lane W2 2026-09-08, hardware watchpoint)"
},
{
"name": "StrategySim_ModCountBump_unresolved_0086c3e6",
"addr": "0x0086c3e6",
"convention": "site",
"prototype": "A ModCount bump observed live but NOT attributable to a named function: the trap EIP is 0x0086c3e9 and the nearest preceding known symbol is ServerTradeManager_ProcessTurn 0x0086b300, +0x10e9 away -- far too far to claim containment. Its return address is 0x0088fce2 (inside StrategySim::ApplyTurnCommandBatch 0x0088f9b0), so it IS one of lane A2's twenty command handlers; only the handler's identity is open. Recorded as an address to disassemble rather than dropped",
"status": "mapped",
"source": "findings/control-flow/watchpoints-modcount-status.md §2 (lane W2 2026-09-08)"
},
{
"name": "StrategySim_ModCountBump_unresolved_00821a84",
"addr": "0x00821a84",
"convention": "site",
"prototype": "A ModCount bump observed live FOUR times per turn -- the single most frequent command handler on this save. Trap EIP 0x00821a87; nearest known symbol is StrategyServer_MarkPlayerTurnEnded 0x00821a40, but lane T2 measured that function at 60 bytes (ending 0x00821a7c), so this is the NEXT function and MarkPlayerTurnEnded's neighbour, not MarkPlayerTurnEnded. Return address 0x0088ffcb (StrategySim::ApplyTurnCommandBatch)",
"status": "mapped",
"source": "findings/control-flow/watchpoints-modcount-status.md §2 (lane W2 2026-09-08)"
},
{
"name": "StrategySim_ModCountBump_unresolved_0084946b",
"addr": "0x0084946b",
"convention": "site",
"prototype": "A ModCount bump observed live once per turn; trap EIP 0x0084946e, nearest known symbol ServerTradeManagerImpl_vslot11 0x00848570 at +0xefe (not containment). Return address 0x008900a4 (StrategySim::ApplyTurnCommandBatch)",
"status": "mapped",
"source": "findings/control-flow/watchpoints-modcount-status.md §2 (lane W2 2026-09-08)"
},
{
"name": "StrategySim_ModCountBump_unresolved_0088befe",
"addr": "0x0088befe",
"convention": "site",
"prototype": "A ModCount bump observed live once per turn; trap EIP 0x0088bf01, nearest known symbol StrategyServer_DestroyFleet 0x0088b980 at +0x581 -- plausibly inside it, but unproven. Return address 0x008902b4 (StrategySim::ApplyTurnCommandBatch), so it is a command handler called from the batch applier rather than an inlined site",
"status": "mapped",
"source": "findings/control-flow/watchpoints-modcount-status.md §2 (lane W2 2026-09-08)"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

View file

@ -0,0 +1,35 @@
watchhit seq=0 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012ec3e9 value=13(0x0000000d) tid=5928 ebpret=0x0130fce2 ebpret2=0x0120f6e6 scan=0x0130fce2,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=1 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=14(0x0000000e) tid=5928 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=2 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=15(0x0000000f) tid=5928 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=3 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=16(0x00000010) tid=5928 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=4 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=17(0x00000011) tid=5928 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=5 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012c946e value=18(0x00000012) tid=5928 ebpret=0x013100a4 ebpret2=0x0120f6e6 scan=0x013100a4,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=6 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0130bf01 value=19(0x00000013) tid=5928 ebpret=0x013102b4 ebpret2=0x0120f6e6 scan=0x01402148,0x012c9514,0x0141bb08,0x013102b4
watchhit seq=7 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0131046f value=20(0x00000014) tid=5928 ebpret=0x0120f6e6 ebpret2=0x0139dfa1 scan=0x013dad98,0x0120f6e6,0x01204909,0x00000000
watchhit seq=8 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0131046f value=21(0x00000015) tid=5928 ebpret=0x0120f6e6 ebpret2=0x0139dfa1 scan=0x013dad98,0x0120f6e6,0x01204909,0x00000000
watchhit seq=9 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012e57ad value=22(0x00000016) tid=5928 ebpret=0x01310652 ebpret2=0x0120f6e6 scan=0x01339285,0x013fb198,0x01310652,0x013dad98
watchhit seq=10 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0125c6f3 value=23(0x00000017) tid=5928 ebpret=0x72433b04 ebpret2=0x724337d4 scan=0x0141005c,0x012e57ad,0x00000000,0x00000000
watchhit seq=11 mark=1 slot=2 dr6=0xffff0ff4 eip=0x0125cc94 value=1(0x00000001) tid=5928 ebpret=0x72433b04 ebpret2=0x724337d4 scan=0x0141005c,0x012e57ad,0x00000000,0x00000000
watchhit seq=12 mark=1 slot=2 dr6=0xffff0ff4 eip=0x01205055 value=4(0x00000004) tid=5928 ebpret=0x0139dfa1 ebpret2=0x013a5651 scan=0x012317aa,0x0140c080,0x0124cac1,0x0124cad9
watchhit seq=13 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012592cd value=24(0x00000018) tid=5928 ebpret=0x72406104 ebpret2=0x72405dd4 scan=0x0140fca0,0x00e81000,0x01205055,0x00000000
watchhit seq=14 mark=1 slot=2 dr6=0xffff0ff4 eip=0x0125dd49 value=0(0x00000000) tid=5928 ebpret=0x01204fe4 ebpret2=0x0139dfa1 scan=0x01410138,0x01204fe4,0x010345f4,0x012290be
watchhit seq=15 mark=1 slot=3 dr6=0xffff0ff8 eip=0x0125dd49 value=0(0x00000000) tid=5928 ebpret=0x01204fe4 ebpret2=0x0139dfa1 scan=0x01410138,0x01204fe4,0x010345f4,0x012290be
watchhit seq=16 mark=1 slot=3 dr6=0xffff0ff8 eip=0x012a1a75 value=4(0x00000004) tid=5928 ebpret=0x01259b2a ebpret2=0x0120481b scan=0x01259b2a,0x0140fd08,0x0120481b,0x0135e74a
watchhit seq=17 mark=1 slot=2 dr6=0xffff0ff4 eip=0x012a1a75 value=4(0x00000004) tid=5928 ebpret=0x01259b2a ebpret2=0x0120481b scan=0x01259b2a,0x0140fd08,0x0120481b,0x0135e74a
watchhit seq=18 mark=1 slot=1 dr6=0xffff0ff2 eip=0x0125990d value=4(0x00000004) tid=5928 ebpret=0x012048dd ebpret2=0x0139dfa1 scan=0x0140fce0,0x012048dd,0x00000000,0x00000000
watchhit seq=19 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012ec3e9 value=25(0x00000019) tid=5928 ebpret=0x0130fce2 ebpret2=0x0120f6e6 scan=0x0130fce2,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=20 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=26(0x0000001a) tid=5928 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=21 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=27(0x0000001b) tid=5928 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=22 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=28(0x0000001c) tid=5928 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=23 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=29(0x0000001d) tid=5928 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=24 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012c946e value=30(0x0000001e) tid=5928 ebpret=0x013100a4 ebpret2=0x0120f6e6 scan=0x013100a4,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=25 mark=2 slot=0 dr6=0xffff0ff1 eip=0x0130bf01 value=31(0x0000001f) tid=5928 ebpret=0x013102b4 ebpret2=0x0120f6e6 scan=0x01402148,0x012c9514,0x0141bb08,0x013102b4
watchhit seq=26 mark=2 slot=0 dr6=0xffff0ff1 eip=0x0131046f value=32(0x00000020) tid=5928 ebpret=0x0120f6e6 ebpret2=0x0139dfa1 scan=0x013dad98,0x0120f6e6,0x01204909,0x00000000
watchhit seq=27 mark=2 slot=0 dr6=0xffff0ff1 eip=0x0131046f value=33(0x00000021) tid=5928 ebpret=0x0120f6e6 ebpret2=0x0139dfa1 scan=0x013dad98,0x0120f6e6,0x01204909,0x00000000
watchhit seq=28 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012e57ad value=34(0x00000022) tid=5928 ebpret=0x01310652 ebpret2=0x0120f6e6 scan=0x01339285,0x013fb198,0x01310652,0x013dad98
watchhit seq=29 mark=2 slot=0 dr6=0xffff0ff1 eip=0x0125c6f3 value=35(0x00000023) tid=5928 ebpret=0x72433b04 ebpret2=0x724337d4 scan=0x0141005c,0x012e57ad,0x00000000,0x00000000
watchhit seq=30 mark=2 slot=2 dr6=0xffff0ff4 eip=0x0125cc94 value=1(0x00000001) tid=5928 ebpret=0x72433b04 ebpret2=0x724337d4 scan=0x0141005c,0x012e57ad,0x00000000,0x00000000
watchhit seq=31 mark=2 slot=2 dr6=0xffff0ff4 eip=0x01205055 value=4(0x00000004) tid=5928 ebpret=0x0139dfa1 ebpret2=0x013a5651 scan=0x012317aa,0x0140c080,0x0124cac1,0x0124cad9
watchhit seq=32 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012592cd value=36(0x00000024) tid=5928 ebpret=0x72406104 ebpret2=0x72405dd4 scan=0x0140fca0,0x00e81000,0x01205055,0x00000000
watchhit seq=33 mark=2 slot=2 dr6=0xffff0ff4 eip=0x0125dd49 value=0(0x00000000) tid=5928 ebpret=0x01204fe4 ebpret2=0x0139dfa1 scan=0x01410138,0x01204fe4,0x010345f4,0x012290be
watchhit seq=34 mark=2 slot=3 dr6=0xffff0ff8 eip=0x0125dd49 value=0(0x00000000) tid=5928 ebpret=0x01204fe4 ebpret2=0x0139dfa1 scan=0x01410138,0x01204fe4,0x010345f4,0x012290be

View file

@ -0,0 +1,87 @@
15:06:12.285 [tid 5928] ==== sots-engine shim (binkw32 proxy) build w2watch-3512c9a-dirty-20260908T1900Z ====
15:06:12.285 [tid 5928] config: watch=on
15:06:12.285 [tid 5928] config: watch.players=2
15:06:12.285 [tid 5928] config: watch.out=C:\SOTS\shim.watch.txt
15:06:12.769 [tid 5928] watch: VEH=006a8308 text=[0x00e81000,0x0145ca00) out=C:\SOTS\shim.watch.txt
15:06:12.785 [tid 5928] watch: arm hook StrategyServer::ApplyAllTurnCommands rva=0x0038f6a0 va=0120f6a0 create=MH_OK
enable=MH_OK
15:12:27.093 [tid 5928] watch: players vector @0e0b267c begin=0e163528 end=0e163548 count=8
15:12:27.093 [tid 5928] watch: SELFTEST canary writes=1 traps=1 dr7=0xdddd0055 PASS
15:12:27.093 [tid 5928] watch: slot 0 -> S+0x8 (A2:ModCount / T:PhaseCounter) = 0x0e0b2630
15:12:27.093 [tid 5928] watch: slot 1 -> S+0xc (A2:Frame / addresses.json:ModCount) = 0x0e0b2634
15:12:27.093 [tid 5928] watch: slot 2 -> player[0]+0x164 Status = 0x0e0cc2d4
15:12:27.093 [tid 5928] watch: slot 3 -> player[1]+0x164 Status = 0x0e0c5d44
15:12:27.093 [tid 5928] watch: ARMED on tid 5928 dr7=0xdddd0055, S=0e0b2628 (ApplyAllTurnCommands this)
15:12:27.578 [tid 5300] watchhit seq=0 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012ec3e9 value=13(0x0000000d) tid=5928
ebpret=0x0130fce2 ebpret2=0x0120f6e6 scan=0x0130fce2,0x013dad98,0x0120f6e6,0x01204909
15:12:27.578 [tid 5300] watchhit seq=1 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=14(0x0000000e) tid=5928
ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
15:12:27.578 [tid 5300] watchhit seq=2 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=15(0x0000000f) tid=5928
ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
15:12:27.578 [tid 5300] watchhit seq=3 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=16(0x00000010) tid=5928
ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
15:12:27.578 [tid 5300] watchhit seq=4 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=17(0x00000011) tid=5928
ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
15:12:27.578 [tid 5300] watchhit seq=5 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012c946e value=18(0x00000012) tid=5928
ebpret=0x013100a4 ebpret2=0x0120f6e6 scan=0x013100a4,0x013dad98,0x0120f6e6,0x01204909
15:12:27.578 [tid 5300] watchhit seq=6 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0130bf01 value=19(0x00000013) tid=5928
ebpret=0x013102b4 ebpret2=0x0120f6e6 scan=0x01402148,0x012c9514,0x0141bb08,0x013102b4
15:12:27.578 [tid 5300] watchhit seq=7 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0131046f value=20(0x00000014) tid=5928
ebpret=0x0120f6e6 ebpret2=0x0139dfa1 scan=0x013dad98,0x0120f6e6,0x01204909,0x00000000
15:12:27.578 [tid 5300] watchhit seq=8 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0131046f value=21(0x00000015) tid=5928
ebpret=0x0120f6e6 ebpret2=0x0139dfa1 scan=0x013dad98,0x0120f6e6,0x01204909,0x00000000
15:12:27.578 [tid 5300] watchhit seq=9 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012e57ad value=22(0x00000016) tid=5928
ebpret=0x01310652 ebpret2=0x0120f6e6 scan=0x01339285,0x013fb198,0x01310652,0x013dad98
15:12:27.578 [tid 5300] watchhit seq=10 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0125c6f3 value=23(0x00000017) tid=5928
ebpret=0x72433b04 ebpret2=0x724337d4 scan=0x0141005c,0x012e57ad,0x00000000,0x00000000
15:12:27.578 [tid 5300] watchhit seq=11 mark=1 slot=2 dr6=0xffff0ff4 eip=0x0125cc94 value=1(0x00000001) tid=5928
ebpret=0x72433b04 ebpret2=0x724337d4 scan=0x0141005c,0x012e57ad,0x00000000,0x00000000
15:12:27.578 [tid 5300] watchhit seq=12 mark=1 slot=2 dr6=0xffff0ff4 eip=0x01205055 value=4(0x00000004) tid=5928
ebpret=0x0139dfa1 ebpret2=0x013a5651 scan=0x012317aa,0x0140c080,0x0124cac1,0x0124cad9
15:12:27.578 [tid 5300] watchhit seq=13 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012592cd value=24(0x00000018) tid=5928
ebpret=0x72406104 ebpret2=0x72405dd4 scan=0x0140fca0,0x00e81000,0x01205055,0x00000000
15:12:27.578 [tid 5300] watchhit seq=14 mark=1 slot=2 dr6=0xffff0ff4 eip=0x0125dd49 value=0(0x00000000) tid=5928
ebpret=0x01204fe4 ebpret2=0x0139dfa1 scan=0x01410138,0x01204fe4,0x010345f4,0x012290be
15:12:27.578 [tid 5300] watchhit seq=15 mark=1 slot=3 dr6=0xffff0ff8 eip=0x0125dd49 value=0(0x00000000) tid=5928
ebpret=0x01204fe4 ebpret2=0x0139dfa1 scan=0x01410138,0x01204fe4,0x010345f4,0x012290be
15:15:26.531 [tid 5928] watchhit seq=16 mark=1 slot=3 dr6=0xffff0ff8 eip=0x012a1a75 value=4(0x00000004) tid=5928
ebpret=0x01259b2a ebpret2=0x0120481b scan=0x01259b2a,0x0140fd08,0x0120481b,0x0135e74a
15:15:26.531 [tid 5928] watchhit seq=17 mark=1 slot=2 dr6=0xffff0ff4 eip=0x012a1a75 value=4(0x00000004) tid=5928
ebpret=0x01259b2a ebpret2=0x0120481b scan=0x01259b2a,0x0140fd08,0x0120481b,0x0135e74a
15:15:26.531 [tid 5928] watchhit seq=18 mark=1 slot=1 dr6=0xffff0ff2 eip=0x0125990d value=4(0x00000004) tid=5928
ebpret=0x012048dd ebpret2=0x0139dfa1 scan=0x0140fce0,0x012048dd,0x00000000,0x00000000
15:15:26.968 [tid 5300] watchhit seq=19 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012ec3e9 value=25(0x00000019) tid=5928
ebpret=0x0130fce2 ebpret2=0x0120f6e6 scan=0x0130fce2,0x013dad98,0x0120f6e6,0x01204909
15:15:26.968 [tid 5300] watchhit seq=20 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=26(0x0000001a) tid=5928
ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
15:15:26.968 [tid 5300] watchhit seq=21 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=27(0x0000001b) tid=5928
ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
15:15:26.968 [tid 5300] watchhit seq=22 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=28(0x0000001c) tid=5928
ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
15:15:26.968 [tid 5300] watchhit seq=23 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=29(0x0000001d) tid=5928
ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
15:15:26.968 [tid 5300] watchhit seq=24 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012c946e value=30(0x0000001e) tid=5928
ebpret=0x013100a4 ebpret2=0x0120f6e6 scan=0x013100a4,0x013dad98,0x0120f6e6,0x01204909
15:15:26.968 [tid 5300] watchhit seq=25 mark=2 slot=0 dr6=0xffff0ff1 eip=0x0130bf01 value=31(0x0000001f) tid=5928
ebpret=0x013102b4 ebpret2=0x0120f6e6 scan=0x01402148,0x012c9514,0x0141bb08,0x013102b4
15:15:26.968 [tid 5300] watchhit seq=26 mark=2 slot=0 dr6=0xffff0ff1 eip=0x0131046f value=32(0x00000020) tid=5928
ebpret=0x0120f6e6 ebpret2=0x0139dfa1 scan=0x013dad98,0x0120f6e6,0x01204909,0x00000000
15:15:26.968 [tid 5300] watchhit seq=27 mark=2 slot=0 dr6=0xffff0ff1 eip=0x0131046f value=33(0x00000021) tid=5928
ebpret=0x0120f6e6 ebpret2=0x0139dfa1 scan=0x013dad98,0x0120f6e6,0x01204909,0x00000000
15:15:26.968 [tid 5300] watchhit seq=28 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012e57ad value=34(0x00000022) tid=5928
ebpret=0x01310652 ebpret2=0x0120f6e6 scan=0x01339285,0x013fb198,0x01310652,0x013dad98
15:15:26.968 [tid 5300] watchhit seq=29 mark=2 slot=0 dr6=0xffff0ff1 eip=0x0125c6f3 value=35(0x00000023) tid=5928
ebpret=0x72433b04 ebpret2=0x724337d4 scan=0x0141005c,0x012e57ad,0x00000000,0x00000000
15:15:26.968 [tid 5300] watchhit seq=30 mark=2 slot=2 dr6=0xffff0ff4 eip=0x0125cc94 value=1(0x00000001) tid=5928
ebpret=0x72433b04 ebpret2=0x724337d4 scan=0x0141005c,0x012e57ad,0x00000000,0x00000000
15:15:26.968 [tid 5300] watchhit seq=31 mark=2 slot=2 dr6=0xffff0ff4 eip=0x01205055 value=4(0x00000004) tid=5928
ebpret=0x0139dfa1 ebpret2=0x013a5651 scan=0x012317aa,0x0140c080,0x0124cac1,0x0124cad9
15:15:26.968 [tid 5300] watchhit seq=32 mark=2 slot=0 dr6=0xffff0ff1 eip=0x012592cd value=36(0x00000024) tid=5928
ebpret=0x72406104 ebpret2=0x72405dd4 scan=0x0140fca0,0x00e81000,0x01205055,0x00000000
15:15:26.968 [tid 5300] watchhit seq=33 mark=2 slot=2 dr6=0xffff0ff4 eip=0x0125dd49 value=0(0x00000000) tid=5928
ebpret=0x01204fe4 ebpret2=0x0139dfa1 scan=0x01410138,0x01204fe4,0x010345f4,0x012290be
15:15:26.968 [tid 5300] watchhit seq=34 mark=2 slot=3 dr6=0xffff0ff8 eip=0x0125dd49 value=0(0x00000000) tid=5928
ebpret=0x01204fe4 ebpret2=0x0139dfa1 scan=0x01410138,0x01204fe4,0x010345f4,0x012290be