From 983c7d0bb0eec8d00a5eaf02e5f2f01d43282667 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 21 Jul 2026 20:14:07 -0400 Subject: [PATCH] Correct native packed resource addressing docs --- docs/asset-resolution-re.md | 184 ++++++++++++++++-------------------- docs/engine-re.md | 41 ++++++-- docs/opcode-reference.md | 28 +++--- docs/phase-b-framework.md | 26 ++--- docs/tools-reference.md | 4 +- tools/resolve_asset.py | 17 ++-- vm-map/opcodes.toml | 34 +++---- 7 files changed, 168 insertions(+), 166 deletions(-) diff --git a/docs/asset-resolution-re.md b/docs/asset-resolution-re.md index 1a1c795..5f92ffb 100644 --- a/docs/asset-resolution-re.md +++ b/docs/asset-resolution-re.md @@ -54,58 +54,41 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b all 13206 `offset+size` fit inside their real `.ALF`; 837 name-matched files → 0 size mismatches. `files[]` preserves directory order (feeds step 2's order-correlation). Re-run: `py -3.11 -X utf8 tools/parse_sys4ini.py --check`. (Ref: asmodean's `exs4alf` / GARbro Eushully `ArcALF.cs`.) -2. **Resolve `resId → asset file`.** **✅ SOLVED for scene-manifest references (2026-07-06); - system/global raw ids are a separate path identified 2026-07-10.** +2. **Resolve `resource_id → asset file`.** **NATIVE RULE CONFIRMED IN GHIDRA (2026-07-21): resource + operands are universal packed SYS4INI/AAI ids. There is no scene-relative path or fallback.** - **The rule:** SYS4INI's file list is organized into **SECTIONS, one per scene** — each is a - `SCxxxx.BIN` script entry followed by that scene's **asset MANIFEST**: every asset it references, - across *all* archives and types (EV/BG/CS/AE graphics **and** OGG/WAV audio), interleaved in usage - order. `file_number` is the **0-based index within the section**. So: + `asset_catalog_parse_base_tables@0x44e7e0` creates one flat base entry array in serialized SYS4INI + order. `asset_open_indexed_entry@0x44f390` receives the operand unchanged. If its high byte is zero, + native bounds-checks and directly indexes that base array. Otherwise the signed high byte selects a + mounted AAI catalog and the low 24 bits index only that catalog. The selected record then follows exact + loose-basename-first, bounded-ALF-second opening. The opener receives no executing-script identity, + section base, or active manifest and contains no fallback branch. - > **`resId → files[ section_base(scene) + resId ]`**, where `section_base` = the start of the SYS4INI - > section containing the scene's `SCxxxx.BIN`. + This one contract is shared by script loading, `set-texture` (`0x1f9`), mode-1 texture load (`0x249`), + voice, SFX, cursor, and both movie paths. Type-specific facades should filter the selected packed record + after lookup, not reinterpret the numeric id. `play-bgm` remains the separate direct-name exception: + `BGM{id:03d}.OGG`. - The manifest rule holds first for `set-texture(resId)` and `play-voice(id)`. Non-SC frontend scripts - such as ROOM instead carry universal raw catalog ids, so both typed resolvers fall back to raw lookup - when the executing script has no SC section. **⚠ `play-bgm` is the EXCEPTION — - it does NOT use the manifest; it uses direct literal names `BGM{id:03d}.OGG` (see step 4, by-ear - corrected 2026-07-06).** **Tool:** `tools/resolve_asset.py --build` → `build/asset-sections.json` - (359 sections, 136 scenes); `resolve_asset.py [resId]` resolves. **Validated:** `file_number == - position − section_base` for 12848/13206 files (97%); SC0000 resolves 17/17 across archives vs the Frida - capture (`0x25→EV052CA`, `0x36→BG030A` background, `0x6c→EM* effect`); 586/595 distinct captured loads - (all sections) satisfy `files[base+fn]==name`. This is the derivable rule that generalizes to any - AGE game with the same container — **the "scope" was just which SYS4INI section the scene lives in.** - (The old `play-bgm 5→BGM006` validation point was a mis-attribution — the real game plays BGM005.) + SC0010 provides the decisive static check that SC0000 could not because SC0000 begins at catalog zero. + SC0010 bytecode executes `set-texture 0x21`; raw entry `0x21` is `SO013A.AGF`, while adding SC0010's + catalog position `0x11e` lands on unrelated `COL0023.OGG`. Its first voices are `0x120`, `0x121`, and + `0x122`, which directly name raw `LILA1414.OGG`, `LILB0053.OGG`, and `LILC0054.OGG`. Their + `file_number` values 2/3/4 describe grouping inside the SC0010 run, but native bytecode already contains + the absolute indexes `0x11e + file_number`. - **System/global-id exception (identified 2026-07-10; implemented).** Some SYSTEM4 loads use - the SYS4INI record's universal `raw_index` directly, including the two `@` placeholder records, rather - than a scene-local manifest index. `SYSTEM4.BIN` writes `G[0x69b]=0x337e`, then - `set-texture(G[0x69b], slot=0x11)`. SYS4INI `raw_index 0x337e` is `DATA1/SO001.AGF`, the shared - 800×300 RGBA system-chrome sheet. `ResourceMap.ResolveTexture` therefore tries the active script's - scene manifest first and then the distinct universal raw-id lookup; the scene-manifest rule above - remains correct for ordinary SC texture/voice ids. ROOM voice `0x3365`, for example, resolves as raw - `EUA0016.OGG`; treating it only as a ROOM-local id produces no asset because ROOM owns no SC range. + The former `files[section_base(scene)+resId]` model inverted that relationship. The 586/595 Frida + correlation and the strong `file_number == position - group_start` pattern remain useful evidence about + catalog construction/order, but they do not describe runtime resolution. SC0000 starting at zero hid + the mistake, while later large ids often fell outside the invented scene range and happened to reach the + port's raw fallback. Low raw ids used from later scripts can instead be silently misresolved today. + `tools/resolve_asset.py` and `build/asset-sections.json` are therefore correlation/manifest-inventory + diagnostics only; they must not drive runtime lookup. - **Explicit raw texture loader (identified and implemented 2026-07-21).** Opcode `0x249` is the - unambiguous packed raw-id texture path even while a scene section is active. It shares `0x1f9`'s surface - replacement, AGF decode, and RGB colorkey contract, but passes native surface mode 1 and does not apply - the executing frame's section base. FIELD uses `0x32da..0x32dd`, the universal SYS4INI indexes for - `SO005.AGF`, `SO007.AGF`, `SO008A.AGF`, and `SO007A.AGF`, to populate map-sheet surfaces `0x3e..0x41`. - The port therefore forwards those ids directly to `ResolveRawTexture`; it must not run them through - `ResolveTextureResourceId` first. Native mode 1 is a large-image wrapper which tiles the same decoded - logical pixels over ordinary child textures; it is not a different AGF/spritesheet interpretation or - blend rule. The port's contiguous CPU image is therefore equivalent for rendering purposes. - - *How we got here (condensed):* first confirmed `resId == file_number` via Frida load-order correlation - for SC0000's opening, but `file_number` is not globally unique so a per-scene "scope" was needed. A long - hunt for the selector (thought it was native scene state; even tried reading `G[0x62424]` live — the - VM global memory is structured/packed, see `docs/global-memory-re.md`) missed the real structure until a - **full multi-archive capture** (user domain tip: DATA1 holds BG/CS/CB/CA/CP graphics by name prefix, not - just DATA2 EV CGs) revealed `file_number == SYS4INI position` inside per-scene sections. Superseded tools: - `tools/correlate_scope.py`, `vm0.py --settex` (VM set-texture trace; still useful, but vm0 diverges on - branchy non-opening scenes — use the C# VM to trace those). Runtime note for future work: the game is - **packed** (main VM logic in a per-run heap `r-x` region) and streams archives through a heap block-cache - via `ReadFile` (not mmap); the stable AGF decoder is `AGE.EXE+0x74f1f`. + `0x1f9` and `0x249` also do not represent scene-local versus raw addressing. Both pass the same packed + operand unchanged to the same opener and use the same colorkey/load path. Their native distinction is + surface mode: `0x1f9` creates ordinary mode 0, while `0x249` creates the tiled large-image mode 1 wrapper. + FIELD's `0x32da..0x32dd` map sheets and SYSTEM4's `0x337e` SO001 are ordinary examples of the universal + base indexes used throughout the corpus, not special fallback cases. 3. **Wire the backend.** **✅ FIRST-PASS RENDER LANDED (2026-07-06).** `Age.Engine/Sys4/ResourceMap.cs` (Resolve + BMP path) + `GodotAdvHost` texture ops → `TextureRect` compositing behind the dialogue; `IHost.DrawTexture` extended with dst x/y; 800×600 window; `convert_agf.py --scene` pre-converts a @@ -125,25 +108,22 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b still injects the same known inherited layout and surface state because it intentionally bypasses SYSTEM4; that shortcut is no longer the shipped/default route. - The active manifest is frame-local, not fixed to the root scene: every VM call frame brackets host work - with its script context. `set-texture` resolves the local id at load time and retains the normalized raw - catalog id in the graphics surface, so that surface remains stable after a nested helper returns or a - sibling script becomes active. Voice and scene-movie op `0x236` likewise resolve against the executing - frame; modal whole-movie op `0x20f` and SFX are separate universal raw/packed-id families documented below. + Resource lookup is independent of the active VM frame. Every texture surface retains the packed catalog + id supplied by bytecode, and nested helpers/sibling scripts use the same global base/append catalogs. + Script-context bracketing remains useful for diagnostics/page location, but it must not alter texture, + voice, cursor, SFX, or movie resolution. 4. **Audio.** **BGM/voice/SFX wired; VFS bytes complete (2026-07-11); packed-raw SFX corrected 2026-07-20.** `IHost.PlayBgm/PlayVoice` + VM dispatch (`play-bgm` 0xbf / `play-voice` 0xc4, both argc 1); `ResourceMap.ReadAudio` opens the resolved catalog entry through `IAssetStore`; `GodotAdvHost` passes the bytes to `Main`'s players (`AudioStreamOggVorbis.LoadFromBuffer`; BGM loops, voice interrupt-on-new). Non-Godot hosts no-op it → `--selftest`/8-8 byte-identical. SC0000 fires 18 BGM + 198 voice. **By-ear VALIDATED (2026-07-06):** voices play on their lines (`play-voice` med→HIGH). **BUT the two audio ops use DIFFERENT - addressing — the earlier "unified graphics+audio manifest" claim was WRONG for BGM:** - - **Voice** (`play-voice`) → SC-section manifest `files[base+id]` at **offset 0**, with a type-checked - universal-raw fallback for frontend scripts without an SC section (same as textures). Proven: - the manifest interleaves graphics/voice (`files[35]=EV049AA`, `[36]=MAN999`, `[37]=EV052CA`, `[38]=SYL0001`), - so `id-1` would land voices on `.AGF` (silent) — they play, so offset is exactly 0. - - **BGM** (`play-bgm`) → **DIRECT LITERAL NAME**, `id → BGM{id:03d}.OGG` (DATA3), NOT the manifest. - Confirmed by ear (`play-bgm 5→BGM005`, `8→BGM008`; the manifest gave BGM006/009 = off-by-one) and proven - by `play-bgm 0x23→BGM035.OGG` — a real standalone track (BGM set skips 030-034) the manifest mis-resolved + addressing:** + - **Voice** (`play-voice`) → the universal packed SYS4INI/AAI resource contract shared with textures + and movies. Native passes the bytecode operand unchanged to `asset_open_indexed_entry`. + - **BGM** (`play-bgm`) → **DIRECT LITERAL NAME**, `id → BGM{id:03d}.OGG` (DATA3), not the packed + resource table. Confirmed by ear (`play-bgm 5→BGM005`, `8→BGM008`) and proven + by `play-bgm 0x23→BGM035.OGG` — a real standalone track (BGM set skips 030-034) the old scene model mis-resolved to a graphics entry. Implemented as `ResourceMap.ResolveBgm(id)`; `GodotAdvHost.PlayBgm` uses it. The prior "Frida-confirmed play-bgm 5→BGM006" record was a mis-attribution. @@ -151,8 +131,8 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b packed catalog ids through the same byte store while retaining the existing channel lifecycle. See `docs/phase-a-slice-plan.md`. Diagnostic: `Age.Cli audio `. -5. **Movies** (`OP`/`MVB`, MPEG) — SC0000 `0x236` now resolves its scene-local id through the same catalog - and reads owned payload bytes through `IAssetStore`; see the movie section below. +5. **Movies** (`OP`/`MVB`, MPEG) — `0x236` and `0x20f` receive universal packed resource ids and read the + selected payload through `IAssetStore`; see the movie sections below. ## Validation reality (why this is the big haul) @@ -164,25 +144,24 @@ rendering what the executed bytecode + the map produce (never a hardcoded image) ## Status -A2b-background: **steps 1–3 landed.** Step 1 = `build/asset-index.json`. Step 2 = **`resId → -files[section_base(scene) + resId]`** via SYS4INI per-scene sections (`tools/resolve_asset.py` + -`build/asset-sections.json`) — no runtime capture, all archives/types plus SC-section voice and typed raw -fallback for non-SC frontend scripts. Step 3 = **first-pass +A2b-background: **steps 1–3 landed, but step 2's resolver must be corrected.** Step 1 = +`build/asset-index.json`. Step 2 originally normalized through inferred per-scene sections; native RE now +proves runtime operands are already universal packed ids. `tools/resolve_asset.py` and +`build/asset-sections.json` remain grouping/correlation diagnostics, not runtime inputs. Step 3 = **first-pass render** (ResourceMap + GodotAdvHost texture ops → TextureRect compositing): the full-screen event-CG layer renders end-to-end from the bytecode. Remaining (next chunk): the **graphics geometry/blend subsystem** — native geometry ops (`0x208` + sprite position/animation) so sprites/`BG*` position, plus -alpha/blend for fades + chromakey. See `docs/phase-a-slice-plan.md` (A2b). Audio (step 4): **`play-voice` -uses the SC manifest first** (`files[base+id]`) and universal raw lookup for non-SC frontend scripts; -**`play-bgm` uses direct names** (`BGM{id:03d}.OGG`) — NOT unified. +alpha/blend for fades + chromakey. See `docs/phase-a-slice-plan.md` (A2b). Audio (step 4): voice uses the +universal packed catalog; **`play-bgm` uses direct names** (`BGM{id:03d}.OGG`). ## Native SFX resource proof (2026-07-11; addressing corrected 2026-07-20) -SFX uses a universal packed catalog id, not the scene-local graphics/voice rule. A zero high byte directly +SFX uses the universal packed catalog rule shared by graphics/voice/movies. A zero high byte directly indexes SYS4INI; a nonzero high byte selects the matching AAI mount and uses the low 24-bit index. The matching native trace at SC0000 `0xc29` captures raw id `0x28`, channel 0, which is `DATA1/E0808.WAV`; SC0000 being the first section previously hid the distinction. TITLE makes it decisive: `0x2aea` is raw `SE020.WAV` for hover, `0x3321` is raw `SE015.WAV` for activation, and neither fits TITLE's -14-entry manifest. `play-bgm` remains the separate direct-name family. +14-entry inferred group. `play-bgm` remains the separate direct-name family. The Phase-A backend now resolves the OGG/WAV catalog entry and opens it through `IAssetStore`; Godot decodes the returned bytes into its existing BGM, voice, and fixed SC0000 SFX channel players. The earlier @@ -197,18 +176,18 @@ behavior: > resolve the resource record → try a loose file with that record's name in the game/mod root → otherwise > read exactly `offset..offset+size` from the record's ALF → decode the contained format in process. -Native evidence already proves this ordering for scripts: `resource_open_by_raw_id@0x44f390` indexes the +Native evidence proves this ordering for scripts: `asset_open_indexed_entry@0x44f390` indexes the 80-byte SYS4 record and calls `CreateFileA(record.name)` before opening `record.archive`, seeking to `record.offset`, and reading `record.size`. The same service is the correct common seam for scripts, -graphics, voice/SFX, and movie bytes. Resolution and opening must remain separate: scene-local ids and -universal `raw_index` ids select a record differently, but both records flow through the same loose-first -store. +graphics, voice/SFX, and movie bytes. Resolution and opening remain separate only by concern: the universal +packed id selects one record, then the store applies loose-first/archive-second opening. There is no +scene-local numeric addressing mode. ### Proposed layers 1. **Catalog + read-only ALF store (VFS-A DONE).** `Sys4AssetCatalog` parses SYS4INI at runtime while preserving all 13208 raw records - (including the two `@` placeholders), archive names, scene sections, and the existing three lookup modes: - universal raw id, scene-local manifest id, and direct name where the opcode family genuinely uses one. + (including the two `@` placeholders), archive names, diagnostic scene groupings, universal packed ids, + and direct-name lookup where an opcode family genuinely uses it. An ALF is a payload container at this layer: open the named archive and return a bounded stream/byte range at the indexed offset/size. Before that fallback, probe the configured loose override roots by the record's exact basename. `Sys4AssetStore` opens a separate read-only file handle per request and constrains archive @@ -235,18 +214,17 @@ store. on-disk `BinExtractALF.exe` are validation references; the Kelebek repository exposes no clear license, so its code should not be copied without clarification. The focused `LzssDecoder` is shared with `Sys4AssetCatalog`; raw and compressed information/pixel/ACIF sections use the same bounded primitive. -4. **Runtime consumers (complete for scripts, textures, and current audio families).** `ResourceMap.ResolveTexture` preserves - scene-local resolution and falls back to universal raw ids for SYSTEM4 assets; `GodotAdvHost` caches - decoded RGBA surfaces by catalog identity and supplies synchronous dimensions to opcode `0x208`. - Godot no longer reads `build/textures/*.BMP`. BGM direct-name, scene-first/raw-fallback voice, and packed-raw SFX - entries are opened through the same `IAssetStore`. `ResourceMap.ResolveSoundEffect` owns op `0xb4`'s - `ResolvePacked` lookup and audio filtering; the existing Godot WAV/channel path consumes its result. - Extraction and conversion tools remain diagnostics. +4. **Runtime consumers (packed-id correction pending).** Script loading and SFX already use + `ResolvePacked`. Texture/voice/non-modal movie facades still contain the disproven scene-first/raw-fallback + compatibility layer and must be switched to the same typed packed lookup. Godot caches decoded RGBA + surfaces by catalog identity and supplies synchronous dimensions to opcode `0x208`; it no longer reads + `build/textures/*.BMP`. BGM remains direct-name. Extraction, grouping, and conversion tools remain + diagnostics. ### Acceptance gates -- Catalog: 13208 raw slots / 13206 real base entries; every ALF range is in bounds; scene-local mappings - remain identical to the current resolver and `raw_index 0x337e` resolves to `SO001.AGF`. +- Catalog: 13208 raw slots / 13206 real base entries; every ALF range is in bounds; packed base/append + selection matches native and `raw_index 0x337e` resolves to `SO001.AGF`. - Store: representative base reads are byte-identical to `extracted/`; a temporary loose file with the same record name wins, and removing it deterministically reveals the archive bytes. Root path traversal is rejected and archive reads are bounded/thread-safe. @@ -298,7 +276,7 @@ dimensions, hotspot, transparent background, grayscale, and a non-gray palette p ### SC0000 movie payload and presentation (2026-07-11) -The scene-local implementation was first validated at SC0000 `0x236@0x13c8`. Resource `0x33` resolves through the +The first implementation was validated at SC0000 `0x236@0x13c8`. Universal base-catalog id `0x33` resolves through the authoritative catalog to `DATA1.ALF:CHAPTER.AGF` (archive offset 3,908,816; size 8,194,052). Despite the `.AGF` name, its payload begins with MPEG program-stream pack start code `00 00 01 BA`; the installed asset is MPEG-1 program stream video at 800x600, 29.97 fps, approximately 11.98 seconds, with video stream `E0` @@ -328,8 +306,8 @@ that test/bootstrap path is unrelated to movie asset loading and was not folded ### BTL combat-effect movie resolution and decoder boundary (diagnosed 2026-07-21) -BTL's movie call at `BTL.BIN@0x2b21` is the same non-modal opcode `0x236`, but its table supplies universal -raw SYS4INI ids because `BTL.BIN` has no `SC####` manifest section. The first accepted combat run reached +BTL's movie call at `BTL.BIN@0x2b21` is the same non-modal opcode `0x236` and, like every native resource +consumer, its table supplies universal packed SYS4INI ids. The first accepted combat run reached five such ids: | Raw id | Catalog asset | MPEG size | Sequence size | @@ -341,10 +319,10 @@ five such ids: | `0x2bca` | `MVB914.AGF` | 133,124 bytes | 400x400 | All five archive payloads begin with MPEG program-stream pack code `00 00 01 BA`; none is a corrupt still -AGF. `GodotAdvHost.PlayMovieToSurface` currently calls only `ResourceMap.Resolve(BTL,id)`, which must return -null because the catalog builds local ranges only for `SC####.BIN`. Texture and voice resolution already -use the required ordering: active SC manifest first, then a type-checked universal raw fallback. Non-modal -movie resolution needs the same typed ordering. The logged `AGF decode failed MVB914.AGF` is downstream: +AGF. `GodotAdvHost.PlayMovieToSurface` currently calls the port's scene-relative `ResourceMap.Resolve`, +which returns null because BTL owns no inferred SC group. Native instead passes the id unchanged to the +universal packed opener. Texture and voice facades require the same correction, not a new movie-only +fallback. The logged `AGF decode failed MVB914.AGF` is downstream: after movie lookup fails, no movie identity is registered, the VM still binds the raw id to the surface, and the compositor falls through to the still-image decoder because MPEG movies also use `.AGF` names. @@ -357,8 +335,8 @@ are 280x352. Requesting RGB24 instead of RGB32 does not change negotiation. The failure to DirectShow/filter compatibility with these non-16-aligned MPEG display widths; it does not prove which internal stock filter imposes the restriction. -The correct implementation boundary therefore has three parts: add a typed scene-first/raw-fallback -resolver for non-modal movies; introduce the already-planned decoder interface/factory and a software MPEG +The correct implementation boundary therefore has three parts: switch all ordinary typed resource +consumers to the native universal packed lookup; introduce the already-planned decoder interface/factory and a software MPEG fallback that handles the installed non-16-aligned effects; and preserve the destination surface's created dimensions instead of replacing every movie surface with the SC0000-specific 800x600 value. Decoder failure should also leave a completed movie identity long enough to suppress bogus still-AGF fallback. Regressions @@ -367,17 +345,15 @@ must cover at least raw BTL resolution, a 280x352 effect (`MVB001`), a 400x400 e ### Modal startup/ending movie resources (implemented 2026-07-20) -Opcode `0x20f` uses universal raw SYS4INI indexes rather than the executing script's manifest. Its complete +Opcode `0x20f` uses the same universal packed resource contract as `0x236`. Its complete corpus is `LOGO.BIN (0x335f,42,4)`, `OP.BIN (0x3364,42,4)`, and -`ED.BIN (0x3324,42,dynamic_flags)`. Raw records `0x335f`, `0x3364`, and `0x3324` are respectively -`LOGO.AGF`, `OP.AGF`, and `ED.AGF`; all begin with MPEG pack code `00 00 01 BA`. The current -`ResourceMap.Resolve(scene,id)` path correctly returns out-of-range for the large LOGO/OP values, proving -that an implementation must expose a typed raw-movie resolver rather than add manifest fallbacks globally. +`ED.BIN (0x3324,42,dynamic_flags)`. Base records `0x335f`, `0x3364`, and `0x3324` are respectively +`LOGO.AGF`, `OP.AGF`, and `ED.AGF`; all begin with MPEG pack code `00 00 01 BA`. Native `0x20f` also arms modal run-state `0x2000`; unlike `0x236`, these six-instruction wrapper scripts depend on the movie service itself to park until EOF/input cancellation before they release surface 42. -`ResourceMap.ResolveRawMovie` now supplies that typed universal lookup, while `ReadMovie` remains the MPEG -signature gate. `IHost.PlayModalMovieToSurface` is distinct from the scene-local non-modal call: Godot +`ResourceMap.ResolveRawMovie` currently supplies that typed universal lookup, while `ReadMovie` remains the MPEG +signature gate. `IHost.PlayModalMovieToSurface` is distinct from the non-modal call for lifecycle only: Godot reuses the asynchronous DirectShow frame decoder and retained compositor but parks the VM thread until EOF or mouse/Accept/Cancel input. The wrapper's following release then tears down the completed/cancelled movie. The decoder still intentionally leaves audio unrendered; OP/ED audio parity needs an explicit synchronized @@ -394,8 +370,8 @@ through `0x01xxxxxx`; direct base-name lookup deliberately does not see append r ### Deliberate non-goals - Writing/repacking ALF or AAI; loose overrides already provide the native mod/translation workflow. -- AGF encoding, movie audio, or generalized video APIs. Implemented `0x236` and `0x20f` playback remains - deliberately limited to Windows' native DirectShow MPEG decoder. +- AGF encoding or movie audio. A decoder interface/software MPEG fallback is now required for the installed + non-16-aligned MVB effects; synchronized audio remains a separate slice. - A generalized multi-mod dependency manager. Start with native game-root loose overrides; configurable ordered mod roots can be layered onto the same store later. - Removing the extraction/conversion tools immediately. They remain independent parity oracles until the diff --git a/docs/engine-re.md b/docs/engine-re.md index ab4d447..5ace62a 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -339,10 +339,11 @@ SYS4INI indexes `0x335f`/`LOGO.AGF` and `0x3364`/`OP.AGF`. `ED.BIN` is the only non-modal scene-movie op `0x236`, then starts playback and sets run-state bit `0x2000`; the engine main loop and window procedure treat that state as the modal whole-movie service. This parks the script at the opcode until EOF or input cancellation, after which the script's following instructions release the object/surface. -The port implements `0x20f` through a typed raw-movie resolver and a distinct modal host call. The existing +The port implements `0x20f` through a typed packed-movie resolver and a distinct modal host call. The existing asynchronous decoder publishes frames through the retained surface while only the VM thread is parked; EOF, mouse click, Accept, or Cancel resumes the wrapper so its scripted cleanup releases surface 42. -`0x236` retains its scene-local, non-modal contract. Both paths still intentionally leave the MPEG audio +`0x236` retains its non-modal contract; native passes its packed resource operand through the same universal +catalog opener as `0x20f`. Both paths still intentionally leave the MPEG audio pin unrendered; synchronized movie audio remains a deliberate backend/audio-clock slice. An existing native operand trace identifies every observed heap codebase by a 100% match against its static @@ -1765,7 +1766,7 @@ The semantic text/index records remain untouched, so reopening History rebuilds cannot leave the old labels above the resumed ADV page. Stored voice replay now follows the native split. Ordinary op `0xc4` and History op `0x1bd` share the -section-manifest resolver, Skip replacement queue, and Auto voice-pending state, but carry variants 0 and 1 +packed-resource resolver, Skip replacement queue, and Auto voice-pending state, but carry variants 0 and 1 respectively through `IHost.PlayVoice`. Both append their pair to the backlog when recording is enabled; History's surrounding `0x1bb(0)` suppression prevents the replay from recording itself. Native `voice_play_indexed_asset@0x488330` stores that variant in the channel-12 sound-buffer state before starting @@ -2100,6 +2101,28 @@ replacement. The selector extraction is an arithmetic `SAR 24`, so ids whose hig index before the mount table rather than slots `0x80..0xff`; the port rejects those selectors instead of inventing unsigned behavior. `/v2` names/comments this mount/load/open chain and is saved. +**Universal packed resource addressing confirmed (2026-07-21).** `asset_catalog_parse_base_tables@0x44e7e0` +parses one entry count and copies one flat array of 0x50-byte SYS4INI records in serialized order. +`asset_open_indexed_entry@0x44f390` directly bounds-checks a high-byte-zero operand against that count and +indexes `entries[id]`; a nonzero high byte takes the AAI branch described above. It receives neither +`EngineCtx` nor a script-frame/scene identifier and contains no section-base calculation or fallback. + +The callers pass their operands unchanged: `script_frame_load_resource@0x40e980`, +`gfx_op_0x1f9_load_surface@0x422360`, `op_0x249_load_raw_texture_surface@0x424b20`, +`voice_play_indexed_asset@0x488330`, SFX/cursor services, and +`movie_to_texture_open_asset_graph@0x463e20`. For movies, `op_0x236_play_movie_to_surface@0x423ee0` +fetches operand 1 and immediately forwards it to that helper. Thus scene-local and raw-fallback are not +native modes: ordinary resource operands are already universal packed SYS4INI/AAI ids. + +SC0010 supplies a clean corpus proof outside SC0000's base-zero coincidence. Its `set-texture 0x21` must +open raw entry `0x21` (`SO013A.AGF`); adding SC0010's catalog position `0x11e` instead selects unrelated +`COL0023.OGG`. Its `play-voice 0x120/0x121/0x122` operands directly select +`LILA1414/LILB0053/LILC0054.OGG`, whose SC0010-local file numbers are 2/3/4. The catalog grouping relation +is therefore `absolute id = group start + file_number`; the compiler has already performed that addition. +The port's scene-first compatibility resolver inverted this relationship and must be replaced by typed +`ResolvePacked` lookup for texture, voice, and movie consumers. Ghidra `/v2` renames the catalog parser and +entry-name helper, corrects the opener/caller comments, and is saved. + --- ### ADV text line spacing -- opcode `0x8b` (2026-07-19) @@ -2288,12 +2311,12 @@ retained objects when `0x20c` targets an offscreen surface, composites the captu source on the shared frame clock, blocks the VM through the endpoint, and only then permits the following surface release/root reload. Headless hosts retain their non-rendering no-op policy. -The same manual pass exposed a separate resource-addressing issue after `0x6c` was fixed: ROOM did execute -`play-voice`, but `GodotAdvHost` attempted only SC-section resolution. ROOM is a frontend script without an -SC section, and its voice operands `0x3365..0x3376` are universal raw SYS4INI indices (for example raw -`0x3365` is `EUA0016.OGG`). Voice resolution now matches the already-required texture rule: try the active -SC section first, then a type-checked raw-catalog fallback. SC0000's local `0x24 -> MAN999.OGG` mapping is -unchanged. +The same manual pass exposed a resource-addressing issue after `0x6c` was fixed: ROOM did execute +`play-voice`, but `GodotAdvHost` attempted only inferred SC-section resolution. ROOM's voice operands +`0x3365..0x3376` are universal packed SYS4INI indices (for example `0x3365` is `EUA0016.OGG`). The first +compatibility fix tried an SC section and then a type-checked base-catalog fallback. Subsequent native RE +proved there is no first stage: ROOM and SC scripts alike pass an already absolute packed id directly to +the catalog opener. SC0000's `0x24 -> MAN999.OGG` works because that scene's inferred group begins at zero. --- diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index 7ae01de..173e732 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -159,9 +159,9 @@ - **evidence:** Ghidra op 0xb6 handler 0x420250 -> sfx_channel_release@0x482600 -> sound_buffer_destroy@0x4831a0, which releases the per-channel object under its critical section and clears the slot. Native trace captured SC0000's channels 0..9 release sweep in consecutive calls. ### 0xbf `play-bgm` (play-bgm, argc 1) -- **summary:** Play background music by id. BGM is addressed by DIRECT LITERAL NAME: id -> BGM{id:03d}.OGG (in DATA3), NOT the per-scene section manifest (that's voices/textures). E.g. play-bgm 5 -> BGM005. +- **summary:** Play background music by id. BGM is addressed by DIRECT LITERAL NAME: id -> BGM{id:03d}.OGG (in DATA3), unlike ordinary resource operands, which are universal packed SYS4INI/AAI ids. E.g. play-bgm 5 -> BGM005. - **grounding:** source=investigation, confidence=high -- **evidence:** By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the manifest = off-by-one). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (BGM set skips 030-034) that the manifest mis-resolved to a graphics entry (EV049AA.AGF). CORRECTS the earlier 'unified manifest / Frida BGM006' claim, which was wrong by one. Voices/textures still use the manifest (files[base+id], offset 0). Diagnostic: `Age.Cli audio SC0000.BIN`. +- **evidence:** By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the disproven scene-section model). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (the BGM set skips 030-034). Ghidra /v2 op_0xbf_handler@0x420390 forwards the numeric track to the BGM facade rather than asset_open_indexed_entry. Diagnostic: `Age.Cli audio SC0000.BIN`. ### 0xc2 `fade-bgm` (u0041D2B0, argc 2) - **summary:** (target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Target 0 releases the current BGM source at completion. @@ -169,9 +169,9 @@ - **evidence:** Ghidra op 0xc2 handler 0x4204c0 sets run-state 0x200, arms the service timer, and calls bgm_fade_arm@0x464830; bgm_fade_tick@0x464960 interpolates current/target percent and applies volume, releasing at target 0. Native SC0000 trace at 0x7c1/0x126c shows target 0, duration 3000, 1% ticks at about 30 ms. ### 0xc4 `play-voice` (play-voice, argc 1) -- **summary:** Play a voice clip by id with native playback/history variant 0. SC scripts resolve through their SYS4INI section manifest; frontend scripts without an SC section use the id as a universal raw-catalog index. While all-message Skip is active, retain/replace the queued voice request instead of starting it; playback resumes from the latest queued request after Skip clears. Same scene-first/raw-fallback resolver family as set-texture (NOT play-bgm, which is direct-name BGM{id:03d}). +- **summary:** Play a voice clip by universal packed SYS4INI/AAI id with native playback/history variant 0. While all-message Skip is active, retain/replace the queued voice request instead of starting it; playback resumes from the latest queued request after Skip clears. Uses the same packed-id opener as textures/scripts/movies, unlike play-bgm's direct BGM{id:03d} naming. - **grounding:** source=investigation, confidence=high -- **evidence:** By-ear confirmed (2026-07-06): SC0000 prologue voices play on their lines via Godot AudioStreamPlayer. Off-by-one disproven structurally: manifest interleaves graphics/voice (files[35]=EV049AA, [36]=MAN999, [37]=EV052CA, [38]=SYL0001), so files[base+id] lands voices on OGGs while files[base+id-1] would land them on .AGF graphics (silent) -- and they play, so the SC-section offset is exactly 0. ROOM supplies raw ids 0x3365..0x3376 from a non-SC frontend script; raw 0x3365 is EUA0016.OGG. A scene-only resolver returned null there despite the opcode executing, while the established texture path already required the same raw fallback for frontend resources. Lily's lines are correctly form-gated (G[0xa57/0xa58/0xa59]) and stay silent when no form flag is seeded -- not a bug. Ghidra /v2 op_0xc4_handler@0x420610: when run_state_flags bit 0x08000000 is clear it calls voice_play_indexed_asset@0x488330 with variant 0 and records pair {id,0}; while Skip is active it stores the latest id/zero variant at ctx+0x6dbf4/+0x6dbf8. adv_interpreter_tick starts and clears that deferred voice when Skip/read-skip input is no longer active. +- **evidence:** By-ear confirmed (2026-07-06): SC0000 prologue voices play on their lines via Godot AudioStreamPlayer. Ghidra /v2 op_0xc4_handler@0x420610 passes operand 1 unchanged to voice_play_indexed_asset@0x488330, which passes it unchanged to asset_open_indexed_entry@0x44f390. That opener directly indexes the flat base table for a zero high byte or selects mounted_aai[high_byte] and low24 for append resources; it has no executing-scene input or section-base branch. SC0010 is decisive beyond base-zero SC0000: play-voice 0x120 is raw SYS4INI entry LILA1414.OGG, also SC0010 file_number 2; applying SC0010 base 0x11e again is wrong. ROOM's 0x3365 similarly resolves directly to EUA0016.OGG. While Skip is active the handler stores the latest id/zero variant at ctx+0x6dbf4/+0x6dbf8; adv_interpreter_tick starts and clears it when Skip/read-skip input ends. Lily's lines remain correctly form-gated. ### 0x1bd `play-history-voice` (u0041D910, argc 1) - **summary:** Replay a voice id selected from retained ADV text history using native playback/history variant 1, preserving normal Skip and Auto-voice state behavior. @@ -525,9 +525,9 @@ The handler clears the map embedded at retained-gfx owner+0x408, resets its coun - **evidence:** SC0000 CG/UI-draw path disasm; slot/w/h roles read off the operands (400x30 text bars, etc.). ### 0x1f9 `set-texture` (set-texture, argc 3) -- **summary:** Load asset #resId into texture slot: (resId, slot, flag=-1). resId resolves via the SYS4INI per-scene section manifest: files[section_base(scene)+resId] (same rule for play-bgm/play-voice). See docs/asset-resolution-re.md. -- **grounding:** source=frida, confidence=high -- **evidence:** SC0000 Frida-confirmed 17/17 (0x25->EV052CA, 0x2e->EV052DB, 0x36->BG030A background); resolution rule validated on 586/595 captured loads. Traced in CG-load subroutine label_12649 as `set-texture G[0x62424] -1`. +- **summary:** Load a texture by universal packed SYS4INI/AAI id into a mode-0 surface slot: (resource_id, slot, colorkey). A zero high byte directly indexes the flat base catalog; a nonzero high byte selects an AAI mount and uses the low 24 bits. This is the same addressing contract as 0x249; only the native surface mode differs. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2 gfx_op_0x1f9_load_surface@0x422360 fetches operand 1 and passes it unchanged to asset_open_indexed_entry@0x44f390. asset_catalog_parse_base_tables@0x44e7e0 builds one flat entry array; the opener has no scene/context input and directly indexes that array for high-byte zero. SC0010 set-texture 0x21 proves the distinction outside SC0000: raw 0x21 is SO013A.AGF, while adding SC0010's 0x11e section position lands on unrelated COL0023.OGG. The earlier 586/595 Frida correlation established that SYS4INI file_number describes grouping/order, not runtime operand rebasing. ### 0x1fa `gfx-elem-release` (gfx-elem-release, argc 1) - **summary:** 0x1fa (surface_slot) — release the surface at ctx+0x52bd4[slot] (virtual free, then null) and call FUN_00474e40(slot). It releases a surface slot, not a retained object handle. SC0000 feeds it the slot returned by op 0x215 after op 0x1f7 erases the associated object group. @@ -615,11 +615,11 @@ For slots below 1000 the native worker obtains that surface's level-0 D3D textur This is a target-pixel operation, not retained-object teardown. It invokes IDirect3DDevice9::Clear with zero rectangles, flags D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER (3), color 0, depth 1.0, and stencil 0. The port tracks 0x20d target ownership and forwards the clear to the host; its retained compositor already rebuilds the backbuffer from black. ### 0x20f `play-modal-movie-to-surface` (play-modal-movie-to-surface, argc 3) -- **summary:** (raw_resource_id)(surface_slot)(movie_flags) - open a universal raw-catalog MPEG asset into an existing retained surface, start its native movie graph, and arm modal run-state bit 0x2000 so script execution remains parked until the movie completes or is skipped. This is the LOGO/OP/ED whole-movie path, distinct from non-modal scene movie op 0x236. +- **summary:** (packed_resource_id)(surface_slot)(movie_flags) - open a universal packed SYS4INI/AAI MPEG asset into an existing retained surface, start its native movie graph, and arm modal run-state bit 0x2000 so script execution remains parked until the movie completes or is skipped. This is the LOGO/OP/ED whole-movie path; op 0x236 uses the same resolver with non-modal lifecycle. - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra /v2: op_0x20f_play_modal_movie_to_surface@0x422e50 shares the movie-object allocation, DirectShow graph open, audio-route, and volume setup used by 0x236, then calls movie_start_modal_playback@0x463280, ORs EngineCtx+0xa0ce4 with 0x2000, and marks movie presentation dirty. The main loop and window procedure special-case run-state 0x2000. Corpus has exactly three sites: LOGO (0x335f,42,4), OP (0x3364,42,4), and ED (0x3324,42,dynamic flags). Those ids are universal raw SYS4INI indexes for MPEG-pack LOGO.AGF, OP.AGF, and ED.AGF; each script releases its surface only after 0x20f resumes. -Implemented through IHost.PlayModalMovieToSurface. ResourceMap.ResolveRawMovie deliberately bypasses scene manifests while retaining MPEG signature validation in ReadMovie. Godot reuses the asynchronous decoder/retained-surface compositor, parks only the VM thread until EOF, and treats mouse click or Accept/Cancel input as completion before wrapper cleanup releases the decoder. MPEG audio remains a separate backend/audio-clock contract. +Implemented through IHost.PlayModalMovieToSurface. Its operand uses the same native universal packed-id catalog contract as 0x236; the separate host call exists for modal wait/cancel lifecycle, not a different resolver. ResourceMap must retain MPEG signature validation in ReadMovie. Godot reuses the asynchronous decoder/retained-surface compositor, parks only the VM thread until EOF, and treats mouse click or Accept/Cancel input as completion before wrapper cleanup releases the decoder. MPEG audio remains a separate backend/audio-clock contract. ### 0x212 `set-gfx-field64` (set-gfx-field64, argc 2) - **summary:** 0x212 (obj_idx)(val) — handler gfx_op_0x212_set_field64 @0x4230c0: obj=[ctx+0x14d54 + obj_idx*4]; if obj: *(obj+0x64)=val. The generic instruction length is 5 dwords. See docs/engine-re.md gfx op-contract table. @@ -745,11 +745,11 @@ Implemented through IHost.PlayModalMovieToSurface. ResourceMap.ResolveRawMovie d - **evidence:** Ghidra handler 0x423da0 converts axis ints to floats -> worker 0x47f060. gfx_object_anim_interpolate@0x473ed0 consumes obj+0x228/+0x214/+0x244 on retained-gfx owner+0xb550 (EngineCtx+0x51b64) and matrix4_make_axis_angle@0x48b215. gfx_object_composite@0x47f650 calls one-shot transform first, cyclic animation second. ### 0x236 `play-movie-to-surface` (play-movie-to-surface, argc 4) -- **summary:** (resource_id)(surface_slot)(movie_flags)(sync_mask) - synchronously resolve/open an archive movie and construct its DirectShow graph, then start asynchronous frame delivery into the retained destination surface. Resource ids use the executing SC manifest when present and universal raw catalog ids in non-SC workers such as BTL. The opcode itself is non-blocking: the VM advances to the next instruction. SC0000's native site evaluates (0x33, 0, 2, 0) at 0x13c8 and resumes at bytecode 0x13d1; its later 0x21c service boundary yields until movie EOF before cleanup. +- **summary:** (packed_resource_id)(surface_slot)(movie_flags)(sync_mask) - synchronously open a universal packed SYS4INI/AAI movie and construct its DirectShow graph, then start asynchronous frame delivery into the retained destination surface. The opcode itself is non-blocking: the VM advances to the next instruction. SC0000's 0x33 and BTL's 0x2axx/0x2bxx MVB ids are already absolute base-catalog indexes. - **grounding:** source=investigation, confidence=high -- **evidence:** Native handler 0x423ee0 and helpers 0x463c50/0x463aa0/0x463e20/0x4625e0; SC0000 native operand capture; exact 0x13c8->0x13d1 trace; archive-only changing-frame decoder and Godot lifecycle tests. BTL's live 0x2b21 site supplies table-selected values 0x2af1/0x2af5/0x2bca/0x2bd8/0x2bde; those exact raw SYS4INI slots are MPEG-backed MVB001/MVB004/MVB914/MVB958/MVB955 rather than BTL-local manifest offsets. +- **evidence:** Ghidra /v2 op_0x236_play_movie_to_surface@0x423ee0 fetches operand 1 and passes it unchanged to movie_to_texture_open_asset_graph@0x463e20, which passes it unchanged to asset_open_indexed_entry@0x44f390. The opener directly indexes the flat base table or selected AAI table and has no scene input. SC0000 native operand capture and exact 0x13c8->0x13d1 trace prove nonblocking behavior. BTL's live 0x2b21 site supplies 0x2af1/0x2af5/0x2bca/0x2bd8/0x2bde, the exact base entries MVB001/MVB004/MVB914/MVB958/MVB955. -The handler requires an existing destination texture, allocates/reuses a 0x478-byte movie-to-texture object for the surface, opens operand 1 through the native indexed-asset reader, builds FilterGraph/IGraphBuilder/IMediaControl/IMediaPosition/IMediaEvent/IBasicAudio, and presents bottom-up RGB samples through the movie texture renderer. The port must resolve operand 1 through the active SC manifest first and then the typed universal raw movie catalog, matching non-SC BTL's raw MVB ids without weakening still-image/audio type gates. Operand 3 selects movie/sound routing policy: bits 0x10000/0x20000/0x40000/0x80000 force sound route 0/1/2/3, otherwise set:DependMovieSound is used; SC0000's low value 2 is retained as native movie mode state. Operand 4 is stored as the movie sync/device mask at object+0x42c; SC0000 passes 0. Static layer preparation after 0x236 does not terminate the retained movie; 0x21c services it through EOF and subsequent surface cleanup stops/detaches it. +The handler requires an existing destination texture, allocates/reuses a 0x478-byte movie-to-texture object for the surface, opens operand 1 through the native universal packed-id reader, builds FilterGraph/IGraphBuilder/IMediaControl/IMediaPosition/IMediaEvent/IBasicAudio, and presents bottom-up RGB samples through the movie texture renderer. Operand 3 selects movie/sound routing policy: bits 0x10000/0x20000/0x40000/0x80000 force sound route 0/1/2/3, otherwise set:DependMovieSound is used; SC0000's low value 2 is retained as native movie mode state. Operand 4 is the movie sync/device mask. Static layer preparation after 0x236 does not terminate the retained movie; 0x21c services it through EOF and subsequent surface cleanup stops/detaches it. The port should type-check the selected packed record as MPEG but must not add a scene base or scene-first fallback. ### 0x238 `set-anim-clock` (set-anim-clock, argc 1) - **summary:** (duration) — set the GLOBAL animation clock: native ctx+0x51b78=0 (elapsed), +0x51b7c=duration. The generic instruction length is 3 dwords. NON-BLOCKING: only configures; the render loop advances it and interpolates all animating objects. SC0000 opening @0x123bd/@0x13858. Handler 0x4240e0; Kelebek VA 0x422390 is drift. @@ -807,9 +807,9 @@ The setter get-or-creates the object and writes the complete operand. During ret - **evidence:** Ghidra handler 0x4182d0: if !(ctx+0x51b80 & 2), set retained-gfx owner+0xb55c (EngineCtx+0x51b70)=1 and zero owner+0xb564/+0xb568. gfx_object_apply_transform_channels treats force value 1 as immediate completion unless obj+0x2d0 bit 0 is set. SC0000 label_1235a calls it before present-frame. ### 0x249 `load-raw-texture-surface` (load-raw-texture-surface, argc 3) -- **summary:** Load an AGF by universal packed SYS4INI/AAI catalog id into a retained surface slot using native surface mode 1 and the same RGB colorkey contract as set-texture (0x1f9). +- **summary:** Load an AGF by universal packed SYS4INI/AAI catalog id into a retained surface slot using native surface mode 1 and the same packed-id/RGB-colorkey contract as set-texture (0x1f9). - **grounding:** source=investigation, confidence=high -- **evidence:** Ghidra /v2: op_0x249_load_raw_texture_surface@0x424b20 is instruction-length 7 and is contract-identical to gfx_op_0x1f9_load_surface through release, asset_open_indexed_entry, RGB colorkey conversion, load failure, and cleanup. Its mode-1 gfx_surface_mode1_ctor selects a tiled large-image wrapper: gfx_tiled_surface_create@0x432ff0 splits the logical dimensions into DAT_005b15b0-sized ordinary mode-0 child textures; gfx_tiled_surface_upload_agf@0x431a10 decodes and uploads each region; gfx_tiled_surface_blit@0x4316b0 subdivides a requested logical source rectangle across those tiles. It is not a spritesheet interpretation or alternate blend mode, so the port's contiguous CPU image is behaviorally equivalent. Corpus literals are universal raw indexes, including FIELD 0x32da..0x32dd -> SO005/SO007/SO008A/SO007A, and therefore bypass scene-section normalization. +- **evidence:** Ghidra /v2: op_0x249_load_raw_texture_surface@0x424b20 is instruction-length 7 and is contract-identical to gfx_op_0x1f9_load_surface through release, unchanged packed operand, asset_open_indexed_entry, RGB colorkey conversion, load failure, and cleanup. Its only relevant distinction is mode-1 gfx_surface_mode1_ctor, a tiled large-image wrapper: gfx_tiled_surface_create@0x432ff0 splits logical dimensions into ordinary mode-0 child textures; gfx_tiled_surface_upload_agf@0x431a10 decodes/uploads regions; gfx_tiled_surface_blit@0x4316b0 subdivides logical source rectangles. Corpus literals include FIELD 0x32da..0x32dd -> SO005/SO007/SO008A/SO007A. The former claim that only 0x249 bypasses scene normalization was wrong because native never performs scene normalization for 0x1f9 either. ### 0x24e `set-gfx-animation-service-flags` (set-gfx-animation-service-flags, argc 1) - **summary:** Replace the retained graphics animation-service flags with operand 1. BTL brackets combat presentation with values 1 and 0; GAMECLEAR uses 3 and 0. diff --git a/docs/phase-b-framework.md b/docs/phase-b-framework.md index ec448cb..1eae464 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -101,7 +101,7 @@ scene return plus boundary cleanup still belongs to B1 completion. **TITLE SFX packed-raw correction (2026-07-20).** Hover and activation callbacks were already executing their scripted `0xb5` starts. The load failed earlier because op `0xb4` uses universal packed SYS4INI/AAI ids, -while Godot treated them as active-script manifest ids. The new packed-raw resolver maps TITLE's +while Godot treated them as active-script manifest ids. The packed resolver maps TITLE's `0x2aea`/`SE020.WAV` hover, `0x3321`/`SE015.WAV` activation, and GAMESTART's `0x2aeb`/`SE013.WAV` cancel through the existing channel players. A synchronized TITLE→GAMESTART→TITLE trace records every load/start with its filename, and manual validation confirms they are audible; BGM remains unaffected. @@ -111,9 +111,9 @@ port did not lose an executable-side launcher. Its sole op `0x130` call returns that is one at context construction and cleared only when op `0x9` resets/reloads root script id zero. SYSTEM4 calls `LOGO.BIN` and `OP.BIN` only while that flag is nonzero. The former stubbed-zero output explained the direct jump to TITLE. LOGO and OP then use the modal movie op -`0x20f` with raw catalog movies `0x335f`/`LOGO.AGF` and `0x3364`/`OP.AGF`; existing `0x236` is the distinct -non-modal, scene-local movie-to-surface path. The VM now models the initial-root flag and clears it at the -op-`0x9` whole-stack root-reload boundary. Godot resolves a typed raw MPEG asset, reuses the asynchronous decoder +`0x20f` with packed catalog movies `0x335f`/`LOGO.AGF` and `0x3364`/`OP.AGF`; existing `0x236` is the distinct +non-modal movie-to-surface path and uses the same packed lookup. The VM now models the initial-root flag and clears +it at the op-`0x9` whole-stack root-reload boundary. Godot resolves a typed MPEG asset, reuses the asynchronous decoder and retained compositor, and parks the VM until EOF or mouse/Accept/Cancel input. Focused natural-boot tests prove `LOGO -> OP -> INIT -> TITLE` ordering and exact movie operands. MPEG audio remains explicitly deferred until the decoder abstraction has an engine-owned synchronized audio/volume contract. @@ -257,7 +257,8 @@ rechecked on the updated build. The first recheck confirmed the transition was v follow-up corrections: the decompiled alpha-step branch had initially been read backwards (`arg<=64` means step 16, not step 1), making argument 10 about 160 ms and argument 30 about 480 ms; and ROOM's raw voice ids were reaching `play-voice` but failing the port's SC-section-only lookup because ROOM owns no SC -section. Voice resolution now uses the same scene-first, type-checked raw fallback as frontend textures. +section. The initial compatibility fix added a raw fallback, but later native RE proved the broader rule: +voice and frontend texture operands are already universal packed catalog ids and never receive a scene base. **Natural Game Start diagnostic gate (2026-07-21; captured).** The opt-in `-StartupDiagnostics` route kept the persistent `SYSTEM4` root and native exit semantics and added no seeds, boot prefix, timing changes, or @@ -562,15 +563,18 @@ transparency. Manual DEBUGMAP acceptance reached player combat and exposed the n combat-effect movies do not play. The resolver/decoder diagnosis is canonical in `docs/asset-resolution-re.md`; fix that bounded movie path before proceeding to enemy-turn/end-turn breadth. -**Combat-effect movie gap diagnosed; implementation pending.** BTL's `0x236@0x2b21` consumes universal -raw MVB ids because BTL has no SC manifest, while the host currently permits only scene-local resolution. +**Combat-effect movie gap diagnosed; native resolver generalized; implementation pending.** BTL's +`0x236@0x2b21` consumes universal packed MVB ids, exactly as texture, voice, script-load, and modal-movie +consumers do. Native Ghidra analysis shows that none of those paths applies an SC section base or fallback; +the port's scene-first compatibility resolver is therefore generally wrong, not merely incomplete for BTL. That explains each `movie unresolved BTL:...` warning and the secondary attempt to decode MPEG-backed -`MVB914.AGF` as a still image. A raw-catalog decoder probe also found a separate backend wall: the current +`MVB914.AGF` as a still image. A packed-catalog decoder probe also found a separate backend wall: the current DirectShow graph handles `MVB914` (400x400) but rejects the reached 280x352 MVB001/MVB004/MVB955/MVB958 assets with `0x80040217`. Broader samples tie current compatibility to 16-aligned display widths, while 125 -installed MVB assets use 280x352. The next slice is therefore typed scene-first/raw movie resolution plus -the existing portable-decoder seam and a software fallback for these effects, with destination dimensions -and failed-movie identity preserved correctly. +installed MVB assets use 280x352. The next bounded slice is to replace typed texture, voice, and movie +consumers with universal packed resolution and regress SC0010's low texture/voice ids plus BTL effects. +The existing portable-decoder seam and a software fallback for 280x352 effects remain the following, +separate step, with destination dimensions and failed-movie identity preserved correctly. **Mutable-surface fill/blend regression corrected.** The first visual recheck exposed BUNKI's menu interior as transparent. SYSTEM4 creates 800x600 surface 3 and fills it opaque white through `0x20b`; the metadata-only diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 3c364de..0cf2889 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -84,7 +84,7 @@ frame sharing globals. `trace`/`audio`/`gfx` stay **provider-less** (call-script subsystem oracles. Test scenes are **synthesized** via `Age.Engine/Sys4/ScriptAssembler` (see [[testing-synthesize-dont-disable]]: synthesize test data, never disable a feature to keep a golden green). -The runtime SYS4 front-end is `Sys4AssetCatalog` (raw-id, scene-local, and name views), `IAssetStore` / +The runtime SYS4 front-end is `Sys4AssetCatalog` (universal packed-id and name views; scene groupings are diagnostic only), `IAssetStore` / `Sys4AssetStore` (exact-basename loose roots, then a bounded ALF range), and `Sys4ScriptProvider` (cached root/call-script parsing). Generated asset/callscript JSON remains a tooling and test oracle only. @@ -206,7 +206,7 @@ branching/state can shift page ordinals between runs. Resolve a reported page wi |---|---|---|---| | `tools/frida/capture_native_transforms.py` | Capture native `0x21f`/`0x223`/`0x234` worker operands, corrected integer base/anchor coordinates, all one-shot/cyclic retained fields, the one-shot 4×4 matrix, and the final post-cyclic 4×4 matrix. Optional handle filter; read-only. | `py -3.11 -u -X utf8 tools/frida/capture_native_transforms.py [secs] [pid|AGE.EXE] [--handle 0xHANDLE]` | running game → `build/native-transform-trace.jsonl` | | `parse_sys4ini.py` | Parse `SYS4INI.BIN` (S4IC422, LZSS-compressed) into the diagnostic JSON asset-index mirror — name ↔ archive ↔ offset ↔ size for all DATA*.ALF. Each real entry carries universal `raw_index`; the runtime parses SYS4INI itself, while these generated files remain tooling/test oracles. Also emits the `call-script → name` annotation map. | `parse_sys4ini.py [--check]` (`--check` validates vs `extracted/` + `.ALF` sizes) | `姫狩り…/SYS4INI.BIN` → `build/asset-index.json` + `build/callscript-names.json` | -| `resolve_asset.py` | ★ **The static asset resolver.** SYS4INI is sectioned (one per scene: `SCxxxx.BIN` + its cross-archive manifest; `file_number` = index within section). Resolves `resId → files[section_base(scene) + resId]` for graphics AND audio, no capture. | `resolve_asset.py --build` · `resolve_asset.py [resId]` | `build/asset-index.json` → `build/asset-sections.json`; resolves any (scene, resId) | +| `resolve_asset.py` | **Historical scene-group correlation diagnostic, not a runtime resolver.** Builds/queries the strong `file_number ≈ position − group_start` relationship that helped classify SYS4INI ordering. Native RE proves bytecode resources are already universal packed ids, so do not feed this tool's scene-relative result to runtime lookup. | `resolve_asset.py --build` · `resolve_asset.py [resId]` | `build/asset-index.json` → `build/asset-sections.json`; inspects inferred groups | | `resolve_frida_reads.py` | Rescue noisy Frida archive-read offsets → asset names via the index (per-archive range search; drops 0x20000 paging reads); recovers the per-scene asset load order. | `resolve_frida_reads.py [reads.log] [-o out.json]` | `build/frida-reads.log` + `build/asset-index.json` → `build/frida-asset-loads.json` | | `convert_agf.py` | Convert AGF stills to BMP via `AGF2BMP2AGF.exe` (searches all `extracted/DATA*`). `--scene` batch-converts a scene's whole SYS4INI manifest. Since VFS-C, output is a diagnostic pixel-parity oracle; the runtime decodes AGF bytes directly. | `convert_agf.py EV052CA.AGF …` · `convert_agf.py --scene SC0000` | `extracted/DATA*/*.AGF` → `build/textures/*.BMP` | diff --git a/tools/resolve_asset.py b/tools/resolve_asset.py index 1dc9956..9bdfc80 100644 --- a/tools/resolve_asset.py +++ b/tools/resolve_asset.py @@ -1,22 +1,21 @@ #!/usr/bin/env python3 -"""Static, general asset resolver: (scene, resId) -> asset file. Solves asset resolution -(docs/asset-resolution-re.md) with NO runtime capture. +"""Diagnostic SYS4INI scene-group correlation tool; NOT a native runtime resource resolver. -Mechanism (proven): SYS4INI's file list is organized into SECTIONS, one per scene -- each is a +SYS4INI's file list is organized into SECTIONS, one per scene -- each is a `SCxxxx.BIN` script entry followed by that scene's asset MANIFEST (all assets it references, across every archive and type: EV/BG/CS/AE event & sprite graphics, OGG/WAV audio, ...). `file_number` is -the 0-based index within the section. So a bytecode resId resolves as: +usually the 0-based index within that grouping. This tool explores the correlation: resId -> files[ section_base(scene) + resId ] -where section_base(scene) is the start of the SYS4INI section containing the scene's script. -This is the same rule for set-texture(resId), play-bgm(id), play-voice(id) -- one unified manifest. -Validated: 97% of files fit `fn == position - section_base`; SC0000's opening resolves 17/17 vs -Frida ground truth; 586/595 captured loads across all sections satisfy `files[base+fn] == name`. +That relationship explained 97% of file ordering and 586/595 captured load/name pairs, but Ghidra +native RE on 2026-07-21 proved bytecode operands are already universal packed SYS4INI/AAI ids. +asset_open_indexed_entry applies no section base. Do not use this tool's scene-relative output in +runtime code; see docs/asset-resolution-re.md. Usage: py -3.11 -X utf8 tools/resolve_asset.py --build # emit build/asset-sections.json - py -3.11 -X utf8 tools/resolve_asset.py [resId] # resolve one, or dump the manifest + py -3.11 -X utf8 tools/resolve_asset.py [resId] # inspect one correlation, or dump a group """ from __future__ import annotations import json diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 8f4a7df..7884480 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -1882,12 +1882,12 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "play-bgm" category = "audio" -summary = "Play background music by id. BGM is addressed by DIRECT LITERAL NAME: id -> BGM{id:03d}.OGG (in DATA3), NOT the per-scene section manifest (that's voices/textures). E.g. play-bgm 5 -> BGM005." +summary = "Play background music by id. BGM is addressed by DIRECT LITERAL NAME: id -> BGM{id:03d}.OGG (in DATA3), unlike ordinary resource operands, which are universal packed SYS4INI/AAI ids. E.g. play-bgm 5 -> BGM005." noop_headless = false source = "investigation" confidence = "high" depends_on = [] -evidence = "By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the manifest = off-by-one). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (BGM set skips 030-034) that the manifest mis-resolved to a graphics entry (EV049AA.AGF). CORRECTS the earlier 'unified manifest / Frida BGM006' claim, which was wrong by one. Voices/textures still use the manifest (files[base+id], offset 0). Diagnostic: `Age.Cli audio SC0000.BIN`." +evidence = "By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the disproven scene-section model). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (the BGM set skips 030-034). Ghidra /v2 op_0xbf_handler@0x420390 forwards the numeric track to the BGM facade rather than asset_open_indexed_entry. Diagnostic: `Age.Cli audio SC0000.BIN`." [[opcode.semantics.args]] i = 1 @@ -1950,16 +1950,16 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "play-voice" category = "audio" -summary = "Play a voice clip by id with native playback/history variant 0. SC scripts resolve through their SYS4INI section manifest; frontend scripts without an SC section use the id as a universal raw-catalog index. While all-message Skip is active, retain/replace the queued voice request instead of starting it; playback resumes from the latest queued request after Skip clears. Same scene-first/raw-fallback resolver family as set-texture (NOT play-bgm, which is direct-name BGM{id:03d})." +summary = "Play a voice clip by universal packed SYS4INI/AAI id with native playback/history variant 0. While all-message Skip is active, retain/replace the queued voice request instead of starting it; playback resumes from the latest queued request after Skip clears. Uses the same packed-id opener as textures/scripts/movies, unlike play-bgm's direct BGM{id:03d} naming." noop_headless = false source = "investigation" confidence = "high" depends_on = [] -evidence = "By-ear confirmed (2026-07-06): SC0000 prologue voices play on their lines via Godot AudioStreamPlayer. Off-by-one disproven structurally: manifest interleaves graphics/voice (files[35]=EV049AA, [36]=MAN999, [37]=EV052CA, [38]=SYL0001), so files[base+id] lands voices on OGGs while files[base+id-1] would land them on .AGF graphics (silent) -- and they play, so the SC-section offset is exactly 0. ROOM supplies raw ids 0x3365..0x3376 from a non-SC frontend script; raw 0x3365 is EUA0016.OGG. A scene-only resolver returned null there despite the opcode executing, while the established texture path already required the same raw fallback for frontend resources. Lily's lines are correctly form-gated (G[0xa57/0xa58/0xa59]) and stay silent when no form flag is seeded -- not a bug. Ghidra /v2 op_0xc4_handler@0x420610: when run_state_flags bit 0x08000000 is clear it calls voice_play_indexed_asset@0x488330 with variant 0 and records pair {id,0}; while Skip is active it stores the latest id/zero variant at ctx+0x6dbf4/+0x6dbf8. adv_interpreter_tick starts and clears that deferred voice when Skip/read-skip input is no longer active." +evidence = "By-ear confirmed (2026-07-06): SC0000 prologue voices play on their lines via Godot AudioStreamPlayer. Ghidra /v2 op_0xc4_handler@0x420610 passes operand 1 unchanged to voice_play_indexed_asset@0x488330, which passes it unchanged to asset_open_indexed_entry@0x44f390. That opener directly indexes the flat base table for a zero high byte or selects mounted_aai[high_byte] and low24 for append resources; it has no executing-scene input or section-base branch. SC0010 is decisive beyond base-zero SC0000: play-voice 0x120 is raw SYS4INI entry LILA1414.OGG, also SC0010 file_number 2; applying SC0010 base 0x11e again is wrong. ROOM's 0x3365 similarly resolves directly to EUA0016.OGG. While Skip is active the handler stores the latest id/zero variant at ctx+0x6dbf4/+0x6dbf8; adv_interpreter_tick starts and clears it when Skip/read-skip input ends. Lily's lines remain correctly form-gated." [[opcode.semantics.args]] i = 1 -role = "voice id (SC-section index or frontend raw-catalog index)" +role = "universal packed SYS4INI/AAI voice id" observed_types = ["imm", "l-int", "l-ptr"] [[opcode]] @@ -4593,16 +4593,16 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "set-texture" category = "draw" -summary = "Load asset #resId into texture slot: (resId, slot, flag=-1). resId resolves via the SYS4INI per-scene section manifest: files[section_base(scene)+resId] (same rule for play-bgm/play-voice). See docs/asset-resolution-re.md." +summary = "Load a texture by universal packed SYS4INI/AAI id into a mode-0 surface slot: (resource_id, slot, colorkey). A zero high byte directly indexes the flat base catalog; a nonzero high byte selects an AAI mount and uses the low 24 bits. This is the same addressing contract as 0x249; only the native surface mode differs." noop_headless = false -source = "frida" +source = "investigation" confidence = "high" depends_on = [] -evidence = "SC0000 Frida-confirmed 17/17 (0x25->EV052CA, 0x2e->EV052DB, 0x36->BG030A background); resolution rule validated on 586/595 captured loads. Traced in CG-load subroutine label_12649 as `set-texture G[0x62424] -1`." +evidence = "Ghidra /v2 gfx_op_0x1f9_load_surface@0x422360 fetches operand 1 and passes it unchanged to asset_open_indexed_entry@0x44f390. asset_catalog_parse_base_tables@0x44e7e0 builds one flat entry array; the opener has no scene/context input and directly indexes that array for high-byte zero. SC0010 set-texture 0x21 proves the distinction outside SC0000: raw 0x21 is SO013A.AGF, while adding SC0010's 0x11e section position lands on unrelated COL0023.OGG. The earlier 586/595 Frida correlation established that SYS4INI file_number describes grouping/order, not runtime operand rebasing." [[opcode.semantics.args]] i = 1 -role = "resId (section-manifest index into SYS4INI)" +role = "universal packed SYS4INI/AAI texture id" observed_types = ["imm", "g-int", "l-int", "l-ptr"] [[opcode.semantics.args]] @@ -5190,17 +5190,17 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "play-modal-movie-to-surface" category = "draw" -summary = "(raw_resource_id)(surface_slot)(movie_flags) - open a universal raw-catalog MPEG asset into an existing retained surface, start its native movie graph, and arm modal run-state bit 0x2000 so script execution remains parked until the movie completes or is skipped. This is the LOGO/OP/ED whole-movie path, distinct from non-modal scene movie op 0x236." +summary = "(packed_resource_id)(surface_slot)(movie_flags) - open a universal packed SYS4INI/AAI MPEG asset into an existing retained surface, start its native movie graph, and arm modal run-state bit 0x2000 so script execution remains parked until the movie completes or is skipped. This is the LOGO/OP/ED whole-movie path; op 0x236 uses the same resolver with non-modal lifecycle." noop_headless = false source = "investigation" confidence = "high" depends_on = [] evidence = "Ghidra /v2: op_0x20f_play_modal_movie_to_surface@0x422e50 shares the movie-object allocation, DirectShow graph open, audio-route, and volume setup used by 0x236, then calls movie_start_modal_playback@0x463280, ORs EngineCtx+0xa0ce4 with 0x2000, and marks movie presentation dirty. The main loop and window procedure special-case run-state 0x2000. Corpus has exactly three sites: LOGO (0x335f,42,4), OP (0x3364,42,4), and ED (0x3324,42,dynamic flags). Those ids are universal raw SYS4INI indexes for MPEG-pack LOGO.AGF, OP.AGF, and ED.AGF; each script releases its surface only after 0x20f resumes." -details = "Implemented through IHost.PlayModalMovieToSurface. ResourceMap.ResolveRawMovie deliberately bypasses scene manifests while retaining MPEG signature validation in ReadMovie. Godot reuses the asynchronous decoder/retained-surface compositor, parks only the VM thread until EOF, and treats mouse click or Accept/Cancel input as completion before wrapper cleanup releases the decoder. MPEG audio remains a separate backend/audio-clock contract." +details = "Implemented through IHost.PlayModalMovieToSurface. Its operand uses the same native universal packed-id catalog contract as 0x236; the separate host call exists for modal wait/cancel lifecycle, not a different resolver. ResourceMap must retain MPEG signature validation in ReadMovie. Godot reuses the asynchronous decoder/retained-surface compositor, parks only the VM thread until EOF, and treats mouse click or Accept/Cancel input as completion before wrapper cleanup releases the decoder. MPEG audio remains a separate backend/audio-clock contract." [[opcode.semantics.args]] i = 1 -role = "raw_resource_id" +role = "packed_resource_id" observed_types = ["imm"] [[opcode.semantics.args]] @@ -6181,13 +6181,13 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "play-movie-to-surface" category = "draw" -summary = "(resource_id)(surface_slot)(movie_flags)(sync_mask) - synchronously resolve/open an archive movie and construct its DirectShow graph, then start asynchronous frame delivery into the retained destination surface. Resource ids use the executing SC manifest when present and universal raw catalog ids in non-SC workers such as BTL. The opcode itself is non-blocking: the VM advances to the next instruction. SC0000's native site evaluates (0x33, 0, 2, 0) at 0x13c8 and resumes at bytecode 0x13d1; its later 0x21c service boundary yields until movie EOF before cleanup." +summary = "(packed_resource_id)(surface_slot)(movie_flags)(sync_mask) - synchronously open a universal packed SYS4INI/AAI movie and construct its DirectShow graph, then start asynchronous frame delivery into the retained destination surface. The opcode itself is non-blocking: the VM advances to the next instruction. SC0000's 0x33 and BTL's 0x2axx/0x2bxx MVB ids are already absolute base-catalog indexes." noop_headless = false source = "investigation" confidence = "high" depends_on = [] -evidence = "Native handler 0x423ee0 and helpers 0x463c50/0x463aa0/0x463e20/0x4625e0; SC0000 native operand capture; exact 0x13c8->0x13d1 trace; archive-only changing-frame decoder and Godot lifecycle tests. BTL's live 0x2b21 site supplies table-selected values 0x2af1/0x2af5/0x2bca/0x2bd8/0x2bde; those exact raw SYS4INI slots are MPEG-backed MVB001/MVB004/MVB914/MVB958/MVB955 rather than BTL-local manifest offsets." -details = "The handler requires an existing destination texture, allocates/reuses a 0x478-byte movie-to-texture object for the surface, opens operand 1 through the native indexed-asset reader, builds FilterGraph/IGraphBuilder/IMediaControl/IMediaPosition/IMediaEvent/IBasicAudio, and presents bottom-up RGB samples through the movie texture renderer. The port must resolve operand 1 through the active SC manifest first and then the typed universal raw movie catalog, matching non-SC BTL's raw MVB ids without weakening still-image/audio type gates. Operand 3 selects movie/sound routing policy: bits 0x10000/0x20000/0x40000/0x80000 force sound route 0/1/2/3, otherwise set:DependMovieSound is used; SC0000's low value 2 is retained as native movie mode state. Operand 4 is stored as the movie sync/device mask at object+0x42c; SC0000 passes 0. Static layer preparation after 0x236 does not terminate the retained movie; 0x21c services it through EOF and subsequent surface cleanup stops/detaches it." +evidence = "Ghidra /v2 op_0x236_play_movie_to_surface@0x423ee0 fetches operand 1 and passes it unchanged to movie_to_texture_open_asset_graph@0x463e20, which passes it unchanged to asset_open_indexed_entry@0x44f390. The opener directly indexes the flat base table or selected AAI table and has no scene input. SC0000 native operand capture and exact 0x13c8->0x13d1 trace prove nonblocking behavior. BTL's live 0x2b21 site supplies 0x2af1/0x2af5/0x2bca/0x2bd8/0x2bde, the exact base entries MVB001/MVB004/MVB914/MVB958/MVB955." +details = "The handler requires an existing destination texture, allocates/reuses a 0x478-byte movie-to-texture object for the surface, opens operand 1 through the native universal packed-id reader, builds FilterGraph/IGraphBuilder/IMediaControl/IMediaPosition/IMediaEvent/IBasicAudio, and presents bottom-up RGB samples through the movie texture renderer. Operand 3 selects movie/sound routing policy: bits 0x10000/0x20000/0x40000/0x80000 force sound route 0/1/2/3, otherwise set:DependMovieSound is used; SC0000's low value 2 is retained as native movie mode state. Operand 4 is the movie sync/device mask. Static layer preparation after 0x236 does not terminate the retained movie; 0x21c services it through EOF and subsequent surface cleanup stops/detaches it. The port should type-check the selected packed record as MPEG but must not add a scene base or scene-first fallback." [[opcode.semantics.args]] i = 1 @@ -6528,12 +6528,12 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "load-raw-texture-surface" category = "draw" -summary = "Load an AGF by universal packed SYS4INI/AAI catalog id into a retained surface slot using native surface mode 1 and the same RGB colorkey contract as set-texture (0x1f9)." +summary = "Load an AGF by universal packed SYS4INI/AAI catalog id into a retained surface slot using native surface mode 1 and the same packed-id/RGB-colorkey contract as set-texture (0x1f9)." noop_headless = false source = "investigation" confidence = "high" depends_on = [] -evidence = "Ghidra /v2: op_0x249_load_raw_texture_surface@0x424b20 is instruction-length 7 and is contract-identical to gfx_op_0x1f9_load_surface through release, asset_open_indexed_entry, RGB colorkey conversion, load failure, and cleanup. Its mode-1 gfx_surface_mode1_ctor selects a tiled large-image wrapper: gfx_tiled_surface_create@0x432ff0 splits the logical dimensions into DAT_005b15b0-sized ordinary mode-0 child textures; gfx_tiled_surface_upload_agf@0x431a10 decodes and uploads each region; gfx_tiled_surface_blit@0x4316b0 subdivides a requested logical source rectangle across those tiles. It is not a spritesheet interpretation or alternate blend mode, so the port's contiguous CPU image is behaviorally equivalent. Corpus literals are universal raw indexes, including FIELD 0x32da..0x32dd -> SO005/SO007/SO008A/SO007A, and therefore bypass scene-section normalization." +evidence = "Ghidra /v2: op_0x249_load_raw_texture_surface@0x424b20 is instruction-length 7 and is contract-identical to gfx_op_0x1f9_load_surface through release, unchanged packed operand, asset_open_indexed_entry, RGB colorkey conversion, load failure, and cleanup. Its only relevant distinction is mode-1 gfx_surface_mode1_ctor, a tiled large-image wrapper: gfx_tiled_surface_create@0x432ff0 splits logical dimensions into ordinary mode-0 child textures; gfx_tiled_surface_upload_agf@0x431a10 decodes/uploads regions; gfx_tiled_surface_blit@0x4316b0 subdivides logical source rectangles. Corpus literals include FIELD 0x32da..0x32dd -> SO005/SO007/SO008A/SO007A. The former claim that only 0x249 bypasses scene normalization was wrong because native never performs scene normalization for 0x1f9 either." [[opcode.semantics.args]] i = 1