diff --git a/src/main/java/com/livingworld/commands/LivingWorldCommandRoot.java b/src/main/java/com/livingworld/commands/LivingWorldCommandRoot.java index 6fc0bd2..fba16e2 100644 --- a/src/main/java/com/livingworld/commands/LivingWorldCommandRoot.java +++ b/src/main/java/com/livingworld/commands/LivingWorldCommandRoot.java @@ -55,9 +55,14 @@ public final class LivingWorldCommandRoot { .executes(context -> listModules( context.getSource(), moduleRegistry)))) .then(Commands.literal("simulate") - .then(Commands.argument("ticks", IntegerArgumentType.integer(1, 1000)) - .executes(context -> placeholder( - context.getSource(), "Forced simulation is not connected yet."))))); + .then(Commands.argument( + "ticks", + IntegerArgumentType.integer( + 1, SimulationManager.MAX_FORCED_SIMULATION_TICKS)) + .executes(context -> SimulateCommand.execute( + context.getSource(), + simulationManager, + IntegerArgumentType.getInteger(context, "ticks")))))); } private static int showStatus( diff --git a/src/main/java/com/livingworld/commands/SimulateCommand.java b/src/main/java/com/livingworld/commands/SimulateCommand.java new file mode 100644 index 0000000..c7776b6 --- /dev/null +++ b/src/main/java/com/livingworld/commands/SimulateCommand.java @@ -0,0 +1,47 @@ +package com.livingworld.commands; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; + +import com.livingworld.core.simulation.SimulationManager; + +/** + * Runs a bounded number of forced Living World simulation cycles. + */ +public final class SimulateCommand { + + private SimulateCommand() { + } + + public static int execute( + CommandSourceStack source, + SimulationManager simulationManager, + int ticks) { + if (source == null) { + throw new IllegalArgumentException("source must not be null"); + } + if (simulationManager == null) { + throw new IllegalArgumentException("simulationManager must not be null"); + } + + if (ticks <= 0 || ticks > SimulationManager.MAX_FORCED_SIMULATION_TICKS) { + source.sendFailure(Component.literal( + "Ticks must be between 1 and " + + SimulationManager.MAX_FORCED_SIMULATION_TICKS + ".")); + return 0; + } + + try { + simulationManager.runForcedSimulationTicks(ticks); + source.sendSuccess( + () -> Component.literal( + "Ran " + ticks + " Living World simulation ticks."), + true); + return ticks; + } catch (RuntimeException exception) { + source.sendFailure(Component.literal( + "Simulation failed: " + exception.getMessage())); + return 0; + } + } +} diff --git a/src/main/java/com/livingworld/core/simulation/SimulationManager.java b/src/main/java/com/livingworld/core/simulation/SimulationManager.java index 66cac8d..5181afb 100644 --- a/src/main/java/com/livingworld/core/simulation/SimulationManager.java +++ b/src/main/java/com/livingworld/core/simulation/SimulationManager.java @@ -23,6 +23,8 @@ import com.livingworld.regions.Region; */ public final class SimulationManager { + public static final int MAX_FORCED_SIMULATION_TICKS = 1000; + private final SimulationScheduler scheduler; private final RegionManager regionManager; private final ModuleRegistry moduleRegistry; @@ -152,9 +154,12 @@ public final class SimulationManager { * Runs forced simulation ticks for debugging or testing. */ public void runForcedSimulationTicks(int ticks) { - if (ticks <= 0 || ticks > 100) { + if (ticks <= 0 || ticks > MAX_FORCED_SIMULATION_TICKS) { throw new IllegalArgumentException( - String.format("forced ticks must be in range [1, 100], got: %d", ticks)); + String.format( + "forced ticks must be in range [1, %d], got: %d", + MAX_FORCED_SIMULATION_TICKS, + ticks)); } for (int i = 0; i < ticks; i++) { diff --git a/src/test/java/com/livingworld/core/simulation/SimulationManagerTest.java b/src/test/java/com/livingworld/core/simulation/SimulationManagerTest.java index fe3ecbb..f48fe0e 100644 --- a/src/test/java/com/livingworld/core/simulation/SimulationManagerTest.java +++ b/src/test/java/com/livingworld/core/simulation/SimulationManagerTest.java @@ -3,6 +3,8 @@ package com.livingworld.core.simulation; 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.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.ArrayList; import java.util.Collections; @@ -86,6 +88,22 @@ class SimulationManagerTest { assertEquals(0, fixture.scheduler.getMinecraftTickCounter()); } + @Test + void forcedTicksAcceptSafeMaximumAndRejectValuesOutsideRange() { + Fixture fixture = new Fixture(25); + + assertDoesNotThrow(() -> fixture.manager.runForcedSimulationTicks( + SimulationManager.MAX_FORCED_SIMULATION_TICKS)); + assertEquals( + SimulationManager.MAX_FORCED_SIMULATION_TICKS, + fixture.timeService.getSimulationTick()); + assertThrows(IllegalArgumentException.class, + () -> fixture.manager.runForcedSimulationTicks(0)); + assertThrows(IllegalArgumentException.class, + () -> fixture.manager.runForcedSimulationTicks( + SimulationManager.MAX_FORCED_SIMULATION_TICKS + 1)); + } + @Test void timeBudgetRequeuesJobsNotYetProcessed() { Fixture fixture = new Fixture(1);