Implement ROOM fades and character voices

This commit is contained in:
gamer147
2026-07-21 00:47:29 -04:00
parent 2fa42b085a
commit cffc75c64d
18 changed files with 442 additions and 71 deletions

View File

@@ -77,6 +77,9 @@ public interface IHost
// read/message-skip branch reaches op 0x20c and presents the completed endpoint immediately.
void WaitForForegroundTransition(GfxState gfx) { }
void PresentFrame(GfxState gfx) { }
// Legacy SYS4 screen-transition family (op 0x25): scripts render two complete frames into
// numbered surfaces, then block while the engine alpha-composites target over source.
void CrossfadeSurfaces(GfxState gfx, int sourceSurface, int targetSurface, long intervalArgument) { }
void CreateTexture(int slot, int width, int height);
void SetTexture(long resourceId, int slot);
void ReleaseSurface(int slot) { }

View File

@@ -0,0 +1,13 @@
namespace Age.Engine.Model;
/// <summary>Native timing conversion shared by the legacy op-0x25 screen-transition family.</summary>
public static class LegacyScreenTransitionTiming
{
public static long DurationMilliseconds(long argument)
{
long interval = argument <= 64 ? argument : argument / 16;
long step = argument <= 64 ? 16 : 1;
interval = System.Math.Max(1, interval);
return System.Math.Clamp(((256 + step - 1) / step) * interval, 1, 60_000);
}
}

View File

@@ -33,6 +33,14 @@ public sealed class ResourceMap
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)
{
var entry = _catalog.ResolveScene(scene, resId) ?? _catalog.ResolveRaw(resId);
return entry is { IsPlaceholder: false } && IsAudio(entry) ? entry : null;
}
/// <summary>Resolve an already-normalized raw catalog id without applying a scene section base.</summary>
public AssetEntry? ResolveRawTexture(long rawId)
{

View File

@@ -649,7 +649,26 @@ public sealed class VirtualMachine
Write(a[0], Read(a[0]) & ~(1L << (int)bit)); return pc + 1;
}
case "check-bit": Write(a[0], (Read(a[1]) >> (int)(Read(a[2]) & 31)) & 1); return pc + 1;
case "copy-to-global": Write(a[0], Read(a[1])); return pc + 1;
case "zero-int-range":
case "copy-to-global": // pre-reference compatibility for opcode 0x6c
{
int count = System.Math.Max(0, checked((int)Read(a[1])));
for (int i = 0; i < count; i++) WriteConsecutive(a[0], i, 0);
return pc + 1;
}
case "random-modulo": // 0x60: native CRT rand() % bound
case "u0041A270":
{
long bound = Read(a[1]);
if (bound == 0)
{
Write(a[0], 0);
HaltReason ??= "random-modulo-zero";
return HALT;
}
Write(a[0], System.Random.Shared.Next(0x8000) % bound);
return pc + 1;
}
case "jmp": return _cur.Script.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1);
case "call": _cur.CallStack.Add(pc + 1); return _cur.Script.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1);
case "ret":
@@ -1301,6 +1320,10 @@ public sealed class VirtualMachine
Read(a[4]), (int)Read(a[5]), Read(a[6]), Read(a[7])); return pc + 1;
case "present-frame": // 0x20c: read/message-skip path snaps a queued transition to its endpoint
_host.PresentFrame(Gfx); return pc + 1;
case "crossfade-surfaces": // 0x25: legacy full-frame surface alpha transition
case "u00418B40":
_host.CrossfadeSurfaces(Gfx, (int)Read(a[0]), (int)Read(a[1]), Read(a[2]));
return pc + 1;
case "mark-frame-yield": // 0x21c: normal foreground-transition scheduler/resume boundary
_host.WaitForForegroundTransition(Gfx); return pc + 1;
case "clear-gfx-command-queue": // 0x224: retained compositor does not use this native queue