From 4c7f35c61c0c04184a035259f42f2e08e8c22d95 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 7 Jul 2026 23:34:52 -0400 Subject: [PATCH] feat(gfx): GfxState animation-channel model (per-object transform + global clock) Co-Authored-By: Claude Opus 4.8 --- engine/Age.Engine.Tests/GfxAnimationTests.cs | 45 +++++++++++++++++ engine/Age.Engine/Model/GfxState.cs | 52 ++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 engine/Age.Engine.Tests/GfxAnimationTests.cs diff --git a/engine/Age.Engine.Tests/GfxAnimationTests.cs b/engine/Age.Engine.Tests/GfxAnimationTests.cs new file mode 100644 index 0000000..8c17d28 --- /dev/null +++ b/engine/Age.Engine.Tests/GfxAnimationTests.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using Age.Engine.Model; +using Age.Engine.Sys4; +using Age.Engine.Vm; +using Xunit; + +public class GfxAnimationTests +{ + // ---- Task 2: GfxState animation-channel data model (revised global-clock model, see engine-re.md) ---- + + [Fact] + public void SetAnimTransform_RecordsTargetParamsAndEnables() + { + var g = new GfxState(); + g.SetAnimTransform(0x1000, p1: 7, p2: 9, target: (100, 100, 100), normalized: true); + var o = g.TryGet(0x1000)!; + Assert.Equal((100L, 100L, 100L), o.AnimTarget); + Assert.Equal(7, o.AnimParam1); + Assert.Equal(9, o.AnimParam2); + Assert.True(o.AnimNormalized); + Assert.True(o.AnimEnabled); + } + + [Fact] + public void StartAnim_SetsTargetDurationEnablesAndBumpsGeneration() + { + var g = new GfxState(); + g.StartAnim(0x1000, durationTicks: 30, target: (0, 0, 5)); + var o = g.TryGet(0x1000)!; + Assert.Equal((0L, 0L, 5L), o.AnimTarget); + Assert.Equal(30, o.AnimDurationTicks); + Assert.True(o.AnimEnabled); + Assert.Equal(1, o.AnimGeneration); // fresh object starts at 0, one anim-start -> 1 + } + + [Fact] + public void SetAnimClock_SetsGlobalDurationAndBumpsClockGeneration() + { + var g = new GfxState(); + var cg0 = g.AnimClockGeneration; + g.SetAnimClock(400); + Assert.Equal(400, g.AnimClockDurationTicks); + Assert.Equal(cg0 + 1, g.AnimClockGeneration); + } +} diff --git a/engine/Age.Engine/Model/GfxState.cs b/engine/Age.Engine/Model/GfxState.cs index 3a904c8..4ae6631 100644 --- a/engine/Age.Engine/Model/GfxState.cs +++ b/engine/Age.Engine/Model/GfxState.cs @@ -26,6 +26,17 @@ public sealed class GfxState public int SourceSlot = -1; public (int X, int Y, int W, int H) SrcRect; public bool Visible; + + // ---- animation channel (cluster 0x21c-0x243; see docs/engine-re.md "sprite transform / ANIMATION"). + // 0x21e/0x220 set the transform directly (worker gfx_anim_set_channel@0x47eaa0: obj+0x3c=p1, +0x50=p2, + // +0xac=target, +0x68=enable); 0x234 anim-start animates toward a target over the GLOBAL clock (0x238). + // Passive: recorded here, interpolated by the Godot compositor over wall-clock. ---- + public (long X, long Y, long Z) AnimTarget; + public long AnimParam1, AnimParam2; + public bool AnimNormalized; // 0x21e (operands ~percent, /_DAT_00571c28) vs 0x220 (absolute) + public bool AnimEnabled; // obj+0x68 + public long AnimDurationTicks; // 0x234 anim-start op2 (this object's duration; maxed into the clock) + public long AnimGeneration; // bumped by anim-start (0x234); the compositor's per-object re-trigger } // ---- geometry/draw object store (V18/V24/draw bind, the compositor's input) ---- @@ -43,6 +54,13 @@ public sealed class GfxState private readonly Dictionary _fieldTable = new(); // ctx+0x46d14 (0x216); no family writer -> default 0 public long CurrentObject { get; private set; } + // ---- GLOBAL animation clock (op 0x238 set-anim-clock; native ctx+0x51b7c total / +0x51b78 elapsed). + // Non-blocking: the op only configures duration; the host advances elapsed per-frame and tweens all armed + // objects over it (docs/engine-re.md, "anim_start/set_anim_clock decoded"). Generation bumps on each set so + // the compositor resets its wall-clock elapsed. ---- + public long AnimClockDurationTicks { get; private set; } + public long AnimClockGeneration { get; private set; } + /// Live geometry objects and the surface slot they draw from — for the CLI gfx oracle. public IEnumerable<(long Handle, int Slot)> Objects { @@ -106,6 +124,40 @@ public sealed class GfxState } } + /// Op 0x21e/0x220 (set-anim-transform): record the transform target + two scalar params on the + /// object and enable its animation channel. normalized = 0x21e (operands ~percent, /_DAT_00571c28); + /// absolute = 0x220. Native worker gfx_anim_set_channel@0x47eaa0 sets obj+0x3c=p1, +0x50=p2, +0xac=target, + /// +0x68=1. + public void SetAnimTransform(long handle, long p1, long p2, (long X, long Y, long Z) target, bool normalized) + { + lock (_lock) + { + var o = GetOrCreate(handle); + o.AnimParam1 = p1; o.AnimParam2 = p2; o.AnimTarget = target; + o.AnimNormalized = normalized; o.AnimEnabled = true; + } + } + + /// Op 0x234 (anim-start): animate the object toward over the global + /// clock; is this object's duration (native label_1235a maxes them into + /// the clock). Bumps AnimGeneration — the compositor's per-object re-trigger. + public void StartAnim(long handle, long durationTicks, (long X, long Y, long Z) target) + { + lock (_lock) + { + var o = GetOrCreate(handle); + o.AnimTarget = target; o.AnimDurationTicks = durationTicks; + o.AnimEnabled = true; o.AnimGeneration++; + } + } + + /// Op 0x238 (set-anim-clock): set the GLOBAL animation duration (game ticks) and bump the clock + /// generation so the host resets its wall-clock elapsed. Non-blocking (the render loop advances it). + public void SetAnimClock(long durationTicks) + { + lock (_lock) { AnimClockDurationTicks = durationTicks; AnimClockGeneration++; } + } + /// Visible objects in ascending-handle order (= the engine's z-order), each with its source /// surface (resId/colorkey) resolved from its live source slot — for the host per-frame compositor. public IReadOnlyList SnapshotVisibleObjects()