Implement DEBUGMAP combat frontier
This commit is contained in:
127
engine/Age.Engine.Tests/BattleFrontierOpsTests.cs
Normal file
127
engine/Age.Engine.Tests/BattleFrontierOpsTests.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Vm;
|
||||
using Xunit;
|
||||
|
||||
public class BattleFrontierOpsTests
|
||||
{
|
||||
private const int Immediate = 0, InlineString = 2, GlobalInt = 3, GlobalString = 5,
|
||||
LocalString = 11;
|
||||
|
||||
private static OpcodeTable Table() => OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
private static Operand I(long value) => new(Immediate, value);
|
||||
private static Operand G(int address) => new(GlobalInt, address);
|
||||
|
||||
[Fact]
|
||||
public void SurfaceRectCopy_ForwardsTheCompleteBlitRequest()
|
||||
{
|
||||
var table = Table();
|
||||
var script = ScriptAssembler.Assemble(table, "SURFACE_COPY", new List<(int, Operand[])>
|
||||
{
|
||||
(0x207, new[] { I(72), I(67), I(12), I(15), I(3), I(4), I(30), I(33) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var host = new RecordingHost();
|
||||
|
||||
new VirtualMachine(script, table, host).Run();
|
||||
|
||||
Assert.Equal(new SurfaceRectCopy(72, 67, 12, 15, 3, 4, 30, 33),
|
||||
Assert.Single(host.SurfaceCopies));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScalarBattleHelpers_PreserveNativeSignedBehaviorAndStringAliasing()
|
||||
{
|
||||
var table = Table();
|
||||
var script = ScriptAssembler.Assemble(table, "BATTLE_SCALARS", new List<(int, Operand[])>
|
||||
{
|
||||
(0x191, new[] { G(0x100), I(int.MinValue) }),
|
||||
(0x193, new[] { new Operand(GlobalString, 0x500), new Operand(GlobalString, 0x500),
|
||||
new Operand(InlineString, 0) }),
|
||||
(0x1c8, new[] { new Operand(LocalString, 0), I(-42) }),
|
||||
(0x55, new[] { new Operand(GlobalString, 0x501), new Operand(LocalString, 0) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, new[] { "/99" });
|
||||
var vm = new VirtualMachine(script, table, new RecordingHost());
|
||||
vm.GlobalStrings[0x500] = "17";
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(int.MinValue, vm.Globals[0x100]);
|
||||
Assert.Equal("17/99", vm.GlobalStrings[0x500]);
|
||||
Assert.Equal("-42", vm.GlobalStrings[0x501]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MonotonicAndFrameTimeOps_StoreNativeLowDwordSamples()
|
||||
{
|
||||
var table = Table();
|
||||
var host = new SequencedClockHost(0x1_0000_0005, 100, 116);
|
||||
var script = ScriptAssembler.Assemble(table, "BATTLE_CLOCK", new List<(int, Operand[])>
|
||||
{
|
||||
(0xd0, new[] { G(0x100) }),
|
||||
(0x23c, Array.Empty<Operand>()),
|
||||
(0x23c, Array.Empty<Operand>()),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var vm = new VirtualMachine(script, table, host);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(5, vm.Globals[0x100]);
|
||||
Assert.Equal(100u, vm.Gfx.PreviousFrameTimeMilliseconds);
|
||||
Assert.Equal(116u, vm.Gfx.CurrentFrameTimeMilliseconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MovieActivityAndAnimationServiceFlags_ControlBattlePollingAndReset()
|
||||
{
|
||||
var table = Table();
|
||||
var host = new RecordingHost();
|
||||
host.ActiveMovieSurfaces.Add(7);
|
||||
var script = ScriptAssembler.Assemble(table, "BATTLE_CONTROL", new List<(int, Operand[])>
|
||||
{
|
||||
(0x23a, new[] { G(0x100), I(7) }),
|
||||
(0x23a, new[] { G(0x101), I(8) }),
|
||||
(0x238, new[] { I(400) }),
|
||||
(0x24e, new[] { I(2) }),
|
||||
(0x243, Array.Empty<Operand>()),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var vm = new VirtualMachine(script, table, host);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(1, vm.Globals[0x100]);
|
||||
Assert.Equal(0, vm.Globals[0x101]);
|
||||
Assert.Equal(400, vm.Gfx.AnimClockDurationTicks);
|
||||
Assert.Equal(1, vm.Gfx.AnimClockGeneration);
|
||||
Assert.Equal(2, vm.Gfx.AnimationServiceFlags);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DelayedCombatVoice_ForwardsAllSchedulingOperands()
|
||||
{
|
||||
var table = Table();
|
||||
var host = new RecordingHost();
|
||||
var script = ScriptAssembler.Assemble(table, "BATTLE_VOICE", new List<(int, Operand[])>
|
||||
{
|
||||
(0x2c0, new[] { I(123), I(0), I(275) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
|
||||
new VirtualMachine(script, table, host).Run();
|
||||
|
||||
Assert.Equal((123L, 0, 275L), Assert.Single(host.ScheduledVoiceRequests));
|
||||
}
|
||||
|
||||
private sealed class SequencedClockHost : RecordingHost
|
||||
{
|
||||
private readonly Queue<long> _samples;
|
||||
public SequencedClockHost(params long[] samples) => _samples = new Queue<long>(samples);
|
||||
public override long InputClockMilliseconds => _samples.Dequeue();
|
||||
}
|
||||
}
|
||||
188
engine/Age.Engine.Tests/IntegerQueueOpsTests.cs
Normal file
188
engine/Age.Engine.Tests/IntegerQueueOpsTests.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
using System.Collections.Generic;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Vm;
|
||||
using Xunit;
|
||||
|
||||
public class IntegerQueueOpsTests
|
||||
{
|
||||
private const int Immediate = 0, GlobalInt = 3, LocalInt = 9;
|
||||
|
||||
private static Operand I(long value) => new(Immediate, value);
|
||||
private static Operand G(int address) => new(GlobalInt, address);
|
||||
private static Operand L(int address) => new(LocalInt, address);
|
||||
|
||||
[Fact]
|
||||
public void IntegerQueues_AreIndependentFifosAndResetDiscardsPendingValues()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var script = ScriptAssembler.Assemble(table, "INT_QUEUE", new List<(int, Operand[])>
|
||||
{
|
||||
(0x132, new[] { I(0) }),
|
||||
(0x132, new[] { I(1) }),
|
||||
(0x133, new[] { I(0), I(11) }),
|
||||
(0x133, new[] { I(0), I(-22) }),
|
||||
(0x133, new[] { I(1), I(33) }),
|
||||
|
||||
(0x134, new[] { I(0), L(0), L(1) }),
|
||||
(0x55, new[] { G(0x100), L(0) }),
|
||||
(0x55, new[] { G(0x101), L(1) }),
|
||||
(0x134, new[] { I(0), L(0), L(1) }),
|
||||
(0x55, new[] { G(0x102), L(0) }),
|
||||
(0x55, new[] { G(0x103), L(1) }),
|
||||
(0x134, new[] { I(1), L(0), L(1) }),
|
||||
(0x55, new[] { G(0x104), L(0) }),
|
||||
(0x55, new[] { G(0x105), L(1) }),
|
||||
|
||||
(0x55, new[] { L(1), I(777) }),
|
||||
(0x134, new[] { I(0), L(0), L(1) }),
|
||||
(0x55, new[] { G(0x106), L(0) }),
|
||||
(0x55, new[] { G(0x107), L(1) }),
|
||||
(0x133, new[] { I(1), I(44) }),
|
||||
(0x132, new[] { I(1) }),
|
||||
(0x134, new[] { I(1), L(0), L(1) }),
|
||||
(0x55, new[] { G(0x108), L(0) }),
|
||||
(0x2, System.Array.Empty<Operand>()),
|
||||
}, System.Array.Empty<string>());
|
||||
var vm = new VirtualMachine(script, table, new RecordingHost());
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(1, vm.Globals[0x100]);
|
||||
Assert.Equal(11, vm.Globals[0x101]);
|
||||
Assert.Equal(1, vm.Globals[0x102]);
|
||||
Assert.Equal(-22, vm.Globals[0x103]);
|
||||
Assert.Equal(1, vm.Globals[0x104]);
|
||||
Assert.Equal(33, vm.Globals[0x105]);
|
||||
Assert.Equal(0, vm.Globals[0x106]);
|
||||
Assert.Equal(777, vm.Globals[0x107]);
|
||||
Assert.Equal(0, vm.Globals[0x108]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreloadedScriptSlot_RestartsCodeAndRetainsItsLocalBankAcrossCalls()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var root = ScriptAssembler.Assemble(table, "PRELOADED_ROOT", new List<(int, Operand[])>
|
||||
{
|
||||
(0x6, new[] { I(0x500), I(0x1f) }),
|
||||
(0x8, new[] { I(0x1f) }),
|
||||
(0x8, new[] { I(0x1f) }),
|
||||
(0x2, System.Array.Empty<Operand>()),
|
||||
}, System.Array.Empty<string>());
|
||||
var worker = ScriptAssembler.Assemble(table, "PRELOADED_WORKER", new List<(int, Operand[])>
|
||||
{
|
||||
(0x50, new[] { L(0), L(0), I(1) }),
|
||||
(0x55, new[] { G(0x120), L(0) }),
|
||||
(0x2, System.Array.Empty<Operand>()),
|
||||
}, System.Array.Empty<string>());
|
||||
var vm = new VirtualMachine(root, table, new RecordingHost(),
|
||||
provider: new MapProvider(new Dictionary<long, Script> { [0x500] = worker }));
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal("exit", vm.HaltReason);
|
||||
Assert.Equal(2, vm.Globals[0x120]);
|
||||
Assert.Equal(2, vm.CallScriptDispatches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisteredRealMvseek_ExpandsMovementCostsThroughTheSystem4ServiceAbi()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var mvseek = Sys4Loader.Load(Paths.Scripts()["MVSEEK.BIN"], table);
|
||||
var root = ScriptAssembler.Assemble(table, "SYSTEM4_SERVICE_BRIDGE", new List<(int, Operand[])>
|
||||
{
|
||||
(0x6, new[] { I(0x3381), I(0x1f) }),
|
||||
(0x8, new[] { I(0x1f) }),
|
||||
(0x2, System.Array.Empty<Operand>()),
|
||||
}, System.Array.Empty<string>());
|
||||
var vm = new VirtualMachine(root, table, new RecordingHost(),
|
||||
provider: new MapProvider(new Dictionary<long, Script> { [0x3381] = mvseek }));
|
||||
SeedSearchGrid(vm, movementPoints: 3);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal("exit", vm.HaltReason);
|
||||
Assert.Equal(3, vm.Globals[MovementCell(6, 5)]);
|
||||
Assert.Equal(3, vm.Globals[MovementCell(4, 5)]);
|
||||
Assert.Equal(3, vm.Globals[MovementCell(5, 6)]);
|
||||
Assert.Equal(3, vm.Globals[MovementCell(5, 4)]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RealMvseek_ExpandsMovementCostsBeyondTheOrigin()
|
||||
{
|
||||
var vm = RealSearchVm("MVSEEK.BIN");
|
||||
SeedSearchGrid(vm, movementPoints: 3);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal("exit", vm.HaltReason);
|
||||
Assert.Equal(3, vm.Globals[MovementCell(6, 5)]);
|
||||
Assert.Equal(3, vm.Globals[MovementCell(4, 5)]);
|
||||
Assert.Equal(3, vm.Globals[MovementCell(5, 6)]);
|
||||
Assert.Equal(3, vm.Globals[MovementCell(5, 4)]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RealAtseek_ExpandsAttackDistancesBeyondTheOrigin()
|
||||
{
|
||||
var vm = RealSearchVm("ATSEEK.BIN");
|
||||
SeedSearchGrid(vm, movementPoints: 0);
|
||||
vm.Globals[0xcc9f3] = 2;
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal("exit", vm.HaltReason);
|
||||
Assert.Equal(1, vm.Globals[AttackCell(6, 5)]);
|
||||
Assert.Equal(1, vm.Globals[AttackCell(4, 5)]);
|
||||
Assert.Equal(1, vm.Globals[AttackCell(5, 6)]);
|
||||
Assert.Equal(1, vm.Globals[AttackCell(5, 4)]);
|
||||
}
|
||||
|
||||
private static VirtualMachine RealSearchVm(string name)
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var script = Sys4Loader.Load(Paths.Scripts()[name], table);
|
||||
return new VirtualMachine(script, table, new RecordingHost());
|
||||
}
|
||||
|
||||
private static void SeedSearchGrid(VirtualMachine vm, int movementPoints)
|
||||
{
|
||||
const int entity = 0;
|
||||
const int x = 5, y = 5;
|
||||
vm.Globals[0x66713] = entity;
|
||||
vm.Globals[0x5231f + entity] = x;
|
||||
vm.Globals[0x52351 + entity] = y;
|
||||
vm.Globals[0x4e11b + entity * 14 + 10] = movementPoints;
|
||||
|
||||
vm.Globals[0xccbcf + 1] = 1;
|
||||
vm.Globals[0xccbcf + 2] = -1;
|
||||
vm.Globals[0xccbcf + 3] = 0;
|
||||
vm.Globals[0xccbcf + 4] = 0;
|
||||
vm.Globals[0xccbd4 + 1] = 0;
|
||||
vm.Globals[0xccbd4 + 2] = 0;
|
||||
vm.Globals[0xccbd4 + 3] = 1;
|
||||
vm.Globals[0xccbd4 + 4] = -1;
|
||||
|
||||
// Terrain id 1 is passable. MVSEEK checks destination centers; ATSEEK checks the
|
||||
// fine-grid edge between the origin and each logical neighbor.
|
||||
vm.Globals[0xe6ae0 + 1] = 1;
|
||||
SetTerrain(vm, x * 2, y * 2);
|
||||
SetTerrain(vm, (x + 1) * 2, y * 2);
|
||||
SetTerrain(vm, (x - 1) * 2, y * 2);
|
||||
SetTerrain(vm, x * 2, (y + 1) * 2);
|
||||
SetTerrain(vm, x * 2, (y - 1) * 2);
|
||||
SetTerrain(vm, x * 2 + 1, y * 2);
|
||||
SetTerrain(vm, x * 2 - 1, y * 2);
|
||||
SetTerrain(vm, x * 2, y * 2 + 1);
|
||||
SetTerrain(vm, x * 2, y * 2 - 1);
|
||||
}
|
||||
|
||||
private static void SetTerrain(VirtualMachine vm, int x, int y)
|
||||
=> vm.Globals[0x341ab + y * 53 + x] = 1;
|
||||
|
||||
private static int MovementCell(int x, int y) => 0xaba96 + y * 27 + x;
|
||||
private static int AttackCell(int x, int y) => 0xb8d86 + y * 27 + x;
|
||||
}
|
||||
@@ -93,6 +93,25 @@ public class RenderObjectBlendTests
|
||||
Assert.False(rendered.MultiplyTint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreatedSurfaceMode0_UsesPackedAlphaAsOpacityAndRgbAsModulation()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.CreateSurface(3);
|
||||
g.BindDraw(0x100, 3, 0, 0, 10, 10, 0, 0);
|
||||
g.SetStaticObjectColorResolved(0x100, 0, 0x40, 0x102030);
|
||||
|
||||
var rendered = g.SnapshotVisibleObjects().Single();
|
||||
|
||||
Assert.Equal(0, rendered.SurfaceResId);
|
||||
Assert.Equal(-1, rendered.ColorKey);
|
||||
Assert.Equal(0x40, rendered.Alpha);
|
||||
Assert.Equal(0, rendered.TintStrength);
|
||||
Assert.Equal(0x102030, rendered.Tint);
|
||||
Assert.True(rendered.MultiplyTint);
|
||||
Assert.Equal(BlendKind.Alpha, rendered.Blend);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mode1_UsesAdditiveBlendWithArgbSourceScaleAndRgbModulation()
|
||||
{
|
||||
|
||||
80
engine/Age.Engine.Tests/RgbaSurfaceOpsTests.cs
Normal file
80
engine/Age.Engine.Tests/RgbaSurfaceOpsTests.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Model;
|
||||
using Xunit;
|
||||
|
||||
public class RgbaSurfaceOpsTests
|
||||
{
|
||||
[Fact]
|
||||
public void FillRect_ReplacesClippedPixelsIncludingAlpha()
|
||||
{
|
||||
var surface = Row(1, 2, 3, 4);
|
||||
|
||||
Assert.True(RgbaSurfaceOps.FillRect(surface, -1, 0, 3, 1, 0xd0, 0x112233));
|
||||
|
||||
Assert.Equal(new byte[] { 0x11, 0x22, 0x33, 0xd0 }, surface.Pixels[..4]);
|
||||
Assert.Equal(new byte[] { 0x11, 0x22, 0x33, 0xd0 }, surface.Pixels[4..8]);
|
||||
Assert.Equal(3, Values(surface)[2]);
|
||||
Assert.Equal(4, Values(surface)[3]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeneratedSurfaceBinding_DefaultsToNoColorKey()
|
||||
{
|
||||
var gfx = new GfxState();
|
||||
gfx.CreateSurface(67);
|
||||
gfx.BindDraw(1, 67, 0, 0, 4, 1, 0, 0);
|
||||
|
||||
Assert.Equal(-1, Assert.Single(gfx.SnapshotVisibleObjects()).ColorKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyRect_ClipsSourceAndDestinationAsOnePairedRectangle()
|
||||
{
|
||||
var source = Row(10, 20, 30, 40);
|
||||
var destination = Row(1, 2, 3, 4);
|
||||
|
||||
Assert.True(RgbaSurfaceOps.CopyRect(source, destination, -1, 0, 4, 1, 1, 0));
|
||||
|
||||
Assert.Equal(new byte[] { 1, 2, 10, 20 }, Values(destination));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyRect_UsesStableSourcePixelsForOverlappingSelfCopy()
|
||||
{
|
||||
var surface = Row(10, 20, 30, 40);
|
||||
|
||||
Assert.True(RgbaSurfaceOps.CopyRect(surface, surface, 0, 0, 3, 1, 1, 0));
|
||||
|
||||
Assert.Equal(new byte[] { 10, 10, 20, 30 }, Values(surface));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithColorKey_MakesMatchingSourcePixelsTransparentBeforeComposition()
|
||||
{
|
||||
var source = new RgbaImage(2, 1, new byte[] { 1, 2, 3, 255, 4, 5, 6, 255 });
|
||||
|
||||
var keyed = RgbaSurfaceOps.WithColorKey(source, 0x010203);
|
||||
|
||||
Assert.Equal(0, keyed.Pixels[3]);
|
||||
Assert.Equal(255, keyed.Pixels[7]);
|
||||
Assert.Equal(255, source.Pixels[3]);
|
||||
}
|
||||
|
||||
private static RgbaImage Row(params byte[] values)
|
||||
{
|
||||
var pixels = new byte[values.Length * 4];
|
||||
for (int index = 0; index < values.Length; index++)
|
||||
{
|
||||
pixels[index * 4] = values[index];
|
||||
pixels[index * 4 + 3] = 255;
|
||||
}
|
||||
return new RgbaImage(values.Length, 1, pixels);
|
||||
}
|
||||
|
||||
private static byte[] Values(RgbaImage image)
|
||||
{
|
||||
var values = new byte[image.Width];
|
||||
for (int index = 0; index < values.Length; index++) values[index] = image.Pixels[index * 4];
|
||||
return values;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ internal class RecordingHost : IHost
|
||||
public int HistoryPresentationEnds;
|
||||
public readonly List<int> ClearedTextLayouts = new();
|
||||
public readonly List<SurfaceRectFill> SurfaceFills = new();
|
||||
public readonly List<SurfaceRectCopy> SurfaceCopies = new();
|
||||
public readonly List<(long First, long Count)> PresentedRanges = new();
|
||||
public readonly List<AdvWaitIndicatorConfig> WaitIndicators = new();
|
||||
public readonly List<bool> WaitIndicatorEnabledChanges = new();
|
||||
@@ -32,6 +33,7 @@ internal class RecordingHost : IHost
|
||||
public readonly List<(long Resource, int Channel)> SfxLoads = new();
|
||||
public readonly List<long> Voices = new();
|
||||
public readonly List<(long Id, int PlaybackVariant)> VoiceRequests = new();
|
||||
public readonly List<(long Id, int PlaybackVariant, long DelayMs)> ScheduledVoiceRequests = new();
|
||||
public readonly List<long> VoiceBgmDuckControls = new();
|
||||
public readonly List<int> SfxStarts = new();
|
||||
public readonly List<(int Channel, int StartMode, long DelayMs)> ScheduledSfxStarts = new();
|
||||
@@ -40,6 +42,7 @@ internal class RecordingHost : IHost
|
||||
public readonly List<(long Resource, int Surface, long Flags, long SyncMask)> Movies = new();
|
||||
public System.Action? OnPlayMovie;
|
||||
public long? MovieStopTimeMs;
|
||||
public readonly HashSet<int> ActiveMovieSurfaces = new();
|
||||
public readonly List<(long Resource, int Surface, long Flags)> ModalMovies = new();
|
||||
public readonly List<int> ClearedRenderTargets = new();
|
||||
public readonly List<(int First, int Count)> ReleasedSurfaceRanges = new();
|
||||
@@ -78,6 +81,7 @@ internal class RecordingHost : IHost
|
||||
}
|
||||
public int MessageWindowAlphaSetting { get; set; }
|
||||
public void FillSurfaceRect(SurfaceRectFill fill) => SurfaceFills.Add(fill);
|
||||
public void CopySurfaceRect(SurfaceRectCopy copy) => SurfaceCopies.Add(copy);
|
||||
public void PresentObjectRange(GfxState gfx, long firstHandle, long count)
|
||||
=> PresentedRanges.Add((firstHandle, count));
|
||||
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) => WaitIndicators.Add(config);
|
||||
@@ -143,6 +147,8 @@ internal class RecordingHost : IHost
|
||||
VoiceRequests.Add((id, playbackVariant));
|
||||
}
|
||||
public void SetVoiceBgmDuckControl(long flags) => VoiceBgmDuckControls.Add(flags);
|
||||
public void ScheduleVoicePlayback(long id, int playbackVariant, long delayMs)
|
||||
=> ScheduledVoiceRequests.Add((id, playbackVariant, delayMs));
|
||||
public void LoadSoundEffect(long resourceId, int channel) => SfxLoads.Add((resourceId, channel));
|
||||
public void StartSoundEffect(int channel) => SfxStarts.Add(channel);
|
||||
public void ScheduleSoundEffectStart(int channel, int startMode, long delayMs)
|
||||
@@ -155,6 +161,7 @@ internal class RecordingHost : IHost
|
||||
OnPlayMovie?.Invoke();
|
||||
return MovieStopTimeMs;
|
||||
}
|
||||
public bool IsMovieSurfaceActive(int surfaceSlot) => ActiveMovieSurfaces.Contains(surfaceSlot);
|
||||
public void PlayModalMovieToSurface(long rawResourceId, int surfaceSlot, long movieFlags)
|
||||
=> ModalMovies.Add((rawResourceId, surfaceSlot, movieFlags));
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ public readonly record struct AdvAutoWaitState(
|
||||
public readonly record struct SurfaceRectFill(
|
||||
int SurfaceSlot, int X, int Y, int Width, int Height, int Alpha, long Rgb);
|
||||
|
||||
public readonly record struct SurfaceRectCopy(
|
||||
int SourceSurface, int DestinationSurface, int SourceX, int SourceY,
|
||||
int Width, int Height, int DestinationX, int DestinationY);
|
||||
|
||||
public interface IHost
|
||||
{
|
||||
/// <summary>Report a recoverable runtime discrepancy while allowing script execution to continue.</summary>
|
||||
@@ -36,6 +40,7 @@ public interface IHost
|
||||
void EndTextHistoryPresentation() { }
|
||||
int MessageWindowAlphaSetting => 0;
|
||||
void FillSurfaceRect(SurfaceRectFill fill) { }
|
||||
void CopySurfaceRect(SurfaceRectCopy copy) { }
|
||||
void PresentObjectRange(GfxState gfx, long firstHandle, long count) { }
|
||||
void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) { }
|
||||
// Op 0x1ce explicitly starts/stops the same animated marker that op 0x72 starts for an ADV wait.
|
||||
@@ -87,6 +92,7 @@ public interface IHost
|
||||
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 SetTexture(long resourceId, int slot, long colorKey) => SetTexture(resourceId, slot);
|
||||
void ReleaseSurface(int slot) { }
|
||||
/// <summary>Clear the selected target's pixels; -1 denotes the main backbuffer.</summary>
|
||||
void ClearRenderTarget(int surfaceSlot) { }
|
||||
@@ -101,6 +107,7 @@ public interface IHost
|
||||
// Native voice playback retains a second start argument: ordinary dialogue passes 0,
|
||||
// while History replay (0x1bd) passes 1. Existing non-audio hosts may ignore it.
|
||||
void PlayVoice(long id, int playbackVariant) => PlayVoice(id);
|
||||
void ScheduleVoicePlayback(long id, int playbackVariant, long delayMs) { }
|
||||
// Native op 0x1cf stores a transient control mask. Bit 0 suppresses the automatic
|
||||
// BGM attenuation normally applied when a voice starts.
|
||||
void SetVoiceBgmDuckControl(long flags) { }
|
||||
@@ -117,6 +124,7 @@ public interface IHost
|
||||
/// when the host could not obtain usable timing metadata. Native op 0x23f queries this state
|
||||
/// 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
|
||||
// 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.
|
||||
|
||||
@@ -145,6 +145,9 @@ public sealed class GfxState
|
||||
// Retained for its opcode family; 0x21e scale and 0x220 translation use frame-time directly instead. ----
|
||||
public long AnimClockDurationTicks { get; private set; }
|
||||
public long AnimClockGeneration { get; private set; }
|
||||
public long AnimationServiceFlags { get; private set; }
|
||||
public uint PreviousFrameTimeMilliseconds { get; private set; }
|
||||
public uint CurrentFrameTimeMilliseconds { get; private set; }
|
||||
|
||||
/// <summary>Live geometry objects and the surface slot they draw from — for the CLI gfx oracle.</summary>
|
||||
public IEnumerable<(long Handle, int Slot)> Objects
|
||||
@@ -307,6 +310,7 @@ public sealed class GfxState
|
||||
_objects.Clear();
|
||||
_fieldTable.Clear();
|
||||
_surfaces.Clear();
|
||||
_createdSurfaces.Clear();
|
||||
_movieStopTimesMs.Clear();
|
||||
_surfaceTransitions.Clear();
|
||||
CurrentObject = 0;
|
||||
@@ -316,6 +320,9 @@ public sealed class GfxState
|
||||
_rangeTransform = new GfxObject();
|
||||
AnimClockDurationTicks = 0;
|
||||
AnimClockGeneration++;
|
||||
AnimationServiceFlags = 0;
|
||||
PreviousFrameTimeMilliseconds = 0;
|
||||
CurrentFrameTimeMilliseconds = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +330,9 @@ public sealed class GfxState
|
||||
|
||||
// ---- surfaces (image buffers per slot): ctx+0x52bd4[slot], from create/set-texture ----
|
||||
private readonly Dictionary<int, (long ResId, long ColorKey)> _surfaces = new();
|
||||
// Created surfaces have real pixels but no asset resource id. Keep their class separate from both
|
||||
// loaded textures and truly surfaceless objects because native mode-0 consumes packed alpha differently.
|
||||
private readonly HashSet<int> _createdSurfaces = new();
|
||||
// A separate entry models the native CMovieToTexture object attached to a surface. A null value means
|
||||
// the movie object exists but its host decoder supplied no usable IMediaPosition stop time.
|
||||
private readonly Dictionary<int, long?> _movieStopTimesMs = new();
|
||||
@@ -332,6 +342,7 @@ public sealed class GfxState
|
||||
lock (_lock)
|
||||
{
|
||||
_surfaces[slot] = (resId, colorKey);
|
||||
_createdSurfaces.Remove(slot);
|
||||
_movieStopTimesMs.Remove(slot);
|
||||
}
|
||||
}
|
||||
@@ -366,6 +377,7 @@ public sealed class GfxState
|
||||
for (int slot = firstSlot; slot < end; slot++)
|
||||
{
|
||||
_surfaces.Remove(slot);
|
||||
_createdSurfaces.Remove(slot);
|
||||
_movieStopTimesMs.Remove(slot);
|
||||
_surfaceTransitions.Remove(slot);
|
||||
}
|
||||
@@ -463,12 +475,24 @@ public sealed class GfxState
|
||||
o.ColorAnim = true;
|
||||
}
|
||||
}
|
||||
public void CreateSurface(int slot)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_surfaces[slot] = (0, -1); // create-texture: real mutable pixels, no asset id or color key
|
||||
_createdSurfaces.Add(slot);
|
||||
_movieStopTimesMs.Remove(slot);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearSurface(int slot)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_surfaces[slot] = (0, 0); // create-texture (blank)
|
||||
_surfaces.Remove(slot);
|
||||
_createdSurfaces.Remove(slot);
|
||||
_movieStopTimesMs.Remove(slot);
|
||||
_surfaceTransitions.Remove(slot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -801,12 +825,27 @@ public sealed class GfxState
|
||||
lock (_lock) { AnimClockDurationTicks = durationTicks; AnimClockGeneration++; }
|
||||
}
|
||||
|
||||
public void SetAnimationServiceFlags(long flags)
|
||||
{
|
||||
lock (_lock) AnimationServiceFlags = unchecked((uint)flags);
|
||||
}
|
||||
|
||||
public void SampleFrameTime(long nowMilliseconds)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
PreviousFrameTimeMilliseconds = CurrentFrameTimeMilliseconds;
|
||||
CurrentFrameTimeMilliseconds = unchecked((uint)nowMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x243: force ordinary finite channels to their endpoints and reset the separate
|
||||
/// global animation-service clock. Op-0x242-detached objects ignore the completion request.</summary>
|
||||
public void ResetAnimClock()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if ((AnimationServiceFlags & 2) != 0) return;
|
||||
ForceCompleteOneShotChannels();
|
||||
AnimClockDurationTicks = 0;
|
||||
AnimClockGeneration++;
|
||||
@@ -856,7 +895,10 @@ public sealed class GfxState
|
||||
if (!o.Visible) continue;
|
||||
bool hadOneShot = o.OneShotColorEnabled || o.ScaleEnabled ||
|
||||
o.RotationChannelEnabled || o.TranslationEnabled;
|
||||
var (resId, ck) = _surfaces.TryGetValue(o.SourceSlot, out var s) ? s : (0L, 0L);
|
||||
// Created/mutable surfaces have no file resource id but remain a distinct surface class.
|
||||
// Zero is an active "key black" value, so created and absent slots both default to -1.
|
||||
var (resId, ck) = _surfaces.TryGetValue(o.SourceSlot, out var s) ? s : (0L, -1L);
|
||||
bool createdSurface = _createdSurfaces.Contains(o.SourceSlot);
|
||||
|
||||
// ---- packed color: a static mode 0 treats alpha as tint/fill strength. Once op 0x202 has
|
||||
// armed the one-shot channel, its current/target ARGB instead supplies opacity and D3D-style
|
||||
@@ -898,6 +940,13 @@ public sealed class GfxState
|
||||
// not a request to replace every texel with white.
|
||||
alpha = a; strength = 0; blend = BlendKind.Alpha;
|
||||
}
|
||||
else if (createdSurface)
|
||||
{
|
||||
// Native created/render-target surfaces use packed alpha as object opacity.
|
||||
// SYSTEM4/BUNKI relies on this for translucent panels; FIELD uses the same surface
|
||||
// at alpha 0x40 beneath the minimap so the paper chrome remains visible.
|
||||
alpha = a; strength = 0; blend = BlendKind.Alpha; multiplyTint = true;
|
||||
}
|
||||
else if (resId != 0)
|
||||
{
|
||||
// Native mode 0 is the opaque textured path. RGB modulates the source and the
|
||||
|
||||
82
engine/Age.Engine/Sys4/RgbaSurfaceOps.cs
Normal file
82
engine/Age.Engine/Sys4/RgbaSurfaceOps.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
namespace Age.Engine.Sys4;
|
||||
|
||||
/// <summary>Platform-neutral mutation helpers for AGE's software-modeled RGBA surfaces.</summary>
|
||||
public static class RgbaSurfaceOps
|
||||
{
|
||||
public static bool FillRect(RgbaImage destination, int x, int y, int width, int height,
|
||||
byte alpha, long rgb)
|
||||
{
|
||||
long left = x, top = y, right = left + width, bottom = top + height;
|
||||
if (width <= 0 || height <= 0) return false;
|
||||
left = System.Math.Max(0, left);
|
||||
top = System.Math.Max(0, top);
|
||||
right = System.Math.Min(destination.Width, right);
|
||||
bottom = System.Math.Min(destination.Height, bottom);
|
||||
if (right <= left || bottom <= top) return false;
|
||||
|
||||
byte red = (byte)((rgb >> 16) & 0xff);
|
||||
byte green = (byte)((rgb >> 8) & 0xff);
|
||||
byte blue = (byte)(rgb & 0xff);
|
||||
for (int row = (int)top; row < (int)bottom; row++)
|
||||
{
|
||||
int offset = checked((row * destination.Width + (int)left) * 4);
|
||||
for (int column = (int)left; column < (int)right; column++, offset += 4)
|
||||
{
|
||||
destination.Pixels[offset] = red;
|
||||
destination.Pixels[offset + 1] = green;
|
||||
destination.Pixels[offset + 2] = blue;
|
||||
destination.Pixels[offset + 3] = alpha;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static RgbaImage WithColorKey(RgbaImage source, long colorKey)
|
||||
{
|
||||
if (!Age.Engine.Model.BlendMath.HasColorKey(colorKey)) return source;
|
||||
byte[] pixels = (byte[])source.Pixels.Clone();
|
||||
for (int index = 0; index < pixels.Length; index += 4)
|
||||
if (Age.Engine.Model.BlendMath.ColorKeyMatches(
|
||||
pixels[index], pixels[index + 1], pixels[index + 2], colorKey))
|
||||
pixels[index + 3] = 0;
|
||||
return new RgbaImage(source.Width, source.Height, pixels);
|
||||
}
|
||||
|
||||
/// <summary>Copy a rectangle while clipping source and destination together. A temporary buffer
|
||||
/// preserves native blit behavior when the two rectangles overlap in the same surface.</summary>
|
||||
public static bool CopyRect(RgbaImage source, RgbaImage destination,
|
||||
int sourceX, int sourceY, int width, int height,
|
||||
int destinationX, int destinationY)
|
||||
{
|
||||
long sx = sourceX, sy = sourceY, dx = destinationX, dy = destinationY;
|
||||
long w = width, h = height;
|
||||
if (w <= 0 || h <= 0) return false;
|
||||
|
||||
if (sx < 0) { long n = -sx; sx = 0; dx += n; w -= n; }
|
||||
if (sy < 0) { long n = -sy; sy = 0; dy += n; h -= n; }
|
||||
if (dx < 0) { long n = -dx; dx = 0; sx += n; w -= n; }
|
||||
if (dy < 0) { long n = -dy; dy = 0; sy += n; h -= n; }
|
||||
w = System.Math.Min(w, source.Width - sx);
|
||||
h = System.Math.Min(h, source.Height - sy);
|
||||
w = System.Math.Min(w, destination.Width - dx);
|
||||
h = System.Math.Min(h, destination.Height - dy);
|
||||
if (w <= 0 || h <= 0) return false;
|
||||
|
||||
int clippedWidth = checked((int)w);
|
||||
int clippedHeight = checked((int)h);
|
||||
int rowBytes = checked(clippedWidth * 4);
|
||||
byte[] pixels = new byte[checked(rowBytes * clippedHeight)];
|
||||
for (int row = 0; row < clippedHeight; row++)
|
||||
{
|
||||
int sourceOffset = checked(((int)sy + row) * source.Width * 4 + (int)sx * 4);
|
||||
source.Pixels.AsSpan(sourceOffset, rowBytes).CopyTo(pixels.AsSpan(row * rowBytes, rowBytes));
|
||||
}
|
||||
for (int row = 0; row < clippedHeight; row++)
|
||||
{
|
||||
int destinationOffset = checked(((int)dy + row) * destination.Width * 4 + (int)dx * 4);
|
||||
pixels.AsSpan(row * rowBytes, rowBytes)
|
||||
.CopyTo(destination.Pixels.AsSpan(destinationOffset, rowBytes));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,12 @@ public sealed class VirtualMachine
|
||||
private volatile bool _advSkipServiceEnabled;
|
||||
private AdvTextStyle _advTextStyle = AdvTextStyle.Default;
|
||||
private readonly Dictionary<string, int> _valueSwitchTargets = new(StringComparer.Ordinal);
|
||||
// Native EngineCtx owns 11 lazily allocated integer FIFOs at +0x55130. ATSEEK/MVSEEK use
|
||||
// slot zero as their packed-coordinate flood-fill worklist; op 0x132 replaces a slot.
|
||||
private readonly Queue<int>?[] _intQueues = new Queue<int>?[11];
|
||||
// Opcodes 0x06/0x08 load scripts into numbered EngineCtx frame slots and invoke them later.
|
||||
// Unlike ordinary call-script frames, native non-adjacent slots survive return with locals intact.
|
||||
private readonly Dictionary<int, PreloadedScriptSlot> _preloadedScriptSlots = new();
|
||||
public long CallScriptDispatches { get; private set; }
|
||||
|
||||
public Dictionary<int, long> Globals { get; } = new();
|
||||
@@ -493,6 +499,7 @@ public sealed class VirtualMachine
|
||||
private sealed class RootReloadRequestedException : Exception { }
|
||||
private sealed class ProcessExitRequestedException : Exception { }
|
||||
private sealed record DebugFrameReturnRequest(ExecFrame Frame, IReadOnlyDictionary<int, long> GlobalWrites);
|
||||
private sealed record PreloadedScriptSlot(long ScriptId, ExecFrame Frame);
|
||||
private enum FrameOutcome { Returned, DebugReturned, RootReload, ExitRequested, Halted, RanOff }
|
||||
|
||||
public void Run(int entryOffset = 0)
|
||||
@@ -537,6 +544,7 @@ public sealed class VirtualMachine
|
||||
{
|
||||
Gfx.ResetSceneContext();
|
||||
_valueSwitchTargets.Clear();
|
||||
_preloadedScriptSlots.Clear();
|
||||
lock (_interactiveLock)
|
||||
{
|
||||
_interactiveFrame = null;
|
||||
@@ -717,6 +725,25 @@ public sealed class VirtualMachine
|
||||
case "string-not-equals":
|
||||
Write(a[0], string.Equals(ReadStr(a[1]), ReadStr(a[2]), StringComparison.Ordinal) ? 0 : 1);
|
||||
return pc + 1;
|
||||
case "concat":
|
||||
{
|
||||
string left = ReadStr(a[1]);
|
||||
string right = ReadStr(a[2]);
|
||||
WriteStr(a[0], left + right);
|
||||
return pc + 1;
|
||||
}
|
||||
case "toString":
|
||||
WriteStr(a[0], unchecked((int)Read(a[1])).ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
return pc + 1;
|
||||
case "absolute-value":
|
||||
{
|
||||
int value = unchecked((int)Read(a[1]));
|
||||
int sign = value >> 31;
|
||||
Write(a[0], unchecked((value ^ sign) - sign));
|
||||
return pc + 1;
|
||||
}
|
||||
case "get-monotonic-time-ms":
|
||||
Write(a[0], unchecked((int)_host.InputClockMilliseconds)); return pc + 1;
|
||||
case "lt": Write(a[0], Read(a[1]) < Read(a[2]) ? 1 : 0); return pc + 1;
|
||||
case "lte": Write(a[0], Read(a[1]) <= Read(a[2]) ? 1 : 0); return pc + 1;
|
||||
case "gr": Write(a[0], Read(a[1]) > Read(a[2]) ? 1 : 0); return pc + 1;
|
||||
@@ -840,6 +867,62 @@ public sealed class VirtualMachine
|
||||
}
|
||||
return pc + 1;
|
||||
}
|
||||
case "u0041EF00":
|
||||
case "reset-int-queue": // 0x132 (queue_id): destroy/recreate one of 11 native FIFO slots
|
||||
{
|
||||
int queueId = unchecked((int)Read(a[0]));
|
||||
if ((uint)queueId >= (uint)_intQueues.Length)
|
||||
{
|
||||
HaltReason ??= $"int-queue-id-out-of-range:{queueId}";
|
||||
return HALT;
|
||||
}
|
||||
_intQueues[queueId] = new Queue<int>(0x100);
|
||||
return pc + 1;
|
||||
}
|
||||
case "u0041EFF0":
|
||||
case "enqueue-int": // 0x133 (queue_id, value)
|
||||
{
|
||||
int queueId = unchecked((int)Read(a[0]));
|
||||
if ((uint)queueId >= (uint)_intQueues.Length)
|
||||
{
|
||||
HaltReason ??= $"int-queue-id-out-of-range:{queueId}";
|
||||
return HALT;
|
||||
}
|
||||
if (_intQueues[queueId] is not { } queue)
|
||||
{
|
||||
HaltReason ??= $"int-queue-uninitialized:{queueId}";
|
||||
return HALT;
|
||||
}
|
||||
queue.Enqueue(unchecked((int)Read(a[1])));
|
||||
return pc + 1;
|
||||
}
|
||||
case "u0041F050":
|
||||
case "try-dequeue-int": // 0x134 (queue_id, out_success, out_value)
|
||||
{
|
||||
int queueId = unchecked((int)Read(a[0]));
|
||||
if ((uint)queueId >= (uint)_intQueues.Length)
|
||||
{
|
||||
HaltReason ??= $"int-queue-id-out-of-range:{queueId}";
|
||||
return HALT;
|
||||
}
|
||||
if (_intQueues[queueId] is not { } queue)
|
||||
{
|
||||
HaltReason ??= $"int-queue-uninitialized:{queueId}";
|
||||
return HALT;
|
||||
}
|
||||
if (queue.TryDequeue(out int value))
|
||||
{
|
||||
Write(a[1], 1);
|
||||
Write(a[2], value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Native writes success=0 and an implementation pointer to operand 3. Shipped
|
||||
// callers branch on success before reading it, so retain the prior destination.
|
||||
Write(a[1], 0);
|
||||
}
|
||||
return pc + 1;
|
||||
}
|
||||
case "bit-set":
|
||||
{
|
||||
long bit = Read(a[1]);
|
||||
@@ -977,6 +1060,60 @@ public sealed class VirtualMachine
|
||||
if (outcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException();
|
||||
return pc + 1; // Returned / RanOff: resume caller
|
||||
}
|
||||
case "u00417E80":
|
||||
case "preload-script-slot": // 0x06 (script_id, frame_slot), valid slots 0..39
|
||||
{
|
||||
long id = Read(a[0]);
|
||||
int slot = unchecked((int)Read(a[1]));
|
||||
if ((uint)slot >= 40)
|
||||
{
|
||||
HaltReason ??= $"preloaded-script-slot-out-of-range:{slot}";
|
||||
return HALT;
|
||||
}
|
||||
if (_provider == null)
|
||||
{
|
||||
HaltReason ??= $"preloaded-script-provider-unavailable:0x{id:x}";
|
||||
return HALT;
|
||||
}
|
||||
var script = _provider.GetById(id);
|
||||
if (script == null)
|
||||
{
|
||||
HaltReason ??= $"preloaded-script-unresolved:0x{id:x}";
|
||||
return HALT;
|
||||
}
|
||||
int entry = script.IndexByOffset.TryGetValue(0, out int loadedEntry) ? loadedEntry : 0;
|
||||
_preloadedScriptSlots[slot] = new PreloadedScriptSlot(id, new ExecFrame(script, entry));
|
||||
return pc + 1;
|
||||
}
|
||||
case "u00417FC0":
|
||||
case "call-preloaded-script-slot": // 0x08 (frame_slot)
|
||||
{
|
||||
int slot = unchecked((int)Read(a[0]));
|
||||
if ((uint)slot >= 40)
|
||||
{
|
||||
HaltReason ??= $"preloaded-script-slot-out-of-range:{slot}";
|
||||
return HALT;
|
||||
}
|
||||
if (!_preloadedScriptSlots.TryGetValue(slot, out var loaded))
|
||||
{
|
||||
HaltReason ??= $"preloaded-script-slot-empty:{slot}";
|
||||
return HALT;
|
||||
}
|
||||
if (_depth >= _o.CallDepthCap) { HaltReason ??= "call-depth-exceeded"; return HALT; }
|
||||
|
||||
CallScriptDispatches++;
|
||||
_sink.Emit(TraceEvent.CallScript(loaded.ScriptId, loaded.Frame.Script.Name));
|
||||
// PC restarts at codebase while the native slot's local banks remain allocated.
|
||||
// Balanced local calls leave this empty; clearing the port-only emission guard makes
|
||||
// each invocation an independent diagnostic activation.
|
||||
loaded.Frame.CallStack.Clear();
|
||||
loaded.Frame.EmitSeen.Clear();
|
||||
var outcome = RunFrame(loaded.Frame, FrameCause.CallScript, loaded.ScriptId);
|
||||
if (outcome == FrameOutcome.Halted) return HALT;
|
||||
if (outcome == FrameOutcome.RootReload) return ROOT_RELOAD;
|
||||
if (outcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException();
|
||||
return pc + 1;
|
||||
}
|
||||
case "show-text":
|
||||
foreach (var o in a)
|
||||
{
|
||||
@@ -1398,7 +1535,7 @@ public sealed class VirtualMachine
|
||||
return pc + 1;
|
||||
case "create-texture": // 0x1f8 (slot)(w)(h) — allocate a blank surface at the slot
|
||||
_host.ReleaseSurface((int)Read(a[0]));
|
||||
Gfx.ClearSurface((int)Read(a[0]));
|
||||
Gfx.CreateSurface((int)Read(a[0]));
|
||||
_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
|
||||
{
|
||||
@@ -1408,8 +1545,9 @@ public sealed class VirtualMachine
|
||||
System.Console.Error.WriteLine($"[settex] resId=0x{requestedResourceId:x}->0x{resolvedResourceId: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]));
|
||||
Gfx.SetSurface((int)Read(a[1]), resolvedResourceId, a.Count > 2 ? Read(a[2]) : 0);
|
||||
_host.SetTexture(resolvedResourceId, (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);
|
||||
return pc + 1; // host still tracks dims for get-texture-size
|
||||
}
|
||||
case "u00422EB0": // pre-reference compatibility
|
||||
@@ -1423,7 +1561,7 @@ public sealed class VirtualMachine
|
||||
int surfaceSlot = (int)Read(a[1]);
|
||||
_host.ReleaseSurface(surfaceSlot);
|
||||
Gfx.SetSurface(surfaceSlot, rawResourceId, Read(a[2]));
|
||||
_host.SetTexture(rawResourceId, surfaceSlot);
|
||||
_host.SetTexture(rawResourceId, surfaceSlot, Read(a[2]));
|
||||
return pc + 1;
|
||||
}
|
||||
case "draw-texture": // 0x1fb (handle)(slot)(srcX)(srcY)(w)(h)(dstX)(dstY) — bind object -> surface + rect + pos
|
||||
@@ -1471,6 +1609,11 @@ public sealed class VirtualMachine
|
||||
(int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]), (int)Read(a[4]),
|
||||
(int)System.Math.Min(Read(a[5]), 255), Read(a[6]) & 0x00ff_ffff));
|
||||
return pc + 1;
|
||||
case "copy-surface-rect": // 0x207: paired-clipped source-to-destination surface copy
|
||||
_host.CopySurfaceRect(new SurfaceRectCopy(
|
||||
(int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]),
|
||||
(int)Read(a[4]), (int)Read(a[5]), (int)Read(a[6]), (int)Read(a[7])));
|
||||
return pc + 1;
|
||||
case "clear-retained-gfx-objects": // 0x1f6: erase object records, but preserve surfaces
|
||||
Gfx.ClearRetainedObjects(); return pc + 1;
|
||||
case "select-render-target": // 0x20d: slot <1000 selects a surface; >=1000 restores backbuffer
|
||||
@@ -1492,6 +1635,8 @@ public sealed class VirtualMachine
|
||||
_host.PlayVoice(Read(a[0]), 1); return pc + 1;
|
||||
case "set-voice-bgm-duck-control": // 0x1cf: bit 0 suppresses automatic voice ducking
|
||||
_host.SetVoiceBgmDuckControl(Read(a[0])); return pc + 1;
|
||||
case "schedule-voice-playback": // 0x2c0: replace the pending delayed combat voice request
|
||||
_host.ScheduleVoicePlayback(Read(a[0]), (int)Read(a[1]), Read(a[2])); return pc + 1;
|
||||
case "play-sound-effect": // 0xb4 / semantics: sfx-load
|
||||
_host.LoadSoundEffect(Read(a[0]), (int)Read(a[1])); return pc + 1;
|
||||
case "u0041D050": // 0xb5 / semantics: sfx-start
|
||||
@@ -1626,6 +1771,10 @@ public sealed class VirtualMachine
|
||||
Write(a[0], stopTimeMs.Value);
|
||||
return pc + 1;
|
||||
}
|
||||
case "query-movie-surface-active": // 0x23a (out)(surface slot)
|
||||
Write(a[0], _host.IsMovieSurfaceActive((int)Read(a[1])) ? 1 : 0); return pc + 1;
|
||||
case "sample-frame-time": // 0x23c: previous <- current; current <- monotonic time
|
||||
Gfx.SampleFrameTime(_host.InputClockMilliseconds); return pc + 1;
|
||||
case "set-gfx-geom3-c": // 0x1ff: set current translation matrix
|
||||
Gfx.SetCurrentTranslation(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1;
|
||||
case "u00420620": // upstream ABI label
|
||||
@@ -1666,6 +1815,8 @@ public sealed class VirtualMachine
|
||||
Gfx.SetOneShotAnimationControl(Read(a[0]), Read(a[1])); return pc + 1;
|
||||
case "reset-anim-clock": // 0x243: force unprotected one-shots and reset the global service clock
|
||||
Gfx.ResetAnimClock(); return pc + 1;
|
||||
case "set-gfx-animation-service-flags": // 0x24e: bit 1 suppresses op 0x243
|
||||
Gfx.SetAnimationServiceFlags(Read(a[0])); return pc + 1;
|
||||
case "queue-surface-alpha-transition": // 0x223: target surface crossfade over two object ranges
|
||||
Gfx.QueueSurfaceAlphaTransition(Read(a[0]), (int)Read(a[1]), Read(a[2]), (int)Read(a[3]),
|
||||
Read(a[4]), (int)Read(a[5]), Read(a[6]), Read(a[7])); return pc + 1;
|
||||
|
||||
Reference in New Issue
Block a user