Tune ecosystem sensitivity and add simulation richness (Steps 1-6)

Tuning:
- simulationIntervalTicks: 100→50 (faster sim feedback)
- FURNACE_SCAN_INTERVAL: 100→50 (matches sim interval for pollution equilibrium)
- PollutionModule.BASE_DECAY_RATE: 0.02→0.008 (slower natural decay)
- SoilModule.POLLUTION_CONTAMINATION_THRESHOLD: 30→10, CONTAMINATION_FERTILITY_DRAIN: 0.002→0.005
- VegetationModule.DIEOFF_POLLUTION_THRESHOLD: 60→30
- WorldEffectsModule: lower grass-degrade/pollution-indicator thresholds so effects
  trigger from realistic furnace-driven pollution levels
- NeoForgeWorldEffectExecutor.BLOCK_ATTEMPTS: 8→20

New features:
- Campfire pollution source (0.2 air, 0.05 ground per lit campfire)
- Cross-region air pollution spreading (2% of gradient per sim cycle)
- SimulationManager.queueRegionForUpdate() for priority enqueueing
- LivingWorldBootstrap.notifyPlayerInRegion() boosts priority when player enters region
- Player region tracking in LivingWorldMod: checks every 20 MC ticks, queues update
  on region change
- NeoForgeWorldEffectExecutor: implement SAPLING_GROWTH_BOOSTED (oak sapling placement);
  VEGETATION_SPREADS also places short grass and flowers; GRASS_DEGRADES_TO_DIRT clears
  plants and converts dirt→coarse_dirt at intensity>0.5

