Restore ADV wait indicator
This commit is contained in:
@@ -11,6 +11,7 @@ public sealed class GodotAdvHost : IHost
|
||||
private readonly string _scene; // e.g. "SC0000" — for section_base
|
||||
private readonly object _imageLock = new();
|
||||
private readonly Dictionary<int, RgbaImage?> _images = new(); // raw catalog id -> decoded pixels
|
||||
private readonly Dictionary<int, long> _surfaceResources = new(); // surface slot -> scene/raw resource id
|
||||
private readonly Dictionary<long, (RgbaImage Image, string Name, int RawIndex)> _movieFrames = new();
|
||||
private readonly Dictionary<int, long> _movieBySurface = new();
|
||||
private readonly HashSet<long> _completedMovies = new();
|
||||
@@ -27,8 +28,12 @@ public sealed class GodotAdvHost : IHost
|
||||
private readonly Dictionary<int, SurfaceTextDraw> _surfaceText = new();
|
||||
private string _advText = "";
|
||||
private int _advTextX = 100, _advTextY = 47;
|
||||
private int _currentAdvLayout = 1; // SYSTEM4's ordinary SC0000 ADV layout
|
||||
private long _advTextStartedMs;
|
||||
private bool _advTextForceComplete;
|
||||
private readonly Dictionary<int, AdvWaitIndicatorConfig> _waitIndicators = new();
|
||||
private int _activeWaitLayout;
|
||||
private long _waitIndicatorStartedMs;
|
||||
private GfxState? _foregroundGfx;
|
||||
public volatile bool IsWaiting;
|
||||
public volatile bool IsTransitionWaiting;
|
||||
@@ -75,7 +80,12 @@ public sealed class GodotAdvHost : IHost
|
||||
|
||||
public void SetAdvTextCursor(int layoutSlot, int x, int y)
|
||||
{
|
||||
lock (_textLock) { _advTextX = x; _advTextY = y; }
|
||||
lock (_textLock)
|
||||
{
|
||||
if (layoutSlot != 0) _currentAdvLayout = layoutSlot;
|
||||
_advTextX = x;
|
||||
_advTextY = y;
|
||||
}
|
||||
_timeline?.Event("text-cursor", new() { ["slot"] = layoutSlot, ["x"] = x, ["y"] = y });
|
||||
}
|
||||
|
||||
@@ -99,15 +109,50 @@ public sealed class GodotAdvHost : IHost
|
||||
public bool TryGetSurfaceText(int surfaceSlot, out SurfaceTextDraw draw)
|
||||
{ lock (_textLock) return _surfaceText.TryGetValue(surfaceSlot, out draw); }
|
||||
|
||||
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config)
|
||||
{
|
||||
lock (_textLock) _waitIndicators[config.LayoutSlot] = config;
|
||||
_timeline?.Event("wait-indicator-config", new()
|
||||
{
|
||||
["layout"] = config.LayoutSlot, ["x"] = config.X, ["y"] = config.Y,
|
||||
["surface"] = config.SurfaceSlot, ["cell_w"] = config.CellWidth,
|
||||
["cell_h"] = config.CellHeight, ["terminal_frame"] = config.TerminalFrame,
|
||||
["period_ms"] = config.FramePeriodMs,
|
||||
});
|
||||
}
|
||||
|
||||
public AdvWaitIndicatorSnapshot? SnapshotAdvWaitIndicator()
|
||||
{
|
||||
if (!IsWaiting) return null;
|
||||
AdvWaitIndicatorConfig config;
|
||||
long resourceId;
|
||||
lock (_textLock)
|
||||
{
|
||||
if (!_waitIndicators.TryGetValue(_activeWaitLayout, out config)) return null;
|
||||
if (!_surfaceResources.TryGetValue(config.SurfaceSlot, out resourceId)) return null;
|
||||
}
|
||||
var asset = _res.ResolveTexture(_scene, resourceId);
|
||||
var image = asset != null ? Decode(asset) : null;
|
||||
if (asset == null || image == null || config.CellWidth <= 0 || config.CellHeight <= 0) return null;
|
||||
int frames = System.Math.Max(1, config.TerminalFrame + 1);
|
||||
long period = System.Math.Max(1, config.FramePeriodMs);
|
||||
int frame = (int)((_clock.NowMs - _waitIndicatorStartedMs) / period % frames);
|
||||
return new AdvWaitIndicatorSnapshot(image, asset.Name, asset.RawIndex, config, frame);
|
||||
}
|
||||
|
||||
public volatile int Pages; // VM-thread page counter (incremented before IsWaiting so shot-gating can't race)
|
||||
|
||||
public void WaitForInput()
|
||||
public void WaitForInput() => WaitForInput(0);
|
||||
|
||||
public void WaitForInput(int layoutSlot)
|
||||
{
|
||||
Pages++;
|
||||
_main.CallDeferred("PageBreak");
|
||||
// Publish retained mutations accumulated before the wait once. A static input wait is not itself a
|
||||
// reason to rebuild the 800x600 background every frame; ambient channels are queried separately.
|
||||
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
||||
lock (_textLock) _activeWaitLayout = layoutSlot == 0 ? _currentAdvLayout : layoutSlot;
|
||||
_waitIndicatorStartedMs = _clock.NowMs;
|
||||
IsWaiting = true;
|
||||
_timeline?.State("input-wait", new() { ["page"] = Pages });
|
||||
_gate.Wait();
|
||||
@@ -244,13 +289,18 @@ public sealed class GodotAdvHost : IHost
|
||||
public void CreateTexture(int slot, int width, int height)
|
||||
{
|
||||
lock (_textLock) _surfaceText.Remove(slot);
|
||||
lock (_textLock) _surfaceResources.Remove(slot);
|
||||
_slotDims[slot] = (width, height);
|
||||
if (TraceOps) Godot.GD.Print($"[op] create-texture slot={slot} {width}x{height}");
|
||||
}
|
||||
|
||||
public void SetTexture(long resourceId, int slot)
|
||||
{
|
||||
lock (_textLock) _surfaceText.Remove(slot);
|
||||
lock (_textLock)
|
||||
{
|
||||
_surfaceText.Remove(slot);
|
||||
_surfaceResources[slot] = resourceId;
|
||||
}
|
||||
var asset = _res.ResolveTexture(_scene, resourceId);
|
||||
var image = asset != null ? Decode(asset) : null;
|
||||
_slotDims[slot] = image != null ? (image.Width, image.Height) : (0, 0);
|
||||
@@ -428,3 +478,5 @@ public sealed class GodotAdvHost : IHost
|
||||
}
|
||||
|
||||
public readonly record struct SurfaceTextDraw(int X, int Y, string Text);
|
||||
public readonly record struct AdvWaitIndicatorSnapshot(
|
||||
RgbaImage Image, string Name, int AssetId, AdvWaitIndicatorConfig Config, int Frame);
|
||||
|
||||
@@ -17,6 +17,10 @@ public partial class Main : Godot.Control
|
||||
private TextureRect _screenView = null!; // shows the composited screen backbuffer
|
||||
private Image _screen = null!; // 800x600 immediate-mode canvas
|
||||
private ImageTexture _screenTex = null!;
|
||||
private TextureRect _waitIndicator = null!;
|
||||
private ImageTexture? _waitIndicatorSheet;
|
||||
private AtlasTexture? _waitIndicatorAtlas;
|
||||
private int _waitIndicatorAssetId = -1;
|
||||
// One managed composition target for the entire frame. Layer helpers mutate it in place; only the
|
||||
// completed frame crosses the Godot Image boundary, avoiding a full GetData/SetData round-trip per layer.
|
||||
private readonly byte[] _screenPixels = new byte[ScreenWidth * ScreenHeight * 4];
|
||||
@@ -72,6 +76,17 @@ public partial class Main : Godot.Control
|
||||
_screenView.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
AddChild(_screenView); // added first -> draws behind the text/status labels
|
||||
|
||||
// Native ADV wait marker: a tiny independently animated atlas region. Keeping it separate from the
|
||||
// 800x600 software backbuffer avoids recompositing the entire retained scene throughout static waits.
|
||||
_waitIndicator = new TextureRect
|
||||
{
|
||||
Visible = false,
|
||||
ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize,
|
||||
StretchMode = TextureRect.StretchModeEnum.Keep,
|
||||
MouseFilter = MouseFilterEnum.Ignore,
|
||||
};
|
||||
AddChild(_waitIndicator);
|
||||
|
||||
_text = new Label { AutowrapMode = TextServer.AutowrapMode.WordSmart, MouseFilter = MouseFilterEnum.Ignore };
|
||||
_text.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
AddChild(_text);
|
||||
@@ -181,6 +196,16 @@ public partial class Main : Godot.Control
|
||||
_host.SetTexture(systemChrome.RawIndex, 0x11);
|
||||
_vm.Gfx.SetSurface(0x11, systemChrome.RawIndex, 0);
|
||||
}
|
||||
// SYSTEM4 also loads SO000 and configures op 0x73 before entering scene code. The Phase-A
|
||||
// single-scene harness does not replay those graphics side effects, so inject their exact state
|
||||
// alongside the existing SO001 bootstrap until Phase B runs the complete SYSTEM4 entrypoint.
|
||||
if (!_selftest && resources.ResolveName("SO000.AGF") is { } waitIndicator)
|
||||
{
|
||||
_host.SetTexture(waitIndicator.RawIndex, 0x0c);
|
||||
_vm.Gfx.SetSurface(0x0c, waitIndicator.RawIndex, 0xff00);
|
||||
_host.ConfigureAdvWaitIndicator(new AdvWaitIndicatorConfig(
|
||||
1, 385, 140, 0x0c, 0, 0, 30, 27, 12, 48));
|
||||
}
|
||||
// --boot: run SYSTEM4's state prefix (INITCONFIG/INIT2/INIT) so the scene sees boot state — chiefly
|
||||
// INIT2's gfx handle array 0x62455.. (skips the UI scripts LOGO/OP/TITLE). State carries via globals.
|
||||
if (boot && !_selftest)
|
||||
@@ -230,6 +255,7 @@ public partial class Main : Godot.Control
|
||||
if (!_selftest && _vm != null && _host != null && _host.ShouldRecomposite(_vm.Gfx))
|
||||
Recomposite(); // native publishes retained mutations only at present/service boundaries
|
||||
if (!_selftest && _host != null) UpdateAdvTextPresentation();
|
||||
if (!_selftest && _host != null) UpdateAdvWaitIndicatorPresentation();
|
||||
// --shot-sequence: dump one PNG per frame across the opening so a time-based (paced) effect can be
|
||||
// verified as distinct frames, not just the final state. Captures after Recomposite; quits when full.
|
||||
if (_seqDir != null && _seqIdx < _seqFrames && !_done)
|
||||
@@ -397,6 +423,33 @@ public partial class Main : Godot.Control
|
||||
_text.Text = count == 0 ? "" : t.Text[..count];
|
||||
}
|
||||
|
||||
private void UpdateAdvWaitIndicatorPresentation()
|
||||
{
|
||||
var snapshot = _host.SnapshotAdvWaitIndicator();
|
||||
if (snapshot == null)
|
||||
{
|
||||
_waitIndicator.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var s = snapshot.Value;
|
||||
if (_waitIndicatorAssetId != s.AssetId)
|
||||
{
|
||||
var image = Image.CreateFromData(s.Image.Width, s.Image.Height, false, Image.Format.Rgba8, s.Image.Pixels);
|
||||
_waitIndicatorSheet = ImageTexture.CreateFromImage(image);
|
||||
_waitIndicatorAtlas = new AtlasTexture { Atlas = _waitIndicatorSheet };
|
||||
_waitIndicator.Texture = _waitIndicatorAtlas;
|
||||
_waitIndicatorAssetId = s.AssetId;
|
||||
}
|
||||
|
||||
var c = s.Config;
|
||||
_waitIndicatorAtlas!.Region = new Rect2(
|
||||
c.SourceX + s.Frame * c.CellWidth, c.SourceY, c.CellWidth, c.CellHeight);
|
||||
_waitIndicator.Position = new Vector2(c.X, 430 + c.Y);
|
||||
_waitIndicator.Size = new Vector2(c.CellWidth, c.CellHeight);
|
||||
_waitIndicator.Visible = true;
|
||||
}
|
||||
|
||||
private static string ColorTimeline(Age.Engine.Model.ColorTransitionState? state)
|
||||
=> state is { } c
|
||||
? $" color=0x{c.Current:x8}->0x{c.Target:x8} colorProgress={c.Progress:0.000}"
|
||||
|
||||
Reference in New Issue
Block a user