Use universal packed resource resolution

This commit is contained in:
gamer147
2026-07-21 20:23:32 -04:00
parent 5f816bb438
commit 1a2ea008c6
19 changed files with 161 additions and 154 deletions

View File

@@ -43,7 +43,7 @@ if (args[0] == "audio")
var sceneName = args[1];
var sceneKey = Path.GetFileNameWithoutExtension(sceneName).ToUpperInvariant();
var res = ResourceMap.Load();
var host = new AudioTraceHost(res, sceneKey);
var host = new AudioTraceHost(res);
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
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 sceneKey = Path.GetFileNameWithoutExtension(sceneName).ToUpperInvariant();
var res = ResourceMap.Load();
var host = new GfxTraceHost(res, sceneKey);
var host = new GfxTraceHost(res);
var session = new GameSession();
foreach (var s in args.Where(a => a.Contains('=')))
{
@@ -100,7 +100,7 @@ if (args[0] == "gfx")
var vis = vm.Gfx.SnapshotVisibleObjects();
Console.WriteLine($" visible objects ({vis.Count}, ascending-handle = z-order):");
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;
}
@@ -345,17 +345,16 @@ sealed class TraceSetup : IDisposable
sealed class AudioTraceHost : IHost
{
private readonly ResourceMap _res;
private readonly string _scene;
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
{
var entry = _res.ResolveBgm(id);
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}"));
}
public void ShowText(int offset, string text) { }
@@ -371,12 +370,11 @@ sealed class AudioTraceHost : IHost
sealed class GfxTraceHost : IHost
{
private readonly ResourceMap _res;
private readonly string _scene;
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.
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
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)
{
@@ -387,7 +385,7 @@ sealed class GfxTraceHost : IHost
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;
_slotAsset[slot] = e?.Name;
_slotDims[slot] = image != null ? (image.Width, image.Height) : (0, 0);

View File

@@ -75,7 +75,7 @@ public class AgfDecoderTests
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
var store = new Sys4AssetStore(catalog, Paths.GameDir, Paths.GameDir);
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)!);
Assert.Equal((800, 300), (image.Width, image.Height));
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)
{
var resources = ResourceMap.Load();
var asset = resources.ResolveRawTexture(rawId);
var asset = resources.ResolveTexture(rawId);
Assert.NotNull(asset);
Assert.Equal(name, asset.Name);
var image = resources.DecodeTexture(asset);

View File

@@ -15,7 +15,6 @@ public class CallScriptTests
{
public virtual void EnterScriptContext(string scriptName) { }
public virtual void ExitScriptContext() { }
public virtual long ResolveTextureResourceId(long resourceId) => resourceId;
public void ShowText(int o, string t) { }
public void WaitForInput() { }
public void Sleep(long duration) { }
@@ -52,9 +51,6 @@ public class CallScriptTests
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));
}
@@ -133,7 +129,7 @@ public class CallScriptTests
}
[Fact]
public void ScriptLocalTextureIdsFollowTheActiveNestedFrame()
public void PackedTextureIdsRemainUnchangedAcrossNestedFrames()
{
var t = Table();
var callee = Asm(t, "CALLEE",
@@ -149,7 +145,7 @@ public class CallScriptTests
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);
}
}

View File

@@ -126,7 +126,7 @@ public class GfxCommandBufferTests
}
[Fact]
public void RawTextureLoadBypassesSceneResourceNormalizationAndFeedsRetainedDraws()
public void ModeOneTextureLoadPreservesPackedIdAndFeedsRetainedDraws()
{
var t = T();
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) }),
Exit(),
}, System.Array.Empty<string>());
var host = new RecordingHost { TextureResourceIdOffset = 0x1000 };
var host = new RecordingHost();
var vm = new VirtualMachine(scene, t, host);
vm.Run();

View File

@@ -342,7 +342,7 @@ public class HistoryInteractionOpsTests
Assert.Equal(new[] { (expectedVoice, checked((int)expectedVariant)) }, host.VoiceRequests);
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.NotEmpty(resources.ReadAudio(voice).Bytes);
}

View File

@@ -196,7 +196,7 @@ public class MovieOpcodeTests
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
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);
var movie = resources.ReadMovie(entry!);
@@ -207,11 +207,11 @@ public class MovieOpcodeTests
[Theory]
[InlineData(0x335f, "LOGO.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 resources = new ResourceMap(catalog, new Sys4AssetStore(catalog, Paths.GameDir));
var entry = resources.ResolveRawMovie(rawId);
var entry = resources.ResolveMovie(resourceId);
Assert.Equal(expectedName, entry?.Name);
var movie = resources.ReadMovie(entry!);
@@ -224,7 +224,7 @@ public class MovieOpcodeTests
if (!OperatingSystem.IsWindows()) return;
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
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));
Assert.True(decoder.StopTimeMs > 0, "DirectShow should expose a positive IMediaPosition stop time");

