From ea4bf99fab121d8da2217fffec3c218829664d73 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 7 Jul 2026 16:40:59 -0400 Subject: [PATCH] docs(gfx): settle render drift as native cmd-buffer op 0x215 (Ghidra), align docs Read op 0x215's real handler FUN_0042a0b0 (resolved via the dispatch table ctx[0x26c93+op]; Kelebek's 0x421160 is VA-drift). It writes cmd-type 5 into the current gfx-object record and returns a std::map::find over an engine-internal registry populated by sibling gfx ops (0x1a2 hash insert). The return is native command-buffer state, not the VM global bank -> seeding story-state cannot fix the drift. Verdict: (b) a genuine native op, NOT (a) state-divergence. Reconcile the previously contradictory drift accounts onto one canonical home (engine-re.md op 0x215), with opcodes.toml carrying the opcode-level semantics and phase-a-slice-plan / tools-reference / frida README corrected to point at it instead of repeating the disproven state-divergence conclusion. Co-Authored-By: Claude Opus 4.8 --- docs/engine-re.md | 49 ++++++++++++++++++++++++++++++++++- docs/opcode-reference.md | 6 ++--- docs/phase-a-slice-plan.md | 44 +++++++++++++++---------------- docs/tools-reference.md | 2 +- tools/age_opcodes_himegari.py | 2 +- tools/frida/README.md | 3 ++- vm-map/opcodes.toml | 6 ++--- 7 files changed, 80 insertions(+), 32 deletions(-) diff --git a/docs/engine-re.md b/docs/engine-re.md index 41689ca..f17d2f4 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -136,6 +136,49 @@ decision→scene (scenes are just `SCxxxx.BIN` records loaded by their SYS4INI i --- +### op `0x215` (`query-gfx-object?`) is a native command-buffer op — settles the render drift as (b) (2026-07-07) + +**This is the canonical account of the background/sprite "drift" bug** (background pinned off-centre / +bottom-right, rest grey — `Screenshot 2026-07-06 211353.png`). It supersedes the earlier "drift = +state-divergence, seed state and it's fixed" conclusion in `docs/phase-a-slice-plan.md` and the status +memory, which are corrected to point here. + +Resolved via the dispatch table (`ctx[0x26c93 + 0x215]`): the registration routine `FUN_00413860` stores +`[ESI + 0x9baa0] = 0x42a0b0`, so op `0x215`'s **real handler is `FUN_0042a0b0`**. (Kelebek's `0x421160` is +VA-drift — it lands inside the unrelated `FUN_00421090`. Same lesson as `0x1a2`: never trust a Kelebek raw VA.) + +`FUN_0042a0b0(ctx)` does exactly two things: +1. **`*(ctx + 0x53d88 + ctx[0x53d14]*0x78) = 5`** — writes **cmd-type 5** into the *current* gfx-object + record. A **command-buffer registration** side-effect, directly parallel to op `0x1a2` (`FUN_0042d360`) + writing cmd-type 3. So `0x215` is part of the gfx command-buffer subsystem, not a pure query. +2. **`out = FUN_0047f280(FUN_0041b940(2))`** — `FUN_0041b940(2)` fetches operand 2 (the bytecode handle + key); `FUN_0047f280` is a **`std::map::find`** over an engine-internal associative registry, returning + the mapped value or **`0xffffffff` (not-found)**; `FUN_00425fb0(1, out)` writes it to operand 1. That + registry is **populated by sibling gfx ops** — op `0x1a2`'s handler builds a `"%c%8.8x"` key and calls + `FUN_0042cf70`, an open-addressing hash **insert** into the same kind of store. + +**(a) vs (b) — the verdict is (b).** The value `0x215` returns is **native command-buffer state**: "has a +gfx object already been registered under this handle?" (`≥0` = existing → use its slot; `-1` = new). That +state lives in the engine's own registry, maintained by the gfx ops, **not in the VM global bank**. So +**seeding story-state globals cannot reproduce it** — the drift is *not* the Phase-B state-divergence +problem. Stubbing `0x215` returns a constant → `label_12649`'s slot-select always takes one branch → every +draw collapses onto slot 0 → the anchor-preserve math measures foreign-sized textures → cumulative drift. + +**Why the prior "state-divergence" conclusion was wrong.** It was grounded in `capture_gfx_objects.py`, +which polled the object-*record* array (`[esi+0x53d64]`) at ~2/s and saw only 3 persistent UI objects, "0 +CG objects." But (i) the branch is driven by the **map lookup** (a different structure the poll never +observed), and (ii) command-buffer records are **transient** — a 2/s poll can't prove CG records weren't +used. Absence in that capture ≠ absence of the native path. + +**The fix is tractable and Frida-free.** (b) does *not* mean an opaque native state machine. The subsystem +is a **modelable data structure**: an object-record array (slot / geometry / cmd-type per object) plus a +handle→object registry (a hash map). The gfx ops are inserts/queries/writes against these, and the inserts +are **bytecode-driven** — so a faithful host-side model, with the gfx ops (`0x1a2`, `0x215`, and the +`0x212–0x21a` family) *executed* instead of stubbed, rebuilds the state from the same scripts. The opcode- +level summary lives in `vm-map/opcodes.toml` op `0x215`. + +--- + ## Native walls backlog (targets for this loop) - ~~**call-script dispatch**~~ — **SOLVED** (above): `call-script ` = raw SYS4INI file index. @@ -144,4 +187,8 @@ decision→scene (scenes are just `SCxxxx.BIN` records loaded by their SYS4INI i value is turned into a scene *id* (a caller of SCJUMP; re-aimed away from `u00428010`). - **op `0x60`** (`u0041A270`) — the rand-like value gating 1732/1755 SCJUMP decisions. - **gfx command-buffer** — the `0x212–0x21a` positioned-object subsystem (`scjump`-unrelated; the - rendering drift). + rendering drift). **Understood, not unidentified** (see the op `0x215` finding above): it's a native + record array + a handle→object hash registry that the gfx ops insert/query/write. The drift verdict is + settled — (b), a genuine native op, not state-divergence. Remaining work is *implementation*, not RE: + model the registry + record array host-side and execute the gfx ops (`0x1a2`/`0x215`/`0x212–0x21a`) + instead of stubbing — static, Frida-free. diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index 014d8be..8f5dc21 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -110,9 +110,9 @@ This also names the whole call graph statically (build/callscript-names.json). - **evidence:** SC0000 label_12649: set-texture(resId,slot) then 0x208(slot)->w,h feeds w/2 horizontal-center + foot-anchor subtraction into draw-texture dst; stubbing yields 0x0 sizes / off-center draws ### 0x215 `query-gfx-object?` (u00421160, argc 2) -- **summary:** 0x215 (out)(handle_id) — queries the native graphics-object manager by element handle-id (the value in 0x62455[idx], often +1/+2 for a sub-element); writes the object's slot/status into `out`, sign-tested (gre/lt 0) to drive label_12649's slot-select branch and set the working slot G[0x62452]. KEYSTONE for per-object slot selection — stubbing it collapses every draw onto slot 0, so the anchor-preserve geometry reads foreign-sized textures → cumulative bg/sprite drift (see docs/phase-a-slice-plan.md A2b-Geometry). Reads native object-manager state (NOT VM-computable). Exact return semantics: RE via unicorn (native handler @0x421160). -- **grounding:** source=investigation, confidence=med -- **evidence:** SC0000 label_12649 (0x12670) + label_123ef (0x12419/0x12450): called with 0x62455[idx] handle-ids (±offset); result gre/lt 0 branches slot-select and feeds ui-elem?(0x1f7)/set-texture slot. Record table 0x3239 (label_125bd @0x0050f) assigns per-object slots 4..13. Handles are the 0xcf08/0xe678/0xd6d8 element-id family. +- **summary:** 0x215 (out)(handle_id) — native graphics command-buffer op. Real handler FUN_0042a0b0 (Ghidra-resolved via the dispatch table ctx[0x26c93+op]; Kelebek's 0x421160 is VA-drift, lands in an unrelated fn). Does TWO things: (1) writes cmd-type 5 into the CURRENT gfx-object record `[ctx+0x53d88 + ctx[0x53d14]*0x78]` (a command-buffer registration, parallel to op 0x1a2→type 3); (2) returns `out = map.find(handle_id)` over an engine-internal associative registry (found value, else 0xffffffff=not-found), sign-tested (gre/lt 0) to drive label_12649's slot-select branch + set working slot G[0x62452]. So `out` is NATIVE COMMAND-BUFFER STATE (the registry is populated by sibling gfx ops — op 0x1a2→FUN_0042cf70 is the hash insert), NOT the VM global bank → seeding story-state CANNOT reproduce it. Stubbed → constant return → every draw collapses to slot 0 → anchor-preserve reads foreign-sized textures → the cumulative bg/sprite drift. SETTLES the drift as (b) a genuine native op, NOT (a) state-divergence. Faithful fix = model the gfx command-buffer (record array + handle→object registry) and run the gfx ops instead of stubbing — static/Frida-free (handlers now readable; inserts are bytecode-driven). Full decode + verdict: docs/engine-re.md (op 0x215 section). +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra: real handler FUN_0042a0b0 = {*(ctx+0x53d88+ctx[0x53d14]*0x78)=5; out=FUN_0047f280(FUN_0041b940(2))}. FUN_0047f280 = std::map::find (returns mapped value or 0xffffffff); FUN_0041b940(2) = operand-fetch of operand 2 (the handle key); FUN_00425fb0(1,val) = operand-write to `out`. Registry populated by op 0x1a2 handler FUN_0042d360 → FUN_0042cf70 (open-addressing hash insert). Bytecode sites: SC0000 label_12649 (0x12670) + label_123ef (0x12419/0x12450), handle-ids from 0x62455[idx] (±offset); result gre/lt 0 branches slot-select. Record table 0x3239 (label_125bd @0x0050f) assigns per-object slots 4..13. ### 0x217 `gfx-geom?` (u004211E0, argc 4) - **summary:** 4 global-ints; part of a 0x217/0x218/0x21a geometry chain diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index f701df5..60061d9 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -273,28 +273,28 @@ measured `0×0`, and the anchor-preserve math (`base' = center − (w_new/2, h_n corruption. Fix: seed `_slotDims[0] = (800,600)` (and record `create-texture(w,h)` dims) so the first CG's anchor stays an identity. This is the faithful stand-in for the skipped boot-time primary-surface creation. -**Post-opening bg/sprite drift — RESOLVED as a STATE-DIVERGENCE artifact, NOT a missing native op -(2026-07-06/07).** Symptom: everything blits through slot 0 as an immediate-mode canvas; the anchor-preserve -base globals **accumulate drift** across differently-sized textures (`BG030A→(300,500)`, next→`(450,100)`, -→`(800,350)`… marching bottom-right). We reverse-engineered the whole chain (systematic-debugging): -1. Root cause traced to **`0x215` = native graphics-object query** (opcodes.toml `query-gfx-object?`), which we - stub → `label_12649` takes the wrong branch → all draws collapse onto slot 0 → anchor-preserve reads foreign - textures → drift. -2. **Engine now statically analyzable (major, general unlock):** `SYS4AB.BIN` = `XOR-0xFF(AGE.EXE)` (dead end), - but `AGE.EXE` unpacks **in-place at 0x400000** in the live process → `tools/frida/dump_engine.py` → - `build/engine-dump/` (validated via AGF-decoder landmark; interpreter confirmed to run from the module, so - handlers are hookable). Handler ABI + object-record layout (`[esi+0x53d64]`, 120B/rec, cmd-type at rec+0x24) - decoded. See `docs/vm-mapping-plan.md` appendix + `tools/frida/README.md`. -3. **Live capture verdict (the resolution):** `tools/frida/capture_gfx_objects.py` polled the object-record - array through the **real** opening — it held only **3 persistent UI objects, ZERO CG objects**. So the real - game does **not** draw the opening CGs via the `0x212–0x21a` positioned-object path our headless VM uses; - with proper state it takes a different (direct) branch that we already render correctly. **⇒ the drift is - downstream of our unseeded headless VM taking `label_12649`'s else-branch (compute-from-drifting-base) where - the real game hits the if-branch (stored/record geometry). The fix is the Phase B state/choices-flow work, - not a separate native-op subsystem.** Seeding real per-scene/object state makes `label_12649` branch right. -Fades/alpha (`AE*`, `0x202/0x203`) + green chromakey + true multi-surface remain deferred; the compositor is -built to accept alpha later. The full-screen opening path is correct and unaffected. **Native gfx-op modeling -is only needed for scenes that genuinely use runtime-positioned sprites — revisit later with the dump in hand.** +**Post-opening bg/sprite drift — root cause SETTLED as (b) a native command-buffer op via Ghidra +(2026-07-07). ⚠ Corrects an earlier wrong "state-divergence" verdict here.** Symptom (screenshot +`Screenshot 2026-07-06 211353.png`): everything blits through slot 0 as an immediate-mode canvas; the +anchor-preserve base globals **accumulate drift** across differently-sized textures (`BG030A→(300,500)`, +next→`(450,100)`, →`(800,350)`… marching bottom-right; the background ends up pinned off-centre / bottom-right +with the rest of the screen grey). Root cause = the stubbed native op **`0x215`** collapsing every draw onto +slot 0 (its return drives `label_12649`'s slot-select). + +**The canonical decode + verdict now lives in `docs/engine-re.md` (op `0x215` section)** — don't duplicate it +here. In brief: `0x215`'s real handler `FUN_0042a0b0` (Ghidra) writes cmd-type 5 into the current gfx-object +record and returns a **`std::map::find`** over an engine-internal command-buffer registry (populated by sibling +gfx ops like `0x1a2`). That return is **native command-buffer state, not the VM global bank** → seeding +story-state **cannot** fix it. So this is **(b) a genuine native op**, *not* (a) the Phase-B state-divergence +problem. The prior conclusion in this doc — grounded in a 2/s `capture_gfx_objects.py` poll of the object-*record* +array — was wrong: it observed the wrong structure (not the lookup map) and can't rule out transient records. + +**Fix (Frida-free, implementation not RE):** model the gfx command-buffer host-side (record array + +handle→object hash registry) and *execute* the gfx ops (`0x1a2`/`0x215`/`0x212–0x21a`) instead of stubbing; +the inserts are bytecode-driven, so the state rebuilds from the same scripts. Fades/alpha (`AE*`, +`0x202/0x203`) + green chromakey + true multi-surface remain deferred; the compositor is built to accept alpha +later. The full-screen event-CG opening path (`EV052*` at `(0,0)`) is correct and unaffected — the drift is +specifically the `BG*`/sprite draws routed through `label_12649`. --- diff --git a/docs/tools-reference.md b/docs/tools-reference.md index ffa030c..9f38a36 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -123,7 +123,7 @@ texture ops (no GPU context) — run windowed for real scenes. User args (after | `tools/frida/find_global_by_sequence.py` | Runtime-global RE (SHELVED): differential resId value-scan + stability filter. Finds stack proxies; proved `G[0x62424]` is a transient arg-register. | `py -3.11 -u -X utf8 tools/frida/find_global_by_sequence.py [pid]` | running game + index → stdout | | `tools/frida/dump_engine.py` | ★ **Dump the UNPACKED engine code** from the live process for offline static RE (native handlers). `AGE.EXE` unpacks in-place at `0x400000`; Kelebek VAs map `VA−0x400000` = file-off. Validated via the AGF-decoder landmark `+0x74f1f`. | `py -3.11 -u -X utf8 tools/frida/dump_engine.py [pid]` | running game → `build/engine-dump/{manifest.json,range_.bin}` | | `tools/frida/probe_handlers.py` | Probe which region the interpreter executes from (module vs heap). Confirmed: **operand-fetch `+0x1b940` fires ~8500/s ⇒ interpreter runs from the module `0x400000`** (handlers hookable by dump address). | `py -3.11 -u -X utf8 tools/frida/probe_handlers.py [pid]` | running game → stdout (per-hook fire counts) | -| `tools/frida/capture_gfx_objects.py` | Capture the native gfx object-manager state: grab engine ctx (`esi` via operand-fetch `ecx`), poll the object-record array `[esi+0x53d64]` (20×120B; `field[0]=0xffffffff`=free, cmd-type at rec+0x24). **Finding: the real opening uses ZERO CG records ⇒ the bg/sprite drift is a state-divergence artifact, not a missing op.** | `py -3.11 -u -X utf8 tools/frida/capture_gfx_objects.py [pid] [secs]` | running game → `build/gfx-objects.jsonl` | +| `tools/frida/capture_gfx_objects.py` | Capture the native gfx object-manager state: grab engine ctx (`esi` via operand-fetch `ecx`), poll the object-record array `[esi+0x53d64]` (20×120B; `field[0]=0xffffffff`=free, cmd-type at rec+0x24). **⚠ Its "0 CG records ⇒ drift is state-divergence" reading was DISPROVEN** (Ghidra: op 0x215 read settles the drift as a native command-buffer op — `docs/engine-re.md`; the poll observed the record array, not the lookup map that drives the branch, and cmd-buffer records are transient). Kept as a runtime-observation tool. | `py -3.11 -u -X utf8 tools/frida/capture_gfx_objects.py [pid] [secs]` | running game → `build/gfx-objects.jsonl` | *(Static disassembly of `build/engine-dump/range_00400000.bin` uses **capstone** — `py -3.11 -m pip install capstone`; VA `X` → file offset `X−0x400000`.)* diff --git a/tools/age_opcodes_himegari.py b/tools/age_opcodes_himegari.py index b9dd8f6..a48393c 100644 --- a/tools/age_opcodes_himegari.py +++ b/tools/age_opcodes_himegari.py @@ -21,7 +21,7 @@ INFERRED: dict[int, dict] = { 0x1ff: dict(name='draw?', category='draw', noop=False, confidence='low', source='inference', summary='4 args (global+imms); follows 0x217, then call'), 0x202: dict(name='draw-blit?', category='draw', noop=False, confidence='med', source='inference', summary='5 args (coords/sizes); preceded by coord arithmetic, near draw ops'), 0x203: dict(name='draw?', category='draw', noop=False, confidence='med', source='inference', summary='4 args; chains with 0x202/draw-texture'), - 0x215: dict(name='query-gfx-object?', category='draw', noop=False, confidence='med', source='investigation', summary="0x215 (out)(handle_id) — queries the native graphics-object manager by element handle-id (the value in 0x62455[idx], often +1/+2 for a sub-element); writes the object's slot/status into `out`, sign-tested (gre/lt 0) to drive label_12649's slot-select branch and set the working slot G[0x62452]. KEYSTONE for per-object slot selection — stubbing it collapses every draw onto slot 0, so the anchor-preserve geometry reads foreign-sized textures → cumulative bg/sprite drift (see docs/phase-a-slice-plan.md A2b-Geometry). Reads native object-manager state (NOT VM-computable). Exact return semantics: RE via unicorn (native handler @0x421160)."), + 0x215: dict(name='query-gfx-object?', category='draw', noop=False, confidence='high', source='investigation', summary="0x215 (out)(handle_id) — native graphics command-buffer op. Real handler FUN_0042a0b0 (Ghidra-resolved via the dispatch table ctx[0x26c93+op]; Kelebek's 0x421160 is VA-drift, lands in an unrelated fn). Does TWO things: (1) writes cmd-type 5 into the CURRENT gfx-object record `[ctx+0x53d88 + ctx[0x53d14]*0x78]` (a command-buffer registration, parallel to op 0x1a2→type 3); (2) returns `out = map.find(handle_id)` over an engine-internal associative registry (found value, else 0xffffffff=not-found), sign-tested (gre/lt 0) to drive label_12649's slot-select branch + set working slot G[0x62452]. So `out` is NATIVE COMMAND-BUFFER STATE (the registry is populated by sibling gfx ops — op 0x1a2→FUN_0042cf70 is the hash insert), NOT the VM global bank → seeding story-state CANNOT reproduce it. Stubbed → constant return → every draw collapses to slot 0 → anchor-preserve reads foreign-sized textures → the cumulative bg/sprite drift. SETTLES the drift as (b) a genuine native op, NOT (a) state-divergence. Faithful fix = model the gfx command-buffer (record array + handle→object registry) and run the gfx ops instead of stubbing — static/Frida-free (handlers now readable; inserts are bytecode-driven). Full decode + verdict: docs/engine-re.md (op 0x215 section)."), 0x217: dict(name='gfx-geom?', category='draw', noop=False, confidence='low', source='inference', summary='4 global-ints; part of a 0x217/0x218/0x21a geometry chain'), 0x218: dict(name='gfx-geom?', category='draw', noop=False, confidence='low', source='inference', summary='4 global-ints; chains with 0x21a/0x217'), 0x21a: dict(name='gfx-geom?', category='draw', noop=False, confidence='low', source='inference', summary='4 global-ints; chains with 0x218/0x217'), diff --git a/tools/frida/README.md b/tools/frida/README.md index 3da5d14..60beb0f 100644 --- a/tools/frida/README.md +++ b/tools/frida/README.md @@ -28,7 +28,8 @@ Prereq: `py -3.11 -m pip install frida` (core only — `frida-tools` CLI is not region (~30 MB, nonstable base). Attach → it enumerates ranges, dumps the module image + every r-x range ≥ 1 MB (chunked) → `build/engine-dump/{manifest.json,range_.bin}`, and prints the landmark bytes at `AGE.EXE+0x74f1f` to validate. Then disassemble (capstone) and locate a handler - (e.g. `0x215` @ Kelebek VA `0x421160`) via the opcode dispatch table. Attach by pid. + (e.g. `0x215` → real handler `0x42a0b0`, resolved via the opcode dispatch table — Kelebek's `0x421160` + is VA-drift, an unrelated fn). Attach by pid. - **`capture_load_order.py`** — ★ the working asset-resolution capture. Hooks `ReadFile` on `DATA2.ALF`; each asset load starts with header reads **at its exact archive offset**, so exact-start reads give the clean per-asset **load order** (→ names via `build/asset-index.json`). `--analyze` diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 8369add..7f4b1a6 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -5249,12 +5249,12 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "query-gfx-object?" category = "draw" -summary = "0x215 (out)(handle_id) — queries the native graphics-object manager by element handle-id (the value in 0x62455[idx], often +1/+2 for a sub-element); writes the object's slot/status into `out`, sign-tested (gre/lt 0) to drive label_12649's slot-select branch and set the working slot G[0x62452]. KEYSTONE for per-object slot selection — stubbing it collapses every draw onto slot 0, so the anchor-preserve geometry reads foreign-sized textures → cumulative bg/sprite drift (see docs/phase-a-slice-plan.md A2b-Geometry). Reads native object-manager state (NOT VM-computable). Exact return semantics: RE via unicorn (native handler @0x421160)." +summary = "0x215 (out)(handle_id) — native graphics command-buffer op. Real handler FUN_0042a0b0 (Ghidra-resolved via the dispatch table ctx[0x26c93+op]; Kelebek's 0x421160 is VA-drift, lands in an unrelated fn). Does TWO things: (1) writes cmd-type 5 into the CURRENT gfx-object record `[ctx+0x53d88 + ctx[0x53d14]*0x78]` (a command-buffer registration, parallel to op 0x1a2→type 3); (2) returns `out = map.find(handle_id)` over an engine-internal associative registry (found value, else 0xffffffff=not-found), sign-tested (gre/lt 0) to drive label_12649's slot-select branch + set working slot G[0x62452]. So `out` is NATIVE COMMAND-BUFFER STATE (the registry is populated by sibling gfx ops — op 0x1a2→FUN_0042cf70 is the hash insert), NOT the VM global bank → seeding story-state CANNOT reproduce it. Stubbed → constant return → every draw collapses to slot 0 → anchor-preserve reads foreign-sized textures → the cumulative bg/sprite drift. SETTLES the drift as (b) a genuine native op, NOT (a) state-divergence. Faithful fix = model the gfx command-buffer (record array + handle→object registry) and run the gfx ops instead of stubbing — static/Frida-free (handlers now readable; inserts are bytecode-driven). Full decode + verdict: docs/engine-re.md (op 0x215 section)." noop_headless = false source = "investigation" -confidence = "med" +confidence = "high" depends_on = [] -evidence = "SC0000 label_12649 (0x12670) + label_123ef (0x12419/0x12450): called with 0x62455[idx] handle-ids (±offset); result gre/lt 0 branches slot-select and feeds ui-elem?(0x1f7)/set-texture slot. Record table 0x3239 (label_125bd @0x0050f) assigns per-object slots 4..13. Handles are the 0xcf08/0xe678/0xd6d8 element-id family." +evidence = "Ghidra: real handler FUN_0042a0b0 = {*(ctx+0x53d88+ctx[0x53d14]*0x78)=5; out=FUN_0047f280(FUN_0041b940(2))}. FUN_0047f280 = std::map::find (returns mapped value or 0xffffffff); FUN_0041b940(2) = operand-fetch of operand 2 (the handle key); FUN_00425fb0(1,val) = operand-write to `out`. Registry populated by op 0x1a2 handler FUN_0042d360 → FUN_0042cf70 (open-addressing hash insert). Bytecode sites: SC0000 label_12649 (0x12670) + label_123ef (0x12419/0x12450), handle-ids from 0x62455[idx] (±offset); result gre/lt 0 branches slot-select. Record table 0x3239 (label_125bd @0x0050f) assigns per-object slots 4..13." [[opcode.semantics.args]] i = 1