namespace Aeshnidae.InstancesNoDat;
///
/// Enclaves: a set of landblocks that, for one group of players, is always that group's own
/// copy - registered at runtime by another mod (Aeshnidae.Towns, 2026-09-29: towns an allegiance
/// buys). InstancesNoDat supplies the mechanism; the owning mod decides who, what and where.
///
/// Register(name, copy, ownerKey, landblocks): every landblock of the set, for a player whose
/// allegiance's monarch is ownerKey, resolves to copy `copy` (Context.CopyFor, after explicit
/// claims, before awakened worlds) - however they arrive: a portal, a recall, a login. Anyone
/// else arriving the same way gets the real landblock. That is the whole access rule: no portal
/// has to be restricted, because no way in leads an outsider into the copy.
///
/// The set's outdoor landblocks open as one region (Regions.OpenWorld, membership = the set), so
/// an island town's seams join and its edge is the sea. A dungeon landblock in a set is a plain
/// copy. Copy numbers are the owner mod's (Towns uses 200000 + its town id) and must not collide
/// with worlds (Worlds: CopyBase .. CopyBase + 99999).
///
/// An enclave copy is Shared by copy (fellowships welcome; Mod.ScopeOf), kept (Keep - dropped
/// items and corpses stay, under the copy number), never scaled, takes no variant, and has no
/// housing in it. The arrival line is the owner mod's (Arrival.OnArrival); ours stays quiet.
///
/// The registry lives in memory; the owning mod registers its enclaves again when it loads, and
/// InstancesNoDat asks the owner nothing - a reload of either mod re-registers from the database.
///
public static class Enclaves
{
public sealed class Enclave
{
public string Name { get; init; } = "";
public int Copy { get; init; }
public uint OwnerKey { get; init; }
public HashSet Landblocks { get; init; } = new();
public override string ToString() => $"enclave '{Name}' copy {Copy} (owner 0x{OwnerKey:X8}, {Landblocks.Count} landblocks)";
}
private static readonly ConcurrentDictionary _byCopy = new();
/// (owner key, landblock) -> copy: the hot-path lookup.
private static readonly ConcurrentDictionary<(uint Owner, ushort Landblock), int> _route = new();
/// Every landblock any enclave covers, for the one-lookup fast path.
private static readonly ConcurrentDictionary _covered = new();
public static IReadOnlyCollection All => _byCopy.Values.ToList();
public static bool IsEnclaveCopy(int copy) => copy > 0 && _byCopy.ContainsKey(copy);
public static Enclave? Of(int copy) => _byCopy.TryGetValue(copy, out var e) ? e : null;
///
/// Registers (or re-registers) an enclave. Called by the owning mod through reflection:
/// string, int, uint and a list of ushort cross the mod boundary cleanly. Returns false when
/// the copy number is taken by a different enclave or collides with the worlds' range.
///
public static bool Register(string name, int copy, uint ownerKey, List landblocks)
{
if (copy <= 0 || Worlds.IsWorldCopy(copy) || landblocks is null || landblocks.Count == 0)
return false;
if (_byCopy.TryGetValue(copy, out var existing) && existing.OwnerKey != ownerKey)
return false;
Unregister(copy);
var enclave = new Enclave { Name = name, Copy = copy, OwnerKey = ownerKey, Landblocks = landblocks.ToHashSet() };
_byCopy[copy] = enclave;
foreach (var lb in enclave.Landblocks)
{
_route[(ownerKey, lb)] = copy;
_covered[lb] = 0;
}
Mod.Trace($"registered {enclave}");
return true;
}
public static bool Unregister(int copy)
{
if (!_byCopy.TryRemove(copy, out var enclave))
return false;
foreach (var lb in enclave.Landblocks)
_route.TryRemove((enclave.OwnerKey, lb), out _);
_covered.Clear();
foreach (var e in _byCopy.Values)
foreach (var lb in e.Landblocks)
_covered[lb] = 0;
return true;
}
/// The key a player's enclaves are registered under: their allegiance's monarch (themselves, if they are one).
public static uint OwnerKeyOf(Player player) =>
player.Allegiance?.MonarchId ?? player.MonarchId ?? 0;
/// The enclave copy of this landblock for this player, 0 for none.
public static int CopyFor(Player player, ushort landblock)
{
if (_covered.IsEmpty || !_covered.ContainsKey(landblock) || !Mod.Settings.Enabled)
return 0;
var owner = OwnerKeyOf(player);
return owner != 0 && _route.TryGetValue((owner, landblock), out var copy) ? copy : 0;
}
/// InstanceWorld.GetOrCreate for an outdoor landblock of an enclave: opened as part of the enclave's region.
public static bool IsOutdoorOf(int copy, ushort landblock) =>
_byCopy.TryGetValue(copy, out var e) && e.Landblocks.Contains(landblock) && Worlds.IsOutdoor(landblock);
public static Landblock? OpenOutdoor(ushort landblock, int copy)
{
if (!_byCopy.TryGetValue(copy, out var enclave))
return null;
var region = Regions.OpenWorld(enclave.Name, copy, lb => enclave.Landblocks.Contains(lb) && Worlds.IsOutdoor(lb), null, landblock);
return region is not null && region.Blocks.TryGetValue(landblock, out var block) ? block : null;
}
}