View File

@@ -22,6 +22,7 @@ public class Sys4AssetStoreTests
Assert.All(append.Files, entry =>
{
Assert.Equal(1, entry.PackId);
Assert.Equal(0x01000000 | entry.RawIndex, entry.PackedId);
Assert.StartsWith("$1$", entry.Name);
Assert.Equal("APPEND01.ALF", entry.Archive);
});
@@ -178,14 +179,14 @@ public class Sys4AssetStoreTests
Assert.Equal("BGM005.OGG", bgm?.Name);
AssertOgg(resources.ReadAudio(bgm!));
var voice = resources.ResolveVoice("SC0000", 0x24);
var voice = resources.ResolveVoice(0x24);
Assert.Equal("MAN999.OGG", voice?.Name);
AssertOgg(resources.ReadAudio(voice!));
var roomVoice = resources.ResolveVoice("ROOM", 0x3365);
var roomVoice = resources.ResolveVoice(0x3365);
Assert.Equal("EUA0016.OGG", roomVoice?.Name);
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);
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("WAVE", Encoding.ASCII.GetString(wav.Bytes, 8, 4));
Assert.Null(resources.Resolve("TITLE", 0x2aea));
Assert.Equal("SE020.WAV", resources.ResolveSoundEffect(0x2aea)?.Name);
Assert.Equal("SE013.WAV", resources.ResolveSoundEffect(0x2aeb)?.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")!));
}
[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]
public void AllInstalledLooseScriptOverridesShadowArchiveCopies()
{

View File

@@ -55,9 +55,7 @@ internal class RecordingHost : IHost
public readonly List<bool> AdvPagePresentationSuspended = new();
public int CursorClearCount;
public int SceneContextResets;
public long TextureResourceIdOffset;
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 SetAdvTextCursor(int layoutSlot, int x, int y) => TextCursors.Add((layoutSlot, x, y));
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text)
@@ -162,8 +160,8 @@ internal class RecordingHost : IHost
return MovieStopTimeMs;
}
public bool IsMovieSurfaceActive(int surfaceSlot) => ActiveMovieSurfaces.Contains(surfaceSlot);
public void PlayModalMovieToSurface(long rawResourceId, int surfaceSlot, long movieFlags)
=> ModalMovies.Add((rawResourceId, surfaceSlot, movieFlags));
public void PlayModalMovieToSurface(long resourceId, int surfaceSlot, long movieFlags)
=> ModalMovies.Add((resourceId, surfaceSlot, movieFlags));
}
internal sealed class MapProvider : IScriptProvider

View File

@@ -21,11 +21,9 @@ public interface IHost
{
/// <summary>Report a recoverable runtime discrepancy while allowing script execution to continue.</summary>
void ReportWarning(string message) => System.Console.Error.WriteLine(message);
// Script-local resource ids resolve against the currently executing frame's SYS4INI section.
// Interactive hosts track this stack; headless hosts may keep the no-op/default identity behavior.
// Script context is retained for diagnostics/page location; resource operands are universal packed ids.
void EnterScriptContext(string scriptName) { }
void ExitScriptContext() { }
long ResolveTextureResourceId(long resourceId) => resourceId;
void ShowText(int offset, string text);
// 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.
@@ -125,8 +123,8 @@ public interface IHost
/// immediately after 0x236 returns.</returns>
long? PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask) => null;
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
// 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) { }
}

View File

