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