All 400 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
George
2026-06-07 18:55:08 +01:00
parent 577c14b6ea
commit 773fb0223f
10 changed files with 173 additions and 47 deletions
@@ -10,9 +10,11 @@ import net.neoforged.neoforge.event.server.ServerStoppingEvent;
import net.minecraft.world.level.storage.LevelResource; import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.CampfireBlockEntity;
import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
@@ -24,9 +26,14 @@ import com.livingworld.debug.LivingWorldLogger;
import com.livingworld.platform.neoforge.NeoForgePlatformAdapter; import com.livingworld.platform.neoforge.NeoForgePlatformAdapter;
import com.livingworld.platform.neoforge.NeoForgeWorldEffectExecutor; import com.livingworld.platform.neoforge.NeoForgeWorldEffectExecutor;
import com.livingworld.regions.Region; import com.livingworld.regions.Region;
import com.livingworld.regions.RegionCoordinate;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.neoforged.neoforge.event.tick.ServerTickEvent; import net.neoforged.neoforge.event.tick.ServerTickEvent;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/** /**
* Mod entrypoint for Living World. * Mod entrypoint for Living World.
* <p> * <p>
@@ -38,14 +45,20 @@ public class LivingWorldMod {
public static final String MOD_ID = LivingWorldConstants.MOD_ID; public static final String MOD_ID = LivingWorldConstants.MOD_ID;
private static final int FURNACE_SCAN_INTERVAL = 100; private static final int FURNACE_SCAN_INTERVAL = 50;
private static final double AIR_POLLUTION_PER_FURNACE = 0.5; private static final double AIR_POLLUTION_PER_FURNACE = 0.5;
private static final double GROUND_POLLUTION_PER_FURNACE = 0.1; private static final double GROUND_POLLUTION_PER_FURNACE = 0.1;
private static final double WATER_POLLUTION_PER_FURNACE = 0.1; private static final double WATER_POLLUTION_PER_FURNACE = 0.1;
private static final double AIR_POLLUTION_PER_CAMPFIRE = 0.2;
private static final double GROUND_POLLUTION_PER_CAMPFIRE = 0.05;
private static final int PLAYER_CHECK_INTERVAL = 20;
private final LivingWorldBootstrap bootstrap; private final LivingWorldBootstrap bootstrap;
private MinecraftServer minecraftServer; private MinecraftServer minecraftServer;
private int furnaceScanTick = 0; private int furnaceScanTick = 0;
private int playerCheckTick = 0;
private final Map<UUID, RegionCoordinate> playerRegionCache = new HashMap<>();
public LivingWorldMod(IEventBus eventBus) { public LivingWorldMod(IEventBus eventBus) {
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting..."); LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting...");
@@ -74,13 +87,18 @@ public class LivingWorldMod {
NeoForge.EVENT_BUS.addListener(ServerStoppingEvent.class, event -> { NeoForge.EVENT_BUS.addListener(ServerStoppingEvent.class, event -> {
bootstrap.onServerStopping(); bootstrap.onServerStopping();
bootstrap.setOverworldRaining(null); bootstrap.setOverworldRaining(null);
playerRegionCache.clear();
this.minecraftServer = null; this.minecraftServer = null;
}); });
NeoForge.EVENT_BUS.addListener(ServerTickEvent.Post.class, event -> { NeoForge.EVENT_BUS.addListener(ServerTickEvent.Post.class, event -> {
if (minecraftServer == null || !bootstrap.isServerReady()) return; if (minecraftServer == null || !bootstrap.isServerReady()) return;
if (++furnaceScanTick % FURNACE_SCAN_INTERVAL != 0) return; if (++furnaceScanTick % FURNACE_SCAN_INTERVAL == 0) {
scanAndRecordFurnaceActivity(); scanAndRecordFurnaceActivity();
}
if (++playerCheckTick % PLAYER_CHECK_INTERVAL == 0) {
checkPlayerRegions();
}
}); });
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World Bootstrap initialized successfully."); LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World Bootstrap initialized successfully.");
@@ -92,33 +110,49 @@ public class LivingWorldMod {
ServerLevel level = minecraftServer.getLevel( ServerLevel level = minecraftServer.getLevel(
ResourceKey.create(Registries.DIMENSION, ResourceLocation.parse(dimensionId))); ResourceKey.create(Registries.DIMENSION, ResourceLocation.parse(dimensionId)));
if (level == null) continue; if (level == null) continue;
int litCount = countLitFurnaces(level, region);
if (litCount > 0) {
bootstrap.handleFurnaceActivity(region,
litCount * AIR_POLLUTION_PER_FURNACE,
litCount * GROUND_POLLUTION_PER_FURNACE,
litCount * WATER_POLLUTION_PER_FURNACE);
}
}
}
private int countLitFurnaces(ServerLevel level, Region region) {
int count = 0;
int baseChunkX = region.getCoordinate().x() * LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS; int baseChunkX = region.getCoordinate().x() * LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS;
int baseChunkZ = region.getCoordinate().z() * LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS; int baseChunkZ = region.getCoordinate().z() * LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS;
int furnaces = 0;
int campfires = 0;
for (int cx = baseChunkX; cx < baseChunkX + LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS; cx++) { for (int cx = baseChunkX; cx < baseChunkX + LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS; cx++) {
for (int cz = baseChunkZ; cz < baseChunkZ + LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS; cz++) { for (int cz = baseChunkZ; cz < baseChunkZ + LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS; cz++) {
LevelChunk chunk = level.getChunkSource().getChunkNow(cx, cz); LevelChunk chunk = level.getChunkSource().getChunkNow(cx, cz);
if (chunk == null) continue; if (chunk == null) continue;
for (BlockEntity be : chunk.getBlockEntities().values()) { for (BlockEntity be : chunk.getBlockEntities().values()) {
if (!(be instanceof AbstractFurnaceBlockEntity)) continue; if (be instanceof AbstractFurnaceBlockEntity
if (be.getBlockState().hasProperty(BlockStateProperties.LIT) && be.getBlockState().hasProperty(BlockStateProperties.LIT)
&& Boolean.TRUE.equals(be.getBlockState().getValue(BlockStateProperties.LIT))) { && Boolean.TRUE.equals(be.getBlockState().getValue(BlockStateProperties.LIT))) {
count++; furnaces++;
} else if (be instanceof CampfireBlockEntity
&& be.getBlockState().hasProperty(BlockStateProperties.LIT)
&& Boolean.TRUE.equals(be.getBlockState().getValue(BlockStateProperties.LIT))) {
campfires++;
} }
} }
} }
} }
return count;
double air = furnaces * AIR_POLLUTION_PER_FURNACE + campfires * AIR_POLLUTION_PER_CAMPFIRE;
double ground = furnaces * GROUND_POLLUTION_PER_FURNACE + campfires * GROUND_POLLUTION_PER_CAMPFIRE;
double water = furnaces * WATER_POLLUTION_PER_FURNACE;
if (air > 0 || ground > 0) {
bootstrap.handleFurnaceActivity(region, air, ground, water);
}
}
}
private void checkPlayerRegions() {
for (ServerPlayer player : minecraftServer.getPlayerList().getPlayers()) {
String dimId = player.level().dimension().location().toString();
int regionX = (int) Math.floor(player.getX() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
int regionZ = (int) Math.floor(player.getZ() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
RegionCoordinate coord = new RegionCoordinate(dimId, regionX, regionZ);
RegionCoordinate previous = playerRegionCache.put(player.getUUID(), coord);
if (!coord.equals(previous)) {
bootstrap.notifyPlayerInRegion(coord);
}
}
} }
} }
@@ -47,7 +47,9 @@ import com.livingworld.regions.query.RegionQueryEngine;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
/** /**
@@ -219,6 +221,12 @@ public final class LivingWorldBootstrap {
regionManager.markDirty(region); regionManager.markDirty(region);
} }
public void notifyPlayerInRegion(RegionCoordinate coordinate) {
if (!serverReady || coordinate == null) return;
regionManager.getOrCreateRegion(coordinate);
simulationManager.queueRegionForUpdate(coordinate, 10, com.livingworld.core.simulation.UpdateReason.PLAYER_NEARBY);
}
public void setOverworldRaining(BooleanSupplier supplier) { public void setOverworldRaining(BooleanSupplier supplier) {
this.overworldRaining = supplier != null ? supplier : () -> false; this.overworldRaining = supplier != null ? supplier : () -> false;
} }
@@ -231,10 +239,49 @@ public final class LivingWorldBootstrap {
simulationManager.onMinecraftServerTick(); simulationManager.onMinecraftServerTick();
if (simulationManager.getSimulationTickCounter() != previousSimulationTick) { if (simulationManager.getSimulationTickCounter() != previousSimulationTick) {
applyWeatherFeedback(); applyWeatherFeedback();
spreadPollutionAcrossRegions();
regionManager.saveDirtyRegions(); regionManager.saveDirtyRegions();
} }
} }
private static final double POLLUTION_SPREAD_RATE = 0.02;
private void spreadPollutionAcrossRegions() {
Collection<Region> active = regionManager.getActiveRegions();
if (active.size() < 2) return;
Map<RegionCoordinate, Region> byCoord = new HashMap<>();
for (Region r : active) byCoord.put(r.getCoordinate(), r);
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (Region region : active) {
RegionCoordinate coord = region.getCoordinate();
PollutionRegionData data = region.getModuleData()
.get(PollutionModule.MODULE_ID, PollutionRegionData.class)
.orElse(null);
if (data == null) continue;
for (int[] off : offsets) {
RegionCoordinate neighbourCoord = new RegionCoordinate(
coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]);
Region neighbour = byCoord.get(neighbourCoord);
if (neighbour == null) continue;
PollutionRegionData neighbourData = neighbour.getModuleData()
.get(PollutionModule.MODULE_ID, PollutionRegionData.class)
.orElse(null);
if (neighbourData == null) continue;
double diff = data.getAirPollution() - neighbourData.getAirPollution();
if (diff <= 0) continue;
double transfer = diff * POLLUTION_SPREAD_RATE;
data.addPollution(-transfer, 0, 0);
neighbourData.addPollution(transfer, 0, 0);
neighbour.getModuleData().put(PollutionModule.MODULE_ID, neighbourData);
regionManager.markDirty(neighbour);
}
region.getModuleData().put(PollutionModule.MODULE_ID, data);
regionManager.markDirty(region);
}
}
private void applyWeatherFeedback() { private void applyWeatherFeedback() {
boolean raining = overworldRaining.getAsBoolean(); boolean raining = overworldRaining.getAsBoolean();
for (Region region : regionManager.getActiveRegions()) { for (Region region : regionManager.getActiveRegions()) {
@@ -18,7 +18,7 @@ public final class SimulationConfig {
private int regionSizeChunks = 8; private int regionSizeChunks = 8;
/** Interval between simulation cycles, in game ticks (must be >= 1). */ /** Interval between simulation cycles, in game ticks (must be >= 1). */
private int simulationIntervalTicks = 100; private int simulationIntervalTicks = 50;
/** Maximum number of regions processed per cycle (must be >= 1). */ /** Maximum number of regions processed per cycle (must be >= 1). */
private int maxRegionsPerCycle = 50; private int maxRegionsPerCycle = 50;
@@ -227,6 +227,14 @@ public final class SimulationManager {
} }
} }
/** Enqueues a region for a priority update, e.g. because a player entered it. */
public void queueRegionForUpdate(RegionCoordinate coordinate, int priority, UpdateReason reason) {
if (coordinate == null) throw new IllegalArgumentException("coordinate must not be null");
if (reason == null) throw new IllegalArgumentException("reason must not be null");
long tick = getSimulationTickCounter();
this.scheduler.queueRegion(new RegionUpdateJob(coordinate, priority, tick, null, reason));
}
/** Returns a profile snapshot of the last completed simulation cycle. */ /** Returns a profile snapshot of the last completed simulation cycle. */
public SimulationProfileSnapshot createProfileSnapshot() { public SimulationProfileSnapshot createProfileSnapshot() {
if (profiler instanceof com.livingworld.debug.SimulationProfiler concrete) { if (profiler instanceof com.livingworld.debug.SimulationProfiler concrete) {
@@ -42,7 +42,7 @@ public final class PollutionModule implements SimulationModule {
public static final String MODULE_ID = "pollution"; public static final String MODULE_ID = "pollution";
private static final double BASE_DECAY_RATE = 0.02; private static final double BASE_DECAY_RATE = 0.008;
private static final double GROUND_TO_WATER_LEACH = 0.005; private static final double GROUND_TO_WATER_LEACH = 0.005;
private static final double WATER_QUALITY_IMPACT = 0.05; private static final double WATER_QUALITY_IMPACT = 0.05;
private static final double CHANGE_THRESHOLD = 0.01; private static final double CHANGE_THRESHOLD = 0.01;
@@ -33,11 +33,11 @@ public final class SoilModule implements SimulationModule {
public static final String MODULE_ID = "soil"; public static final String MODULE_ID = "soil";
/** Pollution score above which contamination begins accumulating per tick. */ /** Pollution score above which contamination begins accumulating per tick. */
private static final double POLLUTION_CONTAMINATION_THRESHOLD = 30.0; private static final double POLLUTION_CONTAMINATION_THRESHOLD = 10.0;
/** Fraction of excess pollution score that becomes contamination per tick. */ /** Fraction of excess pollution score that becomes contamination per tick. */
private static final double POLLUTION_TO_CONTAMINATION_RATE = 0.003; private static final double POLLUTION_TO_CONTAMINATION_RATE = 0.003;
/** Fertility reduction per unit of contamination per tick. */ /** Fertility reduction per unit of contamination per tick. */
private static final double CONTAMINATION_FERTILITY_DRAIN = 0.002; private static final double CONTAMINATION_FERTILITY_DRAIN = 0.005;
/** Vegetation pressure threshold for fertility recovery to kick in. */ /** Vegetation pressure threshold for fertility recovery to kick in. */
private static final double VEGETATION_RECOVERY_THRESHOLD = 40.0; private static final double VEGETATION_RECOVERY_THRESHOLD = 40.0;
/** Fertility gained per unit of excess vegetation pressure per tick. */ /** Fertility gained per unit of excess vegetation pressure per tick. */
@@ -52,7 +52,7 @@ public final class VegetationModule implements SimulationModule {
// --- die-off thresholds --- // --- die-off thresholds ---
private static final double DIEOFF_SOIL_THRESHOLD = 20.0; private static final double DIEOFF_SOIL_THRESHOLD = 20.0;
private static final double DIEOFF_POLLUTION_THRESHOLD = 60.0; private static final double DIEOFF_POLLUTION_THRESHOLD = 30.0;
private static final double GRASS_DIEOFF_RATE = 0.30; private static final double GRASS_DIEOFF_RATE = 0.30;
private static final double DEAD_ACCUMULATION_RATE = 0.20; private static final double DEAD_ACCUMULATION_RATE = 0.20;
@@ -30,7 +30,7 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
private static final int REGION_BLOCKS = private static final int REGION_BLOCKS =
LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16; LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16;
private static final int BLOCK_ATTEMPTS = 8; private static final int BLOCK_ATTEMPTS = 20;
private static final int MIN_GRASS_LIGHT = 9; private static final int MIN_GRASS_LIGHT = 9;
private final Supplier<MinecraftServer> serverSupplier; private final Supplier<MinecraftServer> serverSupplier;
@@ -63,9 +63,9 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
spreadVegetation(level, baseX, baseZ, request.intensity()); spreadVegetation(level, baseX, baseZ, request.intensity());
case POLLUTION_VISUAL_INDICATOR -> case POLLUTION_VISUAL_INDICATOR ->
spawnPollutionParticles(level, baseX, baseZ, request.intensity()); spawnPollutionParticles(level, baseX, baseZ, request.intensity());
case SAPLING_GROWTH_SLOWED, SAPLING_GROWTH_BOOSTED -> { case SAPLING_GROWTH_BOOSTED ->
// Sapling growth manipulation requires mixin hooks; deferred. placeSaplings(level, baseX, baseZ, request.intensity());
} case SAPLING_GROWTH_SLOWED -> {} // requires mixin; deferred
} }
} }
@@ -74,10 +74,24 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
for (int i = 0; i < attempts; i++) { for (int i = 0; i < attempts; i++) {
BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS), BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
baseZ + random.nextInt(REGION_BLOCKS)); baseZ + random.nextInt(REGION_BLOCKS));
if (pos != null && level.getBlockState(pos).is(Blocks.GRASS_BLOCK)) { if (pos == null) continue;
BlockPos above = pos.above();
// Clear plants sitting on top of the block first
if (level.isLoaded(above)) {
var aboveState = level.getBlockState(above);
if (aboveState.is(Blocks.SHORT_GRASS) || aboveState.is(Blocks.TALL_GRASS)
|| aboveState.is(Blocks.DANDELION) || aboveState.is(Blocks.POPPY)
|| aboveState.is(Blocks.OAK_SAPLING)) {
level.setBlock(above, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
}
}
var state = level.getBlockState(pos);
if (state.is(Blocks.GRASS_BLOCK)) {
level.setBlock(pos, Blocks.DIRT.defaultBlockState(), Block.UPDATE_ALL); level.setBlock(pos, Blocks.DIRT.defaultBlockState(), Block.UPDATE_ALL);
LivingWorldLogger.info(DiagnosticCategory.SIMULATION, LivingWorldLogger.info(DiagnosticCategory.SIMULATION,
"WorldEffect GRASS_DEGRADES_TO_DIRT at " + pos); "WorldEffect GRASS_DEGRADES_TO_DIRT at " + pos);
} else if (state.is(Blocks.DIRT) && intensity > 0.5) {
level.setBlock(pos, Blocks.COARSE_DIRT.defaultBlockState(), Block.UPDATE_ALL);
} }
} }
} }
@@ -87,22 +101,45 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
for (int i = 0; i < attempts; i++) { for (int i = 0; i < attempts; i++) {
BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS), BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
baseZ + random.nextInt(REGION_BLOCKS)); baseZ + random.nextInt(REGION_BLOCKS));
if (pos == null || !level.getBlockState(pos).is(Blocks.DIRT)) { if (pos == null) continue;
continue;
}
BlockPos above = pos.above(); BlockPos above = pos.above();
if (level.getBlockState(above).isAir() boolean aboveAir = level.isLoaded(above) && level.getBlockState(above).isAir();
&& level.getRawBrightness(above, 0) >= MIN_GRASS_LIGHT) { boolean brightEnough = aboveAir && level.getRawBrightness(above, 0) >= MIN_GRASS_LIGHT;
var state = level.getBlockState(pos);
if (state.is(Blocks.DIRT) && brightEnough) {
level.setBlock(pos, Blocks.GRASS_BLOCK.defaultBlockState(), Block.UPDATE_ALL); level.setBlock(pos, Blocks.GRASS_BLOCK.defaultBlockState(), Block.UPDATE_ALL);
LivingWorldLogger.info(DiagnosticCategory.SIMULATION, LivingWorldLogger.info(DiagnosticCategory.SIMULATION,
"WorldEffect VEGETATION_SPREADS at " + pos); "WorldEffect VEGETATION_SPREADS at " + pos);
} else if (state.is(Blocks.GRASS_BLOCK) && brightEnough) {
// Add surface plants — flowers 1-in-5 chance, short grass otherwise
Block plant = random.nextInt(5) == 0
? (random.nextBoolean() ? Blocks.DANDELION : Blocks.POPPY)
: Blocks.SHORT_GRASS;
level.setBlock(above, plant.defaultBlockState(), Block.UPDATE_ALL);
} }
} }
} }
private void placeSaplings(ServerLevel level, int baseX, int baseZ, double intensity) {
int attempts = Math.max(1, (int) (intensity * BLOCK_ATTEMPTS));
for (int i = 0; i < attempts; i++) {
BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
baseZ + random.nextInt(REGION_BLOCKS));
if (pos == null) continue;
var state = level.getBlockState(pos);
if (!state.is(Blocks.GRASS_BLOCK) && !state.is(Blocks.DIRT)) continue;
BlockPos above = pos.above();
if (!level.isLoaded(above) || !level.getBlockState(above).isAir()) continue;
level.setBlock(above, Blocks.OAK_SAPLING.defaultBlockState(), Block.UPDATE_ALL);
LivingWorldLogger.info(DiagnosticCategory.SIMULATION,
"WorldEffect SAPLING_GROWTH_BOOSTED at " + pos);
}
}
private void spawnPollutionParticles( private void spawnPollutionParticles(
ServerLevel level, int baseX, int baseZ, double intensity) { ServerLevel level, int baseX, int baseZ, double intensity) {
int count = Math.max(1, (int) (intensity * 5)); int count = Math.max(1, (int) (intensity * 8));
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
double x = baseX + random.nextDouble() * REGION_BLOCKS; double x = baseX + random.nextDouble() * REGION_BLOCKS;
double z = baseZ + random.nextDouble() * REGION_BLOCKS; double z = baseZ + random.nextDouble() * REGION_BLOCKS;
@@ -41,7 +41,7 @@ class LivingWorldBootstrapTest {
assertTrue(bootstrap.getServices().isRegistered(CoreServices.TIME)); assertTrue(bootstrap.getServices().isRegistered(CoreServices.TIME));
assertTrue(bootstrap.getServices().isRegistered(CoreServices.DEBUG)); assertTrue(bootstrap.getServices().isRegistered(CoreServices.DEBUG));
for (int tick = 0; tick < 100; tick++) { for (int tick = 0; tick < 50; tick++) {
bootstrap.onServerTick(); bootstrap.onServerTick();
} }
TimeService timeService = bootstrap.getServices().get(CoreServices.TIME); TimeService timeService = bootstrap.getServices().get(CoreServices.TIME);
@@ -31,7 +31,7 @@ class SimulationConfigTest {
@Test @Test
void defaultSimulationIntervalTicks() { void defaultSimulationIntervalTicks() {
final SimulationConfig config = new SimulationConfig(); final SimulationConfig config = new SimulationConfig();
assertEquals(100, config.getSimulationIntervalTicks()); assertEquals(50, config.getSimulationIntervalTicks());
} }
@Test @Test
@@ -276,6 +276,6 @@ class SimulationConfigTest {
final String result = config.toString(); final String result = config.toString();
assertTrue(result.contains("SimulationConfig")); assertTrue(result.contains("SimulationConfig"));
assertTrue(result.contains("regionSizeChunks=8")); assertTrue(result.contains("regionSizeChunks=8"));
assertTrue(result.contains("simulationIntervalTicks=100")); assertTrue(result.contains("simulationIntervalTicks=50"));
} }
} }