From 66dc2f336ab8c7d8da37a5b2e45485c13489e2f2 Mon Sep 17 00:00:00 2001 From: George Date: Sun, 7 Jun 2026 22:16:26 +0100 Subject: [PATCH] Add /lw speed command for real-time simulation acceleration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /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 --- .../bootstrap/LivingWorldBootstrap.java | 41 +++++++++++++++---- .../commands/LivingWorldCommandRoot.java | 25 ++++++++++- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java b/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java index 1a3b3a1..3864cde 100644 --- a/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java +++ b/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java @@ -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 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() { diff --git a/src/main/java/com/livingworld/commands/LivingWorldCommandRoot.java b/src/main/java/com/livingworld/commands/LivingWorldCommandRoot.java index 387d875..f256ac8 100644 --- a/src/main/java/com/livingworld/commands/LivingWorldCommandRoot.java +++ b/src/main/java/com/livingworld/commands/LivingWorldCommandRoot.java @@ -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, Function hudToggle, Function atmosphereStatus, - Function climateStatus) { + Function 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))); }