diff --git a/Maple2.File.Parser/Maple2.File.Parser.csproj b/Maple2.File.Parser/Maple2.File.Parser.csproj
index 0441abb..58c23d3 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.35
+ 2.4.36
net8.0
README.md
enable
diff --git a/Maple2.File.Parser/ServerTableParser.cs b/Maple2.File.Parser/ServerTableParser.cs
index c959bd0..27e41ed 100644
--- a/Maple2.File.Parser/ServerTableParser.cs
+++ b/Maple2.File.Parser/ServerTableParser.cs
@@ -45,6 +45,7 @@ public class ServerTableParser {
private readonly XmlSerializer groupSpawnSerializer;
private readonly XmlSerializer shadowExpeditionDropSerializer;
private readonly XmlSerializer additionalEffectGroupSerializer;
+ private readonly XmlSerializer globalEquipmentRankProbabilitySerializer;
private readonly XmlSerializer fishSerializer;
private readonly XmlSerializer fishingSpotSerializer;
private readonly XmlSerializer fishLureSerializer;
@@ -95,6 +96,7 @@ public ServerTableParser(M2dReader xmlReader) {
groupSpawnSerializer = new XmlSerializer(typeof(GroupSpawnRoot));
shadowExpeditionDropSerializer = new XmlSerializer(typeof(ShadowExpeditionDropRoot));
additionalEffectGroupSerializer = new XmlSerializer(typeof(AdditionalEffectGroupRoot));
+ globalEquipmentRankProbabilitySerializer = new XmlSerializer(typeof(GlobalEquipmentRankProbabilityRoot));
fishSerializer = new XmlSerializer(typeof(FishRoot));
fishingSpotSerializer = new XmlSerializer(typeof(FishingSpotRoot));
fishLureSerializer = new XmlSerializer(typeof(FishLureRoot));
@@ -537,6 +539,17 @@ public IEnumerable ParseShadowExpeditionDrop() {
}
}
+ public IEnumerable ParseGlobalEquipmentRankProbability() {
+ string xml = Sanitizer.RemoveEmpty(xmlReader.GetString(xmlReader.GetEntry("table/Server/globalEquipmentRankProbability.xml")));
+ var reader = XmlReader.Create(new StringReader(xml));
+ var data = globalEquipmentRankProbabilitySerializer.Deserialize(reader) as GlobalEquipmentRankProbabilityRoot;
+ Debug.Assert(data != null);
+
+ foreach (GlobalEquipmentRankProbability row in data.DropProbability) {
+ yield return row;
+ }
+ }
+
public IEnumerable<(int Id, Fish Fish)> ParseFish() {
string xml = Sanitizer.RemoveEmpty(xmlReader.GetString(xmlReader.GetEntry("table/Server/fish.xml")));
var reader = XmlReader.Create(new StringReader(xml));
diff --git a/Maple2.File.Parser/Xml/Item/Property.cs b/Maple2.File.Parser/Xml/Item/Property.cs
index 1d26778..4f684f4 100644
--- a/Maple2.File.Parser/Xml/Item/Property.cs
+++ b/Maple2.File.Parser/Xml/Item/Property.cs
@@ -5,7 +5,9 @@ namespace Maple2.File.Parser.Xml.Item;
public partial class Property {
[XmlAttribute] public bool skin;
- [XmlAttribute] public short rank = 1;
+ // 0 when the item names no rank, as the client's CItemData starts it. A drop then rolls the rank from
+ // globalEquipmentRankProbability.xml, so 0 must stay apart from an explicit rank="1".
+ [XmlAttribute] public short rank;
[XmlAttribute] public int skinType;
[XmlAttribute] public int slotMax = 1; // if > 99999, set to 99999
[XmlAttribute] public int type;
diff --git a/Maple2.File.Parser/Xml/Table/Server/GlobalEquipmentRankProbability.cs b/Maple2.File.Parser/Xml/Table/Server/GlobalEquipmentRankProbability.cs
new file mode 100644
index 0000000..150a17e
--- /dev/null
+++ b/Maple2.File.Parser/Xml/Table/Server/GlobalEquipmentRankProbability.cs
@@ -0,0 +1,24 @@
+using System.Xml.Serialization;
+using M2dXmlGenerator;
+
+namespace Maple2.File.Parser.Xml.Table.Server;
+
+// ./data/server/table/Server/globalEquipmentRankProbability.xml
+// The rank an item drops at when its drop row names no grade and the item has no fixed rank. One row per
+// npcRank, npcLevel and assistCondition; KR and CN override some npcRank 4 rows.
+[XmlRoot("ms2")]
+public partial class GlobalEquipmentRankProbabilityRoot {
+ [M2dFeatureLocale(Name = "DropProbability", Selector = "npcRank|npcLevel|assistCondition")]
+ private IList _dropProbability;
+}
+
+// The four columns are percentages of ranks 2 to 5; whatever they leave is rank 1. An empty cell is 0.
+public partial class GlobalEquipmentRankProbability : IFeatureLocale {
+ [XmlAttribute] public int npcRank;
+ [XmlAttribute] public int npcLevel;
+ [XmlAttribute] public int assistCondition;
+ [XmlAttribute] public float Rare;
+ [XmlAttribute] public float Elite;
+ [XmlAttribute] public float Excellent;
+ [XmlAttribute] public float Legendary;
+}
diff --git a/Maple2.File.Tests/ItemParserTest.cs b/Maple2.File.Tests/ItemParserTest.cs
index 5eb9fe7..6f4c557 100644
--- a/Maple2.File.Tests/ItemParserTest.cs
+++ b/Maple2.File.Tests/ItemParserTest.cs
@@ -31,6 +31,23 @@ public void TestItemParser() {
Assert.AreEqual(35309, count);
}
+ [TestMethod]
+ public void TestItemRank() {
+ Filter.Load(TestUtils.XmlReader, Locale.NA.ToString(), "Live");
+ var parser = new ItemParser(TestUtils.XmlReader, "en");
+
+ Dictionary ranks = parser.Parse()
+ .Where(entry => entry.Id is 11300106 or 13200172 or 11300001 or 11300003)
+ .ToDictionary(entry => entry.Id, entry => entry.Data.property.rank);
+
+ // 11300106 Apprentice Hat names no rank. 13200172's NA environment has ,
+ // 11300001 and 11300003 .
+ Assert.AreEqual(0, ranks[11300106]);
+ Assert.AreEqual(1, ranks[13200172]);
+ Assert.AreEqual(2, ranks[11300001]);
+ Assert.AreEqual(3, ranks[11300003]);
+ }
+
[TestMethod]
public void TestItemParserNew() {
var locale = Locale.KR;
diff --git a/Maple2.File.Tests/ServerTableParserTest.cs b/Maple2.File.Tests/ServerTableParserTest.cs
index a037be6..3289ebc 100644
--- a/Maple2.File.Tests/ServerTableParserTest.cs
+++ b/Maple2.File.Tests/ServerTableParserTest.cs
@@ -1,6 +1,7 @@
using System.Numerics;
using Maple2.File.Parser;
using Maple2.File.Parser.Enum;
+using Maple2.File.Parser.Tools;
using Maple2.File.Parser.Xml.Table;
using Maple2.File.Parser.Xml.Table.Server;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -309,6 +310,61 @@ public void TestAdditionalEffectGroup() {
CollectionAssert.AreEqual(new short[] { 2 }, groups[1].value[3].effectLevel);
}
+ [TestMethod]
+ public void TestGlobalEquipmentRankProbability() {
+ var parser = new ServerTableParser(TestUtils.ServerReader);
+
+ Filter.Load(TestUtils.XmlReader, "NA", "Live");
+ Dictionary<(int, int, int), GlobalEquipmentRankProbability> na = parser.ParseGlobalEquipmentRankProbability()
+ .ToDictionary(row => (row.npcRank, row.npcLevel, row.assistCondition));
+ Filter.Load(TestUtils.XmlReader, "KR", "Live");
+ Dictionary<(int, int, int), GlobalEquipmentRankProbability> kr = parser.ParseGlobalEquipmentRankProbability()
+ .ToDictionary(row => (row.npcRank, row.npcLevel, row.assistCondition));
+ Filter.Load(TestUtils.XmlReader, "CN", "Live");
+ Dictionary<(int, int, int), GlobalEquipmentRankProbability> cn = parser.ParseGlobalEquipmentRankProbability()
+ .ToDictionary(row => (row.npcRank, row.npcLevel, row.assistCondition));
+
+ // 18 npcRanks x levels 1-99 x assistCondition 0/1. The KR and CN rows replace rows rather than add them.
+ Assert.AreEqual(18 * 99 * 2, na.Count);
+ Assert.AreEqual(18 * 99 * 2, kr.Count);
+ Assert.AreEqual(18 * 99 * 2, cn.Count);
+
+ // line 41: npcRank="1" npcLevel="20" assistCondition="0" Rare="16.5" Elite="" Excellent="" Legendary=""
+ AssertRow(na[(1, 20, 0)], 16.5f, 0f, 0f, 0f);
+
+ // line 437: npcRank="4" npcLevel="20" assistCondition="0" Rare="80" Elite="20", not its KR or CN override
+ AssertRow(na[(4, 20, 0)], 80f, 20f, 0f, 0f);
+ // line 635: the same key with locale="KR": Rare="100" Elite="0" Excellent="0"
+ AssertRow(kr[(4, 20, 0)], 100f, 0f, 0f, 0f);
+ // line 833: the same key with locale="CN": Rare="16.5"
+ AssertRow(cn[(4, 20, 0)], 16.5f, 0f, 0f, 0f);
+ // lines 488 and 686: npcRank="4" npcLevel="45" assistCondition="1", NA then KR
+ AssertRow(na[(4, 45, 1)], 80f, 20f, 0f, 0f);
+ AssertRow(kr[(4, 45, 1)], 0f, 50f, 50f, 0f);
+ // KR overrides only npcRank 4.
+ AssertRow(kr[(1, 20, 0)], 16.5f, 0f, 0f, 0f);
+
+ // line 1013: npcRank="6" npcLevel="11" assistCondition="0" Rare="16" Elite="83.99999" Excellent="1E-05" Legendary="0"
+ AssertRow(na[(6, 11, 0)], 16f, 83.99999f, 1E-05f, 0f);
+ // lines 1051 and 1052: assistCondition 0 and 1 differ for npcRank="6" npcLevel="30"
+ AssertRow(na[(6, 30, 0)], 0f, 99.99999f, 1E-05f, 0f);
+ AssertRow(na[(6, 30, 1)], 0f, 0f, 100f, 0f);
+
+ // line 1229: npcRank="7" npcLevel="20" assistCondition="0" Rare="16.52" Elite="0.00826446280991736" Excellent="0.000208"
+ AssertRow(na[(7, 20, 0)], 16.52f, 0.00826446280991736f, 0.000208f, 0f);
+ // line 1873: npcRank="90" npcLevel="45" assistCondition="0" Rare="93.8999" Elite="6" Excellent="0.1" Legendary="0.0001"
+ AssertRow(na[(90, 45, 0)], 93.8999f, 6f, 0.1f, 0.0001f);
+ // line 3962, the last row: npcRank="920" npcLevel="99" assistCondition="1" Excellent="100"
+ AssertRow(na[(920, 99, 1)], 0f, 0f, 100f, 0f);
+ }
+
+ private static void AssertRow(GlobalEquipmentRankProbability row, float rare, float elite, float excellent, float legendary) {
+ Assert.AreEqual(rare, row.Rare);
+ Assert.AreEqual(elite, row.Elite);
+ Assert.AreEqual(excellent, row.Excellent);
+ Assert.AreEqual(legendary, row.Legendary);
+ }
+
[TestMethod]
public void TestSpawnInteractObject() {
var parser = new ServerTableParser(TestUtils.ServerReader);