diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index 519b3a2..2021c0f 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -166,7 +166,9 @@ animation, numeric-glyph, and handle-range contracts. `engine/Age.Engine/Model/G resource/color-key state, created/reloadable classification, movie stop-time metadata, render-target/tile configuration, and surface lifecycle operations. `engine/Age.Engine/Model/GfxState.RetainedObjects.cs` owns the retained-object record and registry/index, range-transform state, object creation/query/clone/erase and draw -binding, plus numeric-glyph object generation. +binding, plus numeric-glyph object generation. `engine/Age.Engine/Model/GfxState.Animation.cs` owns the shared +animation clock, object color/source-cell/matrix/cyclic channels, animation control and forced completion, and +the interpolation helpers consumed by retained-scene sampling. The disposable `build/page-map-.jsonl` files are produced by editor/development Godot runs and map runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports diff --git a/docs/remake-architecture-and-roadmap.md b/docs/remake-architecture-and-roadmap.md index ad4cf59..72ef128 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -612,6 +612,12 @@ do not mix mechanical moves with semantic changes. into `engine/Age.Engine/Model/GfxState.RetainedObjects.cs`. Cross-domain reset/persistence, animation, and presentation sampling retain direct access through the sealed partial class; runtime validation remains green. + The fourth bounded `GfxState` split moved shared animation-clock state, object color/source-cell/matrix/cyclic + channels, animation control and forced completion, and interpolation helpers into + `engine/Age.Engine/Model/GfxState.Animation.cs`. Presentation diagnostics, dirty-reason accounting, transition + queues, and visible-scene sampling consume the moved state through the sealed partial class; runtime validation + remains green. + **Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after each domain move. @@ -983,7 +989,7 @@ layer's rendering diverges from ADV; save layout. ## 8. Immediate next step Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed by the tracked launcher and layered validation driver. With the planned `Main` and `GodotAdvHost` domains -isolated and the first three `GfxState` domains separated, move animation ownership next, preserving public types, -commands, and generated output. +isolated and the first four `GfxState` domains separated, move presentation ownership next, preserving public +types, commands, and generated output. Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does not replace Phase B gameplay validation or the open cross-platform gates. diff --git a/engine/Age.Engine/Model/GfxState.Animation.cs b/engine/Age.Engine/Model/GfxState.Animation.cs new file mode 100644 index 0000000..164cd80 --- /dev/null +++ b/engine/Age.Engine/Model/GfxState.Animation.cs @@ -0,0 +1,458 @@ +namespace Age.Engine.Model; + +public sealed partial class GfxState +{ + // ---- Separate global animation service clock (op 0x238; ctx+0x51b7c total / +0x51b78 elapsed). + // 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; } + private long _previousFrameTimeMs; + private long _currentFrameTimeMs; + + /// Ops 0x202/0x203: record a packed 0xAARRGGBB color/alpha modulation on the object and mark it + /// HasColor so the compositor applies alpha+tint (vs the opaque default). + public void SetObjectColor(long handle, long packed) + { + lock (_lock) { var o = GetOrCreate(handle); o.Color = packed; o.HasColor = true; } + } + + /// Resolve 0x202/0x203's native negative sentinels against obj+0x60 (the current static + /// packed color): negative alpha preserves its alpha byte; negative RGB preserves its RGB bytes. + public void SetObjectColorResolved(long handle, long alpha, long rgb) + { + lock (_lock) + { + var o = GetOrCreate(handle); + long current = o.HasColor ? o.Color : 0xffffffff; + long resolvedAlpha = alpha < 0 ? (current >> 24) & 0xff : System.Math.Min(alpha, 0xff); + long resolvedRgb = rgb < 0 ? current & 0xffffff : rgb & 0xffffff; + o.Color = PackColor(resolvedAlpha, resolvedRgb); + o.HasColor = true; + } + } + + /// Op 0x202: arm the delayed one-shot packed-color interpolation without replacing the + /// current static color. Negative alpha/RGB operands resolve from current obj+0x60 independently. + public void SetAnimatedObjectColorResolved(long handle, long delayMs, long durationMs, long alpha, long rgb) + { + lock (_lock) + { + var o = GetOrCreate(handle); + long current = o.HasColor ? o.Color : 0xffffffff; + long resolvedAlpha = alpha < 0 ? (current >> 24) & 0xff : System.Math.Min(alpha, 0xff); + long resolvedRgb = rgb < 0 ? current & 0xffffff : rgb & 0xffffff; + if (!o.HasColor) { o.Color = current; o.HasColor = true; } + o.OneShotColorTarget = PackColor(resolvedAlpha, resolvedRgb); + o.ColorDelayMs = delayMs; + o.ColorDurationMs = durationMs; + o.OneShotColorEnabled = true; + o.OneShotColorBlend = true; + o.OneShotStartMs = -1; + } + } + + public void SetStaticObjectColorResolved(long handle, long mode, long alpha, long rgb) + { + SetObjectColorResolved(handle, alpha, rgb); + lock (_lock) + { + var o = GetOrCreate(handle); + o.StaticColorMode = mode; + o.PostBindStaticColorBlend = mode == 0 && o.Visible; + } + } + + /// 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.SrcFrameCount = frameCount < 1 ? 1 : frameCount; + o.SrcColumns = columns < 1 ? 1 : columns; + o.SrcCell = cell; o.SrcPeriod = period; o.SrcStart = -1; o.SrcAnim = true; + } + } + + /// Op 0x232 worker contract: ping-pong a temporary copy of static packed ARGB toward + /// . The sampled value is consumed by the object's unchanged blend mode. + public void SetColorAnim(long handle, long period, long target) + { + lock (_lock) + { + var o = GetOrCreate(handle); + o.ColorPeriod = period; o.ColorTarget = target; o.ColorStart = -1; o.ColorAnim = true; + } + } + + /// Op 0x232 handler contract: resolve negative alpha/RGB sentinels from static obj+0x60, + /// clamp alpha above 255, then arm the cyclic packed-color channel without changing blend mode. + public void SetColorAnimResolved(long handle, long period, long alpha, long rgb) + { + lock (_lock) + { + var o = GetOrCreate(handle); + long current = o.Color & 0xffffffff; + long resolvedAlpha = alpha < 0 ? (current >> 24) & 0xff : System.Math.Min(alpha, 0xff); + long resolvedRgb = rgb < 0 ? current & 0xffffff : rgb & 0xffffff; + o.ColorPeriod = period; + o.ColorTarget = PackColor(resolvedAlpha, resolvedRgb); + o.ColorStart = -1; + o.ColorAnim = true; + } + } + + /// 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. + public void SetOneShotAnimationControl(long handle, long flags) + { + lock (_lock) GetOrCreate(handle).OneShotAnimationControlFlags = flags; + } + + /// Part of op 0x243: immediately commit every unprotected finite one-shot channel. Objects + /// marked by op 0x242 bit 0 keep sampling asynchronously. + private void ForceCompleteOneShotChannels() + { + CommitOneShotChannels(_rangeTransform); + foreach (var o in _objects.Values) + { + if ((o.OneShotAnimationControlFlags & 1) != 0) continue; + CommitOneShotChannels(o); + } + } + + private static void CommitOneShotChannels(GfxObject o) + { + 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; + } + + /// Op 0x1fd: immediately replace the object's current scale matrix. Script operands are + /// integer percentages; native divides them by 100 before matrix4_make_scale at obj+0x6c. + public void SetCurrentScale(long handle, (long X, long Y, long Z) percent) + { + lock (_lock) + { + var o = GetOrCreate(handle); + o.ScaleCurrent = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0); + } + } + + /// Op 0x1fe: immediately replace the object's current axis-angle rotation matrix at + /// obj+0xec. Axis components and angle are retained as native floating-point values. + public void SetCurrentRotation(long handle, (long X, long Y, long Z) axis, long angleDegrees) + { + lock (_lock) + { + var o = GetOrCreate(handle); + o.RotationCurrent = (axis.X, axis.Y, axis.Z, angleDegrees); + } + } + + /// Op 0x1ff: immediately replace the object's current translation matrix at obj+0x16c. + public void SetCurrentTranslation(long handle, (long X, long Y, long Z) translation) + { + lock (_lock) + { + var o = GetOrCreate(handle); + o.V16c = translation; + o.TranslationCurrent = translation; + } + } + + /// Op 0x21e: normalized scale target (100 = identity), with independent delay/duration. + public void SetScaleChannel(long handle, long delayMs, long durationMs, (long X, long Y, long Z) percent) + { + lock (_lock) + { + var o = GetOrCreate(handle); + // Native setters get-or-create first, then require object flag bit 0 (draw-bound/visible). + // Pre-bind calls leave a neutral, queryable placeholder and do not queue latent motion. + if (!o.Visible) return; + o.ScaleDelayMs = delayMs; o.ScaleDurationMs = durationMs; + o.ScaleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0); + o.ScaleEnabled = durationMs > 0; o.OneShotStartMs = -1; + } + } + + /// Op 0x220: absolute translation target, with independent delay/duration. + public void SetTranslationChannel(long handle, long delayMs, long durationMs, (long X, long Y, long Z) target) + { + lock (_lock) + { + var o = GetOrCreate(handle); + if (!o.Visible) return; + o.TranslationDelayMs = delayMs; o.TranslationDurationMs = durationMs; + o.TranslationTarget = target; + o.TranslationEnabled = durationMs > 0; o.OneShotStartMs = -1; + } + } + + /// Op 0x228: query the translation target decomposed from the native target matrix at + /// obj+0x17c (translation obj+0x1ac/+0x1b0/+0x1b4). This is independent of draw/base position V24. + public bool TryQueryTranslationTarget(long handle, out (double X, double Y, double Z) target) + { + lock (_lock) + { + if (_objects.TryGetValue(handle, out var o)) + { + target = o.TranslationTarget; + return true; + } + target = default; + return false; + } + } + + /// Op 0x21f: delayed one-shot axis-angle rotation target, sharing obj+0x34's start timestamp. + public void SetRotationChannel(long handle, long delayMs, long durationMs, + (long X, long Y, long Z) axis, long angleDegrees) + { + lock (_lock) + { + var o = GetOrCreate(handle); + if (!o.Visible) return; + o.RotationDelayMs = delayMs; o.RotationDurationMs = durationMs; + o.RotationTarget = (axis.X, axis.Y, axis.Z, angleDegrees); + o.RotationChannelEnabled = durationMs > 0; o.OneShotStartMs = -1; + } + } + + /// Op 0x234: retain the cyclic rotation period and axis separately. Native interpolation uses + /// frame-time retained-gfx owner+0xb550 (EngineCtx+0x51b64) and rotates through 360 degrees per period. + public void SetRotationCycle(long handle, long periodMs, (long X, long Y, long Z) axis) + { + lock (_lock) + { + var o = GetOrCreate(handle); + o.RotationPeriodMs = periodMs; o.RotationAxis = axis; o.RotationEnabled = periodMs > 0; + o.RotationStartMs = -1; + } + } + + /// Op 0x233: cyclic identity-to-target scale, sampled with a triangular ping-pong phase. + public void SetScaleCycle(long handle, long periodMs, (long X, long Y, long Z) percent) + { + lock (_lock) + { + var o = GetOrCreate(handle); + o.ScaleCyclePeriodMs = periodMs; + o.ScaleCycleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0); + o.ScaleCycleEnabled = periodMs > 0; + o.ScaleCycleStartMs = -1; + } + } + + /// Op 0x230: stop every cyclic object channel without changing its base/current state, + /// one-shot channels, or retained cyclic targets. Native clears flag bit 2 and the five start/period + /// pairs at obj+0x20c..0x230, including one secondary matrix channel not otherwise modeled here. + public void ResetCyclicAnimationChannels(long handle) + { + lock (_lock) + { + var o = GetOrCreate(handle); + o.ColorStart = -1; + o.ScaleCycleStartMs = -1; + o.RotationStartMs = -1; + o.SrcStart = -1; + o.ColorPeriod = 0; + o.ScaleCyclePeriodMs = 0; + o.RotationPeriodMs = 0; + o.SrcPeriod = 0; + o.ColorAnim = false; + o.ScaleCycleEnabled = false; + o.RotationEnabled = false; + o.SrcAnim = false; + + // Preserve drop-in numbered-save compatibility for the unmodeled secondary cyclic matrix + // as well as the modeled channels. The ten native timing dwords form one contiguous range. + if (o.NativePersistenceRecord is { Length: >= 0x234 } raw) + { + raw[0] &= 0xfb; + System.Array.Clear(raw, 0x20c, 0x28); + } + } + } + + /// Op 0x238: set its separate global animation-service duration and reset marker. + public void SetAnimClock(long durationTicks) + { + lock (_lock) + { + AnimClockDurationTicks = durationTicks; + AnimClockGeneration++; + MarkRetainedMutation(); + } + } + + public void SetAnimationServiceFlags(long flags) + { + lock (_lock) AnimationServiceFlags = unchecked((uint)flags); + } + + public void SampleFrameTime(long nowMilliseconds) + { + lock (_lock) + { + _previousFrameTimeMs = _currentFrameTimeMs; + _currentFrameTimeMs = nowMilliseconds; + PreviousFrameTimeMilliseconds = CurrentFrameTimeMilliseconds; + CurrentFrameTimeMilliseconds = unchecked((uint)nowMilliseconds); + } + } + + /// 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. + public void ResetAnimClock() + { + lock (_lock) + { + if ((AnimationServiceFlags & 2) != 0) return; + ForceCompleteOneShotChannels(); + AnimClockDurationTicks = 0; + AnimClockGeneration++; + MarkRetainedMutation(); + } + } + + private static long SourceCellAt(GfxObject o, long nowMs) + { + long elapsed = System.Math.Max(0, nowMs - o.SrcStart); + return elapsed / o.SrcPeriod % o.SrcFrameCount; + } + + private static (long Packed, ColorTransitionState State) SampleOneShotColor(GfxObject o, long nowMs) + { + long current = o.Color & 0xffffffff; + long target = o.OneShotColorTarget & 0xffffffff; + long start = o.OneShotStartMs; + long elapsed = nowMs - start - o.ColorDelayMs; + if (o.ColorDurationMs <= 0 || elapsed >= o.ColorDurationMs) + { + long delay = o.ColorDelayMs, duration = o.ColorDurationMs; + o.Color = target; + o.ColorDelayMs = 0; + o.ColorDurationMs = 0; + o.OneShotColorTarget = -1; + o.OneShotColorEnabled = false; + return (target, new ColorTransitionState(current, target, delay, duration, start, 1.0, false)); + } + if (elapsed <= 0) + return (current, new ColorTransitionState(current, target, o.ColorDelayMs, + o.ColorDurationMs, start, 0.0, true)); + + long packed = 0; + long remaining = o.ColorDurationMs - elapsed; + for (int shift = 0; shift <= 24; shift += 8) + { + long c = (current >> shift) & 0xff; + long t = (target >> shift) & 0xff; + packed |= ((c * remaining + t * elapsed) / o.ColorDurationMs & 0xff) << shift; + } + return (packed, new ColorTransitionState(current, target, o.ColorDelayMs, + o.ColorDurationMs, start, elapsed / (double)o.ColorDurationMs, true)); + } + + private static (double X, double Y, double Z) SampleMatrixChannel( + ref (double X, double Y, double Z) current, + (double X, double Y, double Z) target, + long delayMs, long durationMs, long startMs, ref bool enabled, long nowMs) + { + if (!enabled || durationMs <= 0 || startMs < 0) return current; + long elapsed = nowMs - startMs - delayMs; + if (elapsed <= 0) return current; + if (elapsed >= durationMs) + { + current = target; + enabled = false; + return current; + } + double t = (double)elapsed / durationMs; + return (current.X + (target.X - current.X) * t, + current.Y + (target.Y - current.Y) * t, + current.Z + (target.Z - current.Z) * t); + } + + private static (double X, double Y, double Z, double Angle) SampleRotationChannel( + ref (double X, double Y, double Z, double Angle) current, + (double X, double Y, double Z, double Angle) target, + long delayMs, long durationMs, long startMs, ref bool enabled, long nowMs) + { + if (!enabled || durationMs <= 0 || startMs < 0) return current; + long elapsed = nowMs - startMs - delayMs; + if (elapsed <= 0) return current; + if (elapsed >= durationMs) { current = target; enabled = false; return current; } + double t = (double)elapsed / durationMs; + return (current.X + (target.X - current.X) * t, + current.Y + (target.Y - current.Y) * t, + current.Z + (target.Z - current.Z) * t, + current.Angle + (target.Angle - current.Angle) * t); + } + + private static long InterpolatePackedColor(long current, long target, double t) + { + t = System.Math.Clamp(t, 0.0, 1.0); + long packed = 0; + for (int shift = 0; shift <= 24; shift += 8) + { + long c = (current >> shift) & 0xff; + long v = (target >> shift) & 0xff; + packed |= ((long)(c + (v - c) * t) & 0xff) << shift; + } + return packed; + } + + /// Ping-pong interpolation weight in [0,1] toward the target: 0 at cycle start, 1 at half-period. + private static double PingPongWeight(long now, long start, long period) + { + if (period <= 0) return 0; + long half = period / 2; if (half <= 0) return 0; + return (double)BlendMath.PingPong(now, start, period) / half; + } + + private static double ScaleCycleWeight(long now, long start, long period) + { + if (period <= 0) return 0; + long elapsed = System.Math.Max(0, now - start); + long position = elapsed % period; + return 2.0 * System.Math.Min(position, period - position) / period; + } + + /// Pack (alpha, rgb) → 0xAARRGGBB, matching op 0x202/0x203's handler bit-manipulation for the + /// common (non-negative-sentinel) case. The alpha<0 / color<0 native-fetch path is deferred. + public static long PackColor(long alpha, long color) + => ((alpha & 0xff) << 24) | (color & 0xffffff); +} diff --git a/engine/Age.Engine/Model/GfxState.cs b/engine/Age.Engine/Model/GfxState.cs index 81164a8..7054ffe 100644 --- a/engine/Age.Engine/Model/GfxState.cs +++ b/engine/Age.Engine/Model/GfxState.cs @@ -26,15 +26,6 @@ public sealed partial class GfxState } - // ---- Separate global animation service clock (op 0x238; ctx+0x51b7c total / +0x51b78 elapsed). - // 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; } - private long _previousFrameTimeMs; - private long _currentFrameTimeMs; private long _mutationGeneration; private long _publishedMutationGeneration; @@ -135,100 +126,6 @@ public sealed partial class GfxState private readonly Dictionary _surfaceTransitions = new(); private readonly Dictionary _movieMaskTransitions = new(); - /// Ops 0x202/0x203: record a packed 0xAARRGGBB color/alpha modulation on the object and mark it - /// HasColor so the compositor applies alpha+tint (vs the opaque default). - public void SetObjectColor(long handle, long packed) - { - lock (_lock) { var o = GetOrCreate(handle); o.Color = packed; o.HasColor = true; } - } - - /// Resolve 0x202/0x203's native negative sentinels against obj+0x60 (the current static - /// packed color): negative alpha preserves its alpha byte; negative RGB preserves its RGB bytes. - public void SetObjectColorResolved(long handle, long alpha, long rgb) - { - lock (_lock) - { - var o = GetOrCreate(handle); - long current = o.HasColor ? o.Color : 0xffffffff; - long resolvedAlpha = alpha < 0 ? (current >> 24) & 0xff : System.Math.Min(alpha, 0xff); - long resolvedRgb = rgb < 0 ? current & 0xffffff : rgb & 0xffffff; - o.Color = PackColor(resolvedAlpha, resolvedRgb); - o.HasColor = true; - } - } - - /// Op 0x202: arm the delayed one-shot packed-color interpolation without replacing the - /// current static color. Negative alpha/RGB operands resolve from current obj+0x60 independently. - public void SetAnimatedObjectColorResolved(long handle, long delayMs, long durationMs, long alpha, long rgb) - { - lock (_lock) - { - var o = GetOrCreate(handle); - long current = o.HasColor ? o.Color : 0xffffffff; - long resolvedAlpha = alpha < 0 ? (current >> 24) & 0xff : System.Math.Min(alpha, 0xff); - long resolvedRgb = rgb < 0 ? current & 0xffffff : rgb & 0xffffff; - if (!o.HasColor) { o.Color = current; o.HasColor = true; } - o.OneShotColorTarget = PackColor(resolvedAlpha, resolvedRgb); - o.ColorDelayMs = delayMs; - o.ColorDurationMs = durationMs; - o.OneShotColorEnabled = true; - o.OneShotColorBlend = true; - o.OneShotStartMs = -1; - } - } - - public void SetStaticObjectColorResolved(long handle, long mode, long alpha, long rgb) - { - SetObjectColorResolved(handle, alpha, rgb); - lock (_lock) - { - var o = GetOrCreate(handle); - o.StaticColorMode = mode; - o.PostBindStaticColorBlend = mode == 0 && o.Visible; - } - } - - /// 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.SrcFrameCount = frameCount < 1 ? 1 : frameCount; - o.SrcColumns = columns < 1 ? 1 : columns; - o.SrcCell = cell; o.SrcPeriod = period; o.SrcStart = -1; o.SrcAnim = true; - } - } - - /// Op 0x232 worker contract: ping-pong a temporary copy of static packed ARGB toward - /// . The sampled value is consumed by the object's unchanged blend mode. - public void SetColorAnim(long handle, long period, long target) - { - lock (_lock) - { - var o = GetOrCreate(handle); - o.ColorPeriod = period; o.ColorTarget = target; o.ColorStart = -1; o.ColorAnim = true; - } - } - - /// Op 0x232 handler contract: resolve negative alpha/RGB sentinels from static obj+0x60, - /// clamp alpha above 255, then arm the cyclic packed-color channel without changing blend mode. - public void SetColorAnimResolved(long handle, long period, long alpha, long rgb) - { - lock (_lock) - { - var o = GetOrCreate(handle); - long current = o.Color & 0xffffffff; - long resolvedAlpha = alpha < 0 ? (current >> 24) & 0xff : System.Math.Min(alpha, 0xff); - long resolvedRgb = rgb < 0 ? current & 0xffffff : rgb & 0xffffff; - o.ColorPeriod = period; - o.ColorTarget = PackColor(resolvedAlpha, resolvedRgb); - o.ColorStart = -1; - o.ColorAnim = true; - } - } /// Op 0x223: queue a type-0 timed alpha transition into a target surface slot. public void QueueSurfaceAlphaTransition(long commandKey, int targetSlot, @@ -368,58 +265,6 @@ public sealed partial class GfxState o.RotationChannelEnabled, o.RotationDelayMs, o.RotationDurationMs, o.TranslationEnabled, o.TranslationDelayMs, o.TranslationDurationMs); - /// 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. - public void SetOneShotAnimationControl(long handle, long flags) - { - lock (_lock) GetOrCreate(handle).OneShotAnimationControlFlags = flags; - } - - /// Part of op 0x243: immediately commit every unprotected finite one-shot channel. Objects - /// marked by op 0x242 bit 0 keep sampling asynchronously. - private void ForceCompleteOneShotChannels() - { - CommitOneShotChannels(_rangeTransform); - foreach (var o in _objects.Values) - { - if ((o.OneShotAnimationControlFlags & 1) != 0) continue; - CommitOneShotChannels(o); - } - } - - private static void CommitOneShotChannels(GfxObject o) - { - 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; - } /// 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 @@ -519,193 +364,6 @@ public sealed partial class GfxState t.DelayMs, t.DurationMs, t.StartMs, TransitionProgress(t, nowMs), t.Forced); - /// Op 0x1fd: immediately replace the object's current scale matrix. Script operands are - /// integer percentages; native divides them by 100 before matrix4_make_scale at obj+0x6c. - public void SetCurrentScale(long handle, (long X, long Y, long Z) percent) - { - lock (_lock) - { - var o = GetOrCreate(handle); - o.ScaleCurrent = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0); - } - } - - /// Op 0x1fe: immediately replace the object's current axis-angle rotation matrix at - /// obj+0xec. Axis components and angle are retained as native floating-point values. - public void SetCurrentRotation(long handle, (long X, long Y, long Z) axis, long angleDegrees) - { - lock (_lock) - { - var o = GetOrCreate(handle); - o.RotationCurrent = (axis.X, axis.Y, axis.Z, angleDegrees); - } - } - - /// Op 0x1ff: immediately replace the object's current translation matrix at obj+0x16c. - public void SetCurrentTranslation(long handle, (long X, long Y, long Z) translation) - { - lock (_lock) - { - var o = GetOrCreate(handle); - o.V16c = translation; - o.TranslationCurrent = translation; - } - } - - /// Op 0x21e: normalized scale target (100 = identity), with independent delay/duration. - public void SetScaleChannel(long handle, long delayMs, long durationMs, (long X, long Y, long Z) percent) - { - lock (_lock) - { - var o = GetOrCreate(handle); - // Native setters get-or-create first, then require object flag bit 0 (draw-bound/visible). - // Pre-bind calls leave a neutral, queryable placeholder and do not queue latent motion. - if (!o.Visible) return; - o.ScaleDelayMs = delayMs; o.ScaleDurationMs = durationMs; - o.ScaleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0); - o.ScaleEnabled = durationMs > 0; o.OneShotStartMs = -1; - } - } - - /// Op 0x220: absolute translation target, with independent delay/duration. - public void SetTranslationChannel(long handle, long delayMs, long durationMs, (long X, long Y, long Z) target) - { - lock (_lock) - { - var o = GetOrCreate(handle); - if (!o.Visible) return; - o.TranslationDelayMs = delayMs; o.TranslationDurationMs = durationMs; - o.TranslationTarget = target; - o.TranslationEnabled = durationMs > 0; o.OneShotStartMs = -1; - } - } - - /// Op 0x228: query the translation target decomposed from the native target matrix at - /// obj+0x17c (translation obj+0x1ac/+0x1b0/+0x1b4). This is independent of draw/base position V24. - public bool TryQueryTranslationTarget(long handle, out (double X, double Y, double Z) target) - { - lock (_lock) - { - if (_objects.TryGetValue(handle, out var o)) - { - target = o.TranslationTarget; - return true; - } - target = default; - return false; - } - } - - /// Op 0x21f: delayed one-shot axis-angle rotation target, sharing obj+0x34's start timestamp. - public void SetRotationChannel(long handle, long delayMs, long durationMs, - (long X, long Y, long Z) axis, long angleDegrees) - { - lock (_lock) - { - var o = GetOrCreate(handle); - if (!o.Visible) return; - o.RotationDelayMs = delayMs; o.RotationDurationMs = durationMs; - o.RotationTarget = (axis.X, axis.Y, axis.Z, angleDegrees); - o.RotationChannelEnabled = durationMs > 0; o.OneShotStartMs = -1; - } - } - - /// Op 0x234: retain the cyclic rotation period and axis separately. Native interpolation uses - /// frame-time retained-gfx owner+0xb550 (EngineCtx+0x51b64) and rotates through 360 degrees per period. - public void SetRotationCycle(long handle, long periodMs, (long X, long Y, long Z) axis) - { - lock (_lock) - { - var o = GetOrCreate(handle); - o.RotationPeriodMs = periodMs; o.RotationAxis = axis; o.RotationEnabled = periodMs > 0; - o.RotationStartMs = -1; - } - } - - /// Op 0x233: cyclic identity-to-target scale, sampled with a triangular ping-pong phase. - public void SetScaleCycle(long handle, long periodMs, (long X, long Y, long Z) percent) - { - lock (_lock) - { - var o = GetOrCreate(handle); - o.ScaleCyclePeriodMs = periodMs; - o.ScaleCycleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0); - o.ScaleCycleEnabled = periodMs > 0; - o.ScaleCycleStartMs = -1; - } - } - - /// Op 0x230: stop every cyclic object channel without changing its base/current state, - /// one-shot channels, or retained cyclic targets. Native clears flag bit 2 and the five start/period - /// pairs at obj+0x20c..0x230, including one secondary matrix channel not otherwise modeled here. - public void ResetCyclicAnimationChannels(long handle) - { - lock (_lock) - { - var o = GetOrCreate(handle); - o.ColorStart = -1; - o.ScaleCycleStartMs = -1; - o.RotationStartMs = -1; - o.SrcStart = -1; - o.ColorPeriod = 0; - o.ScaleCyclePeriodMs = 0; - o.RotationPeriodMs = 0; - o.SrcPeriod = 0; - o.ColorAnim = false; - o.ScaleCycleEnabled = false; - o.RotationEnabled = false; - o.SrcAnim = false; - - // Preserve drop-in numbered-save compatibility for the unmodeled secondary cyclic matrix - // as well as the modeled channels. The ten native timing dwords form one contiguous range. - if (o.NativePersistenceRecord is { Length: >= 0x234 } raw) - { - raw[0] &= 0xfb; - System.Array.Clear(raw, 0x20c, 0x28); - } - } - } - - /// Op 0x238: set its separate global animation-service duration and reset marker. - public void SetAnimClock(long durationTicks) - { - lock (_lock) - { - AnimClockDurationTicks = durationTicks; - AnimClockGeneration++; - MarkRetainedMutation(); - } - } - - public void SetAnimationServiceFlags(long flags) - { - lock (_lock) AnimationServiceFlags = unchecked((uint)flags); - } - - public void SampleFrameTime(long nowMilliseconds) - { - lock (_lock) - { - _previousFrameTimeMs = _currentFrameTimeMs; - _currentFrameTimeMs = nowMilliseconds; - PreviousFrameTimeMilliseconds = CurrentFrameTimeMilliseconds; - CurrentFrameTimeMilliseconds = unchecked((uint)nowMilliseconds); - } - } - - /// 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. - public void ResetAnimClock() - { - lock (_lock) - { - if ((AnimationServiceFlags & 2) != 0) return; - ForceCompleteOneShotChannels(); - AnimClockDurationTicks = 0; - AnimClockGeneration++; - MarkRetainedMutation(); - } - } /// Sample the shared native frame clock and report why the retained scene needs publishing. /// Continuous channels remain frame-driven; op-0x231 spritesheets become dirty only when the shared @@ -757,11 +415,6 @@ public sealed partial class GfxState } } - private static long SourceCellAt(GfxObject o, long nowMs) - { - long elapsed = System.Math.Max(0, nowMs - o.SrcStart); - return elapsed / o.SrcPeriod % o.SrcFrameCount; - } /// Back-compat: snapshot with no animation clock (nowMs = 0) — deterministic, for headless /// callers and existing tests. @@ -972,105 +625,4 @@ public sealed partial class GfxState } - private static (long Packed, ColorTransitionState State) SampleOneShotColor(GfxObject o, long nowMs) - { - long current = o.Color & 0xffffffff; - long target = o.OneShotColorTarget & 0xffffffff; - long start = o.OneShotStartMs; - long elapsed = nowMs - start - o.ColorDelayMs; - if (o.ColorDurationMs <= 0 || elapsed >= o.ColorDurationMs) - { - long delay = o.ColorDelayMs, duration = o.ColorDurationMs; - o.Color = target; - o.ColorDelayMs = 0; - o.ColorDurationMs = 0; - o.OneShotColorTarget = -1; - o.OneShotColorEnabled = false; - return (target, new ColorTransitionState(current, target, delay, duration, start, 1.0, false)); - } - if (elapsed <= 0) - return (current, new ColorTransitionState(current, target, o.ColorDelayMs, - o.ColorDurationMs, start, 0.0, true)); - - long packed = 0; - long remaining = o.ColorDurationMs - elapsed; - for (int shift = 0; shift <= 24; shift += 8) - { - long c = (current >> shift) & 0xff; - long t = (target >> shift) & 0xff; - packed |= ((c * remaining + t * elapsed) / o.ColorDurationMs & 0xff) << shift; - } - return (packed, new ColorTransitionState(current, target, o.ColorDelayMs, - o.ColorDurationMs, start, elapsed / (double)o.ColorDurationMs, true)); - } - - private static (double X, double Y, double Z) SampleMatrixChannel( - ref (double X, double Y, double Z) current, - (double X, double Y, double Z) target, - long delayMs, long durationMs, long startMs, ref bool enabled, long nowMs) - { - if (!enabled || durationMs <= 0 || startMs < 0) return current; - long elapsed = nowMs - startMs - delayMs; - if (elapsed <= 0) return current; - if (elapsed >= durationMs) - { - current = target; - enabled = false; - return current; - } - double t = (double)elapsed / durationMs; - return (current.X + (target.X - current.X) * t, - current.Y + (target.Y - current.Y) * t, - current.Z + (target.Z - current.Z) * t); - } - - private static (double X, double Y, double Z, double Angle) SampleRotationChannel( - ref (double X, double Y, double Z, double Angle) current, - (double X, double Y, double Z, double Angle) target, - long delayMs, long durationMs, long startMs, ref bool enabled, long nowMs) - { - if (!enabled || durationMs <= 0 || startMs < 0) return current; - long elapsed = nowMs - startMs - delayMs; - if (elapsed <= 0) return current; - if (elapsed >= durationMs) { current = target; enabled = false; return current; } - double t = (double)elapsed / durationMs; - return (current.X + (target.X - current.X) * t, - current.Y + (target.Y - current.Y) * t, - current.Z + (target.Z - current.Z) * t, - current.Angle + (target.Angle - current.Angle) * t); - } - - private static long InterpolatePackedColor(long current, long target, double t) - { - t = System.Math.Clamp(t, 0.0, 1.0); - long packed = 0; - for (int shift = 0; shift <= 24; shift += 8) - { - long c = (current >> shift) & 0xff; - long v = (target >> shift) & 0xff; - packed |= ((long)(c + (v - c) * t) & 0xff) << shift; - } - return packed; - } - - /// Ping-pong interpolation weight in [0,1] toward the target: 0 at cycle start, 1 at half-period. - private static double PingPongWeight(long now, long start, long period) - { - if (period <= 0) return 0; - long half = period / 2; if (half <= 0) return 0; - return (double)BlendMath.PingPong(now, start, period) / half; - } - - private static double ScaleCycleWeight(long now, long start, long period) - { - if (period <= 0) return 0; - long elapsed = System.Math.Max(0, now - start); - long position = elapsed % period; - return 2.0 * System.Math.Min(position, period - position) / period; - } - - /// Pack (alpha, rgb) → 0xAARRGGBB, matching op 0x202/0x203's handler bit-manipulation for the - /// common (non-negative-sentinel) case. The alpha<0 / color<0 native-fetch path is deferred. - public static long PackColor(long alpha, long color) - => ((alpha & 0xff) << 24) | (color & 0xffffff); }