namespace Aeshnidae.Towns; /// /// What this mod asks Aeshnidae.InstancesNoDat, bound by name at runtime (the sibling pattern of /// Trials' and Expedition's bindings: each mod has its own load context, so a project reference /// would never cast). The assembly is checked on every use and re-bound after a reload of /// InstancesNoDat - and every town is registered again then, since the enclave registry lives in /// InstancesNoDat's memory. /// /// Enclaves.Register / Unregister a town's landblocks as a copy, and as one region /// Enclaves.Of the registration, for the diagnostics (its ToString) /// InstanceWorld.CopyOf / IsOurs whether a Landblock is a town's copy /// Context.CopyFor the copy a player is routed to for a landblock, 0 for none /// Context.Assign / Mod.ScopeFor a claim on a copy - held only while /town go teleports /// Context.Force "this teleport goes to this copy" (0 = the real landblock) /// Arrival.OnArrival the moment a player steps into a copy: the welcome line /// /// **A town copy is opt-in (2026-10-09).** Enclaves route a landblock to its owner's copy by /// the OWNER KEY the enclave is registered under (a monarch's guid): every member arriving any /// way at all was put in the copy. Tom asked for the opposite - the real town for everyone, /// however they arrive, and the copy only for a member who asks with `/town go`. So a town is /// registered with **owner key 0**, which no player can ever match (Enclaves.CopyFor needs a /// non-zero key, and a player's key is their monarch's guid): the registration still names the /// landblocks, opens them as one region and marks the copy shared and kept, but it routes /// nobody. This mod decides who is inside, and it is the only thing that does - see Towns.cs /// (Go, LoginCopyFor, Sweep). Nothing in InstancesNoDat was changed for it. /// public static class Instances { private const string ModName = "Aeshnidae.InstancesNoDat"; /// /// The owner key every town is registered under: 0, so no player is ever routed into a town /// copy automatically. See the note above - this one number is what makes a town opt-in. /// public const uint NoAutomaticOwner = 0; private static readonly object _lock = new(); private static Assembly? _boundTo; private static string _status = "not looked up yet"; private static Func, bool>? _register; private static Func? _unregister; private static Func? _copyOf; private static Func? _isOurs; private static Func? _getOrCreate; // InstanceWorld.GetOrCreate - for town-admin open private static Func? _copyForPlayer; // Context.CopyFor private static Action? _force; // Context.Force private static MethodInfo? _assign; // Context.Assign(Player, ushort, int, InstanceScope) private static MethodInfo? _scopeFor; // Mod.ScopeFor(ushort) -> InstanceScope private static MethodInfo? _enclaveOf; // Enclaves.Of(int) -> Enclave? private static FieldInfo? _onArrival; private static readonly Action ArrivalHandler = Towns.OnArrival; public static string Status { get { lock (_lock) return _status; } } /// Makes sure the binding matches the InstancesNoDat loaded now; on a new binding, every town is registered again. public static bool Refresh() { try { var container = ModManager.GetModContainerByName(ModName, allowPartial: false); if (container is null || container.Status != ModStatus.Active || container.ModAssembly is null) return Drop(container is null ? "InstancesNoDat is not installed" : $"InstancesNoDat is {container.Status}"); lock (_lock) { if (ReferenceEquals(_boundTo, container.ModAssembly)) return true; } if (!Bind(container.ModAssembly)) return false; Towns.RegisterAll(); return true; } catch (Exception ex) { return Drop($"{ex.GetType().Name}: {ex.Message}"); } } private static bool Bind(Assembly assembly) { var enclaves = assembly.GetType($"{ModName}.Enclaves", throwOnError: false); var world = assembly.GetType($"{ModName}.InstanceWorld", throwOnError: false); var arrival = assembly.GetType($"{ModName}.Arrival", throwOnError: false); var context = assembly.GetType($"{ModName}.Context", throwOnError: false); var instancesMod = assembly.GetType($"{ModName}.Mod", throwOnError: false); if (enclaves is null || world is null || context is null || instancesMod is null) return Drop("InstancesNoDat has no enclaves (older than 2026-09-29?)"); try { T D(Type type, string name, Type[] args) where T : Delegate => (AccessTools.Method(type, name, args) ?? throw new MissingMethodException(type.Name, name)).CreateDelegate(); var register = D, bool>>(enclaves, "Register", [typeof(string), typeof(int), typeof(uint), typeof(List)]); var unregister = D>(enclaves, "Unregister", [typeof(int)]); var copyOf = D>(world, "CopyOf", [typeof(Landblock)]); var isOurs = D>(world, "IsOurs", [typeof(Landblock)]); var getOrCreate = D>(world, "GetOrCreate", [typeof(ushort), typeof(int)]); var copyForPlayer = D>(context, "CopyFor", [typeof(Player), typeof(ushort)]); var force = D>(context, "Force", [typeof(Player), typeof(ushort), typeof(int)]); // Assign takes InstancesNoDat's own InstanceScope enum and ScopeFor returns it, so // these two cross the mod boundary as MethodInfo and the value is passed straight // from one to the other without this assembly ever naming the type. var assign = AccessTools.Method(context, "Assign") ?? throw new MissingMethodException("Context", "Assign"); var scopeFor = AccessTools.Method(instancesMod, "ScopeFor", [typeof(ushort)]) ?? throw new MissingMethodException("Mod", "ScopeFor"); var enclaveOf = AccessTools.Method(enclaves, "Of", [typeof(int)]) ?? throw new MissingMethodException("Enclaves", "Of"); lock (_lock) { Unhook(); _register = register; _unregister = unregister; _copyOf = copyOf; _isOurs = isOurs; _getOrCreate = getOrCreate; _copyForPlayer = copyForPlayer; _force = force; _assign = assign; _scopeFor = scopeFor; _enclaveOf = enclaveOf; _onArrival = arrival is null ? null : AccessTools.Field(arrival, "OnArrival"); if (_onArrival is not null) { var current = _onArrival.GetValue(null) as Delegate; _onArrival.SetValue(null, Delegate.Combine(Delegate.Remove(current, ArrivalHandler), ArrivalHandler)); } _boundTo = assembly; _status = "bound"; } ModManager.Log($"[{Mod.Name}] bound to {ModName} ({assembly.GetName().Version})"); return true; } catch (Exception ex) { return Drop($"InstancesNoDat does not have what this mod needs ({ex.Message})"); } } private static void Unhook() { try { if (_onArrival?.GetValue(null) is Delegate current) _onArrival.SetValue(null, Delegate.Remove(current, ArrivalHandler)); } catch { /* the assembly may be gone */ } _onArrival = null; } private static bool Drop(string why) { lock (_lock) { var was = _boundTo is not null; Unhook(); _boundTo = null; _register = null; _unregister = null; _copyOf = null; _isOurs = null; _getOrCreate = null; _copyForPlayer = null; _force = null; _assign = null; _scopeFor = null; _enclaveOf = null; _status = why; if (was) ModManager.Log($"[{Mod.Name}] lost {ModName}: {why}", ModManager.LogLevel.Warn); } return false; } /// Takes the arrival handler off InstancesNoDat (this mod unloading). public static void Unbind() => Drop("unbound"); public static bool Register(string name, int copy, uint owner, List landblocks) { Func, bool>? f; lock (_lock) f = _register; return f?.Invoke(name, copy, owner, landblocks) ?? false; } public static bool Unregister(int copy) { if (!Refresh()) return false; Func? f; lock (_lock) f = _unregister; return f?.Invoke(copy) ?? false; } /// Opens a copy (staff test: town-admin open). World thread. public static Landblock? GetOrCreate(ushort landblock, int copy) { if (!Refresh()) return null; Func? f; lock (_lock) f = _getOrCreate; return f?.Invoke(landblock, copy); } public static int CopyOf(Landblock? block) { if (block is null || !Refresh()) return 0; Func? ours; Func? copyOf; lock (_lock) { ours = _isOurs; copyOf = _copyOf; } return ours?.Invoke(block) == true ? copyOf?.Invoke(block) ?? 0 : 0; } /// Whether a town's enclave is registered with InstancesNoDat right now. public static bool IsRegistered(int copy) { MethodInfo? of; lock (_lock) of = _enclaveOf; try { return of?.Invoke(null, [copy]) is not null; } catch { return false; } } /// The enclave's own description (InstancesNoDat's ToString), for town-admin. Null if it is not registered. public static string? Describe(int copy) { MethodInfo? of; lock (_lock) of = _enclaveOf; try { return of?.Invoke(null, [copy])?.ToString(); } catch { return null; } } /// The copy a player is routed to for a landblock (Context.CopyFor), 0 for the real one. public static int RouteFor(Player player, ushort landblock) { Func? f; lock (_lock) f = _copyForPlayer; try { return f?.Invoke(player, landblock) ?? 0; } catch { return 0; } } /// "This player's next teleport to this landblock goes to this copy" - 0 for the real landblock. Self-expiring. public static void Force(Player player, ushort landblock, int copy) { Action? f; lock (_lock) f = _force; try { f?.Invoke(player, landblock, copy); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] could not aim {player.Name} at copy {copy} of {landblock:X4}: {ex.Message}", ModManager.LogLevel.Warn); } } /// /// A claim on a copy of a landblock (Context.Assign; 0 removes it). Towns holds one only /// while a `/town go` teleport is in flight - InstancesNoDat's teleport hook reads it to open /// the copy and point the physics at it - and drops it the moment the teleport is done /// (Towns.FinishEntry). A claim that outlived that would route the player's next portal or /// recall into the copy, which is the behaviour this change exists to remove. /// public static bool Claim(Player player, ushort landblock, int copy) { MethodInfo? assign; MethodInfo? scopeFor; lock (_lock) { assign = _assign; scopeFor = _scopeFor; } if (assign is null || scopeFor is null) return false; try { var scope = scopeFor.Invoke(null, [landblock]); assign.Invoke(null, [player, landblock, copy, scope]); return true; } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] could not {(copy > 0 ? "claim" : "release")} copy {copy} of {landblock:X4} for {player.Name}: {ex.Message}", ModManager.LogLevel.Warn); return false; } } }