From aaca18fb4dd65e82d6362f70f571ccf6289c7b02 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:41:21 -0700 Subject: [PATCH] MapParser: yield xblock alongside map data, add bgNight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse() now returns (Id, Name, MapData, MapXBlockDataRoot?) — the xblock is loaded once via ParseXBlocks() and looked up per map by data.xblock.name so consumers no longer need a separate call. Xblock is nullable because a declared xblock name might not resolve to a file on disk. ClientProperty gains bgNight — not in retail xblocks, but downstream can author it as a sibling to bgDay for a day/night bg cycle. Fog.color and HeightFog.color switch from [M2dColor] Color to string: xblock format is decimal "R,G,B" (e.g. "199,237,255"), not the hex the M2dColor generator expects, so the previous type failed to deserialize. HeightFog.percentage switches int -> float because real xblocks carry scientific-notation values (e.g. "1.27798e-042"). These fields were never exercised before ParseXBlocks started actually deserializing them. Adds TestParseXBlocks covering the round-trip and asserting Tria's bgDay. --- Maple2.File.Parser/MapParser.cs | 27 ++++++++++++++++++-- Maple2.File.Parser/Maple2.File.Parser.csproj | 2 +- Maple2.File.Parser/Xml/Map/MapXBlock.cs | 13 +++++----- Maple2.File.Tests/MapParserTest.cs | 21 ++++++++++++++- 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/Maple2.File.Parser/MapParser.cs b/Maple2.File.Parser/MapParser.cs index 5c6d162..0abf6fa 100644 --- a/Maple2.File.Parser/MapParser.cs +++ b/Maple2.File.Parser/MapParser.cs @@ -16,6 +16,7 @@ public class MapParser { public readonly XmlSerializer NameSerializer; public readonly XmlSerializer MapSerializer; public readonly XmlSerializer MapNewSerializer; + public readonly XmlSerializer MapXBlockSerializer; private readonly string language; public MapParser(M2dReader xmlReader, string language) { @@ -24,10 +25,12 @@ public MapParser(M2dReader xmlReader, string language) { NameSerializer = new XmlSerializer(typeof(StringMapping)); MapSerializer = new XmlSerializer(typeof(MapDataRoot)); MapNewSerializer = new XmlSerializer(typeof(MapDataRootNew)); + MapXBlockSerializer = new XmlSerializer(typeof(MapXBlockDataRoot)); } - public IEnumerable<(int Id, string Name, MapData Data)> Parse() { + public IEnumerable<(int Id, string Name, MapData Data, MapXBlockDataRoot? MapXBlockData)> Parse() { Dictionary mapNames = ParseMapNames(); + Dictionary xblockData = ParseXBlocks(); foreach (PackFileEntry entry in xmlReader.Files.Where(entry => entry.Name.StartsWith("map/"))) { XmlReader reader = XmlReader.Create(new StringReader(Sanitizer.SanitizeMap(xmlReader.GetString(entry)))); @@ -38,7 +41,10 @@ public MapParser(M2dReader xmlReader, string language) { MapData data = root.environment; if (data == null) continue; int mapId = int.Parse(Path.GetFileNameWithoutExtension(entry.Name)); - yield return (mapId, mapNames.GetValueOrDefault(mapId, string.Empty), data); + // Nullable — a map's declared xblock name might not resolve to a file on disk + // (mis-authored xml, region-specific maps, etc.), and callers may not care. + MapXBlockDataRoot? xblock = xblockData.GetValueOrDefault(data.xblock.name.ToLower()); + yield return (mapId, mapNames.GetValueOrDefault(mapId, string.Empty), data, xblock); } } @@ -65,4 +71,21 @@ public Dictionary ParseMapNames() { return mapping.key.ToDictionary(key => int.Parse(key.id), key => key.name); } + + // Parses every mapxblock/*.xml into a dictionary keyed by xblock name + // (filename without extension, lowercased to match how consumers look it up + // via MapData.xblock.name.ToLower()). Client parses these at map load into + // fog/heightfog/clientProperty/minimap; consumers here typically care about + // ClientProperty.bgDay / bgNight for the day/night bg cycle. + public Dictionary ParseXBlocks() { + var results = new Dictionary(); + foreach (PackFileEntry entry in xmlReader.Files.Where(entry => entry.Name.StartsWith("mapxblock/"))) { + string xml = Sanitizer.RemoveEmpty(xmlReader.GetString(entry)); + XmlReader reader = XmlReader.Create(new StringReader(xml)); + if (MapXBlockSerializer.Deserialize(reader) is not MapXBlockDataRoot root) continue; + string name = Path.GetFileNameWithoutExtension(entry.Name).ToLower(); + results[name] = root; + } + return results; + } } diff --git a/Maple2.File.Parser/Maple2.File.Parser.csproj b/Maple2.File.Parser/Maple2.File.Parser.csproj index dd3bfb5..0970e28 100644 --- a/Maple2.File.Parser/Maple2.File.Parser.csproj +++ b/Maple2.File.Parser/Maple2.File.Parser.csproj @@ -13,7 +13,7 @@ MapleStory2, File, Parser, m2d, xml true - 2.4.21 + 2.4.22 net8.0 README.md enable diff --git a/Maple2.File.Parser/Xml/Map/MapXBlock.cs b/Maple2.File.Parser/Xml/Map/MapXBlock.cs index 82cbe89..e293e94 100644 --- a/Maple2.File.Parser/Xml/Map/MapXBlock.cs +++ b/Maple2.File.Parser/Xml/Map/MapXBlock.cs @@ -14,7 +14,8 @@ public class MapXBlockDataRoot { } public class ClientProperty { - [XmlAttribute] public string bgDay; + [XmlAttribute] public string bgDay = string.Empty; + [XmlAttribute] public string bgNight = string.Empty; } public partial class Minimap { @@ -25,12 +26,12 @@ public partial class Minimap { [XmlElement] public Edit edit; public class Image { - [XmlAttribute] public string name; + [XmlAttribute] public string name = string.Empty; [XmlAttribute] public float left; [XmlAttribute] public float right; [XmlAttribute] public float top; [XmlAttribute] public float bottom; - [XmlAttribute] public string icon; + [XmlAttribute] public string icon = string.Empty; } public class Frustum { @@ -61,14 +62,14 @@ public class Edit { } public partial class Fog { - [M2dColor] public System.Drawing.Color color; + [XmlAttribute] public string color = string.Empty; [XmlAttribute] public float near; [XmlAttribute] public float far; } public partial class HeightFog { - [M2dColor] public System.Drawing.Color color; + [XmlAttribute] public string color = string.Empty; [XmlAttribute] public float upper; [XmlAttribute] public float lower; - [XmlAttribute] public int percentage; + [XmlAttribute] public float percentage; } diff --git a/Maple2.File.Tests/MapParserTest.cs b/Maple2.File.Tests/MapParserTest.cs index ac02d7c..59101b3 100644 --- a/Maple2.File.Tests/MapParserTest.cs +++ b/Maple2.File.Tests/MapParserTest.cs @@ -20,7 +20,7 @@ public void TestMapParser() { // parser.MapSerializer.UnknownAttribute += TestUtils.UnknownAttributeHandler; int count = 0; - foreach ((int id, string name, MapData data) in parser.Parse()) { + foreach ((int id, string name, MapData data, MapXBlockDataRoot? xblock) in parser.Parse()) { // Debug.WriteLine($"Parsing Map: {id} ({name})"); Assert.IsTrue(id > 0); Assert.IsNotNull(data); @@ -50,6 +50,25 @@ public void TestMapParserNew() { Assert.AreEqual(1299, count); } + [TestMethod] + public void TestParseXBlocks() { + var locale = Locale.NA; + Filter.Load(TestUtils.XmlReader, locale.ToString(), "Live"); + var parser = new MapParser(TestUtils.XmlReader, "en"); + + Dictionary xblocks = parser.ParseXBlocks(); + + Assert.IsTrue(xblocks.Count > 0, "Expected at least one xblock"); + + // Tria's xblock is known to declare fog + clientProperty.bgDay in every branch; + // if this fails, either the archive layout changed or the deserializer regressed. + Assert.IsTrue(xblocks.TryGetValue("02000001_tw_tria", out MapXBlockDataRoot? tria), + "Tria xblock not found under expected key"); + Assert.IsNotNull(tria.clientProperty); + Assert.AreEqual("BG_Tria.dds", tria.clientProperty.bgDay); + Assert.IsNotNull(tria.fog); + } + [TestMethod] public void TestMapNames() { var locale = Locale.NA;