Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Maple2.File.Flat/maplestory2library/IMS2InteractActor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> NpcList => new Dictionary<string, string>();
[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;
}
}
8 changes: 8 additions & 0 deletions Maple2.File.Parser/MapXBlock/RuntimeClassLookup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
2 changes: 1 addition & 1 deletion Maple2.File.Parser/Maple2.File.Parser.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<PackageTags>MapleStory2, File, Parser, m2d, xml</PackageTags>
<!-- Use following lines to write the generated files to disk. -->
<EmitCompilerGeneratedFiles Condition=" '$(Configuration)' == 'Debug' ">true</EmitCompilerGeneratedFiles>
<PackageVersion>2.4.16</PackageVersion>
<PackageVersion>2.4.17</PackageVersion>
<TargetFramework>net8.0</TargetFramework>
<PackageReadmeFile>README.md</PackageReadmeFile>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
71 changes: 71 additions & 0 deletions Maple2.File.Tests/InteractActorSpawnerTest.cs
Original file line number Diff line number Diff line change
@@ -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<IMS2InteractActor>()
.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<string>();
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
Loading