namespace Aeshnidae.DiscordRelay;
///
/// Entry point ACE looks for.
///
/// ModContainer resolves the type purely by convention:
/// assembly "Aeshnidae.DiscordRelay.dll" -> type "Aeshnidae.DiscordRelay.Mod"
/// so this class must be named Mod and live in a namespace matching the
/// assembly name. Renaming one without the other is the most common reason a mod
/// loads its assembly and then logs "Missing IHarmonyMod Type".
///
public class Mod : IHarmonyMod
{
///
/// "Aeshnidae.DiscordRelay". Taken from the assembly rather than written out, so a
/// rename cannot leave a stale literal behind.
/// (Note: nameof(Aeshnidae.DiscordRelay) would yield just "DiscordRelay" - nameof on
/// a namespace returns only its last segment.)
///
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.DiscordRelay
public static readonly string ModPath = Path.Combine(ModManager.ModPath, Name);
public static ModContainer? Container => ModManager.GetModContainerByPath(ModPath);
/// Live settings, re-read every time the mod is enabled.
public static Settings Settings { get; private set; } = new();
///
/// The running bridge, or null if the mod is disabled or the chat hook could not be
/// found. The patch null-checks this, so a missing relay is simply a quiet no-op.
///
internal static ChatRelay? Relay { get; private set; }
private static Harmony? _harmony;
private bool _disposed;
///
/// Called by ModContainer.Enable() once the assembly is loaded.
/// The world is not open yet at this point, so keep this cheap and defensive -
/// anything that throws leaves the mod stuck Inactive.
///
public void Initialize()
{
Settings = Settings.Load(ModPath);
// Resolve the target before patching. It is a private ACE method referenced by
// name, so an upstream rename would otherwise take the whole mod down with a
// Harmony exception out of Initialize.
if (AccessTools.Method(typeof(TurbineChatHandler), Patches.LogTurbineChat) is null)
{
ModManager.Log($"[{Name}] TurbineChatHandler.{Patches.LogTurbineChat} not found - " +
"ACE's chat handler has changed shape. Nothing will be relayed.",
ModManager.LogLevel.Error);
return;
}
_harmony = new Harmony(HarmonyId);
// Applies [HarmonyPatch] classes that are NOT in a [HarmonyPatchCategory].
// 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);
Relay = new ChatRelay(Settings);
ModManager.Log($"[{Name}] initialized from {ModPath}; relaying {Describe()}");
}
/// Which channels are actually wired up, for the startup log line.
private static string Describe()
{
if (!Settings.Enabled)
return "nothing (Enabled: false)";
var live = Settings.Channels
.Where(c => c.Value is { Enabled: true } && !string.IsNullOrWhiteSpace(c.Value.WebhookUrl))
.Select(c => c.Key)
.ToList();
return live.Count > 0
? string.Join(", ", live)
: "nothing yet - add webhook URLs to Settings.json, then /discordrelay-reload";
}
///
/// Called by ModContainer.Disable() on shutdown, hot-reload, and /mod disable.
/// Every patch and background task added above must come off here, or the old
/// assembly cannot be collected and the next load will double-patch.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
// Unpatch first: no new chat can arrive while the relay drains what it has.
_harmony?.UnpatchAll(HarmonyId);
_harmony = null;
Relay?.Dispose();
Relay = null;
ModManager.Log($"[{Name}] shut down");
}
_disposed = true;
}
}