@@ -1,8 +1,8 @@
namespace Age.Engine.Sys4;
/// <summary>
/// Compatibility facade over the runtime SYS4 catalog. Scene-local graphics/voice/movie ids resolve
/// through the executing script's manifest; BGM uses direct names; SFX/cursors use universal packed ids.
/// Typed facade over the runtime SYS4 catalog. Graphics, voice, movie, SFX, and cursor operands use
/// universal packed ids; BGM uses direct names.
/// See docs/asset-resolution-re.md.
/// </summary>
public sealed class ResourceMap
@@ -18,43 +18,26 @@ public sealed class ResourceMap
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>
public AssetEntry? Resolve(string scene, long resId)
/// <summary>Resolve a universal packed SYS4INI/AAI id to an AGF texture record.</summary>
public AssetEntry? ResolveTexture(long resourceId)
{
return _catalog.ResolveScene(scene, resId);
}
/// <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);
var entry = _catalog.ResolvePacked(resourceId);
return entry is { IsPlaceholder: false } &&
entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase) ? entry : null;
}
/// <summary>Resolve voice audio through the active SC section when one exists, then through the
/// universal raw catalog used by non-SC frontend scripts such as ROOM.</summary>
public AssetEntry? ResolveVoice(string scene, long resId)
/// <summary>Resolve a universal packed SYS4INI/AAI id to a voice audio record.</summary>
public AssetEntry? ResolveVoice(long resourceId)
{
var entry = _catalog.ResolveScene(scene, resId) ?? _catalog.ResolveRaw(resId);
var entry = _catalog.ResolvePacked(resourceId);
return entry is { IsPlaceholder: false } && IsAudio(entry) ? entry : null;
}
/// <summary>Resolve an already-normalized packed catalog id without applying a scene section base.</summary>
public AssetEntry? ResolveRawTexture(long rawId)
/// <summary>Resolve a universal packed SYS4INI/AAI id to an AGF-named movie record. ReadMovie
/// validates the MPEG signature because still images use the same extension.</summary>
public AssetEntry? ResolveMovie(long resourceId)
{
var entry = _catalog.ResolvePacked(rawId);
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);
var entry = _catalog.ResolvePacked(resourceId);
return entry is { IsPlaceholder: false } &&
entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase) ? entry : null;
}
@@ -81,7 +64,7 @@ public sealed class ResourceMap
/// <summary>
/// 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),
/// which the manifest mis-resolved to a graphics entry. See docs/asset-resolution-re.md.
/// </summary>

View File

@@ -14,7 +14,11 @@ public sealed record AssetEntry(
int ArchiveId = -1,
int FileNumber = -1,
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>
public sealed record PackedAssetEntry(long PackedId, AssetEntry Asset);

View File

@@ -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;
case "set-texture": // 0x1f9 (resId)(slot)(colorkey) — load a file into the slot's surface
{
long requestedResourceId = Read(a[0]);
long resolvedResourceId = _host.ResolveTextureResourceId(requestedResourceId);
long resourceId = Read(a[0]);
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}]" : "")}");
_host.ReleaseSurface((int)Read(a[1]));
long colorKey = a.Count > 2 ? Read(a[2]) : -1;
Gfx.SetSurface((int)Read(a[1]), resolvedResourceId, colorKey);
_host.SetTexture(resolvedResourceId, (int)Read(a[1]), colorKey);
Gfx.SetSurface((int)Read(a[1]), resourceId, colorKey);
_host.SetTexture(resourceId, (int)Read(a[1]), colorKey);
return pc + 1; // host still tracks dims for get-texture-size
}
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
// surface subclass and receives an already-global SYS4INI catalog index. The
// CPU compositor does not need the D3D subclass distinction; it does need the
// resource id to bypass the executing script's scene-section normalization.
long rawResourceId = Read(a[0]);
// surface subclass. Both texture opcodes receive the same universal packed id;
// the CPU compositor does not need the D3D subclass distinction.
long resourceId = Read(a[0]);
int surfaceSlot = (int)Read(a[1]);
_host.ReleaseSurface(surfaceSlot);
Gfx.SetSurface(surfaceSlot, rawResourceId, Read(a[2]));
_host.SetTexture(rawResourceId, surfaceSlot, Read(a[2]));
Gfx.SetSurface(surfaceSlot, resourceId, Read(a[2]));
_host.SetTexture(resourceId, surfaceSlot, Read(a[2]));
return pc + 1;
}
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)
Write(a[0], _initialRootRun ? 1 : 0);
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]);
// The modal and scene-local paths share retained-surface composition. The host's
// distinct entry point preserves 0x20f's raw-id resolver and blocking lifecycle.
Gfx.SetSurface(surfaceSlot, rawResourceId, 0);
_host.PlayModalMovieToSurface(rawResourceId, surfaceSlot, Read(a[2]));
// Modal and non-modal paths share packed resolution and retained-surface composition.
// The distinct host entry point owns only 0x20f's blocking lifecycle.
Gfx.SetSurface(surfaceSlot, resourceId, 0);
_host.PlayModalMovieToSurface(resourceId, surfaceSlot, Read(a[2]));
return pc + 1;
}
case "u004221A0": // pre-reference compatibility