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.
105 lines
4 KiB
C#
105 lines
4 KiB
C#
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.
|
|
}
|
|
}
|