From ef918e9e7a8cd51331de8f770c9a0b7269ce63e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Mon, 27 Jul 2026 03:49:27 -0300 Subject: [PATCH] Read spawner properties off MS2InteractActor entities A handful of MS2InteractActor entities double as NPC spawn points and carry the SpawnPointNPC property set (SpawnPointID / SpawnRadius / NpcCount / NpcList) on the entity itself. Neither the flat definition nor any mixin declares those properties, so XBlockParser hit a null FlatProperty and discarded every one of them. MS2Actor already had the same treatment: the properties are declared on the interface and patched into the type index by RuntimeClassLookup's manual override block. MS2InteractActor never got it. Mirror that here. A sweep over every xblock finds exactly one such entity today: 11000119_MS2InteractActor_Moonrabbit on 80000022_bonus, which is both the Moonrabbit mortar interact object and the spawn point for the Moon Bunny (27000036). Refs MS2Community/PrivateMaple2#270 --- .../maplestory2library/IMS2InteractActor.cs | 12 ++++ .../MapXBlock/RuntimeClassLookup.cs | 8 +++ Maple2.File.Parser/Maple2.File.Parser.csproj | 2 +- Maple2.File.Tests/InteractActorSpawnerTest.cs | 71 +++++++++++++++++++ 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 Maple2.File.Tests/InteractActorSpawnerTest.cs diff --git a/Maple2.File.Flat/maplestory2library/IMS2InteractActor.cs b/Maple2.File.Flat/maplestory2library/IMS2InteractActor.cs index eeaed21..6b17c4f 100644 --- a/Maple2.File.Flat/maplestory2library/IMS2InteractActor.cs +++ b/Maple2.File.Flat/maplestory2library/IMS2InteractActor.cs @@ -12,5 +12,17 @@ public interface IMS2InteractActor : IActor, IMS2InteractObject, I3DProxy, IMS2M bool MinimapInVisible => true; bool Transparency => true; bool UseInstancing => false; + // Manually added: a handful of MS2InteractActor entities double as NPC spawn points and + // carry the SpawnPointNPC property set (e.g. 11000119_MS2InteractActor_Moonrabbit on + // 80000022_bonus). Neither the flat definition nor any mixin declares them, so they are + // patched into the type index by RuntimeClassLookup the same way MS2Actor's are. + [Obsolete("This property should not exist")] + IDictionary NpcList => new Dictionary(); + [Obsolete("This property should not exist")] + int SpawnPointID => 0; + [Obsolete("This property should not exist")] + float SpawnRadius => 0; + [Obsolete("This property should not exist")] + uint NpcCount => 0; } } diff --git a/Maple2.File.Parser/MapXBlock/RuntimeClassLookup.cs b/Maple2.File.Parser/MapXBlock/RuntimeClassLookup.cs index 5952c2f..88ff607 100644 --- a/Maple2.File.Parser/MapXBlock/RuntimeClassLookup.cs +++ b/Maple2.File.Parser/MapXBlock/RuntimeClassLookup.cs @@ -48,6 +48,14 @@ public RuntimeClassLookup(FlatTypeIndex index) : base(index) { ms2Actor.Properties.Add(spawnRadius.Name, spawnRadius); ms2Actor.Properties.Add(npcCount.Name, npcCount); ms2Actor.Properties.Add(reactableSequenceName.Name, reactableSequenceName); + // Some MS2InteractActor entities double as NPC spawn points (e.g. + // 11000119_MS2InteractActor_Moonrabbit on 80000022_bonus) and carry the SpawnPointNPC + // property set, which the flat definition does not declare. + FlatType ms2InteractActor = index.GetType("MS2InteractActor"); + ms2InteractActor.Properties.TryAdd(npcList.Name, npcList); + ms2InteractActor.Properties.TryAdd(spawnPointID.Name, spawnPointID); + ms2InteractActor.Properties.TryAdd(spawnRadius.Name, spawnRadius); + ms2InteractActor.Properties.TryAdd(npcCount.Name, npcCount); // Generate classes for all model types. This is very quick and allows parallelism without conflicts. foreach (FlatType type in index.GetAllTypes()) { diff --git a/Maple2.File.Parser/Maple2.File.Parser.csproj b/Maple2.File.Parser/Maple2.File.Parser.csproj index 64e582f..f16bc4d 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.16 + 2.4.17 net8.0 README.md enable diff --git a/Maple2.File.Tests/InteractActorSpawnerTest.cs b/Maple2.File.Tests/InteractActorSpawnerTest.cs new file mode 100644 index 0000000..b850588 --- /dev/null +++ b/Maple2.File.Tests/InteractActorSpawnerTest.cs @@ -0,0 +1,71 @@ +using Maple2.File.Flat; +using Maple2.File.Flat.maplestory2library; +using Maple2.File.Parser.Flat; +using Maple2.File.Parser.MapXBlock; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Maple2.File.Tests; + +// A few MS2InteractActor entities double as NPC spawn points and carry the SpawnPointNPC property +// set (SpawnPointID / NpcList / NpcCount / SpawnRadius). Neither the flat definition nor any mixin +// declares those, so RuntimeClassLookup patches them into the type index. See MS2Community/ +// PrivateMaple2#270. +#pragma warning disable CS0618 // properties are intentionally marked obsolete +[TestClass] +public class InteractActorSpawnerTest { + [TestMethod] + public void MoonrabbitInteractActorCarriesSpawnerProperties() { + var index = new FlatTypeIndex(TestUtils.ExportedReader); + var parser = new XBlockParser(TestUtils.ExportedReader, index); + + IMS2InteractActor? moonrabbit = null; + parser.ParseMap("80000022_bonus", entities => { + moonrabbit = entities.OfType() + .FirstOrDefault(actor => actor.interactID == 11000119); + }); + + Assert.IsNotNull(moonrabbit, "80000022_bonus should contain interact actor 11000119"); + Assert.AreEqual(199, moonrabbit.SpawnPointID); + Assert.AreEqual(1u, moonrabbit.NpcCount); + Assert.AreEqual(150f, moonrabbit.SpawnRadius); + CollectionAssert.AreEquivalent( + new[] { "27000036" }, + moonrabbit.NpcList.Keys.ToArray()); + Assert.AreEqual("1", moonrabbit.NpcList["27000036"]); + } + + // Diagnostic, not a regression guard: prints every interact actor in the game that carries + // spawner data so the scope of the oddity is known rather than assumed. As of writing there is + // exactly one. Parses every xblock, so it is opt-in. + [TestMethod] + [Ignore("Diagnostic sweep over every xblock; run manually.")] + public void ReportAllInteractActorsWithSpawnerData() { + var index = new FlatTypeIndex(TestUtils.ExportedReader); + var parser = new XBlockParser(TestUtils.ExportedReader, index); + + var found = new List(); + parser.Parallel().ForAll(map => { + foreach (IMapEntity entity in map.entities) { + if (entity is not IMS2InteractActor actor) { + continue; + } + if (actor.SpawnPointID == 0 && actor.NpcList.Count == 0) { + continue; + } + + string npcs = string.Join(",", actor.NpcList.Select(kv => $"{kv.Key}x{kv.Value}")); + lock (found) { + found.Add($"{map.xblock}: {entity.EntityName} interactID={actor.interactID} " + + $"spawnPoint={actor.SpawnPointID} radius={actor.SpawnRadius} " + + $"count={actor.NpcCount} npcs=[{npcs}]"); + } + } + }); + + Console.WriteLine($"Interact actors with spawner data: {found.Count}"); + foreach (string line in found.OrderBy(x => x)) { + Console.WriteLine(line); + } + } +} +#pragma warning restore CS0618