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:
parent
1e14680c39
commit
130273570b
4 changed files with 112 additions and 4 deletions
2
ui/JevOverlay/.gitignore
vendored
2
ui/JevOverlay/.gitignore
vendored
|
|
@ -1,4 +1,4 @@
|
|||
bin/
|
||||
obj/
|
||||
# Local absolute path to the decision log.
|
||||
JevOverlay.config.json
|
||||
JevOverlay.conf
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
105
ui/JevOverlay/Mod.cs
Normal file
105
ui/JevOverlay/Mod.cs
Normal 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.
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue