merge lane H probe module (header regenerated, not hand-resolved)
This commit is contained in:
commit
db99971efb
17 changed files with 1293 additions and 3 deletions
|
|
@ -102,11 +102,15 @@ if(WIN32)
|
||||||
src/shim/hooks/fleet_movement.cpp
|
src/shim/hooks/fleet_movement.cpp
|
||||||
src/shim/hooks/player_turn.cpp
|
src/shim/hooks/player_turn.cpp
|
||||||
src/shim/hooks/tail_rng.cpp
|
src/shim/hooks/tail_rng.cpp
|
||||||
src/shim/hooks/draw_sites.cpp)
|
src/shim/hooks/draw_sites.cpp
|
||||||
|
src/shim/hooks/probe_entry.cpp)
|
||||||
|
# `minhook` is here for its include directory: lane H's probe_entry.cpp installs its own
|
||||||
|
# detours (MH_CreateHook/MH_EnableHook) rather than handing descriptors back to main.cpp,
|
||||||
|
# because a register-transparent asm stub has no C++ prototype for the template to take.
|
||||||
target_link_libraries(shim_hooks PUBLIC shim_trace sots_addresses sots_game_config sots_game_sim
|
target_link_libraries(shim_hooks PUBLIC shim_trace sots_addresses sots_game_config sots_game_sim
|
||||||
sots_game_effects mars_rng shim_budget shim_techfx
|
sots_game_effects mars_rng shim_budget shim_techfx
|
||||||
shim_colony shim_movement shim_events
|
shim_colony shim_movement shim_events
|
||||||
shim_player_turn shim_rng_ledger)
|
shim_player_turn shim_rng_ledger minhook)
|
||||||
target_compile_options(shim_hooks PRIVATE -Wall -Wextra -Werror)
|
target_compile_options(shim_hooks PRIVATE -Wall -Wextra -Werror)
|
||||||
|
|
||||||
add_library(binkw32 SHARED src/shim/main.cpp src/shim/fpu_force.cpp src/shim/binkw32.def)
|
add_library(binkw32 SHARED src/shim/main.cpp src/shim/fpu_force.cpp src/shim/binkw32.def)
|
||||||
|
|
|
||||||
315
docs/H-probes.md
Normal file
315
docs/H-probes.md
Normal file
|
|
@ -0,0 +1,315 @@
|
||||||
|
# Lane H — five live probes, predicted before the build
|
||||||
|
|
||||||
|
Lane H, 2026-09-08. Worktree `wip/probes` off `main` `b2bad30`. Every number below was written
|
||||||
|
**before** the shim was built and before any run (method rule 2). Each section ends with the ways
|
||||||
|
the prediction can fail and the symptom of each, so a wrong prediction is informative rather than
|
||||||
|
merely wrong.
|
||||||
|
|
||||||
|
The lane exists because of method rule 18: five open questions that are one probe away and that
|
||||||
|
other lanes have been circling statically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1 — the determinism oracle, regenerated
|
||||||
|
|
||||||
|
Lane N's End Turns overwrote the three autosave *contents* on VM140. The file set is unchanged, so
|
||||||
|
every documented Load-dialog row position still holds, and `ref-turn2.sav` on the VM still hashes
|
||||||
|
`ab4ac2d7e2977260…` (checked before the run). What is gone is the byte-identical reference pair.
|
||||||
|
|
||||||
|
**P1. Loading `ref-turn2.sav` and pressing End Turn once, under build
|
||||||
|
`recap-7584bad-20260908T0615Z` with `hooks=trace`, writes**
|
||||||
|
|
||||||
|
```
|
||||||
|
(Autosave EndTurn).sav 66,732 B bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948
|
||||||
|
(Autosave).sav 67,219 B 978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921
|
||||||
|
```
|
||||||
|
|
||||||
|
These are the values `findings/subsystems/determinism-oracle.md` recorded on 2026-09-07 across five
|
||||||
|
runs in two processes, and which lane R reproduced under the recap build.
|
||||||
|
|
||||||
|
**Why this is worth a run of its own.** Everything since has changed the process the oracle runs in:
|
||||||
|
a different shim build with nineteen template hooks and seven entry-point detours installed, an
|
||||||
|
x87 control-word module, a Zuul game played to turn 64 in the same `SavedGames` directory, and eight
|
||||||
|
save files where there were four. If the pair still reproduces, the oracle is revalidated **across
|
||||||
|
all of that**, and every Phase-2 comparison that leans on it keeps its footing.
|
||||||
|
|
||||||
|
**Falsification.**
|
||||||
|
|
||||||
|
* *Either hash differs.* Then the oracle is not stable across the session's changes and nothing
|
||||||
|
downstream of it can be trusted until the cause is found. Candidate causes, in the order they
|
||||||
|
would be checked: the installed `binkw32.dll` is not the recap build (hash it); `shim.cfg` is not
|
||||||
|
the recap trace config (read it); `display.cfg` / `Profile_re.ini` changed (they are inputs to
|
||||||
|
the process, not to the save, so this would be a *new* finding); `ref-turn2.sav` is not the file
|
||||||
|
it was (already excluded — hashed before the run).
|
||||||
|
* *Only `(Autosave).sav` differs.* The pre-turn marker is written from loaded state and the
|
||||||
|
post-turn one from computed state, so a pre-turn match with a post-turn mismatch localises the
|
||||||
|
problem to the turn computation rather than to the load or the writer.
|
||||||
|
* *Only `(Autosave EndTurn).sav` differs.* That would be the more alarming of the two: it is the
|
||||||
|
load → save round trip, which `determinism-oracle.md` proved stable for the *pre-turn* form, and
|
||||||
|
it would mean the loader or the writer moved.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P2 — the four tail callees that can draw, and have never been seen to
|
||||||
|
|
||||||
|
Lane V2 closed lane K's tier-4 blind spot: of the eleven phase-23/33 vtable targets on
|
||||||
|
`StrategyServer+0x158` / `+0x15c`, **four** reach a draw on the strategic generator at
|
||||||
|
`StrategyServer+0x16c`, at eight instructions —
|
||||||
|
|
||||||
|
| outer callee | slot | inner | draw sites |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `ServerSpyManager_vslot13` 0x008877b0 | spy 13 | — | `Chance` 0x00887c8a |
|
||||||
|
| " | " | 0x008408e0 | `Chance` 0x00840929, `Chance` 0x00840a3c, `NextInt` 0x008409c7 |
|
||||||
|
| `ServerSpyManager_vslot14` 0x0088db80 | spy 14 | — | `Chance` 0x0088dc43 |
|
||||||
|
| `ServerTradeManagerImpl_vslot15` 0x0082cca0 | trade 15 | — | `Chance` 0x0082cdb8 |
|
||||||
|
| `ServerTradeManagerImpl_vslot13` 0x0088ef80 | trade 13 | 0x00820ca0 | `NextFloat` 0x00820e18 |
|
||||||
|
| " | " | 0x0088b440 | `NextInt` 0x0088b613 |
|
||||||
|
|
||||||
|
Lane Z measured **0 tail words on 8 turns**, so `tail-rng-ledger.md`'s "the tail draws nothing" is a
|
||||||
|
property of *eight turns*, not of the code. Our corpus has no live trade routes and no spy program.
|
||||||
|
|
||||||
|
**The distinction this probe exists to make** is the one a word count cannot: *not reached* versus
|
||||||
|
*reached and gated*. "Never observed firing" is currently ambiguous between them, and the two have
|
||||||
|
opposite consequences for a reimplementation — the first says the phase is conditional, the second
|
||||||
|
says the phase always runs and the gate is a data condition our saves never satisfy.
|
||||||
|
|
||||||
|
**P2a. Each of the four outer callees is entered exactly once per End Turn, on every turn, on every
|
||||||
|
save.** The phase-23 and phase-33 dispatch sites (0x007d97a7 … 0x007d9811, 0x007d989b, 0x007d98a8)
|
||||||
|
are unconditional `call [vptr+n]` in a straight-line block, so entry is not a function of game
|
||||||
|
state at all. Entry counters read `calls = 1` per End Turn for each of 0x008877b0, 0x0088db80,
|
||||||
|
0x0082cca0, 0x0088ef80.
|
||||||
|
|
||||||
|
**P2b. On the existing corpus none of the eight draw sites fires.** No `draw_sites` row appears with
|
||||||
|
`ret_rva` in {0x00887c8f, 0x0084092e, 0x00840a41, 0x008409cc, 0x0088dc48, 0x0082cdbd, 0x00820e1d,
|
||||||
|
0x0088b618} (the return address is the site plus the five bytes of the `call`), and the tail's
|
||||||
|
bracket cost stays 0 — reproducing lane Z's number under a strictly better instrument.
|
||||||
|
|
||||||
|
**P2c. The three inner functions 0x008408e0, 0x00820ca0 and 0x0088b440 are entered 0 times.** This
|
||||||
|
is the load-bearing half. Together with P2a it says the gate is *inside* the outer callee, above the
|
||||||
|
inner call — i.e. an empty container (no spy programs, no trade routes) short-circuits before any
|
||||||
|
draw is reached — and it localises the condition to one function each rather than to "somewhere in
|
||||||
|
a 200-function closure".
|
||||||
|
|
||||||
|
**Falsification.**
|
||||||
|
|
||||||
|
* *An outer callee is entered 0 times.* Then phase 23/33 is itself conditional, and lane V2's
|
||||||
|
"reached ONLY virtually, from `<site>`" needs a gate note added: reachability from that site is
|
||||||
|
not the same as being called. This is the single most useful way for P2 to be wrong.
|
||||||
|
* *An inner is entered but draws 0.* The gate is below the inner call, not above it. The word count
|
||||||
|
is still 0 and lane Z's table is unchanged, but the map moves.
|
||||||
|
* *Any of the eight sites fires on the existing corpus.* Then `tail-rng-ledger.md` §2's "the tail's
|
||||||
|
cost on these turns is 0" is still true of those eight turns and false of ours, and the ledger
|
||||||
|
needs the tail term after all — the standalone's generator model would be short by that many
|
||||||
|
words on exactly the turns that matter.
|
||||||
|
* *An entry counter reads more than 1 per turn.* The dispatch block would then be inside a loop
|
||||||
|
nobody has read, which changes the cost model from "0 or 1 draw per site" to "per iteration".
|
||||||
|
|
||||||
|
### P2b-alt — an amendment, written after P2b and before any run
|
||||||
|
|
||||||
|
Everything above was committed before I read the four callee bodies. I then read them (to name the
|
||||||
|
three inner functions for `addresses.d`), and **one of the four does not look like the other three**.
|
||||||
|
This is recorded as a competing prediction rather than folded into P2b, because retrofitting P2b
|
||||||
|
after seeing the code would destroy the test.
|
||||||
|
|
||||||
|
Read from the decompiled bodies, each confirmed to be a real function start with exactly one caller:
|
||||||
|
|
||||||
|
| callee | the loop it is | empty when |
|
||||||
|
|---|---|---|
|
||||||
|
| `ServerTradeManagerImpl_vslot15` 0x0082cca0 | over the vector at `tradeManager+0x3c .. +0x40` | no trade routes |
|
||||||
|
| `ServerSpyManager_vslot14` 0x0088db80 | over the vector at `spyManager+0x10 .. +0x14` | no spy programs |
|
||||||
|
| `ServerSpyManager_vslot13` 0x008877b0 | 1,869 bytes, same manager, not fully read | — |
|
||||||
|
| **`ServerTradeManagerImpl_vslot13` 0x0088ef80** | **over `GetServer()+0x64 .. +0x68` — the FLEETS vector** | **no fleets** |
|
||||||
|
|
||||||
|
The first two are the shape P2b assumes: an empty container short-circuits the loop and no draw is
|
||||||
|
reachable, which is exactly "no trade routes and no spy program". **The fourth is not.** Its loop
|
||||||
|
iterates fleets — every save has fleets — and the draw is gated by a chain of per-fleet tests inside
|
||||||
|
the body (`0x0088f036` calls 0x00820ca0, whose result gates `0x0088f042`'s call to 0x0088b440).
|
||||||
|
|
||||||
|
**P2b-alt. On a save with fleets, `ServerTradeManagerImpl_vslot13`'s loop body runs, and probe row 5
|
||||||
|
(0x00820ca0) fires with a non-zero count — which means a `NextFloat` at 0x00820e18 and therefore a
|
||||||
|
NON-ZERO TAIL WORD COUNT.** If so, `tail-rng-ledger.md`'s "the tail's cost on these turns is 0" is
|
||||||
|
not merely workload-limited, it is limited by a per-fleet gate that our two ledger saves happened to
|
||||||
|
fail, and the Zuul save named for having 23 fleets is the obvious place to look.
|
||||||
|
|
||||||
|
P2b and P2b-alt disagree about the same run, which is what makes it worth making. If P2b holds on a
|
||||||
|
23-fleet save, the gate is above 0x00820ca0 and the fleet loop is not the reason. If P2b-alt holds,
|
||||||
|
the four-callee blind spot is not "no trade, no spies" — it is one gate on one fleet property, and
|
||||||
|
the ledger needs the tail term now rather than after a manufactured workload.
|
||||||
|
|
||||||
|
**What this probe cannot settle without a manufactured workload.** Whether the sites draw *when*
|
||||||
|
trade routes or spy programs exist. That needs a save with both, and building one is the expensive
|
||||||
|
part of this lane; if it is not built, P2 downgrades from "the tail never draws" to "the tail never
|
||||||
|
draws on a game with no trade and no espionage", which is a **narrower** claim than the one in the
|
||||||
|
ledger today and must be written that way (rule 6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P3 — `DetectEncounters`, the last non-research generator residual
|
||||||
|
|
||||||
|
Lane Y's standalone models 16 of a turn's ~20 words; the residual on its two calibrated pairs is 4
|
||||||
|
and 2, and **2 words on both are encounter detection** — sites 0x0050329d (`NextFloat`, depth 4) and
|
||||||
|
0x007929a4 (`NextInt`, depth 3), each 1 call and 1 word per turn on all three measured turns. Lane Y
|
||||||
|
refused to model it: *"fitting 2/turn to three observations is not a model."*
|
||||||
|
|
||||||
|
Lane I bounds the containing family at `|contacts| × |detectors|` and established that
|
||||||
|
`EncounterDetect_AssignContacts` draws one inlined `NextFloat` **per (contact, detector) trial,
|
||||||
|
before the accept test** — so an unteched detector has threshold `0.0f`, burns a word, and can never
|
||||||
|
accept. But lane Z's site run found `AssignContacts` **never ran**: the gate in
|
||||||
|
`EncounterDetect_ProcessTeamRecord` (0x007ca640) was not satisfied on any measured turn.
|
||||||
|
|
||||||
|
So the probe is on the **caller**. `EncounterDetect_ProcessTeamRecord` is bracketed with the three
|
||||||
|
quantities lane I derived from its instruction stream, computed at hook entry from the record:
|
||||||
|
|
||||||
|
* `gate` — ∃ entry of `rec->(+0x28 .. +0x2c)`, stride 0x44, with `entry[0]->+0xfc != 0`;
|
||||||
|
* `contacts` — entries whose object has `+0xfc != 0`;
|
||||||
|
* `detectors` — entries whose object has `+0xfc == 0` **and** `+0xfb == 0`;
|
||||||
|
* `bound` = `contacts × detectors`, the `AssignContacts` worst case.
|
||||||
|
|
||||||
|
**P3a. `ProcessTeamRecord` is called once per 0x74-byte `TeamRecord`** in the vector
|
||||||
|
`EncounterDetect_Run` receives — the same `StrategyServer+0x1e8` vector the tail hooks count as
|
||||||
|
`encounters`. On `ref-turn2` that vector holds **1** record by the time the tail runs, so the
|
||||||
|
expected call count per turn is small and single-digit.
|
||||||
|
|
||||||
|
**P3b. The turn's 2 detection words are consumed inside `ProcessTeamRecord`.** Both residual sites
|
||||||
|
sit at depth 3 and 4 below `DetectEncounters`, and `ProcessTeamRecord` is the only depth-2 function
|
||||||
|
on that path. The bracket over it therefore reads **2 words per turn**, and the detection residual
|
||||||
|
outside it is **0**.
|
||||||
|
|
||||||
|
**P3c — the one that changes the map. The 2 words are *not* the `AssignContacts` family.** `gate`
|
||||||
|
reads **false** on every record, `AssignContacts` is not called, and the `|contacts| × |detectors|`
|
||||||
|
bound is **irrelevant to the observed residual**. If that holds, lane Y's 2-word residual and lane
|
||||||
|
I's bound are two different things that have been discussed as one, and the residual is a *fixed*
|
||||||
|
per-record cost (one `NextInt` + one `NextFloat`) somewhere above the gate — which is a formula, and
|
||||||
|
a much cheaper one than the family bound.
|
||||||
|
|
||||||
|
**Falsification.**
|
||||||
|
|
||||||
|
* *`ProcessTeamRecord` costs 0 words.* Then the two residual sites are reached by some other path
|
||||||
|
out of `DetectEncounters` and P3b's depth reasoning is wrong; the next probe is a bracket on
|
||||||
|
`DetectEncounters` itself.
|
||||||
|
* *`gate` is true and `AssignContacts` fires.* Excellent outcome, not a failure: `contacts` and
|
||||||
|
`detectors` are then both recorded live and the bound becomes checkable against a measured word
|
||||||
|
count for the first time. P3c is wrong and lane I's family is the residual after all.
|
||||||
|
* *The cost is 2 per record rather than 2 per turn.* Then it scales with the record count and the
|
||||||
|
standalone needs `2 × |records|`, not `+2`. This is distinguishable only if some turn has more
|
||||||
|
than one record — which is exactly why the call count is recorded, not assumed.
|
||||||
|
* *My computed `contacts`/`detectors` disagree with the callee's own when `AssignContacts` does
|
||||||
|
fire.* Then lane I's classification rules are wrong or the record layout is, and the hook says so
|
||||||
|
by printing both numbers side by side rather than only mine.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P4 — `CreateRaidEncounter`: separating two hypotheses a word count cannot
|
||||||
|
|
||||||
|
A *successful* trade-raid roll calls `ServerTradeManagerImpl` slot 17,
|
||||||
|
`ServerTradeManager_CreateRaidEncounter` 0x008938a0, which draws a `NextInt` at 0x008939ee to pick a
|
||||||
|
target but returns at 0x0089391c **without drawing** when the candidate vector is empty. It cost 0
|
||||||
|
words on all three of lane Z's measured turns.
|
||||||
|
|
||||||
|
Lane Z read that as "consistent either with no roll succeeding (≈11% on the defaults, so three quiet
|
||||||
|
turns in a row is unremarkable) or with an empty candidate list every time".
|
||||||
|
|
||||||
|
**A correction to that reading, before the run (rule 11).** 11% is the probability of a quiet
|
||||||
|
*turn*, not of three. With eight player entries and the image defaults the per-turn probability that
|
||||||
|
no roll at all succeeds is `0.8^8 × 0.95^8 = 0.1113`; three consecutive quiet turns is `0.00138`, or
|
||||||
|
about **1 in 720**. The two hypotheses were never equally likely. The expected number of successes
|
||||||
|
per turn is `8 × 0.2 + 8 × 0.05 = 2.0`.
|
||||||
|
|
||||||
|
**P4. `CreateRaidEncounter` is entered on essentially every turn — about 2 calls per turn — and
|
||||||
|
draws 0 words because the candidate list is empty.** Entry counter ≥ 1 on at least two of three
|
||||||
|
turns; no `draw_sites` row at `ret_rva` 0x008939f3.
|
||||||
|
|
||||||
|
**Falsification.**
|
||||||
|
|
||||||
|
* *0 entries across three turns.* Under the model that is a 1-in-720 event, so it would mean the
|
||||||
|
model is wrong, not that we were unlucky. Two concrete candidates, both checkable next: the odds
|
||||||
|
are not the image defaults on this save (a `StrategyVar` override in the game data), or one of the
|
||||||
|
two per-player skip gates lane Z found in `GenerateTradeRaidEncounters` (the `visitedMask` bit
|
||||||
|
test at 0x00893302, the `>2` pre-filter at 0x008933e0) suppresses the *call* while still spending
|
||||||
|
the `Chance` word — which would also mean the 16 words are not 16 independent rolls.
|
||||||
|
* *Entries > 0 and words > 0.* Then a raid target was picked, the candidate list is non-empty, and
|
||||||
|
the standalone needs the extra word **and** the target-selection draw — the row lane Y flagged as
|
||||||
|
"the one that will bite" becomes live rather than theoretical.
|
||||||
|
* *Entries far above 2 per turn.* The roll is not the only caller, or the odds are higher than the
|
||||||
|
defaults.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P5 — the Zuul word count: what the raid loop actually iterates
|
||||||
|
|
||||||
|
Lane Z's free prediction, restated by lane Y as a consequence of its model: the two `Chance` sites in
|
||||||
|
`GenerateTradeRaidEncounters` cost one word per entry of `StrategyServer::Players`, which holds
|
||||||
|
**8** on the Human saves and **7** on the Zuul ones (`Sim.NumPlrs`: `#empires + one rebel AI per
|
||||||
|
distinct empire species + 4 NPC pseudo-players`). Lane N ran a Zuul End Turn but with the RNG hooks
|
||||||
|
**off**, so it does not answer this.
|
||||||
|
|
||||||
|
**P5. On a Zuul save each of 0x00893426 and 0x00893513 records `calls = 7`, `words = 7`, for 14
|
||||||
|
words at the trade-raid block instead of 16**, and the turn total is `14 + 2 (detection) + 0–2
|
||||||
|
(research)` = **16–18**, against 18–22 on the Human save.
|
||||||
|
|
||||||
|
**Falsification, and this is the point of the probe.**
|
||||||
|
|
||||||
|
* *8 calls per site on a 7-entry player vector.* The loop is not iterating the player vector. The
|
||||||
|
next candidates, in order: the `TeamRecord` vector it is handed as an argument, a species list, or
|
||||||
|
a fixed NPC count plus empires. This is the outcome that would matter most — lane Y's
|
||||||
|
`trade_raid.cpp` counts `NumPlrs` and would be wrong on every save.
|
||||||
|
* *7 calls at one site and 8 at the other.* The two sites have different loop bounds; the NPC site's
|
||||||
|
player-independent gate (`0.0f < S->+0x1a0`) is already known to be all-or-nothing per turn, so
|
||||||
|
this would mean something else again.
|
||||||
|
* *Fewer than 7.* One of the two per-player skip gates fires on the Zuul save and never fired on the
|
||||||
|
Human one — which would make the 16 an upper bound rather than the hard bound lane Z reported.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What is being measured, and what is not
|
||||||
|
|
||||||
|
Every hook in this lane declares the strategic generator and **nothing else**, exactly as lane Z's
|
||||||
|
family does. None of them makes any claim about whether the turn was computed correctly. The entry
|
||||||
|
counters are register-transparent asm stubs that log and tail-jump to the trampoline: they cannot
|
||||||
|
be wrong about a calling convention because they never assume one, and they cannot report a word
|
||||||
|
count, only a call count — which is deliberate, because the word counts already come from two
|
||||||
|
independent instruments that agree.
|
||||||
|
|
||||||
|
A clean run here is a statement about RNG accounting and coverage, and about nothing else.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Outcome — written after the runs
|
||||||
|
|
||||||
|
Everything above this line was committed before the shim was built. This section is the adjudication;
|
||||||
|
the full report with the evidence is `sots-re/findings/control-flow/tail-probes.md`.
|
||||||
|
|
||||||
|
| prediction | verdict |
|
||||||
|
|---|---|
|
||||||
|
| **P1** oracle reproduces | **held.** `bb4fd9ac89f41e3b…` and `978041acd168b56e…`, both byte-exact |
|
||||||
|
| **P2a** four outer callees entered once per End Turn | **held.** 1 per turn, 3 turns, 2 saves, every row |
|
||||||
|
| **P2b** none of the eight draw sites fires on the corpus | **held.** No draw-site row at any of the eight return addresses; tail cost 0 |
|
||||||
|
| **P2c** the three inner callees entered 0 times | **held.** The gate is inside each outer body, above the inner call |
|
||||||
|
| **P2b-alt** the fleet loop makes slot 13 draw on a fleet-rich save | **falsified.** 23 fleets, slot 13 entered, 0x00820ca0 entered zero times — the gate is the per-fleet test chain |
|
||||||
|
| **P3a** `ProcessTeamRecord` called once per record | **vacuous.** It was called **zero** times; the record vector is empty |
|
||||||
|
| **P3b** the 2 detection words are inside `ProcessTeamRecord` | **falsified**, exactly as its falsification clause anticipated. They are in 0x007d5150's subtree, one call from `DetectEncounters` |
|
||||||
|
| **P3c** the 2 words are not the `AssignContacts` family | **held**, and for a stronger reason than predicted: not a failed gate, an empty vector |
|
||||||
|
| **P4** `CreateRaidEncounter` entered ~2/turn, drawing 0 | **held.** 2 entries on one turn, 1 on the next, 0 words on all of them — candidate list empty |
|
||||||
|
| **P5** 7 calls / 7 words per site on a Zuul save, 14 not 16 | **held exactly**, on two consecutive turns |
|
||||||
|
|
||||||
|
## The thing no prediction covered
|
||||||
|
|
||||||
|
**The instrument changed the game.** Three End Turns under the full lane-H build produced 19/18/19
|
||||||
|
words where lane Z recorded 19/18/20, from a *byte-identical* input save, and an autosave 4 bytes
|
||||||
|
different from the one lane Z's runs produced twice. Six single-End-Turn runs from `z2-endturn.sav`
|
||||||
|
bisected it to one address: a MinHook detour on `ServerTradeManager_GenerateTradeRaidEncounters`
|
||||||
|
0x00893290. `hooks=off`, the template hooks alone, and the first eight probes all reproduce the
|
||||||
|
un-instrumented result; adding the ninth does not.
|
||||||
|
|
||||||
|
Two consequences for this repo:
|
||||||
|
|
||||||
|
1. `probes=` was added to `shim.cfg` so the probe set is switchable and bisectable. Every number in
|
||||||
|
the report was re-taken at `probes=8`, the largest configuration proved byte-identical to the
|
||||||
|
un-instrumented game.
|
||||||
|
2. **Lane Z's ledger is behaviour-neutral and its published numbers stand** — that had never been
|
||||||
|
checked, and it came out the right way.
|
||||||
|
|
||||||
|
The mechanism is undetermined. The five bytes MinHook patches at 0x00893290 land on a clean
|
||||||
|
instruction boundary (`push ebp; mov ebp,esp; push -1`) with no branch target inside them, so a
|
||||||
|
mangled prologue is not the explanation. The suppressed draw is `RollResearchEvent`'s at 0x0088df4f,
|
||||||
|
which runs *earlier in the turn* than the hooked function — which is the part to chase.
|
||||||
204
src/shim/hooks/probe_entry.cpp
Normal file
204
src/shim/hooks/probe_entry.cpp
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
#include "shim/hooks/probe_entry.h"
|
||||||
|
|
||||||
|
#include <cstdarg>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "MinHook.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "generated/sots_addresses.h"
|
||||||
|
|
||||||
|
// ---- the counters ------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Plain 32-bit counters, incremented from a stub that has already saved the flags, so no atomic is
|
||||||
|
// needed for correctness of the arithmetic on x86 and none is used: the turn pipeline is
|
||||||
|
// single-threaded (lane Z §7) and a torn read here would cost a count, not a crash. If a probe ever
|
||||||
|
// reads a wild number, that is itself the finding -- it would mean the address is entered off the
|
||||||
|
// turn thread.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr std::size_t kMaxProbes = 16;
|
||||||
|
std::uint32_t g_calls[kMaxProbes];
|
||||||
|
std::uint32_t g_total[kMaxProbes];
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" void ProbeHit(int index) {
|
||||||
|
if (index < 0 || static_cast<std::size_t>(index) >= kMaxProbes) return;
|
||||||
|
++g_calls[index];
|
||||||
|
++g_total[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- one trampoline slot and one asm stub per probe ---------------------------------------------
|
||||||
|
//
|
||||||
|
// The stub is written out per index rather than generated at runtime because a runtime-generated
|
||||||
|
// thunk would need an executable allocation and a relocation, and MinHook already owns that
|
||||||
|
// problem. Sixteen slots is the cap; `kProbes` below is shorter and the rest are inert.
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#define PROBE_STUB(i) \
|
||||||
|
extern "C" void* g_tr_probe##i; \
|
||||||
|
void* g_tr_probe##i = nullptr; \
|
||||||
|
extern "C" void ProbeStub##i(void); \
|
||||||
|
asm(".text\n" \
|
||||||
|
".globl _ProbeStub" #i "\n" \
|
||||||
|
"_ProbeStub" #i ":\n" \
|
||||||
|
" pushfl\n" \
|
||||||
|
" pushal\n" \
|
||||||
|
" pushl $" #i "\n" \
|
||||||
|
" call _ProbeHit\n" \
|
||||||
|
" addl $4, %esp\n" \
|
||||||
|
" popal\n" \
|
||||||
|
" popfl\n" \
|
||||||
|
" jmp *_g_tr_probe" #i "\n")
|
||||||
|
|
||||||
|
PROBE_STUB(0);
|
||||||
|
PROBE_STUB(1);
|
||||||
|
PROBE_STUB(2);
|
||||||
|
PROBE_STUB(3);
|
||||||
|
PROBE_STUB(4);
|
||||||
|
PROBE_STUB(5);
|
||||||
|
PROBE_STUB(6);
|
||||||
|
PROBE_STUB(7);
|
||||||
|
PROBE_STUB(8);
|
||||||
|
PROBE_STUB(9);
|
||||||
|
PROBE_STUB(10);
|
||||||
|
PROBE_STUB(11);
|
||||||
|
#undef PROBE_STUB
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace shim::hooks {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
namespace A = sots::addr;
|
||||||
|
|
||||||
|
struct ProbeDef {
|
||||||
|
const char* name;
|
||||||
|
std::uint32_t rva;
|
||||||
|
void* stub;
|
||||||
|
void** trampoline;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::uintptr_t g_exe_base = 0;
|
||||||
|
void (*g_log)(const char*) = nullptr;
|
||||||
|
bool g_installed[kMaxProbes];
|
||||||
|
|
||||||
|
void logf(const char* fmt, ...) {
|
||||||
|
if (!g_log) return;
|
||||||
|
char line[512];
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
std::vsnprintf(line, sizeof line, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
g_log(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#define PROBE(name, rva, i) ProbeDef{name, rva, reinterpret_cast<void*>(&ProbeStub##i), &g_tr_probe##i}
|
||||||
|
#else
|
||||||
|
#define PROBE(name, rva, i) ProbeDef{name, rva, nullptr, nullptr}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// The probe set. Order is the report order and the stub index, so it is fixed.
|
||||||
|
//
|
||||||
|
// Rows 0-3 are the four phase-23/33 callees lane V2 proved can reach the generator; rows 4-6 are
|
||||||
|
// the three inner functions those bodies reach the draw *through*, so a row-0..3 hit with no
|
||||||
|
// row-4..6 hit localises the gate to the outer body. Rows 7-8 are the trade-raid pair (P4). Rows
|
||||||
|
// 9-11 are CONTROLS, and they are the reason a zero in rows 0-6 can be believed: 9 and 10 are two
|
||||||
|
// phase-23/33 callees lane V2 measured as draw-free, and 11 is `EncounterDetect_Run`, which every
|
||||||
|
// prior lane has observed running once per turn. If the controls read 0 the instrument is broken,
|
||||||
|
// not the game (method rule 1 -- a hook that compares nothing prints a clean verdict).
|
||||||
|
const ProbeDef kProbes[] = {
|
||||||
|
PROBE("Game::ServerSpyManager::vslot13", A::ServerSpyManager_vslot13, 0),
|
||||||
|
PROBE("Game::ServerSpyManager::vslot14", A::ServerSpyManager_vslot14, 1),
|
||||||
|
PROBE("Game::ServerTradeManagerImpl::vslot13", A::ServerTradeManagerImpl_vslot13, 2),
|
||||||
|
PROBE("Game::ServerTradeManagerImpl::vslot15", A::ServerTradeManagerImpl_vslot15, 3),
|
||||||
|
PROBE("Game::SpyManager::Slot13RngCallee", A::SpyManager_Slot13RngCallee, 4),
|
||||||
|
PROBE("Game::TradeManager::Slot13RngCalleeA", A::TradeManager_Slot13RngCalleeA, 5),
|
||||||
|
PROBE("Game::TradeManager::Slot13RngCalleeB", A::TradeManager_Slot13RngCalleeB, 6),
|
||||||
|
PROBE("Game::ServerTradeManager::CreateRaidEncounter", A::ServerTradeManager_CreateRaidEncounter, 7),
|
||||||
|
PROBE("Game::ServerTradeManager::GenerateTradeRaidEncounters",
|
||||||
|
A::ServerTradeManager_GenerateTradeRaidEncounters, 8),
|
||||||
|
PROBE("Game::ServerSpyManager::vslot15 [control]", A::ServerSpyManager_vslot15, 9),
|
||||||
|
PROBE("Game::ServerTradeManagerImpl::vslot14 [control]", A::ServerTradeManagerImpl_vslot14, 10),
|
||||||
|
PROBE("Game::EncounterDetect::Run [control]", A::EncounterDetect_Run, 11),
|
||||||
|
};
|
||||||
|
#undef PROBE
|
||||||
|
|
||||||
|
constexpr std::size_t kProbeCount = sizeof kProbes / sizeof kProbes[0];
|
||||||
|
static_assert(kProbeCount <= kMaxProbes, "add more PROBE_STUB() slots");
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::size_t g_install_count = kProbeCount; // `probes=` overrides; default is all of them
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool probe_config(const char* key, const char* value, std::size_t* count_out) {
|
||||||
|
if (std::strcmp(key, "probes") != 0) return false;
|
||||||
|
if (std::strcmp(value, "off") == 0 || std::strcmp(value, "none") == 0) {
|
||||||
|
g_install_count = 0;
|
||||||
|
} else if (std::strcmp(value, "all") == 0 || std::strcmp(value, "on") == 0) {
|
||||||
|
g_install_count = kProbeCount;
|
||||||
|
} else {
|
||||||
|
char* end = nullptr;
|
||||||
|
const long n = std::strtol(value, &end, 10);
|
||||||
|
if (end == value || n < 0) return true; // ours, but unparseable: leave the default
|
||||||
|
g_install_count = static_cast<std::size_t>(n) < kProbeCount
|
||||||
|
? static_cast<std::size_t>(n)
|
||||||
|
: kProbeCount;
|
||||||
|
}
|
||||||
|
if (count_out) *count_out = g_install_count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_probe_entries(std::uintptr_t exe_base, void (*log_line)(const char* line)) {
|
||||||
|
g_exe_base = exe_base;
|
||||||
|
g_log = log_line;
|
||||||
|
}
|
||||||
|
|
||||||
|
void install_probe_entries() {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
logf("probe: installing %u of %u (probes= in shim.cfg)",
|
||||||
|
static_cast<unsigned>(g_install_count), static_cast<unsigned>(kProbeCount));
|
||||||
|
for (std::size_t i = 0; i < g_install_count; ++i) {
|
||||||
|
void* target = reinterpret_cast<void*>(g_exe_base + kProbes[i].rva);
|
||||||
|
MH_STATUS s1 = MH_CreateHook(target, kProbes[i].stub, kProbes[i].trampoline);
|
||||||
|
MH_STATUS s2 = s1 == MH_OK ? MH_EnableHook(target) : s1;
|
||||||
|
g_installed[i] = (s2 == MH_OK);
|
||||||
|
logf("probe: %s rva=0x%08x -> va=%p create=%s enable=%s", kProbes[i].name, kProbes[i].rva,
|
||||||
|
target, MH_StatusToString(s1), MH_StatusToString(s2));
|
||||||
|
if (!g_installed[i])
|
||||||
|
logf("COVERAGE: probe %s NOT INSTALLED -- its call count is meaningless, not zero",
|
||||||
|
kProbes[i].name);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
for (std::size_t i = 0; i < kProbeCount; ++i) g_installed[i] = false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void probe_entries_reset() {
|
||||||
|
for (std::size_t i = 0; i < kProbeCount; ++i) g_calls[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t probe_entries_count() { return kProbeCount; }
|
||||||
|
|
||||||
|
std::size_t probe_entries_snapshot(ProbeEntryRow* out, std::size_t max) {
|
||||||
|
const std::size_t n = kProbeCount < max ? kProbeCount : max;
|
||||||
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
out[i].name = kProbes[i].name;
|
||||||
|
out[i].rva = kProbes[i].rva;
|
||||||
|
out[i].calls = g_calls[i];
|
||||||
|
out[i].total = g_total[i];
|
||||||
|
out[i].installed = g_installed[i];
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace shim::hooks
|
||||||
69
src/shim/hooks/probe_entry.h
Normal file
69
src/shim/hooks/probe_entry.h
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
// Lane H -- entry counters for functions whose *reachability* is the open question.
|
||||||
|
//
|
||||||
|
// WHY THIS IS NOT A TEMPLATE HOOK, AND NOT A DRAW-SITE DETOUR.
|
||||||
|
//
|
||||||
|
// Lane V2 found four of the eleven phase-23/33 vtable targets on `StrategyServer+0x158`/`+0x15c`
|
||||||
|
// reach a draw on the strategic generator, and lane Z measured 0 tail words over eight turns. Those
|
||||||
|
// two facts leave a question a word count cannot answer: were the callees **not reached**, or
|
||||||
|
// **reached and gated**? The consequences are opposite. "Not reached" means the phase is
|
||||||
|
// conditional and a reimplementation may skip it; "reached and gated" means the phase always runs
|
||||||
|
// and the gate is a data condition our saves never satisfy -- which is a latent divergence exactly
|
||||||
|
// like the node-line decay lane K found.
|
||||||
|
//
|
||||||
|
// The instrument therefore has to answer "did control enter this address", for functions whose
|
||||||
|
// prototypes are **not verified**. Lane V2 read the dispatch sites, not the bodies: it recorded
|
||||||
|
// `void (this)` for the vtable slots from the pushes at the call site, and nothing at all for the
|
||||||
|
// three inner functions. Declaring a C++ detour with a guessed calling convention and then calling
|
||||||
|
// the trampoline is how a shim corrupts a stack -- and the campaign has already paid for that once
|
||||||
|
// (`main.cpp`: v1 of the Initialize hook was a thiscall wrapper and crashed the game).
|
||||||
|
//
|
||||||
|
// So each probe is a **register-transparent asm stub**: `pushfl; pushal; push <index>;
|
||||||
|
// call ProbeHit; add esp,4; popal; popfl; jmp *trampoline`. It saves and restores every register
|
||||||
|
// and the flags, touches nothing but its own counter, and tail-jumps to MinHook's trampoline so
|
||||||
|
// the ORIGINAL's own `ret` does the stack cleanup. It cannot be wrong about a calling convention
|
||||||
|
// because it never assumes one, it cannot perturb arguments, and it works for `ret 0`, `ret 4`,
|
||||||
|
// `cdecl`, `thiscall` and `fastcall` alike.
|
||||||
|
//
|
||||||
|
// WHAT IT CANNOT DO, stated because it is the price of that safety: a tail-jumping stub never
|
||||||
|
// regains control, so it cannot read the generator after the call and **cannot report a word
|
||||||
|
// count** -- only a call count. That is deliberate and costs nothing here, because the word counts
|
||||||
|
// already come from two independent instruments that agree (the boundary ledger and the
|
||||||
|
// return-address detours in `draw_sites.cpp`). A probe entry row and a draw-site row are meant to
|
||||||
|
// be read together: `calls > 0` with no matching draw-site row is precisely "reached and gated".
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace shim::hooks {
|
||||||
|
|
||||||
|
struct ProbeEntryRow {
|
||||||
|
const char* name = nullptr;
|
||||||
|
std::uint32_t rva = 0;
|
||||||
|
std::uint32_t calls = 0; // entries since the last reset
|
||||||
|
std::uint32_t total = 0; // entries since process start (never reset)
|
||||||
|
bool installed = false; // false => MinHook refused; `calls == 0` means NOTHING for this row
|
||||||
|
};
|
||||||
|
|
||||||
|
// `log_line` is used once per probe at install time. Nothing logs per call: some of these run
|
||||||
|
// inside the turn pipeline and a line per entry would be a different experiment.
|
||||||
|
void init_probe_entries(std::uintptr_t exe_base, void (*log_line)(const char* line));
|
||||||
|
|
||||||
|
// Installs the probes. Safe to call once, after MH_Initialize.
|
||||||
|
void install_probe_entries();
|
||||||
|
|
||||||
|
// Config, from `probes=` in shim.cfg. Returns false if `key` is not ours.
|
||||||
|
//
|
||||||
|
// WHY THIS KEY EXISTS, and it is not convenience. Lane H measured a real perturbation: with these
|
||||||
|
// probes installed, one End Turn from a byte-identical input save produced an autosave 4 bytes
|
||||||
|
// different from the one the un-instrumented game produces, and spent 19 generator words where the
|
||||||
|
// game spends 20. An instrument that changes the thing it measures has to be switchable, or the
|
||||||
|
// question "which of the twelve does it" cannot be asked. `probes=off` installs none; `probes=N`
|
||||||
|
// installs the first N in the fixed table order, which makes the set bisectable in one build.
|
||||||
|
bool probe_config(const char* key, const char* value, std::size_t* count_out);
|
||||||
|
|
||||||
|
void probe_entries_reset();
|
||||||
|
std::size_t probe_entries_snapshot(ProbeEntryRow* out, std::size_t max);
|
||||||
|
std::size_t probe_entries_count();
|
||||||
|
|
||||||
|
} // namespace shim::hooks
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include "generated/sots_addresses.h"
|
#include "generated/sots_addresses.h"
|
||||||
#include "mars/rng/mt19937.h"
|
#include "mars/rng/mt19937.h"
|
||||||
#include "shim/hooks/draw_sites.h"
|
#include "shim/hooks/draw_sites.h"
|
||||||
|
#include "shim/hooks/probe_entry.h"
|
||||||
#include "shim/hooks/rng_ledger.h"
|
#include "shim/hooks/rng_ledger.h"
|
||||||
|
|
||||||
namespace shim::hooks {
|
namespace shim::hooks {
|
||||||
|
|
@ -403,6 +404,7 @@ void StrategyHostAutosaveHook::describe_args(std::vector<Tv>& out, void* self, v
|
||||||
// independently; a shortfall is an unattributed word, which is the whole point of collecting it.
|
// independently; a shortfall is an unattributed word, which is the whole point of collecting it.
|
||||||
if (end_turn) {
|
if (end_turn) {
|
||||||
draw_sites_reset();
|
draw_sites_reset();
|
||||||
|
probe_entries_reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DrawSiteRow rows[64];
|
DrawSiteRow rows[64];
|
||||||
|
|
@ -424,6 +426,26 @@ void StrategyHostAutosaveHook::describe_args(std::vector<Tv>& out, void* self, v
|
||||||
out.push_back(tv::u32(draw_sites_other_words()).named("draw_site_words_other_rng"));
|
out.push_back(tv::u32(draw_sites_other_words()).named("draw_site_words_other_rng"));
|
||||||
out.push_back(tv::u32(draw_sites_other_calls()).named("draw_site_calls_other_rng"));
|
out.push_back(tv::u32(draw_sites_other_calls()).named("draw_site_calls_other_rng"));
|
||||||
out.push_back(tv::u32(draw_sites_overflow()).named("draw_site_overflow"));
|
out.push_back(tv::u32(draw_sites_overflow()).named("draw_site_overflow"));
|
||||||
|
|
||||||
|
// Lane H: entry counts for addresses whose reachability -- not their cost -- is the question.
|
||||||
|
// Emitted on the same marker as the draw sites so the two are read together: a probe row with
|
||||||
|
// `calls > 0` and no draw-site row for the sites inside it is "reached and gated", which is
|
||||||
|
// exactly what eight turns of `tail words = 0` could not distinguish from "not reached".
|
||||||
|
// `installed` is carried on every row because a MinHook failure would otherwise present as a
|
||||||
|
// confident zero.
|
||||||
|
ProbeEntryRow probes[16];
|
||||||
|
const std::size_t np = probe_entries_snapshot(probes, sizeof probes / sizeof probes[0]);
|
||||||
|
std::vector<Tv> plist;
|
||||||
|
for (std::size_t i = 0; i < np; ++i) {
|
||||||
|
Tv one = tv::struct_();
|
||||||
|
one.add("name", tv::str(probes[i].name));
|
||||||
|
one.add("rva", tv::u32(probes[i].rva));
|
||||||
|
one.add("calls", tv::u32(probes[i].calls));
|
||||||
|
one.add("calls_since_launch", tv::u32(probes[i].total));
|
||||||
|
one.add("installed", tv::boolean(probes[i].installed));
|
||||||
|
plist.push_back(std::move(one));
|
||||||
|
}
|
||||||
|
out.push_back(tv::list(std::move(plist)).named("probe_entries"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Tv StrategyHostAutosaveHook::describe_ret(void* r) { return tv::ptr(r); }
|
Tv StrategyHostAutosaveHook::describe_ret(void* r) { return tv::ptr(r); }
|
||||||
|
|
@ -779,6 +801,166 @@ void EncounterDetectAssignContactsHook::coverage(trace::Coverage& c) {
|
||||||
"arg:max_trials is the upper bound only");
|
"arg:max_trials is the upper bound only");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Game::EncounterDetect::ProcessTeamRecord (lane H) -------------------------------------------
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// The record's entry vector. Lane I: the gate walks `rec->(+0x28 .. +0x2c)` with stride 0x44 and
|
||||||
|
// tests `entry[0]->+0xfc`. Both bounds are read here rather than assumed, and an implausible span
|
||||||
|
// is reported as unknown rather than iterated.
|
||||||
|
constexpr std::size_t kRecEntriesFirst = 0x28;
|
||||||
|
constexpr std::size_t kRecEntriesLast = 0x2c;
|
||||||
|
constexpr std::size_t kRecEntryStride = 0x44;
|
||||||
|
constexpr std::size_t kObjFb = 0xfb;
|
||||||
|
constexpr std::size_t kObjFc = 0xfc;
|
||||||
|
constexpr std::size_t kMaxRecEntries = 4096;
|
||||||
|
constexpr std::size_t kMaxEntryDetail = 12;
|
||||||
|
|
||||||
|
struct TeamRecordStats {
|
||||||
|
std::int32_t entries = -1;
|
||||||
|
std::int32_t gate = -1; // 1 if any entry object has +0xfc != 0
|
||||||
|
std::int32_t contacts = -1; // +0xfc != 0
|
||||||
|
std::int32_t detectors = -1; // +0xfc == 0 && +0xfb == 0
|
||||||
|
std::int32_t neither = -1; // +0xfc == 0 && +0xfb != 0 -- counted, not silently dropped
|
||||||
|
std::int32_t bound = -1; // contacts * detectors, lane I's AssignContacts worst case
|
||||||
|
std::int32_t fc_wide_disagrees = -1; // see the coverage note: byte vs dword at +0xfc
|
||||||
|
};
|
||||||
|
|
||||||
|
// Per-entry detail, so the classification is checkable instead of asserted. Lane I's rules read
|
||||||
|
// `+0xfc` and `+0xfb` as bytes; if `+0xfc` is really a dword whose low byte happens to be zero,
|
||||||
|
// every count above is wrong in the same direction and nothing else would show it. So the dword is
|
||||||
|
// read too and disagreements are counted.
|
||||||
|
struct EntryDetail {
|
||||||
|
std::uint8_t fb = 0;
|
||||||
|
std::uint8_t fc = 0;
|
||||||
|
std::uint32_t fc_wide = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
TeamRecordStats team_record_stats(void* rec, EntryDetail* detail, std::size_t* ndetail) {
|
||||||
|
TeamRecordStats st;
|
||||||
|
if (ndetail) *ndetail = 0;
|
||||||
|
if (!readable(rec, kRecEntriesLast + 4)) return st;
|
||||||
|
const char* first = static_cast<const char*>(ptr_at(rec, kRecEntriesFirst));
|
||||||
|
const char* last = static_cast<const char*>(ptr_at(rec, kRecEntriesLast));
|
||||||
|
if (!first || !last || last < first) return st;
|
||||||
|
const std::size_t span = static_cast<std::size_t>(last - first);
|
||||||
|
if (span % kRecEntryStride != 0) return st;
|
||||||
|
const std::size_t n = span / kRecEntryStride;
|
||||||
|
if (n > kMaxRecEntries || !readable(first, span)) return st;
|
||||||
|
|
||||||
|
st.entries = static_cast<std::int32_t>(n);
|
||||||
|
st.gate = st.contacts = st.detectors = st.neither = st.fc_wide_disagrees = 0;
|
||||||
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
const char* e = first + i * kRecEntryStride;
|
||||||
|
const char* obj = static_cast<const char*>(ptr_at(e, 0));
|
||||||
|
if (!readable(obj, kObjFc + 4)) continue;
|
||||||
|
const std::uint8_t fb = peek<std::uint8_t>(obj, kObjFb);
|
||||||
|
const std::uint8_t fc = peek<std::uint8_t>(obj, kObjFc);
|
||||||
|
const std::uint32_t fcw = peek<std::uint32_t>(obj, kObjFc);
|
||||||
|
if ((fc != 0) != (fcw != 0)) ++st.fc_wide_disagrees;
|
||||||
|
if (fc != 0) {
|
||||||
|
++st.contacts;
|
||||||
|
st.gate = 1;
|
||||||
|
} else if (fb == 0) {
|
||||||
|
++st.detectors;
|
||||||
|
} else {
|
||||||
|
++st.neither;
|
||||||
|
}
|
||||||
|
if (detail && ndetail && *ndetail < kMaxEntryDetail) {
|
||||||
|
detail[*ndetail].fb = fb;
|
||||||
|
detail[*ndetail].fc = fc;
|
||||||
|
detail[*ndetail].fc_wide = fcw;
|
||||||
|
++*ndetail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (st.contacts > 0 && st.detectors > 0) st.bound = st.contacts * st.detectors;
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TeamRecordState {
|
||||||
|
RngEntry entry;
|
||||||
|
};
|
||||||
|
TeamRecordState g_team_rec;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void EncounterDetectProcessTeamRecordHook::describe_args(std::vector<Tv>& out, void* self,
|
||||||
|
void* rec) {
|
||||||
|
void* server = readable(self, 4) ? ptr_at(self, 0) : nullptr;
|
||||||
|
g_team_rec.entry = observe_entry(server);
|
||||||
|
|
||||||
|
EntryDetail detail[kMaxEntryDetail];
|
||||||
|
std::size_t ndetail = 0;
|
||||||
|
const TeamRecordStats st = team_record_stats(rec, detail, &ndetail);
|
||||||
|
|
||||||
|
out.push_back(tv::ptr(self).named("ctx"));
|
||||||
|
out.push_back(tv::ptr(server).named("server"));
|
||||||
|
out.push_back(tv::ptr(rec).named("record"));
|
||||||
|
out.push_back(tv::i32(st.entries).named("entries"));
|
||||||
|
// `gate` is the predicate lane I read at 0x007ca671, recomputed here. `false` predicts that
|
||||||
|
// AssignContacts will NOT be called on this record -- a prediction the AssignContacts hook
|
||||||
|
// either confirms by staying silent or falsifies by firing.
|
||||||
|
out.push_back((st.gate < 0 ? tv::null() : tv::boolean(st.gate != 0)).named("gate"));
|
||||||
|
out.push_back(tv::i32(st.contacts).named("pred_contacts"));
|
||||||
|
out.push_back(tv::i32(st.detectors).named("pred_detectors"));
|
||||||
|
out.push_back(tv::i32(st.neither).named("pred_neither"));
|
||||||
|
out.push_back(tv::i32(st.bound).named("pred_max_trials"));
|
||||||
|
out.push_back(tv::i32(st.fc_wide_disagrees).named("fc_byte_vs_dword_disagreements"));
|
||||||
|
|
||||||
|
std::vector<Tv> ents;
|
||||||
|
for (std::size_t i = 0; i < ndetail; ++i) {
|
||||||
|
Tv one = tv::struct_();
|
||||||
|
one.add("fb", tv::u32(detail[i].fb));
|
||||||
|
one.add("fc", tv::u32(detail[i].fc));
|
||||||
|
one.add("fc_dword", tv::u32(detail[i].fc_wide));
|
||||||
|
ents.push_back(std::move(one));
|
||||||
|
}
|
||||||
|
out.push_back(tv::list(std::move(ents)).named("entry_flags"));
|
||||||
|
push_rng_args(out, g_team_rec.entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EncounterDetectProcessTeamRecordHook::regions(std::vector<trace::Region>& out, void*, void*) {
|
||||||
|
push_rng_region(out, g_team_rec.entry.rng);
|
||||||
|
}
|
||||||
|
|
||||||
|
EncounterDetectProcessTeamRecordHook::Args EncounterDetectProcessTeamRecordHook::rebind(
|
||||||
|
trace::Scratch&, void* self, void* rec) {
|
||||||
|
return Args(self, rec);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EncounterDetectProcessTeamRecordHook::ours(void* self, void* rec) {
|
||||||
|
using H = trace::Hook<EncounterDetectProcessTeamRecordHook>;
|
||||||
|
if (H::mode == trace::Mode::Replace) {
|
||||||
|
refuse_replace("EncounterDetect::ProcessTeamRecord");
|
||||||
|
if (H::original) H::original(self, rec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EncounterDetectProcessTeamRecordHook::coverage(trace::Coverage& c) {
|
||||||
|
tail_rng_common_coverage(c);
|
||||||
|
c.unmodelled("the whole body of ProcessTeamRecord: the gate call, the two vector builds and "
|
||||||
|
"the bucket construction",
|
||||||
|
trace::Risk::Medium,
|
||||||
|
"this hook measures the word cost of the call and recomputes three integers the "
|
||||||
|
"original derives from the same record. It models none of the work and asserts "
|
||||||
|
"nothing about the contact assignment",
|
||||||
|
"region:rng; arg:gate/pred_contacts/pred_detectors are a prediction, not a check");
|
||||||
|
c.unmodelled("`+0xfc` and `+0xfb` are read as BYTES, following lane I's reading of the "
|
||||||
|
"classifier functions",
|
||||||
|
trace::Risk::Medium,
|
||||||
|
"if either is really a wider field, every count here is wrong in the same "
|
||||||
|
"direction and no cross-check inside this hook would notice. So the dword at "
|
||||||
|
"+0xfc is read as well and any row where the two disagree is COUNTED, not "
|
||||||
|
"silently resolved -- a non-zero disagreement count means the byte reading is "
|
||||||
|
"unsafe on this workload",
|
||||||
|
"arg:fc_byte_vs_dword_disagreements; arg:entry_flags carries the raw values");
|
||||||
|
c.unmodelled("an entry whose object pointer is unreadable is skipped",
|
||||||
|
trace::Risk::Low,
|
||||||
|
"it is not counted into any of the three classes, so entries != contacts + "
|
||||||
|
"detectors + neither is the signal that this happened",
|
||||||
|
"arg:entries against the three class counts");
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Game::StrategyServer::ProcessNodeSpaceTravel ------------------------------------------------
|
// ---- Game::StrategyServer::ProcessNodeSpaceTravel ------------------------------------------------
|
||||||
|
|
||||||
void ProcessNodeSpaceTravelHook::describe_args(std::vector<Tv>& out, void* self) {
|
void ProcessNodeSpaceTravelHook::describe_args(std::vector<Tv>& out, void* self) {
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,37 @@ struct EncounterDetectAssignContactsHook {
|
||||||
static void coverage(trace::Coverage& c);
|
static void coverage(trace::Coverage& c);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- lane H: the caller of AssignContacts, which is where the residual actually is ------------
|
||||||
|
//
|
||||||
|
// `EncounterDetect::ProcessTeamRecord` 0x007ca640, `ret 4`, the ONLY caller of `AssignContacts`
|
||||||
|
// and the only depth-2 function under `StrategyServer::DetectEncounters`. Lane Y's standalone
|
||||||
|
// leaves a 2-word residual per turn on both calibrated pairs, attributed by lane Z's site table to
|
||||||
|
// 0x0050329d (`NextFloat`, depth 4) and 0x007929a4 (`NextInt`, depth 3) -- both below this
|
||||||
|
// function. Lane Z also found `AssignContacts` **never ran**, because the gate here was never
|
||||||
|
// satisfied. So the residual is not the `|contacts| x |detectors|` family that lane I bounded; it
|
||||||
|
// is something above that gate, and no instrument has ever bracketed the function that contains
|
||||||
|
// both.
|
||||||
|
//
|
||||||
|
// This hook computes lane I's three quantities from the record AT ENTRY -- the gate predicate, the
|
||||||
|
// contact count and the detector count -- and reports them beside the measured word cost. When the
|
||||||
|
// gate is false they are a prediction of a call that will not happen (and the hook says so); when
|
||||||
|
// it is true they become checkable against the numbers `AssignContactsHook` reads from the callee's
|
||||||
|
// own arguments. Two hooks, one function apart, printing the same two integers derived two
|
||||||
|
// different ways.
|
||||||
|
struct EncounterDetectProcessTeamRecordHook {
|
||||||
|
static constexpr const char* name = "Game::EncounterDetect::ProcessTeamRecord";
|
||||||
|
static constexpr trace::CallConv conv = trace::CallConv::Thiscall;
|
||||||
|
using Ret = void;
|
||||||
|
using Args = std::tuple<void*, void*>; // ctx {S, TechDef*, TechDef*}, TeamRecord* (0x74 B)
|
||||||
|
|
||||||
|
static void describe_args(std::vector<trace::Tv>& out, void* self, void* rec);
|
||||||
|
static void regions(std::vector<trace::Region>& out, void* self, void* rec);
|
||||||
|
static Args rebind(trace::Scratch& s, void* self, void* rec);
|
||||||
|
static void ours(void* self, void* rec);
|
||||||
|
static trace::HookPolicy policy() { return trace::HookPolicy{}; }
|
||||||
|
static void coverage(trace::Coverage& c);
|
||||||
|
};
|
||||||
|
|
||||||
// `ProcessNodeSpaceTravel` 0x007a0e20, run TWICE a turn (ProcessTurn phase 7 and tail phase 10)
|
// `ProcessNodeSpaceTravel` 0x007a0e20, run TWICE a turn (ProcessTurn phase 7 and tail phase 10)
|
||||||
// and never swept for draws by any lane.
|
// and never swept for draws by any lane.
|
||||||
struct ProcessNodeSpaceTravelHook {
|
struct ProcessNodeSpaceTravelHook {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
#include "shim/hooks/global_consts.h"
|
#include "shim/hooks/global_consts.h"
|
||||||
#include "shim/hooks/research.h"
|
#include "shim/hooks/research.h"
|
||||||
#include "shim/hooks/draw_sites.h"
|
#include "shim/hooks/draw_sites.h"
|
||||||
|
#include "shim/hooks/probe_entry.h"
|
||||||
#include "shim/hooks/tail_rng.h"
|
#include "shim/hooks/tail_rng.h"
|
||||||
#include "shim/hooks/tech_effects.h"
|
#include "shim/hooks/tech_effects.h"
|
||||||
#include "shim/trace/hook.h"
|
#include "shim/trace/hook.h"
|
||||||
|
|
@ -84,7 +85,11 @@ Config ReadConfig() {
|
||||||
char* val = eq + 1;
|
char* val = eq + 1;
|
||||||
val[std::strcspn(val, "\r\n")] = '\0';
|
val[std::strcspn(val, "\r\n")] = '\0';
|
||||||
std::string err;
|
std::string err;
|
||||||
if (shim::fpu::apply_config(p, val, &err)) {
|
std::size_t probe_n = 0;
|
||||||
|
if (shim::hooks::probe_config(p, val, &probe_n)) {
|
||||||
|
Log("config: %s=%s -> %u lane-H entry probes", p, val,
|
||||||
|
static_cast<unsigned>(probe_n));
|
||||||
|
} else if (shim::fpu::apply_config(p, val, &err)) {
|
||||||
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
|
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
|
||||||
else Log("config: %s=%s", p, val);
|
else Log("config: %s=%s", p, val);
|
||||||
} else if (cfg.trace.apply(p, val, &err)) {
|
} else if (cfg.trace.apply(p, val, &err)) {
|
||||||
|
|
@ -180,6 +185,8 @@ using ApplyEncounterHook = shim::trace::Hook<shim::hooks::ApplyEncounterResultHo
|
||||||
using NodeDecayHook = shim::trace::Hook<shim::hooks::NodeLineDecayHook>;
|
using NodeDecayHook = shim::trace::Hook<shim::hooks::NodeLineDecayHook>;
|
||||||
using NodeSpaceHook = shim::trace::Hook<shim::hooks::ProcessNodeSpaceTravelHook>;
|
using NodeSpaceHook = shim::trace::Hook<shim::hooks::ProcessNodeSpaceTravelHook>;
|
||||||
using AssignContactsHook = shim::trace::Hook<shim::hooks::EncounterDetectAssignContactsHook>;
|
using AssignContactsHook = shim::trace::Hook<shim::hooks::EncounterDetectAssignContactsHook>;
|
||||||
|
// Lane H: the caller of AssignContacts, where lane Y's 2-word detection residual lives.
|
||||||
|
using ProcessTeamRecordHook = shim::trace::Hook<shim::hooks::EncounterDetectProcessTeamRecordHook>;
|
||||||
|
|
||||||
void InstallHooks(shim::trace::Tracer& tracer) {
|
void InstallHooks(shim::trace::Tracer& tracer) {
|
||||||
const uintptr_t exeBase = reinterpret_cast<uintptr_t>(GetModuleHandleA(nullptr));
|
const uintptr_t exeBase = reinterpret_cast<uintptr_t>(GetModuleHandleA(nullptr));
|
||||||
|
|
@ -252,6 +259,10 @@ void InstallHooks(shim::trace::Tracer& tracer) {
|
||||||
// Lane I's one inlined-draw site inside ProcessTurn's closure. It leaves no call-graph edge, so
|
// Lane I's one inlined-draw site inside ProcessTurn's closure. It leaves no call-graph edge, so
|
||||||
// neither a sweep nor the entry-point detours below can see it; only a boundary hook can.
|
// neither a sweep nor the entry-point detours below can see it; only a boundary hook can.
|
||||||
InstallTemplateHook<shim::hooks::EncounterDetectAssignContactsHook>(tracer, exeBase, sots::addr::EncounterDetect_AssignContacts);
|
InstallTemplateHook<shim::hooks::EncounterDetectAssignContactsHook>(tracer, exeBase, sots::addr::EncounterDetect_AssignContacts);
|
||||||
|
// Lane H: its only caller. AssignContacts never ran on any measured turn because the gate
|
||||||
|
// here was never satisfied, so the 2-word detection residual is above that gate and no
|
||||||
|
// instrument has bracketed the function containing both.
|
||||||
|
InstallTemplateHook<shim::hooks::EncounterDetectProcessTeamRecordHook>(tracer, exeBase, sots::addr::EncounterDetect_ProcessTeamRecord);
|
||||||
|
|
||||||
// Per-call-site attribution: detour the SEVEN generator entry points and record
|
// Per-call-site attribution: detour the SEVEN generator entry points and record
|
||||||
// __builtin_return_address(0) with the word cost of each call. These are NOT template hooks --
|
// __builtin_return_address(0) with the word cost of each call. These are NOT template hooks --
|
||||||
|
|
@ -272,6 +283,12 @@ void InstallHooks(shim::trace::Tracer& tracer) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lane H: register-transparent entry counters. Installed after the draw-site detours and
|
||||||
|
// before the fpu module; they share no target with either. See probe_entry.h for why these are
|
||||||
|
// asm stubs and not C++ detours.
|
||||||
|
shim::hooks::init_probe_entries(exeBase, &ShimLogLine);
|
||||||
|
shim::hooks::install_probe_entries();
|
||||||
|
|
||||||
// Lane F: x87 control-word forcing at the turn gate + the per-tick change sampler.
|
// Lane F: x87 control-word forcing at the turn gate + the per-tick change sampler.
|
||||||
// Installed last so it is nowhere near the template hooks it is meant to measure.
|
// Installed last so it is nowhere near the template hooks it is meant to measure.
|
||||||
shim::fpu::install(exeBase, &ShimLogLine);
|
shim::fpu::install(exeBase, &ShimLogLine);
|
||||||
|
|
@ -331,6 +348,7 @@ void Shim_Init(HMODULE self) {
|
||||||
NodeDecayHook::register_policy(tracer);
|
NodeDecayHook::register_policy(tracer);
|
||||||
NodeSpaceHook::register_policy(tracer);
|
NodeSpaceHook::register_policy(tracer);
|
||||||
AssignContactsHook::register_policy(tracer);
|
AssignContactsHook::register_policy(tracer);
|
||||||
|
ProcessTeamRecordHook::register_policy(tracer);
|
||||||
// A hook that never stated what it does not check is a defect, not a detail: say so in
|
// A hook that never stated what it does not check is a defect, not a detail: say so in
|
||||||
// shim.log as well as in the trace's meta line (docs/harness-audit.md).
|
// shim.log as well as in the trace's meta line (docs/harness-audit.md).
|
||||||
for (const std::string& h : tracer.unstated_hooks())
|
for (const std::string& h : tracer.unstated_hooks())
|
||||||
|
|
|
||||||
3
src/shim/shim.cfg.hoff
Normal file
3
src/shim/shim.cfg.hoff
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Lane H: the control run. hooks=off installs NOTHING (not even the M0 asm stub), so this
|
||||||
|
# is the game with a proxy DLL that only forwards Bink exports.
|
||||||
|
hooks=off
|
||||||
52
src/shim/shim.cfg.hp0
Normal file
52
src/shim/shim.cfg.hp0
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::ProcessTurn=off
|
||||||
|
hook.Game::ServerSystem::GroupOutput=off
|
||||||
|
hook.Game::ServerSystem::ComputeTotalOutput=off
|
||||||
|
hook.Game::StrategyServer::MoveFleet=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
|
||||||
|
hook.Game::StrategyHost::Autosave=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
|
probes=0
|
||||||
52
src/shim/shim.cfg.hp10
Normal file
52
src/shim/shim.cfg.hp10
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::ProcessTurn=off
|
||||||
|
hook.Game::ServerSystem::GroupOutput=off
|
||||||
|
hook.Game::ServerSystem::ComputeTotalOutput=off
|
||||||
|
hook.Game::StrategyServer::MoveFleet=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
|
||||||
|
hook.Game::StrategyHost::Autosave=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
|
probes=10
|
||||||
52
src/shim/shim.cfg.hp11
Normal file
52
src/shim/shim.cfg.hp11
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::ProcessTurn=off
|
||||||
|
hook.Game::ServerSystem::GroupOutput=off
|
||||||
|
hook.Game::ServerSystem::ComputeTotalOutput=off
|
||||||
|
hook.Game::StrategyServer::MoveFleet=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
|
||||||
|
hook.Game::StrategyHost::Autosave=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
|
probes=11
|
||||||
52
src/shim/shim.cfg.hp4
Normal file
52
src/shim/shim.cfg.hp4
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::ProcessTurn=off
|
||||||
|
hook.Game::ServerSystem::GroupOutput=off
|
||||||
|
hook.Game::ServerSystem::ComputeTotalOutput=off
|
||||||
|
hook.Game::StrategyServer::MoveFleet=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
|
||||||
|
hook.Game::StrategyHost::Autosave=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
|
probes=4
|
||||||
52
src/shim/shim.cfg.hp8
Normal file
52
src/shim/shim.cfg.hp8
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::ProcessTurn=off
|
||||||
|
hook.Game::ServerSystem::GroupOutput=off
|
||||||
|
hook.Game::ServerSystem::ComputeTotalOutput=off
|
||||||
|
hook.Game::StrategyServer::MoveFleet=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
|
||||||
|
hook.Game::StrategyHost::Autosave=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
|
probes=8
|
||||||
52
src/shim/shim.cfg.hp9
Normal file
52
src/shim/shim.cfg.hp9
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::ProcessTurn=off
|
||||||
|
hook.Game::ServerSystem::GroupOutput=off
|
||||||
|
hook.Game::ServerSystem::ComputeTotalOutput=off
|
||||||
|
hook.Game::StrategyServer::MoveFleet=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
|
||||||
|
hook.Game::StrategyHost::Autosave=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
|
probes=9
|
||||||
52
src/shim/shim.cfg.hpall
Normal file
52
src/shim/shim.cfg.hpall
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::ProcessTurn=off
|
||||||
|
hook.Game::ServerSystem::GroupOutput=off
|
||||||
|
hook.Game::ServerSystem::ComputeTotalOutput=off
|
||||||
|
hook.Game::StrategyServer::MoveFleet=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
|
||||||
|
hook.Game::StrategyHost::Autosave=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
|
probes=all
|
||||||
49
src/shim/shim.cfg.hprobes
Normal file
49
src/shim/shim.cfg.hprobes
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::ProcessTurn=off
|
||||||
|
hook.Game::StrategyServer::MoveFleet=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
|
||||||
|
hook.Game::StrategyHost::Autosave=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
51
src/shim/shim.cfg.hprobes2
Normal file
51
src/shim/shim.cfg.hprobes2
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::ProcessTurn=off
|
||||||
|
hook.Game::ServerSystem::GroupOutput=off
|
||||||
|
hook.Game::ServerSystem::ComputeTotalOutput=off
|
||||||
|
hook.Game::StrategyServer::MoveFleet=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
|
||||||
|
hook.Game::StrategyHost::Autosave=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
Loading…
Add table
Reference in a new issue