From a014b901899dace9291069bfb8b48a3fad5b5c53 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 8 Jul 2026 22:07:51 -0400 Subject: [PATCH] feat: wire SC0000 anim cluster ops (0x22f/0x229 pos, 0x239/0x231 sheet, 0x232 glow, 0x228/0x23f query) Coverage 67->74/129; parity held (79/79, sweep 284/13). Co-Authored-By: Claude Opus 4.8 --- engine/Age.Engine.Tests/AnimChannelTests.cs | 63 +++++++++++++++++++++ engine/Age.Engine/Vm/VirtualMachine.cs | 20 +++++++ 2 files changed, 83 insertions(+) diff --git a/engine/Age.Engine.Tests/AnimChannelTests.cs b/engine/Age.Engine.Tests/AnimChannelTests.cs index 6e342c6..b619e88 100644 --- a/engine/Age.Engine.Tests/AnimChannelTests.cs +++ b/engine/Age.Engine.Tests/AnimChannelTests.cs @@ -1,8 +1,71 @@ +using System.Collections.Generic; using Age.Engine.Model; +using Age.Engine.Sys4; +using Age.Engine.Vm; using Xunit; public class AnimChannelTests { + private static OpcodeTable T() => OpcodeTableJson.Load(Paths.OpcodesJson); + private static Operand G(int addr) => new(3, addr); + private static (int, Operand[]) Exit() => (0x2, System.Array.Empty()); + + [Fact] + public void Op0x22f_SetsPosition_Op0x228_QueriesItBack() + { + var t = T(); + // g1=handle, g3=x=50, g4=y=60, g5=z=0; op 0x22f sets V24; op 0x228 reads it into g11/g12/g13. + var scene = ScriptAssembler.Assemble(t, "GFX", new List<(int, Operand[])> + { + (0x55, new[]{G(1), new Operand(0,0x1000)}), (0x55, new[]{G(2), new Operand(0,0)}), + (0x55, new[]{G(3), new Operand(0,50)}), (0x55, new[]{G(4), new Operand(0,60)}), (0x55, new[]{G(5), new Operand(0,0)}), + (0x22f, new[]{G(1), G(2), G(3), G(4), G(5)}), // set position + (0x228, new[]{G(10), G(1), G(11), G(12), G(13)}), // query position -> g11,g12,g13 + Exit(), + }, System.Array.Empty()); + var vm = new VirtualMachine(scene, t, new RecordingHost()); + vm.Run(); + Assert.Equal(50, vm.Globals[11]); + Assert.Equal(60, vm.Globals[12]); + Assert.Equal(0, vm.Globals[10]); // success flag + } + + [Fact] + public void Op0x232_SetsColorAnim() + { + var t = T(); + var scene = ScriptAssembler.Assemble(t, "GFX", new List<(int, Operand[])> + { + (0x55, new[]{G(1), new Operand(0,0x1000)}), (0x55, new[]{G(2), new Operand(0,1000)}), + (0x55, new[]{G(3), new Operand(0,0x80)}), (0x55, new[]{G(4), new Operand(0,0xFF0000)}), + (0x232, new[]{G(1), G(2), G(3), G(4)}), Exit(), + }, System.Array.Empty()); + var vm = new VirtualMachine(scene, t, new RecordingHost()); + vm.Run(); + var o = vm.Gfx.TryGet(0x1000)!; + Assert.True(o.ColorAnim); + Assert.Equal(1000, o.ColorPeriod); + Assert.Equal(GfxState.PackColor(0x80, 0xFF0000), o.ColorTarget); + } + + [Fact] + public void Op0x239_SetsSpritesheetGridCell() + { + var t = T(); + var scene = ScriptAssembler.Assemble(t, "GFX", new List<(int, Operand[])> + { + (0x55, new[]{G(1), new Operand(0,0x1000)}), (0x55, new[]{G(2), new Operand(0,0)}), (0x55, new[]{G(3), new Operand(0,0)}), + (0x55, new[]{G(4), new Operand(0,4)}), (0x55, new[]{G(5), new Operand(0,1)}), (0x55, new[]{G(6), new Operand(0,2)}), + (0x239, new[]{G(1), G(2), G(3), G(4), G(5), G(6)}), Exit(), + }, System.Array.Empty()); + var vm = new VirtualMachine(scene, t, new RecordingHost()); + vm.Run(); + var o = vm.Gfx.TryGet(0x1000)!; + Assert.True(o.SrcAnim); + Assert.Equal(4, o.SrcGridW); + Assert.Equal(2, o.SrcCell); + } + private static GfxState VisibleObj(long handle) { var g = new GfxState(); diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index a003314..8d24278 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -255,6 +255,26 @@ public sealed class VirtualMachine Gfx.GetOrCreate(Read(a[0])).V18 = (Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1; case "set-gfx-geom3-b": // 0x219 (handle)(a)(b)(c) -> V24 Gfx.GetOrCreate(Read(a[0])).V24 = (Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1; + + // ---- SC0000 anim/transform/spritesheet cluster (docs/engine-re.md ยง"SC0000 anim ... cluster") ---- + case "u00421DD0": // 0x22f set-position: (handle)(op2)(x)(y)(z) -> base position (direct set) + case "u004219E0": // 0x229 set-position2: same shape, direct position + Gfx.GetOrCreate(Read(a[0])).V24 = (Read(a[2]), Read(a[3]), Read(a[4])); return pc + 1; + case "u004223C0": // 0x239 spritesheet cell: (handle)(p3)(p4)(gridW)(gridH)(cell) โ€” static cell + Gfx.SetSrcRect(Read(a[0]), Read(a[3]), Read(a[4]), Read(a[5]), 0); return pc + 1; + case "u00421EA0": // 0x231 anim spritesheet: (handle)(period)(gridW)(gridH) โ€” ping-pong the cell + Gfx.SetSrcRect(Read(a[0]), Read(a[2]), Read(a[3]), 0, Read(a[1])); return pc + 1; + case "u00421EF0": // 0x232 anim color/glow: (handle)(period)(alpha)(color) โ€” ping-pong the color + Gfx.SetColorAnim(Read(a[0]), Read(a[1]), GfxState.PackColor(Read(a[2]), Read(a[3]))); return pc + 1; + case "u00421940": // 0x228 query-position: (succ)(handle)(outX)(outY)(outZ) <- current V24 + { + var obj = Gfx.TryGet(Read(a[1])); + var v = obj?.V24 ?? default; + Write(a[2], v.X); Write(a[3], v.Y); Write(a[4], v.Z); + Write(a[0], obj != null ? 0 : 1); return pc + 1; + } + case "u00422930": // 0x23f query-object: (out)(handle) <- 0 if the object exists, else -1 + Write(a[0], Gfx.TryGet(Read(a[1])) != null ? 0 : -1); return pc + 1; case "set-gfx-geom3-c": // 0x1ff (handle)(a)(b)(c) -> V16c Gfx.GetOrCreate(Read(a[0])).V16c = (Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1; case "set-gfx-field64": // 0x212 (idx)(val)