Use universal packed resource resolution
This commit is contained in:
@@ -54,8 +54,9 @@ 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.
|
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:
|
`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`.)
|
`py -3.11 -X utf8 tools/parse_sys4ini.py --check`. (Ref: asmodean's `exs4alf` / GARbro Eushully `ArcALF.cs`.)
|
||||||
2. **Resolve `resource_id → asset file`.** **NATIVE RULE CONFIRMED IN GHIDRA (2026-07-21): resource
|
2. **Resolve `resource_id → asset file`.** **NATIVE RULE CONFIRMED IN GHIDRA AND IMPLEMENTED
|
||||||
operands are universal packed SYS4INI/AAI ids. There is no scene-relative path or fallback.**
|
(2026-07-21): resource operands are universal packed SYS4INI/AAI ids. There is no scene-relative path
|
||||||
|
or fallback.**
|
||||||
|
|
||||||
`asset_catalog_parse_base_tables@0x44e7e0` creates one flat base entry array in serialized SYS4INI
|
`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,
|
order. `asset_open_indexed_entry@0x44f390` receives the operand unchanged. If its high byte is zero,
|
||||||
@@ -80,7 +81,7 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
|
|||||||
correlation and the strong `file_number == position - group_start` pattern remain useful evidence about
|
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
|
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
|
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.
|
port's former raw fallback. Low raw ids used from later scripts could instead be silently misresolved.
|
||||||
`tools/resolve_asset.py` and `build/asset-sections.json` are therefore correlation/manifest-inventory
|
`tools/resolve_asset.py` and `build/asset-sections.json` are therefore correlation/manifest-inventory
|
||||||
diagnostics only; they must not drive runtime lookup.
|
diagnostics only; they must not drive runtime lookup.
|
||||||
|
|
||||||
@@ -144,9 +145,10 @@ rendering what the executed bytecode + the map produce (never a hardcoded image)
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
A2b-background: **steps 1–3 landed, but step 2's resolver must be corrected.** Step 1 =
|
A2b-background: **steps 1–3 landed; step 2's packed resolver was corrected 2026-07-21.** Step 1 =
|
||||||
`build/asset-index.json`. Step 2 originally normalized through inferred per-scene sections; native RE now
|
`build/asset-index.json`. Step 2 originally normalized through inferred per-scene sections; native RE proved
|
||||||
proves runtime operands are already universal packed ids. `tools/resolve_asset.py` and
|
runtime operands are already universal packed ids, and every typed runtime facade now uses that contract.
|
||||||
|
`tools/resolve_asset.py` and
|
||||||
`build/asset-sections.json` remain grouping/correlation diagnostics, not runtime inputs. Step 3 = **first-pass
|
`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
|
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
|
layer renders end-to-end from the bytecode. Remaining (next chunk): the **graphics geometry/blend
|
||||||
@@ -214,12 +216,14 @@ scene-local numeric addressing mode.
|
|||||||
on-disk `BinExtractALF.exe` are validation references; the Kelebek repository exposes no clear license,
|
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
|
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.
|
`Sys4AssetCatalog`; raw and compressed information/pixel/ACIF sections use the same bounded primitive.
|
||||||
4. **Runtime consumers (packed-id correction pending).** Script loading and SFX already use
|
4. **Runtime consumers (packed-id correction complete 2026-07-21).** Script loading, textures, voice,
|
||||||
`ResolvePacked`. Texture/voice/non-modal movie facades still contain the disproven scene-first/raw-fallback
|
SFX, cursors, and modal/non-modal movies all select through `ResolvePacked` before type filtering. The VM
|
||||||
compatibility layer and must be switched to the same typed packed lookup. Godot caches decoded RGBA
|
retains bytecode operands unchanged instead of asking the host for scene normalization. Godot caches
|
||||||
surfaces by catalog identity and supplies synchronous dimensions to opcode `0x208`; it no longer reads
|
decoded RGBA and movie identities by the full packed id, preserving AAI selectors rather than colliding
|
||||||
`build/textures/*.BMP`. BGM remains direct-name. Extraction, grouping, and conversion tools remain
|
with equal low indexes in the base catalog. It supplies synchronous dimensions to opcode `0x208` and no
|
||||||
diagnostics.
|
longer reads `build/textures/*.BMP`. BGM remains direct-name. Focused regressions cover SC0010's low
|
||||||
|
`0x21` texture and `0x120..0x122` voices plus append-pack identity. Extraction, grouping, and conversion
|
||||||
|
tools remain diagnostics.
|
||||||
|
|
||||||
### Acceptance gates
|
### Acceptance gates
|
||||||
|
|
||||||
@@ -352,7 +356,7 @@ corpus is `LOGO.BIN (0x335f,42,4)`, `OP.BIN (0x3364,42,4)`, and
|
|||||||
Native `0x20f` also arms modal run-state `0x2000`; unlike `0x236`, these six-instruction wrapper scripts
|
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.
|
depend on the movie service itself to park until EOF/input cancellation before they release surface 42.
|
||||||
|
|
||||||
`ResourceMap.ResolveRawMovie` currently supplies that typed universal lookup, while `ReadMovie` remains the MPEG
|
`ResourceMap.ResolveMovie` 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
|
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
|
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.
|
or mouse/Accept/Cancel input. The wrapper's following release then tears down the completed/cancelled movie.
|
||||||
|
|||||||
@@ -2119,9 +2119,11 @@ open raw entry `0x21` (`SO013A.AGF`); adding SC0010's catalog position `0x11e` i
|
|||||||
`COL0023.OGG`. Its `play-voice 0x120/0x121/0x122` operands directly select
|
`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
|
`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.
|
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
|
The port's former scene-first compatibility resolver inverted this relationship. On 2026-07-21 it was
|
||||||
`ResolvePacked` lookup for texture, voice, and movie consumers. Ghidra `/v2` renames the catalog parser and
|
replaced by typed `ResolvePacked` lookup for texture, voice, and both movie consumers; the VM now preserves
|
||||||
entry-name helper, corrects the opener/caller comments, and is saved.
|
the operand unchanged and Godot cache/movie identities retain the complete packed selector. SC0010 low-id
|
||||||
|
and installed append-pack regressions cover the two failure modes. Ghidra `/v2` renames the catalog parser
|
||||||
|
and entry-name helper, corrects the opener/caller comments, and is saved.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -619,7 +619,7 @@ This is a target-pixel operation, not retained-object teardown. It invokes IDire
|
|||||||
- **grounding:** source=investigation, confidence=high
|
- **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.
|
- **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. 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.
|
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.ResolveMovie selects through ResolvePacked and retains 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)
|
### 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.
|
- **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.
|
||||||
@@ -749,7 +749,7 @@ Implemented through IHost.PlayModalMovieToSurface. Its operand uses the same nat
|
|||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **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.
|
- **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 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.
|
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 type-checks the selected ResolvePacked record as MPEG without adding a scene base or fallback.
|
||||||
|
|
||||||
### 0x238 `set-anim-clock` (set-anim-clock, argc 1)
|
### 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.
|
- **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.
|
||||||
|
|||||||
@@ -561,20 +561,23 @@ Focused regressions cover exact dispatch, signed edge cases, string aliasing, cl
|
|||||||
movie polling, animation reset suppression, delayed voice operands, paired clipping, overlap, and colorkey
|
movie polling, animation reset suppression, delayed voice operands, paired clipping, overlap, and colorkey
|
||||||
transparency. Manual DEBUGMAP acceptance reached player combat and exposed the next concrete frontier:
|
transparency. Manual DEBUGMAP acceptance reached player combat and exposed the next concrete frontier:
|
||||||
combat-effect movies do not play. The resolver/decoder diagnosis is canonical in
|
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.
|
`docs/asset-resolution-re.md`; packed resolution is now corrected, with manual BTL validation and the
|
||||||
|
separate decoder decision remaining before enemy-turn/end-turn breadth.
|
||||||
|
|
||||||
**Combat-effect movie gap diagnosed; native resolver generalized; implementation pending.** BTL's
|
**Universal packed resource resolver implemented; combat movie validation pending.** BTL's
|
||||||
`0x236@0x2b21` consumes universal packed MVB ids, exactly as texture, voice, script-load, and modal-movie
|
`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;
|
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.
|
the port's former scene-first compatibility resolver was therefore generally wrong, not merely incomplete for BTL.
|
||||||
That explains each `movie unresolved BTL:...` warning and the secondary attempt to decode MPEG-backed
|
That explains each `movie unresolved BTL:...` warning and the secondary attempt to decode MPEG-backed
|
||||||
`MVB914.AGF` as a still image. A packed-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
|
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
|
assets with `0x80040217`. Broader samples tie current compatibility to 16-aligned display widths, while 125
|
||||||
installed MVB assets use 280x352. The next bounded slice is to replace typed texture, voice, and movie
|
installed MVB assets use 280x352. Texture, voice, and both movie consumers now use universal packed
|
||||||
consumers with universal packed resolution and regress SC0010's low texture/voice ids plus BTL effects.
|
resolution; VM surface state and Godot caches retain the full selector. Focused tests prove SC0010's low
|
||||||
The existing portable-decoder seam and a software fallback for 280x352 effects remain the following,
|
texture/voice ids and append-pack identity; 302 engine tests, a zero-warning Godot build, and threaded
|
||||||
separate step, with destination dimensions and failed-movie identity preserved correctly.
|
selftest pass. **NEXT:** manually re-enter combat and confirm the BTL ids now resolve into the movie backend.
|
||||||
|
The expected remaining failures are the known 280x352 DirectShow graphs; defer decoder replacement until
|
||||||
|
that resolver-only acceptance check, preserving destination dimensions and failed-movie identity afterward.
|
||||||
|
|
||||||
**Mutable-surface fill/blend regression corrected.** The first visual recheck exposed BUNKI's menu interior
|
**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
|
as transparent. SYSTEM4 creates 800x600 surface 3 and fills it opaque white through `0x20b`; the metadata-only
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ if (args[0] == "audio")
|
|||||||
var sceneName = args[1];
|
var sceneName = args[1];
|
||||||
var sceneKey = Path.GetFileNameWithoutExtension(sceneName).ToUpperInvariant();
|
var sceneKey = Path.GetFileNameWithoutExtension(sceneName).ToUpperInvariant();
|
||||||
var res = ResourceMap.Load();
|
var res = ResourceMap.Load();
|
||||||
var host = new AudioTraceHost(res, sceneKey);
|
var host = new AudioTraceHost(res);
|
||||||
var vm = new VirtualMachine(ScriptByName(sceneName), table, host);
|
var vm = new VirtualMachine(ScriptByName(sceneName), table, host);
|
||||||
// optional: seed globals, e.g. `audio SC0000.BIN 0xa57=1` to set Lily's form-A flag
|
// optional: seed globals, e.g. `audio SC0000.BIN 0xa57=1` to set Lily's form-A flag
|
||||||
foreach (var s in args.Skip(2))
|
foreach (var s in args.Skip(2))
|
||||||
@@ -70,7 +70,7 @@ if (args[0] == "gfx")
|
|||||||
var sceneName = args.First(a => a.EndsWith(".BIN", StringComparison.OrdinalIgnoreCase));
|
var sceneName = args.First(a => a.EndsWith(".BIN", StringComparison.OrdinalIgnoreCase));
|
||||||
var sceneKey = Path.GetFileNameWithoutExtension(sceneName).ToUpperInvariant();
|
var sceneKey = Path.GetFileNameWithoutExtension(sceneName).ToUpperInvariant();
|
||||||
var res = ResourceMap.Load();
|
var res = ResourceMap.Load();
|
||||||
var host = new GfxTraceHost(res, sceneKey);
|
var host = new GfxTraceHost(res);
|
||||||
var session = new GameSession();
|
var session = new GameSession();
|
||||||
foreach (var s in args.Where(a => a.Contains('=')))
|
foreach (var s in args.Where(a => a.Contains('=')))
|
||||||
{
|
{
|
||||||
@@ -100,7 +100,7 @@ if (args[0] == "gfx")
|
|||||||
var vis = vm.Gfx.SnapshotVisibleObjects();
|
var vis = vm.Gfx.SnapshotVisibleObjects();
|
||||||
Console.WriteLine($" visible objects ({vis.Count}, ascending-handle = z-order):");
|
Console.WriteLine($" visible objects ({vis.Count}, ascending-handle = z-order):");
|
||||||
foreach (var v in vis)
|
foreach (var v in vis)
|
||||||
Console.WriteLine($" h=0x{v.Handle:x} surf=0x{v.SurfaceResId:x} ({res.Resolve(sceneKey, v.SurfaceResId)?.Name ?? "?"}) src=({v.SrcX},{v.SrcY} {v.W}x{v.H}) dst=({v.DstX},{v.DstY})");
|
Console.WriteLine($" h=0x{v.Handle:x} surf=0x{v.SurfaceResId:x} ({res.ResolveTexture(v.SurfaceResId)?.Name ?? "?"}) src=({v.SrcX},{v.SrcY} {v.W}x{v.H}) dst=({v.DstX},{v.DstY})");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,17 +345,16 @@ sealed class TraceSetup : IDisposable
|
|||||||
sealed class AudioTraceHost : IHost
|
sealed class AudioTraceHost : IHost
|
||||||
{
|
{
|
||||||
private readonly ResourceMap _res;
|
private readonly ResourceMap _res;
|
||||||
private readonly string _scene;
|
|
||||||
public List<(string Kind, long Id, string Resolved)> Events { get; } = new();
|
public List<(string Kind, long Id, string Resolved)> Events { get; } = new();
|
||||||
public AudioTraceHost(ResourceMap res, string scene) { _res = res; _scene = scene; }
|
public AudioTraceHost(ResourceMap res) { _res = res; }
|
||||||
public void PlayBgm(long id) // BGM: direct name, not the manifest
|
public void PlayBgm(long id) // BGM: direct name, not the manifest
|
||||||
{
|
{
|
||||||
var entry = _res.ResolveBgm(id);
|
var entry = _res.ResolveBgm(id);
|
||||||
Events.Add(("play-bgm", id, entry != null ? $"{entry.Archive} {entry.Name}" : $"BGM{id:D3}.OGG <missing>"));
|
Events.Add(("play-bgm", id, entry != null ? $"{entry.Archive} {entry.Name}" : $"BGM{id:D3}.OGG <missing>"));
|
||||||
}
|
}
|
||||||
public void PlayVoice(long id) // voice: SC section, then frontend raw id
|
public void PlayVoice(long id) // voice: universal packed catalog id
|
||||||
{
|
{
|
||||||
var e = _res.ResolveVoice(_scene, id);
|
var e = _res.ResolveVoice(id);
|
||||||
Events.Add(("play-voice", id, e == null ? "<unresolved>" : $"{e.Archive} {e.Name}"));
|
Events.Add(("play-voice", id, e == null ? "<unresolved>" : $"{e.Archive} {e.Name}"));
|
||||||
}
|
}
|
||||||
public void ShowText(int offset, string text) { }
|
public void ShowText(int offset, string text) { }
|
||||||
@@ -371,12 +370,11 @@ sealed class AudioTraceHost : IHost
|
|||||||
sealed class GfxTraceHost : IHost
|
sealed class GfxTraceHost : IHost
|
||||||
{
|
{
|
||||||
private readonly ResourceMap _res;
|
private readonly ResourceMap _res;
|
||||||
private readonly string _scene;
|
|
||||||
private readonly Dictionary<int, string?> _slotAsset = new(); // slot -> resolved AGF name (or null)
|
private readonly Dictionary<int, string?> _slotAsset = new(); // slot -> resolved AGF name (or null)
|
||||||
// slot -> dimensions of the currently allocated surface. Slot 0 starts as the engine's primary surface.
|
// slot -> dimensions of the currently allocated surface. Slot 0 starts as the engine's primary surface.
|
||||||
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
|
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
|
||||||
public List<string> Events { get; } = new();
|
public List<string> Events { get; } = new();
|
||||||
public GfxTraceHost(ResourceMap res, string scene) { _res = res; _scene = scene; }
|
public GfxTraceHost(ResourceMap res) { _res = res; }
|
||||||
|
|
||||||
public (int Width, int Height) GetTextureSize(int slot)
|
public (int Width, int Height) GetTextureSize(int slot)
|
||||||
{
|
{
|
||||||
@@ -387,7 +385,7 @@ sealed class GfxTraceHost : IHost
|
|||||||
|
|
||||||
public void SetTexture(long resId, int slot)
|
public void SetTexture(long resId, int slot)
|
||||||
{
|
{
|
||||||
var e = _res.ResolveTexture(_scene, resId);
|
var e = _res.ResolveTexture(resId);
|
||||||
RgbaImage? image = e != null ? _res.DecodeTexture(e) : null;
|
RgbaImage? image = e != null ? _res.DecodeTexture(e) : null;
|
||||||
_slotAsset[slot] = e?.Name;
|
_slotAsset[slot] = e?.Name;
|
||||||
_slotDims[slot] = image != null ? (image.Width, image.Height) : (0, 0);
|
_slotDims[slot] = image != null ? (image.Width, image.Height) : (0, 0);
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ public class AgfDecoderTests
|
|||||||
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
|
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
|
||||||
var store = new Sys4AssetStore(catalog, Paths.GameDir, Paths.GameDir);
|
var store = new Sys4AssetStore(catalog, Paths.GameDir, Paths.GameDir);
|
||||||
var resources = new ResourceMap(catalog, store);
|
var resources = new ResourceMap(catalog, store);
|
||||||
Assert.Equal("SO001.AGF", resources.ResolveTexture("SC0000", 0x337e)?.Name);
|
Assert.Equal("SO001.AGF", resources.ResolveTexture(0x337e)?.Name);
|
||||||
var image = AgfDecoder.Decode(store, catalog.ResolveRaw(0x337e)!);
|
var image = AgfDecoder.Decode(store, catalog.ResolveRaw(0x337e)!);
|
||||||
Assert.Equal((800, 300), (image.Width, image.Height));
|
Assert.Equal((800, 300), (image.Width, image.Height));
|
||||||
Assert.Contains(image.Pixels.Where((_, i) => (i & 3) == 3), a => a is > 0 and < 255);
|
Assert.Contains(image.Pixels.Where((_, i) => (i & 3) == 3), a => a is > 0 and < 255);
|
||||||
@@ -89,7 +89,7 @@ public class AgfDecoderTests
|
|||||||
public void InstalledFieldMapSheetsResolveAndDecodeByRawCatalogIndex(int rawId, string name)
|
public void InstalledFieldMapSheetsResolveAndDecodeByRawCatalogIndex(int rawId, string name)
|
||||||
{
|
{
|
||||||
var resources = ResourceMap.Load();
|
var resources = ResourceMap.Load();
|
||||||
var asset = resources.ResolveRawTexture(rawId);
|
var asset = resources.ResolveTexture(rawId);
|
||||||
Assert.NotNull(asset);
|
Assert.NotNull(asset);
|
||||||
Assert.Equal(name, asset.Name);
|
Assert.Equal(name, asset.Name);
|
||||||
var image = resources.DecodeTexture(asset);
|
var image = resources.DecodeTexture(asset);
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ public class CallScriptTests
|
|||||||
{
|
{
|
||||||
public virtual void EnterScriptContext(string scriptName) { }
|
public virtual void EnterScriptContext(string scriptName) { }
|
||||||
public virtual void ExitScriptContext() { }
|
public virtual void ExitScriptContext() { }
|
||||||
public virtual long ResolveTextureResourceId(long resourceId) => resourceId;
|
|
||||||
public void ShowText(int o, string t) { }
|
public void ShowText(int o, string t) { }
|
||||||
public void WaitForInput() { }
|
public void WaitForInput() { }
|
||||||
public void Sleep(long duration) { }
|
public void Sleep(long duration) { }
|
||||||
@@ -52,9 +51,6 @@ public class CallScriptTests
|
|||||||
Events.Add($"exit:{_contexts.Pop()}");
|
Events.Add($"exit:{_contexts.Pop()}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public override long ResolveTextureResourceId(long resourceId)
|
|
||||||
=> resourceId + (_contexts.Peek() == "CALLEE" ? 700 : 70);
|
|
||||||
|
|
||||||
public override void SetTexture(long resourceId, int slot) => Textures.Add((resourceId, slot));
|
public override void SetTexture(long resourceId, int slot) => Textures.Add((resourceId, slot));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +129,7 @@ public class CallScriptTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ScriptLocalTextureIdsFollowTheActiveNestedFrame()
|
public void PackedTextureIdsRemainUnchangedAcrossNestedFrames()
|
||||||
{
|
{
|
||||||
var t = Table();
|
var t = Table();
|
||||||
var callee = Asm(t, "CALLEE",
|
var callee = Asm(t, "CALLEE",
|
||||||
@@ -149,7 +145,7 @@ public class CallScriptTests
|
|||||||
|
|
||||||
vm.Run();
|
vm.Run();
|
||||||
|
|
||||||
Assert.Equal(new[] { (77L, 1), (707L, 2), (77L, 3) }, host.Textures);
|
Assert.Equal(new[] { (7L, 1), (7L, 2), (7L, 3) }, host.Textures);
|
||||||
Assert.Equal(new[] { "enter:CALLER", "enter:CALLEE", "exit:CALLEE", "exit:CALLER" }, host.Events);
|
Assert.Equal(new[] { "enter:CALLER", "enter:CALLEE", "exit:CALLEE", "exit:CALLER" }, host.Events);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ public class GfxCommandBufferTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RawTextureLoadBypassesSceneResourceNormalizationAndFeedsRetainedDraws()
|
public void ModeOneTextureLoadPreservesPackedIdAndFeedsRetainedDraws()
|
||||||
{
|
{
|
||||||
var t = T();
|
var t = T();
|
||||||
var scene = ScriptAssembler.Assemble(t, "RAW-GFX", new List<(int, Operand[])>
|
var scene = ScriptAssembler.Assemble(t, "RAW-GFX", new List<(int, Operand[])>
|
||||||
@@ -135,7 +135,7 @@ public class GfxCommandBufferTests
|
|||||||
(0x1fb, new[] { I(0x100), I(0x3e), I(0), I(0), I(100), I(100), I(20), I(30) }),
|
(0x1fb, new[] { I(0x100), I(0x3e), I(0), I(0), I(100), I(100), I(20), I(30) }),
|
||||||
Exit(),
|
Exit(),
|
||||||
}, System.Array.Empty<string>());
|
}, System.Array.Empty<string>());
|
||||||
var host = new RecordingHost { TextureResourceIdOffset = 0x1000 };
|
var host = new RecordingHost();
|
||||||
var vm = new VirtualMachine(scene, t, host);
|
var vm = new VirtualMachine(scene, t, host);
|
||||||
|
|
||||||
vm.Run();
|
vm.Run();
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ public class HistoryInteractionOpsTests
|
|||||||
Assert.Equal(new[] { (expectedVoice, checked((int)expectedVariant)) }, host.VoiceRequests);
|
Assert.Equal(new[] { (expectedVoice, checked((int)expectedVariant)) }, host.VoiceRequests);
|
||||||
|
|
||||||
var resources = ResourceMap.Load();
|
var resources = ResourceMap.Load();
|
||||||
var voice = Assert.IsType<AssetEntry>(resources.Resolve("SC0000", expectedVoice));
|
var voice = Assert.IsType<AssetEntry>(resources.ResolveVoice(expectedVoice));
|
||||||
Assert.Equal("MAN999.OGG", voice.Name);
|
Assert.Equal("MAN999.OGG", voice.Name);
|
||||||
Assert.NotEmpty(resources.ReadAudio(voice).Bytes);
|
Assert.NotEmpty(resources.ReadAudio(voice).Bytes);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ public class MovieOpcodeTests
|
|||||||
{
|
{
|
||||||
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
|
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
|
||||||
var resources = new ResourceMap(catalog, new Sys4AssetStore(catalog, Paths.GameDir));
|
var resources = new ResourceMap(catalog, new Sys4AssetStore(catalog, Paths.GameDir));
|
||||||
var entry = resources.Resolve("SC0000", 0x33);
|
var entry = resources.ResolveMovie(0x33);
|
||||||
|
|
||||||
Assert.Equal("CHAPTER.AGF", entry?.Name);
|
Assert.Equal("CHAPTER.AGF", entry?.Name);
|
||||||
var movie = resources.ReadMovie(entry!);
|
var movie = resources.ReadMovie(entry!);
|
||||||
@@ -207,11 +207,11 @@ public class MovieOpcodeTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(0x335f, "LOGO.AGF")]
|
[InlineData(0x335f, "LOGO.AGF")]
|
||||||
[InlineData(0x3364, "OP.AGF")]
|
[InlineData(0x3364, "OP.AGF")]
|
||||||
public void ModalMoviePayloadResolvesFromUniversalRawCatalog(int rawId, string expectedName)
|
public void ModalMoviePayloadResolvesFromUniversalPackedCatalog(int resourceId, string expectedName)
|
||||||
{
|
{
|
||||||
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
|
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
|
||||||
var resources = new ResourceMap(catalog, new Sys4AssetStore(catalog, Paths.GameDir));
|
var resources = new ResourceMap(catalog, new Sys4AssetStore(catalog, Paths.GameDir));
|
||||||
var entry = resources.ResolveRawMovie(rawId);
|
var entry = resources.ResolveMovie(resourceId);
|
||||||
|
|
||||||
Assert.Equal(expectedName, entry?.Name);
|
Assert.Equal(expectedName, entry?.Name);
|
||||||
var movie = resources.ReadMovie(entry!);
|
var movie = resources.ReadMovie(entry!);
|
||||||
@@ -224,7 +224,7 @@ public class MovieOpcodeTests
|
|||||||
if (!OperatingSystem.IsWindows()) return;
|
if (!OperatingSystem.IsWindows()) return;
|
||||||
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
|
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
|
||||||
var resources = new ResourceMap(catalog, new Sys4AssetStore(catalog, Paths.GameDir));
|
var resources = new ResourceMap(catalog, new Sys4AssetStore(catalog, Paths.GameDir));
|
||||||
var entry = resources.Resolve("SC0000", 0x33)!;
|
var entry = resources.ResolveMovie(0x33)!;
|
||||||
using var decoder = new DirectShowMovieDecoder(resources.ReadMovie(entry));
|
using var decoder = new DirectShowMovieDecoder(resources.ReadMovie(entry));
|
||||||
|
|
||||||
Assert.True(decoder.StopTimeMs > 0, "DirectShow should expose a positive IMediaPosition stop time");
|
Assert.True(decoder.StopTimeMs > 0, "DirectShow should expose a positive IMediaPosition stop time");
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public class Sys4AssetStoreTests
|
|||||||
Assert.All(append.Files, entry =>
|
Assert.All(append.Files, entry =>
|
||||||
{
|
{
|
||||||
Assert.Equal(1, entry.PackId);
|
Assert.Equal(1, entry.PackId);
|
||||||
|
Assert.Equal(0x01000000 | entry.RawIndex, entry.PackedId);
|
||||||
Assert.StartsWith("$1$", entry.Name);
|
Assert.StartsWith("$1$", entry.Name);
|
||||||
Assert.Equal("APPEND01.ALF", entry.Archive);
|
Assert.Equal("APPEND01.ALF", entry.Archive);
|
||||||
});
|
});
|
||||||
@@ -178,14 +179,14 @@ public class Sys4AssetStoreTests
|
|||||||
Assert.Equal("BGM005.OGG", bgm?.Name);
|
Assert.Equal("BGM005.OGG", bgm?.Name);
|
||||||
AssertOgg(resources.ReadAudio(bgm!));
|
AssertOgg(resources.ReadAudio(bgm!));
|
||||||
|
|
||||||
var voice = resources.ResolveVoice("SC0000", 0x24);
|
var voice = resources.ResolveVoice(0x24);
|
||||||
Assert.Equal("MAN999.OGG", voice?.Name);
|
Assert.Equal("MAN999.OGG", voice?.Name);
|
||||||
AssertOgg(resources.ReadAudio(voice!));
|
AssertOgg(resources.ReadAudio(voice!));
|
||||||
|
|
||||||
var roomVoice = resources.ResolveVoice("ROOM", 0x3365);
|
var roomVoice = resources.ResolveVoice(0x3365);
|
||||||
Assert.Equal("EUA0016.OGG", roomVoice?.Name);
|
Assert.Equal("EUA0016.OGG", roomVoice?.Name);
|
||||||
AssertOgg(resources.ReadAudio(roomVoice!));
|
AssertOgg(resources.ReadAudio(roomVoice!));
|
||||||
Assert.Null(resources.ResolveVoice("ROOM", 0x337e)); // SO001.AGF is not voice audio.
|
Assert.Null(resources.ResolveVoice(0x337e)); // SO001.AGF is not voice audio.
|
||||||
|
|
||||||
var sfx = resources.ResolveSoundEffect(0x28);
|
var sfx = resources.ResolveSoundEffect(0x28);
|
||||||
Assert.Equal("E0808.WAV", sfx?.Name);
|
Assert.Equal("E0808.WAV", sfx?.Name);
|
||||||
@@ -194,7 +195,6 @@ public class Sys4AssetStoreTests
|
|||||||
Assert.Equal("RIFF", Encoding.ASCII.GetString(wav.Bytes, 0, 4));
|
Assert.Equal("RIFF", Encoding.ASCII.GetString(wav.Bytes, 0, 4));
|
||||||
Assert.Equal("WAVE", Encoding.ASCII.GetString(wav.Bytes, 8, 4));
|
Assert.Equal("WAVE", Encoding.ASCII.GetString(wav.Bytes, 8, 4));
|
||||||
|
|
||||||
Assert.Null(resources.Resolve("TITLE", 0x2aea));
|
|
||||||
Assert.Equal("SE020.WAV", resources.ResolveSoundEffect(0x2aea)?.Name);
|
Assert.Equal("SE020.WAV", resources.ResolveSoundEffect(0x2aea)?.Name);
|
||||||
Assert.Equal("SE013.WAV", resources.ResolveSoundEffect(0x2aeb)?.Name);
|
Assert.Equal("SE013.WAV", resources.ResolveSoundEffect(0x2aeb)?.Name);
|
||||||
Assert.Equal("SE015.WAV", resources.ResolveSoundEffect(0x3321)?.Name);
|
Assert.Equal("SE015.WAV", resources.ResolveSoundEffect(0x3321)?.Name);
|
||||||
@@ -204,6 +204,33 @@ public class Sys4AssetStoreTests
|
|||||||
Assert.Throws<InvalidDataException>(() => resources.ReadAudio(catalog.ResolveName("SO001.AGF")!));
|
Assert.Throws<InvalidDataException>(() => resources.ReadAudio(catalog.ResolveName("SO001.AGF")!));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Sc0010LowOperandsAreAlreadyUniversalPackedIds()
|
||||||
|
{
|
||||||
|
var resources = ResourceMap.Load();
|
||||||
|
|
||||||
|
Assert.Equal("SO013A.AGF", resources.ResolveTexture(0x21)?.Name);
|
||||||
|
Assert.Equal("LILA1414.OGG", resources.ResolveVoice(0x120)?.Name);
|
||||||
|
Assert.Equal("LILB0053.OGG", resources.ResolveVoice(0x121)?.Name);
|
||||||
|
Assert.Equal("LILC0054.OGG", resources.ResolveVoice(0x122)?.Name);
|
||||||
|
|
||||||
|
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
|
||||||
|
Assert.Equal("COL0023.OGG", catalog.ResolveRaw(0x11e + 0x21)?.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TypedResourceResolutionPreservesAppendPackSelector()
|
||||||
|
{
|
||||||
|
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
|
||||||
|
var resources = new ResourceMap(catalog);
|
||||||
|
var append = Assert.Single(catalog.AppendPacks).Value;
|
||||||
|
var texture = Assert.Single(append.Files.Where(entry =>
|
||||||
|
entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase)).Take(1));
|
||||||
|
|
||||||
|
Assert.Same(texture, resources.ResolveTexture(texture.PackedId));
|
||||||
|
Assert.NotEqual(texture, resources.ResolveTexture(texture.RawIndex));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AllInstalledLooseScriptOverridesShadowArchiveCopies()
|
public void AllInstalledLooseScriptOverridesShadowArchiveCopies()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,9 +55,7 @@ internal class RecordingHost : IHost
|
|||||||
public readonly List<bool> AdvPagePresentationSuspended = new();
|
public readonly List<bool> AdvPagePresentationSuspended = new();
|
||||||
public int CursorClearCount;
|
public int CursorClearCount;
|
||||||
public int SceneContextResets;
|
public int SceneContextResets;
|
||||||
public long TextureResourceIdOffset;
|
|
||||||
public void ReportWarning(string message) => Warnings.Add(message);
|
public void ReportWarning(string message) => Warnings.Add(message);
|
||||||
public long ResolveTextureResourceId(long resourceId) => resourceId + TextureResourceIdOffset;
|
|
||||||
public void ShowText(int offset, string text) => Lines.Add((offset, text));
|
public void ShowText(int offset, string text) => Lines.Add((offset, text));
|
||||||
public void SetAdvTextCursor(int layoutSlot, int x, int y) => TextCursors.Add((layoutSlot, x, y));
|
public void SetAdvTextCursor(int layoutSlot, int x, int y) => TextCursors.Add((layoutSlot, x, y));
|
||||||
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text)
|
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text)
|
||||||
@@ -162,8 +160,8 @@ internal class RecordingHost : IHost
|
|||||||
return MovieStopTimeMs;
|
return MovieStopTimeMs;
|
||||||
}
|
}
|
||||||
public bool IsMovieSurfaceActive(int surfaceSlot) => ActiveMovieSurfaces.Contains(surfaceSlot);
|
public bool IsMovieSurfaceActive(int surfaceSlot) => ActiveMovieSurfaces.Contains(surfaceSlot);
|
||||||
public void PlayModalMovieToSurface(long rawResourceId, int surfaceSlot, long movieFlags)
|
public void PlayModalMovieToSurface(long resourceId, int surfaceSlot, long movieFlags)
|
||||||
=> ModalMovies.Add((rawResourceId, surfaceSlot, movieFlags));
|
=> ModalMovies.Add((resourceId, surfaceSlot, movieFlags));
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class MapProvider : IScriptProvider
|
internal sealed class MapProvider : IScriptProvider
|
||||||
|
|||||||
@@ -21,11 +21,9 @@ public interface IHost
|
|||||||
{
|
{
|
||||||
/// <summary>Report a recoverable runtime discrepancy while allowing script execution to continue.</summary>
|
/// <summary>Report a recoverable runtime discrepancy while allowing script execution to continue.</summary>
|
||||||
void ReportWarning(string message) => System.Console.Error.WriteLine(message);
|
void ReportWarning(string message) => System.Console.Error.WriteLine(message);
|
||||||
// Script-local resource ids resolve against the currently executing frame's SYS4INI section.
|
// Script context is retained for diagnostics/page location; resource operands are universal packed ids.
|
||||||
// Interactive hosts track this stack; headless hosts may keep the no-op/default identity behavior.
|
|
||||||
void EnterScriptContext(string scriptName) { }
|
void EnterScriptContext(string scriptName) { }
|
||||||
void ExitScriptContext() { }
|
void ExitScriptContext() { }
|
||||||
long ResolveTextureResourceId(long resourceId) => resourceId;
|
|
||||||
void ShowText(int offset, string text);
|
void ShowText(int offset, string text);
|
||||||
// Native ADV text subsystem: op 0x7a updates the selected layout's last 20-byte cursor record;
|
// Native ADV text subsystem: op 0x7a updates the selected layout's last 20-byte cursor record;
|
||||||
// op 0x204 rasterizes a string into a numbered surface before 0x1fb binds that surface.
|
// op 0x204 rasterizes a string into a numbered surface before 0x1fb binds that surface.
|
||||||
@@ -125,8 +123,8 @@ public interface IHost
|
|||||||
/// immediately after 0x236 returns.</returns>
|
/// immediately after 0x236 returns.</returns>
|
||||||
long? PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask) => null;
|
long? PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask) => null;
|
||||||
bool IsMovieSurfaceActive(int surfaceSlot) => false;
|
bool IsMovieSurfaceActive(int surfaceSlot) => false;
|
||||||
// Native op 0x20f uses a universal raw-catalog id and parks script execution until the movie
|
// Native op 0x20f uses a universal packed id and parks script execution until the movie
|
||||||
// reaches EOF or the player cancels it. The decoder remains asynchronous; the interactive host
|
// reaches EOF or the player cancels it. The decoder remains asynchronous; the interactive host
|
||||||
// owns the modal wait so its render loop can continue publishing frames.
|
// owns the modal wait so its render loop can continue publishing frames.
|
||||||
void PlayModalMovieToSurface(long rawResourceId, int surfaceSlot, long movieFlags) { }
|
void PlayModalMovieToSurface(long resourceId, int surfaceSlot, long movieFlags) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
namespace Age.Engine.Sys4;
|
namespace Age.Engine.Sys4;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compatibility facade over the runtime SYS4 catalog. Scene-local graphics/voice/movie ids resolve
|
/// Typed facade over the runtime SYS4 catalog. Graphics, voice, movie, SFX, and cursor operands use
|
||||||
/// through the executing script's manifest; BGM uses direct names; SFX/cursors use universal packed ids.
|
/// universal packed ids; BGM uses direct names.
|
||||||
/// See docs/asset-resolution-re.md.
|
/// See docs/asset-resolution-re.md.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ResourceMap
|
public sealed class ResourceMap
|
||||||
@@ -18,43 +18,26 @@ public sealed class ResourceMap
|
|||||||
|
|
||||||
public static ResourceMap Load() => new(Sys4AssetCatalog.Load(Paths.Sys4Ini));
|
public static ResourceMap Load() => new(Sys4AssetCatalog.Load(Paths.Sys4Ini));
|
||||||
|
|
||||||
/// <summary>Resolve a scene-local resId to its asset, or null if out of range / unknown scene.</summary>
|
/// <summary>Resolve a universal packed SYS4INI/AAI id to an AGF texture record.</summary>
|
||||||
public AssetEntry? Resolve(string scene, long resId)
|
public AssetEntry? ResolveTexture(long resourceId)
|
||||||
{
|
{
|
||||||
return _catalog.ResolveScene(scene, resId);
|
var entry = _catalog.ResolvePacked(resourceId);
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Resolve graphics normally through the scene manifest, with the universal raw-id
|
|
||||||
/// fallback used by SYSTEM4-owned assets such as SO001.</summary>
|
|
||||||
public AssetEntry? ResolveTexture(string scene, long resId)
|
|
||||||
{
|
|
||||||
var entry = _catalog.ResolveScene(scene, resId) ?? _catalog.ResolveRaw(resId);
|
|
||||||
return entry is { IsPlaceholder: false } &&
|
return entry is { IsPlaceholder: false } &&
|
||||||
entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase) ? entry : null;
|
entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase) ? entry : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Resolve voice audio through the active SC section when one exists, then through the
|
/// <summary>Resolve a universal packed SYS4INI/AAI id to a voice audio record.</summary>
|
||||||
/// universal raw catalog used by non-SC frontend scripts such as ROOM.</summary>
|
public AssetEntry? ResolveVoice(long resourceId)
|
||||||
public AssetEntry? ResolveVoice(string scene, long resId)
|
|
||||||
{
|
{
|
||||||
var entry = _catalog.ResolveScene(scene, resId) ?? _catalog.ResolveRaw(resId);
|
var entry = _catalog.ResolvePacked(resourceId);
|
||||||
return entry is { IsPlaceholder: false } && IsAudio(entry) ? entry : null;
|
return entry is { IsPlaceholder: false } && IsAudio(entry) ? entry : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Resolve an already-normalized packed catalog id without applying a scene section base.</summary>
|
/// <summary>Resolve a universal packed SYS4INI/AAI id to an AGF-named movie record. ReadMovie
|
||||||
public AssetEntry? ResolveRawTexture(long rawId)
|
/// validates the MPEG signature because still images use the same extension.</summary>
|
||||||
|
public AssetEntry? ResolveMovie(long resourceId)
|
||||||
{
|
{
|
||||||
var entry = _catalog.ResolvePacked(rawId);
|
var entry = _catalog.ResolvePacked(resourceId);
|
||||||
return entry is { IsPlaceholder: false } &&
|
|
||||||
entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase) ? entry : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Resolve op 0x20f's universal raw-catalog movie id without applying the executing
|
|
||||||
/// script's manifest base. AGE stores these MPEG program streams under .AGF names; ReadMovie
|
|
||||||
/// validates the payload signature before playback.</summary>
|
|
||||||
public AssetEntry? ResolveRawMovie(long rawId)
|
|
||||||
{
|
|
||||||
var entry = _catalog.ResolveRaw(rawId);
|
|
||||||
return entry is { IsPlaceholder: false } &&
|
return entry is { IsPlaceholder: false } &&
|
||||||
entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase) ? entry : null;
|
entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase) ? entry : null;
|
||||||
}
|
}
|
||||||
@@ -81,7 +64,7 @@ public sealed class ResourceMap
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolve a BGM id to its catalog entry. BGM is addressed by DIRECT LITERAL NAME (BGM{id:D3}.OGG), NOT the
|
/// Resolve a BGM id to its catalog entry. BGM is addressed by DIRECT LITERAL NAME (BGM{id:D3}.OGG), NOT the
|
||||||
/// per-scene section manifest that voices/textures use. Confirmed by ear (play-bgm 5->BGM005, 8->BGM008)
|
/// universal packed resource table used by voices/textures. Confirmed by ear (play-bgm 5->BGM005, 8->BGM008)
|
||||||
/// and by the play-bgm 0x23->BGM035 case: BGM035 is a real standalone track (the BGM set skips 030-034),
|
/// and by the play-bgm 0x23->BGM035 case: BGM035 is a real standalone track (the BGM set skips 030-034),
|
||||||
/// which the manifest mis-resolved to a graphics entry. See docs/asset-resolution-re.md.
|
/// which the manifest mis-resolved to a graphics entry. See docs/asset-resolution-re.md.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ public sealed record AssetEntry(
|
|||||||
int ArchiveId = -1,
|
int ArchiveId = -1,
|
||||||
int FileNumber = -1,
|
int FileNumber = -1,
|
||||||
bool IsPlaceholder = false,
|
bool IsPlaceholder = false,
|
||||||
int PackId = 0);
|
int PackId = 0)
|
||||||
|
{
|
||||||
|
/// <summary>The exact packed SYS4INI/AAI id AGE uses to address this record.</summary>
|
||||||
|
public int PackedId => checked((PackId << 24) | RawIndex);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>A real catalog entry paired with the packed resource id AGE uses at runtime.</summary>
|
/// <summary>A real catalog entry paired with the packed resource id AGE uses at runtime.</summary>
|
||||||
public sealed record PackedAssetEntry(long PackedId, AssetEntry Asset);
|
public sealed record PackedAssetEntry(long PackedId, AssetEntry Asset);
|
||||||
|
|||||||
@@ -1539,29 +1539,27 @@ public sealed class VirtualMachine
|
|||||||
_host.CreateTexture((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2])); return pc + 1;
|
_host.CreateTexture((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2])); return pc + 1;
|
||||||
case "set-texture": // 0x1f9 (resId)(slot)(colorkey) — load a file into the slot's surface
|
case "set-texture": // 0x1f9 (resId)(slot)(colorkey) — load a file into the slot's surface
|
||||||
{
|
{
|
||||||
long requestedResourceId = Read(a[0]);
|
long resourceId = Read(a[0]);
|
||||||
long resolvedResourceId = _host.ResolveTextureResourceId(requestedResourceId);
|
|
||||||
if (_diagSetTexture) // AGE_DIAG_SETTEX: log the SLOT operand source (literal vs which global) — grey-BG slot dig
|
if (_diagSetTexture) // AGE_DIAG_SETTEX: log the SLOT operand source (literal vs which global) — grey-BG slot dig
|
||||||
System.Console.Error.WriteLine($"[settex] resId=0x{requestedResourceId:x}->0x{resolvedResourceId:x} slot={(int)Read(a[1])} " +
|
System.Console.Error.WriteLine($"[settex] resId=0x{resourceId:x} slot={(int)Read(a[1])} " +
|
||||||
$"slotOp=(type={a[1].Type} val=0x{a[1].Value:x}){(a[1].Type == 3 ? $" G[0x{a[1].Value:x}]" : "")}");
|
$"slotOp=(type={a[1].Type} val=0x{a[1].Value:x}){(a[1].Type == 3 ? $" G[0x{a[1].Value:x}]" : "")}");
|
||||||
_host.ReleaseSurface((int)Read(a[1]));
|
_host.ReleaseSurface((int)Read(a[1]));
|
||||||
long colorKey = a.Count > 2 ? Read(a[2]) : -1;
|
long colorKey = a.Count > 2 ? Read(a[2]) : -1;
|
||||||
Gfx.SetSurface((int)Read(a[1]), resolvedResourceId, colorKey);
|
Gfx.SetSurface((int)Read(a[1]), resourceId, colorKey);
|
||||||
_host.SetTexture(resolvedResourceId, (int)Read(a[1]), colorKey);
|
_host.SetTexture(resourceId, (int)Read(a[1]), colorKey);
|
||||||
return pc + 1; // host still tracks dims for get-texture-size
|
return pc + 1; // host still tracks dims for get-texture-size
|
||||||
}
|
}
|
||||||
case "u00422EB0": // pre-reference compatibility
|
case "u00422EB0": // pre-reference compatibility
|
||||||
case "load-raw-texture-surface": // 0x249 (raw catalog id)(slot)(colorkey)
|
case "load-raw-texture-surface": // 0x249 (packed resource id)(slot)(colorkey)
|
||||||
{
|
{
|
||||||
// Native shares 0x1f9's release/load/colorkey path, but constructs its mode-1
|
// Native shares 0x1f9's release/load/colorkey path, but constructs its mode-1
|
||||||
// surface subclass and receives an already-global SYS4INI catalog index. The
|
// surface subclass. Both texture opcodes receive the same universal packed id;
|
||||||
// CPU compositor does not need the D3D subclass distinction; it does need the
|
// the CPU compositor does not need the D3D subclass distinction.
|
||||||
// resource id to bypass the executing script's scene-section normalization.
|
long resourceId = Read(a[0]);
|
||||||
long rawResourceId = Read(a[0]);
|
|
||||||
int surfaceSlot = (int)Read(a[1]);
|
int surfaceSlot = (int)Read(a[1]);
|
||||||
_host.ReleaseSurface(surfaceSlot);
|
_host.ReleaseSurface(surfaceSlot);
|
||||||
Gfx.SetSurface(surfaceSlot, rawResourceId, Read(a[2]));
|
Gfx.SetSurface(surfaceSlot, resourceId, Read(a[2]));
|
||||||
_host.SetTexture(rawResourceId, surfaceSlot, Read(a[2]));
|
_host.SetTexture(resourceId, surfaceSlot, Read(a[2]));
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
}
|
}
|
||||||
case "draw-texture": // 0x1fb (handle)(slot)(srcX)(srcY)(w)(h)(dstX)(dstY) — bind object -> surface + rect + pos
|
case "draw-texture": // 0x1fb (handle)(slot)(srcX)(srcY)(w)(h)(dstX)(dstY) — bind object -> surface + rect + pos
|
||||||
@@ -1652,14 +1650,14 @@ public sealed class VirtualMachine
|
|||||||
case "get-initial-root-run": // 0x130 (out)
|
case "get-initial-root-run": // 0x130 (out)
|
||||||
Write(a[0], _initialRootRun ? 1 : 0);
|
Write(a[0], _initialRootRun ? 1 : 0);
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
case "play-modal-movie-to-surface": // 0x20f (raw resource)(surface)(movie flags)
|
case "play-modal-movie-to-surface": // 0x20f (packed resource)(surface)(movie flags)
|
||||||
{
|
{
|
||||||
long rawResourceId = Read(a[0]);
|
long resourceId = Read(a[0]);
|
||||||
int surfaceSlot = (int)Read(a[1]);
|
int surfaceSlot = (int)Read(a[1]);
|
||||||
// The modal and scene-local paths share retained-surface composition. The host's
|
// Modal and non-modal paths share packed resolution and retained-surface composition.
|
||||||
// distinct entry point preserves 0x20f's raw-id resolver and blocking lifecycle.
|
// The distinct host entry point owns only 0x20f's blocking lifecycle.
|
||||||
Gfx.SetSurface(surfaceSlot, rawResourceId, 0);
|
Gfx.SetSurface(surfaceSlot, resourceId, 0);
|
||||||
_host.PlayModalMovieToSurface(rawResourceId, surfaceSlot, Read(a[2]));
|
_host.PlayModalMovieToSurface(resourceId, surfaceSlot, Read(a[2]));
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
}
|
}
|
||||||
case "u004221A0": // pre-reference compatibility
|
case "u004221A0": // pre-reference compatibility
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ public sealed class GodotAdvHost : IHost
|
|||||||
private readonly object _scriptContextLock = new();
|
private readonly object _scriptContextLock = new();
|
||||||
private readonly Stack<string> _scriptContexts = new();
|
private readonly Stack<string> _scriptContexts = new();
|
||||||
private readonly object _imageLock = new();
|
private readonly object _imageLock = new();
|
||||||
private readonly Dictionary<int, RgbaImage?> _images = new(); // raw catalog id -> decoded pixels
|
private readonly Dictionary<int, RgbaImage?> _images = new(); // packed catalog id -> decoded pixels
|
||||||
// Mutable AGE surfaces are published by replacing immutable RgbaImage snapshots, so the compositor
|
// Mutable AGE surfaces are published by replacing immutable RgbaImage snapshots, so the compositor
|
||||||
// can safely finish reading an old frame while the VM prepares a copied-rectangle update.
|
// can safely finish reading an old frame while the VM prepares a copied-rectangle update.
|
||||||
private readonly Dictionary<int, RgbaImage> _surfaceImages = new();
|
private readonly Dictionary<int, RgbaImage> _surfaceImages = new();
|
||||||
private readonly Dictionary<int, long> _surfaceColorKeys = new();
|
private readonly Dictionary<int, long> _surfaceColorKeys = new();
|
||||||
private readonly Dictionary<int, long> _surfaceResources = new(); // surface slot -> normalized raw catalog id
|
private readonly Dictionary<int, long> _surfaceResources = new(); // surface slot -> packed catalog id
|
||||||
private readonly Dictionary<long, (RgbaImage Image, string Name, int RawIndex)> _movieFrames = new();
|
private readonly Dictionary<long, (RgbaImage Image, string Name, int AssetId)> _movieFrames = new();
|
||||||
private readonly Dictionary<int, long> _movieBySurface = new();
|
private readonly Dictionary<int, long> _movieBySurface = new();
|
||||||
private readonly HashSet<long> _completedMovies = new();
|
private readonly HashSet<long> _completedMovies = new();
|
||||||
private readonly string?[] _sfxNames = new string?[10]; // SC0000 native channel subset
|
private readonly string?[] _sfxNames = new string?[10]; // SC0000 native channel subset
|
||||||
@@ -102,9 +102,6 @@ public sealed class GodotAdvHost : IHost
|
|||||||
if (scene != null) _timeline?.Event("script-context-exit", new() { ["scene"] = scene });
|
if (scene != null) _timeline?.Event("script-context-exit", new() { ["scene"] = scene });
|
||||||
}
|
}
|
||||||
|
|
||||||
public long ResolveTextureResourceId(long resourceId)
|
|
||||||
=> _res.ResolveTexture(CurrentScene, resourceId)?.RawIndex ?? resourceId;
|
|
||||||
|
|
||||||
public void ShowText(int offset, string text)
|
public void ShowText(int offset, string text)
|
||||||
{
|
{
|
||||||
Captured.Add((offset, text));
|
Captured.Add((offset, text));
|
||||||
@@ -301,13 +298,13 @@ public sealed class GodotAdvHost : IHost
|
|||||||
if (!_waitIndicators.TryGetValue(_activeWaitLayout, out config)) return null;
|
if (!_waitIndicators.TryGetValue(_activeWaitLayout, out config)) return null;
|
||||||
if (!_surfaceResources.TryGetValue(config.SurfaceSlot, out resourceId)) return null;
|
if (!_surfaceResources.TryGetValue(config.SurfaceSlot, out resourceId)) return null;
|
||||||
}
|
}
|
||||||
var asset = _res.ResolveRawTexture(resourceId);
|
var asset = _res.ResolveTexture(resourceId);
|
||||||
var image = asset != null ? Decode(asset) : null;
|
var image = asset != null ? Decode(asset) : null;
|
||||||
if (asset == null || image == null || config.CellWidth <= 0 || config.CellHeight <= 0) return null;
|
if (asset == null || image == null || config.CellWidth <= 0 || config.CellHeight <= 0) return null;
|
||||||
int frames = System.Math.Max(1, config.TerminalFrame + 1);
|
int frames = System.Math.Max(1, config.TerminalFrame + 1);
|
||||||
long period = System.Math.Max(1, config.FramePeriodMs);
|
long period = System.Math.Max(1, config.FramePeriodMs);
|
||||||
int frame = (int)((_clock.NowMs - _waitIndicatorStartedMs) / period % frames);
|
int frame = (int)((_clock.NowMs - _waitIndicatorStartedMs) / period % frames);
|
||||||
return new AdvWaitIndicatorSnapshot(image, asset.Name, asset.RawIndex, config, frame);
|
return new AdvWaitIndicatorSnapshot(image, asset.Name, asset.PackedId, config, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
public volatile int Pages; // VM-thread page counter (incremented before IsWaiting so shot-gating can't race)
|
public volatile int Pages; // VM-thread page counter (incremented before IsWaiting so shot-gating can't race)
|
||||||
@@ -811,7 +808,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
_surfaceText.Remove(slot);
|
_surfaceText.Remove(slot);
|
||||||
_surfaceResources[slot] = resourceId;
|
_surfaceResources[slot] = resourceId;
|
||||||
}
|
}
|
||||||
var asset = _res.ResolveRawTexture(resourceId);
|
var asset = _res.ResolveTexture(resourceId);
|
||||||
var image = asset != null ? Decode(asset) : null;
|
var image = asset != null ? Decode(asset) : null;
|
||||||
_slotDims[slot] = image != null ? (image.Width, image.Height) : (0, 0);
|
_slotDims[slot] = image != null ? (image.Width, image.Height) : (0, 0);
|
||||||
if (TraceOps) Godot.GD.Print($"[op] set-texture slot={slot} resId=0x{resourceId:x} -> {(asset?.Name ?? "<none>")}");
|
if (TraceOps) Godot.GD.Print($"[op] set-texture slot={slot} resId=0x{resourceId:x} -> {(asset?.Name ?? "<none>")}");
|
||||||
@@ -868,22 +865,22 @@ public sealed class GodotAdvHost : IHost
|
|||||||
return RgbaSurfaceOps.WithColorKey(resolved.Value.Image, colorKey);
|
return RgbaSurfaceOps.WithColorKey(resolved.Value.Image, colorKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Resolve a gfx surface through scene-local or universal raw-id addressing and decode it
|
/// <summary>Resolve a gfx surface through universal packed addressing and decode it from the
|
||||||
/// from the loose-first asset store.</summary>
|
/// loose-first asset store.</summary>
|
||||||
public (RgbaImage Image, string Name, int AssetId, bool IsDynamic)? ResolveResIdTexture(long resId)
|
public (RgbaImage Image, string Name, int AssetId, bool IsDynamic)? ResolveResIdTexture(long resId)
|
||||||
{
|
{
|
||||||
lock (_imageLock)
|
lock (_imageLock)
|
||||||
{
|
{
|
||||||
if (_movieFrames.TryGetValue(resId, out var movie))
|
if (_movieFrames.TryGetValue(resId, out var movie))
|
||||||
return (movie.Image, movie.Name, movie.RawIndex, true);
|
return (movie.Image, movie.Name, movie.AssetId, true);
|
||||||
// Movie payloads use the same .AGF extension as still images. While DirectShow is opening
|
// Movie payloads use the same .AGF extension as still images. While DirectShow is opening
|
||||||
// the graph (or before its first sample arrives), keep the already-created surface blank
|
// the graph (or before its first sample arrives), keep the already-created surface blank
|
||||||
// instead of falling through to AgfDecoder and misclassifying the MPEG program stream.
|
// instead of falling through to AgfDecoder and misclassifying the MPEG program stream.
|
||||||
if (_movieBySurface.Values.Contains(resId)) return null;
|
if (_movieBySurface.Values.Contains(resId)) return null;
|
||||||
}
|
}
|
||||||
var asset = _res.ResolveRawTexture(resId);
|
var asset = _res.ResolveTexture(resId);
|
||||||
var image = asset != null ? Decode(asset) : null;
|
var image = asset != null ? Decode(asset) : null;
|
||||||
return asset != null && image != null ? (image, asset.Name, asset.RawIndex, false) : null;
|
return asset != null && image != null ? (image, asset.Name, asset.PackedId, false) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public (RgbaImage Image, string Name, int AssetId, bool IsDynamic)? ResolveSurfaceTexture(
|
public (RgbaImage Image, string Name, int AssetId, bool IsDynamic)? ResolveSurfaceTexture(
|
||||||
@@ -898,7 +895,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
public long? PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask)
|
public long? PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask)
|
||||||
{
|
{
|
||||||
string scene = CurrentScene;
|
string scene = CurrentScene;
|
||||||
var asset = _res.Resolve(scene, resourceId);
|
var asset = _res.ResolveMovie(resourceId);
|
||||||
if (asset == null) { Godot.GD.Print($"movie unresolved {scene}:0x{resourceId:x}"); return null; }
|
if (asset == null) { Godot.GD.Print($"movie unresolved {scene}:0x{resourceId:x}"); return null; }
|
||||||
return StartMovie(asset, resourceId, surfaceSlot, movieFlags, syncMask, modal: false,
|
return StartMovie(asset, resourceId, surfaceSlot, movieFlags, syncMask, modal: false,
|
||||||
out long? stopTimeMs) ? stopTimeMs : null;
|
out long? stopTimeMs) ? stopTimeMs : null;
|
||||||
@@ -911,12 +908,12 @@ public sealed class GodotAdvHost : IHost
|
|||||||
&& !_completedMovies.Contains(resourceId);
|
&& !_completedMovies.Contains(resourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PlayModalMovieToSurface(long rawResourceId, int surfaceSlot, long movieFlags)
|
public void PlayModalMovieToSurface(long resourceId, int surfaceSlot, long movieFlags)
|
||||||
{
|
{
|
||||||
var asset = _res.ResolveRawMovie(rawResourceId);
|
var asset = _res.ResolveMovie(resourceId);
|
||||||
if (asset == null)
|
if (asset == null)
|
||||||
{
|
{
|
||||||
Godot.GD.Print($"modal movie unresolved raw:0x{rawResourceId:x}");
|
Godot.GD.Print($"modal movie unresolved packed:0x{resourceId:x}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -924,23 +921,23 @@ public sealed class GodotAdvHost : IHost
|
|||||||
_modalMovieWaiting = true;
|
_modalMovieWaiting = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!StartMovie(asset, rawResourceId, surfaceSlot, movieFlags, 0, modal: true,
|
if (!StartMovie(asset, resourceId, surfaceSlot, movieFlags, 0, modal: true,
|
||||||
out _)) return;
|
out _)) return;
|
||||||
_timeline?.State("modal-movie-wait", new()
|
_timeline?.State("modal-movie-wait", new()
|
||||||
{
|
{
|
||||||
["resource"] = rawResourceId, ["surface"] = surfaceSlot, ["file"] = asset.Name,
|
["resource"] = resourceId, ["surface"] = surfaceSlot, ["file"] = asset.Name,
|
||||||
});
|
});
|
||||||
while (!_stopping && !_modalMovieCancelled)
|
while (!_stopping && !_modalMovieCancelled)
|
||||||
{
|
{
|
||||||
lock (_imageLock)
|
lock (_imageLock)
|
||||||
if (_completedMovies.Contains(rawResourceId)) break;
|
if (_completedMovies.Contains(resourceId)) break;
|
||||||
_frameSignal.WaitOne(50);
|
_frameSignal.WaitOne(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancellation is a completed modal presentation from the script's perspective. The
|
// Cancellation is a completed modal presentation from the script's perspective. The
|
||||||
// wrapper's following surface-release opcode performs the ordinary decoder teardown.
|
// wrapper's following surface-release opcode performs the ordinary decoder teardown.
|
||||||
if (_modalMovieCancelled)
|
if (_modalMovieCancelled)
|
||||||
lock (_imageLock) _completedMovies.Add(rawResourceId);
|
lock (_imageLock) _completedMovies.Add(resourceId);
|
||||||
_timeline?.State("running", new()
|
_timeline?.State("running", new()
|
||||||
{
|
{
|
||||||
["modal_movie_complete"] = !_modalMovieCancelled,
|
["modal_movie_complete"] = !_modalMovieCancelled,
|
||||||
@@ -975,7 +972,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
["resource"] = resourceId, ["surface"] = surfaceSlot, ["file"] = movie.Name,
|
["resource"] = resourceId, ["surface"] = surfaceSlot, ["file"] = movie.Name,
|
||||||
["flags"] = movieFlags, ["sync_mask"] = syncMask, ["modal"] = modal,
|
["flags"] = movieFlags, ["sync_mask"] = syncMask, ["modal"] = modal,
|
||||||
});
|
});
|
||||||
return _main.TryPlayMovie(movie.Bytes, movie.Name, resourceId, asset.RawIndex, out stopTimeMs);
|
return _main.TryPlayMovie(movie.Bytes, movie.Name, resourceId, asset.PackedId, out stopTimeMs);
|
||||||
}
|
}
|
||||||
catch (System.Exception e)
|
catch (System.Exception e)
|
||||||
{
|
{
|
||||||
@@ -1079,9 +1076,9 @@ public sealed class GodotAdvHost : IHost
|
|||||||
|
|
||||||
// Main-thread decoder handoff. Replacing the newest frame mirrors the native texture renderer's
|
// Main-thread decoder handoff. Replacing the newest frame mirrors the native texture renderer's
|
||||||
// sample callback: the retained object keeps its surface binding while only the surface pixels change.
|
// sample callback: the retained object keeps its surface binding while only the surface pixels change.
|
||||||
public void PublishMovieFrame(long resourceId, string name, int rawIndex, RgbaImage frame)
|
public void PublishMovieFrame(long resourceId, string name, int assetId, RgbaImage frame)
|
||||||
{
|
{
|
||||||
lock (_imageLock) _movieFrames[resourceId] = (frame, name, rawIndex);
|
lock (_imageLock) _movieFrames[resourceId] = (frame, name, assetId);
|
||||||
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1105,20 +1102,19 @@ public sealed class GodotAdvHost : IHost
|
|||||||
{
|
{
|
||||||
lock (_imageLock)
|
lock (_imageLock)
|
||||||
{
|
{
|
||||||
if (_images.TryGetValue(asset.RawIndex, out var cached)) return cached;
|
if (_images.TryGetValue(asset.PackedId, out var cached)) return cached;
|
||||||
try { return _images[asset.RawIndex] = _res.DecodeTexture(asset); }
|
try { return _images[asset.PackedId] = _res.DecodeTexture(asset); }
|
||||||
catch (System.Exception e)
|
catch (System.Exception e)
|
||||||
{
|
{
|
||||||
Godot.GD.Print($"AGF decode failed {asset.Name}: {e.Message}");
|
Godot.GD.Print($"AGF decode failed {asset.Name}: {e.Message}");
|
||||||
_images[asset.RawIndex] = null;
|
_images[asset.PackedId] = null;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- audio ops (OGG plays natively in Godot) ----
|
// ---- audio ops (OGG plays natively in Godot) ----
|
||||||
// BGM: addressed by direct name (BGM{id:D3}.OGG), NOT the manifest. Voice: SC-section first,
|
// BGM is addressed by direct name (BGM{id:D3}.OGG); voice uses the universal packed catalog.
|
||||||
// then universal raw id for frontend scripts such as ROOM that do not own an SC section.
|
|
||||||
public void PlayBgm(long id)
|
public void PlayBgm(long id)
|
||||||
{
|
{
|
||||||
var asset = _res.ResolveBgm(id);
|
var asset = _res.ResolveBgm(id);
|
||||||
@@ -1131,7 +1127,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
|
|
||||||
public void PlayVoice(long id, int playbackVariant)
|
public void PlayVoice(long id, int playbackVariant)
|
||||||
{
|
{
|
||||||
var asset = _res.ResolveVoice(CurrentScene, id);
|
var asset = _res.ResolveVoice(id);
|
||||||
var audio = asset != null ? LoadAudio(asset) : null;
|
var audio = asset != null ? LoadAudio(asset) : null;
|
||||||
_timeline?.Event("voice", new() { ["id"] = id, ["file"] = audio?.Name,
|
_timeline?.Event("voice", new() { ["id"] = id, ["file"] = audio?.Name,
|
||||||
["playback_variant"] = playbackVariant });
|
["playback_variant"] = playbackVariant });
|
||||||
@@ -1172,7 +1168,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
|
|
||||||
public void ScheduleVoicePlayback(long id, int playbackVariant, long delayMs)
|
public void ScheduleVoicePlayback(long id, int playbackVariant, long delayMs)
|
||||||
{
|
{
|
||||||
var asset = _res.ResolveVoice(CurrentScene, id);
|
var asset = _res.ResolveVoice(id);
|
||||||
var audio = asset != null ? LoadAudio(asset) : null;
|
var audio = asset != null ? LoadAudio(asset) : null;
|
||||||
_timeline?.Event("voice-scheduled", new()
|
_timeline?.Event("voice-scheduled", new()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -252,16 +252,16 @@ public partial class Main : Godot.Control
|
|||||||
// Seed that inherited retained-surface state without replaying the entrypoint's unrelated UI flow.
|
// Seed that inherited retained-surface state without replaying the entrypoint's unrelated UI flow.
|
||||||
if (directSceneHarness && resources.ResolveName("SO001.AGF") is { } systemChrome)
|
if (directSceneHarness && resources.ResolveName("SO001.AGF") is { } systemChrome)
|
||||||
{
|
{
|
||||||
_host.SetTexture(systemChrome.RawIndex, 0x11);
|
_host.SetTexture(systemChrome.PackedId, 0x11);
|
||||||
_vm.Gfx.SetSurface(0x11, systemChrome.RawIndex, 0);
|
_vm.Gfx.SetSurface(0x11, systemChrome.PackedId, 0);
|
||||||
}
|
}
|
||||||
// SYSTEM4 also loads SO000 and configures op 0x73 before entering scene code. The Phase-A
|
// SYSTEM4 also loads SO000 and configures op 0x73 before entering scene code. The Phase-A
|
||||||
// single-scene harness does not replay those graphics side effects, so inject their exact state
|
// single-scene harness does not replay those graphics side effects, so inject their exact state
|
||||||
// alongside the existing SO001 bootstrap until Phase B runs the complete SYSTEM4 entrypoint.
|
// alongside the existing SO001 bootstrap until Phase B runs the complete SYSTEM4 entrypoint.
|
||||||
if (directSceneHarness && resources.ResolveName("SO000.AGF") is { } waitIndicator)
|
if (directSceneHarness && resources.ResolveName("SO000.AGF") is { } waitIndicator)
|
||||||
{
|
{
|
||||||
_host.SetTexture(waitIndicator.RawIndex, 0x0c);
|
_host.SetTexture(waitIndicator.PackedId, 0x0c);
|
||||||
_vm.Gfx.SetSurface(0x0c, waitIndicator.RawIndex, 0xff00);
|
_vm.Gfx.SetSurface(0x0c, waitIndicator.PackedId, 0xff00);
|
||||||
_host.ConfigureAdvWaitIndicator(new AdvWaitIndicatorConfig(
|
_host.ConfigureAdvWaitIndicator(new AdvWaitIndicatorConfig(
|
||||||
1, 385, 140, 0x0c, 0, 0, 30, 27, 12, 48));
|
1, 385, 140, 0x0c, 0, 0, 30, 27, 12, 48));
|
||||||
}
|
}
|
||||||
@@ -1174,14 +1174,14 @@ public partial class Main : Godot.Control
|
|||||||
CreateTween().TweenProperty(_bgm, "volume_db", targetDb, realDurationSeconds);
|
CreateTween().TweenProperty(_bgm, "volume_db", targetDb, realDurationSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryPlayMovie(byte[] mpegBytes, string assetName, long resourceId, int rawIndex,
|
public bool TryPlayMovie(byte[] mpegBytes, string assetName, long resourceId, int assetId,
|
||||||
out long? stopTimeMs)
|
out long? stopTimeMs)
|
||||||
{
|
{
|
||||||
stopTimeMs = null;
|
stopTimeMs = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var payload = new Age.Engine.Sys4.MoviePayload(assetName, mpegBytes);
|
var payload = new Age.Engine.Sys4.MoviePayload(assetName, mpegBytes);
|
||||||
var runtime = new MovieRuntime(assetName, rawIndex, new DirectShowMovieDecoder(payload));
|
var runtime = new MovieRuntime(assetName, assetId, new DirectShowMovieDecoder(payload));
|
||||||
stopTimeMs = runtime.Decoder.StopTimeMs;
|
stopTimeMs = runtime.Decoder.StopTimeMs;
|
||||||
while (!_pendingMovies.TryAdd(resourceId, runtime))
|
while (!_pendingMovies.TryAdd(resourceId, runtime))
|
||||||
if (_pendingMovies.TryRemove(resourceId, out var prior)) prior.Decoder.Dispose();
|
if (_pendingMovies.TryRemove(resourceId, out var prior)) prior.Decoder.Dispose();
|
||||||
@@ -1213,7 +1213,7 @@ public partial class Main : Godot.Control
|
|||||||
{
|
{
|
||||||
if (movie.Decoder.TryTakeFrame(out var frame))
|
if (movie.Decoder.TryTakeFrame(out var frame))
|
||||||
{
|
{
|
||||||
_host.PublishMovieFrame(resourceId, movie.Name, movie.RawIndex, frame);
|
_host.PublishMovieFrame(resourceId, movie.Name, movie.AssetId, frame);
|
||||||
if (_movieFrameSeen.Add(resourceId))
|
if (_movieFrameSeen.Add(resourceId))
|
||||||
GD.Print($"movie first frame {movie.Name}: {frame.Width}x{frame.Height} RGBA8 at render frame {_timelineFrame}");
|
GD.Print($"movie first frame {movie.Name}: {frame.Width}x{frame.Height} RGBA8 at render frame {_timelineFrame}");
|
||||||
}
|
}
|
||||||
@@ -1232,7 +1232,7 @@ public partial class Main : Godot.Control
|
|||||||
_movieFrameSeen.Remove(resourceId);
|
_movieFrameSeen.Remove(resourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record MovieRuntime(string Name, int RawIndex, DirectShowMovieDecoder Decoder);
|
private sealed record MovieRuntime(string Name, int AssetId, DirectShowMovieDecoder Decoder);
|
||||||
|
|
||||||
public void AppendLine(string text) => _text.Text += text + "\n";
|
public void AppendLine(string text) => _text.Text += text + "\n";
|
||||||
public void PageBreak()
|
public void PageBreak()
|
||||||
|
|||||||
@@ -5196,7 +5196,7 @@ source = "investigation"
|
|||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
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."
|
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. 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."
|
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.ResolveMovie selects through ResolvePacked and retains 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]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
@@ -6187,7 +6187,7 @@ source = "investigation"
|
|||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
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."
|
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."
|
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 type-checks the selected ResolvePacked record as MPEG without adding a scene base or fallback."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
|
|||||||
Reference in New Issue
Block a user