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