Add /lw speed command for real-time simulation acceleration

/lw speed <1-20>  — run N full sim cycles (module pipeline + all
                    post-sim hooks) per normal tick interval.
/lw speed reset   — return to real-time (1x).

The multiplier is stored on LivingWorldBootstrap and reset to 1 on
server stop. Extra cycles queue all active regions and call the full
post-sim hook chain (pollution spread, seasonal effects, climate,
warming, water runoff, dynamic cap) so ecosystem state advances
coherently rather than just running modules in isolation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
George
2026-06-07 22:16:26 +01:00
parent b17a8990b0
commit 66dc2f336a
2 changed files with 57 additions and 9 deletions
@@ -10,6 +10,7 @@ import com.livingworld.core.services.ServiceRegistry;
import com.livingworld.core.simulation.DefaultTimeService;
import com.livingworld.core.simulation.SimulationManager;
import com.livingworld.core.simulation.SimulationScheduler;
import com.livingworld.core.simulation.UpdateReason;
import com.livingworld.debug.SimulationProfiler;
import com.livingworld.debug.DiagnosticCategory;
import com.livingworld.debug.LivingWorldLogger;
@@ -90,6 +91,8 @@ public final class LivingWorldBootstrap {
private boolean serverReady;
/** Average surface elevation (block Y) per region, sampled once by the platform layer. */
private final Map<RegionCoordinate, Double> regionElevations = new HashMap<>();
/** Extra sim cycles run per normal tick interval (1 = real-time, >1 = accelerated). */
private int simSpeedMultiplier = 1;
/**
* Called once during mod construction.
@@ -212,6 +215,7 @@ public final class LivingWorldBootstrap {
climateTracker.save(worldSaveDirectory.resolve("living_world/global_climate.dat"));
hudEnabledPlayers.clear();
regionElevations.clear();
simSpeedMultiplier = 1;
serverReady = false;
LivingWorldLogger.info(
DiagnosticCategory.BOOTSTRAP,
@@ -343,16 +347,38 @@ public final class LivingWorldBootstrap {
long previousSimulationTick = simulationManager.getSimulationTickCounter();
simulationManager.onMinecraftServerTick();
if (simulationManager.getSimulationTickCounter() != previousSimulationTick) {
spreadPollutionAcrossRegions();
applySeasonalEffects();
climateTracker.update(regionManager.getActiveRegions());
applyClimateWarmingEffects();
applyWaterRunoff();
applyDynamicCapUpdate();
runPostSimHooks();
for (int extra = 1; extra < simSpeedMultiplier; extra++) {
for (Region r : regionManager.getActiveRegions()) {
simulationManager.queueRegionForUpdate(
r.getCoordinate(), 0, UpdateReason.NORMAL_ROLLING_UPDATE);
}
simulationManager.runSimulationCycle();
runPostSimHooks();
}
regionManager.saveDirtyRegions();
}
}
private void runPostSimHooks() {
spreadPollutionAcrossRegions();
applySeasonalEffects();
climateTracker.update(regionManager.getActiveRegions());
applyClimateWarmingEffects();
applyWaterRunoff();
applyDynamicCapUpdate();
}
/** Sets the simulation speed multiplier (1 = real-time, max 20). */
public int setSimSpeedMultiplier(int multiplier) {
this.simSpeedMultiplier = Math.max(1, Math.min(20, multiplier));
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP,
"Simulation speed set to " + simSpeedMultiplier + "x");
return this.simSpeedMultiplier;
}
public int getSimSpeedMultiplier() { return simSpeedMultiplier; }
private static final double POLLUTION_SPREAD_RATE = 0.02;
private void spreadPollutionAcrossRegions() {
@@ -635,7 +661,8 @@ public final class LivingWorldBootstrap {
() -> requireService(simulationManager, "simulationManager"),
this::toggleHud,
this::getAtmosphereStatusFor,
this::getClimateStatusFor);
this::getClimateStatusFor,
this::setSimSpeedMultiplier);
}
public Path getWorldSaveDirectory() {
@@ -2,6 +2,7 @@ package com.livingworld.commands;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.IntUnaryOperator;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -41,7 +42,8 @@ public final class LivingWorldCommandRoot {
() -> simulationManager,
uuid -> false,
css -> "Atmosphere not available.",
css -> "Climate not available.");
css -> "Climate not available.",
n -> n);
}
public static void registerDeferred(
@@ -51,7 +53,8 @@ public final class LivingWorldCommandRoot {
Supplier<SimulationManager> simulationManager,
Function<UUID, Boolean> hudToggle,
Function<CommandSourceStack, String> atmosphereStatus,
Function<CommandSourceStack, String> climateStatus) {
Function<CommandSourceStack, String> climateStatus,
IntUnaryOperator speedSetter) {
if (dispatcher == null) {
throw new IllegalArgumentException("dispatcher must not be null");
}
@@ -122,6 +125,24 @@ public final class LivingWorldCommandRoot {
context.getSource().sendSuccess(() -> Component.literal(info), false);
return 1;
}))
.then(Commands.literal("speed")
.then(Commands.argument("multiplier", IntegerArgumentType.integer(1, 20))
.executes(context -> {
int n = IntegerArgumentType.getInteger(context, "multiplier");
int actual = speedSetter.applyAsInt(n);
context.getSource().sendSuccess(
() -> Component.literal("[LW] Simulation speed: " + actual + "x"),
false);
return actual;
}))
.then(Commands.literal("reset")
.executes(context -> {
speedSetter.applyAsInt(1);
context.getSource().sendSuccess(
() -> Component.literal("[LW] Simulation speed reset to 1x"),
false);
return 1;
})))
.then(RegionSetCommand.build(regionManager)));
}