diff --git a/ui/JevOverlay/.gitignore b/ui/JevOverlay/.gitignore index 3b7b156..f2ab710 100644 --- a/ui/JevOverlay/.gitignore +++ b/ui/JevOverlay/.gitignore @@ -1,4 +1,4 @@ bin/ obj/ # Local absolute path to the decision log. -JevOverlay.config.json +JevOverlay.conf diff --git a/ui/JevOverlay/Core/OverlayConfig.cs b/ui/JevOverlay/Core/OverlayConfig.cs index 7e4e692..30cee3f 100644 --- a/ui/JevOverlay/Core/OverlayConfig.cs +++ b/ui/JevOverlay/Core/OverlayConfig.cs @@ -4,7 +4,7 @@ namespace JevOverlay.Core; public static class OverlayConfig { - public const string FileName = "JevOverlay.config.json"; + public const string FileName = "JevOverlay.conf"; public static string LoadLogPath(string modDirectory) { diff --git a/ui/JevOverlay/Mod.cs b/ui/JevOverlay/Mod.cs new file mode 100644 index 0000000..fa15f6d --- /dev/null +++ b/ui/JevOverlay/Mod.cs @@ -0,0 +1,105 @@ +using System.Diagnostics; +using Godot; +using JevOverlay.Core; +using JevOverlay.UI; +using MegaCrit.Sts2.Core.Modding; + +namespace JevOverlay; + +[ModInitializer("Initialize")] +public static class Mod +{ + private static SceneTree? tree; + private static OverlayView? view; + private static LogTail? reader; + private static readonly DecisionHistory History = new(); + private static Task? poll; + private static double nextPoll; + private static double? lastRows; + private static bool keyDown; + private static string? configError; + private static TailBatch? lastBatch; + + public static void Initialize() + { + if (tree is not null) return; + try + { + string directory = Path.GetDirectoryName(typeof(Mod).Assembly.Location)!; + try { reader = new LogTail(OverlayConfig.LoadLogPath(directory)); } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or + System.Text.Json.JsonException or FormatException or ArgumentException) + { + configError = $"Configure log_path in {OverlayConfig.FileName}, then restart the game."; + GD.PrintErr($"[JevOverlay] Configuration unavailable or invalid ({ex.GetType().Name})."); + } + tree = (SceneTree)Engine.GetMainLoop(); + tree.ProcessFrame += OnFrame; + tree.Root.TreeExiting += Shutdown; + GD.Print("[JevOverlay] Read-only decision viewer initialized. F8 toggles the panel."); + } + catch (Exception ex) + { + GD.PrintErr($"[JevOverlay] Could not initialize ({ex.GetType().Name})."); + } + } + + private static void OnFrame() + { + try + { + view ??= new OverlayView(tree!); + bool pressed = Input.IsPhysicalKeyPressed(Key.F8); + if (pressed && !keyDown) view.ToggleCollapsed(); + keyDown = pressed; + view.Resize(tree!.Root.GetVisibleRect().Size); + if (configError is not null) + { + view.SetStatus(configError); + return; + } + double now = (double)Stopwatch.GetTimestamp() / Stopwatch.Frequency; + if (poll is { IsCompleted: true }) + { + // Only file I/O runs on the worker. All model and UI updates run here. + lastBatch = poll.GetAwaiter().GetResult(); + poll = null; + History.Apply(lastBatch); + if (lastBatch.Lines.Count > 0) lastRows = now; + if (lastBatch.Reset || lastBatch.Lines.Count > 0) view.Render(History); + UpdateStatus(now); + } + if (poll is null && now >= nextPoll) + { + nextPoll = now + 0.5; + poll = Task.Run(reader!.Poll); + UpdateStatus(now); + } + } + catch (Exception ex) + { + // A broken viewer must not interrupt play or produce one error per frame. + GD.PrintErr($"[JevOverlay] Viewer disabled ({ex.GetType().Name})."); + if (tree is not null) tree.ProcessFrame -= OnFrame; + view?.Dispose(); + view = null; + } + } + + private static void UpdateStatus(double now) + { + string text = lastBatch?.Error ?? (lastBatch?.CatchingUp == true ? "Reading pending log rows…" + : lastRows is null ? "Waiting for complete log rows." + : $"Read-only log · no new rows for {Math.Max(0, now - lastRows.Value):0}s"); + if (History.SkippedLines > 0) text += $" · skipped {History.SkippedLines}"; + if (History.Notice is { } notice) + text += "\n" + (notice.Length > 110 ? notice[..110] + "…" : notice); + view!.SetStatus(text, text + "\nFile activity does not prove that the bot or game is running."); + } + + private static void Shutdown() + { + if (tree is not null) tree.ProcessFrame -= OnFrame; + // The scene tree frees the controls. A pending bounded file read can finish independently. + } +} diff --git a/ui/JevOverlay/UI/OverlayView.cs b/ui/JevOverlay/UI/OverlayView.cs index 4ab931a..73bf625 100644 --- a/ui/JevOverlay/UI/OverlayView.cs +++ b/ui/JevOverlay/UI/OverlayView.cs @@ -159,8 +159,11 @@ public sealed class OverlayView else { history.Select(index); - summary.Text = entries[index].Summary(); - evidence.Text = entries[index].Evidence(); + // Do not reset text selection or scroll when another decision arrives. + string nextSummary = entries[index].Summary(); + string nextEvidence = entries[index].Evidence(); + if (summary.Text != nextSummary) summary.Text = nextSummary; + if (evidence.Text != nextEvidence) evidence.Text = nextEvidence; } }