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. } }