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:
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user