Implement DEBUGMAP combat frontier
This commit is contained in:
@@ -17,6 +17,10 @@ public sealed class GodotAdvHost : IHost
|
||||
private readonly Stack<string> _scriptContexts = new();
|
||||
private readonly object _imageLock = new();
|
||||
private readonly Dictionary<int, RgbaImage?> _images = new(); // raw catalog id -> decoded pixels
|
||||
// Mutable AGE surfaces are published by replacing immutable RgbaImage snapshots, so the compositor
|
||||
// can safely finish reading an old frame while the VM prepares a copied-rectangle update.
|
||||
private readonly Dictionary<int, RgbaImage> _surfaceImages = new();
|
||||
private readonly Dictionary<int, long> _surfaceColorKeys = new();
|
||||
private readonly Dictionary<int, long> _surfaceResources = new(); // surface slot -> normalized raw catalog id
|
||||
private readonly Dictionary<long, (RgbaImage Image, string Name, int RawIndex)> _movieFrames = new();
|
||||
private readonly Dictionary<int, long> _movieBySurface = new();
|
||||
@@ -47,6 +51,8 @@ public sealed class GodotAdvHost : IHost
|
||||
private volatile bool _messageSkipActive;
|
||||
private int _voiceBgmDuckControl;
|
||||
private (AudioPayload Audio, int PlaybackVariant)? _queuedSkippedVoice;
|
||||
private readonly object _scheduledVoiceLock = new();
|
||||
private (AudioPayload Audio, int PlaybackVariant, uint DelayMs, uint? StartMs)? _scheduledVoice;
|
||||
private int _activeWaitLayout;
|
||||
private long _waitIndicatorStartedMs;
|
||||
private bool _waitIndicatorEnabled;
|
||||
@@ -218,10 +224,30 @@ public sealed class GodotAdvHost : IHost
|
||||
{
|
||||
lock (_textLock)
|
||||
{
|
||||
if (!_surfaceText.TryGetValue(fill.SurfaceSlot, out var draws)) return;
|
||||
int right = fill.X + System.Math.Max(0, fill.Width);
|
||||
int bottom = fill.Y + System.Math.Max(0, fill.Height);
|
||||
draws.RemoveAll(draw => draw.X >= fill.X && draw.X < right && draw.Y >= fill.Y && draw.Y < bottom);
|
||||
if (_surfaceText.TryGetValue(fill.SurfaceSlot, out var draws))
|
||||
{
|
||||
long right = (long)fill.X + System.Math.Max(0, fill.Width);
|
||||
long bottom = (long)fill.Y + System.Math.Max(0, fill.Height);
|
||||
draws.RemoveAll(draw => draw.X >= fill.X && draw.X < right
|
||||
&& draw.Y >= fill.Y && draw.Y < bottom);
|
||||
}
|
||||
}
|
||||
|
||||
RgbaImage? destination = ResolveSurfacePixels(fill.SurfaceSlot);
|
||||
if (destination == null && _slotDims.TryGetValue(fill.SurfaceSlot, out var dimensions)
|
||||
&& dimensions.W >= 0 && dimensions.H >= 0)
|
||||
destination = new RgbaImage(dimensions.W, dimensions.H,
|
||||
new byte[checked(dimensions.W * dimensions.H * 4)]);
|
||||
if (destination != null)
|
||||
{
|
||||
var updated = new RgbaImage(destination.Width, destination.Height,
|
||||
(byte[])destination.Pixels.Clone());
|
||||
if (RgbaSurfaceOps.FillRect(updated, fill.X, fill.Y, fill.Width, fill.Height,
|
||||
unchecked((byte)fill.Alpha), fill.Rgb))
|
||||
{
|
||||
lock (_imageLock) _surfaceImages[fill.SurfaceSlot] = updated;
|
||||
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
||||
}
|
||||
}
|
||||
_timeline?.Event("surface-fill", new()
|
||||
{
|
||||
@@ -663,6 +689,7 @@ public sealed class GodotAdvHost : IHost
|
||||
_messageSkipActive = false;
|
||||
_queuedSkippedVoice = null;
|
||||
}
|
||||
lock (_scheduledVoiceLock) _scheduledVoice = null;
|
||||
System.Threading.Volatile.Write(ref _voiceBgmDuckControl, 0);
|
||||
_advPagePresentationSuspended = false;
|
||||
_modalMovieCancelled = false;
|
||||
@@ -685,7 +712,33 @@ public sealed class GodotAdvHost : IHost
|
||||
}
|
||||
|
||||
// Main thread, once per rendered frame: releases a VM thread parked in Sleep or a presentation/input wait.
|
||||
public void PulseFrame() => _frameSignal.Set();
|
||||
public void PulseFrame()
|
||||
{
|
||||
(AudioPayload Audio, int PlaybackVariant)? due = null;
|
||||
lock (_scheduledVoiceLock)
|
||||
{
|
||||
if (_scheduledVoice is { } pending)
|
||||
{
|
||||
uint now = unchecked((uint)_clock.NowMs);
|
||||
if (!pending.StartMs.HasValue)
|
||||
_scheduledVoice = pending with { StartMs = now };
|
||||
else if (unchecked(now - pending.StartMs.Value) >= pending.DelayMs)
|
||||
{
|
||||
due = (pending.Audio, pending.PlaybackVariant);
|
||||
_scheduledVoice = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (due.HasValue)
|
||||
{
|
||||
_timeline?.Event("voice-scheduled-start", new()
|
||||
{
|
||||
["file"] = due.Value.Audio.Name, ["playback_variant"] = due.Value.PlaybackVariant,
|
||||
});
|
||||
DispatchVoice(due.Value.Audio, due.Value.PlaybackVariant);
|
||||
}
|
||||
_frameSignal.Set();
|
||||
}
|
||||
|
||||
// Ordinary opcode bursts run to the next service boundary without frame pacing. Persistent message
|
||||
// Skip removes most of those boundaries, but native adv_interpreter_tick still executes one opcode per
|
||||
@@ -732,12 +785,27 @@ public sealed class GodotAdvHost : IHost
|
||||
{
|
||||
lock (_textLock) _surfaceText.Remove(slot);
|
||||
lock (_textLock) _surfaceResources.Remove(slot);
|
||||
_slotDims[slot] = (width, height);
|
||||
int safeWidth = System.Math.Max(0, width);
|
||||
int safeHeight = System.Math.Max(0, height);
|
||||
lock (_imageLock)
|
||||
{
|
||||
_surfaceImages[slot] = new RgbaImage(safeWidth, safeHeight,
|
||||
new byte[checked(safeWidth * safeHeight * 4)]);
|
||||
_surfaceColorKeys.Remove(slot);
|
||||
}
|
||||
_slotDims[slot] = (safeWidth, safeHeight);
|
||||
if (TraceOps) Godot.GD.Print($"[op] create-texture slot={slot} {width}x{height}");
|
||||
}
|
||||
|
||||
public void SetTexture(long resourceId, int slot)
|
||||
public void SetTexture(long resourceId, int slot) => SetTexture(resourceId, slot, -1);
|
||||
|
||||
public void SetTexture(long resourceId, int slot, long colorKey)
|
||||
{
|
||||
lock (_imageLock)
|
||||
{
|
||||
_surfaceImages.Remove(slot);
|
||||
_surfaceColorKeys[slot] = colorKey;
|
||||
}
|
||||
lock (_textLock)
|
||||
{
|
||||
_surfaceText.Remove(slot);
|
||||
@@ -757,6 +825,49 @@ public sealed class GodotAdvHost : IHost
|
||||
// the visible objects each frame in ascending-handle order. No immediate blit here.
|
||||
public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY) { }
|
||||
|
||||
public void CopySurfaceRect(SurfaceRectCopy copy)
|
||||
{
|
||||
RgbaImage? source = ResolveSurfacePixels(copy.SourceSurface);
|
||||
RgbaImage? destination = ResolveSurfacePixels(copy.DestinationSurface);
|
||||
if (destination == null && _slotDims.TryGetValue(copy.DestinationSurface, out var dimensions)
|
||||
&& dimensions.W >= 0 && dimensions.H >= 0)
|
||||
destination = new RgbaImage(dimensions.W, dimensions.H,
|
||||
new byte[checked(dimensions.W * dimensions.H * 4)]);
|
||||
if (source == null || destination == null)
|
||||
{
|
||||
ReportWarning($"surface copy unresolved source={copy.SourceSurface} destination={copy.DestinationSurface}");
|
||||
return;
|
||||
}
|
||||
|
||||
var updated = new RgbaImage(destination.Width, destination.Height, (byte[])destination.Pixels.Clone());
|
||||
if (RgbaSurfaceOps.CopyRect(source, updated, copy.SourceX, copy.SourceY, copy.Width, copy.Height,
|
||||
copy.DestinationX, copy.DestinationY))
|
||||
{
|
||||
lock (_imageLock) _surfaceImages[copy.DestinationSurface] = updated;
|
||||
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
||||
}
|
||||
_timeline?.Event("surface-copy", new()
|
||||
{
|
||||
["source"] = copy.SourceSurface, ["source_x"] = copy.SourceX, ["source_y"] = copy.SourceY,
|
||||
["w"] = copy.Width, ["h"] = copy.Height, ["destination"] = copy.DestinationSurface,
|
||||
["destination_x"] = copy.DestinationX, ["destination_y"] = copy.DestinationY,
|
||||
});
|
||||
}
|
||||
|
||||
private RgbaImage? ResolveSurfacePixels(int slot)
|
||||
{
|
||||
lock (_imageLock)
|
||||
if (_surfaceImages.TryGetValue(slot, out var mutable)) return mutable;
|
||||
long resourceId;
|
||||
lock (_textLock)
|
||||
if (!_surfaceResources.TryGetValue(slot, out resourceId)) return null;
|
||||
var resolved = ResolveResIdTexture(resourceId);
|
||||
if (resolved == null) return null;
|
||||
long colorKey;
|
||||
lock (_imageLock) colorKey = _surfaceColorKeys.GetValueOrDefault(slot, -1);
|
||||
return RgbaSurfaceOps.WithColorKey(resolved.Value.Image, colorKey);
|
||||
}
|
||||
|
||||
/// <summary>Resolve a gfx surface through scene-local or universal raw-id addressing and decode it
|
||||
/// from the loose-first asset store.</summary>
|
||||
public (RgbaImage Image, string Name, int AssetId, bool IsDynamic)? ResolveResIdTexture(long resId)
|
||||
@@ -775,6 +886,15 @@ public sealed class GodotAdvHost : IHost
|
||||
return asset != null && image != null ? (image, asset.Name, asset.RawIndex, false) : null;
|
||||
}
|
||||
|
||||
public (RgbaImage Image, string Name, int AssetId, bool IsDynamic)? ResolveSurfaceTexture(
|
||||
int surfaceSlot, long fallbackResourceId)
|
||||
{
|
||||
lock (_imageLock)
|
||||
if (_surfaceImages.TryGetValue(surfaceSlot, out var surface))
|
||||
return (surface, $"<surface:{surfaceSlot}>", int.MinValue + surfaceSlot, true);
|
||||
return fallbackResourceId != 0 ? ResolveResIdTexture(fallbackResourceId) : null;
|
||||
}
|
||||
|
||||
public long? PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask)
|
||||
{
|
||||
string scene = CurrentScene;
|
||||
@@ -784,6 +904,13 @@ public sealed class GodotAdvHost : IHost
|
||||
out long? stopTimeMs) ? stopTimeMs : null;
|
||||
}
|
||||
|
||||
public bool IsMovieSurfaceActive(int surfaceSlot)
|
||||
{
|
||||
lock (_imageLock)
|
||||
return _movieBySurface.TryGetValue(surfaceSlot, out long resourceId)
|
||||
&& !_completedMovies.Contains(resourceId);
|
||||
}
|
||||
|
||||
public void PlayModalMovieToSurface(long rawResourceId, int surfaceSlot, long movieFlags)
|
||||
{
|
||||
var asset = _res.ResolveRawMovie(rawResourceId);
|
||||
@@ -873,6 +1000,8 @@ public sealed class GodotAdvHost : IHost
|
||||
{
|
||||
if (!_movieBySurface.Remove(slot, out resourceId))
|
||||
{
|
||||
_surfaceImages.Remove(slot);
|
||||
_surfaceColorKeys.Remove(slot);
|
||||
lock (_textLock)
|
||||
{
|
||||
_surfaceText.Remove(slot);
|
||||
@@ -888,6 +1017,8 @@ public sealed class GodotAdvHost : IHost
|
||||
}
|
||||
_movieFrames.Remove(resourceId);
|
||||
_completedMovies.Remove(resourceId);
|
||||
_surfaceImages.Remove(slot);
|
||||
_surfaceColorKeys.Remove(slot);
|
||||
}
|
||||
lock (_textLock)
|
||||
{
|
||||
@@ -925,6 +1056,8 @@ public sealed class GodotAdvHost : IHost
|
||||
_movieFrames.Remove(resourceId);
|
||||
_completedMovies.Remove(resourceId);
|
||||
}
|
||||
_surfaceImages.Remove(slot);
|
||||
_surfaceColorKeys.Remove(slot);
|
||||
_slotDims.Remove(slot);
|
||||
}
|
||||
}
|
||||
@@ -1037,6 +1170,21 @@ public sealed class GodotAdvHost : IHost
|
||||
_timeline?.State("voice-bgm-duck-control", new() { ["flags"] = flags });
|
||||
}
|
||||
|
||||
public void ScheduleVoicePlayback(long id, int playbackVariant, long delayMs)
|
||||
{
|
||||
var asset = _res.ResolveVoice(CurrentScene, id);
|
||||
var audio = asset != null ? LoadAudio(asset) : null;
|
||||
_timeline?.Event("voice-scheduled", new()
|
||||
{
|
||||
["id"] = id, ["file"] = audio?.Name, ["playback_variant"] = playbackVariant,
|
||||
["delay_ms"] = unchecked((uint)delayMs),
|
||||
});
|
||||
lock (_scheduledVoiceLock)
|
||||
_scheduledVoice = audio == null
|
||||
? null
|
||||
: (audio, playbackVariant, unchecked((uint)delayMs), null);
|
||||
}
|
||||
|
||||
public void LoadSoundEffect(long resourceId, int channel)
|
||||
{
|
||||
if ((uint)channel >= (uint)_sfxNames.Length) return;
|
||||
|
||||
@@ -673,6 +673,10 @@ public partial class Main : Godot.Control
|
||||
int dstY = (int)System.Math.Round(projected.Y);
|
||||
float opacity = v.Alpha / 255f * globalOpacity; // transform Z is never opacity
|
||||
float strength = v.TintStrength / 255f; // tint-blend / fill strength
|
||||
var rawObject = _vm.Gfx.TryGet(v.Handle);
|
||||
var surfaceTexture = rawObject != null
|
||||
? _host.ResolveSurfaceTexture(rawObject.SourceSlot, v.SurfaceResId)
|
||||
: null;
|
||||
string outcome;
|
||||
if (v.SurfaceTransition is { } transition)
|
||||
{
|
||||
@@ -680,7 +684,7 @@ public partial class Main : Godot.Control
|
||||
outcome = $"TRANSITION slot={transition.TargetSlot} key=0x{transition.CommandKey:x} " +
|
||||
$"progress={transition.Progress:0.000} forced={transition.Forced} layers={layers}";
|
||||
}
|
||||
else if (v.SurfaceResId == 0)
|
||||
else if (v.SurfaceResId == 0 && surfaceTexture == null)
|
||||
{
|
||||
// A colored object with no bound surface = a fade/flash fill (e.g. fade-to-black). Its presence
|
||||
// is the tint STRENGTH (0=absent, 255=solid), scaled by any object opacity. Uncolored surfaceless
|
||||
@@ -702,24 +706,22 @@ public partial class Main : Godot.Control
|
||||
}
|
||||
else
|
||||
{
|
||||
var texture = _host.ResolveResIdTexture(v.SurfaceResId);
|
||||
var texture = surfaceTexture ?? _host.ResolveResIdTexture(v.SurfaceResId);
|
||||
if (texture == null) outcome = $"SKIP(resId=0x{v.SurfaceResId:x} UNRESOLVED)";
|
||||
else
|
||||
{
|
||||
BlitLayer(texture.Value.Image, texture.Value.AssetId, v.ColorKey, v.Tint, strength, v.SrcX, v.SrcY, v.W, v.H,
|
||||
localToDest, opacity, v.MultiplyTint, texture.Value.IsDynamic, v.Blend);
|
||||
var raw = _vm.Gfx.TryGet(v.Handle);
|
||||
outcome = $"slot={raw?.SourceSlot} DRAWN resId=0x{v.SurfaceResId:x} {texture.Value.Name} " +
|
||||
outcome = $"slot={rawObject?.SourceSlot} DRAWN resId=0x{v.SurfaceResId:x} {texture.Value.Name} " +
|
||||
$"src=({v.SrcX},{v.SrcY} {v.W}x{v.H}) base=({v.DstX},{v.DstY}) " +
|
||||
$"anchor=({t.AnchorX:0.0},{t.AnchorY:0.0}) dst=({dstX},{dstY}) " +
|
||||
$"scale=({t.ScaleX:0.00},{t.ScaleY:0.00}) trans=({t.TranslateX:0.0},{t.TranslateY:0.0}) " +
|
||||
$"rot=({t.RotationAngleDegrees:0.0}+{v.Rotation.AngleDegrees:0.0}) " +
|
||||
$"mode={raw?.StaticColorMode} op={opacity:0.00} tintStr={strength:0.00}" +
|
||||
$"mode={rawObject?.StaticColorMode} op={opacity:0.00} tintStr={strength:0.00}" +
|
||||
ColorTimeline(v.ColorTransition);
|
||||
}
|
||||
}
|
||||
if (decisions != null) decisions[v.Handle] = $"z{z} {outcome}";
|
||||
var rawObject = _vm.Gfx.TryGet(v.Handle);
|
||||
if (includeSurfaceText && rawObject != null)
|
||||
{
|
||||
foreach (var surfaceText in _host.SnapshotSurfaceText(rawObject.SourceSlot))
|
||||
@@ -880,7 +882,11 @@ public partial class Main : Godot.Control
|
||||
if (source.RangeTransform is { } rangeTransform)
|
||||
affine = affine.Then(rangeTransform);
|
||||
float opacity = source.Alpha / 255f * (float)transition.Progress;
|
||||
if (source.SurfaceResId == 0)
|
||||
var rawObject = _vm.Gfx.TryGet(source.Handle);
|
||||
var texture = rawObject != null
|
||||
? _host.ResolveSurfaceTexture(rawObject.SourceSlot, source.SurfaceResId)
|
||||
: null;
|
||||
if (source.SurfaceResId == 0 && texture == null)
|
||||
{
|
||||
if (source.Blend == BlendKind.Opaque) continue;
|
||||
int w = source.W > 0 ? source.W : 800, h = source.H > 0 ? source.H : 600;
|
||||
@@ -888,7 +894,7 @@ public partial class Main : Godot.Control
|
||||
}
|
||||
else
|
||||
{
|
||||
var texture = _host.ResolveResIdTexture(source.SurfaceResId);
|
||||
texture ??= _host.ResolveResIdTexture(source.SurfaceResId);
|
||||
if (texture == null) continue;
|
||||
BlitLayer(texture.Value.Image, texture.Value.AssetId, source.ColorKey, source.Tint, source.TintStrength / 255f,
|
||||
source.SrcX, source.SrcY, source.W, source.H, affine, opacity, source.MultiplyTint,
|
||||
|
||||
Reference in New Issue
Block a user