Implement native layout-3 save restoration

This commit is contained in:
gamer147
2026-07-24 16:23:01 -04:00
parent 3b63c42826
commit 3ace5371e6
21 changed files with 1659 additions and 81 deletions

View File

@@ -71,8 +71,8 @@ public sealed record AdvTextHistoryRenderBatch(
AdvTextStyle Style);
/// <summary>
/// Engine-owned retained ADV backlog. It deliberately has no persistence behavior: native numbered-save
/// restoration belongs to the future unified save architecture, while live HISTORY.BIN reads this model.
/// Engine-owned retained ADV backlog. Native numbered saves serialize this same model; live HISTORY.BIN
/// reads it directly, so restored history is immediately available to the script UI.
/// </summary>
public sealed class AdvTextHistory
{
@@ -185,6 +185,35 @@ public sealed class AdvTextHistory
_navigationAnchorIndex = -1;
}
/// <summary>Replace retained history from AGE's numbered-save history tail.</summary>
public void RestorePersistenceSnapshot(
IReadOnlyList<AdvTextHistoryEntry> entries,
IReadOnlyList<AdvTextHistoryRecord> records)
{
ArgumentNullException.ThrowIfNull(entries);
ArgumentNullException.ThrowIfNull(records);
_records.Clear();
_records.AddRange(records);
_entries.Clear();
_entries.AddRange(entries);
_pendingGroupStarts.Clear();
_layouts.Clear();
foreach (AdvTextHistoryRecord record in records)
{
var layout = GetOrCreateLayout(record.Layout.Slot);
layout.Width = record.Layout.Width;
layout.Height = record.Layout.Height;
layout.OriginX = record.Layout.OriginX;
layout.OriginY = record.Layout.OriginY;
layout.CursorX = record.Layout.CursorX;
layout.CursorY = record.Layout.CursorY;
layout.Right = record.Layout.Right;
layout.Bottom = record.Layout.Bottom;
}
CurrentLayoutSlot = entries.Count > 0 ? entries[^1].LayoutSlot : 0;
_navigationAnchorIndex = entries.Count - 1;
}
/// <summary>
/// Resolve a logical entry relative to AGE's latest-boundary navigation anchor. Repeated calls do not
/// mutate the anchor; HISTORY.BIN supplies cumulative deltas while counting and paging backward.

View File

@@ -56,6 +56,13 @@ public sealed record GfxDiagnosticSnapshot(
BlockingGfxObjectDiagnostic? BlockingRangeTransform,
IReadOnlyList<BlockingGfxObjectDiagnostic> BlockingObjects);
public sealed record GfxPersistenceSnapshot(
IReadOnlyList<(int Slot, long ResourceId, long ColorKey, bool Created)> Surfaces,
IReadOnlyList<(long Handle, GfxState.GfxObject Object)> Objects,
long RangeFirst,
long RangeCount,
GfxState.GfxObject RangeTransform);
/// <summary>A renderable view of one visible gfx object — the host composites these in ascending-handle order
/// (= the engine's z-order) each frame. Built by <see cref="GfxState.SnapshotVisibleObjects"/>; the surface
/// resId/colorkey are resolved from the object's live source slot at snapshot time (see docs/engine-re.md,
@@ -293,31 +300,7 @@ public sealed class GfxState
{
if (!_objects.TryGetValue(sourceHandle, out var s)) return false;
bool destinationIsNew = !_objects.ContainsKey(destinationHandle);
_objects[destinationHandle] = new GfxObject
{
V18 = s.V18, V24 = s.V24, V16c = s.V16c,
Field64 = s.Field64, Field68 = s.Field68, Field6c = s.Field6c,
Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode,
OneShotColorTarget = s.OneShotColorTarget, ColorDelayMs = s.ColorDelayMs,
ColorDurationMs = s.ColorDurationMs, OneShotColorEnabled = s.OneShotColorEnabled,
OneShotColorBlend = s.OneShotColorBlend,
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,
};
_objects[destinationHandle] = CloneState(s);
if (destinationIsNew) InsertOrderedHandle(destinationHandle);
CurrentObject = destinationHandle;
MarkRetainedMutation();
@@ -418,6 +401,79 @@ public sealed class GfxState
}
}
public GfxPersistenceSnapshot CapturePersistenceSnapshot()
{
lock (_lock)
{
var surfaces = _surfaces
.Select(pair => (
pair.Key, pair.Value.ResId, pair.Value.ColorKey,
_createdSurfaces.Contains(pair.Key)))
.OrderBy(item => item.Key)
.ToArray();
var objects = _orderedObjectHandles
.Select(handle => (handle, CloneState(_objects[handle])))
.ToArray();
return new GfxPersistenceSnapshot(
surfaces, objects, _rangeTransformFirst, _rangeTransformCount,
CloneState(_rangeTransform));
}
}
public void RestorePersistenceSnapshot(GfxPersistenceSnapshot snapshot)
{
ArgumentNullException.ThrowIfNull(snapshot);
lock (_lock)
{
_surfaces.Clear();
_createdSurfaces.Clear();
foreach (var (slot, resourceId, colorKey, created) in snapshot.Surfaces)
{
_surfaces[slot] = (resourceId, colorKey);
if (created) _createdSurfaces.Add(slot);
}
_objects.Clear();
_orderedObjectHandles.Clear();
foreach (var (handle, state) in snapshot.Objects)
{
_objects[handle] = CloneState(state);
_orderedObjectHandles.Add(handle);
}
_orderedObjectHandles.Sort();
_rangeTransformFirst = snapshot.RangeFirst;
_rangeTransformCount = snapshot.RangeCount;
_rangeTransform = CloneState(snapshot.RangeTransform);
MarkRetainedMutation();
}
}
private static GfxObject CloneState(GfxObject s)
=> new()
{
V18 = s.V18, V24 = s.V24, V16c = s.V16c,
Field64 = s.Field64, Field68 = s.Field68, Field6c = s.Field6c,
Color = s.Color, HasColor = s.HasColor, StaticColorMode = s.StaticColorMode,
OneShotColorTarget = s.OneShotColorTarget, ColorDelayMs = s.ColorDelayMs,
ColorDurationMs = s.ColorDurationMs, OneShotColorEnabled = s.OneShotColorEnabled,
OneShotColorBlend = s.OneShotColorBlend,
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,
};
private readonly object _lock = new();
// ---- surfaces (image buffers per slot): ctx+0x52bd4[slot], from create/set-texture ----

View File

@@ -12,5 +12,9 @@ public sealed class Script
public IReadOnlyList<uint> BodyDwords { get; init; } = Array.Empty<uint>();
/// <summary>T1 entries: code DWORD offsets of op-0x71 message boundaries.</summary>
public IReadOnlyList<int> ReadMessageOffsets { get; init; } = Array.Empty<int>();
/// <summary>T2 entries: code DWORD offsets of resumable call-script sites.</summary>
public IReadOnlyList<int> ScriptCallOffsets { get; init; } = Array.Empty<int>();
/// <summary>T3 entries: code DWORD offsets used to reconstruct the intra-script call stack.</summary>
public IReadOnlyList<int> LocalCallOffsets { get; init; } = Array.Empty<int>();
public string GetString(int offset) => Strings.TryGetValue(offset, out var s) ? s : "";
}