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);
+ }
}