From 7d0a7036c2e4ec6cda9084bb0242dd9843ae0d72 Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:56:53 -0700 Subject: [PATCH] Parse table/reactor.xml and table/reactorstate.xml `table/reactor.xml` and `table/reactorstate.xml` had no type here, so any consumer that needs to look up which GameEvent a reactor id belongs to (Maple2's `` gate flows through `reactor.gameEventId`) would have to hand-roll an `XmlDocument` walk. - `Xml/Table/Reactor.cs`: `ReactorRoot` locale-filtered by `id` (same row appears once per locale), then attributes `id`, `reactDistance`, `reactorStateList`, `connectedEffect`, `gameEventId`, `rewardItemListXml`, `reactTimeMSec` (defaults 15000), `reactAnimation`, `createAnimation`. `reactorStateList` and `rewardItemListXml` stay strings on purpose: the former is `"[100, 101, 102, 103]"` (M2dArray chokes on the `[]`), the latter is an escaped `` fragment. - `Xml/Table/ReactorState.cs`: `id`, `type` (connect/sleep/reward), `nextTimeSec`, `expireTimeSec`, `kfmName`, `startAni`, `idleAni`, `actionStringKey`, plus a nested `CondItem { code, consume }` element. Only `connect` states carry a populated ``; others self-close. - `TableParser` gets `ParseReactor()` and `ParseReactorState()` matching the other `Parse*` methods. - `PackageVersion` 2.4.21 -> 2.4.22. `dotnet build` clean. All 150 TableParserTest cases pass, including the two new ones (`TestReactor`, `TestReactorState`). Co-Authored-By: Claude Opus 4.7 --- Maple2.File.Parser/Maple2.File.Parser.csproj | 2 +- Maple2.File.Parser/TableParser.cs | 24 ++++++++++++++++ Maple2.File.Parser/Xml/Table/Reactor.cs | 29 ++++++++++++++++++++ Maple2.File.Parser/Xml/Table/ReactorState.cs | 29 ++++++++++++++++++++ Maple2.File.Tests/TableParserTest.cs | 14 ++++++++++ 5 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 Maple2.File.Parser/Xml/Table/Reactor.cs create mode 100644 Maple2.File.Parser/Xml/Table/ReactorState.cs diff --git a/Maple2.File.Parser/Maple2.File.Parser.csproj b/Maple2.File.Parser/Maple2.File.Parser.csproj index c1af205..577b591 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.23 + 2.4.24 net8.0 README.md enable diff --git a/Maple2.File.Parser/TableParser.cs b/Maple2.File.Parser/TableParser.cs index 4c126e9..c45e1f4 100644 --- a/Maple2.File.Parser/TableParser.cs +++ b/Maple2.File.Parser/TableParser.cs @@ -123,6 +123,8 @@ public class TableParser { private readonly XmlSerializer maidRecipeSerializer; private readonly XmlSerializer maidRecipeGroupSerializer; private readonly XmlSerializer maidSalarySerializer; + private readonly XmlSerializer reactorSerializer; + private readonly XmlSerializer reactorStateSerializer; private readonly string locale; private readonly string language; @@ -240,6 +242,8 @@ public TableParser(M2dReader xmlReader, string language) { maidRecipeSerializer = new XmlSerializer(typeof(MaidRecipeRoot)); maidRecipeGroupSerializer = new XmlSerializer(typeof(MaidRecipeGroupRoot)); maidSalarySerializer = new XmlSerializer(typeof(MaidSalaryRoot)); + reactorSerializer = new XmlSerializer(typeof(ReactorRoot)); + reactorStateSerializer = new XmlSerializer(typeof(ReactorStateRoot)); locale = FeatureLocaleFilter.Locale.ToLower(); this.language = language; @@ -1824,4 +1828,24 @@ public IEnumerable ParseJobTableNew() { yield return (entry.id, entry); } } + + public IEnumerable<(int Id, Reactor Reactor)> ParseReactor() { + XmlReader reader = xmlReader.GetXmlReader(xmlReader.GetEntry("table/reactor.xml")); + var data = reactorSerializer.Deserialize(reader) as ReactorRoot; + Debug.Assert(data != null); + + foreach (Reactor reactor in data.reactor) { + yield return (reactor.id, reactor); + } + } + + public IEnumerable<(int Id, ReactorState State)> ParseReactorState() { + XmlReader reader = xmlReader.GetXmlReader(xmlReader.GetEntry("table/reactorstate.xml")); + var data = reactorStateSerializer.Deserialize(reader) as ReactorStateRoot; + Debug.Assert(data != null); + + foreach (ReactorState state in data.reactorState) { + yield return (state.id, state); + } + } } diff --git a/Maple2.File.Parser/Xml/Table/Reactor.cs b/Maple2.File.Parser/Xml/Table/Reactor.cs new file mode 100644 index 0000000..933fd17 --- /dev/null +++ b/Maple2.File.Parser/Xml/Table/Reactor.cs @@ -0,0 +1,29 @@ +using System.Xml.Serialization; +using M2dXmlGenerator; + +namespace Maple2.File.Parser.Xml.Table; + +// ./data/xml/table/reactor.xml +[XmlRoot("ms2")] +public partial class ReactorRoot { + [M2dFeatureLocale(Selector = "id")] private IList _reactor; +} + +public partial class Reactor : IFeatureLocale { + [XmlAttribute] public int id; + [XmlAttribute] public float reactDistance; + // Bracket-wrapped comma list, e.g. "[100, 101, 102, 103]". Consumers strip + // the surrounding [] and split on ','; M2dArray cannot parse it directly + // because int.Parse would choke on the leading '[' / trailing ']' tokens. + [XmlAttribute] public string reactorStateList = string.Empty; + [XmlAttribute] public string connectedEffect = string.Empty; + // gameEventId links this reactor definition to a GameEvent whose eventType is + // "reactor" (GMS2 client RE: reactor+0x70 read by CReactor_CheckReactorGameEvent). + [XmlAttribute] public int gameEventId; + // Escaped XML fragment: . + // Consumers deserialize this separately if they need the reward item list. + [XmlAttribute] public string rewardItemListXml = string.Empty; + [XmlAttribute] public int reactTimeMSec = 15000; + [XmlAttribute] public string reactAnimation = string.Empty; + [XmlAttribute] public string createAnimation = string.Empty; +} diff --git a/Maple2.File.Parser/Xml/Table/ReactorState.cs b/Maple2.File.Parser/Xml/Table/ReactorState.cs new file mode 100644 index 0000000..e79b2b5 --- /dev/null +++ b/Maple2.File.Parser/Xml/Table/ReactorState.cs @@ -0,0 +1,29 @@ +using System.Xml.Serialization; + +namespace Maple2.File.Parser.Xml.Table; + +// ./data/xml/table/reactorstate.xml +[XmlRoot("ms2")] +public class ReactorStateRoot { + [XmlElement] public List reactorState; +} + +public class ReactorState { + [XmlAttribute] public int id; + // Observed values: connect, sleep, reward. + [XmlAttribute] public string type = string.Empty; + [XmlAttribute] public int nextTimeSec; + [XmlAttribute] public int expireTimeSec; + [XmlAttribute] public string kfmName = string.Empty; + [XmlAttribute] public string startAni = string.Empty; + [XmlAttribute] public string idleAni = string.Empty; + [XmlAttribute] public string actionStringKey = string.Empty; + + // Only present on states of type="connect"; absent (self-closing tag) on other states. + [XmlElement] public CondItem condItem; + + public class CondItem { + [XmlAttribute] public int code; + [XmlAttribute] public int consume; + } +} diff --git a/Maple2.File.Tests/TableParserTest.cs b/Maple2.File.Tests/TableParserTest.cs index 3ba5cbb..e67ade7 100644 --- a/Maple2.File.Tests/TableParserTest.cs +++ b/Maple2.File.Tests/TableParserTest.cs @@ -888,4 +888,18 @@ public void TestMaid() { continue; } } + + [TestMethod] + public void TestReactor() { + foreach ((_, _) in _parser.ParseReactor()) { + continue; + } + } + + [TestMethod] + public void TestReactorState() { + foreach ((_, _) in _parser.ParseReactorState()) { + continue; + } + } }