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