/************************************************************************* * ModernUO * * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: Map.cs * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * *************************************************************************/ using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using Server.Buffers; using Server.Collections; using Server.Items; using Server.Logging; using Server.Network; using Server.Targeting; namespace Server; [Flags] public enum MapRules { None = 0x0000, Internal = 0x0001, // Internal map (used for dragging, commodity deeds, etc) FreeMovement = 0x0002, // Anyone can move over anyone else without taking stamina loss BeneficialRestrictions = 0x0004, // Disallow performing beneficial actions on criminals/murderers HarmfulRestrictions = 0x0008, // Disallow performing harmful actions on innocents TrammelRules = FreeMovement | BeneficialRestrictions | HarmfulRestrictions, FeluccaRules = None } /// /// Indicates why a spawn position check failed. Used by spawners to determine /// if optimization (caching) should be enabled. /// [Flags] public enum SpawnFailureReason { None = 0, InvalidMap = 1 << 0, // Internal map or out of bounds RegionBlocked = 1 << 1, // Region doesn't allow spawning TransientBlocker = 1 << 2, // Mobile or movable item blocking NonTransientBlocker = 1 << 3 // Static, multi, impassable land, or non-movable item } public sealed partial class Map : IComparable, ISpanFormattable, ISpanParsable { public const int SectorSize = 16; public const int SectorShift = 4; public const int SectorActiveRange = 2; private static ILogger logger = LogFactory.GetLogger(typeof(Map)); private readonly int m_FileIndex; private readonly Sector[][] m_Sectors; private readonly int m_SectorsHeight; private readonly int m_SectorsWidth; private Region m_DefaultRegion; private string m_Name; private TileMatrix m_Tiles; public Map(int mapID, int mapIndex, int fileIndex, int width, int height, int season, string name, MapRules rules) { MapID = mapID; MapIndex = mapIndex; m_FileIndex = fileIndex; Width = width; Height = height; Season = season; m_Name = name; Rules = rules; Regions = new Dictionary(StringComparer.OrdinalIgnoreCase); _invalidSector = new Sector(0, 0, this); m_SectorsWidth = width >> SectorShift; m_SectorsHeight = height >> SectorShift; m_Sectors = new Sector[m_SectorsWidth][]; } public static Map[] Maps { get; } = new Map[0x100]; public static Map Felucca => Maps[0]; public static Map Trammel => Maps[1]; public static Map Ilshenar => Maps[2]; public static Map Malas => Maps[3]; public static Map Tokuno => Maps[4]; public static Map TerMur => Maps[5]; public static Map Internal => Maps[0x7F]; public static List AllMaps { get; } = new(); public int Season { get; set; } public TileMatrix Tiles => m_Tiles ??= new TileMatrix(this, m_FileIndex, MapID, Width, Height); public int MapID { get; } public int MapIndex { get; } public int Width { get; } public int Height { get; } public Dictionary Regions { get; } public Region DefaultRegion { get => m_DefaultRegion ??= new Region(null, this, 0, Array.Empty()); set => m_DefaultRegion = value; } public MapRules Rules { get; set; } private readonly Sector _invalidSector; public string Name { get { if (this == Internal && m_Name != "Internal") { logger.Warning( $"Internal map name was '{{Name}}'{Environment.NewLine}{{StackTrace}}", m_Name, new StackTrace() ); m_Name = "Internal"; } return m_Name; } set { if (this == Internal && value != "Internal") { logger.Warning( $"Attempted to set internal map name to '{{Value}}'{Environment.NewLine}{{StackTrace}}", value, new StackTrace() ); value = "Internal"; } m_Name = value; } } public static int[] InvalidLandTiles { get; set; } = { 0x244 }; public static int MaxLOSDistance { get; set; } = 25; public int CompareTo(Map other) => other == null ? -1 : MapID.CompareTo(other.MapID); public static string[] GetMapNames() { var mapCount = 0; for (var i = 0; i < Maps.Length; i++) { var map = Maps[i]; if (map != null) { mapCount++; } } var mapNames = new string[mapCount]; for (int i = 0, mIndex = 0; i < Maps.Length; i++) { var map = Maps[i]; if (map != null) { mapNames[mIndex++] = map.Name; } } return mapNames; } public static Map[] GetMapValues() { var mapCount = 0; for (var i = 0; i < Maps.Length; i++) { var map = Maps[i]; if (map != null) { mapCount++; } } var mapValues = new Map[mapCount]; for (int i = 0, mIndex = 0; i < Maps.Length; i++) { var map = Maps[i]; if (map != null) { mapValues[mIndex++] = map; } } return mapValues; } public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) { if (destination.Length >= Name.Length) { Name.CopyTo(destination); charsWritten = Name.Length; return true; } charsWritten = 0; return false; } public override string ToString() => Name; public string ToString(string format, IFormatProvider formatProvider) { // format and formatProvider are not doing anything right now, so use the // default ToString implementation. return ToString(); } public int GetAverageZ(int x, int y) { GetAverageZ(x, y, out _, out var avg, out _); return avg; } public void GetAverageZ(int x, int y, out int z, out int avg, out int top) { var zTop = Tiles.GetLandTile(x, y).Z; var zLeft = Tiles.GetLandTile(x, y + 1).Z; var zRight = Tiles.GetLandTile(x + 1, y).Z; var zBottom = Tiles.GetLandTile(x + 1, y + 1).Z; z = zTop; if (zLeft < z) { z = zLeft; } if (zRight < z) { z = zRight; } if (zBottom < z) { z = zBottom; } top = zTop; if (zLeft > top) { top = zLeft; } if (zRight > top) { top = zRight; } if (zBottom > top) { top = zBottom; } avg = (zTop - zBottom).Abs() > (zLeft - zRight).Abs() ? FloorAverage(zLeft, zRight) : FloorAverage(zTop, zBottom); } private static int FloorAverage(int a, int b) { var v = a + b; if (v < 0) { --v; } return v / 2; } private static void AcquireFixItems(Map map, int x, int y, Item[] pool, out int length) { length = 0; if (map == null || map == Internal || x < 0 || x > map.Width || y < 0 || y > map.Height) { return; } foreach (var item in map.GetItemsAt(x, y)) { if (item is not BaseMulti && item.ItemID <= TileData.MaxItemValue) { if (length == 128) { break; } pool[length++] = item; } } Array.Sort(pool, 0, length, ZComparer.Default); } public void FixColumn(int x, int y) { var landTile = Tiles.GetLandTile(x, y); GetAverageZ(x, y, out _, out var landAvg, out _); var items = STArrayPool.Shared.Rent(128); AcquireFixItems(this, x, y, items, out var length); for (var i = 0; i < length; i++) { var toFix = items[i]; if (!toFix.Movable) { continue; } var z = int.MinValue; var currentZ = toFix.Z; if (!landTile.Ignored && landAvg <= currentZ) { z = landAvg; } foreach (var tile in Tiles.GetStaticAndMultiTiles(x, y)) { var id = TileData.ItemTable[tile.ID & TileData.MaxItemValue]; var checkZ = tile.Z; var checkTop = checkZ + id.CalcHeight; if (checkTop == checkZ && !id.Surface) { ++checkTop; } if (checkTop > z && checkTop <= currentZ) { z = checkTop; } } for (var j = 0; j < length; ++j) { if (j == i) { continue; } var item = items[j]; var id = item.ItemData; var checkZ = item.Z; var checkTop = checkZ + id.CalcHeight; if (checkTop == checkZ && !id.Surface) { ++checkTop; } if (checkTop > z && checkTop <= currentZ) { z = checkTop; } } if (z != int.MinValue) { toFix.Location = new Point3D(toFix.X, toFix.Y, z); } } STArrayPool.Shared.Return(items, true); } /// /// Gets the highest surface that is lower than . /// /// The reference point. /// A surface or . public object GetTopSurface(Point3D p) { if (this == Internal) { return null; } object surface = null; var surfaceZ = int.MinValue; var lt = Tiles.GetLandTile(p.X, p.Y); if (!lt.Ignored) { var avgZ = GetAverageZ(p.X, p.Y); if (avgZ <= p.Z) { surface = lt; surfaceZ = avgZ; if (surfaceZ == p.Z) { return surface; } } } foreach (var tile in Tiles.GetStaticAndMultiTiles(p.X, p.Y)) { var id = TileData.ItemTable[tile.ID & TileData.MaxItemValue]; if (id.Surface || id.Wet) { var tileZ = tile.Z + id.CalcHeight; if (tileZ > surfaceZ && tileZ <= p.Z) { surface = tile; surfaceZ = tileZ; if (surfaceZ == p.Z) { return surface; } } } } var sector = GetSector(p.X, p.Y); foreach (var item in sector.Items) { if (item is BaseMulti || item.ItemID > TileData.MaxItemValue || !item.AtWorldPoint(p.X, p.Y) || item.Movable) { continue; } var id = item.ItemData; if (id.Surface || id.Wet) { var itemZ = item.Z + id.CalcHeight; if (itemZ > surfaceZ && itemZ <= p.Z) { surface = item; surfaceZ = itemZ; if (surfaceZ == p.Z) { return surface; } } } } return surface; } /// /// Gets the Z level of the highest surface that is at or below . /// /// The reference point. /// The Z level of the surface, or p.Z if no surface is found below. public int GetTopSurfaceZ(Point3D p) { if (this == Internal) { return p.Z; } var surfaceZ = int.MinValue; var lt = Tiles.GetLandTile(p.X, p.Y); if (!lt.Ignored) { var avgZ = GetAverageZ(p.X, p.Y); if (avgZ <= p.Z) { surfaceZ = avgZ; if (surfaceZ == p.Z) { return surfaceZ; } } } foreach (var tile in Tiles.GetStaticAndMultiTiles(p.X, p.Y)) { var id = TileData.ItemTable[tile.ID & TileData.MaxItemValue]; if (id.Surface || id.Wet) { var tileZ = tile.Z + id.CalcHeight; if (tileZ > surfaceZ && tileZ <= p.Z) { surfaceZ = tileZ; if (surfaceZ == p.Z) { return surfaceZ; } } } } var sector = GetSector(p.X, p.Y); foreach (var item in sector.Items) { if (item is BaseMulti || item.ItemID > TileData.MaxItemValue || !item.AtWorldPoint(p.X, p.Y) || item.Movable) { continue; } var id = item.ItemData; if (id.Surface || id.Wet) { var itemZ = item.Z + id.CalcHeight; if (itemZ > surfaceZ && itemZ <= p.Z) { surfaceZ = itemZ; if (surfaceZ == p.Z) { return surfaceZ; } } } } // If no surface found below, return the original Z return surfaceZ == int.MinValue ? p.Z : surfaceZ; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Bound(int x, int y, out int newX, out int newY) { newX = Math.Clamp(x, 0, Width - 1); newY = Math.Clamp(y, 0, Height - 1); } public Point2D Bound(Point3D p) { Bound(p.m_X, p.m_Y, out var x, out var y); return new Point2D(x, y); } public Point2D Bound(Point2D p) { Bound(p.m_X, p.m_Y, out var x, out var y); return new Point2D(x, y); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CalculateSectors( Rectangle2D bounds, out int sectorStartX, out int sectorStartY, out int sectorEndX, out int sectorEndY) { var left = bounds.Start.X; var top = bounds.Start.Y; var right = bounds.End.X; var bottom = bounds.End.Y; // Limit the coordinates to inside the valid map region Bound(left, top, out left, out top); Bound(right - 1, bottom - 1, out right, out bottom); // Calculate the top left sector sectorStartX = left >> SectorShift; sectorStartY = top >> SectorShift; // Calculate the bottom right sector. sectorEndX = right >> SectorShift; sectorEndY = bottom >> SectorShift; } public void ActivateSectors(int cx, int cy) { for (var x = cx - SectorActiveRange; x <= cx + SectorActiveRange; ++x) { for (var y = cy - SectorActiveRange; y <= cy + SectorActiveRange; ++y) { var sect = GetRealSector(x, y); if (sect != _invalidSector) { sect.Activate(); } } } } public void DeactivateSectors(int cx, int cy) { for (var x = cx - SectorActiveRange; x <= cx + SectorActiveRange; ++x) { for (var y = cy - SectorActiveRange; y <= cy + SectorActiveRange; ++y) { var sect = GetRealSector(x, y); if (sect != _invalidSector && !PlayersInRange(sect, SectorActiveRange)) { sect.Deactivate(); } } } } private bool PlayersInRange(Sector sect, int range) { for (var x = sect.X - range; x <= sect.X + range; ++x) { for (var y = sect.Y - range; y <= sect.Y + range; ++y) { var check = GetRealSector(x, y); if (check != _invalidSector && check.Clients.Count > 0) { return true; } } } return false; } public void OnClientChange(NetState oldState, NetState newState, Mobile m) { if (this != Internal) { GetSector(m.Location).OnClientChange(oldState, newState); } } internal void OnEnter(Mobile m) { OnEnter(m.Location, m); } internal void OnEnter(Point3D p, Mobile m) { if (this != Internal) { GetSector(p).OnEnter(m); } } internal void OnEnter(Item item) { OnEnter(item.Location, item); } internal void OnEnter(Point3D p, Item item) { if (this == Internal || item.Parent != null) { return; } GetSector(p).OnEnter(item); if (item is BaseMulti m) { var mcl = m.Components; var start = GetMultiMinSector(m.Location, mcl); var end = GetMultiMaxSector(m.Location, mcl); AddMulti(m, start, end); } } internal void OnLeave(Mobile m) { OnLeave(m.Location, m); } internal void OnLeave(Point3D p, Mobile m) { if (this != Internal) { GetSector(p).OnLeave(m); } } internal void OnLeave(Item item) { OnLeave(item.Location, item); } internal void OnLeave(Point3D p, Item item) { if (this == Internal || item.Parent != null) { return; } GetSector(p).OnLeave(item); if (item is BaseMulti m) { var mcl = m.Components; var start = GetMultiMinSector(m.Location, mcl); var end = GetMultiMaxSector(m.Location, mcl); RemoveMulti(m, start, end); } } private void RemoveMulti(BaseMulti m, Sector start, Sector end) { if (this == Internal) { return; } for (var x = start.X; x <= end.X; ++x) { for (var y = start.Y; y <= end.Y; ++y) { InternalGetSector(x, y).OnMultiLeave(m); } } } private void AddMulti(BaseMulti m, Sector start, Sector end) { if (this == Internal) { return; } for (var x = start.X; x <= end.X; ++x) { for (var y = start.Y; y <= end.Y; ++y) { InternalGetSector(x, y).OnMultiEnter(m); } } } public Sector GetMultiMinSector(Point3D loc, MultiComponentList mcl) => GetSector(Bound(new Point2D(loc.m_X + mcl.Min.m_X, loc.m_Y + mcl.Min.m_Y))); public Sector GetMultiMaxSector(Point3D loc, MultiComponentList mcl) => GetSector(Bound(new Point2D(loc.m_X + mcl.Max.m_X, loc.m_Y + mcl.Max.m_Y))); public void OnMove(Point3D oldLocation, Mobile m) { if (this == Internal) { return; } var oldSector = GetSector(oldLocation); var newSector = GetSector(m.Location); if (oldSector != newSector) { oldSector.OnLeave(m); newSector.OnEnter(m); } } public void OnMove(Point3D oldLocation, Item item) { if (this == Internal) { return; } var oldSector = GetSector(oldLocation); var newSector = GetSector(item.Location); if (oldSector != newSector) { oldSector.OnLeave(item); newSector.OnEnter(item); } if (item is BaseMulti m) { var mcl = m.Components; var start = GetMultiMinSector(m.Location, mcl); var end = GetMultiMaxSector(m.Location, mcl); var oldStart = GetMultiMinSector(oldLocation, mcl); var oldEnd = GetMultiMaxSector(oldLocation, mcl); if (oldStart != start || oldEnd != end) { RemoveMulti(m, oldStart, oldEnd); AddMulti(m, start, end); } } } public void RegisterRegion(Region reg) { var regName = reg.Name; if (regName == null) { return; } if (Regions.ContainsKey(regName)) { logger.Warning("Duplicate region name '{RegionName}' for map '{MapName}'", regName, Name); } else { Regions[regName] = reg; } } public void UnregisterRegion(Region reg) { var regName = reg.Name; if (regName != null) { Regions.Remove(regName); } } public Point3D GetPoint(object o, bool eye) { Point3D p; if (o is Mobile mobile) { p = mobile.Location; p.Z += 14; // eye ? 15 : 10; } else if (o is Item item) { // Calculate the height based on the container, not the item inside. var rootParent = item.RootParent; if (rootParent != null) { p = GetPoint(rootParent, eye); } else { p = item.GetWorldLocation(); p.Z += item.ItemData.Height / 2 + 1; } } else if (o is Point3D point3D) { p = point3D; } else if (o is LandTarget target) { p = target.Location; GetAverageZ(p.X, p.Y, out _, out _, out var top); p.Z = top + 1; } else if (o is StaticTarget st) { var id = TileData.ItemTable[st.ItemID & TileData.MaxItemValue]; p = new Point3D(st.X, st.Y, st.Z - id.CalcHeight + id.Height / 2 + 1); } else if (o is IPoint3D d) { p = new Point3D(d.X, d.Y, d.Z); } else { logger.Warning("Warning: Invalid object ({Object}) in line of sight", o); p = Point3D.Zero; } return p; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CanFit( Point3D p, int height, bool checkBlocksFit = false, bool checkMobiles = true, bool requireSurface = true ) => CanFit(p.m_X, p.m_Y, p.m_Z, height, checkBlocksFit, checkMobiles, requireSurface); [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CanFit( Point2D p, int z, int height, bool checkBlocksFit = false, bool checkMobiles = true, bool requireSurface = true ) => CanFit(p.m_X, p.m_Y, z, height, checkBlocksFit, checkMobiles, requireSurface); public bool CanFit( int x, int y, int z, int height, bool checkBlocksFit = false, bool checkMobiles = true, bool requireSurface = true ) { if (this == Internal) { return false; } if (x < 0 || y < 0 || x >= Width || y >= Height) { return false; } var hasSurface = false; var lt = Tiles.GetLandTile(x, y); GetAverageZ(x, y, out var lowZ, out var avgZ, out _); var landFlags = TileData.LandTable[lt.ID & TileData.MaxLandValue].Flags; if ((landFlags & TileFlag.Impassable) != 0 && avgZ > z && z + height > lowZ) { return false; } if ((landFlags & TileFlag.Impassable) == 0 && z == avgZ && !lt.Ignored) { hasSurface = true; } bool surface, impassable; foreach (var tile in Tiles.GetStaticAndMultiTiles(x, y)) { var id = TileData.ItemTable[tile.ID & TileData.MaxItemValue]; surface = id.Surface; impassable = id.Impassable; if ((surface || impassable) && tile.Z + id.CalcHeight > z && z + height > tile.Z) { return false; } if (surface && !impassable && z == tile.Z + id.CalcHeight) { hasSurface = true; } } var sector = GetSector(x, y); foreach (var item in sector.Items) { if (item is BaseMulti || item.ItemID > TileData.MaxItemValue || !item.AtWorldPoint(x, y)) { continue; } var id = item.ItemData; surface = id.Surface; impassable = id.Impassable; if ((surface || impassable || checkBlocksFit && item.BlocksFit) && item.Z + id.CalcHeight > z && z + height > item.Z) { return false; } if (surface && !impassable && !item.Movable && z == item.Z + id.CalcHeight) { hasSurface = true; } } if (checkMobiles) { foreach (var m in sector.Mobiles) { if (m.Location.m_X == x && m.Location.m_Y == y && (m.AccessLevel == AccessLevel.Player || !m.Hidden) && m.Z + 16 > z && z + height > m.Z) { return false; } } } return !requireSurface || hasSurface; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CanFitItem(Point3D p, int height) => CanFitItem(p.m_X, p.m_Y, p.m_Z, height); [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CanFitItem(Point2D p, int z, int height) => CanFitItem(p.m_X, p.m_Y, z, height); /// /// Checks if an item can be placed at the specified location. /// Unlike CanFit, this treats Surface+Impassable tiles (tables, furniture) as valid surfaces, /// matching the behavior of item drop logic. /// public bool CanFitItem(int x, int y, int z, int height) { if (this == Internal || x < 0 || y < 0 || x >= Width || y >= Height) { return false; } var hasSurface = false; var lt = Tiles.GetLandTile(x, y); GetAverageZ(x, y, out var lowZ, out var avgZ, out _); var landFlags = TileData.LandTable[lt.ID & TileData.MaxLandValue].Flags; // Impassable land still blocks items if ((landFlags & TileFlag.Impassable) != 0 && avgZ > z && z + height > lowZ) { return false; } // Passable land is a valid surface if ((landFlags & TileFlag.Impassable) == 0 && z == avgZ && !lt.Ignored) { hasSurface = true; } foreach (var tile in Tiles.GetStaticAndMultiTiles(x, y)) { var id = TileData.ItemTable[tile.ID & TileData.MaxItemValue]; var surface = id.Surface; var impassable = id.Impassable; // Tiles block if item would intersect with them if ((surface || impassable) && tile.Z + id.CalcHeight > z && z + height > tile.Z) { return false; } // Surface tiles (including Surface+Impassable like tables) are valid surfaces for items if (surface && z == tile.Z + id.CalcHeight) { hasSurface = true; } } var sector = GetSector(x, y); foreach (var item in sector.Items) { if (item is BaseMulti || item.ItemID > TileData.MaxItemValue || !item.AtWorldPoint(x, y)) { continue; } var id = item.ItemData; var surface = id.Surface; var impassable = id.Impassable; // Items block if placement would intersect if ((surface || impassable) && item.Z + id.CalcHeight > z && z + height > item.Z) { return false; } // Surface items (including Surface+Impassable like tables) are valid surfaces // Must be non-movable to be a stable surface if (surface && !item.Movable && z == item.Z + id.CalcHeight) { hasSurface = true; } } return hasSurface; } public bool CanSpawnMobile(Point3D p) => CanSpawnMobile(p.m_X, p.m_Y, p.m_Z); public bool CanSpawnMobile(Point2D p, int z) => CanSpawnMobile(p.m_X, p.m_Y, z); public bool CanSpawnMobile(int x, int y, int z) => Region.Find(new Point3D(x, y, z), this).AllowSpawn() && CanFit(x, y, z, 16); /// /// Finds a valid spawn Z within the specified range by checking land, static, multi tiles, and world items. /// Prefers the lowest valid surface (ground/floor over tables/platforms). /// /// X coordinate /// Y coordinate /// Minimum Z (inclusive) /// Maximum Z (inclusive) /// Whether the spawned entity can swim (water surfaces valid) /// Whether the spawned entity cannot walk (water-only) /// The valid spawn Z if found /// True if a valid spawn Z was found within the range [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CanSpawnMobile(int x, int y, int minZ, int maxZ, bool canSwim, bool cantWalk, out int spawnZ) => CanSpawnMobile(x, y, minZ, maxZ, canSwim, cantWalk, out spawnZ, out _); /// /// Finds a valid spawn Z within the specified range by checking land, static, multi tiles, and world items. /// Prefers the lowest valid surface (ground/floor over tables/platforms). /// Also reports the reason for failure if no valid position is found. /// /// X coordinate /// Y coordinate /// Minimum Z (inclusive) /// Maximum Z (inclusive) /// Whether the spawned entity can swim (water surfaces valid) /// Whether the spawned entity cannot walk (water-only) /// The valid spawn Z if found /// Indicates why spawn failed (if it did) /// True if a valid spawn Z was found within the range public bool CanSpawnMobile(int x, int y, int minZ, int maxZ, bool canSwim, bool cantWalk, out int spawnZ, out SpawnFailureReason failureReason) { spawnZ = 0; failureReason = SpawnFailureReason.None; if (this == Internal || x < 0 || y < 0 || x >= Width || y >= Height) { failureReason = SpawnFailureReason.InvalidMap; return false; } if (!Region.Find(new Point3D(x, y, minZ), this).AllowSpawn()) { failureReason = SpawnFailureReason.RegionBlocked; return false; } // 256-bit bitmask approach for full Z range support (-128 to 127). // Each bit represents a Z level relative to minZ. // openSlots: bit set = Z level is not blocked // surfaces: bit set = Z level has a valid surface // Final result: lowest set bit in (surfaces & openSlots) var openSlots = BitMask256.AllSet(); var surfaces = BitMask256.AllClear(); // Track what types of blockers we encounter (for failure reason) var hasNonTransientBlocker = false; var hasTransientBlocker = false; // 1. Land tile var landTile = Tiles.GetLandTile(x, y); GetAverageZ(x, y, out var lowZ, out var avgZ, out _); if (!landTile.Ignored) { var landFlags = TileData.LandTable[landTile.ID & TileData.MaxLandValue].Flags; var isImpassable = (landFlags & TileFlag.Impassable) != 0; var isWet = (landFlags & TileFlag.Wet) != 0; // Impassable land blocks, except water tiles don't block swimming mobs if (isImpassable && !(canSwim && isWet)) { // Impassable land blocks z in range (lowZ - 16, avgZ) ApplyBlockerMask(ref openSlots, lowZ - 16, avgZ, minZ); hasNonTransientBlocker = true; } // Surface: water for swimmers, passable land for walkers if (avgZ >= minZ && avgZ <= maxZ && (canSwim && isWet || !cantWalk && !isImpassable)) { surfaces.SetBit(avgZ - minZ); } } // 2. Static and multi tiles (always non-transient) foreach (var tile in Tiles.GetStaticAndMultiTiles(x, y)) { var id = TileData.ItemTable[tile.ID & TileData.MaxItemValue]; var tileTop = tile.Z + id.CalcHeight; var isSurface = id.Surface; var isImpassable = id.Impassable; var isWet = id.Wet; // Blocking: (surface || impassable) tiles block z in range (tile.Z - 16, tileTop) // Exception: water tiles (Impassable | Wet) don't block swimming mobs if ((isSurface || isImpassable) && !(canSwim && isWet)) { ApplyBlockerMask(ref openSlots, tile.Z - 16, tileTop, minZ); hasNonTransientBlocker = true; } // Surface candidate if (tileTop >= minZ && tileTop <= maxZ && (canSwim && isWet || !cantWalk && isSurface && !isImpassable)) { surfaces.SetBit(tileTop - minZ); } } // 3. World items var sector = GetSector(x, y); foreach (var item in sector.Items) { if (item is BaseMulti || item.ItemID > TileData.MaxItemValue || !item.AtWorldPoint(x, y)) { continue; } var id = item.ItemData; var itemTop = item.Z + id.CalcHeight; var isSurface = id.Surface; var isImpassable = id.Impassable; var isWet = id.Wet; // Blocking: (surface || impassable) items block z in range (item.Z - 16, itemTop) // Exception: water items (Impassable | Wet) don't block swimming mobs if ((isSurface || isImpassable) && !(canSwim && isWet)) { ApplyBlockerMask(ref openSlots, item.Z - 16, itemTop, minZ); // Movable items are transient, non-movable are permanent if (item.Movable || item.CanDecay()) { hasTransientBlocker = true; } else { hasNonTransientBlocker = true; } } // Surface candidate (non-movable only) if (!item.Movable && itemTop >= minZ && itemTop <= maxZ && (canSwim && isWet || !cantWalk && isSurface && !isImpassable)) { surfaces.SetBit(itemTop - minZ); } } // 4. Mobiles (always transient blockers) foreach (var m in sector.Mobiles) { if (m.Location.m_X == x && m.Location.m_Y == y && (m.AccessLevel == AccessLevel.Player || !m.Hidden)) { // Mobiles block z in range (m.Z - 16, m.Z + 16) ApplyBlockerMask(ref openSlots, m.Z - 16, m.Z + 16, minZ); hasTransientBlocker = true; } } // Find the lowest unblocked surface var validSurfaces = surfaces.And(in openSlots); var lowestBit = validSurfaces.LowestSetBit(); if (lowestBit < 0) { // Determine failure reason based on what blockers we encountered // If no surfaces existed at all, that's also a non-transient issue (map geometry) if (hasNonTransientBlocker || surfaces.IsEmpty()) { failureReason |= SpawnFailureReason.NonTransientBlocker; } if (hasTransientBlocker) { failureReason |= SpawnFailureReason.TransientBlocker; } return false; } spawnZ = minZ + lowestBit; return true; } /// /// Finds a valid spawn Z for items within the specified range. /// Unlike CanSpawnMobile, treats Surface+Impassable tiles (tables, furniture) as valid surfaces. /// /// X coordinate /// Y coordinate /// Minimum Z (inclusive) /// Maximum Z (inclusive) /// The valid spawn Z if found /// True if a valid spawn Z was found within the range public bool CanSpawnItem(int x, int y, int minZ, int maxZ, out int spawnZ) { spawnZ = 0; if (this == Internal || x < 0 || y < 0 || x >= Width || y >= Height) { return false; } if (!Region.Find(new Point3D(x, y, minZ), this).AllowSpawn()) { return false; } // 256-bit bitmask approach for full Z range support. // Unlike CanSpawnMobile, Surface+Impassable tiles (tables) are valid surfaces for items. var openSlots = BitMask256.AllSet(); var surfaces = BitMask256.AllClear(); // 1. Land tile var landTile = Tiles.GetLandTile(x, y); GetAverageZ(x, y, out var lowZ, out var avgZ, out _); if (!landTile.Ignored) { var landFlags = TileData.LandTable[landTile.ID & TileData.MaxLandValue].Flags; var isImpassable = (landFlags & TileFlag.Impassable) != 0; // Impassable land blocks if (isImpassable) { ApplyBlockerMask(ref openSlots, lowZ - 16, avgZ, minZ); } // Passable land is a valid surface if (!isImpassable && avgZ >= minZ && avgZ <= maxZ) { surfaces.SetBit(avgZ - minZ); } } // 2. Static and multi tiles foreach (var tile in Tiles.GetStaticAndMultiTiles(x, y)) { var id = TileData.ItemTable[tile.ID & TileData.MaxItemValue]; var tileTop = tile.Z + id.CalcHeight; var isSurface = id.Surface; var isImpassable = id.Impassable; // Blocking: (surface || impassable) tiles block z in range (tile.Z - 16, tileTop) if (isSurface || isImpassable) { ApplyBlockerMask(ref openSlots, tile.Z - 16, tileTop, minZ); } // Surface candidate: Surface flag (including Surface+Impassable like tables) if (isSurface && tileTop >= minZ && tileTop <= maxZ) { surfaces.SetBit(tileTop - minZ); } } // 3. World items var sector = GetSector(x, y); foreach (var item in sector.Items) { if (item is BaseMulti || item.ItemID > TileData.MaxItemValue || !item.AtWorldPoint(x, y)) { continue; } var id = item.ItemData; var itemTop = item.Z + id.CalcHeight; var isSurface = id.Surface; var isImpassable = id.Impassable; // Blocking: (surface || impassable) items block if (isSurface || isImpassable) { ApplyBlockerMask(ref openSlots, item.Z - 16, itemTop, minZ); } // Surface candidate: non-movable Surface items (including Surface+Impassable) if (!item.Movable && isSurface && itemTop >= minZ && itemTop <= maxZ) { surfaces.SetBit(itemTop - minZ); } } // Find the lowest unblocked surface var validSurfaces = surfaces.And(in openSlots); var lowestBit = validSurfaces.LowestSetBit(); if (lowestBit < 0) { return false; } spawnZ = minZ + lowestBit; return true; } /// /// Clears bits in the mask for Z levels blocked by an object. /// Converts (blockLow, blockHigh) exclusive world Z range to bit indices relative to minZ. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ApplyBlockerMask(ref BitMask256 mask, int blockLow, int blockHigh, int minZ) { // Blocked range is (blockLow, blockHigh) exclusive = [blockLow + 1, blockHigh - 1] inclusive var startBit = blockLow - minZ + 1; var endBit = blockHigh - minZ - 1; if (endBit < 0 || startBit > 255 || startBit > endBit) { return; } mask.ClearRange(Math.Max(0, startBit), Math.Min(255, endBit)); } private class ZComparer : IComparer { public static readonly ZComparer Default = new(); public int Compare(Item x, Item y) => x!.Z.CompareTo(y!.Z); } public Sector GetSector(Point3D p) => InternalGetSector(p.m_X >> SectorShift, p.m_Y >> SectorShift); public Sector GetSector(Point2D p) => InternalGetSector(p.m_X >> SectorShift, p.m_Y >> SectorShift); public Sector GetSector(int x, int y) => InternalGetSector(x >> SectorShift, y >> SectorShift); public Sector GetRealSector(int x, int y) => InternalGetSector(x, y); private Sector InternalGetSector(int x, int y) { if (x >= 0 && x < m_SectorsWidth && y >= 0 && y < m_SectorsHeight) { var xSectors = m_Sectors[x]; if (xSectors == null) { m_Sectors[x] = xSectors = new Sector[m_SectorsHeight]; } var sec = xSectors[y]; if (sec == null) { xSectors[y] = sec = new Sector(x, y, this); } return sec; } return _invalidSector; } public bool LineOfSight(Point3D origin, Point3D destination) { if (this == Internal) { return false; } if (!Utility.InRange(origin, destination, MaxLOSDistance)) { return false; } if (origin == destination) { return true; } var end = destination; if (origin.X > destination.X || origin.X == destination.X && origin.Y > destination.Y || origin.X == destination.X && origin.Y == destination.Y && origin.Z > destination.Z) { (origin, destination) = (destination, origin); } var path = new Point3DList(); var xd = destination.X - origin.X; var yd = destination.Y - origin.Y; var zd = destination.Z - origin.Z; var zslp = Math.Sqrt(xd * xd + yd * yd); var sq3d = zd != 0 ? Math.Sqrt(zslp * zslp + zd * zd) : zslp; var rise = yd / sq3d; var run = xd / sq3d; zslp = zd / sq3d; double y = origin.Y; double z = origin.Z; double x = origin.X; while (Utility.NumberBetween(x, destination.X, origin.X, 0.5) && Utility.NumberBetween(y, destination.Y, origin.Y, 0.5) && Utility.NumberBetween(z, destination.Z, origin.Z, 0.5)) { var ix = (int)Math.Round(x); var iy = (int)Math.Round(y); var iz = (int)Math.Round(z); if (path.Count > 0) { var p = path.Last; if (p.X != ix || p.Y != iy || p.Z != iz) { path.Add(ix, iy, iz); } } else { path.Add(ix, iy, iz); } x += run; y += rise; z += zslp; } if (path.Count == 0) { return true; // <--should never happen, but to be safe. } if (path.Last != destination) { path.Add(destination); } var pathCount = path.Count; var endTop = end.Z + 1; for (var i = 0; i < pathCount; ++i) { var point = path[i]; var pointTop = point.Z + 1; var landTile = Tiles.GetLandTile(point.X, point.Y); GetAverageZ(point.X, point.Y, out var landZ, out _, out var landTop); if (landZ <= pointTop && landTop >= point.m_Z && (point.X != end.X || point.Y != end.Y || landZ > endTop || landTop < end.Z) && !landTile.Ignored) { return false; } /* --Do land tiles need to be checked? There is never land between two people, always statics.-- LandTile landTile = Tiles.GetLandTile( point.X, point.Y ); if (landTile.Z-1 >= point.Z && landTile.Z+1 <= point.Z && (TileData.LandTable[landTile.ID & TileData.MaxLandValue].Flags & TileFlag.Impassable) != 0) return false; */ var contains = false; var ltID = landTile.ID; for (var j = 0; !contains && j < InvalidLandTiles.Length; ++j) { contains = ltID == InvalidLandTiles[j]; } var foundStatic = false; foreach (var t in Tiles.GetStaticAndMultiTiles(point.X, point.Y)) { foundStatic = true; var id = TileData.ItemTable[t.ID & TileData.MaxItemValue]; var flags = id.Flags; if ( t.Z <= pointTop && t.Z + id.CalcHeight >= point.Z && (flags & (TileFlag.Window | TileFlag.NoShoot)) != 0 && (point.X != end.X || point.Y != end.Y || t.Z > endTop || t.Z + id.CalcHeight < end.Z) ) { return false; } } if (contains && !foundStatic) { foreach (var item in GetItemsAt(point)) { if (item.Visible) { contains = false; break; } } if (contains) { return false; } } } var pTop = origin; var pBottom = destination; Utility.FixPoints(ref pTop, ref pBottom); var rect = new Rectangle2D(pTop.X, pTop.Y, pBottom.X - pTop.X + 1, pBottom.Y - pTop.Y + 1); foreach (var item in GetItemsInBounds(rect)) { if (!item.Visible) { continue; } if (item is BaseMulti || item.ItemID > TileData.MaxItemValue) { continue; } var id = item.ItemData; var flags = id.Flags; if ((flags & (TileFlag.Window | TileFlag.NoShoot)) == 0) { continue; } for (var i = 0; i < path.Count; ++i) { var pathPoint = path[i]; var pointTop = pathPoint.Z + 1; var itemLocation = item.Location; if ( // Item is on same tile as this point along the LOS path itemLocation.X == pathPoint.X && itemLocation.Y == pathPoint.Y && itemLocation.Z <= pointTop && // Item rests on the same level as the path itemLocation.Z + id.CalcHeight >= pathPoint.Z && // Fix door bugging monsters when door is at the START or END of the LOS path by allowing LOS !(flags.HasFlag(TileFlag.Door) && itemLocation.X == origin.X && itemLocation.Y == origin.Y || itemLocation.X == destination.X && itemLocation.Y == destination.Y) && // Item is at some point along the path BEFORE the target (itemLocation.X != end.X || itemLocation.Y != end.Y || // Item is diagonally looking DOWN at the target itemLocation.Z > endTop || // Item is diagonally looking UP at the target itemLocation.Z + id.CalcHeight < end.Z) ) { return false; } } } return true; } public bool LineOfSight(object from, object dest) => from == dest || (from as Mobile)?.AccessLevel > AccessLevel.Player || (dest as Item)?.RootParent == from || LineOfSight(GetPoint(from, true), GetPoint(dest, false)); public bool LineOfSight(Mobile from, Point3D target) { if (from.AccessLevel > AccessLevel.Player) { return true; } var eye = from.Location; eye.Z += 14; return LineOfSight(eye, target); } public bool LineOfSight(Mobile from, Mobile to) { if (from == to || from.AccessLevel > AccessLevel.Player) { return true; } var eye = from.Location; var target = to.Location; eye.Z += 14; target.Z += 14; // 10; return LineOfSight(eye, target); } public Point3D GetRandomNearbyLocation( Point3D loc, int maxRange = 2, int minRange = 0, int retryCount = 10, int height = 16, bool checkBlocksFit = false, bool checkMobiles = false ) { var j = 0; var range = maxRange - minRange; var locs = range <= 10 ? new bool[range + 1, range + 1] : null; do { var xRand = Utility.Random(range); var yRand = Utility.Random(range); if (locs?[xRand, yRand] != true) { var x = loc.X + xRand + minRange; var y = loc.Y + yRand + minRange; if (CanFit(x, y, loc.Z, height, checkBlocksFit, checkMobiles)) { loc = new Point3D(x, y, loc.Z); break; } var z = GetAverageZ(x, y); if (CanFit(x, y, z, height, checkBlocksFit, checkMobiles)) { loc = new Point3D(x, y, z); break; } if (locs != null) { locs[xRand, yRand] = true; } } j++; } while (j < retryCount); return loc; } #pragma warning restore CA1000 // Do not declare static members on generic types [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Map Parse(string s) => Parse(s, null); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Map Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryParse(string s, IFormatProvider provider, out Map result) => TryParse(s.AsSpan(), provider, out result); public static Map Parse(ReadOnlySpan s, IFormatProvider provider) { s = s.Trim(); if (s.Length == 0) { throw new FormatException($"The input string '{s}' was not in a correct format."); } if (s.InsensitiveEquals("Internal")) { return Internal; } if (!int.TryParse(s, provider, out var index)) { index = -1; } else if (index == 127) { return Internal; } for (var i = 0; i < Maps.Length; i++) { var map = Maps[i]; if (map == null) { continue; } if (index >= 0 && map.MapIndex == index || s.InsensitiveEquals(map.Name)) { return map; } } throw new FormatException($"The input string '{s}' was not in a correct format."); } public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, out Map result) { s = s.Trim(); if (s.Length == 0) { result = default; return false; } if (s.InsensitiveEquals("Internal")) { result = Internal; return true; } if (!int.TryParse(s, provider, out var index)) { index = -1; } else if (index == 127) { result = Internal; return true; } for (var i = 0; i < Maps.Length; i++) { var map = Maps[i]; if (map == null) { continue; } if (index >= 0 && map.MapIndex == index || s.InsensitiveEquals(map.Name)) { result = map; return true; } } result = default; return false; } public class Sector { // TODO: Can we avoid this? private static readonly List m_DefaultRectList = new(); private static readonly List m_DefaultMultiList = new(); private bool m_Active; private ValueLinkList _clients; private ValueLinkList _items; private ValueLinkList _mobiles; private List _multis; private int _multisVersion; private List _regions; public Sector(int x, int y, Map owner) { X = x; Y = y; Owner = owner; m_Active = false; } public List Regions => _regions ?? m_DefaultRectList; internal List Multis => _multis ?? m_DefaultMultiList; // Cheap public "does this sector currently contain any multi" check. MultisVersion can't // answer this (it counts enter AND leave, so a place-then-remove leaves it non-zero with // zero multis). Used by the pathfinding step cache to route multi-covered cells to the // live movement path instead of the static-only chunk cache. public bool HasMultis => _multis is { Count: > 0 }; public int MultisVersion => _multisVersion; internal ref readonly ValueLinkList Mobiles => ref _mobiles; internal ref readonly ValueLinkList Items => ref _items; internal ref readonly ValueLinkList Clients => ref _clients; public bool Active => m_Active && Owner != Internal; public Map Owner { get; } public int X { get; } public int Y { get; } public void OnClientChange(NetState oldState, NetState newState) { var count = _clients.Count; if (oldState != null) { _clients.Remove(oldState); } if (newState != null) { _clients.AddLast(newState); } if (_clients.Count == 0 && count > 0) { Owner.DeactivateSectors(X, Y); } else if (count == 0 && _clients.Count > 0) { Owner.ActivateSectors(X, Y); } } public void OnEnter(Item item) { _items.AddLast(item); } public void OnLeave(Item item) { _items.Remove(item); } public void OnEnter(Mobile mob) { _mobiles.AddLast(mob); if (mob.NetState != null) { _clients.AddLast(mob.NetState); Owner.ActivateSectors(X, Y); } } public void OnLeave(Mobile mob) { _mobiles.Remove(mob); if (mob.NetState != null) { _clients.Remove(mob.NetState); Owner.DeactivateSectors(X, Y); } } public void OnEnter(Region region, Rectangle3D rect) { if (_regions?.Contains(region) == true) { return; } Utility.Add(ref _regions, region); _regions.Sort(); UpdateMobileRegions(); } public void OnLeave(Region region) { if (_regions != null) { for (var i = _regions.Count - 1; i >= 0; i--) { var r = _regions[i]; if (r == region) { _regions.RemoveAt(i); break; } } if (_regions.Count == 0) { _regions = null; } } UpdateMobileRegions(); } private void UpdateMobileRegions() { if (_mobiles.Count > 0) { using var queue = PooledRefQueue.Create(_mobiles.Count); foreach (var mob in _mobiles) { queue.Enqueue(mob); } while (queue.Count > 0) { queue.Dequeue().UpdateRegion(); } } } public void OnMultiEnter(BaseMulti multi) { _multis ??= new List(); _multis.Add(multi); _multisVersion++; } public void OnMultiLeave(BaseMulti multi) { if (_multis?.Remove(multi) == true) { _multisVersion++; } } public void Activate() { if (!Active) { foreach (var item in _items) { item.OnSectorActivate(); } foreach (var mob in _mobiles) { mob.OnSectorActivate(); } m_Active = true; } } public void Deactivate() { if (Active) { foreach (var item in _items) { item.OnSectorDeactivate(); } foreach (var mob in _mobiles) { mob.OnSectorDeactivate(); } m_Active = false; } } } }