[Maps] mangoszero map compatibility [Claude]#100
Conversation
Add MapFileFormats.cs with binary struct definitions matching MangosZero's map extractor output, enabling MangosSharp to read maps extracted with MangosZero tools. Includes: - MapFileHeader (GridMapFileHeader) with holes data support - MapAreaHeader, MapHeightHeader, MapLiquidHeader section headers - MmapTileHeader matching MoveMapSharedDefines.h (20-byte layout) - Format detection and filename helpers for both legacy and MangosZero naming conventions https://claude.ai/code/session_0194PFFCeXmh82deGbisQtP2
Port MangosZero's GridMap class to C# for reading .map files extracted by MangosZero tools. The implementation includes: - GridMap.cs: Full port of MangosZero's GridMap with LoadData supporting all height storage formats (float, uint16, uint8, flat), area data, liquid data with type/entry/height maps, and holes bitmask. Height queries use proper 4-triangle decomposition per cell matching the C++ implementation exactly. - TMapTile auto-detection: On load, checks if a .map file starts with the "MAPS" magic number. MangosZero-format files delegate all queries to the GridMap instance; legacy MangosSharp-format files continue to load as before. - WS_Maps query delegation: GetZCoord, GetWaterLevel, GetTerrainType, and GetAreaFlag all check for a MangosZeroGridMap on the tile and delegate appropriately, preserving VMAP fallback behavior. - MapFileFormats version magic updated to "z1.5" (MangosZero current) with backward compatibility for "v9.0" format. https://claude.ai/code/session_0194PFFCeXmh82deGbisQtP2
- Remove all legacy MangosSharp map format handling from TMapTile and WS_Maps, making GridMap (MangosZero format) the sole map reader - Remove legacy arrays (ZCoord, AreaFlag, AreaTerrain, WaterLevel), legacy resolution config (RESOLUTION_ZMAP), legacy interpolation, and legacy filename format (D3/XY -> D4/YX MangosZero format) - Implement VMap system for line-of-sight and collision detection: - BIH (Bounding Interval Hierarchy) spatial acceleration structure - WorldModel/GroupModel with Moller-Trumbore triangle intersection - ModelInstance with world-to-model coordinate transforms - StaticMapTree for per-map model instance management - VMapManager for LoS, height, and hit-position queries - Implement MMap pathfinding system using Detour navmesh: - DtNavMesh with tile loading and polygon spatial queries - DtNavMeshQuery with FindNearestPoly, FindPath, FindStraightPath - PathInfo for high-level pathfinding requests - MMapManager for navmesh tile loading and path computation - Wire VMap/MMap into WS_Maps: IsInLineOfSight, GetVMapHeight, and GetObjectHitPos now delegate to VMapManager when enabled - Clean up MapFileFormats: remove GetLegacyMapFileName https://claude.ai/code/session_0194PFFCeXmh82deGbisQtP2
There was a problem hiding this comment.
Pull request overview
This PR updates the world server’s map subsystem to be compatible with MaNGOSZero-style extracted data by replacing legacy per-tile arrays and resolution configuration with GridMap-based queries, and by introducing initial VMap (LOS/height/collision) and MMap (navmesh/pathfinding) manager implementations.
Changes:
- Removed legacy ZMap resolution configuration and switched terrain queries to MaNGOSZero GridMap (height/area/liquid/terrain).
- Added VMap and MMap manager implementations and wiring points in
WS_Maps. - Updated map tile loading to use MaNGOSZero filename conventions and added tile-fraction logic for neighbor-tile loading.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/server/Mangos.World/WorldServer.cs | Removes legacy ZMap resolution configuration usage. |
| src/server/Mangos.World/Maps/WS_Maps.cs | Introduces VMap/MMap managers; rewrites coordinate math and terrain/LOS APIs to use GridMap/VMap fallbacks. |
| src/server/Mangos.World/Maps/WS_Maps.TMapTile.cs | Replaces legacy tile arrays with GridMap loading using MaNGOSZero filenames. |
| src/server/Mangos.World/Maps/VMapManager.cs | Adds VMap manager for LOS/height/object-hit queries. |
| src/server/Mangos.World/Maps/VMap/* | Adds core VMap structures (BIH, StaticMapTree, models, math) for raycasting/height. |
| src/server/Mangos.World/Maps/MapFileFormats.cs | Adds MaNGOSZero-compatible filename/format constants and headers. |
| src/server/Mangos.World/Maps/MMapManager.cs | Adds MMap manager scaffolding for navmesh loading and pathfinding. |
| src/server/Mangos.World/Maps/MMap/* | Adds simplified Detour-like navmesh/query/path types. |
| src/server/Mangos.World/Maps/GridMap.cs | Adds GridMap reader/query implementation for MaNGOSZero .map files. |
| src/server/Mangos.World/Handlers/WS_CharMovement.cs | Switches neighbor-tile selection logic to use fractional tile position. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (nodeIndex >= m_tree.Length) | ||
| return 1; | ||
| var node = m_tree[nodeIndex]; | ||
| var nodeType = node & BIH_AXIS_MASK; | ||
| if (nodeType == BIH_LEAF) | ||
| { | ||
| return 1 + (int)(node >> 2); // leaf header + primitive indices | ||
| } | ||
| return 3; // internal node: header + 2 split planes |
There was a problem hiding this comment.
BIH.GetNodeSize returns a constant size (3) for internal nodes, but it is used to compute farChild from childOffset. For non-trivial trees this will point traversal at the wrong node index, leading to incorrect/unstable ray intersection results (LOS, collision, height). Either compute subtree sizes correctly (recursive), or adjust the child-offset logic to match the on-disk BIH encoding (e.g., if the offset already points to the right child, you shouldn’t be adding a computed node size).
| if (nodeIndex >= m_tree.Length) | |
| return 1; | |
| var node = m_tree[nodeIndex]; | |
| var nodeType = node & BIH_AXIS_MASK; | |
| if (nodeType == BIH_LEAF) | |
| { | |
| return 1 + (int)(node >> 2); // leaf header + primitive indices | |
| } | |
| return 3; // internal node: header + 2 split planes | |
| // Return the total number of uint entries used by the subtree rooted at nodeIndex. | |
| // This is used to compute child indices during traversal, so it must reflect | |
| // the actual serialized layout, not a constant size. | |
| if (nodeIndex < 0 || m_tree == null || nodeIndex >= m_tree.Length) | |
| return 0; | |
| var node = m_tree[nodeIndex]; | |
| var nodeType = node & BIH_AXIS_MASK; | |
| if (nodeType == BIH_LEAF) | |
| { | |
| // Leaf layout: [header][primitive0][primitive1]...[primitiveN-1] | |
| var count = (int)(node >> 2); | |
| // header (1) + primitive indices (count) | |
| var size = 1 + count; | |
| // Clamp to remaining array length to be robust against corrupt data. | |
| var maxSize = m_tree.Length - nodeIndex; | |
| return size > maxSize ? maxSize : size; | |
| } | |
| // Internal node layout (depth-first): | |
| // [header][splitPlane0][splitPlane1][leftSubtree...][rightSubtree...] | |
| // childOffset (node >> 2) encodes the offset from the left child to the right child. | |
| var childOffset = (int)(node >> 2); | |
| // Left child starts immediately after the header and two split planes. | |
| var leftChild = nodeIndex + 3; | |
| if (leftChild >= m_tree.Length) | |
| return 3; // header + 2 split planes only; children are out of range | |
| // Right child is located childOffset entries after the left child. | |
| var rightChild = leftChild + childOffset; | |
| if (rightChild < 0 || rightChild >= m_tree.Length) | |
| { | |
| // If the encoded offset is invalid, fall back to counting only the left subtree. | |
| var leftSizeOnly = GetNodeSize(leftChild); | |
| return 3 + leftSizeOnly; | |
| } | |
| var leftSize = GetNodeSize(leftChild); | |
| var rightSize = GetNodeSize(rightChild); | |
| return 3 + leftSize + rightSize; |
| /// </summary> | ||
| public static bool ExistMap(string mapsDir, uint mapId, int gx, int gy) | ||
| { | ||
| string filename = Path.Combine(mapsDir, $"{mapId:D3}{gx:D2}{gy:D2}.map"); |
There was a problem hiding this comment.
GridMap.ExistMap builds filenames as $"{mapId:D3}{gx:D2}{gy:D2}.map", which doesn’t match the MangosZero naming used elsewhere in this PR (MapFileFormats.GetMapFileName uses 4-digit mapId and Y,X order). As written, ExistMap will return false for valid MangosZero-format map tiles. Consider delegating to MapFileFormats.GetMapFileName (and aligning parameter order) to keep the naming consistent.
| string filename = Path.Combine(mapsDir, $"{mapId:D3}{gx:D2}{gy:D2}.map"); | |
| string filename = Path.Combine(mapsDir, MapFileFormats.GetMapFileName(mapId, gy, gx)); |
| /// <summary> | ||
| /// Gets the area flag at the given world coordinates. | ||
| /// </summary> | ||
| public int GetAreaFlag(float x, float y, int Map) | ||
| { | ||
| x = ValidateMapCoord(x); | ||
| y = ValidateMapCoord(y); | ||
| checked | ||
| var MapTileX = checked((byte)(32f - (x / SIZE_OF_GRIDS))); | ||
| var MapTileY = checked((byte)(32f - (y / SIZE_OF_GRIDS))); | ||
|
|
||
| if (!Maps.ContainsKey((uint)Map) || Maps[(uint)Map].Tiles[MapTileX, MapTileY] == null) | ||
| { | ||
| var MapTileX = (byte)(32f - (x / WorldServiceLocator.GlobalConstants.SIZE)); | ||
| var MapTileY = (byte)(32f - (y / WorldServiceLocator.GlobalConstants.SIZE)); | ||
| var MapTile_LocalX = (byte)Math.Round(WorldServiceLocator.GlobalConstants.RESOLUTION_FLAGS * (32f - (x / WorldServiceLocator.GlobalConstants.SIZE) - MapTileX)); | ||
| var MapTile_LocalY = (byte)Math.Round(WorldServiceLocator.GlobalConstants.RESOLUTION_FLAGS * (32f - (y / WorldServiceLocator.GlobalConstants.SIZE) - MapTileY)); | ||
| return Maps[(uint)Map].Tiles[MapTileX, MapTileY] == null | ||
| ? 0 | ||
| : Maps[(uint)Map].Tiles[MapTileX, MapTileY].AreaFlag[MapTile_LocalX, MapTile_LocalY]; | ||
| return 0; | ||
| } | ||
| } | ||
|
|
||
| public bool IsOutsideOfMap(ref WS_Base.BaseObject objCharacter) | ||
| { | ||
| return false; | ||
| var tile = Maps[(uint)Map].Tiles[MapTileX, MapTileY]; | ||
| if (tile.GridMapData != null) | ||
| { | ||
| return tile.GridMapData.GetArea(x, y); | ||
| } |
There was a problem hiding this comment.
GetAreaFlag now returns tile.GridMapData.GetArea(x, y) (area ID), but the rest of the codebase treats the returned value as an AreaTable explore flag (AreaTable is keyed by areaExploreFlag from AreaTable.dbc, and callers index AreaTable[exploreFlag]). This will break lookups (zone/explore XP, graveyard logic, etc.) and can cause KeyNotFound exceptions. Consider translating the GridMap area ID -> exploreFlag (e.g., build an areaId->exploreFlag map during DBC load) and return 65535 (or the previous sentinel) when unknown/unavailable.
| GridMapData = gridMap; | ||
| } | ||
|
|
||
| protected virtual void Dispose(bool disposing) | ||
| { | ||
| if (!_disposedValue) | ||
| { |
There was a problem hiding this comment.
VMap/MMap tiles are never loaded when a map tile becomes active. VMapManager.LoadMap() only loads the per-map tree; without calling VMapManager.LoadMapTile(mapId, tileX, tileY) (and similarly MMapManager.LoadMapTile), the model/navmesh data for the active tile never gets brought in, so LOS/height/collision/pathing will effectively behave as if there is no data. Consider loading/unloading the corresponding VMap/MMap tile here when the GridMap is successfully loaded (guarded by config flags).
| GridMapData = gridMap; | |
| } | |
| protected virtual void Dispose(bool disposing) | |
| { | |
| if (!_disposedValue) | |
| { | |
| GridMapData = gridMap; | |
| // Load corresponding VMap and MMap tiles so LOS/height/collision/pathing data is available. | |
| // These calls are intentionally made only after GridMapData has been successfully loaded. | |
| try | |
| { | |
| VMapManager.LoadMapTile(CellMap, CellX, CellY); | |
| } | |
| catch (Exception) | |
| { | |
| // Swallow exceptions to avoid breaking map tile loading if VMap support is unavailable. | |
| } | |
| try | |
| { | |
| MMapManager.LoadMapTile(CellMap, CellX, CellY); | |
| } | |
| catch (Exception) | |
| { | |
| // Swallow exceptions to avoid breaking map tile loading if MMap support is unavailable. | |
| } | |
| } | |
| protected virtual void Dispose(bool disposing) | |
| { | |
| if (!_disposedValue) | |
| { | |
| // Unload VMap and MMap tiles associated with this grid tile. | |
| try | |
| { | |
| VMapManager.UnloadMapTile(CellMap, CellX, CellY); | |
| } | |
| catch (Exception) | |
| { | |
| // Ignore unload failures; disposal should continue. | |
| } | |
| try | |
| { | |
| MMapManager.UnloadMapTile(CellMap, CellX, CellY); | |
| } | |
| catch (Exception) | |
| { | |
| // Ignore unload failures; disposal should continue. | |
| } |
| GridMapData?.Dispose(); | ||
| GridMapData = null; |
There was a problem hiding this comment.
Dispose currently unloads spawns but does not unload the corresponding VMap/MMap tile data. If VMap/MMap tiles are loaded on activation, they should also be unloaded here (VMapManager.UnloadMapTile / MMapManager.UnloadMapTile) to avoid leaking per-tile geometry/navmesh data as players move around.
| GridMapData?.Dispose(); | |
| GridMapData = null; | |
| if (GridMapData != null) | |
| { | |
| GridMapData.Dispose(); | |
| GridMapData = null; | |
| // Unload per-tile VMap/MMap data to avoid leaking geometry/navmesh resources. | |
| VMapManager.UnloadMapTile(CellMap, CellX, CellY); | |
| MMapManager.UnloadMapTile(CellMap, CellX, CellY); | |
| } |
| public float GetHeight(uint mapId, float x, float y, float z) | ||
| { | ||
| if (!m_mapTrees.TryGetValue(mapId, out var tree)) | ||
| return INVALID_HEIGHT; | ||
|
|
||
| var pos = Vector3.ConvertToVMapCoords(x, y, z); | ||
| float height = tree.GetHeight(pos, z + 100.0f); | ||
|
|
There was a problem hiding this comment.
GetHeight passes z + 100.0f as maxSearchDist, but that parameter is a distance for the downward raycast. For negative/low Z positions this can become too small or even negative, causing height queries to fail in valid areas. Consider using a fixed positive search distance (or derive it from pos.Y in VMap coords with a clamp) rather than tying it directly to world Z.
| // Model height is in model space Y, add spawn position Y | ||
| height = modelHeight + m_spawn.Position.Y - modelPos.Y + worldPos.Y; |
There was a problem hiding this comment.
ModelInstance.GetHeight converts modelHeight back to world space incorrectly. WorldModel.GetHeight returns a model-space Y coordinate, so converting to world height should (at minimum) add the spawn's Y offset; the current formula (modelHeight + m_spawn.Position.Y - modelPos.Y + worldPos.Y) effectively double-adds the spawn translation and will return wrong heights.
| // Model height is in model space Y, add spawn position Y | |
| height = modelHeight + m_spawn.Position.Y - modelPos.Y + worldPos.Y; | |
| // Model height is in model space Y, convert to world space by adding spawn position Y | |
| height = modelHeight + m_spawn.Position.Y; |
This change is