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;
}
}