namespace Aeshnidae.OverworldCopies; /// /// Everything this mod asks of Aeshnidae.InstancesNoDat, which owns the copy machinery: /// making a copy, ticking it, moving a player in and out of it, closing it. A bubble is /// one of its copies, of an outdoor landblock; this mod adds only what an outdoor copy /// needs on top - the edge, and leaving housing out. /// /// Asked by reflection, the sibling-mod pattern AwakenedRewards' Instances uses: each /// mod lives in its own collectible load context, so a project reference would pin a /// second copy of InstancesNoDat's types and cast nothing. Host types (Landblock, /// Player) cross the boundary directly; InstancesNoDat's own (InstanceScope) are boxed. /// /// Two speeds. Refresh() checks the loaded assembly and re-binds when InstancesNoDat /// has been reloaded; it runs from the once-a-second housekeeping and before every /// command. The physics hook calls only AmbientFast(), a plain delegate call, because /// LScape.get_landblock runs thousands of times a second. After a reload of /// InstancesNoDat and before the next Refresh, that delegate reads the old assembly's /// thread-local - always null - so the hooks go quiet rather than wrong. /// public static class Bridge { private const string ModName = "Aeshnidae.InstancesNoDat"; private static readonly object _lock = new(); private static Assembly? _boundTo; private static string _status = "not looked up yet"; private static volatile Func? _ambient; // Context.Ambient private static Action? _setAmbient; // Context.Ambient (set) private static Func? _getOrCreate; // InstanceWorld.GetOrCreate private static Func? _placeIn; // InstanceWorld.PlaceIn private static Func? _copyOf; // InstanceWorld.CopyOf private static Func? _find; // InstanceWorld.Find private static Func? _close; // InstanceWorld.Close private static Func? _allocate; // InstanceWorld.AllocateCopy private static Func>? _playersIn; // InstanceWorld.PlayersIn private static Func? _master; // InstanceWorld.Master private static Func? _isInstanced; // Mod.IsInstanced private static MethodInfo? _scopeFor; // Mod.ScopeFor -> InstanceScope (boxed) private static MethodInfo? _assign; // Context.Assign(Player, ushort, int, InstanceScope) private static PropertyInfo? _settings; // Mod.Settings private static PropertyInfo? _maxCopies; // Settings.MaxCopiesPerLandblock private static PropertyInfo? _enabled; // Settings.Enabled private static PropertyInfo? _all; // InstanceWorld.All // Regions (InstancesNoDat 2026-09-25) - optional: an older InstancesNoDat has none, and regions are then simply unavailable. private static Func>? _regionLandblocks; // Mod.RegionLandblocks private static Func? _regionOf; // Mod.RegionOf private static Func, int, int>? _freeRegionCopy; // Regions.FreeCopy private static Func? _openRegion; // Regions.Open private static Func>? _regionPortals; // Mod.RegionPortals private static Func? _lastPortalOf; // Patches.LastPortalOf private static Func? _copyFor; // Context.CopyFor private static Action? _force; // Context.Force - optional (an admin following someone in) // Layers (InstancesNoDat 2026-09-28) - optional: an older InstancesNoDat has none, and every portal opens copy as before. private static Func? _regionCopyVia; // Mod.RegionCopyVia private static Func? _regionLayerName; // Mod.RegionLayerName private static Func? _regionLayerCopy; // Mod.RegionLayerCopy // Awakened worlds (InstancesNoDat 2026-09-28) - optional: a world's outdoor copies are its own, never bubbles. private static Func? _isWorldCopy; // Worlds.IsWorldCopy private static Func? _isEnclaveCopy; // Enclaves.IsEnclaveCopy (2026-09-29, a town) - optional public static string Status { get { lock (_lock) return _status; } } public static bool Bound { get { lock (_lock) return _boundTo is not null; } } /// The copy InstancesNoDat is ticking on this thread. For the hot path: no checks. public static Landblock? AmbientFast() { try { return _ambient?.Invoke(); } catch { return null; } } /// Binds to the InstancesNoDat assembly loaded now. False when there is none. public static bool Refresh() { try { var container = ModManager.GetModContainerByName(ModName, allowPartial: false); if (container is null) return Drop("InstancesNoDat is not installed"); if (container.Status != ModStatus.Active || container.ModAssembly is null) return Drop($"InstancesNoDat is {container.Status}"); lock (_lock) { if (ReferenceEquals(_boundTo, container.ModAssembly)) return true; } return Bind(container.ModAssembly); } catch (Exception ex) { return Drop($"{ex.GetType().Name}: {ex.Message}"); } } private static bool Bind(Assembly assembly) { var context = assembly.GetType($"{ModName}.Context", throwOnError: false); var world = assembly.GetType($"{ModName}.InstanceWorld", throwOnError: false); var mod = assembly.GetType($"{ModName}.Mod", throwOnError: false); if (context is null || world is null || mod is null) return Drop("a type is missing from InstancesNoDat - renamed?"); 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 ambient = AccessTools.PropertyGetter(context, "Ambient") ?? throw new MissingMethodException("Context", "Ambient"); var setAmbient = AccessTools.PropertySetter(context, "Ambient") ?? throw new MissingMethodException("Context", "Ambient (set)"); var scopeFor = AccessTools.Method(mod, "ScopeFor", [typeof(ushort)]) ?? throw new MissingMethodException("Mod", "ScopeFor"); var assign = AccessTools.GetDeclaredMethods(context).FirstOrDefault(m => m.Name == "Assign" && m.GetParameters().Length == 4) ?? throw new MissingMethodException("Context", "Assign"); var settings = AccessTools.Property(mod, "Settings") ?? throw new MissingMemberException("Mod", "Settings"); var built = ( Ambient: ambient.CreateDelegate>(), GetOrCreate: D>(world, "GetOrCreate", [typeof(ushort), typeof(int)]), PlaceIn: D>(world, "PlaceIn", [typeof(Player), typeof(ushort), typeof(int), typeof(string)]), CopyOf: D>(world, "CopyOf", [typeof(Landblock)]), Find: D>(world, "Find", [typeof(ushort), typeof(int)]), Close: D>(world, "Close", [typeof(ushort), typeof(int), typeof(bool)]), Allocate: D>(world, "AllocateCopy", [typeof(ushort), typeof(int)]), PlayersIn: D>>(world, "PlayersIn", [typeof(Landblock)]), Master: D>(world, "Master", [typeof(ushort)]), IsInstanced: D>(mod, "IsInstanced", [typeof(ushort)])); lock (_lock) { _ambient = built.Ambient; _setAmbient = setAmbient.CreateDelegate>(); _getOrCreate = built.GetOrCreate; _placeIn = built.PlaceIn; _copyOf = built.CopyOf; _find = built.Find; _close = built.Close; _allocate = built.Allocate; _playersIn = built.PlayersIn; _master = built.Master; _isInstanced = built.IsInstanced; _scopeFor = scopeFor; _assign = assign; _settings = settings; _maxCopies = AccessTools.Property(settings.PropertyType, "MaxCopiesPerLandblock"); _enabled = AccessTools.Property(settings.PropertyType, "Enabled"); _all = AccessTools.Property(world, "All"); var regions = assembly.GetType($"{ModName}.Regions", throwOnError: false); _regionLandblocks = AccessTools.Method(mod, "RegionLandblocks", [typeof(string)])?.CreateDelegate>>(); _regionOf = AccessTools.Method(mod, "RegionOf", [typeof(ushort)])?.CreateDelegate>(); _freeRegionCopy = regions is null ? null : AccessTools.Method(regions, "FreeCopy", [typeof(IReadOnlyCollection), typeof(int)])?.CreateDelegate, int, int>>(); _openRegion = regions is null ? null : AccessTools.Method(regions, "Open", [typeof(string), typeof(Player), typeof(int), typeof(ushort)])?.CreateDelegate>(); _regionPortals = AccessTools.Method(mod, "RegionPortals", [typeof(string)])?.CreateDelegate>>(); var patches = assembly.GetType($"{ModName}.Patches", throwOnError: false); _lastPortalOf = patches is null ? null : AccessTools.Method(patches, "LastPortalOf", [typeof(Player)])?.CreateDelegate>(); _copyFor = AccessTools.Method(context, "CopyFor", [typeof(Player), typeof(ushort)])?.CreateDelegate>(); _force = AccessTools.Method(context, "Force", [typeof(Player), typeof(ushort), typeof(int)])?.CreateDelegate>(); _regionCopyVia = AccessTools.Method(mod, "RegionCopyVia", [typeof(string), typeof(uint)])?.CreateDelegate>(); _regionLayerName = AccessTools.Method(mod, "RegionLayerName", [typeof(string), typeof(int)])?.CreateDelegate>(); _regionLayerCopy = AccessTools.Method(mod, "RegionLayerCopy", [typeof(string), typeof(string)])?.CreateDelegate>(); var worlds = assembly.GetType($"{ModName}.Worlds", throwOnError: false); _isWorldCopy = worlds is null ? null : AccessTools.Method(worlds, "IsWorldCopy", [typeof(int)])?.CreateDelegate>(); var enclaves = assembly.GetType($"{ModName}.Enclaves", throwOnError: false); _isEnclaveCopy = enclaves is null ? null : AccessTools.Method(enclaves, "IsEnclaveCopy", [typeof(int)])?.CreateDelegate>(); _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}) - renamed?"); } } private static bool Drop(string why) { bool was; lock (_lock) { was = _boundTo is not null; _boundTo = null; _ambient = null; _setAmbient = null; _getOrCreate = null; _placeIn = null; _copyOf = null; _find = null; _close = null; _allocate = null; _playersIn = null; _master = null; _isInstanced = null; _scopeFor = null; _regionLandblocks = null; _regionOf = null; _freeRegionCopy = null; _openRegion = null; _regionPortals = null; _lastPortalOf = null; _copyFor = null; _force = null; _regionCopyVia = null; _regionLayerName = null; _regionLayerCopy = null; _isWorldCopy = null; _isEnclaveCopy = null; _assign = null; _settings = null; _maxCopies = null; _enabled = null; _all = null; _status = why; } if (was) ModManager.Log($"[{Mod.Name}] lost {ModName}: {why}", ModManager.LogLevel.Warn); return false; } // ---- the calls --------------------------------------------------------------------------- /// /// Marks a copy as the one being worked on, on this thread - InstancesNoDat's own /// signal, which its physics routing and ours both read. Only ever set around a /// bounded piece of work and restored after it (Patches.PreInit / FinalInit). /// public static void SetAmbient(Landblock? block) => _setAmbient?.Invoke(block); public static Landblock? GetOrCreate(ushort landblock, int copy) => _getOrCreate?.Invoke(landblock, copy); public static bool PlaceIn(Player player, ushort landblock, int copy, string why) => _placeIn?.Invoke(player, landblock, copy, why) ?? false; public static int CopyOf(Landblock? block) => _copyOf?.Invoke(block) ?? 0; public static Landblock? Find(ushort landblock, int copy) => _find?.Invoke(landblock, copy); public static bool Close(ushort landblock, int copy, bool ejectPlayers) => _close?.Invoke(landblock, copy, ejectPlayers) ?? false; public static int AllocateCopy(ushort landblock, int max) => _allocate?.Invoke(landblock, max) ?? 0; public static IReadOnlyList PlayersIn(Landblock? block) => _playersIn?.Invoke(block) ?? Array.Empty(); public static Landblock? Master(ushort landblock) => _master?.Invoke(landblock); /// A landblock InstancesNoDat hands out copies of by itself - a configured dungeon. public static bool IsInstanced(ushort landblock) => _isInstanced?.Invoke(landblock) ?? false; /// InstancesNoDat's own switch. Its hooks do nothing while it is off, so neither can a bubble. public static bool InstancesEnabled() { try { var s = _settings?.GetValue(null); return s is not null && _enabled?.GetValue(s) is true; } catch { return false; } } public static int MaxCopies() { try { var s = _settings?.GetValue(null); return s is not null && _maxCopies?.GetValue(s) is int max && max > 0 ? max : 8; } catch { return 8; } } /// /// Drops a player's claim on a landblock's copy, so walking back onto that landblock /// lands in the real world rather than routing them back into the bubble. /// /// /// Whether copies of this landblock are group copies (InstancesNoDat's Outdoor Scope /// "Fellowship", or OutdoorScope): a fellowship shares one, with fellowship rules. /// False means solo (Personal) - dungeon rules. /// public static bool IsGroup(ushort landblock) { try { return string.Equals(_scopeFor?.Invoke(null, [landblock])?.ToString(), "Fellowship", StringComparison.Ordinal); } catch { return false; } } /// The landblocks of a region (InstancesNoDat Outdoor entry with Landblocks); empty when none, or unsupported. public static List RegionLandblocks(string name) { try { return _regionLandblocks?.Invoke(name) ?? new List(); } catch { return new List(); } } /// The region this landblock belongs to, or null. public static string? RegionOf(ushort landblock) { try { return _regionOf?.Invoke(landblock); } catch { return null; } } public static int FreeRegionCopy(IReadOnlyCollection landblocks) => _freeRegionCopy?.Invoke(landblocks, MaxCopies()) ?? 0; /// Opens a region copy - the 3x3 around `around` first, the rest as players move (Regions.Open). World thread. public static int OpenRegion(string name, Player owner, int copy, ushort around) => _openRegion?.Invoke(name, owner, copy, around) ?? 0; /// The portals that send a player into a region (its Outdoor entry's OnlyVia). public static List RegionPortals(string name) { try { return _regionPortals?.Invoke(name) ?? new List(); } catch { return new List(); } } /// /// The copy a portal into this region opens (Mod.RegionCopyVia): a layer's copy for one of /// its portals, 1 for a Shared region's own portal, 0 when the portal names no copy. /// public static int RegionCopyVia(string region, uint portal) { try { return _regionCopyVia?.Invoke(region, portal) ?? 0; } catch { return 0; } } /// Whether this copy number is an awakened world's or an enclave's (InstancesNoDat Worlds, Enclaves) - never a bubble of this mod's. public static bool IsWorldCopy(int copy) { try { return (_isWorldCopy?.Invoke(copy) ?? false) || (_isEnclaveCopy?.Invoke(copy) ?? false); } catch { return false; } } /// The name of the region's layer with this copy number ("pk"), or null. public static string? RegionLayerName(string region, int copy) { try { return _regionLayerName?.Invoke(region, copy); } catch { return null; } } /// The copy number of the region's named layer, or 0. public static int RegionLayerCopy(string region, string layer) { try { return _regionLayerCopy?.Invoke(region, layer) ?? 0; } catch { return 0; } } /// The portal this player stepped through in the last few seconds (InstancesNoDat's record), 0 for none. public static uint LastPortalOf(Player player) { try { return _lastPortalOf?.Invoke(player) ?? 0; } catch { return 0; } } /// The copy of this landblock the player has a claim on (Context.CopyFor), 0 for none. public static int CopyFor(Player player, ushort landblock) { try { return _copyFor?.Invoke(player, landblock) ?? 0; } catch { return 0; } } /// /// "Put this player in exactly this copy on their next arrival here" (Context.Force): /// beats InstancesNoDat's stay-put rule, which would otherwise keep someone already /// standing in another copy of the same landblock where they are. Expires by itself. /// public static void Force(Player player, ushort landblock, int copy) { try { _force?.Invoke(player, landblock, copy); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] could not point {player.Name} at {landblock:X4} copy {copy}: {ex.Message}", ModManager.LogLevel.Warn); } } /// Claims a landblock's copy for a player (Context.Assign), so routing, a login and a step onto it land there. public static void Assign(Player player, ushort landblock, int copy) { try { var scope = _scopeFor?.Invoke(null, [landblock]); if (scope is not null) _assign?.Invoke(null, [player, landblock, copy, scope]); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] could not claim {landblock:X4} copy {copy} for {player.Name}: {ex.Message}", ModManager.LogLevel.Warn); } } /// Whether copies of this landblock are Shared: one for the whole server, everyone who goes in joins it. public static bool IsShared(ushort landblock) { try { return string.Equals(_scopeFor?.Invoke(null, [landblock])?.ToString(), "Shared", StringComparison.Ordinal); } catch { return false; } } public static void Unassign(Player player, ushort landblock) { try { var scope = _scopeFor?.Invoke(null, [landblock]); if (scope is not null) _assign?.Invoke(null, [player, landblock, 0, scope]); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] could not drop {player.Name}'s claim on {landblock:X4}: {ex.Message}", ModManager.LogLevel.Warn); } } /// Every copy InstancesNoDat has open: ((landblock, copy), block). public static List<(ushort Landblock, int Copy, Landblock Block)> AllCopies() { var list = new List<(ushort, int, Landblock)>(); try { if (_all?.GetValue(null) is System.Collections.IEnumerable items) foreach (var item in items) { // ((ushort Landblock, int Copy) Key, Landblock Block), boxed var t = item.GetType(); var key = t.GetField("Item1")?.GetValue(item); var block = t.GetField("Item2")?.GetValue(item) as Landblock; if (key is null || block is null) continue; var kt = key.GetType(); if (kt.GetField("Item1")?.GetValue(key) is ushort lb && kt.GetField("Item2")?.GetValue(key) is int copy) list.Add((lb, copy, block)); } } catch { } return list; } }