From 7acf6b6841ac7b2bb07e9d3a0d0759b2653b1933 Mon Sep 17 00:00:00 2001 From: seton Date: Fri, 11 Sep 2026 15:19:22 -0400 Subject: [PATCH] Core: log ring buffer and live module TrySetActive Agents can read recent S3 log lines without scraping UMM files. TrySetActive changes session state only, so benchmarks and MCP can toggle modules without writing the UMM enabled flags. --- src/Core/Log.cs | 52 ++++++++++++++++++++++++++++++-- src/Core/ModuleRegistry.cs | 62 ++++++++++++++++++++++++++++++++++++++ src/Main.cs | 2 ++ 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/src/Core/Log.cs b/src/Core/Log.cs index 310e23f..01bb7a8 100644 --- a/src/Core/Log.cs +++ b/src/Core/Log.cs @@ -1,3 +1,4 @@ +using System; using UnityModManagerNet; namespace S3.Core; @@ -8,11 +9,56 @@ namespace S3.Core; /// public static class Log { + private const int RingN = 200; + private static readonly object RingLock = new(); + private static readonly string[] Ring = new string[RingN]; + private static int _ringCount; + private static UnityModManager.ModEntry.ModLogger? _logger; public static void Init(UnityModManager.ModEntry modEntry) => _logger = modEntry.Logger; - public static void Info(string msg) => _logger?.Log(msg); - public static void Warn(string msg) => _logger?.Warning(msg); - public static void Error(string msg) => _logger?.Error(msg); + public static void Info(string msg) + { + Push("INF", msg); + _logger?.Log(msg); + } + + public static void Warn(string msg) + { + Push("WRN", msg); + _logger?.Warning(msg); + } + + public static void Error(string msg) + { + Push("ERR", msg); + _logger?.Error(msg); + } + + public static string[] Tail(int count) + { + if (count < 1) count = 1; + if (count > RingN) count = RingN; + lock (RingLock) + { + int have = Math.Min(_ringCount, RingN); + int take = Math.Min(count, have); + var result = new string[take]; + int start = _ringCount - take; + for (int i = 0; i < take; i++) + result[i] = Ring[(start + i) % RingN]; + return result; + } + } + + static void Push(string level, string msg) + { + string line = DateTime.Now.ToString("HH:mm:ss") + " [" + level + "] " + msg; + lock (RingLock) + { + Ring[_ringCount % RingN] = line; + _ringCount++; + } + } } diff --git a/src/Core/ModuleRegistry.cs b/src/Core/ModuleRegistry.cs index 9133aca..1c34e72 100644 --- a/src/Core/ModuleRegistry.cs +++ b/src/Core/ModuleRegistry.cs @@ -10,6 +10,7 @@ namespace S3.Core; public sealed class ModuleRegistry { private readonly List _modules = new(); + private readonly HashSet _active = new(); public IReadOnlyList Modules => _modules; @@ -29,6 +30,7 @@ public sealed class ModuleRegistry try { m.OnEnable(); + _active.Add(m); Log.Info($"[{m.Id}] enabled."); } catch (Exception e) @@ -38,6 +40,66 @@ public sealed class ModuleRegistry } } + public bool IsActive(IModule module) => _active.Contains(module); + + public bool IsActive(string id) + { + IModule? module = Find(id); + return module != null && _active.Contains(module); + } + + public IModule? Find(string id) + { + foreach (IModule module in _modules) + if (string.Equals(module.Id, id, StringComparison.OrdinalIgnoreCase)) + return module; + return null; + } + + /// + /// Changes a module's live state without changing its persisted Enabled setting. + /// Callers that want the change persisted must update Enabled and SaveSettings. + /// + public bool TrySetActive(string id, bool active, out string message) + { + IModule? module = Find(id); + if (module == null) + { + message = "unknown module: " + id; + return false; + } + + bool current = _active.Contains(module); + if (current == active) + { + message = $"{module.Id} active={active} (unchanged)"; + return true; + } + + try + { + if (active) + { + module.OnEnable(); + _active.Add(module); + } + else + { + module.OnDisable(); + _active.Remove(module); + } + message = $"{module.Id} active={active}"; + Log.Info($"[{module.Id}] live active={active}."); + return true; + } + catch (Exception e) + { + message = $"{module.Id} active={current}; transition failed: {e.Message}"; + Log.Error($"[{module.Id}] live transition to active={active} failed: {e}"); + return false; + } + } + /// Persist every module's settings (called from UMM's OnSaveGUI). public void SaveAll() { diff --git a/src/Main.cs b/src/Main.cs index faef95c..127fa84 100644 --- a/src/Main.cs +++ b/src/Main.cs @@ -16,6 +16,7 @@ namespace S3; public static class Main { internal static UnityModManager.ModEntry ModEntry { get; private set; } = null!; + public static ModuleRegistry Registry { get; private set; } = null!; private static ModuleRegistry _registry = null!; @@ -26,6 +27,7 @@ public static class Main SettingsStore.Init(modEntry.Path); _registry = new ModuleRegistry(); + Registry = _registry; // Modules are registered here. Each module loads its own settings in its // constructor. Order here is display order in the settings panel. _registry.Register(new Modules.PhysicsOptimizer.PhysicsOptimizerModule());