diff --git a/Maple2.File.Parser/Maple2.File.Parser.csproj b/Maple2.File.Parser/Maple2.File.Parser.csproj
index 45eca3a..ab4ad23 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.32
+ 2.4.33
net8.0
README.md
enable
diff --git a/Maple2.File.Parser/TableParser.cs b/Maple2.File.Parser/TableParser.cs
index b058a08..f0329be 100644
--- a/Maple2.File.Parser/TableParser.cs
+++ b/Maple2.File.Parser/TableParser.cs
@@ -112,6 +112,7 @@ public class TableParser {
private readonly XmlSerializer questGroupSerializer;
private readonly XmlSerializer darkStreamSerializer;
private readonly XmlSerializer clubBuffSerializer;
+ private readonly XmlSerializer shadowBuffSerializer;
private readonly XmlSerializer characterCreateSelectSerializer;
private readonly XmlSerializer fameSerializer;
private readonly XmlSerializer fameDefSerializer;
@@ -232,6 +233,7 @@ public TableParser(M2dReader xmlReader, string language) {
questGroupSerializer = new XmlSerializer(typeof(QuestGroupRoot));
darkStreamSerializer = new XmlSerializer(typeof(DarkStreamRoot));
clubBuffSerializer = new XmlSerializer(typeof(ClubBuffRoot));
+ shadowBuffSerializer = new XmlSerializer(typeof(ShadowBuffRoot));
characterCreateSelectSerializer = new XmlSerializer(typeof(CharacterCreateSelect));
fameSerializer = new XmlSerializer(typeof(FameRoot));
fameDefSerializer = new XmlSerializer(typeof(FameDef));
@@ -1704,6 +1706,17 @@ public IEnumerable ParseJobTableNew() {
}
}
+ public IEnumerable<(int Id, ShadowBuff Buff)> ParseShadowBuff() {
+ string xml = Sanitizer.RemoveSpaces(xmlReader.GetString(xmlReader.GetEntry("table/shadowbuff.xml")));
+ var reader = XmlReader.Create(new StringReader(xml));
+ var data = shadowBuffSerializer.Deserialize(reader) as ShadowBuffRoot;
+ Debug.Assert(data != null);
+
+ foreach (ShadowBuff entry in data.ShadowBuff) {
+ yield return (entry.id, entry);
+ }
+ }
+
public IEnumerable<(int Id, DungeonRewardCoupon Coupon)> ParseDungeonRewardCoupon() {
string xml = Sanitizer.RemoveSpaces(xmlReader.GetString(xmlReader.GetEntry($"table/{locale}/dungeonrewardcoupon.xml")));
var reader = XmlReader.Create(new StringReader(xml));
diff --git a/Maple2.File.Parser/Xml/Table/ShadowBuff.cs b/Maple2.File.Parser/Xml/Table/ShadowBuff.cs
new file mode 100644
index 0000000..82cbaba
--- /dev/null
+++ b/Maple2.File.Parser/Xml/Table/ShadowBuff.cs
@@ -0,0 +1,26 @@
+using System.Xml.Serialization;
+using M2dXmlGenerator;
+
+namespace Maple2.File.Parser.Xml.Table;
+
+// ./data/xml/table/shadowbuff.xml
+[XmlRoot("ms2")]
+public partial class ShadowBuffRoot {
+ [M2dFeatureLocale(Selector = "id")] private IList _ShadowBuff;
+}
+
+public partial class ShadowBuff : IFeatureLocale {
+ [XmlAttribute] public int id;
+ // Absent on the common traits, which every class lists.
+ [XmlAttribute] public int jobCode;
+ [XmlAttribute] public string iconPath = string.Empty;
+ [XmlElement] public List ShadowBuffLevel;
+}
+
+public partial class ShadowBuffLevel {
+ [XmlAttribute] public int level;
+ // Retail's spelling. Additional effect ids, 10 digits, so they do not fit an int.
+ [M2dArray] public long[] addifectGuids = Array.Empty();
+ // Absent on a class's free level 1.
+ [XmlAttribute] public int levelUpCost;
+}
diff --git a/Maple2.File.Tests/TableParserTest.cs b/Maple2.File.Tests/TableParserTest.cs
index 4dd9b30..5d0049f 100644
--- a/Maple2.File.Tests/TableParserTest.cs
+++ b/Maple2.File.Tests/TableParserTest.cs
@@ -829,6 +829,27 @@ public void TestClubBuff() {
}
}
+ [TestMethod]
+ public void TestShadowBuff() {
+ var rows = _parser.ParseShadowBuff().ToList();
+ Assert.IsTrue(rows.Count > 0);
+ foreach ((int id, ShadowBuff buff) in rows) {
+ Assert.AreEqual(id, buff.id);
+ Assert.IsTrue(buff.ShadowBuffLevel.Count > 0, $"{id} has no levels");
+ foreach (ShadowBuffLevel level in buff.ShadowBuffLevel) {
+ Assert.IsTrue(level.level is >= 1 and <= 20, $"{id} level {level.level}");
+ }
+ }
+
+ // Soul Binder trait 211001, from table/shadowbuff.xml.
+ (_, ShadowBuff? soulBinder) = rows.FirstOrDefault(r => r.Id == 211001);
+ Assert.IsNotNull(soulBinder);
+ Assert.AreEqual(110, soulBinder.jobCode);
+ ShadowBuffLevel level2 = soulBinder.ShadowBuffLevel.Single(l => l.level == 2);
+ Assert.AreEqual(40, level2.levelUpCost);
+ Assert.AreEqual(9201100102L, level2.addifectGuids[0]);
+ }
+
[TestMethod]
public void TestDungeonRewardCoupon() {
foreach ((_, _) in _parser.ParseDungeonRewardCoupon()) {