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