Fix retained ADV text in menu layouts
This commit is contained in:
@@ -51,10 +51,20 @@ public sealed class GodotAdvHost : IHost
|
||||
private readonly object _textLock = new();
|
||||
private readonly Dictionary<int, List<SurfaceTextDraw>> _surfaceText = new();
|
||||
private readonly Dictionary<int, AdvTextHistoryRenderBatch> _historyText = new();
|
||||
private sealed class LiveTextState
|
||||
{
|
||||
public required AdvLiveTextRun Run;
|
||||
public required long StartedMs;
|
||||
public required int GlyphDelayMilliseconds;
|
||||
}
|
||||
private readonly List<LiveTextState> _liveText = new();
|
||||
private LiveTextState? _activeLiveText;
|
||||
private string _advText = "";
|
||||
private int _advTextX = 100, _advTextY = 47;
|
||||
private int _currentAdvLayout = 1; // SYSTEM4's ordinary SC0000 ADV layout
|
||||
private long _advTextStartedMs;
|
||||
private int _activeGlyphDelayMilliseconds = 50;
|
||||
private int _messageGlyphDelayMilliseconds = 50;
|
||||
private bool _advTextForceComplete;
|
||||
private readonly Dictionary<int, AdvWaitIndicatorConfig> _waitIndicators = new();
|
||||
private readonly object _messageSkipLock = new();
|
||||
@@ -138,22 +148,72 @@ public sealed class GodotAdvHost : IHost
|
||||
_presentationRequestConsumed.WaitOne(50);
|
||||
}
|
||||
|
||||
public int MessageGlyphDelayMilliseconds
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_textLock) return _messageGlyphDelayMilliseconds;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetMessageGlyphDelayMilliseconds(int milliseconds)
|
||||
{
|
||||
lock (_textLock) _messageGlyphDelayMilliseconds = System.Math.Max(0, milliseconds);
|
||||
_timeline?.Event("message-glyph-delay", new() { ["delay_ms"] = milliseconds });
|
||||
}
|
||||
|
||||
public void ShowText(int offset, string text)
|
||||
{
|
||||
Captured.Add((offset, text));
|
||||
_locator.Text(offset, text);
|
||||
AdvTextLayoutSnapshot layout;
|
||||
int delay;
|
||||
lock (_textLock)
|
||||
{
|
||||
_advText = text;
|
||||
_advTextStartedMs = _clock.NowMs;
|
||||
layout = new AdvTextLayoutSnapshot(
|
||||
_currentAdvLayout, 800, 600, 0, 0, _advTextX, _advTextY, 800, 600);
|
||||
delay = _messageGlyphDelayMilliseconds;
|
||||
}
|
||||
ShowText(new AdvLiveTextRun(
|
||||
offset, layout, AdvTextStyle.Default, text, Array.Empty<string>()), delay);
|
||||
}
|
||||
|
||||
public void ShowText(AdvLiveTextRun run, int glyphDelayMilliseconds)
|
||||
{
|
||||
Captured.Add((run.SourceOffset, run.Text));
|
||||
_locator.Text(run.SourceOffset, run.Text);
|
||||
int delay = System.Math.Max(0, glyphDelayMilliseconds);
|
||||
var state = new LiveTextState
|
||||
{
|
||||
Run = run,
|
||||
StartedMs = _clock.NowMs,
|
||||
GlyphDelayMilliseconds = delay,
|
||||
};
|
||||
lock (_textLock)
|
||||
{
|
||||
_liveText.Add(state);
|
||||
_activeLiveText = state;
|
||||
_advText = run.Text;
|
||||
_advTextX = run.Layout.CursorX;
|
||||
_advTextY = run.Layout.CursorY;
|
||||
_currentAdvLayout = run.Layout.Slot;
|
||||
_advTextStartedMs = state.StartedMs;
|
||||
_activeGlyphDelayMilliseconds = delay;
|
||||
_advTextForceComplete = _messageSkipActive;
|
||||
IsTextRevealing = text.Length > 0 && !_messageSkipActive;
|
||||
IsTextRevealing = run.Text.Length > 0 && delay > 0 && !_messageSkipActive;
|
||||
}
|
||||
_timeline?.State("text-reveal", new()
|
||||
{
|
||||
["offset"] = $"0x{offset:x}", ["x"] = _advTextX, ["y"] = _advTextY,
|
||||
["glyphs"] = text.Length, ["delay_ms"] = 50,
|
||||
["offset"] = $"0x{run.SourceOffset:x}",
|
||||
["layout"] = run.Layout.Slot,
|
||||
["x"] = run.Layout.OriginX + run.Layout.CursorX,
|
||||
["y"] = run.Layout.OriginY + run.Layout.CursorY,
|
||||
["glyphs"] = run.Text.Length, ["delay_ms"] = delay,
|
||||
});
|
||||
if (!IsTextRevealing)
|
||||
{
|
||||
Interlocked.Exchange(ref _presentRequested, 1);
|
||||
_timeline?.State("running", new() { ["text_reveal_complete"] = true });
|
||||
return;
|
||||
}
|
||||
bool scriptSuspended = SuspendScriptForPresentation();
|
||||
try
|
||||
{
|
||||
@@ -162,7 +222,8 @@ public sealed class GodotAdvHost : IHost
|
||||
{
|
||||
lock (_textLock)
|
||||
{
|
||||
if (_advTextForceComplete || _clock.NowMs - _advTextStartedMs >= text.Length * 50L)
|
||||
if (_advTextForceComplete
|
||||
|| _clock.NowMs - _advTextStartedMs >= run.Text.Length * (long)delay)
|
||||
IsTextRevealing = false;
|
||||
}
|
||||
if (IsTextRevealing) _frameSignal.WaitOne(50);
|
||||
@@ -207,11 +268,33 @@ public sealed class GodotAdvHost : IHost
|
||||
{
|
||||
int visible = _advTextForceComplete || !IsTextRevealing
|
||||
? _advText.Length
|
||||
: (int)System.Math.Clamp((_clock.NowMs - _advTextStartedMs) / 50L + 1, 0, _advText.Length);
|
||||
: (int)System.Math.Clamp(
|
||||
(_clock.NowMs - _advTextStartedMs) / System.Math.Max(1, _activeGlyphDelayMilliseconds) + 1,
|
||||
0, _advText.Length);
|
||||
return (_advText, _advTextX, _advTextY, visible, IsTextRevealing);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<LiveAdvTextSnapshot> SnapshotLiveAdvText()
|
||||
{
|
||||
lock (_textLock)
|
||||
{
|
||||
var snapshot = new LiveAdvTextSnapshot[_liveText.Count];
|
||||
for (int i = 0; i < _liveText.Count; i++)
|
||||
{
|
||||
var state = _liveText[i];
|
||||
bool revealing = ReferenceEquals(state, _activeLiveText) && IsTextRevealing;
|
||||
int visible = !revealing || _advTextForceComplete || state.GlyphDelayMilliseconds == 0
|
||||
? state.Run.Text.Length
|
||||
: (int)System.Math.Clamp(
|
||||
(_clock.NowMs - state.StartedMs) / state.GlyphDelayMilliseconds + 1,
|
||||
0, state.Run.Text.Length);
|
||||
snapshot[i] = new LiveAdvTextSnapshot(state.Run, visible, revealing);
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Layout that owns the ordinary ADV overlay. Nested callback scripts such as HISTORY can select and
|
||||
/// mutate other layouts while the parent wait remains parked; those transient selections must not move
|
||||
@@ -244,7 +327,19 @@ public sealed class GodotAdvHost : IHost
|
||||
|
||||
public void ClearRenderedAdvTextLayout(int layoutSlot)
|
||||
{
|
||||
lock (_textLock) _historyText.Remove(layoutSlot == 0 ? _currentAdvLayout : layoutSlot);
|
||||
lock (_textLock)
|
||||
{
|
||||
int slot = layoutSlot == 0 ? _currentAdvLayout : layoutSlot;
|
||||
_historyText.Remove(slot);
|
||||
_liveText.RemoveAll(state => state.Run.Layout.Slot == slot);
|
||||
if (_activeLiveText?.Run.Layout.Slot == slot)
|
||||
{
|
||||
_activeLiveText = null;
|
||||
_advText = "";
|
||||
_advTextForceComplete = false;
|
||||
IsTextRevealing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RenderTextHistory(AdvTextHistoryRenderBatch batch)
|
||||
@@ -817,6 +912,8 @@ public sealed class GodotAdvHost : IHost
|
||||
_surfaceText.Clear();
|
||||
_surfaceResources.Clear();
|
||||
_historyText.Clear();
|
||||
_liveText.Clear();
|
||||
_activeLiveText = null;
|
||||
_advText = "";
|
||||
_advTextX = 100;
|
||||
_advTextY = 47;
|
||||
@@ -1558,6 +1655,8 @@ public sealed class GodotAdvHost : IHost
|
||||
}
|
||||
|
||||
public readonly record struct SurfaceTextDraw(int X, int Y, string Text, AdvTextStyle Style);
|
||||
public readonly record struct LiveAdvTextSnapshot(
|
||||
AdvLiveTextRun Run, int VisibleGlyphs, bool Revealing);
|
||||
public sealed record GodotHostDiagnosticSnapshot(
|
||||
string CurrentScene, bool IsInputWaiting, bool IsTransitionWaiting, bool IsSleeping,
|
||||
bool IsTextRevealing, bool IsModalMovieWaiting, bool IsAdvPagePresentationSuspended,
|
||||
|
||||
@@ -31,6 +31,7 @@ public partial class Main : Godot.Control
|
||||
private readonly byte[] _screenPixels = new byte[ScreenWidth * ScreenHeight * 4];
|
||||
private Label _text = null!;
|
||||
private Label _speaker = null!;
|
||||
private readonly System.Collections.Generic.List<Label> _advTextLabels = new();
|
||||
private readonly System.Collections.Generic.List<Label> _surfaceTextLabels = new();
|
||||
private readonly System.Collections.Generic.Dictionary<int, Label> _historyTextLabels = new();
|
||||
private Font? _presentationRegularFont;
|
||||
@@ -123,6 +124,7 @@ public partial class Main : Godot.Control
|
||||
_text = new Label { AutowrapMode = TextServer.AutowrapMode.WordSmart, MouseFilter = MouseFilterEnum.Ignore };
|
||||
AddChild(_text);
|
||||
_text.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
_advTextLabels.Add(_text);
|
||||
_speaker = new Label { MouseFilter = MouseFilterEnum.Ignore, Visible = false };
|
||||
AddChild(_speaker);
|
||||
_speaker.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
@@ -1238,17 +1240,49 @@ public partial class Main : Godot.Control
|
||||
private void UpdateAdvTextPresentation()
|
||||
{
|
||||
// Modal callback scripts composite their own full-screen UI while the enclosing ADV wait remains
|
||||
// parked. The ordinary dialogue Label is a Godot overlay rather than part of the retained surface,
|
||||
// so hide it while that nested input owner is active or it leaks above HISTORY's background.
|
||||
_text.Visible = !_host.IsAdvPagePresentationSuspended && !_vm.IsRawInputCallbackActive;
|
||||
if (!_text.Visible) return;
|
||||
var t = _host.SnapshotAdvText();
|
||||
var layout = _vm.TextHistory.GetLayoutSnapshot(_host.AdvPageLayoutSlot);
|
||||
_text.Position = new Vector2(layout.OriginX + layout.CursorX, layout.OriginY + layout.CursorY);
|
||||
_text.Size = new Vector2(System.Math.Max(1, layout.Right - layout.CursorX),
|
||||
System.Math.Max(1, layout.Bottom - layout.CursorY));
|
||||
int count = System.Math.Clamp(t.VisibleGlyphs, 0, t.Text.Length);
|
||||
_text.Text = count == 0 ? "" : t.Text[..count];
|
||||
// parked. Live layout text is a Godot overlay rather than part of the retained surface. During a
|
||||
// raw-input callback, keep runs owned by that callback's script stack (STUDY -> MAMES) while hiding
|
||||
// enclosing ADV runs that would otherwise leak above a nested screen such as HISTORY.
|
||||
foreach (var label in _advTextLabels) label.Visible = false;
|
||||
if (_host.IsAdvPagePresentationSuspended) return;
|
||||
|
||||
int labelIndex = 0;
|
||||
string? rawInputOwner = _vm.RawInputCallbackScriptName;
|
||||
var snapshots = _host.SnapshotLiveAdvText();
|
||||
for (int i = 0; i < snapshots.Count; i++)
|
||||
{
|
||||
var snapshot = snapshots[i];
|
||||
var run = snapshot.Run;
|
||||
if (run.Text.Length == 0 || !ShouldShow(run)) continue;
|
||||
var visibleText = new System.Text.StringBuilder();
|
||||
AppendVisible(snapshot);
|
||||
while (i + 1 < snapshots.Count
|
||||
&& snapshots[i + 1].Run.Layout == run.Layout
|
||||
&& snapshots[i + 1].Run.Style == run.Style
|
||||
&& ShouldShow(snapshots[i + 1].Run))
|
||||
{
|
||||
i++;
|
||||
AppendVisible(snapshots[i]);
|
||||
}
|
||||
|
||||
var label = GetAdvTextLabel(labelIndex++);
|
||||
var layout = run.Layout;
|
||||
label.Position = new Vector2(layout.OriginX + layout.CursorX, layout.OriginY + layout.CursorY);
|
||||
label.Size = new Vector2(System.Math.Max(1, layout.Right - layout.CursorX),
|
||||
System.Math.Max(1, layout.Bottom - layout.CursorY));
|
||||
label.Text = visibleText.ToString();
|
||||
ApplyAdvTextStyle(label, run.Style);
|
||||
label.Visible = true;
|
||||
|
||||
void AppendVisible(LiveAdvTextSnapshot item)
|
||||
{
|
||||
int count = System.Math.Clamp(item.VisibleGlyphs, 0, item.Run.Text.Length);
|
||||
if (count != 0) visibleText.Append(item.Run.Text, 0, count);
|
||||
}
|
||||
|
||||
bool ShouldShow(AdvLiveTextRun item)
|
||||
=> rawInputOwner == null || item.BelongsToScript(rawInputOwner);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateHistoryTextPresentation()
|
||||
@@ -1278,6 +1312,12 @@ public partial class Main : Godot.Control
|
||||
return _surfaceTextLabels[index];
|
||||
}
|
||||
|
||||
private Label GetAdvTextLabel(int index)
|
||||
{
|
||||
while (_advTextLabels.Count <= index) _advTextLabels.Add(CreateAdvPresentationLabel());
|
||||
return _advTextLabels[index];
|
||||
}
|
||||
|
||||
private static void ApplySurfaceTextTransform(Label label, Affine2D localToDestination,
|
||||
int localX, int localY)
|
||||
{
|
||||
@@ -1825,7 +1865,11 @@ public partial class Main : Godot.Control
|
||||
_status.Text = "";
|
||||
if (_locatorHudVisible) _locatorHud.Text = _locator.CurrentDisplay;
|
||||
}
|
||||
public void ClearPage() { _text.Text = ""; _status.Text = ""; }
|
||||
public void ClearPage()
|
||||
{
|
||||
foreach (var label in _advTextLabels) label.Text = "";
|
||||
_status.Text = "";
|
||||
}
|
||||
public void ShowEnd() => _status.Text = "— end —";
|
||||
|
||||
// The selftest verifies the GODOT PLUMBING (background thread + semaphore suspend on wait-for-input
|
||||
|
||||
Reference in New Issue
Block a user