Add debug commands, force-update, and weather feedback loop
Track E — commands:
/lw region info — now shows all 7 module data classes (air/ground/water pollution,
soil fertility/moisture/contamination, succession stage, depletion levels, etc.)
/lw region info <x> <z> — inspect any region by block coordinates
/lw region force-update — runs full module pipeline on current region immediately
/lw stats — shows last cycle duration, per-module timings, budget overrun flag
Track F — weather feedback:
Each simulation tick, active overworld regions receive a moisture boost when it
rains (+0.3 waterAvailability, -0.15 droughtRisk) and a drought increase when
dry (+0.05 droughtRisk). The rain state is supplied by LivingWorldMod via a
BooleanSupplier set on ServerStartedEvent and cleared on stop.
400 tests, all passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,9 +51,12 @@ public class LivingWorldMod {
|
||||
bootstrap.onServerStarted();
|
||||
bootstrap.getWorldEffectsModule().registerConsumer(
|
||||
new NeoForgeWorldEffectExecutor(() -> minecraftServer));
|
||||
bootstrap.setOverworldRaining(
|
||||
() -> minecraftServer != null && minecraftServer.overworld().isRaining());
|
||||
});
|
||||
NeoForge.EVENT_BUS.addListener(ServerStoppingEvent.class, event -> {
|
||||
bootstrap.onServerStopping();
|
||||
bootstrap.setOverworldRaining(null);
|
||||
this.minecraftServer = null;
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.livingworld.modules.ServerContext;
|
||||
import com.livingworld.modules.ecosystem.EcosystemModule;
|
||||
import com.livingworld.modules.pollution.PollutionModule;
|
||||
import com.livingworld.modules.ecosystem.EcosystemRegionData;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import com.livingworld.modules.pollution.PollutionRegionData;
|
||||
import com.livingworld.modules.recovery.RecoveryModule;
|
||||
import com.livingworld.modules.recovery.RecoveryRegionData;
|
||||
@@ -52,6 +53,10 @@ import net.minecraft.commands.CommandSourceStack;
|
||||
*/
|
||||
public final class LivingWorldBootstrap {
|
||||
|
||||
private static final double RAIN_MOISTURE_GAIN = 0.3;
|
||||
private static final double RAIN_DROUGHT_RELIEF = 0.15;
|
||||
private static final double DRY_DROUGHT_INCREASE = 0.05;
|
||||
|
||||
private PlatformAdapter platformAdapter;
|
||||
private Path worldSaveDirectory;
|
||||
private ServiceRegistry services;
|
||||
@@ -59,6 +64,7 @@ public final class LivingWorldBootstrap {
|
||||
private ModuleRegistry moduleRegistry;
|
||||
private SimulationManager simulationManager;
|
||||
private WorldEffectsModule worldEffectsModule;
|
||||
private BooleanSupplier overworldRaining = () -> false;
|
||||
private boolean initialized;
|
||||
private boolean serverReady;
|
||||
|
||||
@@ -185,6 +191,14 @@ public final class LivingWorldBootstrap {
|
||||
"onServerStopping - persistence flushed and modules stopped.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the supplier that reports whether it is currently raining in the overworld.
|
||||
* Called from the platform layer after the server starts and cleared on stop.
|
||||
*/
|
||||
public void setOverworldRaining(BooleanSupplier supplier) {
|
||||
this.overworldRaining = supplier != null ? supplier : () -> false;
|
||||
}
|
||||
|
||||
public void onServerTick() {
|
||||
if (!serverReady) {
|
||||
return;
|
||||
@@ -192,10 +206,34 @@ public final class LivingWorldBootstrap {
|
||||
long previousSimulationTick = simulationManager.getSimulationTickCounter();
|
||||
simulationManager.onMinecraftServerTick();
|
||||
if (simulationManager.getSimulationTickCounter() != previousSimulationTick) {
|
||||
applyWeatherFeedback();
|
||||
regionManager.saveDirtyRegions();
|
||||
}
|
||||
}
|
||||
|
||||
private void applyWeatherFeedback() {
|
||||
boolean raining = overworldRaining.getAsBoolean();
|
||||
for (Region region : regionManager.getActiveRegions()) {
|
||||
if (!"minecraft:overworld".equals(region.getCoordinate().dimensionId())) {
|
||||
continue;
|
||||
}
|
||||
WaterRegionData water = region.getModuleData()
|
||||
.get(WaterModule.MODULE_ID, WaterRegionData.class)
|
||||
.orElse(null);
|
||||
if (water == null) {
|
||||
continue;
|
||||
}
|
||||
if (raining) {
|
||||
water.setWaterAvailability(water.getWaterAvailability() + RAIN_MOISTURE_GAIN);
|
||||
water.setDroughtRisk(water.getDroughtRisk() - RAIN_DROUGHT_RELIEF);
|
||||
} else {
|
||||
water.setDroughtRisk(water.getDroughtRisk() + DRY_DROUGHT_INCREASE);
|
||||
}
|
||||
region.getModuleData().put(WaterModule.MODULE_ID, water);
|
||||
regionManager.markDirty(region);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerCommands(CommandDispatcher<CommandSourceStack> dispatcher) {
|
||||
requireInitialized();
|
||||
LivingWorldCommandRoot.registerDeferred(
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.livingworld.commands;
|
||||
|
||||
import com.livingworld.core.simulation.SimulationManager;
|
||||
import com.livingworld.regions.RegionManager;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
* Forces the full module pipeline to run on a single region immediately,
|
||||
* bypassing the scheduler. Useful for observing simulation effects without
|
||||
* waiting for the next scheduled tick.
|
||||
*/
|
||||
public final class ForceUpdateCommand {
|
||||
|
||||
private ForceUpdateCommand() {
|
||||
}
|
||||
|
||||
public static int executeAtSelf(
|
||||
CommandSourceStack source,
|
||||
RegionManager regionManager,
|
||||
SimulationManager simulationManager) {
|
||||
if (source == null) throw new IllegalArgumentException("source must not be null");
|
||||
if (regionManager == null) throw new IllegalArgumentException("regionManager must not be null");
|
||||
if (simulationManager == null) throw new IllegalArgumentException("simulationManager must not be null");
|
||||
|
||||
Vec3 pos = source.getPosition();
|
||||
String dimensionId = source.getLevel().dimension().location().toString();
|
||||
// Ensure the region is loaded/created before asking the simulation manager to update it.
|
||||
var region = regionManager.getOrCreateRegionAtBlock(
|
||||
dimensionId, (int) Math.floor(pos.x), (int) Math.floor(pos.z));
|
||||
simulationManager.forceUpdateRegion(region.getCoordinate());
|
||||
|
||||
String msg = "Forced update on region " + region.getCoordinate().stableId()
|
||||
+ " — run '/lw region info' to see the result.";
|
||||
source.sendSuccess(() -> Component.literal(msg), true);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -64,14 +64,30 @@ public final class LivingWorldCommandRoot {
|
||||
requireService(simulationManager, "simulationManager"))))
|
||||
.then(Commands.literal("region")
|
||||
.then(Commands.literal("info")
|
||||
.executes(context -> RegionInfoCommand.execute(
|
||||
.executes(context -> RegionInfoCommand.executeAtSelf(
|
||||
context.getSource(),
|
||||
requireService(regionManager, "regionManager")))))
|
||||
requireService(regionManager, "regionManager")))
|
||||
.then(Commands.argument("x", IntegerArgumentType.integer())
|
||||
.then(Commands.argument("z", IntegerArgumentType.integer())
|
||||
.executes(context -> RegionInfoCommand.executeAt(
|
||||
context.getSource(),
|
||||
requireService(regionManager, "regionManager"),
|
||||
IntegerArgumentType.getInteger(context, "x"),
|
||||
IntegerArgumentType.getInteger(context, "z"))))))
|
||||
.then(Commands.literal("force-update")
|
||||
.executes(context -> ForceUpdateCommand.executeAtSelf(
|
||||
context.getSource(),
|
||||
requireService(regionManager, "regionManager"),
|
||||
requireService(simulationManager, "simulationManager")))))
|
||||
.then(Commands.literal("modules")
|
||||
.then(Commands.literal("list")
|
||||
.executes(context -> listModules(
|
||||
context.getSource(),
|
||||
requireService(moduleRegistry, "moduleRegistry")))))
|
||||
.then(Commands.literal("stats")
|
||||
.executes(context -> StatsCommand.execute(
|
||||
context.getSource(),
|
||||
requireService(simulationManager, "simulationManager"))))
|
||||
.then(Commands.literal("simulate")
|
||||
.then(Commands.argument(
|
||||
"ticks",
|
||||
|
||||
@@ -1,44 +1,55 @@
|
||||
package com.livingworld.commands;
|
||||
|
||||
import com.livingworld.regions.Region;
|
||||
import com.livingworld.regions.RegionManager;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import com.livingworld.regions.Region;
|
||||
import com.livingworld.regions.RegionManager;
|
||||
|
||||
/**
|
||||
* Prints the Living World region state at the command source position.
|
||||
* Prints the Living World region state — either at the caller's position or
|
||||
* at explicit block coordinates.
|
||||
*/
|
||||
public final class RegionInfoCommand {
|
||||
|
||||
private RegionInfoCommand() {
|
||||
}
|
||||
|
||||
public static int execute(
|
||||
/** Uses the command-source position (player standing location). */
|
||||
public static int executeAtSelf(
|
||||
CommandSourceStack source,
|
||||
RegionManager regionManager) {
|
||||
if (source == null) {
|
||||
throw new IllegalArgumentException("source must not be null");
|
||||
}
|
||||
if (regionManager == null) {
|
||||
throw new IllegalArgumentException("regionManager must not be null");
|
||||
}
|
||||
if (source == null) throw new IllegalArgumentException("source must not be null");
|
||||
if (regionManager == null) throw new IllegalArgumentException("regionManager must not be null");
|
||||
|
||||
Vec3 position = source.getPosition();
|
||||
String dimensionId = source.getLevel().dimension().location().toString();
|
||||
Region region = regionManager.getOrCreateRegionAtBlock(
|
||||
dimensionId,
|
||||
floorToBlock(position.x),
|
||||
floorToBlock(position.z));
|
||||
(int) Math.floor(position.x),
|
||||
(int) Math.floor(position.z));
|
||||
|
||||
return sendLines(source, region);
|
||||
}
|
||||
|
||||
/** Uses explicit block X/Z coordinates in the caller's current dimension. */
|
||||
public static int executeAt(
|
||||
CommandSourceStack source,
|
||||
RegionManager regionManager,
|
||||
int blockX,
|
||||
int blockZ) {
|
||||
if (source == null) throw new IllegalArgumentException("source must not be null");
|
||||
if (regionManager == null) throw new IllegalArgumentException("regionManager must not be null");
|
||||
|
||||
String dimensionId = source.getLevel().dimension().location().toString();
|
||||
Region region = regionManager.getOrCreateRegionAtBlock(dimensionId, blockX, blockZ);
|
||||
return sendLines(source, region);
|
||||
}
|
||||
|
||||
private static int sendLines(CommandSourceStack source, Region region) {
|
||||
for (String line : RegionInfoFormatter.format(region)) {
|
||||
source.sendSuccess(() -> Component.literal(line), false);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int floorToBlock(double coordinate) {
|
||||
return (int) Math.floor(coordinate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
package com.livingworld.commands;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import com.livingworld.modules.ecosystem.EcosystemModule;
|
||||
import com.livingworld.modules.ecosystem.EcosystemRegionData;
|
||||
import com.livingworld.modules.pollution.PollutionModule;
|
||||
import com.livingworld.modules.pollution.PollutionRegionData;
|
||||
import com.livingworld.modules.recovery.RecoveryModule;
|
||||
import com.livingworld.modules.recovery.RecoveryRegionData;
|
||||
import com.livingworld.modules.resources.ResourceDepletionModule;
|
||||
import com.livingworld.modules.resources.ResourceRegionData;
|
||||
import com.livingworld.modules.soil.SoilModule;
|
||||
import com.livingworld.modules.soil.SoilRegionData;
|
||||
import com.livingworld.modules.vegetation.VegetationModule;
|
||||
import com.livingworld.modules.vegetation.VegetationRegionData;
|
||||
import com.livingworld.modules.water.WaterModule;
|
||||
import com.livingworld.modules.water.WaterRegionData;
|
||||
import com.livingworld.regions.Region;
|
||||
import com.livingworld.regions.RegionFlags;
|
||||
import com.livingworld.regions.RegionMetrics;
|
||||
import com.livingworld.regions.RegionModuleData;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Formats region diagnostics without depending on Minecraft classes.
|
||||
@@ -21,26 +34,84 @@ public final class RegionInfoFormatter {
|
||||
throw new IllegalArgumentException("region must not be null");
|
||||
}
|
||||
|
||||
RegionMetrics metrics = region.getMetrics();
|
||||
RegionFlags flags = region.getFlags();
|
||||
Set<String> moduleIds = new TreeSet<>(region.getModuleData().moduleIds());
|
||||
RegionMetrics m = region.getMetrics();
|
||||
RegionFlags f = region.getFlags();
|
||||
RegionModuleData data = region.getModuleData();
|
||||
|
||||
return List.of(
|
||||
"Region: " + region.getCoordinate().stableId(),
|
||||
"Lifecycle: " + region.getLifecycleState() + ", dirty=" + region.isDirty(),
|
||||
"Metrics: ecosystemHealth=" + metrics.getEcosystemHealth()
|
||||
+ ", pollution=" + metrics.getPollutionScore()
|
||||
+ ", soilQuality=" + metrics.getSoilQuality()
|
||||
+ ", waterQuality=" + metrics.getWaterQuality()
|
||||
+ ", vegetationPressure=" + metrics.getVegetationPressure()
|
||||
+ ", resourceDepletion=" + metrics.getResourceDepletion()
|
||||
+ ", recoveryPressure=" + metrics.getRecoveryPressure(),
|
||||
"Flags: playerActivity=" + flags.isHasPlayerActivity()
|
||||
+ ", highPollution=" + flags.isHasHighPollution()
|
||||
+ ", lowSoilQuality=" + flags.isHasLowSoilQuality()
|
||||
+ ", activeEcosystemEvent=" + flags.isHasActiveEcosystemEvent()
|
||||
+ ", forceLoaded=" + flags.isForceLoadedBySimulation()
|
||||
+ ", corrupted=" + flags.isCorrupted(),
|
||||
"Module data: " + (moduleIds.isEmpty() ? "none" : String.join(", ", moduleIds)));
|
||||
List<String> lines = new ArrayList<>();
|
||||
lines.add("Region: " + region.getCoordinate().stableId()
|
||||
+ " lifecycle=" + region.getLifecycleState()
|
||||
+ " dirty=" + region.isDirty()
|
||||
+ " tick=" + region.getLastUpdatedSimulationTick());
|
||||
lines.add("Metrics:"
|
||||
+ " health=" + fmt(m.getEcosystemHealth())
|
||||
+ " poll=" + fmt(m.getPollutionScore())
|
||||
+ " soil=" + fmt(m.getSoilQuality())
|
||||
+ " water=" + fmt(m.getWaterQuality())
|
||||
+ " veg=" + fmt(m.getVegetationPressure())
|
||||
+ " res=" + fmt(m.getResourceDepletion())
|
||||
+ " recov=" + fmt(m.getRecoveryPressure()));
|
||||
lines.add("Flags:"
|
||||
+ " playerActivity=" + f.isHasPlayerActivity()
|
||||
+ " highPollution=" + f.isHasHighPollution()
|
||||
+ " lowSoil=" + f.isHasLowSoilQuality()
|
||||
+ " ecoEvent=" + f.isHasActiveEcosystemEvent()
|
||||
+ " forceLoaded=" + f.isForceLoadedBySimulation());
|
||||
|
||||
lines.add("--- Module Data ---");
|
||||
data.get(PollutionModule.MODULE_ID, PollutionRegionData.class).ifPresentOrElse(
|
||||
d -> lines.add(" pollution: air=" + fmt(d.getAirPollution())
|
||||
+ " ground=" + fmt(d.getGroundPollution())
|
||||
+ " water=" + fmt(d.getWaterPollution())
|
||||
+ " decay=" + fmt(d.getDecayResistance())),
|
||||
() -> lines.add(" pollution: (no data)"));
|
||||
|
||||
data.get(SoilModule.MODULE_ID, SoilRegionData.class).ifPresentOrElse(
|
||||
d -> lines.add(" soil: fertility=" + fmt(d.getFertility())
|
||||
+ " moisture=" + fmt(d.getMoisture())
|
||||
+ " contam=" + fmt(d.getContamination())
|
||||
+ " compact=" + fmt(d.getCompaction())
|
||||
+ " erosion=" + fmt(d.getErosion())),
|
||||
() -> lines.add(" soil: (no data)"));
|
||||
|
||||
data.get(WaterModule.MODULE_ID, WaterRegionData.class).ifPresentOrElse(
|
||||
d -> lines.add(" water: avail=" + fmt(d.getWaterAvailability())
|
||||
+ " purif=" + fmt(d.getPurificationCapacity())
|
||||
+ " drought=" + fmt(d.getDroughtRisk())
|
||||
+ " flood=" + fmt(d.getFloodRisk())),
|
||||
() -> lines.add(" water: (no data)"));
|
||||
|
||||
data.get(VegetationModule.MODULE_ID, VegetationRegionData.class).ifPresentOrElse(
|
||||
d -> lines.add(" vegetation: grass=" + fmt(d.getGrassPressure())
|
||||
+ " flower=" + fmt(d.getFlowerPressure())
|
||||
+ " shrub=" + fmt(d.getShrubPressure())
|
||||
+ " tree=" + fmt(d.getTreePressure())
|
||||
+ " dead=" + fmt(d.getDeadVegetation())),
|
||||
() -> lines.add(" vegetation: (no data)"));
|
||||
|
||||
data.get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class).ifPresentOrElse(
|
||||
d -> lines.add(" resources: mining=" + fmt(d.getMiningDepletion())
|
||||
+ " logging=" + fmt(d.getLoggingDepletion())
|
||||
+ " farming=" + fmt(d.getFarmingDepletion())),
|
||||
() -> lines.add(" resources: (no data)"));
|
||||
|
||||
data.get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).ifPresentOrElse(
|
||||
d -> lines.add(" recovery: stage=" + d.getSuccessionStage()
|
||||
+ " progress=" + fmt(d.getRecoveryProgress())
|
||||
+ " damage=" + fmt(d.getDamageAccumulation())),
|
||||
() -> lines.add(" recovery: (no data)"));
|
||||
|
||||
data.get(EcosystemModule.MODULE_ID, EcosystemRegionData.class).ifPresentOrElse(
|
||||
d -> lines.add(" ecosystem: health=" + fmt(d.getEcosystemHealth())
|
||||
+ " stress=" + fmt(d.getStress())
|
||||
+ " resilience=" + fmt(d.getResilience())
|
||||
+ " rate=" + fmt(d.getRecoveryRate())),
|
||||
() -> lines.add(" ecosystem: (no data)"));
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
private static String fmt(double v) {
|
||||
return String.format("%.1f", v);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.livingworld.commands;
|
||||
|
||||
import com.livingworld.core.simulation.SimulationManager;
|
||||
import com.livingworld.debug.SimulationProfileSnapshot;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
/**
|
||||
* Shows the profiler snapshot from the last simulation cycle.
|
||||
*/
|
||||
public final class StatsCommand {
|
||||
|
||||
private StatsCommand() {
|
||||
}
|
||||
|
||||
public static int execute(CommandSourceStack source, SimulationManager simulationManager) {
|
||||
if (source == null) throw new IllegalArgumentException("source must not be null");
|
||||
if (simulationManager == null) throw new IllegalArgumentException("simulationManager must not be null");
|
||||
|
||||
SimulationProfileSnapshot snap = simulationManager.createProfileSnapshot();
|
||||
|
||||
String cycleMs = String.format("%.2f", snap.totalCycleNanos() / 1_000_000.0);
|
||||
String header = "LW stats:"
|
||||
+ " cycle=" + cycleMs + "ms"
|
||||
+ " events=" + snap.eventsPublished()
|
||||
+ " regions=" + snap.regionsUpdated()
|
||||
+ " saves=" + snap.savesPerformed()
|
||||
+ " budget_overrun=" + snap.budgetExceeded()
|
||||
+ " sim_tick=" + simulationManager.getSimulationTickCounter();
|
||||
source.sendSuccess(() -> Component.literal(header), false);
|
||||
|
||||
if (!snap.moduleTimings().isEmpty()) {
|
||||
StringJoiner timings = new StringJoiner(" ");
|
||||
snap.moduleTimings().forEach((id, nanos) ->
|
||||
timings.add(id + "=" + String.format("%.2f", nanos / 1_000_000.0) + "ms"));
|
||||
String moduleLine = "Modules: " + timings;
|
||||
source.sendSuccess(() -> Component.literal(moduleLine), false);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import com.livingworld.core.services.PersistenceService;
|
||||
import com.livingworld.core.services.TimeService;
|
||||
import com.livingworld.debug.DiagnosticCategory;
|
||||
import com.livingworld.debug.LivingWorldLogger;
|
||||
import com.livingworld.debug.SimulationProfileSnapshot;
|
||||
import com.livingworld.regions.RegionCoordinate;
|
||||
import com.livingworld.events.LivingWorldEventBus;
|
||||
import com.livingworld.modules.ModuleUpdateResult;
|
||||
import com.livingworld.modules.ModuleRegistry;
|
||||
@@ -192,6 +194,32 @@ public final class SimulationManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the full module pipeline to run on a single region immediately,
|
||||
* bypassing the scheduler. Intended for debug commands only.
|
||||
*/
|
||||
public void forceUpdateRegion(RegionCoordinate coordinate) {
|
||||
if (coordinate == null) {
|
||||
throw new IllegalArgumentException("coordinate must not be null");
|
||||
}
|
||||
Optional<Region> region = regionManager.resolve(coordinate);
|
||||
if (region.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
long tick = getSimulationTickCounter();
|
||||
RegionUpdateJob job = new RegionUpdateJob(
|
||||
coordinate, 100, tick, null, UpdateReason.FORCED_DEBUG_COMMAND);
|
||||
runModulesForRegion(region.get(), job, tick);
|
||||
}
|
||||
|
||||
/** Returns a profile snapshot of the last completed simulation cycle. */
|
||||
public SimulationProfileSnapshot createProfileSnapshot() {
|
||||
if (profiler instanceof com.livingworld.debug.SimulationProfiler concrete) {
|
||||
return concrete.createSnapshot();
|
||||
}
|
||||
return new SimulationProfileSnapshot(0L, java.util.Map.of(), 0, 0, 0, false);
|
||||
}
|
||||
|
||||
public long getMinecraftTickCounter() {
|
||||
return this.scheduler.getMinecraftTickCounter();
|
||||
}
|
||||
|
||||
@@ -1,36 +1,79 @@
|
||||
package com.livingworld.commands;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.livingworld.modules.pollution.PollutionRegionData;
|
||||
import com.livingworld.modules.recovery.RecoveryRegionData;
|
||||
import com.livingworld.modules.recovery.SuccessionStage;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.livingworld.regions.Region;
|
||||
import com.livingworld.regions.RegionCoordinate;
|
||||
import com.livingworld.regions.RegionFactory;
|
||||
|
||||
class RegionInfoFormatterTest {
|
||||
|
||||
@Test
|
||||
void includesIdentityLifecycleMetricsFlagsAndSortedModuleIds() {
|
||||
Region region = new RegionFactory().createNewRegion(
|
||||
private static Region region() {
|
||||
return new RegionFactory().createNewRegion(
|
||||
new RegionCoordinate("minecraft:overworld", -1, 2), 0);
|
||||
region.getMetrics().setPollutionScore(75);
|
||||
region.getFlags().setHasHighPollution(true);
|
||||
region.getModuleData().put("water", "data");
|
||||
region.getModuleData().put("soil", "data");
|
||||
}
|
||||
|
||||
List<String> lines = RegionInfoFormatter.format(region);
|
||||
@Test
|
||||
void headerContainsRegionId() {
|
||||
List<String> lines = RegionInfoFormatter.format(region());
|
||||
assertTrue(lines.get(0).contains("minecraft:overworld:-1:2"),
|
||||
"header should contain stable ID; got: " + lines.get(0));
|
||||
assertTrue(lines.get(0).contains("ACTIVE"), lines.get(0));
|
||||
}
|
||||
|
||||
assertEquals(5, lines.size());
|
||||
assertTrue(lines.get(0).contains("minecraft:overworld:-1:2"));
|
||||
assertTrue(lines.get(1).contains("ACTIVE"));
|
||||
assertTrue(lines.get(2).contains("pollution=75.0"));
|
||||
assertTrue(lines.get(3).contains("highPollution=true"));
|
||||
assertEquals("Module data: soil, water", lines.get(4));
|
||||
@Test
|
||||
void metricsLineContainsPollutionScore() {
|
||||
Region r = region();
|
||||
r.getMetrics().setPollutionScore(75);
|
||||
List<String> lines = RegionInfoFormatter.format(r);
|
||||
assertTrue(lines.stream().anyMatch(l -> l.contains("poll=75.0")),
|
||||
"metrics line should contain poll=75.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void flagsLineContainsHighPollution() {
|
||||
Region r = region();
|
||||
r.getFlags().setHasHighPollution(true);
|
||||
List<String> lines = RegionInfoFormatter.format(r);
|
||||
assertTrue(lines.stream().anyMatch(l -> l.contains("highPollution=true")),
|
||||
"flags line should contain highPollution=true");
|
||||
}
|
||||
|
||||
@Test
|
||||
void moduleDataSectionPresentForAllModules() {
|
||||
List<String> lines = RegionInfoFormatter.format(region());
|
||||
assertTrue(lines.stream().anyMatch(l -> l.contains("--- Module Data ---")));
|
||||
assertTrue(lines.stream().anyMatch(l -> l.startsWith(" pollution:")));
|
||||
assertTrue(lines.stream().anyMatch(l -> l.startsWith(" soil:")));
|
||||
assertTrue(lines.stream().anyMatch(l -> l.startsWith(" water:")));
|
||||
assertTrue(lines.stream().anyMatch(l -> l.startsWith(" vegetation:")));
|
||||
assertTrue(lines.stream().anyMatch(l -> l.startsWith(" resources:")));
|
||||
assertTrue(lines.stream().anyMatch(l -> l.startsWith(" recovery:")));
|
||||
assertTrue(lines.stream().anyMatch(l -> l.startsWith(" ecosystem:")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void moduleDataValuesShownWhenPresent() {
|
||||
Region r = region();
|
||||
r.getModuleData().put("pollution", new PollutionRegionData(33.0, 0.0, 0.0, 20.0));
|
||||
r.getModuleData().put("recovery",
|
||||
new RecoveryRegionData(SuccessionStage.YOUNG_WOODLAND, 50.0, 0.0));
|
||||
|
||||
List<String> lines = RegionInfoFormatter.format(r);
|
||||
assertTrue(lines.stream().anyMatch(l -> l.contains("air=33.0")));
|
||||
assertTrue(lines.stream().anyMatch(l -> l.contains("stage=YOUNG_WOODLAND")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noDataShownWhenModuleAbsent() {
|
||||
List<String> lines = RegionInfoFormatter.format(region());
|
||||
assertTrue(lines.stream().anyMatch(l -> l.contains("(no data)")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user