Wire world effect executor — ecosystem simulation now mutates blocks in-game

NeoForgeWorldEffectExecutor translates WorldEffectRequests into actual Minecraft
block operations: GRASS_DEGRADES_TO_DIRT replaces grass with dirt, VEGETATION_SPREADS
spreads grass onto lit dirt, POLLUTION_VISUAL_INDICATOR spawns smoke particles.
SAPLING_GROWTH_SLOWED/BOOSTED are stubs pending mixin hooks.

Block writes are guarded by isLoaded() checks so no chunks are force-loaded.
Intensity scales the number of block candidates attempted per tick (max 8).
MinecraftServer is captured in LivingWorldMod on ServerStartedEvent and cleared
on stop; the executor receives it via Supplier to stay null-safe across restarts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
George
2026-06-07 16:01:57 +01:00
parent 6427677db5
commit 4fd9bb97aa
3 changed files with 135 additions and 14 deletions
@@ -14,6 +14,8 @@ import com.livingworld.core.LivingWorldConstants;
import com.livingworld.debug.DiagnosticCategory; import com.livingworld.debug.DiagnosticCategory;
import com.livingworld.debug.LivingWorldLogger; import com.livingworld.debug.LivingWorldLogger;
import com.livingworld.platform.neoforge.NeoForgePlatformAdapter; import com.livingworld.platform.neoforge.NeoForgePlatformAdapter;
import com.livingworld.platform.neoforge.NeoForgeWorldEffectExecutor;
import net.minecraft.server.MinecraftServer;
/** /**
* Mod entrypoint for Living World. * Mod entrypoint for Living World.
@@ -26,6 +28,7 @@ public class LivingWorldMod {
public static final String MOD_ID = LivingWorldConstants.MOD_ID; public static final String MOD_ID = LivingWorldConstants.MOD_ID;
private final LivingWorldBootstrap bootstrap; private final LivingWorldBootstrap bootstrap;
private MinecraftServer minecraftServer;
public LivingWorldMod(IEventBus eventBus) { public LivingWorldMod(IEventBus eventBus) {
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting..."); LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting...");
@@ -43,10 +46,16 @@ public class LivingWorldMod {
ServerStartingEvent.class, ServerStartingEvent.class,
event -> bootstrap.onServerStarting( event -> bootstrap.onServerStarting(
event.getServer().getWorldPath(LevelResource.ROOT))); event.getServer().getWorldPath(LevelResource.ROOT)));
NeoForge.EVENT_BUS.addListener( NeoForge.EVENT_BUS.addListener(ServerStartedEvent.class, event -> {
ServerStartedEvent.class, event -> bootstrap.onServerStarted()); this.minecraftServer = event.getServer();
NeoForge.EVENT_BUS.addListener( bootstrap.onServerStarted();
ServerStoppingEvent.class, event -> bootstrap.onServerStopping()); bootstrap.getWorldEffectsModule().registerConsumer(
new NeoForgeWorldEffectExecutor(() -> minecraftServer));
});
NeoForge.EVENT_BUS.addListener(ServerStoppingEvent.class, event -> {
bootstrap.onServerStopping();
this.minecraftServer = null;
});
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World Bootstrap initialized successfully."); LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World Bootstrap initialized successfully.");
} }
@@ -125,16 +125,6 @@ public final class LivingWorldBootstrap {
requireServerReady(); requireServerReady();
ServerContext context = new ServerContext(); ServerContext context = new ServerContext();
moduleRegistry.getEnabledModules().forEach(module -> module.onServerStarted(context)); moduleRegistry.getEnabledModules().forEach(module -> module.onServerStarted(context));
// Register the world-effects consumer now that services are live.
// Logs what would be applied; actual block manipulation is Track C.
worldEffectsModule.registerConsumer(request ->
LivingWorldLogger.info(
DiagnosticCategory.SIMULATION,
"WorldEffect: " + request.type()
+ " region=" + request.region()
+ " intensity=" + String.format("%.2f", request.intensity())));
LivingWorldLogger.info( LivingWorldLogger.info(
DiagnosticCategory.BOOTSTRAP, DiagnosticCategory.BOOTSTRAP,
"onServerStarted - server started."); "onServerStarted - server started.");
@@ -0,0 +1,122 @@
package com.livingworld.platform.neoforge;
import com.livingworld.core.LivingWorldConstants;
import com.livingworld.debug.DiagnosticCategory;
import com.livingworld.debug.LivingWorldLogger;
import com.livingworld.modules.worldeffects.WorldEffectConsumer;
import com.livingworld.modules.worldeffects.WorldEffectRequest;
import java.util.Random;
import java.util.function.Supplier;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.levelgen.Heightmap;
/**
* Translates {@link WorldEffectRequest}s from the ecosystem simulation into
* concrete Minecraft block operations and particle effects.
*
* <p>Called on the server tick thread. Block writes are guarded by a
* {@link ServerLevel#isLoaded} check to avoid force-loading chunks.</p>
*/
public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
private static final int REGION_BLOCKS =
LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16;
private static final int BLOCK_ATTEMPTS = 8;
private static final int MIN_GRASS_LIGHT = 9;
private final Supplier<MinecraftServer> serverSupplier;
private final Random random = new Random();
public NeoForgeWorldEffectExecutor(Supplier<MinecraftServer> serverSupplier) {
this.serverSupplier = serverSupplier;
}
@Override
public void consume(WorldEffectRequest request) {
MinecraftServer server = serverSupplier.get();
if (server == null) {
return;
}
ResourceKey<Level> dimensionKey = ResourceKey.create(
Registries.DIMENSION,
ResourceLocation.parse(request.region().dimensionId()));
ServerLevel level = server.getLevel(dimensionKey);
if (level == null) {
return;
}
int baseX = request.region().x() * REGION_BLOCKS;
int baseZ = request.region().z() * REGION_BLOCKS;
switch (request.type()) {
case GRASS_DEGRADES_TO_DIRT ->
degradeGrass(level, baseX, baseZ, request.intensity());
case VEGETATION_SPREADS ->
spreadVegetation(level, baseX, baseZ, request.intensity());
case POLLUTION_VISUAL_INDICATOR ->
spawnPollutionParticles(level, baseX, baseZ, request.intensity());
case SAPLING_GROWTH_SLOWED, SAPLING_GROWTH_BOOSTED -> {
// Sapling growth manipulation requires mixin hooks; deferred.
}
}
}
private void degradeGrass(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 && level.getBlockState(pos).is(Blocks.GRASS_BLOCK)) {
level.setBlock(pos, Blocks.DIRT.defaultBlockState(), Block.UPDATE_ALL);
LivingWorldLogger.info(DiagnosticCategory.SIMULATION,
"WorldEffect GRASS_DEGRADES_TO_DIRT at " + pos);
}
}
}
private void spreadVegetation(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 || !level.getBlockState(pos).is(Blocks.DIRT)) {
continue;
}
BlockPos above = pos.above();
if (level.getBlockState(above).isAir()
&& level.getRawBrightness(above, 0) >= MIN_GRASS_LIGHT) {
level.setBlock(pos, Blocks.GRASS_BLOCK.defaultBlockState(), Block.UPDATE_ALL);
LivingWorldLogger.info(DiagnosticCategory.SIMULATION,
"WorldEffect VEGETATION_SPREADS at " + pos);
}
}
}
private void spawnPollutionParticles(
ServerLevel level, int baseX, int baseZ, double intensity) {
int count = Math.max(1, (int) (intensity * 5));
for (int i = 0; i < count; i++) {
double x = baseX + random.nextDouble() * REGION_BLOCKS;
double z = baseZ + random.nextDouble() * REGION_BLOCKS;
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE, (int) x, (int) z);
level.sendParticles(ParticleTypes.SMOKE, x, y + 1.5, z, 3, 0.5, 0.5, 0.5, 0.02);
}
}
private BlockPos surfaceAt(ServerLevel level, int x, int z) {
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1;
if (y < level.getMinBuildHeight()) {
return null;
}
BlockPos pos = new BlockPos(x, y, z);
return level.isLoaded(pos) ? pos : null;
}
}