namespace Aeshnidae.Trials;
///
/// Entry point ACE looks for (assembly "Aeshnidae.Trials.dll" -> type
/// "Aeshnidae.Trials.Mod").
///
/// The Trials: a fresh personal dungeon copy (Aeshnidae.InstancesNoDat), entered
/// through its portal, is a timed course - the clock runs until everything in it is
/// dead, nothing respawns meanwhile, and leaving, dying or a teleport voids the run.
/// Bests per character and per dungeon and tier on the shard database, Radiance for
/// a clear through Aeshnidae.Bank, a trophy and experience for the week's fastest
/// clear of each dungeon, trials.json for the website, a page in the Codex. /trial.
///
public class Mod : IHarmonyMod
{
public static readonly string Name = typeof(Mod).Assembly.GetName().Name!;
/// Harmony owner id. Unique per mod - it is what UnpatchAll targets.
public static readonly string HarmonyId = $"mod.{Name.ToLowerInvariant()}";
/// Folder this mod was loaded from, e.g. C:\ACEPublic\Mods\Aeshnidae.Trials
public static readonly string ModPath = Path.Combine(ModManager.ModPath, Name);
public static ModContainer? Container => ModManager.GetModContainerByPath(ModPath);
/// Live settings, re-read on enable and by /trial-reload.
public static Settings Settings { get; private set; } = new();
/// landblock -> trial, from the settings.
public static Dictionary Trials { get; private set; } = new();
private static Harmony? _harmony;
private bool _disposed;
public void Initialize()
{
Settings = Settings.Load(ModPath);
Trials = Settings.TrialMap();
_harmony = new Harmony(HarmonyId);
// Pass the assembly explicitly. The parameterless overload finds its target
// by walking the stack, and the JIT inlines a small Initialize(), so the walk
// lands on ACE.Server instead of this mod and silently patches nothing.
_harmony.PatchAllUncategorized(typeof(Mod).Assembly);
// ModManager.Initialize runs before DatabaseManager is started, but ConfigManager
// is already up, and that is all the tables need to open their own connection.
try
{
TrialDb.Initialize();
}
catch (Exception ex)
{
ModManager.Log($"[{Name}] could not open the records tables; runs will be timed and told but nothing recorded: {ex.Message}",
ModManager.LogLevel.Error);
}
// The siblings may not be up yet on a cold start; every use re-checks.
Instances.Refresh();
_ = Bank.Available;
try { Feed.Write(); } catch { /* logged inside */ }
ModManager.Log($"[{Name}] ready - {Trials.Count} trial(s): {string.Join(", ", Trials.OrderBy(kv => kv.Key).Select(kv => $"{TrialName(kv.Key)} ({kv.Key:X4})"))}; " +
$"cooldown {Settings.CooldownMinutes:0} min, {Settings.RadiancePerClear:N0} Radiance per clear, " +
$"trophy {Settings.AwardDayOfWeek} {Settings.AwardHour:00}:00 with {Settings.WeeklyAwardXp:N0} xp (weenie {Settings.TrophyWcid}); " +
$"tables {(TrialDb.Ready ? "open" : "NOT open")}; InstancesNoDat {Instances.Status}; Bank {Bank.Status}");
}
/// The name players read for a Trial: the settings' name, else InstancesNoDat's, else the id.
public static string TrialName(ushort landblock) =>
Trials.TryGetValue(landblock, out var t) && !string.IsNullOrWhiteSpace(t.Name) ? t.Name : Instances.NameFor(landblock);
/// A Trial by name prefix, name fragment, or hex id.
public static ushort? FindTrial(string text)
{
var wanted = (text ?? "").Trim();
if (wanted.Length == 0)
return null;
if (ushort.TryParse(wanted.Replace("0x", "", StringComparison.OrdinalIgnoreCase), NumberStyles.HexNumber, null, out var id) && Trials.ContainsKey(id))
return id;
var keys = Trials.Keys.OrderBy(k => k).ToList();
foreach (var k in keys)
if (TrialName(k).Equals(wanted, StringComparison.OrdinalIgnoreCase))
return k;
// "the Citadel Below" and "Citadel" should both find it.
foreach (var k in keys)
if (TrialName(k).StartsWith(wanted, StringComparison.OrdinalIgnoreCase))
return k;
foreach (var k in keys)
if (TrialName(k).Contains(wanted, StringComparison.OrdinalIgnoreCase))
return k;
return null;
}
/// /trial-reload: the settings, not the patches. Runs in progress are kept.
public static void ReloadSettings()
{
Settings = Settings.Load(ModPath);
Trials = Settings.TrialMap();
try { Feed.Write(); } catch { /* logged inside */ }
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
Instances.Unbind();
Runs.ForgetAll();
_harmony?.UnpatchAll(HarmonyId);
_harmony = null;
ModManager.Log($"[{Name}] shut down");
}
_disposed = true;
}
}