From 14600ad6d2d4fd00ed66b19abaf496b95c1f6fc3 Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:13:58 +0000 Subject: [PATCH 1/2] Export infrastructure and effective terrain per province for the web map. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../simplefactions/map/Compiler.java | 71 +++++++----- .../map/CompilerProvinceExportTest.java | 106 ++++++++++++++++++ 2 files changed, 148 insertions(+), 29 deletions(-) create mode 100644 src/test/java/net/tfminecraft/simplefactions/map/CompilerProvinceExportTest.java diff --git a/src/main/java/net/tfminecraft/simplefactions/map/Compiler.java b/src/main/java/net/tfminecraft/simplefactions/map/Compiler.java index da812c5a..93466e56 100644 --- a/src/main/java/net/tfminecraft/simplefactions/map/Compiler.java +++ b/src/main/java/net/tfminecraft/simplefactions/map/Compiler.java @@ -15,15 +15,17 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import net.tfminecraft.simplefactions.Cache; +import net.tfminecraft.simplefactions.SimpleFactions; import net.tfminecraft.simplefactions.guild.Guild; import net.tfminecraft.simplefactions.managers.FactionManager; import net.tfminecraft.simplefactions.managers.ProvinceManager; import net.tfminecraft.simplefactions.managers.TitleManager; import net.tfminecraft.simplefactions.managers.WarManager; import net.tfminecraft.simplefactions.map.export.OccupationMapExport; +import net.tfminecraft.simplefactions.map.infra.EffectiveTerrain; import net.tfminecraft.simplefactions.map.provinces.Province; import net.tfminecraft.simplefactions.map.provinces.ProvinceDataEntry; -import net.tfminecraft.simplefactions.SimpleFactions; public class Compiler { public void exportQueue(HashMap> queues) { @@ -71,6 +73,44 @@ private static double r2(double v) { return Math.round(v * 100.0) / 100.0; } + static JsonObject provinceToJson(Province p, String occupierId) { + JsonObject o = new JsonObject(); + + o.addProperty("id", p.getId()); + o.addProperty("prosperity", p.getProsperity()); + if (occupierId != null) { + o.addProperty("occupied_by", occupierId); + } + + // trade data + JsonObject trade = new JsonObject(); + for (Map.Entry e : p.getAllData().entrySet()) { + ProvinceDataEntry d = e.getValue(); + if (d.getTrade() < 0.1) continue; + + JsonObject g = new JsonObject(); + g.addProperty("trade", r2(d.getTrade())); + g.addProperty("production", r2(d.getProduction())); + trade.add(e.getKey(), g); + } + o.add("trade", trade); + + if (!p.isSea()) { + double terrain = p.getTradeCarry(); + double infrastructure = p.getInfrastructure(); + o.addProperty("terrain", p.getTerrain().name().toLowerCase()); + o.addProperty("terrain_value", r2(terrain)); + if (infrastructure > 0) { + o.addProperty("infrastructure", r2(infrastructure)); + o.addProperty("infrastructure_fill", r2(Math.min(1, infrastructure / Cache.infrastructureFull))); + } + o.addProperty("effective_terrain", r2(EffectiveTerrain.calculate(terrain, infrastructure, + Cache.infrastructureFull, Cache.infrastructureTarget, 1))); + } + + return o; + } + public void exportProvincesToJson(File out) throws Exception { JsonArray arr = new JsonArray(); @@ -80,34 +120,7 @@ public void exportProvincesToJson(File out) throws Exception { TitleManager::getByProvince); for (Province p : pm.getProvinces()) { - JsonObject o = new JsonObject(); - - o.addProperty("id", p.getId()); - o.addProperty("prosperity", p.getProsperity()); - String occupierId = occupiedBy.get(p.getId()); - if (occupierId != null) { - o.addProperty("occupied_by", occupierId); - } - - // trade data - JsonObject trade = new JsonObject(); - double totalTrade = 0; - - for (Map.Entry e : p.getAllData().entrySet()) { - ProvinceDataEntry d = e.getValue(); - if (d.getTrade() < 0.1) continue; - - JsonObject g = new JsonObject(); - g.addProperty("trade", r2(d.getTrade())); - g.addProperty("production", r2(d.getProduction())); - - trade.add(e.getKey(), g); - totalTrade += d.getTrade(); - } - - o.add("trade", trade); - - arr.add(o); + arr.add(provinceToJson(p, occupiedBy.get(p.getId()))); } try (FileWriter w = new FileWriter(out)) { diff --git a/src/test/java/net/tfminecraft/simplefactions/map/CompilerProvinceExportTest.java b/src/test/java/net/tfminecraft/simplefactions/map/CompilerProvinceExportTest.java new file mode 100644 index 00000000..7bad1dfd --- /dev/null +++ b/src/test/java/net/tfminecraft/simplefactions/map/CompilerProvinceExportTest.java @@ -0,0 +1,106 @@ +package net.tfminecraft.simplefactions.map; + +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 static org.mockito.Mockito.mock; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.google.gson.JsonObject; + +import net.tfminecraft.simplefactions.Cache; +import net.tfminecraft.simplefactions.enums.Terrain; +import net.tfminecraft.simplefactions.guild.Guild; +import net.tfminecraft.simplefactions.map.provinces.Province; +import net.tfminecraft.simplefactions.map.provinces.ProvinceDataEntry; + +class CompilerProvinceExportTest { + private final Map previousCarry = new HashMap<>(Cache.tradeCarry); + private final double previousFull = Cache.infrastructureFull; + private final double previousTarget = Cache.infrastructureTarget; + + @BeforeEach + void setUp() { + Cache.infrastructureFull = 20; + Cache.infrastructureTarget = 0.75; + Cache.tradeCarry.clear(); + Cache.tradeCarry.put(Terrain.BOG, 0.40); + Cache.tradeCarry.put(Terrain.PLAINS, 0.75); + Cache.tradeCarry.put(Terrain.SEA, 0.60); + Cache.tradeCarry.put(Terrain.WATER, 0.75); + } + + @AfterEach + void restore() { + Cache.infrastructureFull = previousFull; + Cache.infrastructureTarget = previousTarget; + Cache.tradeCarry.clear(); + Cache.tradeCarry.putAll(previousCarry); + } + + @Test + void bogWithInfrastructureExportsFillAndEffectiveTerrain() { + Province province = new Province(1, "bog", 0); + province.setInfrastructure(12); + + JsonObject json = Compiler.provinceToJson(province, null); + + assertEquals("bog", json.get("terrain").getAsString()); + assertEquals(0.40, json.get("terrain_value").getAsDouble()); + assertEquals(12, json.get("infrastructure").getAsDouble()); + assertEquals(0.60, json.get("infrastructure_fill").getAsDouble()); + assertEquals(0.61, json.get("effective_terrain").getAsDouble()); + } + + @Test + void bogWithoutInfrastructureOmitsInfrastructureFields() { + JsonObject json = Compiler.provinceToJson(new Province(1, "bog", 0), null); + + assertEquals(0.40, json.get("effective_terrain").getAsDouble()); + assertFalse(json.has("infrastructure")); + assertFalse(json.has("infrastructure_fill")); + } + + @Test + void infrastructureDoesNotRaisePlainsAboveItsTerrainValue() { + Province province = new Province(1, "plains", 0); + province.setInfrastructure(20); + + assertEquals(0.75, Compiler.provinceToJson(province, null).get("effective_terrain").getAsDouble()); + } + + @Test + void seaAndWaterOmitAllNewTerrainFields() { + for (String terrain : new String[] {"sea", "water"}) { + JsonObject json = Compiler.provinceToJson(new Province(1, terrain, 0), null); + assertFalse(json.has("terrain")); + assertFalse(json.has("terrain_value")); + assertFalse(json.has("infrastructure")); + assertFalse(json.has("infrastructure_fill")); + assertFalse(json.has("effective_terrain")); + } + } + + @Test + void tradeObjectAndExistingFieldOrderArePreserved() { + Province province = new Province(1, "bog", 0); + Guild guild = mock(Guild.class); + province.setData("guild-id", new ProvinceDataEntry(guild, 1.234, 0.567)); + + JsonObject json = Compiler.provinceToJson(province, "occupier"); + + assertEquals("{\"trade\":1.23,\"production\":0.57}", json.getAsJsonObject("trade") + .getAsJsonObject("guild-id").toString()); + assertEquals("id", json.keySet().toArray()[0]); + assertEquals("prosperity", json.keySet().toArray()[1]); + assertEquals("occupied_by", json.keySet().toArray()[2]); + assertEquals("trade", json.keySet().toArray()[3]); + assertTrue(json.has("effective_terrain")); + } +} From 590e16e3f9a5d3dfbbc76667bf69b8e964f1a7a4 Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:32:53 +0000 Subject: [PATCH 2/2] Export the raw terrain value and keep the fill fraction in range. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../simplefactions/loaders/ConfigLoader.java | 4 +++- .../simplefactions/map/Compiler.java | 10 +++++---- .../ConfigLoaderInfrastructureTest.java | 14 +++++++++++++ .../map/CompilerProvinceExportTest.java | 21 +++++++++++++++++++ 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/tfminecraft/simplefactions/loaders/ConfigLoader.java b/src/main/java/net/tfminecraft/simplefactions/loaders/ConfigLoader.java index 80c45e45..814cf1cd 100644 --- a/src/main/java/net/tfminecraft/simplefactions/loaders/ConfigLoader.java +++ b/src/main/java/net/tfminecraft/simplefactions/loaders/ConfigLoader.java @@ -73,7 +73,9 @@ public void loadConfig(File configFile) { Cache.mercenaryDefaultBreachRefund = config.getDouble("mercenary-default-breach-refund", 500.0); net.tfminecraft.simplefactions.guild.hub.SupplyHubService.loadConfig(config); net.tfminecraft.simplefactions.guild.hub.HubTransport.loadConfig(config); - Cache.infrastructureFull = config.getDouble("infrastructure.full", 20); + double infrastructureFull = config.getDouble("infrastructure.full", 20); + Cache.infrastructureFull = Double.isFinite(infrastructureFull) && infrastructureFull > 0 + ? infrastructureFull : 20; Cache.infrastructureTarget = config.getDouble("infrastructure.target", 0.75); Cache.infrastructureWildernessSpread = config.getDouble("infrastructure.wilderness-spread", 0.25); double infrastructureSpreadFloor = config.getDouble("infrastructure.spread-floor", 0.5); diff --git a/src/main/java/net/tfminecraft/simplefactions/map/Compiler.java b/src/main/java/net/tfminecraft/simplefactions/map/Compiler.java index 93466e56..3d71ae7e 100644 --- a/src/main/java/net/tfminecraft/simplefactions/map/Compiler.java +++ b/src/main/java/net/tfminecraft/simplefactions/map/Compiler.java @@ -7,6 +7,7 @@ import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import com.google.gson.Gson; @@ -98,11 +99,12 @@ static JsonObject provinceToJson(Province p, String occupierId) { if (!p.isSea()) { double terrain = p.getTradeCarry(); double infrastructure = p.getInfrastructure(); - o.addProperty("terrain", p.getTerrain().name().toLowerCase()); - o.addProperty("terrain_value", r2(terrain)); - if (infrastructure > 0) { + o.addProperty("terrain", p.getTerrain().name().toLowerCase(Locale.ROOT)); + o.addProperty("terrain_value", terrain); + if (infrastructure > 0 && Double.isFinite(Cache.infrastructureFull) && Cache.infrastructureFull > 0) { o.addProperty("infrastructure", r2(infrastructure)); - o.addProperty("infrastructure_fill", r2(Math.min(1, infrastructure / Cache.infrastructureFull))); + o.addProperty("infrastructure_fill", r2(Math.max(0, Math.min(1, + infrastructure / Cache.infrastructureFull)))); } o.addProperty("effective_terrain", r2(EffectiveTerrain.calculate(terrain, infrastructure, Cache.infrastructureFull, Cache.infrastructureTarget, 1))); diff --git a/src/test/java/net/tfminecraft/simplefactions/loaders/ConfigLoaderInfrastructureTest.java b/src/test/java/net/tfminecraft/simplefactions/loaders/ConfigLoaderInfrastructureTest.java index 14458d71..10611dda 100644 --- a/src/test/java/net/tfminecraft/simplefactions/loaders/ConfigLoaderInfrastructureTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/loaders/ConfigLoaderInfrastructureTest.java @@ -71,6 +71,20 @@ void configuredKeysAreReadIntoCache() throws IOException { assertValues(30, 0.80, 0.10, 0.25, 12, 8, 4); } + @Test + void zeroFullUsesDefault() throws IOException { + load("infrastructure:\n full: 0\n"); + + assertEquals(20, Cache.infrastructureFull, 1e-9); + } + + @Test + void negativeFullUsesDefault() throws IOException { + load("infrastructure:\n full: -5\n"); + + assertEquals(20, Cache.infrastructureFull, 1e-9); + } + @Test void zeroSpreadFloorUsesDefault() throws IOException { load("infrastructure:\n spread-floor: 0\n"); diff --git a/src/test/java/net/tfminecraft/simplefactions/map/CompilerProvinceExportTest.java b/src/test/java/net/tfminecraft/simplefactions/map/CompilerProvinceExportTest.java index 7bad1dfd..82659bd8 100644 --- a/src/test/java/net/tfminecraft/simplefactions/map/CompilerProvinceExportTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/map/CompilerProvinceExportTest.java @@ -58,6 +58,27 @@ void bogWithInfrastructureExportsFillAndEffectiveTerrain() { assertEquals(0.61, json.get("effective_terrain").getAsDouble()); } + @Test + void terrainValueIsNotRounded() { + Cache.tradeCarry.put(Terrain.BOG, 0.445); + + JsonObject json = Compiler.provinceToJson(new Province(1, "bog", 0), null); + + assertEquals(0.445, json.get("terrain_value").getAsDouble()); + } + + @Test + void invalidInfrastructureFullOmitsInfrastructureFields() { + Cache.infrastructureFull = -5; + Province province = new Province(1, "bog", 0); + province.setInfrastructure(12); + + JsonObject json = Compiler.provinceToJson(province, null); + + assertFalse(json.has("infrastructure")); + assertFalse(json.has("infrastructure_fill")); + } + @Test void bogWithoutInfrastructureOmitsInfrastructureFields() { JsonObject json = Compiler.provinceToJson(new Province(1, "bog", 0), null);