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..6e2e4fd0 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,31 @@ public ProvinceGrid getProvinceGrid() {
return provinceGrid;
}
+ public boolean refreshTrackProvinces() {
+ return refreshTrackProvinces(provinceManager::recalculate);
+ }
+
+ private boolean refreshTrackProvinces(Runnable recalculate) {
+ return TrackProvinceCache.live().refresh(() -> {
+ 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 =
+ 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..652018e6
--- /dev/null
+++ b/src/main/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCache.java
@@ -0,0 +1,46 @@
+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;
+ private boolean vehicleFrameworkUnavailableLogged;
+
+ public static TrackProvinceCache live() {
+ return LIVE;
+ }
+
+ 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 {
+ 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..71ed649d
--- /dev/null
+++ b/src/test/java/net/tfminecraft/simplefactions/map/infra/TrackProvinceCacheTest.java
@@ -0,0 +1,63 @@
+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());
+ }
+
+ @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());
+ }
+}
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());
+ }
+}