From efcffabe35cb4659051d2a1346a13604e70365bb Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:22:17 +0000 Subject: [PATCH 1/2] Count railway track as infrastructure in every province it crosses. Each province a railway passes through becomes an infrastructure source, in owned and unowned land. The set of track provinces is read from VehicleFramework on the server thread at startup, at the day change and on a timer, and recalculations only read the cached set. Co-Authored-By: Claude Opus 5.5 (1M context) --- pom.xml | 2 +- .../net/tfminecraft/simplefactions/Cache.java | 2 + .../simplefactions/SimpleFactions.java | 28 ++++++++++ .../hub/VehicleFrameworkTrackProvinces.java | 26 +++++++++ .../simplefactions/loaders/ConfigLoader.java | 3 ++ .../managers/FactionManager.java | 3 +- .../managers/ProvinceManager.java | 3 +- .../map/infra/InfrastructureSources.java | 7 ++- .../map/infra/TrackProvinceCache.java | 38 +++++++++++++ .../map/infra/TrackProvinceLookup.java | 24 +++++++++ src/main/resources/config.yml | 2 + .../ConfigLoaderInfrastructureTest.java | 13 +++-- .../map/infra/InfrastructureSourcesTest.java | 37 ++++++++++--- .../map/infra/TrackProvinceCacheTest.java | 41 ++++++++++++++ .../map/infra/TrackProvinceLookupTest.java | 53 +++++++++++++++++++ 15 files changed, 268 insertions(+), 14 deletions(-) create mode 100644 src/main/java/net/tfminecraft/simplefactions/guild/hub/VehicleFrameworkTrackProvinces.java create mode 100644 src/main/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCache.java create mode 100644 src/main/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceLookup.java create mode 100644 src/test/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCacheTest.java create mode 100644 src/test/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceLookupTest.java diff --git a/pom.xml b/pom.xml index 261ec9d2..622e1dfd 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ 0.2.4 2.0.1 2.0.0 - 2.7.0 + 2.8.0 2.1.0 diff --git a/src/main/java/net/tfminecraft/simplefactions/Cache.java b/src/main/java/net/tfminecraft/simplefactions/Cache.java index 5d8ae5cd..f25cda73 100644 --- a/src/main/java/net/tfminecraft/simplefactions/Cache.java +++ b/src/main/java/net/tfminecraft/simplefactions/Cache.java @@ -174,6 +174,8 @@ public static boolean requireProvinces(org.bukkit.command.CommandSender sender) public static double infrastructureStation = 10; public static double infrastructurePort = 10; public static double infrastructureAirport = 5; + public static double infrastructureTrack = 10; + public static int infrastructureTrackRefreshSeconds = 300; public static Map baseEffects = new HashMap<>(); diff --git a/src/main/java/net/tfminecraft/simplefactions/SimpleFactions.java b/src/main/java/net/tfminecraft/simplefactions/SimpleFactions.java index 6a513ac2..9a47650c 100644 --- a/src/main/java/net/tfminecraft/simplefactions/SimpleFactions.java +++ b/src/main/java/net/tfminecraft/simplefactions/SimpleFactions.java @@ -9,9 +9,11 @@ import org.bukkit.event.Listener; import org.bukkit.event.server.PluginEnableEvent; import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; import net.tfminecraft.simplefactions.Cache; import net.tfminecraft.simplefactions.map.ProvinceGrid; +import net.tfminecraft.simplefactions.map.infra.TrackProvinceCache; import net.tfminecraft.simplefactions.map.presence.ProvincePresenceListener; import net.tfminecraft.simplefactions.map.presence.ProvincePresenceService; import net.tfminecraft.simplefactions.map.presence.ProvincePresenceTickService; @@ -169,6 +171,7 @@ public class SimpleFactions extends JavaPlugin{ new net.tfminecraft.simplefactions.mercenary.contract.AttendanceService.Hook(); private ProvinceManager provinceSnapshot = new ProvinceManager(); private ProvinceGrid provinceGrid; + private BukkitTask trackInfrastructureRefreshTask; private final PlayerVehicleRegistry vehicleRegistry = new PlayerVehicleRegistry(); private VehicleRegistryPersistence vehicleRegistryPersistence; private final InstallationVehicleOwnerSync installationVehicleOwnerSync = @@ -345,15 +348,20 @@ public void onEnable() { sessionManager.start(); if (Cache.provincesEnabled) { provinceSnapshot = provinceManager.createSnapshotShell(); + refreshTrackProvinces(() -> {}); provinceManager.recalculate(); // Hub tax is assessed at the day change; do it once now so menus are right before then. net.tfminecraft.simplefactions.guild.hub.HubTaxService.refresh(provinceManager); + long refreshTicks = Cache.infrastructureTrackRefreshSeconds * 20L; + trackInfrastructureRefreshTask = getServer().getScheduler().runTaskTimer( + this, () -> { refreshTrackProvinces(); }, refreshTicks, refreshTicks); } inventoryManager.start(); vehicleMaintenanceDecayTask.start(); } @Override public void onDisable() { + if (trackInfrastructureRefreshTask != null) trackInfrastructureRefreshTask.cancel(); net.tfminecraft.simplefactions.integration.rpcharacters.chat.RpCharactersChatIntegration.unregister(); MercenaryStatService.clearAll(); net.tfminecraft.simplefactions.mercenary.company.MercenaryEligibility.reset(); @@ -518,6 +526,26 @@ public ProvinceGrid getProvinceGrid() { return provinceGrid; } + public boolean refreshTrackProvinces() { + return refreshTrackProvinces(provinceManager::recalculate); + } + + private boolean refreshTrackProvinces(Runnable recalculate) { + return TrackProvinceCache.live().refresh(() -> { + if (provinceGrid == null || !getServer().getPluginManager().isPluginEnabled("VehicleFramework")) { + return java.util.Set.of(); + } + java.util.Map provinces = + new java.util.HashMap<>(); + for (net.tfminecraft.simplefactions.map.provinces.Province province : provinceManager.getProvinces()) { + provinces.put(province.getId(), province); + } + return net.tfminecraft.simplefactions.guild.hub.VehicleFrameworkTrackProvinces.sample( + Cache.worldName, provinceGrid, provinces); + }, recalculate, + message -> getLogger().warning("[SimpleFactions] " + message)); + } + public SessionManager getSessionManager() { return sessionManager; } diff --git a/src/main/java/net/tfminecraft/simplefactions/guild/hub/VehicleFrameworkTrackProvinces.java b/src/main/java/net/tfminecraft/simplefactions/guild/hub/VehicleFrameworkTrackProvinces.java new file mode 100644 index 00000000..6c43b85f --- /dev/null +++ b/src/main/java/net/tfminecraft/simplefactions/guild/hub/VehicleFrameworkTrackProvinces.java @@ -0,0 +1,26 @@ +package net.tfminecraft.simplefactions.guild.hub; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.tfminecraft.simplefactions.map.ProvinceGrid; +import net.tfminecraft.simplefactions.map.infra.TrackProvinceLookup; +import net.tfminecraft.simplefactions.map.infra.TrackProvinceLookup.Point; +import net.tfminecraft.simplefactions.map.provinces.Province; +import net.tfminecraft.vehicleframework.VehicleFramework; +import net.tfminecraft.vehicleframework.tracks.TrackRegistry; +import net.tfminecraft.vehicleframework.tracks.TrackSamplePoint; + +/** The only track sampler here that names VehicleFramework's sample point type. */ +public final class VehicleFrameworkTrackProvinces { + private VehicleFrameworkTrackProvinces() {} + + public static Set sample(String world, ProvinceGrid grid, Map provinces) { + TrackRegistry tracks = VehicleFramework.getTrackRegistry(); + if (tracks == null || world == null) return Set.of(); + List samples = tracks.sampleTrack(world, 8); + return TrackProvinceLookup.collect(grid, samples.stream() + .map(point -> new Point(point.x(), point.y(), point.z())).toList(), provinces); + } +} diff --git a/src/main/java/net/tfminecraft/simplefactions/loaders/ConfigLoader.java b/src/main/java/net/tfminecraft/simplefactions/loaders/ConfigLoader.java index 80c45e45..910ba0d9 100644 --- a/src/main/java/net/tfminecraft/simplefactions/loaders/ConfigLoader.java +++ b/src/main/java/net/tfminecraft/simplefactions/loaders/ConfigLoader.java @@ -82,6 +82,9 @@ public void loadConfig(File configFile) { Cache.infrastructureStation = config.getDouble("infrastructure.station", 10); Cache.infrastructurePort = config.getDouble("infrastructure.port", 10); Cache.infrastructureAirport = config.getDouble("infrastructure.airport", 5); + Cache.infrastructureTrack = config.getDouble("infrastructure.track", 10); + Cache.infrastructureTrackRefreshSeconds = Math.max(30, + config.getInt("infrastructure.track-refresh-seconds", 300)); Cache.settlementLargePopulationThreshold = config.getInt("settlement-large-population-threshold", 8); Cache.portSeaProximityBlocks = config.getInt("port-sea-proximity-blocks", 20); diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/FactionManager.java b/src/main/java/net/tfminecraft/simplefactions/managers/FactionManager.java index 857d7d19..03661f5d 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/FactionManager.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/FactionManager.java @@ -503,7 +503,8 @@ public void time() { // and settle the day on trade that reflects them. runDailyStep("supply hub links", () -> { net.tfminecraft.simplefactions.guild.hub.HubNetwork.forgetRoutes(); - SimpleFactions.getInstance().getProvinceManager().recalculate(); + boolean tracksChanged = SimpleFactions.getInstance().refreshTrackProvinces(); + if (!tracksChanged) SimpleFactions.getInstance().getProvinceManager().recalculate(); net.tfminecraft.simplefactions.guild.hub.HubTaxService.refresh( SimpleFactions.getInstance().getProvinceManager()); }); diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/ProvinceManager.java b/src/main/java/net/tfminecraft/simplefactions/managers/ProvinceManager.java index 5014f703..c0e4b6c4 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/ProvinceManager.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/ProvinceManager.java @@ -152,7 +152,8 @@ private void recalculateInfrastructure() { province.getTradeCarry(), !province.isSea(), origin, province.getNeighbours())); } Map sources = InfrastructureSources.collect( - provinces, FactionManager.getAllGuilds(), FactionManager.getCopy()); + provinces, FactionManager.getAllGuilds(), FactionManager.getCopy(), + net.tfminecraft.simplefactions.map.infra.TrackProvinceCache.live().provinces()); Map infrastructure = InfrastructureSpread.spread( graph, sources, Cache.infrastructureWildernessSpread, Cache.infrastructureSpreadFloor); for (Province province : provinces.values()) { diff --git a/src/main/java/net/tfminecraft/simplefactions/map/infra/InfrastructureSources.java b/src/main/java/net/tfminecraft/simplefactions/map/infra/InfrastructureSources.java index 0a14d0bc..d646a38e 100644 --- a/src/main/java/net/tfminecraft/simplefactions/map/infra/InfrastructureSources.java +++ b/src/main/java/net/tfminecraft/simplefactions/map/infra/InfrastructureSources.java @@ -3,6 +3,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Set; import net.tfminecraft.simplefactions.Cache; import net.tfminecraft.simplefactions.enums.GuildModifier; @@ -16,7 +17,8 @@ public final class InfrastructureSources { private InfrastructureSources() {} public static Map collect( - Map provinces, Collection guilds, Collection factions) { + Map provinces, Collection guilds, Collection factions, + Set trackProvinces) { Map sources = new HashMap<>(); for (Guild guild : guilds) { if (guild == null || !guild.isBase() || !guild.hasCapital()) continue; @@ -34,6 +36,9 @@ public static Map collect( add(provinces, sources, installation.getProvince(), amount); } } + for (Integer id : trackProvinces) { + add(provinces, sources, id, Cache.infrastructureTrack); + } return sources; } diff --git a/src/main/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCache.java b/src/main/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCache.java new file mode 100644 index 00000000..42088e95 --- /dev/null +++ b/src/main/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCache.java @@ -0,0 +1,38 @@ +package net.tfminecraft.simplefactions.map.infra; + +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** Immutable track source snapshot shared by live and preview recalculations. */ +public final class TrackProvinceCache { + private static final TrackProvinceCache LIVE = new TrackProvinceCache(); + + private volatile Set provinces = Set.of(); + private boolean warned; + + public static TrackProvinceCache live() { + return LIVE; + } + + public Set provinces() { + return provinces; + } + + public boolean refresh(Supplier> sampler, Runnable recalculate, Consumer warning) { + Set sampled; + try { + sampled = Set.copyOf(sampler.get()); + } catch (RuntimeException | LinkageError e) { + sampled = Set.of(); + if (!warned) { + warned = true; + warning.accept("Could not read VehicleFramework tracks; track infrastructure is disabled."); + } + } + if (provinces.equals(sampled)) return false; + provinces = sampled; + recalculate.run(); + return true; + } +} diff --git a/src/main/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceLookup.java b/src/main/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceLookup.java new file mode 100644 index 00000000..1dc08123 --- /dev/null +++ b/src/main/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceLookup.java @@ -0,0 +1,24 @@ +package net.tfminecraft.simplefactions.map.infra; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import net.tfminecraft.simplefactions.map.ProvinceGrid; +import net.tfminecraft.simplefactions.map.provinces.Province; + +public final class TrackProvinceLookup { + public record Point(double x, double y, double z) {} + + private TrackProvinceLookup() {} + + public static Set collect(ProvinceGrid grid, Iterable points, Map provinces) { + Set ids = new HashSet<>(); + for (Point point : points) { + int id = grid.getAt((int) Math.floor(point.x()), (int) Math.floor(point.z())); + Province province = provinces.get(id); + if (id != 0 && province != null && !province.isSea()) ids.add(id); + } + return Set.copyOf(ids); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 39ab4746..cda46421 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -193,6 +193,8 @@ infrastructure: station: 10 port: 10 airport: 5 + track: 10 + track-refresh-seconds: 300 terrain-modifiers: - FARMLAND 0.95 diff --git a/src/test/java/net/tfminecraft/simplefactions/loaders/ConfigLoaderInfrastructureTest.java b/src/test/java/net/tfminecraft/simplefactions/loaders/ConfigLoaderInfrastructureTest.java index 14458d71..c9a289bc 100644 --- a/src/test/java/net/tfminecraft/simplefactions/loaders/ConfigLoaderInfrastructureTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/loaders/ConfigLoaderInfrastructureTest.java @@ -26,6 +26,8 @@ void restore() { Cache.infrastructureStation = previous[4]; Cache.infrastructurePort = previous[5]; Cache.infrastructureAirport = previous[6]; + Cache.infrastructureTrack = previous[7]; + Cache.infrastructureTrackRefreshSeconds = (int) previous[8]; } @Test @@ -42,7 +44,7 @@ void missingSectionUsesDefaultsAndResetsPreviousSettings() throws IOException { """); load("enable-map: false\n"); - assertValues(20, 0.75, 0.25, 0.5, 10, 10, 5); + assertValues(20, 0.75, 0.25, 0.5, 10, 10, 5, 10, 300); } @Test @@ -52,7 +54,7 @@ void missingKeysUseDefaultsIndependently() throws IOException { full: 30 """); - assertValues(30, 0.75, 0.25, 0.5, 10, 10, 5); + assertValues(30, 0.75, 0.25, 0.5, 10, 10, 5, 10, 300); } @Test @@ -66,9 +68,11 @@ void configuredKeysAreReadIntoCache() throws IOException { station: 12 port: 8 airport: 4 + track: 17 + track-refresh-seconds: 45 """); - assertValues(30, 0.80, 0.10, 0.25, 12, 8, 4); + assertValues(30, 0.80, 0.10, 0.25, 12, 8, 4, 17, 45); } @Test @@ -101,7 +105,8 @@ private void load(String yaml) throws IOException { private static double[] values() { return new double[] {Cache.infrastructureFull, Cache.infrastructureTarget, Cache.infrastructureWildernessSpread, Cache.infrastructureSpreadFloor, - Cache.infrastructureStation, Cache.infrastructurePort, Cache.infrastructureAirport}; + Cache.infrastructureStation, Cache.infrastructurePort, Cache.infrastructureAirport, + Cache.infrastructureTrack, Cache.infrastructureTrackRefreshSeconds}; } private static void assertValues(double... expected) { diff --git a/src/test/java/net/tfminecraft/simplefactions/map/infra/InfrastructureSourcesTest.java b/src/test/java/net/tfminecraft/simplefactions/map/infra/InfrastructureSourcesTest.java index 7193ea89..5612fc9a 100644 --- a/src/test/java/net/tfminecraft/simplefactions/map/infra/InfrastructureSourcesTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/map/infra/InfrastructureSourcesTest.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -28,6 +29,7 @@ class InfrastructureSourcesTest { private final double station = Cache.infrastructureStation; private final double port = Cache.infrastructurePort; private final double airport = Cache.infrastructureAirport; + private final double track = Cache.infrastructureTrack; @AfterEach void restore() { @@ -35,13 +37,14 @@ void restore() { Cache.infrastructureStation = station; Cache.infrastructurePort = port; Cache.infrastructureAirport = airport; + Cache.infrastructureTrack = track; } @Test void realmCapitalUsesTheResolvedModifierOnce() { Guild guild = guild(true, 1, 12.5); Map sources = InfrastructureSources.collect( - Map.of(1, province(1, "forest")), List.of(guild), List.of()); + Map.of(1, province(1, "forest")), List.of(guild), List.of(), Set.of()); assertEquals(12.5, sources.get(1)); verify(guild, times(1)).getModifier(GuildModifier.INFRASTRUCTURE); @@ -54,7 +57,7 @@ void capitalUsesPreviewOverrideInsteadOfTheLiveModifier() { Guild guild = guild(true, 1, 12.5); GuildModifierOverride.use(guild, Map.of(GuildModifier.INFRASTRUCTURE, 17.0)); Map sources = InfrastructureSources.collect( - Map.of(1, province(1, "forest")), List.of(guild), List.of()); + Map.of(1, province(1, "forest")), List.of(guild), List.of(), Set.of()); assertEquals(17, sources.get(1)); verify(guild, never()).getModifier(GuildModifier.INFRASTRUCTURE); @@ -67,7 +70,7 @@ void nonBaseGuildsAndGuildsWithoutCapitalsAddNothing() { when(noCapital.hasCapital()).thenReturn(false); assertTrue(InfrastructureSources.collect(Map.of(1, province(1, "forest")), - List.of(regular, noCapital), List.of()).isEmpty()); + List.of(regular, noCapital), List.of(), Set.of()).isEmpty()); verify(regular, never()).getModifier(GuildModifier.INFRASTRUCTURE); verify(noCapital, never()).getModifier(GuildModifier.INFRASTRUCTURE); } @@ -84,7 +87,7 @@ void installationsUseConfigAmountsWithoutScalingByLevelAndFortsAddNothing() { Map sources = InfrastructureSources.collect(Map.of( 1, province(1, "forest"), 2, province(2, "forest"), 3, province(3, "forest"), 4, province(4, "forest")), List.of(), - List.of(faction(station, port, airport, fort))); + List.of(faction(station, port, airport, fort)), Set.of()); assertEquals(Map.of(1, 7.0, 2, 11.0, 3, 3.0), sources); verify(station, never()).getLevel(); @@ -98,7 +101,7 @@ void sourcesInOneProvinceAddBeforeTheySpread() { Guild guild = guild(true, 1, 10); Map sources = InfrastructureSources.collect(Map.of( 1, province(1, "forest"), 2, province(2, "plains")), List.of(guild), - List.of(faction(installation(InstallationKind.TRAIN_STATION, 1)))); + List.of(faction(installation(InstallationKind.TRAIN_STATION, 1))), Set.of()); Map result = InfrastructureSpread.spread(Map.of( 1, new InfrastructureSpread.Node(0.6, true, "realm", List.of(2)), 2, new InfrastructureSpread.Node(0.75, true, "realm", List.of(1))), sources, 0.25, 0.5); @@ -114,11 +117,33 @@ void sourcesOutsideTheManagerAndOnSeaOrWaterAreIgnored() { List.of(guild(true, 1, 10), guild(true, 2, 10), guild(true, 3, 10)), List.of(faction(installation(InstallationKind.PORT, 1), installation(InstallationKind.TRAIN_STATION, 2), - installation(InstallationKind.AIRPORT, 3)))); + installation(InstallationKind.AIRPORT, 3))), Set.of()); assertTrue(sources.isEmpty()); } + @Test + void trackProvincesAddOnceAndCombineWithOtherSources() { + Cache.infrastructureTrack = 10; + Map sources = InfrastructureSources.collect(Map.of( + 1, province(1, "forest"), 2, province(2, "sea"), 3, province(3, "plains")), + List.of(), List.of(faction(installation(InstallationKind.TRAIN_STATION, 1))), Set.of(1, 2, 3)); + + assertEquals(Map.of(1, 20.0, 3, 10.0), sources); + } + + @Test + void trackProvinceSourceSpreadsOnlyThroughUnownedLand() { + Map provinces = Map.of(1, province(1, "plains"), 2, province(2, "plains")); + Map sources = InfrastructureSources.collect(provinces, List.of(), List.of(), Set.of(1)); + Map result = InfrastructureSpread.spread(Map.of( + 1, new InfrastructureSpread.Node(0.75, true, null, List.of(2)), + 2, new InfrastructureSpread.Node(0.75, true, "realm", List.of(1))), sources, 0.25, 0.5); + + assertEquals(10, result.get(1).amount()); + assertTrue(!result.containsKey(2)); + } + private static Province province(int id, String terrain) { return new Province(id, terrain, 0); } diff --git a/src/test/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCacheTest.java b/src/test/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCacheTest.java new file mode 100644 index 00000000..a442a2dc --- /dev/null +++ b/src/test/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCacheTest.java @@ -0,0 +1,41 @@ +package net.tfminecraft.simplefactions.map.infra; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +class TrackProvinceCacheTest { + @Test + void changedSetRecalculatesOnceAndUnchangedSetDoesNothing() { + TrackProvinceCache cache = new TrackProvinceCache(); + AtomicInteger recalculations = new AtomicInteger(); + + assertTrue(cache.refresh(() -> Set.of(5, 6), recalculations::incrementAndGet, message -> {})); + assertEquals(Set.of(5, 6), cache.provinces()); + assertFalse(cache.refresh(() -> Set.of(6, 5), recalculations::incrementAndGet, message -> {})); + assertEquals(1, recalculations.get()); + } + + @Test + void linkageFailureClearsSetAndWarnsOnlyOnce() { + TrackProvinceCache cache = new TrackProvinceCache(); + AtomicInteger warnings = new AtomicInteger(); + AtomicInteger recalculations = new AtomicInteger(); + cache.refresh(() -> Set.of(5), recalculations::incrementAndGet, message -> {}); + + assertTrue(cache.refresh(() -> { throw new LinkageError("missing sampleTrack"); }, + recalculations::incrementAndGet, message -> warnings.incrementAndGet())); + assertFalse(cache.refresh(() -> { throw new LinkageError("missing sampleTrack"); }, + recalculations::incrementAndGet, message -> warnings.incrementAndGet())); + + assertEquals(Set.of(), cache.provinces()); + assertEquals(1, warnings.get()); + assertEquals(2, recalculations.get()); + } +} diff --git a/src/test/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceLookupTest.java b/src/test/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceLookupTest.java new file mode 100644 index 00000000..e5b2865c --- /dev/null +++ b/src/test/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceLookupTest.java @@ -0,0 +1,53 @@ +package net.tfminecraft.simplefactions.map.infra; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.zip.GZIPOutputStream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import net.tfminecraft.simplefactions.map.ProvinceGrid; +import net.tfminecraft.simplefactions.map.provinces.Province; + +class TrackProvinceLookupTest { + @TempDir + Path tempDir; + + @Test + void samplesMapToDistinctLandProvinces() throws IOException { + ProvinceGrid grid = grid(); + Map provinces = Map.of( + 5, new Province(5, "plains", 0), + 6, new Province(6, "forest", 0), + 7, new Province(7, "sea", 0)); + + assertEquals(java.util.Set.of(5, 6), TrackProvinceLookup.collect(grid, List.of( + new TrackProvinceLookup.Point(0.5, 64, 0), + new TrackProvinceLookup.Point(1.5, 64, 0), + new TrackProvinceLookup.Point(2.5, 64, 0), + new TrackProvinceLookup.Point(3.5, 64, 0), + new TrackProvinceLookup.Point(4.5, 64, 0)), provinces)); + } + + private ProvinceGrid grid() throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (GZIPOutputStream gzip = new GZIPOutputStream(bytes)) { + gzip.write(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putInt(5).putInt(1).array()); + try (DataOutputStream out = new DataOutputStream(gzip)) { + for (int id : new int[] {5, 5, 6, 0, 7}) out.writeShort(Short.reverseBytes((short) id)); + } + } + Path file = tempDir.resolve("grid.gz"); + java.nio.file.Files.write(file, bytes.toByteArray()); + return ProvinceGrid.load(file.toFile()); + } +} From 0a54ac5436aafb55813074a29c41fa8c9f4bbfb4 Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:32:58 +0000 Subject: [PATCH 2/2] Log once when VehicleFramework is absent and track gives no infrastructure. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../simplefactions/SimpleFactions.java | 7 +++++- .../map/infra/TrackProvinceCache.java | 8 +++++++ .../map/infra/TrackProvinceCacheTest.java | 22 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/tfminecraft/simplefactions/SimpleFactions.java b/src/main/java/net/tfminecraft/simplefactions/SimpleFactions.java index 9a47650c..6e2e4fd0 100644 --- a/src/main/java/net/tfminecraft/simplefactions/SimpleFactions.java +++ b/src/main/java/net/tfminecraft/simplefactions/SimpleFactions.java @@ -532,7 +532,12 @@ public boolean refreshTrackProvinces() { private boolean refreshTrackProvinces(Runnable recalculate) { return TrackProvinceCache.live().refresh(() -> { - if (provinceGrid == null || !getServer().getPluginManager().isPluginEnabled("VehicleFramework")) { + if (provinceGrid == null) { + return java.util.Set.of(); + } + if (!getServer().getPluginManager().isPluginEnabled("VehicleFramework")) { + TrackProvinceCache.live().vehicleFrameworkUnavailable( + message -> getLogger().info("[SimpleFactions] " + message)); return java.util.Set.of(); } java.util.Map provinces = diff --git a/src/main/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCache.java b/src/main/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCache.java index 42088e95..652018e6 100644 --- a/src/main/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCache.java +++ b/src/main/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCache.java @@ -10,6 +10,7 @@ public final class TrackProvinceCache { private volatile Set provinces = Set.of(); private boolean warned; + private boolean vehicleFrameworkUnavailableLogged; public static TrackProvinceCache live() { return LIVE; @@ -19,6 +20,13 @@ public Set provinces() { return provinces; } + public void vehicleFrameworkUnavailable(Consumer info) { + if (!vehicleFrameworkUnavailableLogged) { + vehicleFrameworkUnavailableLogged = true; + info.accept("VehicleFramework is not enabled; railway track gives no infrastructure."); + } + } + public boolean refresh(Supplier> sampler, Runnable recalculate, Consumer warning) { Set sampled; try { diff --git a/src/test/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCacheTest.java b/src/test/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCacheTest.java index a442a2dc..71ed649d 100644 --- a/src/test/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCacheTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCacheTest.java @@ -38,4 +38,26 @@ void linkageFailureClearsSetAndWarnsOnlyOnce() { assertEquals(1, warnings.get()); assertEquals(2, recalculations.get()); } + + @Test + void vehicleFrameworkAbsentLogsOnlyOnce() { + TrackProvinceCache cache = new TrackProvinceCache(); + AtomicInteger infoMessages = new AtomicInteger(); + AtomicInteger recalculations = new AtomicInteger(); + + assertFalse(cache.refresh(() -> { + cache.vehicleFrameworkUnavailable(message -> { + assertEquals("VehicleFramework is not enabled; railway track gives no infrastructure.", message); + infoMessages.incrementAndGet(); + }); + return Set.of(); + }, recalculations::incrementAndGet, message -> {})); + assertFalse(cache.refresh(() -> { + cache.vehicleFrameworkUnavailable(message -> infoMessages.incrementAndGet()); + return Set.of(); + }, recalculations::incrementAndGet, message -> {})); + + assertEquals(1, infoMessages.get()); + assertEquals(0, recalculations.get()); + } }