using ACE.Entity.Models; using ACE.Server.WorldObjects.Managers; namespace Aeshnidae.Trials; /// /// The hooks on ACE itself. InstancesNoDat is reached through ; /// everything here is a stock ACE method, patched by attribute. /// /// Landblock.Init stamps every InstancesNoDat copy with its creation time (freshness) /// Creature.Die a kill in a copy with a run on /// Player.Die the runner died: void /// Player.Teleport into a Trial landblock: noted (a portal was used); out of the run's landblock: void /// Player.LogOut_Inner void /// WorldObject.Generator_Generate no respawns inside a run's copy once a generator has powered up /// Landblock.AddWorldObject a creature spawning after the count joins it /// WorldObject.Destroy a counted creature leaving without dying /// Player.PlayerEnterWorld a pending trophy is handed over /// LandblockManager.Tick the count, the watchdog, the weekly award check, the Keeper's resets /// EmoteManager.ExecuteEmote the Trial Keeper's reset (a Goto carrying Settings.ResetLabel) /// Hotspot.Activate floor hazards (acid) do no damage in a Trial copy (Settings.HarmlessHazards) /// /// Every body is wrapped: a hot-path patch must never throw into ACE. /// [HarmonyPatch] public static class Patches { [HarmonyPostfix] [HarmonyPatch(typeof(Landblock), nameof(Landblock.Init), new[] { typeof(bool) })] public static void PostLandblockInit(Landblock __instance, bool reload) { try { // A reload (inst-reload) re-runs Init on an open copy; that is not a new // copy, and must not make an old one look fresh. if (!reload && Mod.Settings.Enabled && Instances.IsOurs(__instance)) Runs.CopyCreated(__instance); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] Init stamp failed: {ex.Message}", ModManager.LogLevel.Warn); } } [HarmonyPostfix] [HarmonyPatch(typeof(Creature), "Die", new[] { typeof(DamageHistoryInfo), typeof(DamageHistoryInfo) })] public static void PostCreatureDie(Creature __instance) { try { if (Runs.Active > 0) Runs.OnKill(__instance); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] kill count failed: {ex.Message}", ModManager.LogLevel.Warn); } } [HarmonyPostfix] [HarmonyPatch(typeof(Player), "Die", new[] { typeof(DamageHistoryInfo), typeof(DamageHistoryInfo) })] public static void PostPlayerDie(Player __instance) { try { var run = Runs.ByPlayer(__instance); if (run is not null) Runs.Void(run, "you died"); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] death handling failed: {ex.Message}", ModManager.LogLevel.Warn); } } /// /// Every way into and out of a landblock that is not on foot. Into a Trial /// landblock: noted, so the arrival that follows knows a portal (or a recall, or /// an admin) sent it rather than a login. Out of the run's landblock: the run is /// over - a portal, a recall, a summon, /teleto, all the same. A teleport within /// the same landblock (a dungeon's internal portals, a copy change by staff) is /// left to the watchdog and the arrival handler. /// [HarmonyPrefix] [HarmonyPatch(typeof(Player), nameof(Player.Teleport), new[] { typeof(Position), typeof(bool) })] public static void PreTeleport(Player __instance, Position _newPosition) { try { Instances.Refresh(); // re-bind the arrival hook after an InstancesNoDat reload if (!Mod.Settings.Enabled || _newPosition is null) return; var landblock = (ushort)(_newPosition.LandblockId.Raw >> 16); var run = Runs.ByPlayer(__instance); if (run is not null && run.Landblock != landblock) { Runs.Void(run, "you left the dungeon"); return; } if (Mod.Trials.ContainsKey(landblock)) Runs.TeleportingInto(__instance, landblock); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] teleport handling failed: {ex.Message}", ModManager.LogLevel.Warn); } } [HarmonyPostfix] [HarmonyPatch(typeof(Player), nameof(Player.LogOut_Inner), new[] { typeof(bool) })] public static void PostLogOut(Player __instance) { try { var run = Runs.ByPlayer(__instance); if (run is not null) Runs.Void(run, "logged out"); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] logout handling failed: {ex.Message}", ModManager.LogLevel.Warn); } } /// /// A floor hazard's damage tick (acid). Inside a Trial copy it does nothing /// (Settings.HarmlessHazards): the hazard stays in place and looks the same, it /// just cannot wear the runner down while they clear what stands in it. /// [HarmonyPrefix] [HarmonyPatch(typeof(Hotspot), "Activate", new[] { typeof(Creature) })] public static bool PreHotspotActivate(Hotspot __instance) { try { if (!Mod.Settings.Enabled || !Mod.Settings.HarmlessHazards) return true; return !Runs.IsTrialCopy(__instance.CurrentLandblock); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] hazard check failed: {ex.Message}", ModManager.LogLevel.Warn); return true; } } /// /// A generator's regeneration tick. The first call after StartGenerator is the /// power-up (CurrentlyPoweringUp): the initial spawn, which must happen - it is /// the dungeon. Every later call would respawn what has been killed, and inside a /// copy with a run on it is skipped, so "everything dead" can be reached. Nothing /// is changed on the generator: the moment the run ends its next tick respawns as /// normal, and the copy carries on as a copy. /// [HarmonyPrefix] [HarmonyPatch(typeof(WorldObject), nameof(WorldObject.Generator_Generate))] public static bool PreGeneratorGenerate(WorldObject __instance) { try { if (Runs.Active == 0 || __instance.CurrentlyPoweringUp) return true; return !Runs.HoldsRegeneration(__instance.CurrentLandblock); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] generator check failed: {ex.Message}", ModManager.LogLevel.Warn); return true; } } [HarmonyPostfix] [HarmonyPatch(typeof(Landblock), nameof(Landblock.AddWorldObject), new[] { typeof(WorldObject) })] public static void PostAddWorldObject(Landblock __instance, WorldObject wo, bool __result) { try { if (__result && Runs.Active > 0) Runs.OnAdded(__instance, wo); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] late spawn count failed: {ex.Message}", ModManager.LogLevel.Warn); } } [HarmonyPrefix] [HarmonyPatch(typeof(WorldObject), nameof(WorldObject.Destroy), new[] { typeof(bool), typeof(bool) })] public static void PreDestroy(WorldObject __instance) { try { if (Runs.Active > 0 && __instance is Creature and not Player) Runs.OnGone(__instance); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] destroy handling failed: {ex.Message}", ModManager.LogLevel.Warn); } } /// A trophy won while offline is handed over a few seconds after login, once the client is drawing. [HarmonyPostfix] [HarmonyPatch(typeof(Player), nameof(Player.PlayerEnterWorld))] public static void PostEnterWorld(Player __instance) { try { if (!Mod.Settings.Enabled || !TrialDb.Ready) return; var chain = new ActionChain(); chain.AddDelaySeconds(8); chain.AddAction(__instance, () => Awards.ClaimPending(__instance)); chain.EnqueueChain(); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] login handling failed: {ex.Message}", ModManager.LogLevel.Warn); } } /// How many world ticks this mod has seen since enable - /trial-status shows it, as proof the hook is live. public static long Ticks; [HarmonyPostfix] [HarmonyPatch(typeof(LandblockManager), nameof(LandblockManager.Tick))] public static void PostTick() { try { Ticks++; Runs.ProcessResets(); Runs.CloseSpentCopies(); Runs.Tick(); Awards.MaybeAward(); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] tick failed: {ex.Message}", ModManager.LogLevel.Error); } } /// /// The Trial Keeper's reset. Scoped as tightly as the Enlightenment gate: a Goto action, /// carrying the configured label, aimed at a player; every other emote falls through on /// the first comparison. The mod owns the outcome, so the original (a Goto to an emote set /// that does not exist) never runs. /// [HarmonyPrefix] [HarmonyPatch(typeof(EmoteManager), nameof(EmoteManager.ExecuteEmote))] public static bool PreExecuteEmote(EmoteManager __instance, PropertiesEmoteAction emote, WorldObject targetObject, ref float __result) { if (emote is null || emote.Type != (uint)EmoteType.Goto) return true; if (!Mod.Settings.Enabled || !string.Equals(emote.Message, Mod.Settings.ResetLabel, StringComparison.OrdinalIgnoreCase)) return true; if (targetObject is not Player player) return true; __result = 0f; try { Runs.RequestReset(player, __instance.WorldObject); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] reset request failed for {player.Name}: {ex.Message}", ModManager.LogLevel.Error); } return false; } }