re: the base-solver -- 277 name-block loaders indexed, and the analog block is SOLVED
New reusable tool, tools/re-capture/name_block_bases.py -> docs/re/data/name-block-bases.txt (2880 lines, ~65 s, byte-identical across two runs). A loader that reads a table by field name keeps one base pointer and emits "addi rX, rBASE, -N" per name, so no static xref sees the strings. Solve the base from the DISPLACEMENT SET alone: every (string address, displacement) pair implies a candidate base, and the true base collects a vote from every name it explains, so it wins outright. My first cut took candidates from ONE displacement and scored the unit loader at 52/226 against the right answer's 217/226 -- vote over the whole set, not a probe. Control passes with no prior knowledge: the tool recovers sub_82341A20 -> r30 = 0x82088F94 at 217/226, and independently recovers sub_8230D1F8 (129/132), sub_822F9498 (90/91) and sub_822AE628 (81/108). 277 name-block-reading functions image-wide, with the schema each names. The analog block is SOLVED: sub_821A6CF0, r29 = 0x820A1630, 22/24. In code order it names ControlTweakName, YawMagForNormal, the 12 Tweak fields, the 8 AnalogRevice_* curves and GP_MAIN_GAME -- the whole schema in the object's own order plus its pak. r29 is built at 0x821A6D34 as addi r29, r11, 5680 = 0x820A0000 + 5680, matching the solved base exactly. It is the same function that reads PlayerParams. Two of my own verdicts withdrawn: "referenced by nothing" and the softened "not found by these routes". The measurements behind them were right; the conclusions were wrong. The base was solvable from the data the whole time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
@@ -5039,3 +5039,46 @@ in `player-tuning-tables.md`; runtime watch still the cheap way in.
|
||||
|
||||
🟡 Not settled: which table governs the measured player speeds (unchanged); a
|
||||
base-tracking sweep strong enough to follow register-passed bases was not built.
|
||||
|
||||
---
|
||||
|
||||
## ✅✅ 2026-08-27 — the base-solver: 277 name-block loaders, and the analog block is SOLVED
|
||||
|
||||
Item (b). New regenerator `tools/re-capture/name_block_bases.py` →
|
||||
`docs/re/data/name-block-bases.txt` (2 880 lines, ~65 s, byte-identical across
|
||||
two runs). **This is a reusable tool, not a one-off.**
|
||||
|
||||
🔑 **The method.** A loader that reads a table by field name keeps one base
|
||||
pointer and emits `addi rX, rBASE, -N` per name, so no static xref sees the
|
||||
strings. Solve the base from the **displacement set alone**: every (string
|
||||
address, displacement) pair implies a candidate base, and the true base collects
|
||||
a vote from **every name it explains**, so it wins outright.
|
||||
⚠️ My first cut took candidates from ONE displacement and scored the unit loader
|
||||
at **52/226** against the right answer's **217/226** — a plausible wrong base.
|
||||
**Vote over the whole set, not a probe.**
|
||||
|
||||
✅ **Control passes with no prior knowledge:** the tool recovers
|
||||
`sub_82341A20 → r30 = 0x82088F94`, **217/226**. It also independently recovers
|
||||
`sub_8230D1F8` (129/132), `sub_822F9498` (90/91) and `sub_822AE628` (81/108),
|
||||
and reports **277** name-block-reading functions image-wide with the schema each
|
||||
names — an index the corpus did not have.
|
||||
|
||||
✅✅ **The analog block is SOLVED: `sub_821A6CF0`, `r29 = 0x820A1630`, 22/24.**
|
||||
In code order it names `ControlTweakName`, `YawMagForNormal`, the 12 `Tweak`
|
||||
fields, the 8 `AnalogRevice_*` curves and `GP_MAIN_GAME` — the whole schema in
|
||||
the object's own order, plus its pak. `r29` is built at `0x821A6D34` as
|
||||
`addi r29, r11, 5680` = `0x820A0000 + 5680`, matching the solved base exactly.
|
||||
|
||||
🔑 **It is the same function that reads `PlayerParams`** (it references that
|
||||
literal and calls `sub_822F9498`): one function loads the player parameter object
|
||||
*and* the control-tweak/analog table.
|
||||
|
||||
🔴 **Two of my own verdicts withdrawn.** "Referenced by nothing" (🔴) and the
|
||||
softened "not found by these routes" (🟡) were both wrong; the measurements
|
||||
behind them — 0/28 xrefs, 0 hits from naive base tracking — were right. **The
|
||||
base was solvable from the data the whole time.** ⚠️ `r1` groups are the tool's
|
||||
noise floor (stack frames) and are excluded.
|
||||
|
||||
🟡 Not settled: how the 11 curve samples are *applied* (the loader names the
|
||||
records; the interpolation is downstream); the remaining 255 of the 277 solved
|
||||
functions are listed but unexamined; what selects `Booster` (unchanged).
|
||||
|
||||
@@ -103,7 +103,7 @@ files, which is how the same ground got covered twice.
|
||||
| [`structures/stage-settings-table.md`](structures/stage-settings-table.md) | The per-stage settings object — Camera rigs, Player limits, Difficulty_* damage record | ✅ CONFIRMED, 24 settings objects vs 29 resource objects both using Phase_1/2/3; Camera is 13/14 fields constant; GravityFactor non-zero in 4 stages; IsBoss16Enable identifies one object; the Phase block is 94 fields of which 68 never vary between phases; all 24 objects named StageParameter_S<NN> |
|
||||
| [`structures/isl-mission-timer.md`](structures/isl-mission-timer.md) | The mission timer — two sequential countdowns, not a limit and a warning | ✅ CONFIRMED — read from `sub_822639B8`; the threshold reading is |
|
||||
| [`structures/archive-naming.md`](structures/archive-naming.md) | Which archives the disc can name — 100% for menu paks, **0.0%** for the six 2D paks and `GP_READY_ROOM` | ✅ CONFIRMED — 6027 names × 16 prefixes, bimodal; unnamed entries split by content — three UI paks have ZERO unnamed IDXD (artwork only), DefTables is the only real data gap (1295 LOD/motion tables); **route 2 = the `Enumerate` declaration tables** closes DefTables to 1413/1425 IDXD (99.2 %), 12 left; XPR2 manifests refuted as a naming source |
|
||||
| [`structures/player-tuning-tables.md`](structures/player-tuning-tables.md) | **`PlayerParams`** — the player parameter table: `Booster` flight model, special attacks, 8 analog curves, the S16 boss | ✅✅ `sub_822F9498` is `PlayerParams`'s loader, NOT the unit loader `sub_82341A20` (90/90 strings); all 5 player craft fly identically, `Booster` differs on 10 fields; 🟡 nothing selects it, and the analog block has no xref — but 0-xref does not mean no reader |
|
||||
| [`structures/player-tuning-tables.md`](structures/player-tuning-tables.md) | **`PlayerParams`** — the player parameter table: `Booster` flight model, special attacks, 8 analog curves, the S16 boss | ✅✅ `sub_822F9498` is `PlayerParams`'s loader, not the unit loader; **the analog block's reader SOLVED — `sub_821A6CF0`, base `0x820A1630`**, via the new base-solver (277 name-block loaders image-wide, control 217/226 on `sub_82341A20`); 🟡 nothing selects `Booster` |
|
||||
| [`structures/hud-config.md`](structures/hud-config.md) | The in-game HUD configuration — 16 records, 419 asset paths, and a per-stage `ResourceTable` | ✅ CONFIRMED — 0 of 419 config paths resolve as pak entries |
|
||||
| [`structures/mission-script-manifest.md`](structures/mission-script-manifest.md) | `Stage\script.tbl`'s 11 non-`MISSION` fields, and the mission dialogue table they name | ✅ CONFIRMED — 1 of 7 values resolves (`message\MissionDialogMessage.tbl`, 200 records), controls in the same sweep |
|
||||
| [`structures/stage-numbering-and-player-craft.md`](structures/stage-numbering-and-player-craft.md) | The disc's stage numbering (S01–S16, S18–S29) and the player's craft per stage | ✅✅ CONFIRMED — two independent files close on 6 + 15 + 1 + 6 = 28 |
|
||||
|
||||
2880
docs/re/data/name-block-bases.txt
Normal file
2880
docs/re/data/name-block-bases.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -135,35 +135,50 @@ unit `Maneuver` 100 / 350 / 1 200 at a flat ~1.25 and misses `Booster`'s
|
||||
runtime one:** watch which of the two constant sets reaches the live flight
|
||||
struct, and re-measure with the afterburner held.
|
||||
|
||||
## 🟡 The analog-curve block is referenced by nothing *that I can see* — verdict softened
|
||||
## ✅✅ SOLVED — `sub_821A6CF0` reads the analog block, found by solving the base
|
||||
|
||||
The `AnalogRevice_*` / `Tweak` schema is a second contiguous block at
|
||||
**`0x820A119C–0x820A1378`**, in the object's own order:
|
||||
Two iterations called this block unreachable. It is not: it is read through a
|
||||
**base register**, and the base can be *solved from the displacements alone*.
|
||||
|
||||
🔑 **The method** (`tools/re-capture/name_block_bases.py` →
|
||||
[`../data/name-block-bases.txt`](../data/name-block-bases.txt)): for each group
|
||||
of `addi rX, rBASE, -N` sharing a source register, every (string address,
|
||||
displacement) pair implies one candidate base; the true base collects a vote from
|
||||
**every name it explains**, so it wins outright. ⚠️ Taking candidates from one
|
||||
displacement instead misses it — that first cut scored the unit loader at 52/226
|
||||
against the right answer's 217/226.
|
||||
|
||||
✅ **Control, with no prior knowledge: the tool recovers `r30 = 0x82088F94` for
|
||||
the unit loader `sub_82341A20`, resolving 217 of 226 displacements.** It also
|
||||
independently recovers `sub_8230D1F8` (129/132), `sub_822F9498` (90/91) and
|
||||
`sub_822AE628` (81/108) — and finds **277** such functions image-wide.
|
||||
|
||||
✅ **The answer: `sub_821A6CF0`, `r29 = 0x820A1630`, 22 of 24 displacements.** In
|
||||
code order it names
|
||||
|
||||
```
|
||||
ControlTweakName
|
||||
ControlTweakName YawMagForNormal
|
||||
mov_stick_play mov_trigger_play eye_stick_play receipt_after_b
|
||||
receipt_reverse_s receipt_tgt_near receipt_tgt_atk receipt_side_s
|
||||
minimum_side_s receipt_match_spd order_cancel_time YawMagForNormal
|
||||
AnalogRevice_adv_roll … AnalogRevice_throttle (8 curve names)
|
||||
minimum_side_s receipt_match_spd order_cancel_time
|
||||
AnalogRevice_adv_roll … AnalogRevice_throttle (8 curve names)
|
||||
GP_MAIN_GAME
|
||||
```
|
||||
|
||||
**0 of its 28 strings has a code xref**, no instruction names a `0x820A1xxx`
|
||||
operand, and a base-plus-displacement sweep (2 283 functions that form the
|
||||
`0x820A` high half) reaches it **0** times.
|
||||
— the whole `Tweak` + `AnalogRevice_*` schema, in the object's own order, plus
|
||||
the **pak** it comes from. `r29` is built at `0x821A6D34` as `addi r29, r11, 5680`
|
||||
= `0x820A0000 + 5680` = `0x820A1630`, confirming the solved base exactly.
|
||||
|
||||
🟡 **But "no xref" does not mean "no reader", and the corpus proves it.** The
|
||||
*unit*-definition name block at `0x82085B38` — which includes `Maneuver` — also
|
||||
has **0 xrefs**, and it is read by `sub_82341A20`, which builds every key as
|
||||
`addi rX, r30, -N` from a base **passed in a register** (`Maneuver` sits at
|
||||
`0x82088F94 − 13404`). No static xref can see that, and my base-tracking sweep
|
||||
cannot either: run against that known-read block as a **control**, it recovers
|
||||
only **5** references and misses `sub_82341A20` itself.
|
||||
🔑 **And it is the same function that reads `PlayerParams`** — `sub_821A6CF0`
|
||||
references that literal and calls `sub_822F9498`. So one function loads the
|
||||
player's parameter object *and* the control-tweak/analog table. 🟡 It reads the
|
||||
curves as `AnalogRevice_*` record names; how the 11 samples are then *applied*
|
||||
is still unread.
|
||||
|
||||
⇒ The measurement stands; last iteration's 🔴 "the reader is unreachable" was too
|
||||
strong. The honest verdict is **🟡 not found by xref or by naive base tracking,
|
||||
both of which have a demonstrated blind spot** for register-passed bases. A
|
||||
runtime watch is still the cheap way in.
|
||||
> Withdrawn: the earlier 🔴 "referenced by nothing" and the softened 🟡
|
||||
> "not found by these routes". Both measurements were right — 0/28 xrefs, 0 hits
|
||||
> from naive base tracking — and both conclusions were wrong. **The base was
|
||||
> solvable from the data the whole time.**
|
||||
|
||||
⚠️ **The twin-string-block trap, in the flesh.** `mov_stick_play` and
|
||||
`eye_stick_play` each exist **twice**; only the *second* copy (`0x820AA630`,
|
||||
|
||||
108
tools/re-capture/name_block_bases.py
Normal file
108
tools/re-capture/name_block_bases.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Solve the BASE of every register-relative name block, and print the schema it names.
|
||||
|
||||
A loader that reads a table by field name usually does not take each string's
|
||||
address directly -- it keeps one base pointer and emits `addi rX, rBASE, -N` per
|
||||
name. No static xref sees that, so those blocks look unreferenced.
|
||||
|
||||
This solves the base from the DISPLACEMENT SET alone: for a group of `addi`s
|
||||
sharing a source register, try every candidate base implied by (string address -
|
||||
displacement) and keep the one that lands the most displacements on a string
|
||||
start. Control: it must recover `r30 = 0x82088F94` for the unit loader
|
||||
`sub_82341A20` with no prior knowledge.
|
||||
|
||||
Regenerates docs/re/data/name-block-bases.txt.
|
||||
"""
|
||||
import sys, collections, bisect
|
||||
|
||||
DB = '/work/xenia-rs/sylpheed.db'
|
||||
MIN_GROUP = 8 # displacements needed before a group is worth solving
|
||||
MIN_RESOLVED = 12 # report a function only if the base explains this many
|
||||
LOW, HIGH = 0x82000000, 0x82400000
|
||||
|
||||
def main():
|
||||
import duckdb
|
||||
con = duckdb.connect(DB, read_only=True)
|
||||
S = set(a for (a,) in con.execute("SELECT address FROM strings").fetchall())
|
||||
txt = dict(con.execute("SELECT address, content FROM strings").fetchall())
|
||||
funcs = con.execute(
|
||||
"SELECT address,end_address,name FROM functions ORDER BY address").fetchall()
|
||||
starts = [f[0] for f in funcs]
|
||||
ins = con.execute(
|
||||
"SELECT address,operands FROM instructions WHERE mnemonic='addi' ORDER BY address"
|
||||
).fetchall()
|
||||
|
||||
print("# Register-relative name blocks: the base solved from the displacements")
|
||||
print("# Regenerate: python3 tools/re-capture/name_block_bases.py")
|
||||
print("# See docs/re/structures/player-tuning-tables.md")
|
||||
print("\n strings in the image: %d addi instructions: %d" % (len(S), len(ins)))
|
||||
|
||||
byf = collections.defaultdict(lambda: collections.defaultdict(list))
|
||||
order = collections.defaultdict(lambda: collections.defaultdict(list))
|
||||
for a, o in ins:
|
||||
i = bisect.bisect_right(starts, a) - 1
|
||||
if i < 0 or funcs[i][1] <= a:
|
||||
continue
|
||||
p = [x.strip() for x in o.split(',')]
|
||||
if len(p) != 3 or p[0] == p[1]:
|
||||
continue
|
||||
try:
|
||||
d = int(p[2], 0)
|
||||
except ValueError:
|
||||
continue
|
||||
byf[funcs[i][2]][p[1]].append(d)
|
||||
order[funcs[i][2]][p[1]].append((a, d))
|
||||
|
||||
rows = []
|
||||
for name in sorted(byf):
|
||||
for reg in sorted(byf[name]):
|
||||
D = sorted(set(byf[name][reg]))
|
||||
if len(D) < MIN_GROUP or reg == 'r1': # r1 is the stack pointer
|
||||
continue
|
||||
# Vote: every (string, displacement) pair implies one candidate base.
|
||||
# The true base collects a vote from each name it explains, so it wins
|
||||
# outright -- taking candidates from ONE displacement misses it.
|
||||
cand = collections.Counter()
|
||||
for A in S:
|
||||
for d in D:
|
||||
B = A - d
|
||||
if LOW <= B <= HIGH:
|
||||
cand[B] += 1
|
||||
best = None
|
||||
# sorted, not most_common(): a Counter's tie order varies per run
|
||||
top = sorted(cand.items(), key=lambda kv: (-kv[1], kv[0]))[:40]
|
||||
for B, _v in top:
|
||||
tot = sum(1 for d in D if (B + d) in S)
|
||||
if best is None or tot > best[1] or (tot == best[1] and B < best[0]):
|
||||
best = (B, tot)
|
||||
if best and best[1] >= MIN_RESOLVED:
|
||||
rows.append((name, reg, best[0], best[1], len(D)))
|
||||
|
||||
rows.sort(key=lambda r: (-r[3], r[0]))
|
||||
print("\n## %d functions read a name block through a base register" % len(rows))
|
||||
print(" %-18s %-5s %-12s %s" % ('function', 'reg', 'base', 'resolved / displacements'))
|
||||
for name, reg, B, tot, n in rows:
|
||||
print(" %-18s %-5s 0x%08X %d / %d" % (name, reg, B, tot, n))
|
||||
|
||||
print("\n## The schema each one names, in code order")
|
||||
for name, reg, B, tot, n in rows:
|
||||
seen, names = set(), []
|
||||
for a, d in order[name][reg]:
|
||||
t = B + d
|
||||
if t in txt and t not in seen:
|
||||
seen.add(t)
|
||||
names.append(txt[t])
|
||||
if not names:
|
||||
continue
|
||||
print("\n %s (%s, base 0x%08X) names %d:" % (name, reg, B, len(names)))
|
||||
line = " "
|
||||
for nm in names:
|
||||
if len(line) + len(nm) > 96:
|
||||
print(line)
|
||||
line = " "
|
||||
line += nm + " "
|
||||
if line.strip():
|
||||
print(line)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user