namespace Aeshnidae.Towns;
///
/// The hooks on ACE, each wrapped - a hot path must never throw into ACE.
///
/// GeneratorProfile.ProcessQueue a town copy spawns no monsters (Towns.HoldMonsters)
/// Player.PlayerEnterWorld a login inside a town copy goes back into it
/// LandblockManager.AddObject (Towns.PrepareLogin, from both: the one thing an opt-in copy
/// would otherwise lose). The first runs a line before the
/// placing; the second is Priority.First so the aim is set
/// before InstancesNoDat's own hook on that method asks where
/// this player belongs. Neither ever skips the original.
/// Player.LogOut_Inner the record made true while they still stand somewhere
/// Player.Teleport (finalizer) a /town go claim let go of the moment it has been used
/// LandblockManager.Tick keeps the InstancesNoDat binding (and so every town's
/// registration) current after a reload of that mod, and
/// sweeps the records and claims (Towns.Sweep)
///
[HarmonyPatch]
public static class Patches
{
[HarmonyPrefix]
[HarmonyPatch(typeof(GeneratorProfile), nameof(GeneratorProfile.ProcessQueue))]
public static void PreProcessQueue(GeneratorProfile __instance)
{
try
{
if (Mod.Settings.Enabled)
Towns.HoldMonsters(__instance);
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] town spawn check failed: {ex.Message}", ModManager.LogLevel.Warn);
}
}
///
/// A character entering the world, one step before they are placed: ACE calls this and then
/// LandblockManager.AddObject on the very next line. A character who was standing in their
/// allegiance's town copy when they left is aimed back at it here, so that the aim is already
/// set whichever prefix on AddObject runs first - the hook below does the same job and would
/// be enough on its own only if Harmony ordered this mod's prefix before InstancesNoDat's,
/// which is true but is not a thing to rest a player's town on. Idempotent: both set the same
/// aim from the same record.
///
[HarmonyPostfix]
[HarmonyPatch(typeof(Player), nameof(Player.PlayerEnterWorld))]
public static void PostPlayerEnterWorld(Player __instance)
{
try
{
if (!Mod.Settings.Enabled || __instance.Location is null)
return;
var landblock = (ushort)(__instance.Location.LandblockId.Raw >> 16);
if (Towns.MenuLandblocks.Contains(landblock))
Towns.PrepareLogin(__instance, landblock);
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] login routing failed: {ex}", ModManager.LogLevel.Error);
}
}
///
/// Placing an object into the world: for a player, a login. The same aim as above, in the
/// place InstancesNoDat sets its own (its prefix on this method asks Context.Resolve, which
/// reads the aim), for any way into the world that does not come through PlayerEnterWorld.
/// Anyone with nothing recorded is left entirely alone, which is how a portal, a recall and a
/// walk into a town now reach the real town.
///
[HarmonyPrefix]
[HarmonyPriority(Priority.First)]
[HarmonyPatch(typeof(LandblockManager), nameof(LandblockManager.AddObject))]
public static void PreAddObject(WorldObject worldObject, bool loadAdjacents)
{
try
{
if (!Mod.Settings.Enabled || worldObject is not Player player || player.Location is null)
return;
var landblock = (ushort)(player.Location.LandblockId.Raw >> 16);
if (!Towns.MenuLandblocks.Contains(landblock))
return;
Towns.PrepareLogin(player, landblock);
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] login routing failed: {ex}", ModManager.LogLevel.Error);
}
}
///
/// Leaving the world. Both logout paths funnel through LogOut_Inner (the plain one and the
/// delayed player-killer one), and the character is still standing in their landblock here,
/// so this is the last and most reliable moment to write down whether they were inside.
///
[HarmonyPrefix]
[HarmonyPatch(typeof(Player), nameof(Player.LogOut_Inner))]
public static void PreLogOut(Player __instance)
{
try
{
if (Mod.Settings.Enabled)
Towns.NoteLogOut(__instance);
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] logout note failed: {ex.Message}", ModManager.LogLevel.Warn);
}
}
///
/// Any teleport is the end of a `/town go`: the claim it placed is dropped here, whether the
/// teleport was that one or something else entirely, and whether it threw or not. Nothing is
/// left behind that could route the player's next portal into the copy.
///
[HarmonyFinalizer]
[HarmonyPatch(typeof(Player), nameof(Player.Teleport), new[] { typeof(Position), typeof(bool) })]
public static void PostTeleport(Player __instance)
{
try
{
Towns.FinishEntry(__instance);
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] could not release a town claim: {ex.Message}", ModManager.LogLevel.Warn);
}
}
private static DateTime _lastCheck = DateTime.MinValue;
private static DateTime _lastSweep = DateTime.MinValue;
///
/// Every few seconds on the world tick: re-bind if InstancesNoDat was reloaded, which also
/// registers every town again - its enclave registry lives in its own memory and a reload
/// empties it. (A login into a town must find it registered; a teleport hook alone would not.)
/// And every two seconds, Towns.Sweep keeps each character's record of where they are true.
///
[HarmonyPostfix]
[HarmonyPatch(typeof(LandblockManager), nameof(LandblockManager.Tick))]
public static void PostTick()
{
try
{
if (!Mod.Settings.Enabled)
return;
var now = DateTime.UtcNow;
if (now - _lastCheck >= TimeSpan.FromSeconds(3))
{
_lastCheck = now;
Instances.Refresh();
}
if (now - _lastSweep >= TimeSpan.FromSeconds(2))
{
_lastSweep = now;
Towns.Sweep();
}
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] binding check failed: {ex.Message}", ModManager.LogLevel.Warn);
}
}
}