Implement detached object animation control

This commit is contained in:
gamer147
2026-07-20 14:53:34 -04:00
parent ae157da04c
commit d1af8e3b07
10 changed files with 231 additions and 29 deletions

View File

@@ -25,11 +25,12 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
| `0x50ba4` | `gfx_render_target_table` | `void*` | retained_gfx_manager+0xa590 array of per-slot D3D texture wrappers selected by op 0x20d |
| `0x51b44` | `gfx_current_render_target` | `int` | retained_gfx_manager+0xb530 selected surface slot; -1 denotes device backbuffer |
| `0x51b64` | `frame_timer` | `int` | frame timer at retained_gfx_manager+0xb550; present updates 0x51b64/0x51b68 and object animation samples it |
| `0x51b6c` | `gfx_dirty_a` | `int` | retained_gfx_manager+0xb558 dirty flag; op 0x1f6 clears it |
| `0x51b6c` | `gfx_dirty_a` | `int` | retained_gfx_manager+0xb558 redraw-dirty flag; active finite object channels set it even when detached by 0x242; op 0x1f6 clears it |
| `0x51b70` | `gfx_force_complete` | `int` | retained_gfx_manager+0xb55c requests forced completion of one-shot channels; op 0x1f6 clears it |
| `0x51b74` | `gfx_dirty_b` | `int` | retained_gfx_manager+0xb560 dirty flag |
| `0x51b74` | `gfx_dirty_b` | `int` | retained_gfx_manager+0xb560 blocking-presentation dirty flag; active finite object channels set it unless obj+0x2d0 bit 0 detaches them |
| `0x51b78` | `anim_clock_elapsed` | `int` | global anim clock elapsed at retained_gfx_manager+0xb564; op 0x238 and object reset 0x1f6 zero it |
| `0x51b7c` | `anim_clock_duration` | `int` | global anim clock duration at retained_gfx_manager+0xb568; op 0x238 sets and object reset 0x1f6 zeroes it |
| `0x51b80` | `gfx_animation_service_flags` | `uint` | retained_gfx_manager+0xb56c service flags; bit 1 suppresses op 0x243 force-complete/clock-reset request |
| `0x52bd4` | `surfaces` | `void*` | surface array base [~1000 slots]; create/set-texture (0x1f8/0x1f9) allocate |
| `0x53d14` | `cur_ctx_index` | `uint` | current script-context index (curCtx); indexes 0x78-byte coroutine/frame records |
| `0x53d28` | `frame_codebase` | `void*` | current frame codebase (PC = codebase + off*4) |

View File

