Add /lw set debug commands for ecosystem testing

Adds RegionSetCommand under /lw set with sub-commands to directly mutate
simulation data in the player's current region:

  /lw set soil fertility <0-100>     — soil fertility
  /lw set soil moisture  <0-100>     — soil moisture
  /lw set water availability <0-100> — water availability
  /lw set water drought      <0-100> — drought risk
  /lw set pollution <0-100>          — all pollution layers (proportional)
  /lw set stage <STAGE>              — force succession stage (resets progress)
  /lw set cap   <STAGE>              — force succession ceiling
  /lw set rain  <0.0-1.0>            — override regional rain level

Stage names have tab-completion. Forcing /lw set stage to a higher stage
than the current cap auto-raises the cap. All changes are immediately
persisted via markDirty.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
George
2026-06-07 21:50:39 +01:00
parent 32e413cc9f
commit 0f2a265c61
2 changed files with 248 additions and 1 deletions
@@ -8,6 +8,7 @@ import java.util.stream.Collectors;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.arguments.DoubleArgumentType;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands; import net.minecraft.commands.Commands;
@@ -120,7 +121,8 @@ public final class LivingWorldCommandRoot {
String info = climateStatus.apply(context.getSource()); String info = climateStatus.apply(context.getSource());
context.getSource().sendSuccess(() -> Component.literal(info), false); context.getSource().sendSuccess(() -> Component.literal(info), false);
return 1; return 1;
}))); }))
.then(RegionSetCommand.build(regionManager)));
} }
private static int toggleHud(CommandSourceStack source, Function<UUID, Boolean> hudToggle) { private static int toggleHud(CommandSourceStack source, Function<UUID, Boolean> hudToggle) {
@@ -0,0 +1,245 @@
package com.livingworld.commands;
import java.util.Arrays;
import java.util.function.Supplier;
import com.livingworld.core.LivingWorldConstants;
import com.livingworld.modules.atmosphere.AtmosphereModule;
import com.livingworld.modules.atmosphere.AtmosphereRegionData;
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.recovery.SuccessionStage;
import com.livingworld.modules.soil.SoilModule;
import com.livingworld.modules.soil.SoilRegionData;
import com.livingworld.modules.water.WaterModule;
import com.livingworld.modules.water.WaterRegionData;
import com.livingworld.regions.Region;
import com.livingworld.regions.RegionCoordinate;
import com.livingworld.regions.RegionManager;
import com.mojang.brigadier.arguments.DoubleArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
/**
* Debug command branch: {@code /lw set <property> <value>}.
*
* <p>Directly mutates simulation data in the player's current region so that
* ecosystem transformation systems (succession, water runoff, pool formation)
* can be tested without waiting for natural accumulation.
*
* <h3>Sub-commands</h3>
* <pre>
* /lw set soil fertility &lt;0-100&gt;
* /lw set soil moisture &lt;0-100&gt;
* /lw set water availability &lt;0-100&gt;
* /lw set water drought &lt;0-100&gt;
* /lw set pollution &lt;0-100&gt; (sets all three pollution layers proportionally)
* /lw set stage &lt;STAGE_NAME&gt; (forces succession stage; resets progress)
* /lw set cap &lt;STAGE_NAME&gt; (forces succession ceiling)
* /lw set rain &lt;0.0-1.0&gt; (overrides regional rain level for one sim cycle)
* </pre>
*/
public final class RegionSetCommand {
private static final SuggestionProvider<CommandSourceStack> STAGE_SUGGESTIONS =
(ctx, builder) -> SharedSuggestionProvider.suggest(
Arrays.stream(SuccessionStage.values()).map(Enum::name), builder);
private RegionSetCommand() {}
public static LiteralArgumentBuilder<CommandSourceStack> build(Supplier<RegionManager> rm) {
return Commands.literal("set")
// --- soil ---
.then(Commands.literal("soil")
.then(Commands.literal("fertility")
.then(Commands.argument("value", DoubleArgumentType.doubleArg(0, 100))
.executes(ctx -> setSoilFertility(ctx.getSource(), rm.get(),
DoubleArgumentType.getDouble(ctx, "value")))))
.then(Commands.literal("moisture")
.then(Commands.argument("value", DoubleArgumentType.doubleArg(0, 100))
.executes(ctx -> setSoilMoisture(ctx.getSource(), rm.get(),
DoubleArgumentType.getDouble(ctx, "value"))))))
// --- water ---
.then(Commands.literal("water")
.then(Commands.literal("availability")
.then(Commands.argument("value", DoubleArgumentType.doubleArg(0, 100))
.executes(ctx -> setWaterAvailability(ctx.getSource(), rm.get(),
DoubleArgumentType.getDouble(ctx, "value")))))
.then(Commands.literal("drought")
.then(Commands.argument("value", DoubleArgumentType.doubleArg(0, 100))
.executes(ctx -> setWaterDrought(ctx.getSource(), rm.get(),
DoubleArgumentType.getDouble(ctx, "value"))))))
// --- pollution ---
.then(Commands.literal("pollution")
.then(Commands.argument("value", DoubleArgumentType.doubleArg(0, 100))
.executes(ctx -> setPollution(ctx.getSource(), rm.get(),
DoubleArgumentType.getDouble(ctx, "value")))))
// --- succession stage ---
.then(Commands.literal("stage")
.then(Commands.argument("name", StringArgumentType.word())
.suggests(STAGE_SUGGESTIONS)
.executes(ctx -> setStage(ctx.getSource(), rm.get(),
StringArgumentType.getString(ctx, "name")))))
// --- succession cap ---
.then(Commands.literal("cap")
.then(Commands.argument("name", StringArgumentType.word())
.suggests(STAGE_SUGGESTIONS)
.executes(ctx -> setCap(ctx.getSource(), rm.get(),
StringArgumentType.getString(ctx, "name")))))
// --- regional rain level ---
.then(Commands.literal("rain")
.then(Commands.argument("value", DoubleArgumentType.doubleArg(0, 1))
.executes(ctx -> setRain(ctx.getSource(), rm.get(),
DoubleArgumentType.getDouble(ctx, "value")))));
}
// ------------------------------------------------------------------
// Executors
// ------------------------------------------------------------------
private static int setSoilFertility(CommandSourceStack src, RegionManager rm, double value) {
return modifyRegion(src, rm, "soil.fertility=" + value, region -> {
SoilRegionData soil = getOrDefault(region, SoilModule.MODULE_ID, SoilRegionData.class,
SoilRegionData.defaults());
soil.setFertility(value);
region.getModuleData().put(SoilModule.MODULE_ID, soil);
});
}
private static int setSoilMoisture(CommandSourceStack src, RegionManager rm, double value) {
return modifyRegion(src, rm, "soil.moisture=" + value, region -> {
SoilRegionData soil = getOrDefault(region, SoilModule.MODULE_ID, SoilRegionData.class,
SoilRegionData.defaults());
soil.setMoisture(value);
region.getModuleData().put(SoilModule.MODULE_ID, soil);
});
}
private static int setWaterAvailability(CommandSourceStack src, RegionManager rm, double value) {
return modifyRegion(src, rm, "water.availability=" + value, region -> {
WaterRegionData water = getOrDefault(region, WaterModule.MODULE_ID, WaterRegionData.class,
WaterRegionData.defaults());
water.setWaterAvailability(value);
region.getModuleData().put(WaterModule.MODULE_ID, water);
});
}
private static int setWaterDrought(CommandSourceStack src, RegionManager rm, double value) {
return modifyRegion(src, rm, "water.drought=" + value, region -> {
WaterRegionData water = getOrDefault(region, WaterModule.MODULE_ID, WaterRegionData.class,
WaterRegionData.defaults());
water.setDroughtRisk(value);
region.getModuleData().put(WaterModule.MODULE_ID, water);
});
}
private static int setPollution(CommandSourceStack src, RegionManager rm, double value) {
return modifyRegion(src, rm, "pollution=" + value, region -> {
PollutionRegionData existing = getOrDefault(region, PollutionModule.MODULE_ID,
PollutionRegionData.class, PollutionRegionData.defaults());
region.getModuleData().put(PollutionModule.MODULE_ID,
new PollutionRegionData(value, value * 0.5, value * 0.3,
existing.getDecayResistance()));
});
}
private static int setStage(CommandSourceStack src, RegionManager rm, String stageName) {
SuccessionStage stage = parseStage(src, stageName);
if (stage == null) return 0;
return modifyRegion(src, rm, "stage=" + stage.name(), region -> {
RecoveryRegionData rec = getOrDefault(region, RecoveryModule.MODULE_ID,
RecoveryRegionData.class, RecoveryRegionData.defaults());
// Construct a fresh data object at the desired stage, preserving cap.
SuccessionStage cap = rec.getMaxSuccessionStage();
if (stage.ordinal() > cap.ordinal()) {
// Auto-raise cap if forcing to a higher stage.
cap = stage;
}
region.getModuleData().put(RecoveryModule.MODULE_ID,
new RecoveryRegionData(stage, 0.0, 0.0, cap));
});
}
private static int setCap(CommandSourceStack src, RegionManager rm, String stageName) {
SuccessionStage stage = parseStage(src, stageName);
if (stage == null) return 0;
return modifyRegion(src, rm, "cap=" + stage.name(), region -> {
RecoveryRegionData rec = getOrDefault(region, RecoveryModule.MODULE_ID,
RecoveryRegionData.class, RecoveryRegionData.defaults());
rec.setMaxSuccessionStage(stage);
region.getModuleData().put(RecoveryModule.MODULE_ID, rec);
});
}
private static int setRain(CommandSourceStack src, RegionManager rm, double value) {
return modifyRegion(src, rm, "rain=" + value, region -> {
AtmosphereRegionData atm = getOrDefault(region, AtmosphereModule.MODULE_ID,
AtmosphereRegionData.class, AtmosphereRegionData.defaults());
atm.setRainLevel(value);
region.getModuleData().put(AtmosphereModule.MODULE_ID, atm);
});
}
// ------------------------------------------------------------------
// Shared helpers
// ------------------------------------------------------------------
/** Resolves the player's region, applies {@code mutation}, marks dirty, replies. */
private static int modifyRegion(CommandSourceStack src, RegionManager rm,
String change, RegionMutation mutation) {
if (rm == null) {
src.sendFailure(Component.literal("Region manager not ready."));
return 0;
}
ServerPlayer player;
try {
player = src.getPlayerOrException();
} catch (CommandSyntaxException e) {
src.sendFailure(Component.literal("/lw set must be run by a player."));
return 0;
}
String dimId = player.level().dimension().location().toString();
int rx = (int) Math.floor(player.getX() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
int rz = (int) Math.floor(player.getZ() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
RegionCoordinate coord = new RegionCoordinate(dimId, rx, rz);
Region region = rm.getOrCreateRegion(coord);
mutation.apply(region);
rm.markDirty(region);
src.sendSuccess(() -> Component.literal(
String.format("[LW] Set %s in region (%d,%d)", change, rx, rz)), false);
return 1;
}
private static <T> T getOrDefault(Region region, String moduleId, Class<T> type, T fallback) {
return region.getModuleData().get(moduleId, type).orElse(fallback);
}
private static SuccessionStage parseStage(CommandSourceStack src, String name) {
try {
return SuccessionStage.valueOf(name.toUpperCase());
} catch (IllegalArgumentException e) {
src.sendFailure(Component.literal(
"Unknown stage '" + name + "'. Valid: BARREN, SPARSE_GRASS, GRASSLAND,"
+ " SCRUBLAND, YOUNG_WOODLAND, MATURE_FOREST"));
return null;
}
}
@FunctionalInterface
private interface RegionMutation {
void apply(Region region);
}
}