diff --git a/.gitignore b/.gitignore
index c301ca8..bb88804 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,37 +1,37 @@
-# Godot 4.x
-.godot/
-.idea/
-sdk/
-bin/
-obj/
-
-#ai
-.omo/
-.opencode/
-
-gen/
-obj/
-bin/
-
-*.tres~
-*.tscn~
-
-# GDScript LSP
-.godot/
-
-# Export
-*.exe
-*.dmg
-*.apk
-*.aab
-*.pck
-*.zip
-*.log
-
-# OS
-.DS_Store
-Thumbs.db
-
-# Editor
-*.import
-*.godot.uid
+# Godot 4.x
+.godot/
+.idea/
+sdk/
+bin/
+obj/
+
+#ai
+.omo/
+.opencode/
+
+gen/
+obj/
+bin/
+
+*.tres~
+*.tscn~
+
+# GDScript LSP
+.godot/
+
+# Export
+*.exe
+*.dmg
+*.apk
+*.aab
+*.pck
+*.zip
+*.log
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Editor
+*.import
+*.godot.uid
diff --git a/manifest.json b/manifest.json
index 69a0c17..9924556 100644
--- a/manifest.json
+++ b/manifest.json
@@ -8,6 +8,7 @@
"sources": [
"src/**/*.cs"
],
+ "test_sources": ["tests/*.cs"],
"depends": ["ui_base>=1"],
"ui": [
{
diff --git a/src/MapModInitializer.cs b/src/MapModInitializer.cs
index 2526d64..6cc8a79 100644
--- a/src/MapModInitializer.cs
+++ b/src/MapModInitializer.cs
@@ -22,6 +22,8 @@ namespace Cthangover.Mods.Map
public void OnModResourcesReady()
{
var service = MapZoneService.Instance;
+ // Runs again after every mod reload; the service outlives it when map_base stays loaded.
+ service.MarkStale();
MapStateBridge.Register(service.ExportState, service.ImportState, service.ResetOverrides);
}
}
diff --git a/src/MapZoneService.cs b/src/MapZoneService.cs
index 8df5907..a389990 100644
--- a/src/MapZoneService.cs
+++ b/src/MapZoneService.cs
@@ -67,8 +67,10 @@ namespace Cthangover.Mods.Map
///
/// Loads the zone list from the given data, replacing the previous list
- /// but preserving runtime overrides. This is the testable entry point;
- /// runtime code uses .
+ /// but preserving runtime overrides. Zones come in mod load order: a
+ /// zone with an id seen before replaces it in place, so a dependent mod
+ /// overrides a zone the way it overrides any other resource. This is the
+ /// testable entry point; runtime code uses .
///
public void LoadZones(IEnumerable newZones)
{
@@ -82,13 +84,13 @@ namespace Cthangover.Mods.Map
if (zone == null || string.IsNullOrEmpty(zone.ID))
continue;
- if (byId.ContainsKey(zone.ID))
+ if (byId.TryGetValue(zone.ID, out var replaced))
{
- GameLogger.Log("MAP", $"duplicate zone id '{zone.ID}' ignored", LogLevel.Warning);
- continue;
+ GameLogger.Log("MAP", $"zone '{zone.ID}' replaced by a later definition", LogLevel.Warning);
+ zones[zones.IndexOf(replaced)] = zone;
}
-
- zones.Add(zone);
+ else
+ zones.Add(zone);
byId[zone.ID] = zone;
}
}
@@ -114,10 +116,11 @@ namespace Cthangover.Mods.Map
var result = new List();
var options = JsonOptionsProvider.ReadOptions;
- foreach (var kvp in ModManager.Instance.Mods)
+ foreach (var modId in ModRegistry.Instance.GetOrderedModIds())
{
- var modId = kvp.Key;
- var provider = kvp.Value.FileProvider;
+ var provider = ModManager.Instance.GetMod(modId)?.FileProvider;
+ if (provider == null)
+ continue;
foreach (var entry in provider.ListFiles("map"))
{
@@ -157,6 +160,9 @@ namespace Cthangover.Mods.Map
/// Loads zones on first use and re-reads nothing afterwards; call
/// when the mod set or the map files changed.
///
+ /// Drops the loaded zones so the next reads the current mod set (called after every mod set load).
+ public void MarkStale() => loaded = false;
+
public void EnsureLoaded()
{
if (!loaded)
diff --git a/tests/MapZoneServiceTests.cs b/tests/MapZoneServiceTests.cs
new file mode 100644
index 0000000..44eaee4
--- /dev/null
+++ b/tests/MapZoneServiceTests.cs
@@ -0,0 +1,36 @@
+namespace Cthangover.Mods.Map.Tests
+{
+ using System.Collections.Generic;
+ using Cthangover.Core.Mods.Testing;
+
+ ///
+ /// Zone lists arrive in mod load order, and a mod that ships a zone with an
+ /// id already defined replaces it — the same rule as every other mod
+ /// resource, so a dependent mod can reshape a zone of the mod it builds on.
+ ///
+ public class MapZoneServiceTests
+ {
+ [ModTest("zones/later_definition_replaces_earlier")]
+ static void LaterDefinitionReplacesEarlier(ModTestContext ctx)
+ {
+ var service = MapZoneService.Instance;
+ try
+ {
+ service.LoadZones(new List
+ {
+ new() { ID = "test_zone", Title = "owner" },
+ new() { ID = "test_other", Title = "other" },
+ new() { ID = "TEST_ZONE", Title = "dependent" },
+ });
+
+ ModAssert.Equal(2, service.Zones.Count, "zone count");
+ ModAssert.Equal("dependent", service.Get("test_zone").Title, "the later definition wins");
+ ModAssert.Equal("dependent", service.Zones[0].Title, "it keeps the replaced zone's place");
+ }
+ finally
+ {
+ service.ReloadFromMods();
+ }
+ }
+ }
+}