namespace Aeshnidae.Trials; /// /// Everything this mod asks Aeshnidae.InstancesNoDat, bound by name at runtime. /// /// Reflection rather than a reference: each mod lives in its own collectible load /// context, so a project reference would pin a second copy of InstancesNoDat's types /// and nothing would cast (the sibling pattern of Aeshnidae.AwakenedRewards, /// Aeshnidae.Leaderboard and Aeshnidae.AdminAudit, with the same caveats: a rename /// there silently drops the binding, and /mod find, which reloads every mod, changes /// the assembly under us - hence the assembly is checked on every use and re-bound /// when it differs). The ACE types crossing the boundary - Landblock, Player - are the /// host's and shared, so delegates are built over them directly. /// /// The one hook: Arrival.OnArrival, InstancesNoDat's "a player has crossed /// into a copy" moment (Patches.PreRelocate -> Arrival.Announce), fired after its own /// tier line. Our handler combines onto that static delegate field. The rest are /// lookups: which copy a Landblock is, the Landblock of a copy, who is in it, its /// tier, and its name. /// public static class Instances { 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 Func? _find; // InstanceWorld.Find(ushort, int) private static Func? _copyOf; // InstanceWorld.CopyOf(Landblock) private static Func? _isOurs; // InstanceWorld.IsOurs(Landblock) private static Func>? _playersIn; // InstanceWorld.PlayersIn(Landblock) private static Func? _counted; // Scaling.Counted(ushort, int) private static Func? _nameFor; // Mod.NameFor(ushort) private static MethodInfo? _scopeFor; // Mod.ScopeFor(ushort) -> InstanceScope (boxed) private static FieldInfo? _onArrival; // Arrival.OnArrival (static Action?) private static Func? _copyFor; // Context.CopyFor(Player, ushort) private static Func? _close; // InstanceWorld.Close(ushort, int, bool) private static Func? _isCourseCopy; // Context.IsCourseCopy(ushort, int) - optional, newer InstancesNoDat private static Func? _courseCopyFor; // Context.CourseCopyFor(Player, ushort) - optional /// Our handler, kept so it can be taken off the same field on unbind. private static readonly Action ArrivalHandler = Runs.OnArrival; public static string Status { get { lock (_lock) return _status; } } public static bool Bound { get { lock (_lock) return _boundTo is not null; } } public static void Unbind() { lock (_lock) { _boundTo = null; _find = null; _copyOf = null; _isOurs = null; _playersIn = null; _counted = null; _nameFor = null; _scopeFor = null; _copyFor = null; _close = null; _isCourseCopy = null; _courseCopyFor = null; Unhook(); _status = "unbound"; } } private static void Unhook() { try { if (_onArrival is null) return; var current = _onArrival.GetValue(null) as Delegate; _onArrival.SetValue(null, Delegate.Remove(current, ArrivalHandler)); } catch { /* the assembly may already be gone */ } finally { _onArrival = null; } } /// Makes sure the binding matches the InstancesNoDat assembly loaded right now. public static bool Refresh() { try { var container = ModManager.GetModContainerByName(ModName, allowPartial: false); if (container is null) { Drop("mod not installed"); return false; } if (container.Status != ModStatus.Active || container.ModAssembly is null) { Drop($"mod is {container.Status}"); return false; } lock (_lock) { if (ReferenceEquals(_boundTo, container.ModAssembly)) return true; } return Bind(container.ModAssembly); } catch (Exception ex) { Drop($"{ex.GetType().Name}: {ex.Message}"); return false; } } private static bool Bind(Assembly assembly) { var world = assembly.GetType($"{ModName}.InstanceWorld", throwOnError: false); var scaling = assembly.GetType($"{ModName}.Scaling", throwOnError: false); var mod = assembly.GetType($"{ModName}.Mod", throwOnError: false); var arrival = assembly.GetType($"{ModName}.Arrival", throwOnError: false); var context = assembly.GetType($"{ModName}.Context", throwOnError: false); if (world is null || scaling is null || mod is null || arrival is null || context is null) { Drop("a type is missing from that mod's assembly - renamed upstream?"); return false; } var find = AccessTools.Method(world, "Find", new[] { typeof(ushort), typeof(int) }); var copyOf = AccessTools.Method(world, "CopyOf", new[] { typeof(Landblock) }); var isOurs = AccessTools.Method(world, "IsOurs", new[] { typeof(Landblock) }); var playersIn = AccessTools.Method(world, "PlayersIn", new[] { typeof(Landblock) }); var counted = AccessTools.Method(scaling, "Counted", new[] { typeof(ushort), typeof(int) }); var nameFor = AccessTools.Method(mod, "NameFor", new[] { typeof(ushort) }); var scopeFor = AccessTools.Method(mod, "ScopeFor", new[] { typeof(ushort) }); var onArrival = AccessTools.Field(arrival, "OnArrival"); var copyFor = AccessTools.Method(context, "CopyFor", new[] { typeof(Player), typeof(ushort) }); var close = AccessTools.Method(world, "Close", new[] { typeof(ushort), typeof(int), typeof(bool) }); // Optional: an InstancesNoDat from before course copies (2026-09-25) has neither, and // then a Course trial is simply never timed rather than timing somebody's farm. var isCourseCopy = AccessTools.Method(context, "IsCourseCopy", new[] { typeof(ushort), typeof(int) }); var courseCopyFor = AccessTools.Method(context, "CourseCopyFor", new[] { typeof(Player), typeof(ushort) }); if (find is null || copyOf is null || isOurs is null || playersIn is null || counted is null || nameFor is null || scopeFor is null || onArrival is null || copyFor is null || close is null) { Drop("a member is missing from that mod - renamed upstream?"); return false; } try { lock (_lock) { _find = find.CreateDelegate>(); _copyOf = copyOf.CreateDelegate>(); _isOurs = isOurs.CreateDelegate>(); _playersIn = playersIn.CreateDelegate>>(); _counted = counted.CreateDelegate>(); _nameFor = nameFor.CreateDelegate>(); _scopeFor = scopeFor; _onArrival = onArrival; _copyFor = copyFor.CreateDelegate>(); _close = close.CreateDelegate>(); _isCourseCopy = isCourseCopy?.CreateDelegate>(); _courseCopyFor = courseCopyFor?.CreateDelegate>(); var current = _onArrival.GetValue(null) as Delegate; _onArrival.SetValue(null, Delegate.Combine(Delegate.Remove(current, ArrivalHandler), ArrivalHandler)); _boundTo = assembly; _status = _isCourseCopy is not null && _courseCopyFor is not null ? "bound" : "bound (no course copies in that InstancesNoDat - Course trials are not timed)"; } ModManager.Log($"[{Mod.Name}] bound to {ModName} ({assembly.GetName().Version})"); return true; } catch (Exception ex) { Drop($"{ex.GetType().Name}: {ex.Message}"); return false; } } private static void Drop(string why) { bool was; lock (_lock) { was = _boundTo is not null; _status = why; } if (was) { Unbind(); lock (_lock) _status = why; ModManager.Log($"[{Mod.Name}] lost {ModName}: {why}", ModManager.LogLevel.Warn); } } // ------------------------------------------------------------ the lookups public static Landblock? Find(ushort landblock, int copy) { if (!Refresh()) return null; Func? f; lock (_lock) f = _find; return f?.Invoke(landblock, copy); } public static int CopyOf(Landblock? block) { if (block is null || !Refresh()) return 0; Func? f; lock (_lock) f = _copyOf; return f?.Invoke(block) ?? 0; } public static bool IsOurs(Landblock? block) { if (block is null || !Refresh()) return false; Func? f; lock (_lock) f = _isOurs; return f?.Invoke(block) ?? false; } public static IReadOnlyList PlayersIn(Landblock? block) { if (block is null || !Refresh()) return Array.Empty(); Func>? f; lock (_lock) f = _playersIn; return f?.Invoke(block) ?? Array.Empty(); } /// The tier a copy was opened at (InstancesNoDat's Scaling.Counted), 0 when unknown. public static int TierOf(ushort landblock, int copy) { if (!Refresh()) return 0; Func? f; lock (_lock) f = _counted; return Math.Max(0, f?.Invoke(landblock, copy) ?? 0); } public static string NameFor(ushort landblock) { if (!Refresh()) return $"{landblock:X4}"; Func? f; lock (_lock) f = _nameFor; return f?.Invoke(landblock) ?? $"{landblock:X4}"; } /// "Personal", "Fellowship", "Allegiance", "Shared" - or "" when the landblock is not instanced there. public static string ScopeOf(ushort landblock) { if (!Refresh()) return ""; MethodInfo? m; lock (_lock) m = _scopeFor; try { return m?.Invoke(null, new object[] { landblock })?.ToString() ?? ""; } catch { return ""; } } /// The copy of a landblock this player's group owns (Context.CopyFor), 0 for none. public static int CopyFor(Player player, ushort landblock) { if (!Refresh()) return 0; Func? f; lock (_lock) f = _copyFor; return f?.Invoke(player, landblock) ?? 0; } /// Whether a copy was opened through one of its landblock's course portals (Context.IsCourseCopy); false when unknown. public static bool IsCourseCopy(ushort landblock, int copy) { if (copy <= 0 || !Refresh()) return false; Func? f; lock (_lock) f = _isCourseCopy; return f?.Invoke(landblock, copy) ?? false; } /// The player's course copy of a landblock (Context.CourseCopyFor), 0 for none or unknown. public static int CourseCopyFor(Player player, ushort landblock) { if (!Refresh()) return 0; Func? f; lock (_lock) f = _courseCopyFor; return f?.Invoke(player, landblock) ?? 0; } /// Closes a copy now (InstanceWorld.Close). World thread only: the Keeper's reset /// is queued to the world tick for exactly this reason. public static bool Close(ushort landblock, int copy) { if (copy <= 0 || !Refresh()) return false; Func? f; lock (_lock) f = _close; return f?.Invoke(landblock, copy, true) ?? false; } }