namespace Aeshnidae.AllegianceBounty;
///
/// Entry point ACE looks for.
///
/// ModContainer resolves the type purely by convention:
/// assembly "Aeshnidae.AllegianceBounty.dll" -> type "Aeshnidae.AllegianceBounty.Mod"
/// so this class must be named Mod and live in a namespace matching the
/// assembly name.
///
/// The allegiance bounty: a quest one member completes is a day of better kills for
/// the whole allegiance. Quests.json is the list, Settings.json the terms, the shard
/// table `aeshnidae_allegiance_bounty` the memory across restarts.
///
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.AllegianceBounty
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 /bounty-reload.
public static Settings Settings { get; private set; } = new();
private static Harmony? _harmony;
private bool _disposed;
public void Initialize()
{
Settings = Settings.Load(ModPath);
QuestList.Load(ModPath, Settings.QuestsFile);
_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 table needs to open its own connection.
try
{
BountyDb.Initialize();
Bounty.Load();
// A hot reload rebuilds the statics while players stay online and the personal
// day is only read at login; read it for everyone already in the world.
foreach (var online in PlayerManager.GetAllOnline())
Personal.Load(online);
ModManager.Log($"[{Name}] ready - {QuestList.Detectable} detectable quests, +{Settings.QuestPercent:0.##}% per quest, " +
$"+{Settings.KillTaskPercent:0.##}% per kill task, {Settings.HoursActive:0.#}h each, " +
$"{(Settings.MaxPercent > 0 ? $"capped at {Settings.MaxPercent:0.##}%" : "uncapped")}, " +
$"{Bounty.ActiveEntries} entries active across {Bounty.ActiveAllegiances} allegiances (`{BountyDb.TableName}` on the shard database); " +
$"personal {(Settings.PersonalEnabled ? $"+{Settings.PersonalPercentPerStamp:0.##}% per distinct stamp to {Settings.PersonalMaxPercent:0.##}%, reset {Settings.PersonalResetHour:00}:00 (`{Personal.TableName}`)" : "off")}");
}
catch (Exception ex)
{
ModManager.Log($"[{Name}] could not open the bounty table; nothing will be recorded or applied: {ex.Message}",
ModManager.LogLevel.Error);
}
}
/// /bounty-reload: the files, not the patches. Active entries are re-read from the table too.
public static void ReloadFiles()
{
Settings = Settings.Load(ModPath);
QuestList.Load(ModPath, Settings.QuestsFile);
if (BountyDb.Ready)
{
Bounty.Load();
Personal.ForgetAll(); // re-read lazily: the reset hour may have changed
foreach (var online in PlayerManager.GetAllOnline())
Personal.Load(online);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_harmony?.UnpatchAll(HarmonyId);
_harmony = null;
ModManager.Log($"[{Name}] shut down");
}
_disposed = true;
}
}