namespace Aeshnidae.LootTables; /// /// ACE's code-side loot tables made editable at runtime. See Tables for how; the staff /// site (/staff/loot, "Code tables") writes and runs /// /loottables-apply, and reads to show what is there. /// No Harmony patches: the tables are data, edited in place. /// public class Mod : IHarmonyMod { public static readonly string Name = typeof(Mod).Assembly.GetName().Name!; public static readonly string ModPath = Path.Combine(ModManager.ModPath, Name); public static ModContainer? Container => ModManager.GetModContainerByPath(ModPath); public static Settings Settings { get; private set; } = new(); private bool _disposed; public void Initialize() { Settings = Settings.Load(ModPath); try { Tables.Discover(); var result = ApplyNow(); ModManager.Log($"[{Name}] {Tables.All.Count} code-side loot tables found; {result.Applied} overridden" + (result.Errors.Count > 0 ? $"; {result.Errors.Count} override(s) refused: {string.Join(" | ", result.Errors)}" : "")); } catch (Exception ex) { // Retail stays in force: nothing is written to a table until every step before it worked. ModManager.Log($"[{Name}] could not start: {ex}", ModManager.LogLevel.Error); } } /// Reads the overrides file, applies it where nothing is rolling, writes the catalogue. public static Tables.ApplyResult ApplyNow() { var file = Settings.ReadOverrides(ModPath); Tables.ApplyResult? result = null; OnWorldThread(() => result = Tables.Apply(file)); result ??= new Tables.ApplyResult(0, ["the world thread did not run the apply within 15 s - nothing changed; try again"], []); Tables.WriteCatalog(Path.Combine(ModPath, Settings.CatalogFile), result); return result; } /// /// Runs the action between the landblock ticks, when no loot is being rolled - or at once /// while the world is closed (starting up, shutting down), when nothing is rolling anyway. /// public static void OnWorldThread(Action action) { if (WorldManager.WorldStatus != WorldManager.WorldStatusState.Open) { action(); return; } using var done = new ManualResetEventSlim(); Exception? error = null; WorldManager.EnqueueAction(new ActionEventDelegate(() => { try { action(); } catch (Exception ex) { error = ex; } finally { done.Set(); } })); if (done.Wait(TimeSpan.FromSeconds(15)) && error is not null) throw new InvalidOperationException("apply failed on the world thread", error); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { // Retail back in the tables, so the next load (a hot reload) snapshots retail and // not this load's overrides. try { OnWorldThread(Tables.RestoreRetail); } catch (Exception ex) { ModManager.Log($"[{Name}] could not restore retail on unload: {ex.Message}", ModManager.LogLevel.Warn); } ModManager.Log($"[{Name}] shut down (retail restored)"); } _disposed = true; } }