From 0bf6b43a32cf003d89cd8045bc9a79f826a35fde Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 11 Jul 2026 20:37:26 -0400 Subject: [PATCH] Fix AE001H spritesheet animation --- docs/engine-re.md | 11 ++++-- docs/opcode-reference.md | 10 +++--- docs/phase-a-slice-plan.md | 17 ++++++++-- engine/Age.Engine.Tests/AnimChannelTests.cs | 13 +++---- .../Age.Engine.Tests/AnimInterpolatorTests.cs | 18 +++++----- engine/Age.Engine.Tests/GfxAnimationTests.cs | 21 ++++++++++++ engine/Age.Engine/Model/GfxState.cs | 34 +++++++++---------- engine/Age.Engine/Vm/VirtualMachine.cs | 4 +-- vm-map/opcodes.toml | 16 ++++----- 9 files changed, 93 insertions(+), 51 deletions(-) diff --git a/docs/engine-re.md b/docs/engine-re.md index 9316e41..f35c110 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -840,8 +840,8 @@ annotated in Ghidra, saved. |---|---|---| | `0x22f` | `gfx_op_0x22f_set_position_anim` → `gfx_worker_set_translation` | set object **position** (translation vec `obj+0x5d4`); base transform, not a ping-pong channel | | `0x229` | `gfx_op_0x229_set_position` (`FUN_00472bb0`+`FUN_00472be0`) | set object **position/geometry** immediately (`obj+0x420/0x424` + vec `obj+0x440..0x448`) | -| `0x239` | `gfx_op_0x239_set_srcrect_cell` → `gfx_worker_set_srcrect_cell` | set **spritesheet** grid `obj+0x238/0x23c` + static cell `obj+0x234` | -| `0x231` | `gfx_op_0x231_anim_srcrect` → `gfx_worker_anim_srcrect` | **animate spritesheet cell**: bit2 active, period `obj+0x230`, grid `obj+0x238/0x23c` → interpolator SRC-RECT SCROLL channel (ping-pong across the row) | +| `0x239` | `gfx_op_0x239_set_srcrect_cell` → `gfx_worker_set_srcrect_cell` | one-shot **spritesheet-cell** channel: delay/duration `obj+0x48/+0x5c`, total frames/columns `obj+0x238/+0x23c`, target frame `obj+0x234` | +| `0x231` | `gfx_op_0x231_anim_srcrect` → `gfx_worker_anim_srcrect` | looping **spritesheet-cell** channel: milliseconds per frame `obj+0x230`, total frames `obj+0x238`, columns `obj+0x23c`; row-major and wraps, not ping-pong | | `0x232` | `gfx_op_0x232_anim_color` → `gfx_worker_anim_color` | **animate color** (pulsing GLOW): bit2 active, period `obj+0x220`, target `obj+0x240` → interpolator COLOR channel (ping-pong). Distinct from static `0x202`/`0x203` (`obj+0x60/0x64`) | | `0x228` | `gfx_op_0x228_query_position` (`FUN_0047cdd0`) | **query** current computed (x,y,z) → operand slots 3/4/5 (script logic, not render) | | `0x23f` | `gfx_op_0x23f_query_object` (`FUN_0042a520`) | **query** an object status/value → operand slot 1 | @@ -879,6 +879,13 @@ while `0x21c` is parked completes only the active foreground surface transition not pre-arm the following `wait-for-input`, and it does not complete independent retained rotation, matrix, spritesheet, or color-animation channels. +**Source-rectangle correction (2026-07-11):** `gfx_object_anim_interpolate` preserves the source rectangle +set by `draw-texture`; `0x231` does not divide that rectangle by its operands. It computes +`frame = floor(elapsed / frame_period) % frame_count`, then offsets both X bounds by +`rect_width * (frame % columns)` and both Y bounds by `rect_height * (frame / columns)`. SC0000 passes +`(100, 8, 4)` for AE001H: eight 200x200 cells in an 800x400, 4x2 sheet. Treating 8 and 4 as grid width +and height shrank the crop to 25x50 and made the intended cave spirit effectively disappear. + **Resolved 2026-07-11:** `0x236` is the movie-to-retained-surface path described below. The unclassified `0x242/0x23d/0x20a/0x20e` tail (2-arg flags / inline) remains GAP and outside this slice. diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index cd48b79..aa7382a 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -271,8 +271,9 @@ Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is - **grounding:** source=kelebek, confidence=low ### 0x231 `u00421EA0` (u00421EA0, argc 4) -- **summary:** 0x231 anim-srcrect (handle)(period)(gridW)(gridH): ping-pong the spritesheet cell across the grid over period ms. Worker gfx_worker_anim_srcrect @0x47eec0 -> interpolator SRC-RECT SCROLL channel. C# VM: GfxState.SetSrcRect. See docs/engine-re.md §SC0000 anim cluster. -- **grounding:** source=kelebek, confidence=low +- **summary:** (handle)(frame_period_ms)(frame_count)(column_count) — loop row-major through the spritesheet. Every frame preserves draw-texture's source-rectangle width/height; frame=floor(elapsed/frame_period)%frame_count, src offset=(frame%columns*width, frame/columns*height). Worker gfx_worker_anim_srcrect @0x47eec0; consumer gfx_object_anim_interpolate @0x473ed0. +- **grounding:** source=investigation, confidence=high +- **evidence:** Native /v2 decompile: worker stores period at obj+0x230, frame_count at +0x238, columns at +0x23c. Interpolator computes ((now-start)/period)%frame_count, then offsets both source-rect X bounds by rect_width*(frame%columns) and Y bounds by rect_height*(frame/columns). SC0000 uses (100,8,4) with AE001H's eight 200x200 cells in a 4x2 800x400 sheet. ### 0x232 `u00421EF0` (u00421EF0, argc 4) - **summary:** 0x232 anim-color (handle)(period)(alpha)(color): ping-pong the object color/alpha toward the packed target over period ms (pulsing GLOW). Worker gfx_worker_anim_color @0x47ef50 -> interpolator COLOR channel. C# VM: GfxState.SetColorAnim. See docs/engine-re.md §SC0000 anim cluster. @@ -295,8 +296,9 @@ The handler requires an existing destination texture, allocates/reuses a 0x478-b - **grounding:** source=investigation, confidence=high ### 0x239 `u004223C0` (u004223C0, argc 6) -- **summary:** 0x239 set-srcrect-cell (handle)(p3)(p4)(gridW)(gridH)(cell): set the spritesheet grid + static visible cell. Worker gfx_worker_set_srcrect_cell @0x47ed90. C# VM: GfxState.SetSrcRect (period 0). See docs/engine-re.md §SC0000 anim cluster. -- **grounding:** source=kelebek, confidence=low +- **summary:** (handle)(delay_ms)(duration_ms)(frame_count)(column_count)(target_frame) — one-shot row-major source-rectangle cell channel. Worker gfx_worker_set_srcrect_cell @0x47ed90 stores timing at obj+0x48/+0x5c, layout at +0x238/+0x23c, and target at +0x234. C# currently retains the endpoint cell immediately. +- **grounding:** source=investigation, confidence=high +- **evidence:** Native /v2 worker and gfx_object_apply_transform_channels decompile. The consumer advances target_frame cells over duration after delay, preserves the existing source-rect dimensions, and commits the endpoint. ### 0x23f `u00422930` (u00422930, argc 2) - **summary:** 0x23f query-object (out)(handle): return object status (FUN_0042a520; -1 if none). C# VM: 0 if the object exists else -1. See docs/engine-re.md §SC0000 anim cluster. diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 4564754..6ffed03 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -646,7 +646,7 @@ plan `.../plans/2026-07-08-sc0000-anim-transform-cluster.md`, branch `feat/anim- **Built (hybrid: engine resolves, host blits):** - `GfxObject` gains src-rect (spritesheet) + animated-color channels; `GfxState.SetSrcRect` / `SetColorAnim`; `BlendMath.PingPong`; `SnapshotVisibleObjects(long nowMs)` = a port of `gfx_object_anim_interpolate` - (ping-pong the spritesheet cell + color/glow), driven by the **`FrameClock`** (the "mach 5" pacing fix). + (looping row-major spritesheet cells + ping-pong color/glow), driven by the **`FrameClock`** (the "mach 5" pacing fix). - Wired 7 ops: `0x22f`/`0x229` position (direct V24 set), `0x239`/`0x231` spritesheet (static cell / animate), `0x232` color glow (ping-pong), `0x228`/`0x23f` queries (geometry back to script vars). @@ -1380,5 +1380,16 @@ distinct from `0x21e`'s one-shot target. SC0000 scales the two AE001D circles to them at 100%, almost entirely below the viewport. `GfxState.SetCurrentScale` and VM dispatch now reproduce the native matrices, with focused geometry/dispatch tests. Validation: engine **145/145**, opcode build/lint, Godot build/capture, Ghidra annotations saved, and user confirmation that the CG and animated glow are both -visible. This AE001D path is a retained scaled/rotating texture; later missing `0x231` spritesheet-cell -effects remain separately scoped. +visible. This AE001D path is a retained scaled/rotating texture; the later `0x231` spritesheet-cell issue +was separately resolved by preserving draw-texture's 200x200 cell and interpreting `(8,4)` as total +frames/columns rather than a grid divisor. + +### A2b -- SC0000 post-movie AE001H spritesheet correction ✅ DONE (2026-07-11) + +The reported missing cave spirit was the `0x231` path, independent of the earlier AE001D retained glow. +Native interpolation proved `(period, 8, 4)` means milliseconds per frame, total frames, and columns; +the existing 200x200 draw-texture rectangle is one cell and must not be divided again. The retained model +now cycles the eight AE001H cells row-major and wraps. Focused tests cover the exact 800x400/4x2 layout. +Validation: engine **146/146**, opcode sources regenerated, Godot zero-warning build and threaded selftest, +real-scene compositor trace across all eight cells, Ghidra `/v2` annotations saved, and user confirmation +on the normal client path. The automated-shot wrong-CG state remains the pre-existing separate gotcha. diff --git a/engine/Age.Engine.Tests/AnimChannelTests.cs b/engine/Age.Engine.Tests/AnimChannelTests.cs index b619e88..baf4fa2 100644 --- a/engine/Age.Engine.Tests/AnimChannelTests.cs +++ b/engine/Age.Engine.Tests/AnimChannelTests.cs @@ -62,7 +62,8 @@ public class AnimChannelTests vm.Run(); var o = vm.Gfx.TryGet(0x1000)!; Assert.True(o.SrcAnim); - Assert.Equal(4, o.SrcGridW); + Assert.Equal(4, o.SrcFrameCount); + Assert.Equal(1, o.SrcColumns); Assert.Equal(2, o.SrcCell); } @@ -78,9 +79,9 @@ public class AnimChannelTests public void SetSrcRect_StoresGridCellPeriod() { var g = VisibleObj(0x100); - g.SetSrcRect(0x100, gridW: 4, gridH: 1, cell: 2, period: 800); + g.SetSrcRect(0x100, frameCount: 4, columns: 1, cell: 2, period: 800); var o = g.TryGet(0x100)!; - Assert.Equal(4, o.SrcGridW); + Assert.Equal(4, o.SrcFrameCount); Assert.Equal(2, o.SrcCell); Assert.Equal(800, o.SrcPeriod); Assert.True(o.SrcAnim); @@ -91,10 +92,10 @@ public class AnimChannelTests public void SetSrcRect_ClampsGridToAtLeastOne() { var g = VisibleObj(0x100); - g.SetSrcRect(0x100, gridW: 0, gridH: 0, cell: 0, period: 0); + g.SetSrcRect(0x100, frameCount: 0, columns: 0, cell: 0, period: 0); var o = g.TryGet(0x100)!; - Assert.Equal(1, o.SrcGridW); - Assert.Equal(1, o.SrcGridH); + Assert.Equal(1, o.SrcFrameCount); + Assert.Equal(1, o.SrcColumns); } [Fact] diff --git a/engine/Age.Engine.Tests/AnimInterpolatorTests.cs b/engine/Age.Engine.Tests/AnimInterpolatorTests.cs index 779d29a..794ac28 100644 --- a/engine/Age.Engine.Tests/AnimInterpolatorTests.cs +++ b/engine/Age.Engine.Tests/AnimInterpolatorTests.cs @@ -8,7 +8,7 @@ public class AnimInterpolatorTests { var g = new GfxState(); g.SetSurface(1, resId: 5, colorKey: -1); - g.BindDraw(handle, 1, 0, 0, 256, 64, 100, 100); // 256x64 sheet, base pos (100,100) + g.BindDraw(handle, 1, 0, 0, 64, 64, 100, 100); // one 64x64 cell, base pos (100,100) return g; } @@ -24,22 +24,24 @@ public class AnimInterpolatorTests public void SrcRect_StaticCell_SelectsSubRect() { var g = VisibleObj(0x100); - g.SetSrcRect(0x100, gridW: 4, gridH: 1, cell: 2, period: 0); // static cell 2 of 4 across 256px + g.SetSrcRect(0x100, frameCount: 4, columns: 4, cell: 2, period: 0); var ro = g.SnapshotVisibleObjects(0).Single(); Assert.Equal(128, ro.SrcX); // cell 2 * 64 Assert.Equal(64, ro.W); } [Fact] - public void SrcRect_Animated_PingPongsCellAcrossGrid() + public void SrcRect_Animated_AdvancesOneCellPerPeriodAndWraps() { var g = VisibleObj(0x100); - g.SetSrcRect(0x100, gridW: 4, gridH: 1, cell: 0, period: 1000); + g.SetSrcRect(0x100, frameCount: 4, columns: 4, cell: 0, period: 100); g.SnapshotVisibleObjects(0); // seeds start=0 - var half = g.SnapshotVisibleObjects(500).Single(); - Assert.Equal(192, half.SrcX); // t=1 -> last cell (3) * 64 - var back = g.SnapshotVisibleObjects(1000).Single(); - Assert.Equal(0, back.SrcX); // ping-ponged back to cell 0 + var second = g.SnapshotVisibleObjects(100).Single(); + Assert.Equal(64, second.SrcX); + var fourth = g.SnapshotVisibleObjects(300).Single(); + Assert.Equal(192, fourth.SrcX); + var wrapped = g.SnapshotVisibleObjects(400).Single(); + Assert.Equal(0, wrapped.SrcX); } [Fact] diff --git a/engine/Age.Engine.Tests/GfxAnimationTests.cs b/engine/Age.Engine.Tests/GfxAnimationTests.cs index 266311e..6b26958 100644 --- a/engine/Age.Engine.Tests/GfxAnimationTests.cs +++ b/engine/Age.Engine.Tests/GfxAnimationTests.cs @@ -82,6 +82,27 @@ public class GfxAnimationTests Assert.True(rotation.HasActiveVisualPresentation(1000)); } + [Fact] + public void LoopingSpritesheet_PreservesCellSize_AdvancesRowMajor_AndWraps() + { + var g = new GfxState(); + g.SetSurface(4, 0x37, -1); + g.BindDraw(0xcf3a, 4, 0, 0, 200, 200, 300, 100); // AE001H: 800x400, eight 200x200 cells + g.SetSrcRect(0xcf3a, frameCount: 8, columns: 4, cell: 0, period: 100); + + var first = g.SnapshotVisibleObjects(1000).Single(); + var second = g.SnapshotVisibleObjects(1100).Single(); + var fifth = g.SnapshotVisibleObjects(1400).Single(); + var eighth = g.SnapshotVisibleObjects(1700).Single(); + var wrapped = g.SnapshotVisibleObjects(1800).Single(); + + Assert.Equal((0, 0, 200, 200), (first.SrcX, first.SrcY, first.W, first.H)); + Assert.Equal((200, 0, 200, 200), (second.SrcX, second.SrcY, second.W, second.H)); + Assert.Equal((0, 200, 200, 200), (fifth.SrcX, fifth.SrcY, fifth.W, fifth.H)); + Assert.Equal((600, 200, 200, 200), (eighth.SrcX, eighth.SrcY, eighth.W, eighth.H)); + Assert.Equal((0, 0, 200, 200), (wrapped.SrcX, wrapped.SrcY, wrapped.W, wrapped.H)); + } + [Fact] public void OneShotRotation_SharesMatrixClockAndMatchesNativeSample() { diff --git a/engine/Age.Engine/Model/GfxState.cs b/engine/Age.Engine/Model/GfxState.cs index c382d72..808c3eb 100644 --- a/engine/Age.Engine/Model/GfxState.cs +++ b/engine/Age.Engine/Model/GfxState.cs @@ -71,9 +71,10 @@ public sealed class GfxState // fades from a static mode-0 0x203 such as a CG initialized with 0x00ffffff (opaque identity). public bool OneShotColorBlend; - // ---- src-rect / spritesheet-cell channel (ops 0x239 static cell, 0x231 animate). Interpolator - // SRC-RECT SCROLL channel: period obj+0x230, start obj+0x21c, grid obj+0x238/0x23c. ---- - public long SrcGridW = 1, SrcGridH = 1, SrcCell, SrcPeriod, SrcStart = -1; + // ---- src-rect / spritesheet-cell channel (ops 0x239 one-shot cell, 0x231 looping animation). + // Native obj+0x238 is the TOTAL FRAME COUNT and +0x23c is the COLUMN COUNT. Each frame keeps + // draw-texture's original source-rect size; those operands are not a grid to divide it by. ---- + public long SrcFrameCount = 1, SrcColumns = 1, SrcCell, SrcPeriod, SrcStart = -1; public bool SrcAnim; // ---- animated color/glow channel (op 0x232). Interpolator COLOR channel: period obj+0x220, // start obj+0x20c, target obj+0x240 — PING-PONG (distinct from static 0x202/0x203). ---- @@ -158,7 +159,7 @@ public sealed class GfxState OneShotColorTarget = s.OneShotColorTarget, ColorDelayMs = s.ColorDelayMs, ColorDurationMs = s.ColorDurationMs, OneShotColorEnabled = s.OneShotColorEnabled, OneShotColorBlend = s.OneShotColorBlend, - SrcGridW = s.SrcGridW, SrcGridH = s.SrcGridH, SrcCell = s.SrcCell, + SrcFrameCount = s.SrcFrameCount, SrcColumns = s.SrcColumns, SrcCell = s.SrcCell, SrcPeriod = s.SrcPeriod, SrcStart = s.SrcStart, SrcAnim = s.SrcAnim, ColorPeriod = s.ColorPeriod, ColorStart = s.ColorStart, ColorTarget = s.ColorTarget, ColorAnim = s.ColorAnim, SourceSlot = s.SourceSlot, SrcRect = s.SrcRect, Visible = s.Visible, @@ -282,14 +283,16 @@ public sealed class GfxState lock (_lock) GetOrCreate(handle).StaticColorMode = mode; } - /// Ops 0x239 (static cell, period=0) / 0x231 (animate, period>0): set the spritesheet grid + - /// the visible cell. When animated, the interpolator ping-pongs the cell across the grid over the period. - public void SetSrcRect(long handle, long gridW, long gridH, long cell, long period) + /// Ops 0x239 (one-shot cell endpoint, period=0 in this retained model) / 0x231 (looping): + /// set total frame count, column count, and visible cell. Every frame retains draw-texture's source + /// rectangle dimensions. For 0x231, is milliseconds per frame. + public void SetSrcRect(long handle, long frameCount, long columns, long cell, long period) { lock (_lock) { var o = GetOrCreate(handle); - o.SrcGridW = gridW < 1 ? 1 : gridW; o.SrcGridH = gridH < 1 ? 1 : gridH; + o.SrcFrameCount = frameCount < 1 ? 1 : frameCount; + o.SrcColumns = columns < 1 ? 1 : columns; o.SrcCell = cell; o.SrcPeriod = period; o.SrcStart = -1; o.SrcAnim = true; } } @@ -555,23 +558,18 @@ public sealed class GfxState blend = BlendKind.Alpha; } - // ---- src-rect: pick the spritesheet cell (static or ping-ponged across the grid) ---- + // ---- src-rect: preserve cell dimensions and offset it row-major through the sheet ---- int srcX = o.SrcRect.X, srcY = o.SrcRect.Y, w = o.SrcRect.W, h = o.SrcRect.H; - if (o.SrcAnim && o.SrcGridW >= 1) + if (o.SrcAnim && o.SrcFrameCount >= 1) { - int cellW = (int)(o.SrcRect.W / o.SrcGridW); - int cellH = o.SrcGridH >= 1 ? (int)(o.SrcRect.H / o.SrcGridH) : o.SrcRect.H; long cell = o.SrcCell; if (o.SrcPeriod > 0) { if (o.SrcStart < 0) o.SrcStart = nowMs; - double t = PingPongWeight(nowMs, o.SrcStart, o.SrcPeriod); - cell = (long)System.Math.Round(t * (o.SrcGridW - 1)); + cell = ((nowMs - o.SrcStart) / o.SrcPeriod) % o.SrcFrameCount; } - long cols = o.SrcGridW; - srcX = o.SrcRect.X + (int)(cell % cols) * cellW; - srcY = o.SrcRect.Y + (int)(cell / cols) * cellH; - w = cellW; h = cellH; + srcX = o.SrcRect.X + (int)(cell % o.SrcColumns) * o.SrcRect.W; + srcY = o.SrcRect.Y + (int)(cell / o.SrcColumns) * o.SrcRect.H; } // One-shot matrix channels: hold current through delay, then linearly sample current -> target. diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index cd4e6c9..8e3e154 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -387,9 +387,9 @@ public sealed class VirtualMachine 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 + case "u004223C0": // 0x239 spritesheet cell: (handle)(delay)(duration)(frame count)(columns)(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 + case "u00421EA0": // 0x231 looping spritesheet: (handle)(ms per frame)(frame count)(columns) 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; diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 9ce95b3..9f1c00b 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -5999,12 +5999,12 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "u00421EA0" category = "draw" -summary = "0x231 anim-srcrect (handle)(period)(gridW)(gridH): ping-pong the spritesheet cell across the grid over period ms. Worker gfx_worker_anim_srcrect @0x47eec0 -> interpolator SRC-RECT SCROLL channel. C# VM: GfxState.SetSrcRect. See docs/engine-re.md §SC0000 anim cluster." +summary = "(handle)(frame_period_ms)(frame_count)(column_count) — loop row-major through the spritesheet. Every frame preserves draw-texture's source-rectangle width/height; frame=floor(elapsed/frame_period)%frame_count, src offset=(frame%columns*width, frame/columns*height). Worker gfx_worker_anim_srcrect @0x47eec0; consumer gfx_object_anim_interpolate @0x473ed0." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Native /v2 decompile: worker stores period at obj+0x230, frame_count at +0x238, columns at +0x23c. Interpolator computes ((now-start)/period)%frame_count, then offsets both source-rect X bounds by rect_width*(frame%columns) and Y bounds by rect_height*(frame/columns). SC0000 uses (100,8,4) with AE001H's eight 200x200 cells in a 4x2 800x400 sheet." [[opcode.semantics.args]] i = 1 @@ -6211,12 +6211,12 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "u004223C0" category = "draw" -summary = "0x239 set-srcrect-cell (handle)(p3)(p4)(gridW)(gridH)(cell): set the spritesheet grid + static visible cell. Worker gfx_worker_set_srcrect_cell @0x47ed90. C# VM: GfxState.SetSrcRect (period 0). See docs/engine-re.md §SC0000 anim cluster." +summary = "(handle)(delay_ms)(duration_ms)(frame_count)(column_count)(target_frame) — one-shot row-major source-rectangle cell channel. Worker gfx_worker_set_srcrect_cell @0x47ed90 stores timing at obj+0x48/+0x5c, layout at +0x238/+0x23c, and target at +0x234. C# currently retains the endpoint cell immediately." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Native /v2 worker and gfx_object_apply_transform_channels decompile. The consumer advances target_frame cells over duration after delay, preserves the existing source-rect dimensions, and commits the endpoint." [[opcode.semantics.args]] i = 1