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