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