Compare commits
2 commits
c997c6ebce
...
1b5553b0aa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b5553b0aa | ||
|
|
c224ff2214 |
4 changed files with 126 additions and 7 deletions
|
|
@ -292,3 +292,8 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| ModCount vs Frame settled | objects | verified | high | 100% | 2026-09-08 | S+0x8 took 12 writes; **S+0xc took exactly ONE**, from BeginProcessTurn+0x2a, value = the turn number. So S+0x8 IS ModCount and S+0xc is Frame, confirming lane A2 by measurement. `addresses.json` has the name on the wrong word and lane T's `PhaseCounter` IS ModCount - fix at the next fragment fold |
|
||||
| rcex CLOSED - with no VM time at all | objects | verified | high | 100% | 2026-09-08 | 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 the rcex tick in ServerSystem::ProcessTurn decrements it to 0 next turn. **7/7 across the corpus**, including human-turn2-orders.sav - a DIFFERENT game, different system, different player index (AFlags bit 3 <-> nibble 3). Falsifiers listed. `Bats2` is the same shape and remains a rule-6 hypothesis (0 everywhere) |
|
||||
| MY BRIEF GAVE THE WRONG ADDRESS for TShn | meta | verified | high | 100% | 2026-09-08 | I briefed the TShn watchpoint at `player+0x274`. **That is `observed_techs`; the TShn map is `ServerSystem+0x274`.** Lane W2 caught it - arming the briefed address would have watched the wrong object and **reported a confident nothing**. The real obstacle is separate: a std::map's nodes are heap-allocated so there is no fixed leaf to arm; the fix is to arm the MAP HEADER to trap the insertion, which is the better question anyway, ~20 lines. ALSO: both blocked containers are one add from the existing arming point - `*(S+4+0x154)+0x40` (trade routes) and `*(S+4+0x158)+0x14` (spy programs) - and for the spy vector the trapped return addresses would **name the UI nobody has found** |
|
||||
| P11 event posting - 4 leaves, and the bucket now VERIFIES | engine | verified | high | 95% | 2026-09-08 | Lane EV. Pair 1 **131 -> 127 (closed 82)**, pair 2 **72 -> 69 (closed 39)**, **regressed 0** - exactly the paths its prediction named (committed BEFORE the build), nothing else moved. STRONGER THAN THE COUNT: the bucket exists in both saves after the run, **so the checksum DESCENDS INTO IT and reports nothing** - the posted record matches the oracle on ALL EIGHT FIELDS, text included. FLT_MAX vs +inf and the EvAct rule are therefore ACTIVELY DISCONFIRMED as failure modes, not merely untriggered. WHICH EVENTS A TURN POSTS, verified across all 11 saves: turn1->2 posts two (player 0 EVENT_NO_RESEARCH id 1; player 1 EVENT_SHIPS_BUILT id 1); turn2->3 posts three (NO_RESEARCH id 2; SHIPS_BUILT id 2 then RESEARCH_OVERBUDGET id 3). **Order is readable off the ids: the build pass posts BEFORE the research pass.** Ids are per player, so no cross-player order is observable and none is assumed. Only TWO players in the entire corpus ever hold an event |
|
||||
| TWO corrections to events.md | subsystems | verified | high | 100% | 2026-09-08 | (1) **events.md said the turn handed to PostEvent is ModCount. IT IS THE FRAME** - turn2-state has Frame 2, ModCount 12, and buckets EvTurn=2. (2) **The EVENT_NO_RESEARCH gate was read BACKWARDS**: 0x00584e50 is TechTree::CollectResearchedTechs (lane T's name; argument order re-read), NOT a ListAvailableTechs, and the middle test is *"nothing was researched on this turn or later"*, not *"no affordable tech"*. zuul-turn23-fleet23 exercises it live: RESEARCH_COMPLETE on turn 22 with NO no-research event that turn, NO_RESEARCH again on turn 23. The engine's P11 had only two of the three tests and OVER-FIRED |
|
||||
| a CONTROL measurement worth copying | verify | verified | high | 100% | 2026-09-08 | Lane EV did not just report that its phase needs the AI roster - it MEASURED the cost of withholding it: with the roster withheld the phase over-fires and **REGRESSES 16 leaves on pair 1** - and ONLY on pair 1, because pair 2's input already carries the AI targets. That is why the phase stays Blocked and phase-granular. The AI research-selection gate itself is labelled a hypothesis with the workload named: across ~90 AI player-turns no AI ever posts NO_RESEARCH while the human posts it on every target-less turn |
|
||||
| tail phases post NO events (measured negative) | control-flow | verified | high | 100% | 2026-09-08 | Confirming lane K from the other side: T18/T21/T32 post nothing on either reference pair - and this is a MEASURED negative, not an absence of evidence, since other corpus saves DO exercise them. No tail phase manufactures a turn's events |
|
||||
| standalone_report gains --engine-arg | meta | verified | high | 100% | 2026-09-08 | Lane EV added a repeatable `--engine-arg` pass-through so a lane can feed the standalone the OPERATOR INPUTS A SAVE DOES NOT CARRY (e.g. --ai-player N, --data DIR) instead of hard-coding them; every run now records what it was given in the header AND in status.json. That is the right shape - the provenance travels with the number |
|
||||
|
|
|
|||
|
|
@ -216,8 +216,16 @@ this is the single event API for the whole simulation, not a research-specific h
|
|||
Every research-path site computes the turn as `*(int*)(*(char**)(player + 8) + 8)`.
|
||||
`ServerPlayer+8` is the `StrategyServer` *second* base; `0x0080e320` is
|
||||
`__thiscall void* ServerPlayer::GetServer()` = `[this+8] ? [this+8]-4 : 0`, so the field is
|
||||
`StrategyServer(primary base)+0x0c` — i.e. `ModCount`. This matches the B4 finding that
|
||||
`StrategyServer` has two bases four bytes apart.
|
||||
`StrategyServer(primary base)+0x0c`. This matches the B4 finding that `StrategyServer` has
|
||||
two bases four bytes apart.
|
||||
|
||||
**CORRECTION (lane EV 2026-09-08): that field is the FRAME (the turn counter), not
|
||||
`ModCount`.** The two are different words and the saves separate them cleanly:
|
||||
`turn2-state.sav` has `Summary/Turn = 2`, `Sim/Frame = 2` and `Sim/ModCount = 12`, and every
|
||||
event the turn posted landed in bucket `EvTurn = 2`. If the argument were `ModCount` the
|
||||
buckets would be 12 and 24. The value is the **post-increment** turn — a turn run from a save
|
||||
at turn *N* posts into bucket *N+1* — because the frame is bumped in `BeginProcessTurn`,
|
||||
before either turn driver runs.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -368,8 +376,8 @@ Posted at **0x0089168c** in `ServerPlayer::ProcessTurn`. Condition (0x0089162a
|
|||
|
||||
```
|
||||
if (player->ResT (+0x294) == NULL)
|
||||
ListAvailableTechs(&out, turn, INT_MAX, 1) ; 0x00584e50
|
||||
if (out.empty() && TechTree::0x0057da90() != 0)
|
||||
TechTree::CollectResearchedTechs(&out, turn, INT_MAX, sort=1) ; 0x00584e50
|
||||
if (out.empty() && TechTree::FindFirstAvailableTech() != NULL) ; 0x0057da90
|
||||
PostEvent(EVENTSUM_NO_RESEARCH, EVENTMSG_NO_RESEARCH, NULL, NULL, turn,
|
||||
"EVENT_NO_RESEARCH" (0x00a3332c), 1)
|
||||
```
|
||||
|
|
@ -377,6 +385,31 @@ if (player->ResT (+0x294) == NULL)
|
|||
Both strings are `"No Research Project Assigned."` — matches `turn3-state.sav` player 0
|
||||
exactly (`EvAct 1`, `EvLoc 0`, `EvPos` FLT_MAX³).
|
||||
|
||||
**CORRECTION (lane EV 2026-09-08) to this section as first written.** 0x00584e50 was named
|
||||
here as a `ListAvailableTechs` and the middle test read as "no affordable tech". It is
|
||||
`TechTree::CollectResearchedTechs` (lane T read the whole body; the call site's argument order
|
||||
was re-read here byte for byte from 0x00891600), and the test is about what was **researched**,
|
||||
not about what is available:
|
||||
|
||||
1. `ResT == NULL` — the player holds no research target;
|
||||
2. **no tech has `state == 4` (researched) with `turnResearched (+0x24) >= turn`**, i.e.
|
||||
nothing finished on this turn or later. The turn is passed as the collector's `minTurn`
|
||||
and the range runs to `INT_MAX`;
|
||||
3. at least one node is in `state == 2` (available) — that is the availability half, and it is
|
||||
what excludes the four monster factions, whose entire tree is state 4.
|
||||
|
||||
Test 2 is not decoration and the corpus exercises it: in `zuul-turn23-fleet23.sav` the human
|
||||
posts `EVENT_RESEARCH_COMPLETE` on turn 22 and **no** no-research event that turn, then
|
||||
`EVENT_NO_RESEARCH` again on turn 23. `sots-engine`'s P11 implements all three tests; before
|
||||
this lane it implemented tests 1 and 3 only, and over-fired.
|
||||
|
||||
Test 1 is the one a save cannot answer. `ResTNm` on the wire is the target the file was
|
||||
**written** with; on the reference pair all four real players start the turn with none and
|
||||
three of them acquire one *during* it, which is AI research selection. Across all eleven
|
||||
corpus saves no AI player ever posts `EVENT_NO_RESEARCH` (~90 player-turns) while the human
|
||||
posts it on every turn it lacks a target — stated as a hypothesis, with the operator's
|
||||
`--ai-player` roster standing in for the missing input.
|
||||
|
||||
Also in this window (0x008915ec–0x00891624): `RollResearchEvent` (0x0088df20) is called when
|
||||
`ResT != NULL && ResErrRoll(+0x3b4) != 0 && (const at 0x00a2c788) < progressRatio`, then
|
||||
`ResErrRoll` is cleared — the same draw the B2/B3 lanes measured. `RollResearchAccident`
|
||||
|
|
@ -399,3 +432,61 @@ Also in this window (0x008915ec–0x00891624): `RollResearchEvent` (0x0088df20)
|
|||
order is identical in both directions and is confirmed by the save, so this is a gap in
|
||||
coverage, not in confidence about the layout.
|
||||
* The 50-turn prune has never been observed running (our saves are at turn ≤ 3).
|
||||
|
||||
---
|
||||
|
||||
## 5. What a turn actually posts (lane EV 2026-09-08, measured over all 11 saves)
|
||||
|
||||
Read out of the corpus with `verify/state-checksum/state_checksum.py`, not derived. Every
|
||||
`EvImg` value, bucket and id below is a file fact.
|
||||
|
||||
**Only two players in the whole corpus ever hold an event.** `PlyrIdx 0` (the human) and
|
||||
`PlyrIdx 1` (the one AI empire that owns colonies). `PlyrIdx 2..3` are the dormant shadow
|
||||
empires — `Sav = 0`, no colonies — and although they *do* pick research targets every turn,
|
||||
their `EvNxID` is 0 on every save. `PlyrIdx 4..7` are the monster factions, whose whole tech
|
||||
tree is state 4 and whose `EvNxID` is likewise 0 throughout. So "who posts" is not a property
|
||||
of the event API; it is a property of who does anything.
|
||||
|
||||
### The reference pair
|
||||
|
||||
| pair | player | bucket | id | image | EvAct | EvLoc |
|
||||
|---|---|---|---|---|---|---|
|
||||
| turn1 -> turn2 | 0 | `EvTurn=2` | 1 | `EVENT_NO_RESEARCH` | 1 | 0 |
|
||||
| turn1 -> turn2 | 1 | `EvTurn=2` | 1 | `EVENT_SHIPS_BUILT` | 0 | 288 |
|
||||
| turn2 -> turn3 | 0 | `EvTurn=3` | 2 | `EVENT_NO_RESEARCH` | 1 | 0 |
|
||||
| turn2 -> turn3 | 1 | `EvTurn=3` | 2 | `EVENT_SHIPS_BUILT` | 0 | 288 |
|
||||
| turn2 -> turn3 | 1 | `EvTurn=3` | 3 | `EVENT_RESEARCH_OVERBUDGET` | 1 | 0 |
|
||||
|
||||
**Order within a player is readable off the ids**: on turn 3 player 1's construction event is
|
||||
id 2 and its research event id 3, so **the build pass posts before the research pass**. Ids
|
||||
are per player (`EvNxID` is on the player's own storage), so no cross-player order is
|
||||
observable from a save and none should be assumed.
|
||||
|
||||
`EVENT_SHIPS_BUILT` carries `EvAct = 0` **as stored**, because it has a subject (`EvLoc` 288,
|
||||
a real position) — the `act == 0 && !obj && !pos -> 2` rule does not fire. The only stored
|
||||
`EvAct = 2` in the corpus is `EVENT_LABACCIDENT_SMALL`.
|
||||
|
||||
### Every image the corpus contains
|
||||
|
||||
`EVENT_NO_RESEARCH`, `EVENT_SHIPS_BUILT`, `EVENT_RESEARCH_COMPLETE`, `EVENT_TECHS_UNLOCKED`,
|
||||
`EVENT_RESEARCH_OVERBUDGET`, `EVENT_FLEET_ARRIVED`, `EVENT_FLEET_EXPLORED`,
|
||||
`EVENT_FLEET_MULTIPOINT_NONODE`, `EVENT_LABACCIDENT_SMALL`, `EVENT_COLONY_NEWSETTLERS`,
|
||||
`EVENT_ENEMY_INCOMING_Human`. The last is worth noting: the **event type is the `EvImg`
|
||||
string and it is composed at run time** — `_Human` is a species suffix, so the type space is
|
||||
not a fixed enumeration and never was.
|
||||
|
||||
### Text keys used, from `Locale/EN/Strings.csv`
|
||||
|
||||
`EVENTSUM_SHIPS_BUILT` = `Ships Constructed At %s`; `EVENTMSG_SHIPS_BUILT_1SHIP` =
|
||||
`1 ship built in system %s`; `EVENTMSG_SHIPS_BUILT_NSHIPS` = `%s ships built in system %s`.
|
||||
So the construction event picks between **two** message keys on the ship count, and the count
|
||||
is substituted as a `%s`. Both `EVENTSUM_NO_RESEARCH` and `EVENTMSG_NO_RESEARCH` are the same
|
||||
string and take no parameter.
|
||||
|
||||
### Wired into `sots-engine`
|
||||
|
||||
`src/app/event_phase.{h,cpp}` bridges the save's event subtree to `game::events` and P11 posts
|
||||
through it. On the reference pair, with the data root and the AI roster supplied and
|
||||
`--commit-blocked=P11`: **4 leaves closed on turn1->turn2 and 3 on turn2->turn3, 0 regressed**,
|
||||
and the posted record agrees with the oracle on every one of its eight fields. Engine-side
|
||||
write-up: `sots-engine docs/EV-events.md`.
|
||||
|
|
|
|||
12
ghidra/addresses.d/lane-ev.json
Normal file
12
ghidra/addresses.d/lane-ev.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"entries": [
|
||||
{
|
||||
"name": "ServerPlayer_ProcessTurn_NoResearchGate",
|
||||
"addr": "0x0089162a",
|
||||
"convention": "none",
|
||||
"prototype": "the three-test gate that guards EVENT_NO_RESEARCH; the post itself is ServerPlayer_ProcessTurn_PostEventNoResearch 0x0089168c. Read byte for byte (ReVa read-memory 0x00891600+160, decoded by hand): cmp [esi+0x294],ebx / jne past the whole block -> test 1, ResT == NULL. Then a 12-byte stack vector is zeroed at [ebp-0x28] and TechTree::CollectResearchedTechs 0x00584e50 is called as ecx = this->TechTree(+0xf4), push 1 (sort), push 0x7fffffff (maxTurn), push [[esi+8]+8] (the SERVER TURN -- the same word the post below hands PostEvent as its turn argument), push &out; then cmp [ebp-0x28],[ebp-0x24] / jne past -> test 2, THE LIST CAME BACK EMPTY, i.e. NO TECH WAS RESEARCHED ON THIS TURN OR LATER. Then ecx = this->TechTree again, call TechTree::FindFirstAvailableTech 0x0057da90, test eax,eax / setne al / cmp al,bl / je past -> test 3, at least one node is in state 2. CORRECTION to the note on ServerPlayer_ProcessTurn_PostEventNoResearch, which reads 0x00584e50 as a ListAvailableTechs and the gate as 'no tech is available': it is TechTree::CollectResearchedTechs (lane T's reading, confirmed here from the call site's argument order) and the gate is about what was RESEARCHED, not about what is available -- test 3 is the availability half. The turn argument is passed as CollectResearchedTechs' minTurn, so the range is [turn, INT_MAX] over node->turnResearched(+0x24)",
|
||||
"status": "mapped",
|
||||
"source": "findings/subsystems/events.md section 3.5 (lane EV 2026-09-08); the corrected gate is what sots-engine P11 implements, and it is confirmed behaviourally on the corpus -- zuul-turn23-fleet23.sav posts EVENT_RESEARCH_COMPLETE on turn 22 and NO EVENT_NO_RESEARCH that turn, then EVENT_NO_RESEARCH again on turn 23, which is exactly test 2 firing"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -75,15 +75,16 @@ def leaf_paths(a, b, limit=200000):
|
|||
return {e.path: e for e in entries}
|
||||
|
||||
|
||||
def run_pair(binary, src, oracle, note, workdir, keep_saves):
|
||||
def run_pair(binary, src, oracle, note, workdir, keep_saves, engine_args=()):
|
||||
out_sav = os.path.join(workdir, "post-" + os.path.basename(src))
|
||||
metric = os.path.join(workdir, "metric-" + os.path.basename(src) + ".json")
|
||||
cmd = [binary, src, "--out", out_sav, "--metric", metric, "--roundtrip"]
|
||||
cmd = [binary, src, "--out", out_sav, "--metric", metric, "--roundtrip"] + list(engine_args)
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
row = {
|
||||
"input": os.path.basename(src),
|
||||
"oracle": os.path.basename(oracle),
|
||||
"note": note,
|
||||
"engineArgs": list(engine_args),
|
||||
"exit": proc.returncode,
|
||||
"stdout": proc.stdout.strip().splitlines()[-30:],
|
||||
}
|
||||
|
|
@ -148,6 +149,12 @@ def main(argv=None):
|
|||
ap.add_argument("--no-write", action="store_true", help="render only, touch nothing")
|
||||
ap.add_argument("--keep-saves", action="store_true",
|
||||
help="copy each post-turn save into verify/results/standalone/")
|
||||
# Some phases are fed by operator inputs the save does not carry -- the game's data root
|
||||
# (--data), the AI roster (--ai-player N), and which blocked phases may commit
|
||||
# (--commit-blocked=IDS). Those are properties of the recorded game, not of the tool, so
|
||||
# they are passed through rather than hard-coded, and every run records what it was given.
|
||||
ap.add_argument("--engine-arg", dest="engine_args", action="append", default=[],
|
||||
metavar="ARG", help="extra argument for sots_turn; repeatable")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
binary = find_binary(args.binary)
|
||||
|
|
@ -172,7 +179,8 @@ def main(argv=None):
|
|||
with tempfile.TemporaryDirectory() as tmp:
|
||||
for src, oracle, note in pairs:
|
||||
row, remaining = run_pair(binary, src, oracle, note, tmp,
|
||||
args.keep_saves and not args.no_write)
|
||||
args.keep_saves and not args.no_write,
|
||||
args.engine_args)
|
||||
rows.append(row)
|
||||
if row["input"] == os.path.basename(pairs[0][0]):
|
||||
all_remaining = remaining
|
||||
|
|
@ -183,6 +191,7 @@ def main(argv=None):
|
|||
"schema": "sots-standalone-status/1",
|
||||
"generated": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"binary": binary,
|
||||
"engineArgs": list(args.engine_args),
|
||||
"reference": {
|
||||
"input": ref.get("input"),
|
||||
"oracle": ref.get("oracle"),
|
||||
|
|
@ -207,6 +216,8 @@ def main(argv=None):
|
|||
w("# standalone vs the oracle")
|
||||
w("")
|
||||
w(f"generated {status['generated']} binary {binary}")
|
||||
if args.engine_args:
|
||||
w("engine args: " + " ".join(args.engine_args))
|
||||
w("")
|
||||
ph = status["phases"] or {}
|
||||
tp = status["tailPhases"] or {}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue