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.
This commit is contained in:
Seton Carmichael 2026-09-11 15:19:22 -04:00
parent 1d4d5a6312
commit 7acf6b6841
3 changed files with 113 additions and 3 deletions

View file

@ -1,3 +1,4 @@
using System;
using UnityModManagerNet;
namespace S3.Core;
@ -8,11 +9,56 @@ namespace S3.Core;
/// </summary>
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++;
}
}
}

View file

@ -10,6 +10,7 @@ namespace S3.Core;
public sealed class ModuleRegistry
{
private readonly List<IModule> _modules = new();
private readonly HashSet<IModule> _active = new();
public IReadOnlyList<IModule> 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;
}
/// <summary>
/// Changes a module's live state without changing its persisted Enabled setting.
/// Callers that want the change persisted must update Enabled and SaveSettings.
/// </summary>
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;
}
}
/// <summary>Persist every module's settings (called from UMM's OnSaveGUI).</summary>
public void SaveAll()
{

View file

@ -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());