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