From 4fd9bb97aa51a4f04f922c9e521b4038dcf4889b Mon Sep 17 00:00:00 2001 From: George Date: Sun, 7 Jun 2026 16:01:57 +0100 Subject: [PATCH] =?UTF-8?q?Wire=20world=20effect=20executor=20=E2=80=94=20?= =?UTF-8?q?ecosystem=20simulation=20now=20mutates=20blocks=20in-game?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../java/com/livingworld/LivingWorldMod.java | 17 ++- .../bootstrap/LivingWorldBootstrap.java | 10 -- .../neoforge/NeoForgeWorldEffectExecutor.java | 122 ++++++++++++++++++ 3 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java diff --git a/src/main/java/com/livingworld/LivingWorldMod.java b/src/main/java/com/livingworld/LivingWorldMod.java index 2bb6885..12fe0c0 100644 --- a/src/main/java/com/livingworld/LivingWorldMod.java +++ b/src/main/java/com/livingworld/LivingWorldMod.java @@ -14,6 +14,8 @@ import com.livingworld.core.LivingWorldConstants; import com.livingworld.debug.DiagnosticCategory; import com.livingworld.debug.LivingWorldLogger; import com.livingworld.platform.neoforge.NeoForgePlatformAdapter; +import com.livingworld.platform.neoforge.NeoForgeWorldEffectExecutor; +import net.minecraft.server.MinecraftServer; /** * Mod entrypoint for Living World. @@ -26,6 +28,7 @@ public class LivingWorldMod { public static final String MOD_ID = LivingWorldConstants.MOD_ID; private final LivingWorldBootstrap bootstrap; + private MinecraftServer minecraftServer; public LivingWorldMod(IEventBus eventBus) { LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting..."); @@ -43,10 +46,16 @@ public class LivingWorldMod { ServerStartingEvent.class, event -> bootstrap.onServerStarting( event.getServer().getWorldPath(LevelResource.ROOT))); - NeoForge.EVENT_BUS.addListener( - ServerStartedEvent.class, event -> bootstrap.onServerStarted()); - NeoForge.EVENT_BUS.addListener( - ServerStoppingEvent.class, event -> bootstrap.onServerStopping()); + NeoForge.EVENT_BUS.addListener(ServerStartedEvent.class, event -> { + this.minecraftServer = event.getServer(); + bootstrap.onServerStarted(); + 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."); } diff --git a/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java b/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java index caaba28..45fc9df 100644 --- a/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java +++ b/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java @@ -125,16 +125,6 @@ public final class LivingWorldBootstrap { requireServerReady(); ServerContext context = new ServerContext(); 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( DiagnosticCategory.BOOTSTRAP, "onServerStarted - server started."); diff --git a/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java b/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java new file mode 100644 index 0000000..1f729c8 --- /dev/null +++ b/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java @@ -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. + * + *

Called on the server tick thread. Block writes are guarded by a + * {@link ServerLevel#isLoaded} check to avoid force-loading chunks.

+ */ +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 serverSupplier; + private final Random random = new Random(); + + public NeoForgeWorldEffectExecutor(Supplier serverSupplier) { + this.serverSupplier = serverSupplier; + } + + @Override + public void consume(WorldEffectRequest request) { + MinecraftServer server = serverSupplier.get(); + if (server == null) { + return; + } + ResourceKey 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; + } +}