diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index 9cbf164..519b3a2 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -164,7 +164,9 @@ of that state through the sealed partial class. `engine/Age.Engine/Model/GfxState.Contracts.cs` owns its public render, transition, diagnostic, persistence, animation, numeric-glyph, and handle-range contracts. `engine/Age.Engine/Model/GfxState.Surfaces.cs` owns surface resource/color-key state, created/reloadable classification, movie stop-time metadata, render-target/tile -configuration, and surface lifecycle operations. +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. 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 f888131..ad4cf59 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -607,6 +607,11 @@ do not mix mechanical moves with semantic changes. `engine/Age.Engine/Model/GfxState.Surfaces.cs`. Reset, persistence, retained-object sampling, and transition teardown retain direct access through the partial class; runtime validation remains green. + The third bounded `GfxState` split moved the retained-object record and registry/index, range-transform + state and operations, object creation/query/clone/erase and draw binding, and numeric-glyph object generation + 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. + **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. @@ -978,7 +983,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 two `GfxState` domains separated, move retained-object ownership next, preserving public -types, commands, and generated output. +isolated and the first three `GfxState` domains separated, move animation 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.RetainedObjects.cs b/engine/Age.Engine/Model/GfxState.RetainedObjects.cs new file mode 100644 index 0000000..9f1abe0 --- /dev/null +++ b/engine/Age.Engine/Model/GfxState.RetainedObjects.cs @@ -0,0 +1,379 @@ +namespace Age.Engine.Model; + +public sealed partial class GfxState +{ + public sealed class GfxObject + { + // The native numbered-save record contains several full transform matrices and reserved fields + // beyond the port's semantic model. Preserve the decoded bytes so a native load/save round trip + // does not erase state that the port has not modeled yet; known fields are patched by the codec. + public byte[]? NativePersistenceRecord; + public (long X, long Y, long Z) V18, V24, V16c; + public long Color = 0xffffffff; // native gfx_object_init_default obj+0x60: identity packed ARGB + public bool HasColor; // true once op 0x202/0x203 set a color/alpha modulation on this object + public long StaticColorMode; // op 0x203 operand 2 -> obj+0x30; mode 2 is transition alpha/identity + // Op 0x202 one-shot packed-color channel: current +0x60, target +0x64, delay +0x38, + // duration +0x4c. It shares obj+0x34's start timestamp with the one-shot matrix channels. + public long OneShotColorTarget = -1, ColorDelayMs, ColorDurationMs; + public bool OneShotColorEnabled; + // Once op 0x202 arms this object's color channel, its sampled/current ARGB is consumed as D3D + // opacity + multiplicative modulation even after the target commits. This distinguishes ADV chrome + // fades from a static mode-0 0x203 such as a CG initialized with 0x00ffffff (opaque identity). + public bool OneShotColorBlend; + // A mode-0 static color written after draw-texture binds an object consumes packed alpha as + // opacity. FIELD uses this to suppress a bound idle unit, and CALLBACK_SETTING uses it when + // rebuilding the ADV backing after CONFIG. A later draw-texture rebind clears the latch. + public bool PostBindStaticColorBlend; + + // ---- 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). ---- + public long ColorPeriod, ColorStart = -1, ColorTarget; + public bool ColorAnim; + // draw-texture bind (gfx_object_bind_draw): the surface to draw + its source rect + the visible flag. + // Native gfx_object_init_default zeroes obj+4. Querying an object created by a geometry/animation + // setter therefore returns slot 0 even before draw-texture binds it; only an absent object returns -1. + public int SourceSlot; + public (int X, int Y, int W, int H) SrcRect; + public bool Visible; + + // ---- independent one-shot matrix channels (gfx_object_apply_transform_channels@0x472f00). ---- + // Scale: current obj+0x6c, target obj+0xac, delay obj+0x3c, duration obj+0x50. + public (double X, double Y, double Z) ScaleCurrent = (1, 1, 1), ScaleTarget = (1, 1, 1); + public long ScaleDelayMs, ScaleDurationMs; + public bool ScaleEnabled; + // Translation: current obj+0x16c, target obj+0x1ac, delay obj+0x44, duration obj+0x58. + public (double X, double Y, double Z) TranslationCurrent, TranslationTarget; + public long TranslationDelayMs, TranslationDurationMs; + public bool TranslationEnabled; + public (double X, double Y, double Z, double Angle) RotationCurrent; + 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; + + // Op 0x234 is a separate cyclic rotation channel (period obj+0x228, axis obj+0x244..0x24c). + public long RotationPeriodMs; + public (long X, long Y, long Z) RotationAxis; + public bool RotationEnabled; + public long RotationStartMs = -1; + + // Op 0x233 is a separate cyclic scale channel (period obj+0x224, target matrix obj+0x250). + public long ScaleCyclePeriodMs; + public (double X, double Y, double Z) ScaleCycleTarget = (1, 1, 1); + public bool ScaleCycleEnabled; + public long ScaleCycleStartMs = -1; + } + + // ---- geometry/draw object store (V18/V24/draw bind, the compositor's input) ---- + // Populated lazily by the geometry SET ops and draw-texture. Op 0x215 queries this same native map and + // returns the object's live source slot (obj+4), or -1 when the handle has not been drawn/bound yet. + private readonly Dictionary _objects = new(); + // Native composition is handle-ascending z order. Mutations maintain this small index so snapshots do + // not rebuild/sort a dictionary-sized LINQ buffer, while hot handle lookup remains O(1). + private readonly List _orderedObjectHandles = new(); + private readonly NumericGlyphStyle[] _numericGlyphStyles = new NumericGlyphStyle[11]; + + // Ops 0x229-0x22e address one embedded gfx-object record outside the ordinary object map. Its sampled + // matrix is post-multiplied onto only the selected handle range during native composition. FIELD uses + // this as its map camera while the surrounding dungeon UI remains screen-fixed. + private long _rangeTransformFirst, _rangeTransformCount; + private GfxObject _rangeTransform = new(); + + private readonly Dictionary _fieldTable = new(); // ctx+0x46d14 (0x216); no family writer -> default 0 + public long CurrentObject { get; private set; } + /// EngineCtx+0x14e08, selected by op 0x80 and used by op 0x1d9 when its slot is zero. + public int DefaultObjectSlot { 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 + { + get { foreach (var kv in _objects) yield return (kv.Key, kv.Value.SourceSlot); } + } + + public GfxObject GetOrCreate(long handle) + { + // Locked: called from the VM thread (directly by geometry/animation ops and inside BindDraw/anim + // ops) while the main-thread compositor enumerates _objects in SnapshotVisibleObjects. _lock is re-entrant + // (Monitor) so the callers that already hold it are fine. + lock (_lock) + { + if (!_objects.TryGetValue(handle, out var o)) + { + o = new GfxObject(); + _objects[handle] = o; + InsertOrderedHandle(handle); + } + CurrentObject = handle; + MarkRetainedMutation(); + return o; + } + } + + public void SetDefaultObjectSlot(int slot) + { + lock (_lock) DefaultObjectSlot = slot; + } + + + public void SetObjectAnchor(long handle, (long X, long Y, long Z) anchor) + { + lock (_lock) GetOrCreate(handle).V18 = anchor; + } + + public void SetObjectPosition(long handle, (long X, long Y, long Z) position) + { + lock (_lock) GetOrCreate(handle).V24 = position; + } + + /// Op 0x229: reset the embedded range transform, select [first, first+count), and set its + /// anchor/pivot. This does not create or mutate an ordinary retained object. + public void SetRangeTransform(long first, long count, (long X, long Y, long Z) anchor) + { + lock (_lock) + { + _rangeTransformFirst = first; + _rangeTransformCount = System.Math.Max(0, count); + _rangeTransform = new GfxObject { V18 = anchor }; + MarkRetainedMutation(); + } + } + + /// Op 0x22a: immediately replace the embedded range transform's current scale. + public void SetRangeScaleCurrent((long X, long Y, long Z) percent) + { + lock (_lock) + { + _rangeTransform.ScaleCurrent = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0); + MarkRetainedMutation(); + } + } + + /// Op 0x22c: immediately replace the embedded range transform's current translation. + public void SetRangeTranslationCurrent((long X, long Y, long Z) translation) + { + lock (_lock) + { + _rangeTransform.TranslationCurrent = translation; + MarkRetainedMutation(); + } + } + + /// Op 0x22d: arm the range transform's delayed one-shot scale target. + public void SetRangeScaleChannel(long delayMs, long durationMs, (long X, long Y, long Z) percent) + { + lock (_lock) + { + _rangeTransform.ScaleDelayMs = delayMs; + _rangeTransform.ScaleDurationMs = durationMs; + _rangeTransform.ScaleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0); + _rangeTransform.ScaleEnabled = durationMs > 0; + _rangeTransform.OneShotStartMs = -1; + MarkRetainedMutation(); + } + } + + /// Op 0x21d: clone the native 0x2d4-byte retained-object record from source to destination. + public bool CloneObject(long sourceHandle, long destinationHandle) + { + lock (_lock) + { + if (!_objects.TryGetValue(sourceHandle, out var s)) return false; + bool destinationIsNew = !_objects.ContainsKey(destinationHandle); + _objects[destinationHandle] = CloneState(s); + if (destinationIsNew) InsertOrderedHandle(destinationHandle); + CurrentObject = destinationHandle; + MarkRetainedMutation(); + return true; + } + } + + public GfxObject? TryGet(long handle) => _objects.TryGetValue(handle, out var o) ? o : null; + + /// Op 0x215: look up in the retained gfx-object map and return obj+4, + /// the live source-surface slot written by draw-texture, or -1 when absent/unbound. + public int QuerySlot(long handle) + { + lock (_lock) + return _objects.TryGetValue(handle, out var o) ? o.SourceSlot : -1; + } + + /// Rebind one retained object from one source surface to another. Used by SC0000's bounded + /// movie site to reproduce the native warm-engine slot assignment before the following static loaders + /// reuse the port's cold-bootstrap slot. + public void RemapObjectSurface(long handle, int fromSlot, int toSlot) + { + lock (_lock) + if (_objects.TryGetValue(handle, out var obj) && obj.SourceSlot == fromSlot) + { + obj.SourceSlot = toSlot; + MarkRetainedMutation(); + } + } + public long QueryField(long idx) => _fieldTable.TryGetValue(idx, out var v) ? v : 0; + + public void Release(long handle) + { + lock (_lock) // re-entrant: EraseRange already holds _lock + { + if (_objects.Remove(handle)) + { + int index = _orderedObjectHandles.BinarySearch(handle); + if (index >= 0) _orderedObjectHandles.RemoveAt(index); + MarkRetainedMutation(); + } + } + } + + /// Op 0x1f7 semantics (native gfx_object_erase_range @0x47d8b0): erase handles in + /// [handle, handle+count) when count>1, else just . It is a teardown/erase, + /// NOT a create — objects are created lazily by the geometry SET ops (gfx_object_get_or_create). + public void EraseRange(long handle, long count) + { + // Retained-object cleanup (native gfx_object_erase): removes the object from the map, so it stops + // compositing next frame. Faithful to the engine (the render loop iterates the retained-object map). + lock (_lock) + { + if (count > 1) for (long i = handle; i < handle + count; i++) Release(i); + else Release(handle); + } + } + + /// Op 0x1f6: clear every retained gfx-object record without releasing surface resources. + public void ClearRetainedObjects() + { + lock (_lock) + { + _objects.Clear(); + _orderedObjectHandles.Clear(); + CurrentObject = 0; + MarkRetainedMutation(); + } + } + + private static GfxObject CloneState(GfxObject s) + => new() + { + NativePersistenceRecord = s.NativePersistenceRecord?.ToArray(), + V18 = s.V18, V24 = s.V24, V16c = s.V16c, + Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode, + OneShotColorTarget = s.OneShotColorTarget, ColorDelayMs = s.ColorDelayMs, + ColorDurationMs = s.ColorDurationMs, OneShotColorEnabled = s.OneShotColorEnabled, + OneShotColorBlend = s.OneShotColorBlend, PostBindStaticColorBlend = s.PostBindStaticColorBlend, + 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, + ScaleCurrent = s.ScaleCurrent, ScaleTarget = s.ScaleTarget, + ScaleDelayMs = s.ScaleDelayMs, ScaleDurationMs = s.ScaleDurationMs, ScaleEnabled = s.ScaleEnabled, + TranslationCurrent = s.TranslationCurrent, TranslationTarget = s.TranslationTarget, + TranslationDelayMs = s.TranslationDelayMs, TranslationDurationMs = s.TranslationDurationMs, + TranslationEnabled = s.TranslationEnabled, + RotationCurrent = s.RotationCurrent, RotationTarget = s.RotationTarget, + RotationDelayMs = s.RotationDelayMs, RotationDurationMs = s.RotationDurationMs, + RotationChannelEnabled = s.RotationChannelEnabled, + OneShotAnimationControlFlags = s.OneShotAnimationControlFlags, + OneShotStartMs = s.OneShotStartMs, + RotationPeriodMs = s.RotationPeriodMs, RotationAxis = s.RotationAxis, + RotationEnabled = s.RotationEnabled, RotationStartMs = s.RotationStartMs, + ScaleCyclePeriodMs = s.ScaleCyclePeriodMs, ScaleCycleTarget = s.ScaleCycleTarget, + ScaleCycleEnabled = s.ScaleCycleEnabled, ScaleCycleStartMs = s.ScaleCycleStartMs, + }; + + /// draw-texture bind (gfx_object_bind_draw): object draws surface + /// 's rect at (dstX,dstY) and becomes visible. + public void BindDraw(long handle, int slot, int sx, int sy, int w, int h, int dstX, int dstY) + { + lock (_lock) + { + var o = GetOrCreate(handle); + o.SourceSlot = slot; o.SrcRect = (sx, sy, w, h); o.V24 = (dstX, dstY, 0); o.Visible = true; + o.PostBindStaticColorBlend = false; + } + } + + /// Op 0x13a: replace one of eleven safely isolated handler-addressable styles. + /// Native physically clears ten records; its admitted index 10 aliases return-stack storage. + public bool RegisterNumericGlyphStyle(int styleIndex, int surfaceSlot, int atlasX, int atlasY, + int digitWidth, int digitHeight) + { + lock (_lock) + { + if ((uint)styleIndex >= (uint)_numericGlyphStyles.Length) return false; + _numericGlyphStyles[styleIndex] = + new NumericGlyphStyle(surfaceSlot, atlasX, atlasY, digitWidth, digitHeight); + return true; + } + } + + /// Op 0x23b: erase a destination handle range, split a signed 32-bit value into decimal + /// digits, and bind one retained object per displayed atlas cell. Returns false for an invalid or + /// unregistered style, matching the native handler's script-error path. + public bool DrawDecimalGlyphs(long baseHandle, int styleIndex, int value, int x, int y, + int digitCapacity, int flags) + { + lock (_lock) + { + if ((uint)styleIndex >= (uint)_numericGlyphStyles.Length + || !_numericGlyphStyles[styleIndex].Registered) return false; + + NumericGlyphStyle style = _numericGlyphStyles[styleIndex]; + EraseRange(baseHandle, digitCapacity); + + int remaining = value; + int handleOffset = 0; + int digitCount = 1; + for (int n = remaining / 10; n != 0; n /= 10) digitCount++; + + for (int slot = digitCapacity - 1; slot >= 0; slot--) + { + int digit = remaining % 10; + int drawX; + bool draw; + + if ((flags & 0x2) != 0) + { + draw = slot == digitCapacity - 1 || remaining != 0; + drawX = unchecked(x + style.DigitWidth * slot + - (digitCapacity - digitCount) * style.DigitWidth / 2); + } + else if ((flags & 0x4) != 0) + { + digitCount--; + draw = slot == digitCapacity - 1 || remaining != 0; + drawX = unchecked(x + style.DigitWidth * digitCount); + } + else + { + draw = (flags & 0x1) != 0 || slot == digitCapacity - 1 || remaining != 0; + drawX = unchecked(x + style.DigitWidth * slot); + } + + if (draw) + { + int srcX = unchecked(style.AtlasX + digit * style.DigitWidth); + BindDraw(baseHandle + handleOffset, style.SurfaceSlot, + srcX, style.AtlasY, style.DigitWidth, style.DigitHeight, drawX, y); + handleOffset++; + } + remaining /= 10; + } + return true; + } + } + + private void InsertOrderedHandle(long handle) + { + int index = _orderedObjectHandles.BinarySearch(handle); + if (index < 0) _orderedObjectHandles.Insert(~index, handle); + } +} diff --git a/engine/Age.Engine/Model/GfxState.cs b/engine/Age.Engine/Model/GfxState.cs index 21b5ba8..81164a8 100644 --- a/engine/Age.Engine/Model/GfxState.cs +++ b/engine/Age.Engine/Model/GfxState.cs @@ -24,97 +24,7 @@ public sealed partial class GfxState public required MovieMaskTransitionRequest Request; public bool Completed; } - public sealed class GfxObject - { - // The native numbered-save record contains several full transform matrices and reserved fields - // beyond the port's semantic model. Preserve the decoded bytes so a native load/save round trip - // does not erase state that the port has not modeled yet; known fields are patched by the codec. - public byte[]? NativePersistenceRecord; - public (long X, long Y, long Z) V18, V24, V16c; - public long Color = 0xffffffff; // native gfx_object_init_default obj+0x60: identity packed ARGB - public bool HasColor; // true once op 0x202/0x203 set a color/alpha modulation on this object - public long StaticColorMode; // op 0x203 operand 2 -> obj+0x30; mode 2 is transition alpha/identity - // Op 0x202 one-shot packed-color channel: current +0x60, target +0x64, delay +0x38, - // duration +0x4c. It shares obj+0x34's start timestamp with the one-shot matrix channels. - public long OneShotColorTarget = -1, ColorDelayMs, ColorDurationMs; - public bool OneShotColorEnabled; - // Once op 0x202 arms this object's color channel, its sampled/current ARGB is consumed as D3D - // opacity + multiplicative modulation even after the target commits. This distinguishes ADV chrome - // fades from a static mode-0 0x203 such as a CG initialized with 0x00ffffff (opaque identity). - public bool OneShotColorBlend; - // A mode-0 static color written after draw-texture binds an object consumes packed alpha as - // opacity. FIELD uses this to suppress a bound idle unit, and CALLBACK_SETTING uses it when - // rebuilding the ADV backing after CONFIG. A later draw-texture rebind clears the latch. - public bool PostBindStaticColorBlend; - // ---- 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). ---- - public long ColorPeriod, ColorStart = -1, ColorTarget; - public bool ColorAnim; - // draw-texture bind (gfx_object_bind_draw): the surface to draw + its source rect + the visible flag. - // Native gfx_object_init_default zeroes obj+4. Querying an object created by a geometry/animation - // setter therefore returns slot 0 even before draw-texture binds it; only an absent object returns -1. - public int SourceSlot; - public (int X, int Y, int W, int H) SrcRect; - public bool Visible; - - // ---- independent one-shot matrix channels (gfx_object_apply_transform_channels@0x472f00). ---- - // Scale: current obj+0x6c, target obj+0xac, delay obj+0x3c, duration obj+0x50. - public (double X, double Y, double Z) ScaleCurrent = (1, 1, 1), ScaleTarget = (1, 1, 1); - public long ScaleDelayMs, ScaleDurationMs; - public bool ScaleEnabled; - // Translation: current obj+0x16c, target obj+0x1ac, delay obj+0x44, duration obj+0x58. - public (double X, double Y, double Z) TranslationCurrent, TranslationTarget; - public long TranslationDelayMs, TranslationDurationMs; - public bool TranslationEnabled; - public (double X, double Y, double Z, double Angle) RotationCurrent; - 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; - - // Op 0x234 is a separate cyclic rotation channel (period obj+0x228, axis obj+0x244..0x24c). - public long RotationPeriodMs; - public (long X, long Y, long Z) RotationAxis; - public bool RotationEnabled; - public long RotationStartMs = -1; - - // Op 0x233 is a separate cyclic scale channel (period obj+0x224, target matrix obj+0x250). - public long ScaleCyclePeriodMs; - public (double X, double Y, double Z) ScaleCycleTarget = (1, 1, 1); - public bool ScaleCycleEnabled; - public long ScaleCycleStartMs = -1; - } - - // ---- geometry/draw object store (V18/V24/draw bind, the compositor's input) ---- - // Populated lazily by the geometry SET ops and draw-texture. Op 0x215 queries this same native map and - // returns the object's live source slot (obj+4), or -1 when the handle has not been drawn/bound yet. - private readonly Dictionary _objects = new(); - // Native composition is handle-ascending z order. Mutations maintain this small index so snapshots do - // not rebuild/sort a dictionary-sized LINQ buffer, while hot handle lookup remains O(1). - private readonly List _orderedObjectHandles = new(); - private readonly NumericGlyphStyle[] _numericGlyphStyles = new NumericGlyphStyle[11]; - - // Ops 0x229-0x22e address one embedded gfx-object record outside the ordinary object map. Its sampled - // matrix is post-multiplied onto only the selected handle range during native composition. FIELD uses - // this as its map camera while the surrounding dungeon UI remains screen-fixed. - private long _rangeTransformFirst, _rangeTransformCount; - private GfxObject _rangeTransform = new(); - - private readonly Dictionary _fieldTable = new(); // ctx+0x46d14 (0x216); no family writer -> default 0 - public long CurrentObject { get; private set; } - /// EngineCtx+0x14e08, selected by op 0x80 and used by op 0x1d9 when its slot is zero. - public int DefaultObjectSlot { get; private set; } // ---- 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. ---- @@ -130,171 +40,6 @@ public sealed partial class GfxState private void MarkRetainedMutation() => _mutationGeneration++; - /// Live geometry objects and the surface slot they draw from — for the CLI gfx oracle. - public IEnumerable<(long Handle, int Slot)> Objects - { - get { foreach (var kv in _objects) yield return (kv.Key, kv.Value.SourceSlot); } - } - - public GfxObject GetOrCreate(long handle) - { - // Locked: called from the VM thread (directly by geometry/animation ops and inside BindDraw/anim - // ops) while the main-thread compositor enumerates _objects in SnapshotVisibleObjects. _lock is re-entrant - // (Monitor) so the callers that already hold it are fine. - lock (_lock) - { - if (!_objects.TryGetValue(handle, out var o)) - { - o = new GfxObject(); - _objects[handle] = o; - InsertOrderedHandle(handle); - } - CurrentObject = handle; - MarkRetainedMutation(); - return o; - } - } - - public void SetDefaultObjectSlot(int slot) - { - lock (_lock) DefaultObjectSlot = slot; - } - - - public void SetObjectAnchor(long handle, (long X, long Y, long Z) anchor) - { - lock (_lock) GetOrCreate(handle).V18 = anchor; - } - - public void SetObjectPosition(long handle, (long X, long Y, long Z) position) - { - lock (_lock) GetOrCreate(handle).V24 = position; - } - - /// Op 0x229: reset the embedded range transform, select [first, first+count), and set its - /// anchor/pivot. This does not create or mutate an ordinary retained object. - public void SetRangeTransform(long first, long count, (long X, long Y, long Z) anchor) - { - lock (_lock) - { - _rangeTransformFirst = first; - _rangeTransformCount = System.Math.Max(0, count); - _rangeTransform = new GfxObject { V18 = anchor }; - MarkRetainedMutation(); - } - } - - /// Op 0x22a: immediately replace the embedded range transform's current scale. - public void SetRangeScaleCurrent((long X, long Y, long Z) percent) - { - lock (_lock) - { - _rangeTransform.ScaleCurrent = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0); - MarkRetainedMutation(); - } - } - - /// Op 0x22c: immediately replace the embedded range transform's current translation. - public void SetRangeTranslationCurrent((long X, long Y, long Z) translation) - { - lock (_lock) - { - _rangeTransform.TranslationCurrent = translation; - MarkRetainedMutation(); - } - } - - /// Op 0x22d: arm the range transform's delayed one-shot scale target. - public void SetRangeScaleChannel(long delayMs, long durationMs, (long X, long Y, long Z) percent) - { - lock (_lock) - { - _rangeTransform.ScaleDelayMs = delayMs; - _rangeTransform.ScaleDurationMs = durationMs; - _rangeTransform.ScaleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0); - _rangeTransform.ScaleEnabled = durationMs > 0; - _rangeTransform.OneShotStartMs = -1; - MarkRetainedMutation(); - } - } - - /// Op 0x21d: clone the native 0x2d4-byte retained-object record from source to destination. - public bool CloneObject(long sourceHandle, long destinationHandle) - { - lock (_lock) - { - if (!_objects.TryGetValue(sourceHandle, out var s)) return false; - bool destinationIsNew = !_objects.ContainsKey(destinationHandle); - _objects[destinationHandle] = CloneState(s); - if (destinationIsNew) InsertOrderedHandle(destinationHandle); - CurrentObject = destinationHandle; - MarkRetainedMutation(); - return true; - } - } - - public GfxObject? TryGet(long handle) => _objects.TryGetValue(handle, out var o) ? o : null; - - /// Op 0x215: look up in the retained gfx-object map and return obj+4, - /// the live source-surface slot written by draw-texture, or -1 when absent/unbound. - public int QuerySlot(long handle) - { - lock (_lock) - return _objects.TryGetValue(handle, out var o) ? o.SourceSlot : -1; - } - - /// Rebind one retained object from one source surface to another. Used by SC0000's bounded - /// movie site to reproduce the native warm-engine slot assignment before the following static loaders - /// reuse the port's cold-bootstrap slot. - public void RemapObjectSurface(long handle, int fromSlot, int toSlot) - { - lock (_lock) - if (_objects.TryGetValue(handle, out var obj) && obj.SourceSlot == fromSlot) - { - obj.SourceSlot = toSlot; - MarkRetainedMutation(); - } - } - public long QueryField(long idx) => _fieldTable.TryGetValue(idx, out var v) ? v : 0; - - public void Release(long handle) - { - lock (_lock) // re-entrant: EraseRange already holds _lock - { - if (_objects.Remove(handle)) - { - int index = _orderedObjectHandles.BinarySearch(handle); - if (index >= 0) _orderedObjectHandles.RemoveAt(index); - MarkRetainedMutation(); - } - } - } - - /// Op 0x1f7 semantics (native gfx_object_erase_range @0x47d8b0): erase handles in - /// [handle, handle+count) when count>1, else just . It is a teardown/erase, - /// NOT a create — objects are created lazily by the geometry SET ops (gfx_object_get_or_create). - public void EraseRange(long handle, long count) - { - // Retained-object cleanup (native gfx_object_erase): removes the object from the map, so it stops - // compositing next frame. Faithful to the engine (the render loop iterates the retained-object map). - lock (_lock) - { - if (count > 1) for (long i = handle; i < handle + count; i++) Release(i); - else Release(handle); - } - } - - /// Op 0x1f6: clear every retained gfx-object record without releasing surface resources. - public void ClearRetainedObjects() - { - lock (_lock) - { - _objects.Clear(); - _orderedObjectHandles.Clear(); - CurrentObject = 0; - MarkRetainedMutation(); - } - } /// Native scene_context_init_reset ownership boundary used by opcode 0x9: discard /// retained objects, command/query state, surfaces, transitions, render-target selection, and the @@ -384,34 +129,6 @@ public sealed partial class GfxState } } - private static GfxObject CloneState(GfxObject s) - => new() - { - NativePersistenceRecord = s.NativePersistenceRecord?.ToArray(), - V18 = s.V18, V24 = s.V24, V16c = s.V16c, - Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode, - OneShotColorTarget = s.OneShotColorTarget, ColorDelayMs = s.ColorDelayMs, - ColorDurationMs = s.ColorDurationMs, OneShotColorEnabled = s.OneShotColorEnabled, - OneShotColorBlend = s.OneShotColorBlend, PostBindStaticColorBlend = s.PostBindStaticColorBlend, - 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, - ScaleCurrent = s.ScaleCurrent, ScaleTarget = s.ScaleTarget, - ScaleDelayMs = s.ScaleDelayMs, ScaleDurationMs = s.ScaleDurationMs, ScaleEnabled = s.ScaleEnabled, - TranslationCurrent = s.TranslationCurrent, TranslationTarget = s.TranslationTarget, - TranslationDelayMs = s.TranslationDelayMs, TranslationDurationMs = s.TranslationDurationMs, - TranslationEnabled = s.TranslationEnabled, - RotationCurrent = s.RotationCurrent, RotationTarget = s.RotationTarget, - RotationDelayMs = s.RotationDelayMs, RotationDurationMs = s.RotationDurationMs, - RotationChannelEnabled = s.RotationChannelEnabled, - OneShotAnimationControlFlags = s.OneShotAnimationControlFlags, - OneShotStartMs = s.OneShotStartMs, - RotationPeriodMs = s.RotationPeriodMs, RotationAxis = s.RotationAxis, - RotationEnabled = s.RotationEnabled, RotationStartMs = s.RotationStartMs, - ScaleCyclePeriodMs = s.ScaleCyclePeriodMs, ScaleCycleTarget = s.ScaleCycleTarget, - ScaleCycleEnabled = s.ScaleCycleEnabled, ScaleCycleStartMs = s.ScaleCycleStartMs, - }; private readonly object _lock = new(); @@ -801,87 +518,6 @@ public sealed partial class GfxState => new(t.CommandKey, t.TargetSlot, t.RangeAStart, t.RangeACount, t.RangeBStart, t.RangeBCount, t.DelayMs, t.DurationMs, t.StartMs, TransitionProgress(t, nowMs), t.Forced); - /// draw-texture bind (gfx_object_bind_draw): object draws surface - /// 's rect at (dstX,dstY) and becomes visible. - public void BindDraw(long handle, int slot, int sx, int sy, int w, int h, int dstX, int dstY) - { - lock (_lock) - { - var o = GetOrCreate(handle); - o.SourceSlot = slot; o.SrcRect = (sx, sy, w, h); o.V24 = (dstX, dstY, 0); o.Visible = true; - o.PostBindStaticColorBlend = false; - } - } - - /// Op 0x13a: replace one of eleven safely isolated handler-addressable styles. - /// Native physically clears ten records; its admitted index 10 aliases return-stack storage. - public bool RegisterNumericGlyphStyle(int styleIndex, int surfaceSlot, int atlasX, int atlasY, - int digitWidth, int digitHeight) - { - lock (_lock) - { - if ((uint)styleIndex >= (uint)_numericGlyphStyles.Length) return false; - _numericGlyphStyles[styleIndex] = - new NumericGlyphStyle(surfaceSlot, atlasX, atlasY, digitWidth, digitHeight); - return true; - } - } - - /// Op 0x23b: erase a destination handle range, split a signed 32-bit value into decimal - /// digits, and bind one retained object per displayed atlas cell. Returns false for an invalid or - /// unregistered style, matching the native handler's script-error path. - public bool DrawDecimalGlyphs(long baseHandle, int styleIndex, int value, int x, int y, - int digitCapacity, int flags) - { - lock (_lock) - { - if ((uint)styleIndex >= (uint)_numericGlyphStyles.Length - || !_numericGlyphStyles[styleIndex].Registered) return false; - - NumericGlyphStyle style = _numericGlyphStyles[styleIndex]; - EraseRange(baseHandle, digitCapacity); - - int remaining = value; - int handleOffset = 0; - int digitCount = 1; - for (int n = remaining / 10; n != 0; n /= 10) digitCount++; - - for (int slot = digitCapacity - 1; slot >= 0; slot--) - { - int digit = remaining % 10; - int drawX; - bool draw; - - if ((flags & 0x2) != 0) - { - draw = slot == digitCapacity - 1 || remaining != 0; - drawX = unchecked(x + style.DigitWidth * slot - - (digitCapacity - digitCount) * style.DigitWidth / 2); - } - else if ((flags & 0x4) != 0) - { - digitCount--; - draw = slot == digitCapacity - 1 || remaining != 0; - drawX = unchecked(x + style.DigitWidth * digitCount); - } - else - { - draw = (flags & 0x1) != 0 || slot == digitCapacity - 1 || remaining != 0; - drawX = unchecked(x + style.DigitWidth * slot); - } - - if (draw) - { - int srcX = unchecked(style.AtlasX + digit * style.DigitWidth); - BindDraw(baseHandle + handleOffset, style.SurfaceSlot, - srcX, style.AtlasY, style.DigitWidth, style.DigitHeight, drawX, y); - handleOffset++; - } - remaining /= 10; - } - return true; - } - } /// 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. @@ -1335,11 +971,6 @@ public sealed partial class GfxState } } - private void InsertOrderedHandle(long handle) - { - int index = _orderedObjectHandles.BinarySearch(handle); - if (index < 0) _orderedObjectHandles.Insert(~index, handle); - } private static (long Packed, ColorTransitionState State) SampleOneShotColor(GfxObject o, long nowMs) {