namespace Aeshnidae.Enlightenment; /// /// Three patches, each replacing one decision ACE makes about enlightenment. /// /// The interesting one is the emote gate. Retail's requirements are not in C# at /// all - they are a Goto/InqIntStat state machine on the Font of Enlightenment /// (wcid 53412), whose numbers live in two world-database rows. Left alone it /// enforces "exactly level 275" and "enlightenment 0 to 4", so on this server it /// would lock out anyone past 275 (Aeshnidae.MaxLevel raises the cap to 500) and /// still cap enlightenment at five however Settings.json is configured. /// /// The alternative was a SQL migration, and a fresh one every time a number moved. /// Instead the chain is cut at its first instruction: the Goto that starts it is /// intercepted, the mod answers the whole question, and on success it jumps /// straight to the confirmation prompt the chain would have reached anyway. The /// world database is untouched, so a world reimport cannot silently revert the /// rules, and disabling the mod restores retail behaviour exactly. /// [HarmonyPatch] public static class Patches { /// /// Cuts in ahead of the Font's requirement chain. /// /// Scoped as tightly as it can be: a Goto action, carrying the configured /// label, aimed at a player. Every other emote on every other creature in the /// world falls through on the first comparison. /// [HarmonyPrefix] [HarmonyPatch(typeof(EmoteManager), nameof(EmoteManager.ExecuteEmote))] public static bool PreExecuteEmote(EmoteManager __instance, PropertiesEmoteAction emote, WorldObject targetObject, ref float __result) { if (!Mod.Settings.Enabled || emote is null || emote.Type != (uint)EmoteType.Goto) return true; if (!string.Equals(emote.Message, Mod.Settings.GateLabel, StringComparison.OrdinalIgnoreCase)) return true; if (targetObject is not Player player) return true; // From here the mod owns the outcome, so the original never runs. __result // is the emote's delay in seconds; nothing below takes time. __result = 0f; try { if (Requirements.Check(player, out var failures)) { __instance.ExecuteEmoteSet(EmoteCategory.GotoSet, Mod.Settings.ConfirmLabel, targetObject, true); return false; } foreach (var failure in failures) player.Session?.Network.EnqueueSend(new GameMessageSystemChat(failure, ChatMessageType.Broadcast)); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] enlightenment gate failed for {player.Name}: {ex}", ModManager.LogLevel.Error); player.Session?.Network.EnqueueSend(new GameMessageSystemChat( "The Font's judgement is clouded. Please try again shortly.", ChatMessageType.Broadcast)); } return false; } /// /// The payload. EmoteType.Enlightenment (9001) is the only thing in ACE that /// calls this, and the Font's chain is the only thing that raises it, so this /// is the whole of the grant path. /// [HarmonyPrefix] [HarmonyPatch(typeof(AceEnlightenment), nameof(AceEnlightenment.HandleEnlightenment))] public static bool PreHandleEnlightenment(WorldObject npc, Player player) { if (!Mod.Settings.Enabled) return true; Ritual.Perform(npc, player); return false; } /// /// Anything else that asks ACE whether a player is eligible gets the mod's /// answer. Nothing in stock ACE calls this except HandleEnlightenment, which /// no longer runs - it is patched so that a future caller, or another mod, /// cannot end up consulting the retail rules behind this one's back. /// [HarmonyPrefix] [HarmonyPatch(typeof(AceEnlightenment), nameof(AceEnlightenment.VerifyRequirements))] public static bool PreVerifyRequirements(Player player, ref bool __result) { if (!Mod.Settings.Enabled) return true; __result = Requirements.Check(player, out var failures); if (!__result) { foreach (var failure in failures) player?.Session?.Network.EnqueueSend(new GameMessageSystemChat(failure, ChatMessageType.Broadcast)); } return false; } /// Drop a departing character's re-entry state so the tables stay small. [HarmonyPostfix] [HarmonyPatch(typeof(Player), nameof(Player.LogOut_Inner))] public static void PostLogOut(Player __instance) { if (__instance is not null) Guard.Forget(__instance); } }