diff --git a/Maple2.File.Parser/Maple2.File.Parser.csproj b/Maple2.File.Parser/Maple2.File.Parser.csproj index ca4bb7a..dd3bfb5 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.20 + 2.4.21 net8.0 README.md enable diff --git a/Maple2.File.Parser/ServerTableParser.cs b/Maple2.File.Parser/ServerTableParser.cs index 1369b9d..0e84ba4 100644 --- a/Maple2.File.Parser/ServerTableParser.cs +++ b/Maple2.File.Parser/ServerTableParser.cs @@ -65,6 +65,7 @@ public class ServerTableParser { private readonly XmlSerializer npcStatFactorByLevelSerializer; private readonly XmlSerializer maidGradeInfoSerializer; private readonly XmlSerializer maidRecipeSerializer; + private readonly XmlSerializer defaultCharacterInfoSerializer; public ServerTableParser(M2dReader xmlReader) { this.xmlReader = xmlReader; @@ -111,6 +112,7 @@ public ServerTableParser(M2dReader xmlReader) { npcStatFactorByLevelSerializer = new XmlSerializer(typeof(NpcStatFactorByLevelRoot)); maidGradeInfoSerializer = new XmlSerializer(typeof(MaidGradeInfoRoot)); maidRecipeSerializer = new XmlSerializer(typeof(MaidRecipeRoot)); + defaultCharacterInfoSerializer = new XmlSerializer(typeof(DefaultCharacterInfoRoot)); // var seen = new HashSet(); // this.bankTypeSerializer.UnknownAttribute += (sender, args) => { @@ -756,4 +758,14 @@ public IEnumerable ParseNpcStatFactorByLevel() { yield return (entry.Id, entry); } } + + public IEnumerable<(Gender Gender, DefaultCharacterInfo Info)> ParseDefaultCharacterInfo() { + XmlReader reader = xmlReader.GetXmlReader(xmlReader.GetEntry("table/Server/defaultCharacterInfo.xml")); + var data = defaultCharacterInfoSerializer.Deserialize(reader) as DefaultCharacterInfoRoot; + Debug.Assert(data != null); + + foreach (DefaultCharacterInfo info in data.gender) { + yield return (info.value, info); + } + } } diff --git a/Maple2.File.Parser/Xml/Table/Server/DefaultCharacterInfo.cs b/Maple2.File.Parser/Xml/Table/Server/DefaultCharacterInfo.cs new file mode 100644 index 0000000..c6b37e0 --- /dev/null +++ b/Maple2.File.Parser/Xml/Table/Server/DefaultCharacterInfo.cs @@ -0,0 +1,48 @@ +using System.Numerics; +using System.Xml.Serialization; +using M2dXmlGenerator; +using Maple2.File.Parser.Enum; + +namespace Maple2.File.Parser.Xml.Table.Server; + +// ./data/server/table/Server/defaultCharacterInfo.xml +[XmlRoot("ms2")] +public class DefaultCharacterInfoRoot { + [XmlElement] public List gender = []; +} + +public partial class DefaultCharacterInfo { + [M2dEnum] public Gender value = Gender.Invalid; + [XmlElement] public Skin skin = new(); + [XmlElement] public Items items = new(); + + public class Skin { + [XmlAttribute] public int colorPaletteID; + // Absent where the entry keeps the palette's own default swatch. + [XmlAttribute] public int colorSN = -1; + } + + public class Items { + [XmlElement] public List item = []; + } + + public class Item { + [XmlAttribute] public int id; + // Equip slot code: HR, FA, FD, ER. + [XmlAttribute] public string slotHint = string.Empty; + [XmlAttribute] public int colorPaletteID; + [XmlAttribute] public int colorSN = -1; + [XmlElement] public Controls controls = new(); + } + + public class Controls { + [XmlElement] public List control = []; + } + + public partial class Control { + [XmlAttribute] public int index; + [XmlAttribute] public float scale; + [M2dVector3] public Vector3 position; + [M2dVector3] public Vector3 rotation; + } +} diff --git a/Maple2.File.Tests/ServerTableParserTest.cs b/Maple2.File.Tests/ServerTableParserTest.cs index 757d15f..d2b415c 100644 --- a/Maple2.File.Tests/ServerTableParserTest.cs +++ b/Maple2.File.Tests/ServerTableParserTest.cs @@ -1,4 +1,6 @@ -using Maple2.File.Parser; +using System.Numerics; +using Maple2.File.Parser; +using Maple2.File.Parser.Enum; using Maple2.File.Parser.Xml.Table; using Maple2.File.Parser.Xml.Table.Server; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -482,5 +484,34 @@ public void TestMaidRecipe() { continue; } } + + [TestMethod] + public void TestDefaultCharacterInfo() { + var parser = new ServerTableParser(TestUtils.ServerReader); + + Dictionary infos = parser.ParseDefaultCharacterInfo() + .ToDictionary(entry => entry.Gender, entry => entry.Info); + + CollectionAssert.AreEquivalent(new[] { Gender.Male, Gender.Female }, infos.Keys.ToArray()); + + foreach (DefaultCharacterInfo info in infos.Values) { + Assert.IsTrue(info.skin.colorPaletteID > 0); + // The client needs a default for every gender-locked appearance slot. + CollectionAssert.AreEquivalent(new[] { "HR", "FA", "FD", "ER" }, + info.items.item.Select(item => item.slotHint).ToArray()); + Assert.IsTrue(info.items.item.All(item => item.id > 0)); + } + + // Slots with no palette override keep the sentinel rather than falling back to swatch 0. + Assert.IsTrue(infos.Values + .SelectMany(info => info.items.item) + .Any(item => item.colorSN == -1)); + + DefaultCharacterInfo.Item hair = infos[Gender.Female].items.item.First(item => item.slotHint == "HR"); + Assert.AreEqual(2, hair.controls.control.Count); + Assert.AreNotEqual(0f, hair.controls.control[0].scale); + Assert.AreNotEqual(Vector3.Zero, hair.controls.control[0].position); + Assert.AreNotEqual(Vector3.Zero, hair.controls.control[0].rotation); + } }