@@ -439,8 +439,8 @@ The preceding query-gated cleanup had skipped this created-but-unbound object be
`SourceSlot=-1`. Native `gfx_object_init_default` zeroes `obj+4`, so op `0x215` returns slot 0, the cleanup
erases the object, and the later bind recreates identity state. Matching that default leaves EV050EA centered,
unrotated, and opaque in the synchronized page-58 compositor trace. The adjacent op `0x242(handle,0)` is not
a reset: `op_0x242_set_object_field2d0@0x4249d0` calls `gfx_object_set_field2d0@0x47f1a0`, which only writes
the supplied value at `obj+0x2d0`.
a reset: it clears the object's detached-animation control word after binding. Its consumer and complete
lifecycle are documented under “Detached finite object animation” below.
**Page-89 surface-lifetime correction (2026-07-11).** Locator `SC0000 P089` resolves to
`wait@0x89e9`; the loader at `0x8976..0x898a` resolves resource `0x6a` to `BG004D.AGF`. The background
@@ -865,8 +865,9 @@ integer, bytewise linear interpolation to target `+0x64` for duration `+0x4c`. A
when retained-gfx `owner+0xb55c == 1` (`EngineCtx+0x51b70`) requests forced completion—the target commits
to current, delay/duration clear,
target becomes `0xffffffff`, and the one-shot active bit clears when no color/matrix/src-rect sibling remains.
The object-local override bit at `+0x2d0` suppresses the global force. Negative alpha/RGB target operands
independently preserve their bytes from current `+0x60`.
The object-local bit at `+0x2d0` suppresses the global force and keeps that finite group from raising the
blocking-presentation dirty flag; it remains redraw-active and therefore animates asynchronously. Negative
alpha/RGB target operands independently preserve their bytes from current `+0x60`.
The port now carries current and target separately and samples them from the unified `FrameClock`; an op
`0x203` static write after `0x202` therefore becomes the ramp's current value rather than overwriting its
@@ -975,8 +976,9 @@ negative operands in `SetColorAnimResolved`, samples packed ARGB before blend se
the existing mode-specific path. Exact AE001H, mode-0 RGB-modulation, and mode-1 alpha regressions cover the
contract; the white pulse is removed without suppressing the scripted channel.
**Resolved 2026-07-11:** `0x236` is the movie-to-retained-surface path described below. `0x242` is now
classified as the retained-object `+0x2d0` field setter described above; `0x23d/0x20a/0x20e` remain GAP.
**Resolved 2026-07-20:** `0x236` is the movie-to-retained-surface path described below. `0x242` is the
detached finite-animation control described below; `0x23d`, `0x20a`, and `0x20e` are also implemented in
their later lifecycle/presentation slices.
### Movie-to-surface opcode `0x236` (2026-07-11)
@@ -1899,6 +1901,40 @@ backbuffer. Fresh offscreen surfaces commonly use `0x20d` then `0x20e` before dr
---
### Detached finite object animation -- opcodes `0x242` / `0x243` (2026-07-20)
`op_0x242_set_object_animation_detached@0x4249d0` fetches `(handle, flags)` and calls
`gfx_object_set_animation_control@0x47f1a0`, which get-or-creates the retained object and replaces its
32-bit control word at `obj+0x2d0`. Only bit 0 has a located consumer. It does not reset geometry, color,
or matrix state.
`gfx_object_apply_transform_channels@0x472f00` gives bit 0 two connected effects on the finite one-shot
group (packed color, scale, rotation, translation, and timed source rectangle):
- Manager `+0xb55c` value 1 normally forces every finite channel to its endpoint. Bit 0 masks that request
for this object.
- An unfinished ordinary object raises manager `+0xb560`, keeping blocking presentation active. A bit-0
object does not raise that flag, but still raises redraw-dirty `+0xb558`, so it continues animating while
script execution proceeds.
When no finite channel remains, the consumer clears bit 0 and the shared start timestamp. The control is
therefore a per-animation detachment flag, not a persistent object mode. Object initialization sets the
word to zero, and native clone copies it with the rest of the complete object record.
`op_0x243_force_complete_and_reset_anim_clock@0x4182d0` supplies the paired global operation. When service
flags manager `+0xb56c` bit 1 is clear, it sets force-complete `+0xb55c` (`EngineCtx+0x51b70`) to 1 and
zeros the separate animation-service elapsed/duration fields at `+0xb564/+0xb568`. The next composition
commits every ordinary finite group; detached objects ignore the request. The port performs that observable
commit directly when executing `0x243`, excludes detached groups from its blocking wait predicate, and
continues including them in its visual recomposition predicate until natural completion.
Corpus evidence is unusually sharp: all 303 calls pass an immediate flag, with zero used 302 times and one
used once. SC0000's two CG-loader sites (`0x12723`, `0x13310`) write zero. `BTL.BIN@0x2b4d` writes one on an
animated battle object after its texture/movie or sprite-cell setup, matching the nonblocking background
animation contract. No boot seed, script-offset branch, or persistence state is involved.
---
## Native walls backlog (targets for this loop)
- ~~**call-script dispatch**~~ — **SOLVED** (above): `call-script <id>` = raw SYS4INI file index.

View File

