Skip to content

Commit eb90e83

Browse files
committed
Add MangosZero-compatible map file format definitions
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
1 parent 8846935 commit eb90e83

1 file changed

Lines changed: 328 additions & 0 deletions

File tree

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
//
2+
// Copyright (C) 2013-2025 getMaNGOS <https://www.getmangos.eu>
3+
//
4+
// This program is free software. You can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation. either version 2 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY. Without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with this program. If not, write to the Free Software
16+
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17+
//
18+
19+
using System;
20+
using System.IO;
21+
using System.Text;
22+
23+
namespace Mangos.World.Maps;
24+
25+
/// <summary>
26+
/// MangosZero-compatible map file format definitions.
27+
/// These match the binary layout produced by MangosZero's map extractor tools,
28+
/// allowing maps extracted with MangosZero tools to be used with MangosSharp.
29+
/// </summary>
30+
public static class MapFileFormats
31+
{
32+
// Grid dimensions
33+
public const int V9_SIZE = 129; // Height map vertex grid (corners)
34+
public const int V9_SIZE_SQ = V9_SIZE * V9_SIZE;
35+
public const int V8_SIZE = 128; // Height map cell center grid
36+
public const int V8_SIZE_SQ = V8_SIZE * V8_SIZE;
37+
public const float GRID_SIZE = 533.33333f;
38+
public const float GRID_PART = GRID_SIZE / V8_SIZE;
39+
public const int AREA_SIZE = 16; // Area data grid
40+
public const int LIQUID_SIZE = 128; // Max liquid grid
41+
42+
// FourCC magic values (little-endian uint32 representation)
43+
public const uint MAP_MAGIC = 0x5350414D; // "MAPS"
44+
public const uint MAP_VERSION_MAGIC = 0x302E3976; // "v9.0"
45+
public const uint MAP_AREA_MAGIC = 0x41455241; // "AREA"
46+
public const uint MAP_HEIGHT_MAGIC = 0x5447484D; // "MHGT"
47+
public const uint MAP_LIQUID_MAGIC = 0x51494C4D; // "MLIQ"
48+
49+
// Area header flags
50+
public const ushort MAP_AREA_NO_AREA = 0x0001;
51+
52+
// Height header flags
53+
public const uint MAP_HEIGHT_NO_HEIGHT = 0x0001;
54+
public const uint MAP_HEIGHT_AS_INT16 = 0x0002;
55+
public const uint MAP_HEIGHT_AS_INT8 = 0x0004;
56+
57+
// Liquid header flags
58+
public const ushort MAP_LIQUID_NO_TYPE = 0x0001;
59+
public const ushort MAP_LIQUID_NO_HEIGHT = 0x0002;
60+
61+
// VMAP magic
62+
public const string VMAP_MAGIC = "VMAP004";
63+
64+
// MMAP constants
65+
public const uint MMAP_MAGIC = 0x4D4D4150; // "MMAP"
66+
public const uint MMAP_VERSION = 4;
67+
public const uint DT_NAVMESH_VERSION = 7; // Detour navmesh version for MangosZero
68+
69+
/// <summary>
70+
/// Main map file header - first 44 bytes of every .map file.
71+
/// Binary layout matches MangosZero's GridMapFileHeader in GridMap.h.
72+
/// </summary>
73+
public struct MapFileHeader
74+
{
75+
public uint MapMagic; // Must be MAP_MAGIC ("MAPS")
76+
public uint VersionMagic; // Must be MAP_VERSION_MAGIC ("v9.0")
77+
public uint BuildMagic; // Client build number
78+
public uint AreaMapOffset; // Byte offset to area data section
79+
public uint AreaMapSize; // Size of area data section
80+
public uint HeightMapOffset; // Byte offset to height data section
81+
public uint HeightMapSize; // Size of height data section
82+
public uint LiquidMapOffset; // Byte offset to liquid data section
83+
public uint LiquidMapSize; // Size of liquid data section
84+
public uint HolesOffset; // Byte offset to holes data section
85+
public uint HolesSize; // Size of holes data section
86+
87+
public static MapFileHeader Read(BinaryReader reader)
88+
{
89+
return new MapFileHeader
90+
{
91+
MapMagic = reader.ReadUInt32(),
92+
VersionMagic = reader.ReadUInt32(),
93+
BuildMagic = reader.ReadUInt32(),
94+
AreaMapOffset = reader.ReadUInt32(),
95+
AreaMapSize = reader.ReadUInt32(),
96+
HeightMapOffset = reader.ReadUInt32(),
97+
HeightMapSize = reader.ReadUInt32(),
98+
LiquidMapOffset = reader.ReadUInt32(),
99+
LiquidMapSize = reader.ReadUInt32(),
100+
HolesOffset = reader.ReadUInt32(),
101+
HolesSize = reader.ReadUInt32()
102+
};
103+
}
104+
105+
public void Write(BinaryWriter writer)
106+
{
107+
writer.Write(MapMagic);
108+
writer.Write(VersionMagic);
109+
writer.Write(BuildMagic);
110+
writer.Write(AreaMapOffset);
111+
writer.Write(AreaMapSize);
112+
writer.Write(HeightMapOffset);
113+
writer.Write(HeightMapSize);
114+
writer.Write(LiquidMapOffset);
115+
writer.Write(LiquidMapSize);
116+
writer.Write(HolesOffset);
117+
writer.Write(HolesSize);
118+
}
119+
120+
public bool IsValid => MapMagic == MAP_MAGIC && VersionMagic == MAP_VERSION_MAGIC;
121+
}
122+
123+
/// <summary>
124+
/// Area data section header.
125+
/// </summary>
126+
public struct MapAreaHeader
127+
{
128+
public uint FourCC; // Must be MAP_AREA_MAGIC ("AREA")
129+
public ushort Flags;
130+
public ushort GridArea; // Default area ID when MAP_AREA_NO_AREA is set
131+
132+
public static MapAreaHeader Read(BinaryReader reader)
133+
{
134+
return new MapAreaHeader
135+
{
136+
FourCC = reader.ReadUInt32(),
137+
Flags = reader.ReadUInt16(),
138+
GridArea = reader.ReadUInt16()
139+
};
140+
}
141+
142+
public void Write(BinaryWriter writer)
143+
{
144+
writer.Write(FourCC);
145+
writer.Write(Flags);
146+
writer.Write(GridArea);
147+
}
148+
149+
public bool HasNoArea => (Flags & MAP_AREA_NO_AREA) != 0;
150+
}
151+
152+
/// <summary>
153+
/// Height data section header.
154+
/// </summary>
155+
public struct MapHeightHeader
156+
{
157+
public uint FourCC; // Must be MAP_HEIGHT_MAGIC ("MHGT")
158+
public uint Flags;
159+
public float GridHeight; // Base/flat height when MAP_HEIGHT_NO_HEIGHT
160+
public float GridMaxHeight; // Max height for compression range
161+
162+
public static MapHeightHeader Read(BinaryReader reader)
163+
{
164+
return new MapHeightHeader
165+
{
166+
FourCC = reader.ReadUInt32(),
167+
Flags = reader.ReadUInt32(),
168+
GridHeight = reader.ReadSingle(),
169+
GridMaxHeight = reader.ReadSingle()
170+
};
171+
}
172+
173+
public void Write(BinaryWriter writer)
174+
{
175+
writer.Write(FourCC);
176+
writer.Write(Flags);
177+
writer.Write(GridHeight);
178+
writer.Write(GridMaxHeight);
179+
}
180+
181+
public bool HasNoHeight => (Flags & MAP_HEIGHT_NO_HEIGHT) != 0;
182+
public bool IsInt16 => (Flags & MAP_HEIGHT_AS_INT16) != 0;
183+
public bool IsInt8 => (Flags & MAP_HEIGHT_AS_INT8) != 0;
184+
}
185+
186+
/// <summary>
187+
/// Liquid data section header.
188+
/// </summary>
189+
public struct MapLiquidHeader
190+
{
191+
public uint FourCC; // Must be MAP_LIQUID_MAGIC ("MLIQ")
192+
public ushort Flags;
193+
public ushort LiquidType; // Global liquid type
194+
public byte OffsetX; // Start X offset in 128x128 grid
195+
public byte OffsetY; // Start Y offset in 128x128 grid
196+
public byte Width; // Liquid data width
197+
public byte Height; // Liquid data height
198+
public float LiquidLevel; // Base liquid level
199+
200+
public static MapLiquidHeader Read(BinaryReader reader)
201+
{
202+
return new MapLiquidHeader
203+
{
204+
FourCC = reader.ReadUInt32(),
205+
Flags = reader.ReadUInt16(),
206+
LiquidType = reader.ReadUInt16(),
207+
OffsetX = reader.ReadByte(),
208+
OffsetY = reader.ReadByte(),
209+
Width = reader.ReadByte(),
210+
Height = reader.ReadByte(),
211+
LiquidLevel = reader.ReadSingle()
212+
};
213+
}
214+
215+
public void Write(BinaryWriter writer)
216+
{
217+
writer.Write(FourCC);
218+
writer.Write(Flags);
219+
writer.Write(LiquidType);
220+
writer.Write(OffsetX);
221+
writer.Write(OffsetY);
222+
writer.Write(Width);
223+
writer.Write(Height);
224+
writer.Write(LiquidLevel);
225+
}
226+
227+
public bool HasNoType => (Flags & MAP_LIQUID_NO_TYPE) != 0;
228+
public bool HasNoHeight => (Flags & MAP_LIQUID_NO_HEIGHT) != 0;
229+
}
230+
231+
/// <summary>
232+
/// MMap tile header for navigation mesh tiles (20 bytes).
233+
/// Binary layout matches MangosZero's MmapTileHeader in MoveMapSharedDefines.h.
234+
/// </summary>
235+
public struct MmapTileHeader
236+
{
237+
public uint MmapMagic; // Must be MMAP_MAGIC (0x4D4D4150)
238+
public uint DtVersion; // Must be DT_NAVMESH_VERSION (Detour's navmesh version)
239+
public uint MmapVersion; // Must be MMAP_VERSION (4)
240+
public uint Size; // Size of dtNavMesh tile data following this header
241+
public uint UsesLiquids; // Whether this tile includes liquid navigation data
242+
243+
public static MmapTileHeader Read(BinaryReader reader)
244+
{
245+
return new MmapTileHeader
246+
{
247+
MmapMagic = reader.ReadUInt32(),
248+
DtVersion = reader.ReadUInt32(),
249+
MmapVersion = reader.ReadUInt32(),
250+
Size = reader.ReadUInt32(),
251+
UsesLiquids = reader.ReadUInt32()
252+
};
253+
}
254+
255+
public bool IsValid => MmapMagic == MMAP_MAGIC && MmapVersion == MMAP_VERSION;
256+
}
257+
258+
/// <summary>
259+
/// Builds the MangosZero-format filename for a map tile.
260+
/// MangosZero uses: {mapId:D4}{tileY:D2}{tileX:D2}.map
261+
/// Note: MangosZero uses Y,X order in the filename (not X,Y).
262+
/// </summary>
263+
public static string GetMapFileName(uint mapId, byte tileX, byte tileY)
264+
{
265+
return $"{mapId:D4}{tileY:D2}{tileX:D2}.map";
266+
}
267+
268+
/// <summary>
269+
/// Builds the legacy MangosSharp-format filename for a map tile.
270+
/// MangosSharp legacy uses: {mapId:D3}{tileX:D2}{tileY:D2}.map
271+
/// </summary>
272+
public static string GetLegacyMapFileName(uint mapId, byte tileX, byte tileY)
273+
{
274+
return $"{mapId:000}{tileX:00}{tileY:00}.map";
275+
}
276+
277+
/// <summary>
278+
/// Gets the VMAP tree file path for a map.
279+
/// </summary>
280+
public static string GetVMapTreeFileName(uint mapId)
281+
{
282+
return $"{mapId:D4}.vmtree";
283+
}
284+
285+
/// <summary>
286+
/// Gets the VMAP tile file path for a map tile.
287+
/// </summary>
288+
public static string GetVMapTileFileName(uint mapId, byte tileX, byte tileY)
289+
{
290+
return $"{mapId:D4}{tileY:D2}{tileX:D2}.vmtile";
291+
}
292+
293+
/// <summary>
294+
/// Gets the MMap parameter file path for a map.
295+
/// </summary>
296+
public static string GetMMapFileName(uint mapId)
297+
{
298+
return $"{mapId:D4}.mmap";
299+
}
300+
301+
/// <summary>
302+
/// Gets the MMap tile file path for a map tile.
303+
/// </summary>
304+
public static string GetMMapTileFileName(uint mapId, byte tileX, byte tileY)
305+
{
306+
return $"{mapId:D4}{tileY:D2}{tileX:D2}.mmtile";
307+
}
308+
309+
/// <summary>
310+
/// Detects whether a map file is in MangosZero format by checking the magic number.
311+
/// </summary>
312+
public static bool IsMangosZeroFormat(string filePath)
313+
{
314+
if (!File.Exists(filePath)) return false;
315+
try
316+
{
317+
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
318+
using var reader = new BinaryReader(fs);
319+
if (fs.Length < 4) return false;
320+
var magic = reader.ReadUInt32();
321+
return magic == MAP_MAGIC;
322+
}
323+
catch
324+
{
325+
return false;
326+
}
327+
}
328+
}

0 commit comments

Comments
 (0)