From fda254a698b68519b826af750ca4bb6d16723049 Mon Sep 17 00:00:00 2001 From: iisaev Date: Wed, 21 Oct 2026 12:31:57 +0200 Subject: [PATCH] Map zones follow load order and mod reloads; mod unit tests --- .gitignore | 74 ++++++++++++++++++------------------ manifest.json | 1 + src/MapModInitializer.cs | 2 + src/MapZoneService.cs | 26 ++++++++----- tests/MapZoneServiceTests.cs | 36 ++++++++++++++++++ 5 files changed, 92 insertions(+), 47 deletions(-) create mode 100644 tests/MapZoneServiceTests.cs 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(); + } + } + } +}