namespace Aeshnidae.InstancesNoDat;
///
/// A region copy: several neighbouring landblocks opened as ONE copy - an island, a
/// valley - where a copy of a single landblock would end at its edge. Every landblock of
/// the region shares the copy number, and each open landblock's neighbours are its open
/// siblings, so physics, sight, broadcasts and movement carry across the seams exactly as
/// they do between ACE's own landblocks, and never out into the real world.
///
/// Opened lazily (Tom, 2026-09-25: "we should only be activating landblocks that are
/// being entered or adjacent"): a region copy starts as the 3x3 around where its first
/// player arrives, and every time a player crosses into one of its landblocks the ring
/// around that landblock is opened too - on the next world tick, 192 m ahead of them,
/// beyond what they can see. A landblock of the region that is not open yet has no cells
/// (nothing reaches into it), exactly like the world past the region's edge.
///
/// The region is defined in Settings.Outdoor (an entry whose Landblocks lists them); who
/// opens a copy of it is up to the caller - Aeshnidae.OverworldCopies, which binds here
/// by reflection. Everything in InstancesNoDat that used to ask "same landblock?" to
/// decide whether two things are in the same place asks Same() instead, which is the
/// old answer for every copy that is not part of a region.
///
/// Added 2026-09-25 (Tom: an instanced Aerlinthe island with a portal to it), staging.
///
public static class Regions
{
public sealed class RegionCopy
{
public string Name { get; init; } = "";
public int Copy { get; init; }
/// Every landblock the region has, open or not.
public HashSet All { get; init; } = new();
///
/// Membership by rule instead of by list, for a region too big to list: an awakened world's
/// outdoors is every landblock of Dereth that is not a dungeon (Worlds.IsOutdoor), tested
/// when asked. Null for an ordinary region, whose All is the list.
///
public Func? Member { get; init; }
/// Whether this landblock is part of the region: Member's rule, else the list.
public bool Has(ushort landblock) => Member?.Invoke(landblock) ?? All.Contains(landblock);
/// The landblocks open so far.
public ConcurrentDictionary Blocks { get; } = new();
/// Whose copy it is: the tier and the variant of every landblock opened later are recorded for them.
public uint OwnerGuid { get; init; }
/// Terrain-only stand-ins for landblocks of the region that are not open yet (ShellFor).
public ConcurrentDictionary> Shells { get; } = new();
public override string ToString() => $"region '{Name}' copy {Copy} ({Blocks.Count} of {(Member is null ? All.Count.ToString() : "the map's")} landblocks open)";
}
private static readonly ConcurrentDictionary _byBlock = new(ReferenceEqualityComparer.Instance);
public static int Count => _byBlock.Values.Distinct().Count();
public static RegionCopy? Of(Landblock? block) =>
block is not null && _byBlock.TryGetValue(block, out var region) ? region : null;
/// The same region copy's landblock at this id, or null (not in the region, or not open yet).
public static Landblock? Sibling(Landblock? of, ushort landblock) =>
Of(of) is { } region && region.Blocks.TryGetValue(landblock, out var sibling) ? sibling : null;
///
/// Whether two landblocks are the same place: the same Landblock, or two landblocks of
/// one region copy. The question every privacy and visibility rule used to answer with
/// ReferenceEquals.
///
public static bool Same(Landblock? a, Landblock? b)
{
if (a is null || b is null)
return false;
if (ReferenceEquals(a, b))
return true;
return Of(a) is { } region && ReferenceEquals(region, Of(b));
}
/// Every landblock that is the same place as this one (itself included).
public static IEnumerable Place(Landblock block) =>
Of(block) is { } region ? region.Blocks.Values : new[] { block };
/// The lowest copy number free on every one of these landblocks, or 0.
public static int FreeCopy(IReadOnlyCollection landblocks, int max)
{
for (var copy = 1; copy <= Math.Max(1, max); copy++)
{
if (landblocks.All(lb => InstanceWorld.Find(lb, copy) is null))
return copy;
}
return 0;
}
///
/// Opens a copy of the named region (Settings.Outdoor) under one copy number - `copy`,
/// or the lowest free one when 0 - owned by `owner`, starting with the 3x3 around
/// `around`. Returns the copy number, or 0. World thread.
///
public static int Open(string name, Player owner, int copy, ushort around)
{
var landblocks = Mod.RegionLandblocks(name);
if (landblocks.Count == 0)
{
ModManager.Log($"[{Mod.Name}] no region '{name}' in Outdoor", ModManager.LogLevel.Warn);
return 0;
}
if (!landblocks.Contains(around))
around = landblocks[0];
// Joining a region copy that is already open (a fellowship's, or the one copy of a
// Shared region): open the ring around where this player arrives in THAT copy - the
// landblock they land in may have unloaded, idle, while the rest of it stayed open.
if (copy > 0 && All.FirstOrDefault(r => r.Copy == copy && string.Equals(r.Name, name, StringComparison.OrdinalIgnoreCase)) is { } existing)
{
OpenAround(existing, around, owner);
return copy;
}
if (copy <= 0)
copy = FreeCopy(landblocks, Mod.Settings.MaxCopiesPerLandblock);
if (copy <= 0)
{
ModManager.Log($"[{Mod.Name}] region '{name}': no copy number free on all {landblocks.Count} landblocks", ModManager.LogLevel.Warn);
return 0;
}
var region = new RegionCopy { Name = name, Copy = copy, All = landblocks.ToHashSet(), OwnerGuid = owner.Guid.Full };
var started = DateTime.UtcNow;
OpenAround(region, around, owner);
ModManager.Log($"[{Mod.Name}] opened {region} for {owner.Name} around {around:X4} in {(DateTime.UtcNow - started).TotalMilliseconds:N0} ms");
return copy;
}
///
/// Opens (or joins) the outdoor region of an awakened world (Worlds.cs): copy `copy` of every
/// landblock `member` accepts, starting with the ring around `around`. Returns the region.
/// World thread. The same as Open, for a region defined by a rule rather than a list.
///
public static RegionCopy? OpenWorld(string name, int copy, Func member, Player? owner, ushort around)
{
if (!member(around))
return null;
var region = All.FirstOrDefault(r => r.Copy == copy && string.Equals(r.Name, name, StringComparison.OrdinalIgnoreCase));
var opened = region is null;
region ??= new RegionCopy { Name = name, Copy = copy, Member = member, OwnerGuid = owner?.Guid.Full ?? 0 };
OpenAround(region, around, owner);
if (opened && region.Blocks.Count > 0)
ModManager.Log($"[{Mod.Name}] opened {region} around {around:X4}{(owner is null ? "" : $" for {owner.Name}")}");
return region.Blocks.Count > 0 ? region : null;
}
///
/// A player has crossed into this landblock of a region copy: open the ring around it,
/// on the world thread, straight after the tick that moved them. Nothing to do when the
/// ring is open already, which is the usual case.
///
public static void Entered(Landblock block, Player player)
{
if (Of(block) is not { } region)
return;
var center = (ushort)block.Id.Landblock;
if (Ring(region, center).All(region.Blocks.ContainsKey))
return;
WorldManager.EnqueueAction(new ActionEventDelegate(() =>
{
try
{
if (Of(block) is { } still)
OpenAround(still, center, player);
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] opening the ring around {center:X4} of {region} failed: {ex.Message}", ModManager.LogLevel.Warn);
}
}));
}
/// A landblock of the region that is not open yet: open its ring (queued) - for something that tried to go there.
public static void Wanted(RegionCopy region, ushort landblock, Player? player)
{
WorldManager.EnqueueAction(new ActionEventDelegate(() =>
{
try
{
if (region.Blocks.Values.FirstOrDefault() is { } any && Of(any) is { } still && ReferenceEquals(still, region))
OpenAround(region, landblock, player);
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] opening {landblock:X4} of {region} failed: {ex.Message}", ModManager.LogLevel.Warn);
}
}));
}
/// Shells built since the mod loaded.
public static int ShellsBuilt;
///
/// The ground of a landblock of the region that is not open yet: ACE's own terrain and
/// buildings for it, built from the dat - a physics landblock with no server landblock
/// behind it, no monsters, no generators, no scenery. Null when shells are off or the
/// landblock is not part of the region (the world past the region's edge stays nothing).
///
/// Why (found 2026-09-27 on live): an unopened landblock used to be no cells at all, and
/// a creature standing within a body-width of the seam checks the ground on both sides
/// every physics step. With nothing on the far side EVERY step failed, even a fall
/// straight down: the creature hung there and ACE logged "avoided CurCell=null" about
/// ten times a second (4,489 times that day, all at BBE9's north edge, next to BBEA
/// which the landing at BAE8 never opens). A shell is solid ground, so they stand and
/// fall normally. Walking all the way into one is still the removal it always was
/// (Patches: a creature relocating into a landblock of the region not open yet).
///
/// Built once per landblock per region copy on first use (Lazy: physics runs on the
/// landblock group threads), dropped when that landblock opens for real, and gone with
/// the region copy when it closes. A few hundred KB each.
///
public static ACE.Server.Physics.Common.Landblock? ShellFor(RegionCopy region, ushort landblock)
{
if (!Mod.Settings.RegionEdgeShells || !region.Has(landblock))
return null;
return region.Shells.GetOrAdd(landblock, lb => new Lazy(() => BuildShell(region, lb))).Value;
}
/// /inst-shell: build a shell outside any region, for a console check. Not cached.
public static string TestShell(ushort landblock)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var cellLandblock = ACE.Server.Physics.Common.DBObj.GetCellLandblock((uint)landblock << 16 | 0xFFFF);
var shell = new ACE.Server.Physics.Common.Landblock(cellLandblock);
shell.init_landcell();
shell.init_buildings();
sw.Stop();
// The ground at the landblock's centre and at the middle of its north edge, as physics sees it.
string Ground(float x, float y)
{
var cellX = (uint)Math.Min(7, x / 24);
var cellY = (uint)Math.Min(7, y / 24);
var cell = shell.get_landcell((uint)landblock << 16 | (cellX * 8 + cellY + 1));
if (cell is null)
return "no cell";
ACE.Server.Physics.Polygon? walkable = null;
var point = new System.Numerics.Vector3(x, y, 0);
if (!cell.find_terrain_poly(point, ref walkable) || walkable is null)
return $"cell {cell.ID:X8}, no terrain";
ACE.Server.Physics.Extensions.PlaneExtensions.set_height(walkable.Plane, ref point);
return $"cell {cell.ID:X8}, ground z {point.Z:0.0}";
}
return $"shell {landblock:X4}: {shell.LandCells.Count} terrain cell(s), {shell.Buildings.Count} building(s), built in {sw.Elapsed.TotalMilliseconds:0} ms; " +
$"centre {Ground(96, 96)}; north edge {Ground(96, 191)}";
}
private static ACE.Server.Physics.Common.Landblock BuildShell(RegionCopy region, ushort landblock)
{
var cellLandblock = ACE.Server.Physics.Common.DBObj.GetCellLandblock((uint)landblock << 16 | 0xFFFF);
var shell = new ACE.Server.Physics.Common.Landblock(cellLandblock);
// Terrain cells know their landblock; buildings stand in them. Not init_static_objs:
// scenery and dat statics would be placed through LScape, i.e. back through here.
shell.init_landcell();
shell.init_buildings();
// An empty neighbour list, never null (2026-10-01). A creature that steps into a
// shell's cell makes it its CurLandblock, and outdoor visibility then walks that
// landblock's neighbours (PhysicsLandblock.GetServerObjects(true)) - a list ACE
// leaves null until SetAdjacents. The NullReferenceException escaped the whole
// instance tick ("instance tick failed", 289 times that day), so every copy - the
// island, the hubs, every dungeon - skipped its physics and queued work for that
// tick. A shell sees nothing beyond itself, which is what it is.
shell.SetAdjacents(new List());
Interlocked.Increment(ref ShellsBuilt);
Mod.Trace($"{region}: {landblock:X4} is not open - a terrain-only shell stands in for it at the seam");
return shell;
}
/// The landblock and its eight neighbours that belong to the region.
private static IEnumerable Ring(RegionCopy region, ushort center)
{
var x = center >> 8;
var y = center & 0xFF;
for (var dx = -1; dx <= 1; dx++)
{
for (var dy = -1; dy <= 1; dy++)
{
var nx = x + dx;
var ny = y + dy;
if (nx < 0 || nx > 0xFF || ny < 0 || ny > 0xFF)
continue;
var lb = (ushort)((nx << 8) | ny);
if (region.Has(lb))
yield return lb;
}
}
}
/// Set while OpenAround creates a region's landblocks, so InstanceWorld.GetOrCreate takes its ordinary path for them.
[ThreadStatic] public static bool OpeningNow;
private static void OpenAround(RegionCopy region, ushort center, Player? player)
{
var owner = PlayerManager.GetOnlinePlayer(region.OwnerGuid) ?? player;
var opened = new List();
var was = OpeningNow;
OpeningNow = true;
try
{
foreach (var lb in Ring(region, center))
{
if (region.Blocks.ContainsKey(lb))
continue;
// Owner and variant on record before the copy is made, as every opener does.
if (owner is not null)
Scaling.Remember(lb, region.Copy, owner);
var block = InstanceWorld.GetOrCreate(lb, region.Copy);
if (block is null)
continue;
region.Blocks[lb] = block;
_byBlock[block] = region;
region.Shells.TryRemove(lb, out _);
opened.Add(lb);
}
}
finally
{
OpeningNow = was;
}
if (opened.Count == 0)
return;
Link(region);
if (Mod.Settings.LogEveryCopyOpened)
ModManager.Log($"[{Mod.Name}] {region}: opened {string.Join(", ", opened.Select(lb => lb.ToString("X4")))} around {center:X4} " +
$"for {player?.Name ?? owner?.Name ?? "nobody"} (region opened by {owner?.Name ?? "someone offline"}) - " +
$"open now: {InstanceWorld.OpenSummary().Text}");
InstanceWorld.WarnIfMany();
}
///
/// Each open landblock's neighbours are its open siblings among the eight around it:
/// the server side (Landblock.Adjacents - broadcasts, and waking a neighbour when a
/// player nears it) and the physics side (outdoor visibility). Relinked whole whenever
/// landblocks open - a few dozen list assignments.
///
private static void Link(RegionCopy region)
{
foreach (var (lb, block) in region.Blocks)
{
var adjacents = new List();
foreach (var n in Ring(region, lb))
{
if (n != lb && region.Blocks.TryGetValue(n, out var neighbour))
adjacents.Add(neighbour);
}
block.Adjacents = adjacents;
block.PhysicsLandblock.SetAdjacents(adjacents);
}
}
///
/// ACE unloading an idle landblock of a region copy (Landblock.Tick queues it for
/// destruction after UnloadInterval - five minutes - with no player in it or next to
/// it, exactly as for a landblock of the real world): that landblock leaves the region
/// and is closed on its own; the rest of the region stays open. The next player to come
/// near opens it again, fresh - as ACE reloads a real landblock. Queued to the world
/// thread; the tick that asked is still running. Never the last open landblock, and
/// never one with a player in it - the region closes whole, by CloseEmptyCopies.
///
public static bool Unload(Landblock block)
{
if (Of(block) is not { } region || region.Blocks.Count <= 1)
return false;
WorldManager.EnqueueAction(new ActionEventDelegate(() =>
{
try
{
if (!ReferenceEquals(Of(block), region) || region.Blocks.Count <= 1 || InstanceWorld.PlayersIn(block).Count > 0)
return;
var lb = (ushort)block.Id.Landblock;
region.Blocks.TryRemove(lb, out _);
_byBlock.TryRemove(block, out _);
Link(region);
// Let go of the region first, so this closes the one landblock, not the region.
InstanceWorld.Close(lb, region.Copy, ejectPlayers: true);
Mod.Trace($"{region}: {lb:X4} unloaded, idle for {Landblock.UnloadInterval.TotalMinutes:0} min");
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] unloading a landblock of {region} failed: {ex.Message}", ModManager.LogLevel.Warn);
}
}));
return true;
}
/// Whether anyone is in any landblock of this block's place.
public static bool Occupied(Landblock block) =>
Place(block).Any(b => InstanceWorld.PlayersIn(b).Count > 0);
///
/// A landblock of a region closing: the whole region goes with it. Forgets the region
/// and returns its OTHER open landblocks, for the caller to close in turn.
///
public static List<(ushort Landblock, int Copy)> Forget(Landblock block)
{
var others = new List<(ushort, int)>();
if (!_byBlock.TryRemove(block, out var region))
return others;
foreach (var (lb, sibling) in region.Blocks)
{
if (ReferenceEquals(sibling, block))
continue;
_byBlock.TryRemove(sibling, out _);
others.Add((lb, region.Copy));
}
return others;
}
public static void ForgetAll() => _byBlock.Clear();
/// Open region copies, for /inst.
public static IEnumerable All => _byBlock.Values.Distinct();
}