@@ -548,15 +548,19 @@ For each fixed slot in [42,1000), the handler stops/releases the movie-to-textur
- **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.
- **grounding:** source=kelebek, confidence=low
### 0x242 `set-gfx-field2d0` (set-gfx-field2d0, argc 2)
- **summary:** 0x242 (handle)(value) — get-or-create the retained gfx object and write value to obj+0x2d0; the generic instruction length is 5 dwords. SC0000's common CG loader passes zero after draw binding. This field does not reset transform or color channels; its downstream purpose remains unknown.
### 0x242 `set-object-animation-detached` (set-object-animation-detached, argc 2)
- **summary:** Replace the retained object's animation-control word at obj+0x2d0. Bit 0 detaches finite one-shot channels from blocking presentation and protects them from 0x243 forced completion until they finish naturally.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x242_set_object_field2d0@0x4249d0 fetches operands 2 and 1 and calls gfx_object_set_field2d0@0x47f1a0; the worker calls gfx_object_get_or_create then stores operand 2 at returned object+0x2d0.
- **depended on by:** 0x243
- **evidence:** Ghidra /v2: op_0x242_set_object_animation_detached@0x4249d0 calls gfx_object_set_animation_control@0x47f1a0. Consumer gfx_object_apply_transform_channels@0x472f00 tests obj+0x2d0 bit 0 to suppress manager+0xb55c forced completion and manager+0xb560 blocking-dirty publication, while still setting manager+0xb558 redraw dirty; it clears bit 0 when the finite group completes. DATA1: 303 calls; values zero x302, one x1.
The setter get-or-creates the object and writes the complete operand. During retained composition, bit 0 makes the one-shot color/scale/rotation/translation/source-rectangle group ignore the manager's force-complete request and suppresses the blocking-dirty flag while leaving the redraw-dirty flag active. When no finite channel remains, native clears bit 0 and the shared start time. SC0000's two common CG loaders explicitly write zero. Across DATA1, 302 calls write zero and the sole nonzero call is BTL.BIN's animated battle-object setup, which writes one.
### 0x243 `reset-anim-clock` (reset-anim-clock, argc 0)
- **summary:** Reset the native global animation-service elapsed and duration fields to zero when service flag bit 1 is clear.
- **summary:** When animation-service flag bit 1 is clear, request forced completion of ordinary finite one-shot object channels and reset the global animation-service elapsed/duration fields. Objects detached by 0x242 bit 0 ignore the force request.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra handler 0x4182d0: if !(ctx+0x51b80 & 2), set ctx+0x51b70=1 and zero ctx+0x51b78/+0x51b7c. Normal SC0000 label_1235a calls it before present-frame.
- **depends on:** 0x242
- **evidence:** Ghidra handler 0x4182d0: if !(ctx+0x51b80 & 2), set retained-gfx owner+0xb55c (EngineCtx+0x51b70)=1 and zero owner+0xb564/+0xb568. gfx_object_apply_transform_channels treats force value 1 as immediate completion unless obj+0x2d0 bit 0 is set. SC0000 label_1235a calls it before present-frame.
## input

View File

