feat(ui): integrate read-only Jev decision overlay

Initialize the Godot panel, poll the decision log in the background, and preserve inspection state during updates. Use a .conf file to keep local settings separate from mod manifests.
This commit is contained in:
0xrsydn 2026-09-22 15:13:45 +07:00
commit 130273570b
4 changed files with 112 additions and 4 deletions

View file

@ -1,4 +1,4 @@
bin/ bin/
obj/ obj/
# Local absolute path to the decision log. # Local absolute path to the decision log.
JevOverlay.config.json JevOverlay.conf

View file

@ -4,7 +4,7 @@ namespace JevOverlay.Core;
public static class OverlayConfig public static class OverlayConfig
{ {
public const string FileName = "JevOverlay.config.json"; public const string FileName = "JevOverlay.conf";
public static string LoadLogPath(string modDirectory) public static string LoadLogPath(string modDirectory)
{ {

105
ui/JevOverlay/Mod.cs Normal file
View file

@ -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<TailBatch>? 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.
}
}

View file

@ -159,8 +159,11 @@ public sealed class OverlayView
else else
{ {
history.Select(index); history.Select(index);
summary.Text = entries[index].Summary(); // Do not reset text selection or scroll when another decision arrives.
evidence.Text = entries[index].Evidence(); string nextSummary = entries[index].Summary();
string nextEvidence = entries[index].Evidence();
if (summary.Text != nextSummary) summary.Text = nextSummary;
if (evidence.Text != nextEvidence) evidence.Text = nextEvidence;
} }
} }