namespace Aeshnidae.Expedition; /// /// What this mod asks Aeshnidae.InstancesNoDat, bound by name at runtime - the sibling /// pattern of Aeshnidae.Trials' Instances.cs and OverworldCopies' Bridge.cs: each mod has /// its own load context, so a project reference would never cast. The assembly is checked /// on every use and re-bound when it changed (a hot reload of InstancesNoDat). /// /// Lookups: which copy a Landblock is, the Landblock of a copy, who is in it, the region's /// landblocks. And one switch: Context.Ambient, the copy InstancesNoDat treats as "here" on /// this thread. Set to the island's Landblock around an EnterWorld, an object lands in the /// copy rather than the real Aerlinthe with the same landblock id - exactly how InstancesNoDat's /// own /inst-keep drop and OverworldCopies' teleports place things. /// public static class Island { 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 private static Func? _copyOf; // InstanceWorld.CopyOf private static Func? _isOurs; // InstanceWorld.IsOurs private static Func>? _playersIn; // InstanceWorld.PlayersIn private static Func>? _regionLandblocks; // Mod.RegionLandblocks private static Func? _getAmbient; // Context.Ambient (get) private static Action? _setAmbient; // Context.Ambient (set) public static string Status { get { lock (_lock) return _status; } } /// The island's landblocks (the region's), cached per binding; empty until bound. public static HashSet Landblocks { get; private set; } = new(); 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 world = assembly.GetType($"{ModName}.InstanceWorld", throwOnError: false); var mod = assembly.GetType($"{ModName}.Mod", throwOnError: false); var context = assembly.GetType($"{ModName}.Context", throwOnError: false); if (world is null || mod is null || context 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 find = D>(world, "Find", [typeof(ushort), typeof(int)]); var copyOf = D>(world, "CopyOf", [typeof(Landblock)]); var isOurs = D>(world, "IsOurs", [typeof(Landblock)]); var playersIn = D>>(world, "PlayersIn", [typeof(Landblock)]); var regionLandblocks = D>>(mod, "RegionLandblocks", [typeof(string)]); var getAmbient = (AccessTools.PropertyGetter(context, "Ambient") ?? throw new MissingMethodException("Context", "Ambient")).CreateDelegate>(); var setAmbient = (AccessTools.PropertySetter(context, "Ambient") ?? throw new MissingMethodException("Context", "Ambient (set)")).CreateDelegate>(); var landblocks = regionLandblocks(Mod.Settings.Region).ToHashSet(); lock (_lock) { _find = find; _copyOf = copyOf; _isOurs = isOurs; _playersIn = playersIn; _regionLandblocks = regionLandblocks; _getAmbient = getAmbient; _setAmbient = setAmbient; Landblocks = landblocks; _boundTo = assembly; _status = landblocks.Count > 0 ? $"bound; region '{Mod.Settings.Region}' has {landblocks.Count} landblocks" : $"bound, but InstancesNoDat has no region '{Mod.Settings.Region}'"; } ModManager.Log($"[{Mod.Name}] bound to {ModName} ({assembly.GetName().Version}): {Status}"); return true; } catch (Exception ex) { return Drop($"InstancesNoDat does not have what this mod needs ({ex.Message})"); } } private static bool Drop(string why) { bool was; lock (_lock) { was = _boundTo is not null; _boundTo = null; _find = null; _copyOf = null; _isOurs = null; _playersIn = null; _regionLandblocks = null; _getAmbient = null; _setAmbient = null; _status = why; } if (was) ModManager.Log($"[{Mod.Name}] lost {ModName}: {why}", ModManager.LogLevel.Warn); return false; } /// Re-reads the region's landblocks (after InstancesNoDat's settings were reloaded). public static void ReloadLandblocks() { if (!Refresh()) return; Func>? f; lock (_lock) f = _regionLandblocks; if (f is not null) Landblocks = f(Mod.Settings.Region).ToHashSet(); } // ------------------------------------------------------------ lookups /// /// Whether this Landblock is the quest's island: one of the region's landblocks, one of /// InstancesNoDat's copies, and the configured copy (1). The landblock-id test comes first /// and costs a set lookup, so it is safe on every kill and every spawn server-wide. /// public static bool IsIsland(Landblock? block) { if (block is null || !Landblocks.Contains((ushort)block.Id.Landblock)) return false; if (!Refresh()) return false; Func? ours; Func? copyOf; lock (_lock) { ours = _isOurs; copyOf = _copyOf; } return ours?.Invoke(block) == true && copyOf?.Invoke(block) == Mod.Settings.Copy; } /// The island copy's Landblock for this landblock id, or null (not open). public static Landblock? Find(ushort landblock) { if (!Refresh()) return null; Func? f; lock (_lock) f = _find; return f?.Invoke(landblock, Mod.Settings.Copy); } /// Every open Landblock of the island copy. public static List OpenBlocks() { var list = new List(); if (!Refresh()) return list; foreach (var lb in Landblocks) if (Find(lb) is { } block) list.Add(block); return list; } 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(); } /// Everyone standing on the island copy right now. public static List Players() => OpenBlocks().SelectMany(PlayersIn).Distinct().ToList(); /// Runs something with the island copy as the ambient (destroying its objects, say), as InstancesNoDat does in its copies. public static void WithAmbient(Landblock block, Action action) { Func? get; Action? set; lock (_lock) { get = _getAmbient; set = _setAmbient; } if (get is null || set is null) { action(); return; } var previous = get(); set(block); try { action(); } finally { set(previous); } } /// /// Puts an object into the island copy at its Location: EnterWorld with the copy as the /// ambient, so ACE's landblock lookup answers the copy. World thread (or the copy's own). /// public static bool EnterIntoCopy(WorldObject wo, Landblock block) { if (!Refresh()) return false; Func? get; Action? set; lock (_lock) { get = _getAmbient; set = _setAmbient; } if (get is null || set is null) return false; var previous = get(); set(block); try { return wo.EnterWorld(); } finally { set(previous); } } }