@@ -2076,3 +2076,31 @@ and `0x1cb` behind the future profile/save ownership decision.
Validation: all 209 engine tests pass; opcode and EngineCtx tests/lints, vm0 RECOVER, the Godot build,
threaded self-test, and `git diff --check` are clean.
### Slice A2b detached finite object animation implemented (2026-07-20)
The `obj+0x2d0` consumer resolves `0x242(handle,flags)` as a finite-animation detachment control. Bit 0
makes that object's one-shot color/scale/rotation/translation/source-rectangle group ignore `0x243`'s
global force-complete request and prevents it from holding native blocking-presentation dirty state, while
leaving redraw dirty so it continues animating asynchronously. Native clears bit 0 after the finite group
finishes. SC0000's two CG loaders pass zero; 302 of 303 corpus calls do the same. The only value-one use is
an animated battle object in `BTL.BIN`.
`GfxState` now retains and clones the control word, excludes detached channels from blocking waits without
excluding them from visual recomposition, protects them from `0x243`, and clears bit 0 on natural completion.
The `0x243` model is correspondingly completed: it commits every unprotected finite channel as well as
resetting the separate global animation clock. Focused regressions cover get-or-create/clone behavior and
the protected-versus-ordinary completion split.
The canonical opcode source is refined and regenerated. EngineCtx adds the animation-service flags at
`0x51b80` and refines the two dirty-field roles; the expanded 76-field structure is reapplied to `/v2`.
The `0x242` handler/worker, `0x243`, and the shared channel consumer are named/commented and saved. SC0000
is now **122/129 distinct opcodes handled (94.6%)** and **16,039/16,257 instructions handled or safe-noop
(98.7%)**. Its remaining seven gaps total 218 instructions, 206 of them the deferred shared-profile write.
**Next:** investigate the four adjacent one-call unknowns `0x19b`, `0x19c`, `0x1ca`, and `0xae` together
before choosing an implementation. Their low frequency makes a shared native-service relationship more
important than any one opcode's count; persistence-bound `0x1a2`, `0x1ad`, and `0x1cb` remain deferred.
Validation: all 211 engine tests pass; opcode and EngineCtx tests/lints, vm0 RECOVER, the zero-warning
Godot build, threaded self-test, and `git diff --check` are clean.

View File

@@ -0,0 +1,61 @@
using System.Collections.Generic;
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
using Xunit;
namespace Age.Engine.Tests;
public class GfxAnimationDetachTests
{
private static OpcodeTable T() => OpcodeTableJson.Load(Paths.OpcodesJson);
private static Operand G(int address) => new(3, address);
private static Operand I(long value) => new(0, value);
private static (int, Operand[]) Mov(int address, long value) => (0x55, new[] { G(address), I(value) });
private static (int, Operand[]) Exit() => (0x2, System.Array.Empty<Operand>());
[Fact]
public void Opcode242SetsControlWordOnGetOrCreateObjectAndCloneCopiesIt()
{
var table = T();
var scene = ScriptAssembler.Assemble(table, "ANIM-DETACH", new List<(int, Operand[])>
{
Mov(1, 0x100), Mov(2, 0x101),
(0x242, new[] { G(1), I(1) }),
(0x21d, new[] { G(1), G(2) }),
Exit(),
}, System.Array.Empty<string>());
var vm = new VirtualMachine(scene, table, new RecordingHost());
vm.Run();
Assert.Equal(1, vm.Gfx.TryGet(0x100)!.OneShotAnimationControlFlags);
Assert.Equal(1, vm.Gfx.TryGet(0x101)!.OneShotAnimationControlFlags);
}
[Fact]
public void DetachedOneShotSurvivesGlobalCompletionWithoutBlockingAndClearsFlagNaturally()
{
var gfx = new GfxState();
gfx.SetSurface(1, 0x20, -1);
gfx.BindDraw(0x100, 1, 0, 0, 100, 100, 0, 0);
gfx.BindDraw(0x101, 1, 0, 0, 100, 100, 0, 0);
gfx.SetAnimatedObjectColorResolved(0x100, 0, 1000, 0, 0xffffff);
gfx.SetAnimatedObjectColorResolved(0x101, 0, 1000, 0, 0xffffff);
gfx.SetOneShotAnimationControl(0x100, 1);
gfx.ResetAnimClock();
Assert.True(gfx.TryGet(0x100)!.OneShotColorEnabled);
Assert.False(gfx.TryGet(0x101)!.OneShotColorEnabled);
Assert.False(gfx.HasActiveTimedPresentation(1000));
Assert.True(gfx.HasActiveVisualPresentation(1000));
gfx.SnapshotVisibleObjects(1000);
Assert.True(gfx.SnapshotVisibleObjects(1500).Single(x => x.Handle == 0x100).ColorTransition!.Value.Active);
gfx.SnapshotVisibleObjects(2000);
Assert.False(gfx.TryGet(0x100)!.OneShotColorEnabled);
Assert.Equal(0, gfx.TryGet(0x100)!.OneShotAnimationControlFlags & 1);
}
}

