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