using ACE.DatLoader.FileTypes;
using ACE.Database.Models.World;
namespace Aeshnidae.InstancesNoDat;
///
/// The awakened worlds (Tom, 2026-09-28: "An entire dereth accessible to people with a higher
/// awakened level"). A world is a whole copy of Dereth for characters with enough awakenings:
/// one every Settings.Worlds.TierStep awakenings (5 - worlds 5, 10, 15, ...), open to that tier
/// and above, its monsters scaled and its loot awakened at the world's tier, entered and left
/// with /world. It only exists where its players are: a landblock of it opens when someone comes
/// near, as ACE's own landblocks load, and a world nobody is in costs nothing.
///
/// How it is one mechanism rather than a hundred copies:
///
/// Membership is on the character, PropertyInt 9047 (Tag) = the world's tier, so a login, a
/// restart, a death and a recall all keep them in it.
///
/// Routing: Context.CopyFor answers "world N's copy" for every landblock a world covers (all
/// of Dereth but the instanced dungeons, which keep their own copies), after any explicit
/// claim. Everything that routes a player - a portal, a recall, a login, a step - already asks
/// CopyFor, so the world holds without knowing how the player moved. World N is copy
/// CopyBase + N of each landblock, well above the numbers other copies take.
///
/// The outdoors of a world is one region (Regions) whose membership is a rule, IsOutdoor -
/// ACE's own test for a dungeon landblock, from the cell dat - so its landblocks join at their
/// seams like the real world's; any opener of an outdoor world copy is sent through the region
/// (InstanceWorld.GetOrCreate). A dungeon of the world is a plain copy, as ACE's dungeons have
/// no neighbours.
///
/// A world copy is Shared (Mod.ScopeOf): fellowships work, corpses and dropped items are kept
/// under its copy number (Keep), death drops items. It is scaled by its world's tier
/// (Scaling.IsScaled / TierFor), takes no variant, and has no housing in it (a house exists
/// once, with one owner).
///
[HarmonyPatch]
public static class Worlds
{
/// On the character: the tier of the awakened world they are in; absent = the real Dereth.
public const PropertyInt Tag = (PropertyInt)9047;
/// Aeshnidae.Awakens' count, on the character.
public const PropertyInt Awakenings = (PropertyInt)9040;
private static WorldSettings S => Mod.Settings.Worlds ?? new WorldSettings();
public static bool On => Mod.Settings.Enabled && S.Enabled;
/// A world's copy number: CopyBase + tier, tiers up to 99999 (enclaves take numbers above - Enclaves.cs).
public static bool IsWorldCopy(int copy) => copy > S.CopyBase && copy < S.CopyBase + 100000;
public static int TierOfCopy(int copy) => IsWorldCopy(copy) ? copy - S.CopyBase : 0;
public static int CopyOf(int tier) => S.CopyBase + tier;
public static string NameOf(int tier) => $"world {tier}";
/// The world a character is in (0 = the real Dereth).
public static int TierOf(Player? player) => player?.GetProperty(Tag) ?? 0;
/// Everything but the instanced dungeons, which keep their own copies (the ladder, the hubs).
public static bool Covers(ushort landblock) => !Mod.IsInstanced(landblock);
/// The copy of this landblock a player's world puts them in; 0 when they are in no world or it does not cover it.
public static int CopyFor(Player player, ushort landblock)
{
if (!On)
return 0;
var tier = TierOf(player);
return tier > 0 && Covers(landblock) ? CopyOf(tier) : 0;
}
// ------------------------------------------------------------ the outdoors
private static readonly ConcurrentDictionary _outdoor = new();
///
/// Whether a landblock has overworld - ACE's Landblock.IsDungeon, inverted, from the cell dat
/// without loading the landblock: a dungeon has every height 0, at least one indoor cell and
/// no buildings. Cached per landblock.
///
public static bool IsOutdoor(ushort landblock) => _outdoor.GetOrAdd(landblock, lb =>
{
var x = lb >> 8;
var y = lb & 0xFF;
if (x > 0xFE || y > 0xFE)
return false;
// ACE's own exception: the north-west island.
if (x < 0x08 && y > 0xF8)
return true;
try
{
var cell = DatManager.CellDat.ReadFromDat((uint)lb << 16 | 0xFFFF);
if (cell?.Height is null || cell.Height.Any(h => h != 0))
return true;
var info = DatManager.CellDat.ReadFromDat((uint)lb << 16 | 0xFFFE);
var dungeon = info is not null && info.NumCells > 0 && info.Buildings is not null && info.Buildings.Count == 0;
return !dungeon;
}
catch
{
return false;
}
});
/// InstanceWorld.GetOrCreate for an outdoor landblock of a world: opened as part of the world's region.
public static Landblock? OpenOutdoor(ushort landblock, int copy)
{
var tier = TierOfCopy(copy);
var region = Regions.OpenWorld(NameOf(tier), copy, IsOutdoor, null, landblock);
return region is not null && region.Blocks.TryGetValue(landblock, out var block) ? block : null;
}
// ------------------------------------------------------------ who may go where
public static int AwakeningsOf(Player player) => player.GetProperty(Awakenings) ?? 0;
/// The highest world this character may enter (0 = none yet).
public static int HighestFor(Player player)
{
var step = Math.Max(1, S.TierStep);
var tier = AwakeningsOf(player) / step * step;
return S.MaxTier > 0 ? Math.Min(tier, S.MaxTier / step * step) : tier;
}
public static string Describe(int tier)
{
var s = Mod.Settings;
return $"Awakened world {tier}: monsters +{tier * s.DifficultyPercentPerEnlightenment:0.#}% (attributes and health), " +
$"+{tier * s.RewardPercentPerEnlightenment:0.#}% experience and luminance, loot awakened at tier {tier}. " +
"Portals, recalls and dungeons keep you in this world; /world leave returns you to the real Dereth where you stand.";
}
private static string? Refusal(Player player, int tier, bool waiveAwakenings = false)
{
if (!On)
return "The awakened worlds are not open.";
var step = Math.Max(1, S.TierStep);
if (tier <= 0 || tier % step != 0)
return $"There is a world every {step} awakenings: {step}, {step * 2}, {step * 3} and on.";
if (S.MaxTier > 0 && tier > S.MaxTier)
return $"The worlds go up to {S.MaxTier} for now.";
if (AwakeningsOf(player) < tier && !Mod.IsStaff(player) && !waiveAwakenings)
return $"World {tier} needs {tier} awakenings; you have {AwakeningsOf(player)}." +
(HighestFor(player) > 0 ? $" You may enter world {HighestFor(player)} or any below it." : "");
return Where(player);
}
/// Whether the player is somewhere a world can be entered or left from: the real world, or a world.
private static string? Where(Player player)
{
if (player.Teleporting)
return "Wait until you have arrived.";
if (player.PKTimerActive)
return "You have been in a fight too recently.";
if (player.CurrentLandblock is not { } block || player.Location is null)
return "You are nowhere just now.";
if (InstanceWorld.IsOurs(block) && !IsWorldCopy(InstanceWorld.CopyOf(block)))
return "Step out of the dungeon or island copy first.";
if (!Covers((ushort)block.Id.Landblock))
return "Not from here - step outside first.";
return null;
}
/// Into world `tier`, where the player stands. World thread. `waive`: staff sending (world-send).
public static string Enter(Player player, int tier, bool waive = false)
{
if (Refusal(player, tier, waive) is { } no)
return no;
var was = TierOf(player);
if (was == tier)
return $"You are already in world {tier}.";
var landblock = (ushort)(player.Location.LandblockId.Raw >> 16);
player.SetProperty(Tag, tier);
DropClaims(player);
if (!InstanceWorld.PlaceIn(player, landblock, CopyOf(tier), $"/world {tier}"))
{
if (was > 0) player.SetProperty(Tag, was); else player.RemoveProperty(Tag);
return "The world could not be opened here - see the server log. You have not moved.";
}
DropClaims(player);
ModManager.Log($"[{Mod.Name}] {player.Name} ({AwakeningsOf(player)} awakenings) entered awakened world {tier} at {player.Location.ToLOCString()}{(was > 0 ? $" from world {was}" : "")}");
return Describe(tier);
}
/// Back to the real Dereth, where the player stands. World thread.
public static string Leave(Player player, string why = "/world leave")
{
var tier = TierOf(player);
if (tier <= 0)
return "You are in the real Dereth already.";
// In an instanced dungeon entered from inside a world: the world lets go now, and the
// way out of the dungeon leads to the real Dereth.
if (player.CurrentLandblock is { } here && InstanceWorld.IsOurs(here) && !IsWorldCopy(InstanceWorld.CopyOf(here)))
{
player.RemoveProperty(Tag);
DropClaims(player);
return "You are no longer in the awakened world; leaving this dungeon takes you to the real Dereth.";
}
if (Where(player) is { } no)
return no;
var landblock = (ushort)(player.Location.LandblockId.Raw >> 16);
player.RemoveProperty(Tag);
DropClaims(player);
if (!InstanceWorld.PlaceIn(player, landblock, 0, why))
{
player.SetProperty(Tag, tier);
return "Could not return you just now - see the server log.";
}
ModManager.Log($"[{Mod.Name}] {player.Name} left awakened world {tier} ({why}) at {player.Location.ToLOCString()}");
return "You are back in the real Dereth.";
}
///
/// Every claim this player holds on a world copy: the world routes them without one, and a
/// claim left behind would pull them back into the world on that landblock after leaving it.
///
private static void DropClaims(Player player) => Context.DropWhere(player, IsWorldCopy);
// ------------------------------------------------------------ status
/// Every open world: tier, landblocks open, players in it.
public static List<(int Tier, int Landblocks, List Players)> Open()
{
return InstanceWorld.All.Where(a => IsWorldCopy(a.Key.Copy))
.GroupBy(a => TierOfCopy(a.Key.Copy))
.Select(g => (g.Key, g.Count(), g.SelectMany(a => InstanceWorld.PlayersIn(a.Block)).Distinct().ToList()))
.OrderBy(w => w.Item1)
.ToList();
}
public static string Status(Player? player)
{
var sb = new StringBuilder();
var step = Math.Max(1, S.TierStep);
if (!On)
return "The awakened worlds are not open.";
if (player is not null)
{
var tier = TierOf(player);
sb.AppendLine(tier > 0 ? $"You are in awakened world {tier}." : "You are in the real Dereth.");
var highest = HighestFor(player);
sb.AppendLine(highest > 0
? $"With {AwakeningsOf(player)} awakenings you may enter worlds {step} to {highest}: /world , e.g. /world {highest}."
: $"The first world opens at {step} awakenings; you have {AwakeningsOf(player)}.");
}
var open = Open();
sb.AppendLine(open.Count == 0
? "No world is open right now."
: "Open now: " + string.Join("; ", open.Select(w => $"world {w.Tier} - {w.Players.Count} player(s), {w.Landblocks} landblock(s)")));
return sb.ToString();
}
// ------------------------------------------------------------ commands
[CommandHandler("world", AccessLevel.Player, CommandHandlerFlag.RequiresWorld, 0,
"The awakened worlds: whole copies of Dereth for the awakened, one every few awakenings.",
"/world where you are, and which worlds you may enter\n" +
"/world enter that world where you stand, e.g. /world 5\n" +
"/world leave back to the real Dereth where you stand")]
public static void HandleWorld(Session session, params string[] parameters)
{
var player = session.Player;
var arg = parameters.Length > 0 ? parameters[0].Trim().ToLowerInvariant() : "";
if (arg == "")
{
Reply(session, Status(player));
return;
}
if (arg is "leave" or "exit" or "real")
{
Reply(session, Leave(player));
return;
}
if (!int.TryParse(arg, out var tier))
{
Reply(session, "/world, /world , or /world leave");
return;
}
Reply(session, Enter(player, tier));
}
[CommandHandler("world-send", AccessLevel.Admin, CommandHandlerFlag.None, 2,
"Put an online player into an awakened world, or back to the real Dereth, where they stand. Console-safe; ignores the awakening requirement.",
"/world-send ")]
public static void HandleWorldSend(Session session, params string[] parameters)
{
var name = string.Join(" ", parameters.Take(parameters.Length - 1));
var target = PlayerManager.GetOnlinePlayer(name);
if (target is null)
{
Reply(session, $"{name} is not online.");
return;
}
var arg = parameters[^1].ToLowerInvariant();
WorldManager.EnqueueAction(new ActionEventDelegate(() =>
{
string result;
if (arg == "leave")
result = Leave(target, "world-send leave");
else if (int.TryParse(arg, out var tier))
result = Enter(target, tier, waive: true); // staff sending: the awakening requirement is waived, the rest is not
else
result = "/world-send ";
target.SendMessage(result);
ModManager.Log($"[{Mod.Name}] world-send {target.Name} {arg}: {result}");
}));
Reply(session, $"Sending {target.Name} ({arg}) - see the log for the outcome.");
}
[CommandHandler("world-test", AccessLevel.Admin, CommandHandlerFlag.None, 1,
"Awakened worlds, console tests: open a world around a landblock and report what opened, the scaling and the housing; or close a world.",
"/world-test e.g. /world-test 5 A9B4 - opens world 5 around Holtburg (outdoor: the ring; a dungeon: itself)\n" +
"/world-test close close every open copy of that world\n" +
"/world-test outdoor what IsOutdoor says of a landblock (ACE's dungeon rule)")]
public static void HandleWorldTest(Session session, params string[] parameters)
{
static bool Lb(string text, out ushort lb) =>
ushort.TryParse(text.Replace("0x", "", StringComparison.OrdinalIgnoreCase)[..Math.Min(4, text.Replace("0x", "", StringComparison.OrdinalIgnoreCase).Length)],
System.Globalization.NumberStyles.HexNumber, null, out lb);
var verb = parameters[0].ToLowerInvariant();
if (verb == "outdoor" && parameters.Length > 1 && Lb(parameters[1], out var olb))
{
Reply(session, $"{olb:X4}: IsOutdoor {IsOutdoor(olb)}, covered by worlds {Covers(olb)}");
return;
}
if (verb == "close" && parameters.Length > 1 && int.TryParse(parameters[1], out var ctier))
{
WorldManager.EnqueueAction(new ActionEventDelegate(() =>
{
var closed = 0;
foreach (var (key, _) in InstanceWorld.All.Where(a => a.Key.Copy == CopyOf(ctier)).ToList())
if (InstanceWorld.Close(key.Landblock, key.Copy, true)) closed++;
Reply(session, $"world {ctier}: closed {closed} copy(ies)");
}));
return;
}
if (!int.TryParse(verb, out var tier) || parameters.Length < 2 || !Lb(parameters[1], out var landblock))
{
Reply(session, "/world-test | close | outdoor ");
return;
}
WorldManager.EnqueueAction(new ActionEventDelegate(() =>
{
var copy = CopyOf(tier);
var block = InstanceWorld.GetOrCreate(landblock, copy);
if (block is null)
{
Reply(session, $"world {tier}: could not open {landblock:X4}");
return;
}
var region = Regions.Of(block);
Reply(session, $"world {tier} (copy {copy}) {landblock:X4}: {(IsOutdoor(landblock) ? "outdoor" : "dungeon")}, " +
(region is null ? "a plain copy (no region)" : $"{region}: {string.Join(" ", region.Blocks.Keys.OrderBy(k => k).Select(k => k.ToString("X4")))}") +
$"; tier {Scaling.TierFor(landblock, copy)}, scope {Mod.ScopeOf(landblock, copy)}, kept {Mod.IsSharedCopy(landblock, copy)}");
// Once Init's spawn task has run: a monster's attributes against its weenie, and housing.
var chain = new ActionChain();
chain.AddDelaySeconds(4);
chain.AddAction(WorldManager.ActionQueue, () =>
{
var objects = ObjectsOf(block);
var monster = objects.OfType().FirstOrDefault(c => c is not Player && c.IsMonster);
var housing = objects.Count(o => Housing.Contains(o.WeenieType));
var masterHousing = DatabaseManager.World.GetCachedInstancesByLandblock(landblock)
.Count(r => DatabaseManager.World.GetCachedWeenie(r.WeenieClassId) is { } w && Housing.Contains(w.WeenieType));
string scaled = "no monster here to check";
if (monster is not null && DatabaseManager.World.GetCachedWeenie(monster.WeenieClassId) is { } weenie
&& weenie.PropertiesAttribute?.TryGetValue(PropertyAttribute.Strength, out var str) == true)
scaled = $"{monster.Name}: Strength {monster.Strength.StartingValue} (weenie {str.InitLevel}), health {monster.Health.MaxValue}";
Reply(session, $"world {tier} {landblock:X4}: {objects.Count} object(s); {scaled}; housing {housing} (the real landblock places {masterHousing})");
});
chain.EnqueueChain();
}));
}
private static List ObjectsOf(Landblock block) => InstanceWorld.ObjectsOf(block).ToList();
private static void Reply(Session? session, string message)
{
foreach (var line in (message ?? "").Split('\n'))
{
var text = line.TrimEnd('\r');
if (string.IsNullOrWhiteSpace(text))
continue;
if (session?.Player is null)
ModManager.Log(text);
else
session.Player.SendMessage(text);
}
}
// ------------------------------------------------------------ hooks
///
/// Logging in: someone saved inside a world while the worlds were switched off is simply in
/// the real Dereth (CopyFor answered 0), so the world lets go of them; anyone else in a world
/// is told where they are.
///
[HarmonyPostfix]
[HarmonyPatch(typeof(Player), nameof(Player.PlayerEnterWorld))]
public static void PostEnterWorld(Player __instance)
{
try
{
var tier = TierOf(__instance);
if (tier <= 0)
return;
if (!On)
{
__instance.RemoveProperty(Tag);
__instance.SendMessage("The awakened worlds are closed; you are in the real Dereth.");
return;
}
var chain = new ActionChain();
chain.AddDelaySeconds(6);
chain.AddAction(__instance, () => __instance.SendMessage($"You are in awakened world {tier}. /world leave returns you to the real Dereth."));
chain.EnqueueChain();
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] world login check failed: {ex.Message}", ModManager.LogLevel.Warn);
}
}
private static readonly HashSet Housing = new()
{
WeenieType.House, WeenieType.SlumLord, WeenieType.Storage, WeenieType.Hook,
WeenieType.HousePortal, WeenieType.BootSpot, WeenieType.Deed, WeenieType.AllegianceBindstone,
};
///
/// No housing in a world: a house, its slumlord, storage, hooks and portal exist once, with
/// one owner (the same rule as OverworldCopies' bubbles). Filters the placed rows a world copy
/// spawns from; returns a new list - the cache is shared with the real landblock.
///
[HarmonyPostfix]
[HarmonyPriority(Priority.Last)]
[HarmonyPatch(typeof(WorldDatabaseWithEntityCache), nameof(WorldDatabaseWithEntityCache.GetCachedInstancesByLandblock), new[] { typeof(ushort) })]
public static void PostGetCachedInstances(ushort landblock, ref List __result)
{
try
{
if (__result is null || __result.Count == 0 || Context.Ambient is not { } ambient || ambient.Id.Landblock != landblock)
return;
// Worlds and enclaves (an allegiance's town) alike: a house exists once, with one owner.
var copy = InstanceWorld.CopyOf(ambient);
if (!IsWorldCopy(copy) && !Enclaves.IsEnclaveCopy(copy))
return;
var kept = __result.Where(row => DatabaseManager.World.GetCachedWeenie(row.WeenieClassId) is not { } w || !Housing.Contains(w.WeenieType)).ToList();
if (kept.Count != __result.Count)
__result = kept;
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] world housing filter failed: {ex.Message}", ModManager.LogLevel.Warn);
}
}
}