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