View File

@@ -260,7 +260,7 @@ public class GfxAnimationTests
}
[Fact]
public void ResetAnimClock_DispatchClearsOnlyGlobalServiceClock()
public void ResetAnimClock_DispatchClearsGlobalServiceClock()
{
var t = T();
var scene = ScriptAssembler.Assemble(t, "CLOCKRESET", new List<(int, Operand[])>

View File

@@ -100,6 +100,9 @@ public sealed class GfxState
public (double X, double Y, double Z, double Angle) RotationTarget;
public long RotationDelayMs, RotationDurationMs;
public bool RotationChannelEnabled;
// Op 0x242 writes obj+0x2d0. Bit 0 detaches the finite one-shot group from the blocking-dirty
// service and protects it from op 0x243's global force-completion request until natural completion.
public long OneShotAnimationControlFlags;
// Shared matrix-channel start timestamp obj+0x34, seeded from retained-gfx owner+0xb550
// (EngineCtx+0x51b64).
public long OneShotStartMs = -1;
@@ -170,7 +173,9 @@ public sealed class GfxState
TranslationEnabled = s.TranslationEnabled,
RotationCurrent = s.RotationCurrent, RotationTarget = s.RotationTarget,
RotationDelayMs = s.RotationDelayMs, RotationDurationMs = s.RotationDurationMs,
RotationChannelEnabled = s.RotationChannelEnabled, OneShotStartMs = s.OneShotStartMs,
RotationChannelEnabled = s.RotationChannelEnabled,
OneShotAnimationControlFlags = s.OneShotAnimationControlFlags,
OneShotStartMs = s.OneShotStartMs,
RotationPeriodMs = s.RotationPeriodMs, RotationAxis = s.RotationAxis,
RotationEnabled = s.RotationEnabled, RotationStartMs = s.RotationStartMs,
};
@@ -395,10 +400,58 @@ public sealed class GfxState
lock (_lock)
return _surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0) ||
_objects.Values.Any(o => o.Visible &&
(o.OneShotAnimationControlFlags & 1) == 0 &&
(o.OneShotColorEnabled || o.ScaleEnabled ||
o.RotationChannelEnabled || o.TranslationEnabled));
}
/// <summary>Op 0x242: replace the retained object's animation-control word. Native bit 0 makes its
/// finite one-shot channels nonblocking and immune to op 0x243 forced completion.</summary>
public void SetOneShotAnimationControl(long handle, long flags)
{
lock (_lock) GetOrCreate(handle).OneShotAnimationControlFlags = flags;
}
/// <summary>Part of op 0x243: immediately commit every unprotected finite one-shot channel. Objects
/// marked by op 0x242 bit 0 keep sampling asynchronously.</summary>
private void ForceCompleteOneShotChannels()
{
foreach (var o in _objects.Values)
{
if ((o.OneShotAnimationControlFlags & 1) != 0) continue;
if (o.OneShotColorEnabled)
{
o.Color = o.OneShotColorTarget & 0xffffffff;
o.OneShotColorTarget = -1;
o.ColorDelayMs = 0;
o.ColorDurationMs = 0;
o.OneShotColorEnabled = false;
}
if (o.ScaleEnabled)
{
o.ScaleCurrent = o.ScaleTarget;
o.ScaleDelayMs = 0;
o.ScaleDurationMs = 0;
o.ScaleEnabled = false;
}
if (o.RotationChannelEnabled)
{
o.RotationCurrent = o.RotationTarget;
o.RotationDelayMs = 0;
o.RotationDurationMs = 0;
o.RotationChannelEnabled = false;
}
if (o.TranslationEnabled)
{
o.TranslationCurrent = o.TranslationTarget;
o.TranslationDelayMs = 0;
o.TranslationDurationMs = 0;
o.TranslationEnabled = false;
}
o.OneShotStartMs = -1;
}
}
/// <summary>Whether sampling the retained scene at a later frame can change its pixels without another
/// VM mutation. Includes finite presentation work plus the ambient channels that may remain active while
/// the interpreter is parked at an input wait. Static waits themselves are deliberately not animation.</summary>
@@ -555,10 +608,16 @@ public sealed class GfxState
lock (_lock) { AnimClockDurationTicks = durationTicks; AnimClockGeneration++; }
}
/// <summary>Op 0x243: reset the separate global animation-service clock.</summary>
/// <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) { AnimClockDurationTicks = 0; AnimClockGeneration++; }
lock (_lock)
{
ForceCompleteOneShotChannels();
AnimClockDurationTicks = 0;
AnimClockGeneration++;
}
}
/// <summary>Back-compat: snapshot with no animation clock (nowMs = 0) — deterministic, for headless
@@ -579,6 +638,8 @@ public sealed class GfxState
{
var o = kv.Value;
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);
// ---- packed color: a static mode 0 treats alpha as tint/fill strength. Once op 0x202 has
@@ -644,7 +705,7 @@ public sealed class GfxState
}
// One-shot matrix channels: hold current through delay, then linearly sample current -> target.
if ((o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled || o.TranslationEnabled)
if (hadOneShot
&& o.OneShotStartMs < 0)
o.OneShotStartMs = nowMs;
var scale = SampleMatrixChannel(ref o.ScaleCurrent, o.ScaleTarget, o.ScaleDelayMs,
@@ -656,7 +717,10 @@ public sealed class GfxState
o.TranslationDelayMs, o.TranslationDurationMs,
o.OneShotStartMs, ref o.TranslationEnabled, nowMs);
if (!o.OneShotColorEnabled && !o.ScaleEnabled && !o.RotationChannelEnabled && !o.TranslationEnabled)
{
if (hadOneShot) o.OneShotAnimationControlFlags &= ~1L;
o.OneShotStartMs = -1;
}
double cycleAngle = 0;
if (o.RotationEnabled && o.RotationPeriodMs > 0)

View File

@@ -1102,7 +1102,9 @@ public sealed class VirtualMachine
Gfx.SetRotationCycle(Read(a[0]), Read(a[1]), (Read(a[2]), Read(a[3]), Read(a[4]))); return pc + 1;
case "set-anim-clock": // 0x238 (duration) — global, non-blocking (host advances it per-frame)
Gfx.SetAnimClock(Read(a[0])); return pc + 1;
case "reset-anim-clock": // 0x243: reset the separate global animation-service clock
case "set-object-animation-detached": // 0x242 (handle)(flags): bit 0 is nonblocking/force-proof
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 "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]),

View File

@@ -116,7 +116,7 @@ note = "retained_gfx_manager+0xb530 selected surface slot; -1 denotes device bac
offset = 0x51b6c
name = "gfx_dirty_a"
type = "int"
note = "retained_gfx_manager+0xb558 dirty flag; op 0x1f6 clears it"
note = "retained_gfx_manager+0xb558 redraw-dirty flag; active finite object channels set it even when detached by 0x242; op 0x1f6 clears it"
[[field]]
offset = 0x51b70
name = "gfx_force_complete"
@@ -126,7 +126,12 @@ note = "retained_gfx_manager+0xb55c requests forced completion of one-shot chann
offset = 0x51b74
name = "gfx_dirty_b"
type = "int"
note = "retained_gfx_manager+0xb560 dirty flag"
note = "retained_gfx_manager+0xb560 blocking-presentation dirty flag; active finite object channels set it unless obj+0x2d0 bit 0 detaches them"
[[field]]
offset = 0x51b80
name = "gfx_animation_service_flags"
type = "uint"
note = "retained_gfx_manager+0xb56c service flags; bit 1 suppresses op 0x243 force-complete/clock-reset request"
[[field]]
offset = 0x52bd4
name = "surfaces"

View File

@@ -6440,19 +6440,20 @@ observed_types = ["l-int"]
[[opcode]]
op = 0x242
label = "set-gfx-field2d0"
label = "set-object-animation-detached"
argc = 2
abi_source = "kelebek+decode-validated"
[opcode.semantics]
name = "set-gfx-field2d0"
name = "set-object-animation-detached"
category = "draw"
summary = "0x242 (handle)(value) — get-or-create the retained gfx object and write value to obj+0x2d0; the generic instruction length is 5 dwords. SC0000's common CG loader passes zero after draw binding. This field does not reset transform or color channels; its downstream purpose remains unknown."
summary = "Replace the retained object's animation-control word at obj+0x2d0. Bit 0 detaches finite one-shot channels from blocking presentation and protects them from 0x243 forced completion until they finish naturally."
details = "The setter get-or-creates the object and writes the complete operand. During retained composition, bit 0 makes the one-shot color/scale/rotation/translation/source-rectangle group ignore the manager's force-complete request and suppresses the blocking-dirty flag while leaving the redraw-dirty flag active. When no finite channel remains, native clears bit 0 and the shared start time. SC0000's two common CG loaders explicitly write zero. Across DATA1, 302 calls write zero and the sole nonzero call is BTL.BIN's animated battle-object setup, which writes one."
noop_headless = false
source = "investigation"
confidence = "high"
depends_on = []
evidence = "Ghidra /v2: op_0x242_set_object_field2d0@0x4249d0 fetches operands 2 and 1 and calls gfx_object_set_field2d0@0x47f1a0; the worker calls gfx_object_get_or_create then stores operand 2 at returned object+0x2d0."
evidence = "Ghidra /v2: op_0x242_set_object_animation_detached@0x4249d0 calls gfx_object_set_animation_control@0x47f1a0. Consumer gfx_object_apply_transform_channels@0x472f00 tests obj+0x2d0 bit 0 to suppress manager+0xb55c forced completion and manager+0xb560 blocking-dirty publication, while still setting manager+0xb558 redraw dirty; it clears bit 0 when the finite group completes. DATA1: 303 calls; values zero x302, one x1."
[[opcode.semantics.args]]
i = 1
@@ -6461,7 +6462,7 @@ observed_types = ["l-int", "l-ptr"]
[[opcode.semantics.args]]
i = 2
role = "field2d0_value"
role = "animation control flags; bit 0 = detached/nonblocking"
observed_types = ["imm"]
[[opcode]]
@@ -6473,12 +6474,12 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics]
name = "reset-anim-clock"
category = "draw"
summary = "Reset the native global animation-service elapsed and duration fields to zero when service flag bit 1 is clear."
summary = "When animation-service flag bit 1 is clear, request forced completion of ordinary finite one-shot object channels and reset the global animation-service elapsed/duration fields. Objects detached by 0x242 bit 0 ignore the force request."
noop_headless = false
source = "investigation"
confidence = "high"
depends_on = []
evidence = "Ghidra handler 0x4182d0: if !(ctx+0x51b80 & 2), set ctx+0x51b70=1 and zero ctx+0x51b78/+0x51b7c. Normal SC0000 label_1235a calls it before present-frame."
depends_on = [0x242]
evidence = "Ghidra handler 0x4182d0: if !(ctx+0x51b80 & 2), set retained-gfx owner+0xb55c (EngineCtx+0x51b70)=1 and zero owner+0xb564/+0xb568. gfx_object_apply_transform_channels treats force value 1 as immediate completion unless obj+0x2d0 bit 0 is set. SC0000 label_1235a calls it before present-frame."
[[opcode]]
op = 0x248