Add TOML config, region borders, seed dispersal corridors, wind, and ambient sound

1. TOML config (livingworld-server.toml) — NeoForgeModConfig registers a
   server-side config spec exposing all major tuning constants: pollution
   decay/spread, vegetation growth/dieoff rates, recovery rates, and seed
   dispersal params. EcosystemTuning wires these into the service registry
   (CoreServices.TUNING); PollutionModule, VegetationModule, RecoveryModule
   all read from it so admins can tune without recompiling.

2. Persistence audit — CodecKeys confirmed complete and correct for all 8
   data-bearing modules. The 4-arg RecoveryRegionData constructor (from
   the prior session) correctly preserves maxSuccessionStage on every path.
   No structural issues found.

3. /lw region borders [regionX regionZ] — draws cyan DustParticle lines
   along all 4 edges of the region at surface height (32 samples per edge,
   ~4 blocks apart on the 128-block edge).

4. Pollution spreading — already in place via wind-based air diffusion.
   Extended: applyWaterRunoff now also transports ground pollution
   downstream (15 % of ground pollution × runoff rate) with a small water
   bleed, so contaminated upland regions poison their downhill neighbours.

5. Sound ambience — per-region throttled (≥8 check-intervals between
   plays). Forest (YOUNG_WOODLAND+, health>50): azalea leaves rustling.
   Barren (SPARSE_GRASS-): dry sand step. Heavy pollution (score>50):
   basalt deltas mood. Direct ClientboundSoundPacket per player so other
   nearby players aren't spammed.

6. Seed dispersal & ecological corridors — applySeedDispersal() runs each
   post-sim cycle. Healthy regions (YOUNG_WOODLAND+) stochastically emit
   seeds to all 4 neighbours proportional to vegetationPressure. Pollution
   in the target blocks seeds. Corridor effect: if the target has 2+ healthy
   neighbours, seed strength × corridorBoostMultiplier (default 3.5×).
   When accumulated seed rain ≥ 5.0 the target's succession cap is
   pioneered one stage ahead of what physical conditions alone would allow.
   /lw wind shows current wind angle and spread rate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
George
2026-06-09 20:31:56 +01:00
parent 72d94b9f28
commit b68390983c
10 changed files with 617 additions and 68 deletions
@@ -1,6 +1,8 @@
package com.livingworld;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.config.ModConfig;
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.common.NeoForge;
@@ -33,8 +35,13 @@ import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.core.Holder;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.network.protocol.game.ClientboundGameEventPacket;
import net.minecraft.network.protocol.game.ClientboundSoundPacket;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
@@ -46,6 +53,7 @@ import com.livingworld.core.LivingWorldConstants;
import com.livingworld.debug.DiagnosticCategory;
import com.livingworld.debug.LivingWorldLogger;
import com.livingworld.modules.recovery.SuccessionStage;
import com.livingworld.platform.neoforge.NeoForgeModConfig;
import com.livingworld.platform.neoforge.NeoForgePlatformAdapter;
import com.livingworld.platform.neoforge.NeoForgeWorldEffectExecutor;
import com.livingworld.regions.Region;
@@ -98,10 +106,15 @@ public class LivingWorldMod {
/** Stores playerCheckTick value when a region was last water-body-scanned. */
private final Map<RegionCoordinate, Integer> waterBodyLastScan = new HashMap<>();
private final Set<RegionCoordinate> elevationInitialized = new HashSet<>();
/** Stores playerCheckTick when a region last played an ambient sound (per-region throttle). */
private final Map<RegionCoordinate, Integer> regionSoundLastTick = new HashMap<>();
public LivingWorldMod(IEventBus eventBus) {
public LivingWorldMod(IEventBus eventBus, ModContainer modContainer) {
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting...");
// Register server-side TOML config (written to world/serverconfig/livingworld-server.toml).
modContainer.registerConfig(ModConfig.Type.SERVER, NeoForgeModConfig.SPEC);
this.bootstrap = new LivingWorldBootstrap();
NeoForgePlatformAdapter platformAdapter = new NeoForgePlatformAdapter(
bootstrap::getWorldSaveDirectory,
@@ -113,8 +126,11 @@ public class LivingWorldMod {
eventBus.addListener(FMLCommonSetupEvent.class, event -> bootstrap.onCommonSetup());
NeoForge.EVENT_BUS.addListener(
ServerStartingEvent.class,
event -> bootstrap.onServerStarting(
event.getServer().getWorldPath(LevelResource.ROOT)));
event -> {
// Config is loaded before ServerStartingEvent; extract tuning values now.
bootstrap.setEcosystemTuning(NeoForgeModConfig.createTuning());
bootstrap.onServerStarting(event.getServer().getWorldPath(LevelResource.ROOT));
});
NeoForge.EVENT_BUS.addListener(ServerStartedEvent.class, event -> {
this.minecraftServer = event.getServer();
bootstrap.onServerStarted();
@@ -134,6 +150,7 @@ public class LivingWorldMod {
biomeInitialized.clear();
waterBodyLastScan.clear();
elevationInitialized.clear();
regionSoundLastTick.clear();
this.minecraftServer = null;
});
@@ -334,8 +351,61 @@ public class LivingWorldMod {
if (showHud) {
metricsOpt.ifPresent(m -> player.displayClientMessage(buildHud(coord, m, atm), true));
}
// Sound ambience — throttled per region so only one sound fires per region per
// ~8 seconds regardless of how many players are standing in it.
if (player.level() instanceof ServerLevel ambLevel) {
Integer lastSound = regionSoundLastTick.get(coord);
if (lastSound == null || (playerCheckTick - lastSound) >= 8) {
SuccessionStage stage = bootstrap.getSuccessionStageAt(dimId, player.getX(), player.getZ())
.orElse(null);
double health = metricsOpt.map(RegionMetrics::getEcosystemHealth).orElse(0.0);
double poll = metricsOpt.map(RegionMetrics::getPollutionScore).orElse(0.0);
if (tryPlayRegionAmbience(player, stage, health, poll)) {
regionSoundLastTick.put(coord, playerCheckTick);
}
}
}
}
}
/**
* Plays a single ambient sound appropriate for the region's ecological state.
* Each sound fires at low volume and random pitch so it blends naturally.
* Returns true if a sound was played (so the per-region cooldown can be set).
*/
private boolean tryPlayRegionAmbience(
ServerPlayer player, SuccessionStage stage, double health, double pollution) {
float pitch = 0.75f + random.nextFloat() * 0.5f;
if (stage != null && stage.ordinal() >= SuccessionStage.YOUNG_WOODLAND.ordinal()
&& health > 50.0 && random.nextInt(10) == 0) {
// Rustling leaves — healthy forest ambience.
playAmbientSound(player, Holder.direct(SoundEvents.AZALEA_LEAVES_STEP), 0.18f, pitch);
return true;
}
if (stage != null && stage.ordinal() <= SuccessionStage.SPARSE_GRASS.ordinal()
&& random.nextInt(15) == 0) {
// Dry, shifting sand — barren/arid ambience.
playAmbientSound(player, Holder.direct(SoundEvents.SAND_STEP), 0.10f, 0.55f + random.nextFloat() * 0.2f);
return true;
}
if (pollution > 50.0 && random.nextInt(18) == 0) {
// Industrial mood — heavy-pollution ambience (already a Holder.Reference).
playAmbientSound(player, SoundEvents.AMBIENT_BASALT_DELTAS_MOOD, 0.12f, 1.0f + random.nextFloat() * 0.15f);
return true;
}
return false;
}
/** Sends an ambient sound packet directly to one player so others nearby don't hear it. */
private static void playAmbientSound(ServerPlayer player, Holder<SoundEvent> sound, float vol, float pitch) {
player.connection.send(new ClientboundSoundPacket(
sound,
SoundSource.AMBIENT,
player.getX(), player.getEyeY(), player.getZ(),
vol, pitch, player.getRandom().nextLong()));
}
private static final int REGION_BLOCKS =
LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16;
@@ -2,6 +2,7 @@ package com.livingworld.bootstrap;
import com.livingworld.commands.LivingWorldCommandRoot;
import com.livingworld.config.DefaultConfigService;
import com.livingworld.config.EcosystemTuning;
import com.livingworld.config.SimulationConfig;
import com.livingworld.core.LivingWorldConstants;
import com.livingworld.core.services.CoreServices;
@@ -69,11 +70,18 @@ import net.minecraft.commands.CommandSourceStack;
*/
public final class LivingWorldBootstrap {
private static final double WIND_BOOST = 0.5;
private static final double WIND_DRIFT_MAX = 0.05; // radians per sim cycle
private double windAngle = 0.0;
private final Random windRandom = new Random();
/** Ecosystem tuning constants — loaded from TOML config at server start. */
private EcosystemTuning ecosystemTuning = new EcosystemTuning();
/**
* Per-region accumulated incoming seed strength from healthy neighbours.
* Populated each post-sim cycle by {@link #applySeedDispersal()}.
* Transient — not persisted; re-accumulates within a few sim cycles on restart.
*/
private final Map<RegionCoordinate, Double> accumulatedSeedRain = new HashMap<>();
private final Set<UUID> hudEnabledPlayers = new HashSet<>();
private PlatformAdapter platformAdapter;
@@ -206,6 +214,11 @@ public final class LivingWorldBootstrap {
/**
* Called when the server is stopping.
*/
/** Sets the ecosystem tuning loaded from the server config. Must be called before onServerStarting(). */
public void setEcosystemTuning(EcosystemTuning tuning) {
if (tuning != null) ecosystemTuning = tuning;
}
public void onServerStopping() {
if (!serverReady) {
return;
@@ -215,6 +228,7 @@ public final class LivingWorldBootstrap {
climateTracker.save(worldSaveDirectory.resolve("living_world/global_climate.dat"));
hudEnabledPlayers.clear();
regionElevations.clear();
accumulatedSeedRain.clear();
simSpeedMultiplier = 1;
serverReady = false;
LivingWorldLogger.info(
@@ -367,6 +381,7 @@ public final class LivingWorldBootstrap {
applyClimateWarmingEffects();
applyWaterRunoff();
applyDynamicCapUpdate();
applySeedDispersal();
}
/** Sets the simulation speed multiplier (1 = real-time, max 100). */
@@ -379,8 +394,6 @@ public final class LivingWorldBootstrap {
public int getSimSpeedMultiplier() { return simSpeedMultiplier; }
private static final double POLLUTION_SPREAD_RATE = 0.02;
private void spreadPollutionAcrossRegions() {
Collection<Region> active = regionManager.getActiveRegions();
if (active.size() < 2) return;
@@ -413,7 +426,8 @@ public final class LivingWorldBootstrap {
// Wind alignment: positive = downwind (faster spread), negative = upwind (slower).
double offsetAngle = Math.atan2(off[1], off[0]);
double alignment = Math.cos(windAngle - offsetAngle);
double rate = POLLUTION_SPREAD_RATE * (1.0 + alignment * WIND_BOOST);
double rate = ecosystemTuning.getPollutionSpreadRate()
* (1.0 + alignment * ecosystemTuning.getWindBoost());
double transfer = diff * rate;
data.addPollution(-transfer, 0, 0);
neighbourData.addPollution(transfer, 0, 0);
@@ -523,6 +537,20 @@ public final class LivingWorldBootstrap {
nWater.setWaterAvailability(Math.min(100, nWater.getWaterAvailability() + runoff));
nWater.setDroughtRisk(Math.max(0, nWater.getDroughtRisk() - runoff * 0.5));
neighbour.getModuleData().put(WaterModule.MODULE_ID, nWater);
// Runoff also carries ground pollution downstream — contaminants travel with water.
PollutionRegionData srcPoll = region.getModuleData()
.get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
PollutionRegionData dstPoll = neighbour.getModuleData()
.get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
if (srcPoll != null && dstPoll != null && srcPoll.getGroundPollution() > 0.5) {
double pollTransfer = runoff * 0.15 * srcPoll.getGroundPollution();
srcPoll.addPollution(0, -pollTransfer, 0);
dstPoll.addPollution(0, pollTransfer, pollTransfer * 0.1); // small bleed to water
region.getModuleData().put(PollutionModule.MODULE_ID, srcPoll);
neighbour.getModuleData().put(PollutionModule.MODULE_ID, dstPoll);
}
regionManager.markDirty(neighbour);
}
region.getModuleData().put(WaterModule.MODULE_ID, myWater);
@@ -578,6 +606,130 @@ public final class LivingWorldBootstrap {
return SuccessionStage.SPARSE_GRASS;
}
/**
* Spreads seeds from healthy regions (YOUNG_WOODLAND+) to their neighbours, allowing
* degraded regions to recover faster — especially when flanked by multiple healthy regions
* (corridor effect).
*
* <p>Seeds are blocked by pollution in the target region. When a target region receives
* sufficient seeds it can have its succession cap temporarily pioneered one stage ahead of
* what physical conditions would normally allow, representing natural recolonisation.
*/
private void applySeedDispersal() {
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);
// First pass: collect seed emissions from healthy source regions.
Map<RegionCoordinate, Double> incomingSeeds = new HashMap<>();
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (Region region : active) {
RecoveryRegionData recovery = region.getModuleData()
.get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
if (recovery == null) continue;
if (recovery.getSuccessionStage().ordinal() < SuccessionStage.YOUNG_WOODLAND.ordinal()) continue;
double vegPressure = region.getMetrics().getVegetationPressure();
double emissionChance = vegPressure * ecosystemTuning.getSeedEmissionRate();
if (windRandom.nextDouble() > emissionChance) continue; // stochastic emission
double seedStrength = vegPressure * 0.1;
RegionCoordinate coord = region.getCoordinate();
for (int[] off : offsets) {
RegionCoordinate nCoord = new RegionCoordinate(
coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]);
if (byCoord.containsKey(nCoord)) {
incomingSeeds.merge(nCoord, seedStrength, Double::sum);
}
}
}
if (incomingSeeds.isEmpty()) return;
// Second pass: apply incoming seeds to target regions.
for (var entry : incomingSeeds.entrySet()) {
RegionCoordinate targetCoord = entry.getKey();
Region target = byCoord.get(targetCoord);
if (target == null) continue;
// Pollution in the target region blocks seed germination.
double pollution = target.getMetrics().getPollutionScore();
double pollBlock = Math.min(1.0, pollution * ecosystemTuning.getSeedPollutionBlock());
double effectiveSeeds = entry.getValue() * (1.0 - pollBlock);
if (effectiveSeeds <= 0) continue;
// Corridor boost: count how many neighbours are themselves healthy.
int healthyNeighbours = 0;
for (int[] off : offsets) {
RegionCoordinate nCoord = new RegionCoordinate(
targetCoord.dimensionId(), targetCoord.x() + off[0], targetCoord.z() + off[1]);
Region neighbour = byCoord.get(nCoord);
if (neighbour == null) continue;
RecoveryRegionData nRec = neighbour.getModuleData()
.get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
if (nRec != null && nRec.getSuccessionStage().ordinal()
>= SuccessionStage.YOUNG_WOODLAND.ordinal()) {
healthyNeighbours++;
}
}
if (healthyNeighbours >= 2) {
effectiveSeeds *= ecosystemTuning.getCorridorBoostMultiplier();
}
accumulatedSeedRain.merge(targetCoord, effectiveSeeds, Double::sum);
// Attempt to pioneer the succession cap one stage ahead of what conditions currently
// support, representing natural recolonisation via seed rain.
RecoveryRegionData targetRec = target.getModuleData()
.get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
WaterRegionData water = target.getModuleData()
.get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
SoilRegionData soil = target.getModuleData()
.get(SoilModule.MODULE_ID, SoilRegionData.class).orElse(null);
if (targetRec == null || water == null || soil == null) continue;
SuccessionStage conditionCap = computeDynamicCap(water, soil);
// Seeds pioneer only one stage ahead and only if accumulated rain is significant.
double totalRain = accumulatedSeedRain.getOrDefault(targetCoord, 0.0);
if (conditionCap.hasNext() && totalRain >= 5.0) {
SuccessionStage pioneered = conditionCap.next();
if (pioneered.ordinal() > targetRec.getMaxSuccessionStage().ordinal()) {
targetRec.setMaxSuccessionStage(pioneered);
target.getModuleData().put(RecoveryModule.MODULE_ID, targetRec);
regionManager.markDirty(target);
LivingWorldLogger.info(DiagnosticCategory.SIMULATION,
"Region " + targetCoord + " seed-rain pioneered cap to " + pioneered
+ (healthyNeighbours >= 2 ? " [corridor x" + String.format("%.1f",
ecosystemTuning.getCorridorBoostMultiplier()) + "]" : ""));
}
}
}
}
/** Returns a formatted wind status string for the {@code /lw wind} command. */
public String getWindInfo() {
// Map angle to 8-point compass (N=0°, E=90°, S=180°, W=270°).
String[] compass = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
double normalised = ((windAngle % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
int idx = (int) Math.round(normalised / (Math.PI / 4)) % 8;
return String.format("[LW] Wind: %s (%.0f°) | Spread rate: %.3f | Speed: %dx",
compass[idx], Math.toDegrees(normalised),
ecosystemTuning.getPollutionSpreadRate(), simSpeedMultiplier);
}
/** Returns the succession stage for the region at the given world position, if loaded. */
public Optional<SuccessionStage> getSuccessionStageAt(String dimId, double x, double z) {
if (!serverReady) return Optional.empty();
RegionCoordinate coord = RegionCoordinate.fromBlock(
dimId, (int) x, (int) z, LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
return regionManager.resolve(coord)
.flatMap(r -> r.getModuleData().get(RecoveryModule.MODULE_ID, RecoveryRegionData.class))
.map(RecoveryRegionData::getSuccessionStage);
}
/** Returns the current atmospheric state for a region, if it has been initialised. */
public Optional<AtmosphereRegionData> getRegionalWeather(RegionCoordinate coord) {
if (!serverReady || coord == null) return Optional.empty();
@@ -662,7 +814,8 @@ public final class LivingWorldBootstrap {
this::toggleHud,
this::getAtmosphereStatusFor,
this::getClimateStatusFor,
this::setSimSpeedMultiplier);
this::setSimSpeedMultiplier,
this::getWindInfo);
}
public Path getWorldSaveDirectory() {
@@ -712,6 +865,7 @@ public final class LivingWorldBootstrap {
services = new ServiceRegistry();
services.register(CoreServices.CONFIG, configService);
services.register(CoreServices.TUNING, ecosystemTuning);
services.register(CoreServices.REGIONS, regionManager);
services.register(CoreServices.SIMULATION, simulationManager);
services.register(CoreServices.PERSISTENCE, persistenceService);
@@ -43,7 +43,8 @@ public final class LivingWorldCommandRoot {
uuid -> false,
css -> "Atmosphere not available.",
css -> "Climate not available.",
n -> n);
n -> n,
() -> "Wind not available.");
}
public static void registerDeferred(
@@ -54,7 +55,8 @@ public final class LivingWorldCommandRoot {
Function<UUID, Boolean> hudToggle,
Function<CommandSourceStack, String> atmosphereStatus,
Function<CommandSourceStack, String> climateStatus,
IntUnaryOperator speedSetter) {
IntUnaryOperator speedSetter,
Supplier<String> windStatus) {
if (dispatcher == null) {
throw new IllegalArgumentException("dispatcher must not be null");
}
@@ -92,7 +94,8 @@ public final class LivingWorldCommandRoot {
.executes(context -> ForceUpdateCommand.executeAtSelf(
context.getSource(),
requireService(regionManager, "regionManager"),
requireService(simulationManager, "simulationManager")))))
requireService(simulationManager, "simulationManager"))))
.then(RegionBordersCommand.build()))
.then(Commands.literal("modules")
.then(Commands.literal("list")
.executes(context -> listModules(
@@ -125,6 +128,12 @@ public final class LivingWorldCommandRoot {
context.getSource().sendSuccess(() -> Component.literal(info), false);
return 1;
}))
.then(Commands.literal("wind")
.executes(context -> {
String info = windStatus.get();
context.getSource().sendSuccess(() -> Component.literal(info), false);
return 1;
}))
.then(Commands.literal("speed")
.then(Commands.argument("multiplier", IntegerArgumentType.integer(1, 100))
.executes(context -> {
@@ -0,0 +1,91 @@
package com.livingworld.commands;
import com.livingworld.core.LivingWorldConstants;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.core.particles.DustParticleOptions;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.levelgen.Heightmap;
import org.joml.Vector3f;
/**
* Implements {@code /lw region borders [regionX regionZ]}.
*
* <p>Draws temporary cyan dust-particle lines along all four edges of the
* specified region (or the player's current region if no coordinates are given).
* Each edge is sampled at the world surface so borders follow terrain.</p>
*/
public final class RegionBordersCommand {
private static final int REGION_BLOCKS = LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16;
private static final int EDGE_SAMPLES = 32; // particles per edge (~4 blocks apart on 128-block edge)
// Cyan-teal colour for region borders.
private static final DustParticleOptions BORDER_PARTICLE =
new DustParticleOptions(new Vector3f(0.0f, 0.95f, 0.85f), 1.5f);
private RegionBordersCommand() {}
public static LiteralArgumentBuilder<CommandSourceStack> build() {
return Commands.literal("borders")
.executes(ctx -> execute(ctx.getSource(), null, null))
.then(Commands.argument("regionX", IntegerArgumentType.integer())
.then(Commands.argument("regionZ", IntegerArgumentType.integer())
.executes(ctx -> execute(
ctx.getSource(),
IntegerArgumentType.getInteger(ctx, "regionX"),
IntegerArgumentType.getInteger(ctx, "regionZ")))));
}
private static int execute(CommandSourceStack source, Integer regionX, Integer regionZ) {
ServerPlayer player;
try {
player = source.getPlayerOrException();
} catch (CommandSyntaxException e) {
source.sendFailure(Component.literal("/lw region borders requires a player"));
return 0;
}
int rx = regionX != null ? regionX
: (int) Math.floor(player.getX() / REGION_BLOCKS);
int rz = regionZ != null ? regionZ
: (int) Math.floor(player.getZ() / REGION_BLOCKS);
ServerLevel level = player.serverLevel();
int baseX = rx * REGION_BLOCKS;
int baseZ = rz * REGION_BLOCKS;
for (int i = 0; i <= EDGE_SAMPLES; i++) {
int offset = (int) ((double) i / EDGE_SAMPLES * REGION_BLOCKS);
// North edge: z = baseZ, vary x
spawnEdgeParticles(level, baseX + offset, baseZ);
// South edge: z = baseZ + REGION_BLOCKS, vary x
spawnEdgeParticles(level, baseX + offset, baseZ + REGION_BLOCKS);
// West edge: x = baseX, vary z
spawnEdgeParticles(level, baseX, baseZ + offset);
// East edge: x = baseX + REGION_BLOCKS, vary z
spawnEdgeParticles(level, baseX + REGION_BLOCKS, baseZ + offset);
}
source.sendSuccess(() -> Component.literal(String.format(
"[LW] Region (%d,%d) borders — blocks X[%d..%d] Z[%d..%d]",
rx, rz, baseX, baseX + REGION_BLOCKS - 1, baseZ, baseZ + REGION_BLOCKS - 1)),
false);
return 1;
}
private static void spawnEdgeParticles(ServerLevel level, int x, int z) {
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) + 1;
// Use count=3 so the vertical column is visible even in high terrain.
level.sendParticles(BORDER_PARTICLE,
x + 0.5, y + 0.5, z + 0.5,
3, // count
0.0, 0.8, 0.0, // spread x/y/z
0.0); // speed (0 = stationary)
}
}
@@ -0,0 +1,94 @@
package com.livingworld.config;
/**
* Tuning constants for the Living World ecosystem simulation.
*
* <p>Populated from {@code config/livingworld-server.toml} at server startup via
* {@link com.livingworld.platform.neoforge.NeoForgeModConfig}. Modules access this
* via {@link com.livingworld.core.services.CoreServices#TUNING} from the service
* registry during {@code initialize()}.
*
* <p>Default values match the hardcoded constants they replaced, so a missing
* config file produces identical simulation behaviour to earlier versions.
*/
public final class EcosystemTuning {
// -- Pollution --
private double pollutionDecayRate = 0.002;
private double groundToWaterLeach = 0.005;
private double pollutionSpreadRate = 0.02;
private double windBoost = 0.5;
// -- Vegetation growth (per soil-quality unit above threshold per tick) --
private double grassGrowthRate = 0.06;
private double flowerGrowthRate = 0.03;
private double shrubGrowthRate = 0.02;
private double treeGrowthRate = 0.008;
// -- Vegetation die-off (flat reduction per tick under bad conditions) --
private double grassDieoffRate = 1.00;
private double flowerDieoffRate = 0.50;
private double shrubDieoffRate = 0.25;
private double treeDieoffRate = 0.10;
// -- Ecological succession --
private double baseProgressPerTick = 2.0;
private double damagePerBadTick = 5.0;
private double damageDecayRate = 0.03;
// -- Seed dispersal (new ecological corridors feature) --
/** Fraction of vegetation pressure emitted as seeds to each neighbour per tick. */
private double seedEmissionRate = 0.01;
/** Seed multiplier when the target region has 2+ healthy neighbours (corridor effect). */
private double corridorBoostMultiplier = 3.5;
/** Seeds blocked per unit of pollution score in the target region. */
private double seedPollutionBlock = 0.015;
public EcosystemTuning() {}
// -- Pollution getters/setters --
public double getPollutionDecayRate() { return pollutionDecayRate; }
public double getGroundToWaterLeach() { return groundToWaterLeach; }
public double getPollutionSpreadRate() { return pollutionSpreadRate; }
public double getWindBoost() { return windBoost; }
public void setPollutionDecayRate(double v) { pollutionDecayRate = v; }
public void setGroundToWaterLeach(double v) { groundToWaterLeach = v; }
public void setPollutionSpreadRate(double v) { pollutionSpreadRate = v; }
public void setWindBoost(double v) { windBoost = v; }
// -- Vegetation growth getters/setters --
public double getGrassGrowthRate() { return grassGrowthRate; }
public double getFlowerGrowthRate() { return flowerGrowthRate; }
public double getShrubGrowthRate() { return shrubGrowthRate; }
public double getTreeGrowthRate() { return treeGrowthRate; }
public void setGrassGrowthRate(double v) { grassGrowthRate = v; }
public void setFlowerGrowthRate(double v) { flowerGrowthRate = v; }
public void setShrubGrowthRate(double v) { shrubGrowthRate = v; }
public void setTreeGrowthRate(double v) { treeGrowthRate = v; }
// -- Vegetation die-off getters/setters --
public double getGrassDieoffRate() { return grassDieoffRate; }
public double getFlowerDieoffRate() { return flowerDieoffRate; }
public double getShrubDieoffRate() { return shrubDieoffRate; }
public double getTreeDieoffRate() { return treeDieoffRate; }
public void setGrassDieoffRate(double v) { grassDieoffRate = v; }
public void setFlowerDieoffRate(double v) { flowerDieoffRate = v; }
public void setShrubDieoffRate(double v) { shrubDieoffRate = v; }
public void setTreeDieoffRate(double v) { treeDieoffRate = v; }
// -- Succession getters/setters --
public double getBaseProgressPerTick() { return baseProgressPerTick; }
public double getDamagePerBadTick() { return damagePerBadTick; }
public double getDamageDecayRate() { return damageDecayRate; }
public void setBaseProgressPerTick(double v) { baseProgressPerTick = v; }
public void setDamagePerBadTick(double v) { damagePerBadTick = v; }
public void setDamageDecayRate(double v) { damageDecayRate = v; }
// -- Seed dispersal getters/setters --
public double getSeedEmissionRate() { return seedEmissionRate; }
public double getCorridorBoostMultiplier() { return corridorBoostMultiplier; }
public double getSeedPollutionBlock() { return seedPollutionBlock; }
public void setSeedEmissionRate(double v) { seedEmissionRate = v; }
public void setCorridorBoostMultiplier(double v) { corridorBoostMultiplier = v; }
public void setSeedPollutionBlock(double v) { seedPollutionBlock = v; }
}
@@ -1,5 +1,6 @@
package com.livingworld.core.services;
import com.livingworld.config.EcosystemTuning;
import com.livingworld.core.simulation.RegionManager;
import com.livingworld.core.simulation.SimulationManager;
import com.livingworld.events.LivingWorldEventBus;
@@ -40,6 +41,9 @@ public final class CoreServices {
/** ConfigService key — interface already exists. */
public static final ServiceKey<ConfigService> CONFIG = new ServiceKey<>("config", ConfigService.class);
/** EcosystemTuning key — holds all tunable simulation constants (loaded from TOML config). */
public static final ServiceKey<EcosystemTuning> TUNING = new ServiceKey<>("tuning", EcosystemTuning.class);
public static final ServiceKey<RegionManager> REGIONS =
new ServiceKey<>("regions", RegionManager.class);
@@ -1,5 +1,7 @@
package com.livingworld.modules.pollution;
import com.livingworld.config.EcosystemTuning;
import com.livingworld.core.services.CoreServices;
import com.livingworld.data.serialization.PersistenceReader;
import com.livingworld.data.serialization.PersistenceWriter;
import com.livingworld.events.LivingWorldEvent;
@@ -42,11 +44,11 @@ public final class PollutionModule implements SimulationModule {
public static final String MODULE_ID = "pollution";
private static final double BASE_DECAY_RATE = 0.002;
private static final double GROUND_TO_WATER_LEACH = 0.005;
private static final double WATER_QUALITY_IMPACT = 0.05;
private static final double CHANGE_THRESHOLD = 0.01;
private EcosystemTuning tuning = new EcosystemTuning();
private static final ModuleMetadata METADATA = new ModuleMetadata(
MODULE_ID,
"Pollution",
@@ -68,6 +70,7 @@ public final class PollutionModule implements SimulationModule {
@Override
public void initialize(ModuleContext context) {
if (context == null) throw new IllegalArgumentException("context must not be null");
if (context.hasService(CoreServices.TUNING)) this.tuning = context.getService(CoreServices.TUNING);
}
@Override
@@ -94,10 +97,10 @@ public final class PollutionModule implements SimulationModule {
double prevWaterQuality = region.getMetrics().getWaterQuality();
// Natural decay: air decays fastest, water slowest (modulated by resistance).
data.decay(BASE_DECAY_RATE);
data.decay(tuning.getPollutionDecayRate());
// Ground pollution slowly leaches into water even after decay.
double leach = data.getGroundPollution() * GROUND_TO_WATER_LEACH;
double leach = data.getGroundPollution() * tuning.getGroundToWaterLeach();
data.addPollution(0.0, 0.0, leach);
// Summary metric: weighted average emphasising waterPollution as most damaging.
@@ -1,5 +1,7 @@
package com.livingworld.modules.recovery;
import com.livingworld.config.EcosystemTuning;
import com.livingworld.core.services.CoreServices;
import com.livingworld.data.serialization.PersistenceReader;
import com.livingworld.data.serialization.PersistenceWriter;
import com.livingworld.events.LivingWorldEvent;
@@ -41,17 +43,12 @@ public final class RecoveryModule implements SimulationModule {
public static final String MODULE_ID = "recovery";
/** Base recovery progress per tick (added when conditions are met). */
private static final double BASE_PROGRESS_PER_TICK = 2.0;
/** Extra recovery progress per point of ecosystemHealth above 50. */
private static final double HEALTH_PROGRESS_BONUS = 0.05;
/** Damage accumulated per tick when conditions are badly violated. */
private static final double DAMAGE_PER_BAD_TICK = 5.0;
/** Damage decays this fraction per tick when conditions are OK.
* Kept low so damage sticks around during oscillating conditions. */
private static final double DAMAGE_DECAY_RATE = 0.03;
private static final double CHANGE_THRESHOLD = 0.01;
private EcosystemTuning tuning = new EcosystemTuning();
private static final ModuleMetadata METADATA = new ModuleMetadata(
MODULE_ID,
"Recovery & Succession",
@@ -73,6 +70,7 @@ public final class RecoveryModule implements SimulationModule {
@Override
public void initialize(ModuleContext context) {
if (context == null) throw new IllegalArgumentException("context must not be null");
if (context.hasService(CoreServices.TUNING)) this.tuning = context.getService(CoreServices.TUNING);
}
@Override
@@ -105,24 +103,23 @@ public final class RecoveryModule implements SimulationModule {
if (data.getSuccessionStage().conditionsMetForAdvancement(soil, poll, veg)) {
// Conditions are good: advance recovery progress.
double progressGain = BASE_PROGRESS_PER_TICK;
double progressGain = tuning.getBaseProgressPerTick();
if (metrics.getEcosystemHealth() > 50.0) {
progressGain += (metrics.getEcosystemHealth() - 50.0) * HEALTH_PROGRESS_BONUS;
}
data.advanceProgress(progressGain, soil, poll, veg);
// Damage decays passively when conditions are good (bypass accumulateDamage
// to avoid triggering regression with a zero-damage call that might fire at 70+).
// Use 4-arg constructor to preserve the dynamic succession cap.
// Damage decays passively when conditions are good. Use 4-arg constructor to
// preserve the dynamic succession cap set by applyDynamicCapUpdate().
data = new RecoveryRegionData(
data.getSuccessionStage(),
data.getRecoveryProgress(),
Math.max(0.0, data.getDamageAccumulation() * (1.0 - DAMAGE_DECAY_RATE)),
Math.max(0.0, data.getDamageAccumulation() * (1.0 - tuning.getDamageDecayRate())),
data.getMaxSuccessionStage());
} else if (data.getSuccessionStage().conditionsMissedForRegression(soil, poll, veg)) {
// Conditions are bad: accumulate damage toward regression.
data.accumulateDamage(DAMAGE_PER_BAD_TICK, soil, poll, veg);
data.accumulateDamage(tuning.getDamagePerBadTick(), soil, poll, veg);
}
// Recovery pressure reflects distance from mature forest.
@@ -1,5 +1,7 @@
package com.livingworld.modules.vegetation;
import com.livingworld.config.EcosystemTuning;
import com.livingworld.core.services.CoreServices;
import com.livingworld.data.serialization.PersistenceReader;
import com.livingworld.data.serialization.PersistenceWriter;
import com.livingworld.events.LivingWorldEvent;
@@ -38,36 +40,21 @@ public final class VegetationModule implements SimulationModule {
public static final String MODULE_ID = "vegetation";
// --- growth thresholds ---
// --- growth thresholds (not tunable — structural to the succession model) ---
private static final double GROWTH_SOIL_THRESHOLD = 35.0;
private static final double GROWTH_POLLUTION_LIMIT = 40.0;
private static final double SHRUB_UNLOCK_GRASS = 50.0;
private static final double TREE_UNLOCK_SHRUB = 40.0;
// --- growth rates per tick ---
private static final double GRASS_GROWTH_RATE = 0.06;
private static final double FLOWER_GROWTH_RATE = 0.03;
private static final double SHRUB_GROWTH_RATE = 0.02;
private static final double TREE_GROWTH_RATE = 0.008;
// --- die-off thresholds ---
private static final double DIEOFF_SOIL_THRESHOLD = 20.0;
private static final double DIEOFF_POLLUTION_THRESHOLD = 30.0;
// All living tiers die in bad conditions; trees die slowest.
private static final double GRASS_DIEOFF_RATE = 1.00;
private static final double FLOWER_DIEOFF_RATE = 0.50;
private static final double SHRUB_DIEOFF_RATE = 0.25;
private static final double TREE_DIEOFF_RATE = 0.10;
private static final double DEAD_ACCUMULATION_RATE = 0.50;
// Trees also die-off when conditions are severe (heavy pollution or near-zero soil).
private static final double TREE_SEVERE_POLLUTION = 60.0;
private static final double TREE_SEVERE_SOIL = 10.0;
// --- decomposition ---
private static final double DEAD_DECOMPOSITION_RATE = 0.01;
private static final double CHANGE_THRESHOLD = 0.01;
private EcosystemTuning tuning = new EcosystemTuning();
private static final ModuleMetadata METADATA = new ModuleMetadata(
MODULE_ID,
"Vegetation",
@@ -89,6 +76,7 @@ public final class VegetationModule implements SimulationModule {
@Override
public void initialize(ModuleContext context) {
if (context == null) throw new IllegalArgumentException("context must not be null");
if (context.hasService(CoreServices.TUNING)) this.tuning = context.getService(CoreServices.TUNING);
}
@Override
@@ -123,27 +111,27 @@ public final class VegetationModule implements SimulationModule {
if (goodConditions) {
double soilBonus = metrics.getSoilQuality() - GROWTH_SOIL_THRESHOLD;
data.setGrassPressure(data.getGrassPressure() + soilBonus * GRASS_GROWTH_RATE);
data.setFlowerPressure(data.getFlowerPressure() + soilBonus * FLOWER_GROWTH_RATE);
data.setGrassPressure(data.getGrassPressure() + soilBonus * tuning.getGrassGrowthRate());
data.setFlowerPressure(data.getFlowerPressure() + soilBonus * tuning.getFlowerGrowthRate());
// Shrubs grow only once grass is established.
if (data.getGrassPressure() > SHRUB_UNLOCK_GRASS) {
data.setShrubPressure(data.getShrubPressure() + soilBonus * SHRUB_GROWTH_RATE);
data.setShrubPressure(data.getShrubPressure() + soilBonus * tuning.getShrubGrowthRate());
}
// Trees grow only once shrubs are established.
if (data.getShrubPressure() > TREE_UNLOCK_SHRUB) {
data.setTreePressure(data.getTreePressure() + soilBonus * TREE_GROWTH_RATE);
data.setTreePressure(data.getTreePressure() + soilBonus * tuning.getTreeGrowthRate());
}
}
if (badConditions) {
data.setGrassPressure(data.getGrassPressure() - GRASS_DIEOFF_RATE);
data.setFlowerPressure(data.getFlowerPressure() - FLOWER_DIEOFF_RATE);
data.setShrubPressure(data.getShrubPressure() - SHRUB_DIEOFF_RATE);
data.setGrassPressure(data.getGrassPressure() - tuning.getGrassDieoffRate());
data.setFlowerPressure(data.getFlowerPressure() - tuning.getFlowerDieoffRate());
data.setShrubPressure(data.getShrubPressure() - tuning.getShrubDieoffRate());
// Trees die only under severe stress to reflect their resilience.
if (metrics.getPollutionScore() > TREE_SEVERE_POLLUTION
|| metrics.getSoilQuality() < TREE_SEVERE_SOIL) {
data.setTreePressure(data.getTreePressure() - TREE_DIEOFF_RATE);
data.setTreePressure(data.getTreePressure() - tuning.getTreeDieoffRate());
}
data.setDeadVegetation(data.getDeadVegetation() + DEAD_ACCUMULATION_RATE);
}
@@ -0,0 +1,139 @@
package com.livingworld.platform.neoforge;
import com.livingworld.config.EcosystemTuning;
import net.neoforged.neoforge.common.ModConfigSpec;
/**
* NeoForge server-side TOML configuration for the Living World ecosystem simulation.
*
* <p>Registered via {@link net.neoforged.fml.ModContainer#registerConfig} with type
* {@link net.neoforged.fml.config.ModConfig.Type#SERVER}. The resulting file is written to
* {@code saves/<world>/serverconfig/livingworld-server.toml} and can be edited by
* server admins to tune all ecosystem rates without recompiling.
*
* <p>Call {@link #createTuning()} after the server config has loaded (i.e. from
* {@link net.neoforged.neoforge.event.server.ServerStartingEvent}) to obtain an
* {@link EcosystemTuning} populated with the current config values.
*/
public final class NeoForgeModConfig {
public static final ModConfigSpec SPEC;
// -- pollution --
private static final ModConfigSpec.DoubleValue POLLUTION_DECAY_RATE;
private static final ModConfigSpec.DoubleValue GROUND_TO_WATER_LEACH;
private static final ModConfigSpec.DoubleValue POLLUTION_SPREAD_RATE;
private static final ModConfigSpec.DoubleValue WIND_BOOST;
// -- vegetation growth --
private static final ModConfigSpec.DoubleValue GRASS_GROWTH_RATE;
private static final ModConfigSpec.DoubleValue FLOWER_GROWTH_RATE;
private static final ModConfigSpec.DoubleValue SHRUB_GROWTH_RATE;
private static final ModConfigSpec.DoubleValue TREE_GROWTH_RATE;
// -- vegetation die-off --
private static final ModConfigSpec.DoubleValue GRASS_DIEOFF_RATE;
private static final ModConfigSpec.DoubleValue FLOWER_DIEOFF_RATE;
private static final ModConfigSpec.DoubleValue SHRUB_DIEOFF_RATE;
private static final ModConfigSpec.DoubleValue TREE_DIEOFF_RATE;
// -- recovery succession --
private static final ModConfigSpec.DoubleValue BASE_PROGRESS_PER_TICK;
private static final ModConfigSpec.DoubleValue DAMAGE_PER_BAD_TICK;
private static final ModConfigSpec.DoubleValue DAMAGE_DECAY_RATE;
// -- seed dispersal --
private static final ModConfigSpec.DoubleValue SEED_EMISSION_RATE;
private static final ModConfigSpec.DoubleValue CORRIDOR_BOOST_MULTIPLIER;
private static final ModConfigSpec.DoubleValue SEED_POLLUTION_BLOCK;
static {
ModConfigSpec.Builder b = new ModConfigSpec.Builder();
b.comment("Pollution dynamics").push("pollution");
POLLUTION_DECAY_RATE = b
.comment("Fraction of pollution removed per tick. Lower = stickier pollution. Default: 0.002")
.defineInRange("decay_rate", 0.002, 0.0001, 0.5);
GROUND_TO_WATER_LEACH = b
.comment("Fraction of ground pollution that leaches into water each tick. Default: 0.005")
.defineInRange("ground_to_water_leach", 0.005, 0.0, 0.1);
POLLUTION_SPREAD_RATE = b
.comment("Base rate at which air pollution diffuses to adjacent regions. Default: 0.02")
.defineInRange("spread_rate", 0.02, 0.0, 0.5);
WIND_BOOST = b
.comment("Downwind spread multiplier (0 = no wind bias, 2 = strong directional drift). Default: 0.5")
.defineInRange("wind_boost", 0.5, 0.0, 2.0);
b.pop();
b.comment("Vegetation growth rates (per soil-quality unit above threshold per tick)").push("vegetation");
GRASS_GROWTH_RATE = b.comment("Grass growth rate. Default: 0.06")
.defineInRange("grass_growth", 0.06, 0.001, 2.0);
FLOWER_GROWTH_RATE = b.comment("Flower growth rate. Default: 0.03")
.defineInRange("flower_growth", 0.03, 0.001, 2.0);
SHRUB_GROWTH_RATE = b.comment("Shrub growth rate. Default: 0.02")
.defineInRange("shrub_growth", 0.02, 0.001, 2.0);
TREE_GROWTH_RATE = b.comment("Tree growth rate. Default: 0.008")
.defineInRange("tree_growth", 0.008, 0.001, 2.0);
GRASS_DIEOFF_RATE = b.comment("Grass die-off flat reduction per bad tick. Default: 1.0")
.defineInRange("grass_dieoff", 1.0, 0.0, 20.0);
FLOWER_DIEOFF_RATE = b.comment("Flower die-off per bad tick. Default: 0.5")
.defineInRange("flower_dieoff", 0.5, 0.0, 20.0);
SHRUB_DIEOFF_RATE = b.comment("Shrub die-off per bad tick. Default: 0.25")
.defineInRange("shrub_dieoff", 0.25, 0.0, 20.0);
TREE_DIEOFF_RATE = b.comment("Tree die-off per bad tick (only under severe conditions). Default: 0.1")
.defineInRange("tree_dieoff", 0.1, 0.0, 20.0);
b.pop();
b.comment("Ecological succession and recovery").push("recovery");
BASE_PROGRESS_PER_TICK = b
.comment("Recovery progress added per tick under good conditions. Default: 2.0")
.defineInRange("base_progress_per_tick", 2.0, 0.1, 50.0);
DAMAGE_PER_BAD_TICK = b
.comment("Ecological damage accumulated per tick under bad conditions. Default: 5.0")
.defineInRange("damage_per_bad_tick", 5.0, 0.1, 100.0);
DAMAGE_DECAY_RATE = b
.comment("Fraction of accumulated damage that decays per tick when conditions are OK. Default: 0.03")
.defineInRange("damage_decay_rate", 0.03, 0.001, 0.5);
b.pop();
b.comment("Seed dispersal and ecological corridors").push("seed_dispersal");
SEED_EMISSION_RATE = b
.comment("Fraction of vegetation pressure emitted as seeds to each neighbour per tick (requires YOUNG_WOODLAND+). Default: 0.01")
.defineInRange("emission_rate", 0.01, 0.0, 0.5);
CORRIDOR_BOOST_MULTIPLIER = b
.comment("Seed strength multiplier when the target region has 2+ healthy neighbours — the corridor effect. Default: 3.5")
.defineInRange("corridor_boost", 3.5, 1.0, 20.0);
SEED_POLLUTION_BLOCK = b
.comment("Fraction of incoming seeds blocked per unit of pollution score in the target region. Default: 0.015")
.defineInRange("pollution_block", 0.015, 0.0, 0.1);
b.pop();
SPEC = b.build();
}
private NeoForgeModConfig() {}
/** Creates an {@link EcosystemTuning} populated from the currently loaded config values. */
public static EcosystemTuning createTuning() {
EcosystemTuning t = new EcosystemTuning();
t.setPollutionDecayRate(POLLUTION_DECAY_RATE.get());
t.setGroundToWaterLeach(GROUND_TO_WATER_LEACH.get());
t.setPollutionSpreadRate(POLLUTION_SPREAD_RATE.get());
t.setWindBoost(WIND_BOOST.get());
t.setGrassGrowthRate(GRASS_GROWTH_RATE.get());
t.setFlowerGrowthRate(FLOWER_GROWTH_RATE.get());
t.setShrubGrowthRate(SHRUB_GROWTH_RATE.get());
t.setTreeGrowthRate(TREE_GROWTH_RATE.get());
t.setGrassDieoffRate(GRASS_DIEOFF_RATE.get());
t.setFlowerDieoffRate(FLOWER_DIEOFF_RATE.get());
t.setShrubDieoffRate(SHRUB_DIEOFF_RATE.get());
t.setTreeDieoffRate(TREE_DIEOFF_RATE.get());
t.setBaseProgressPerTick(BASE_PROGRESS_PER_TICK.get());
t.setDamagePerBadTick(DAMAGE_PER_BAD_TICK.get());
t.setDamageDecayRate(DAMAGE_DECAY_RATE.get());
t.setSeedEmissionRate(SEED_EMISSION_RATE.get());
t.setCorridorBoostMultiplier(CORRIDOR_BOOST_MULTIPLIER.get());
t.setSeedPollutionBlock(SEED_POLLUTION_BLOCK.get());
return t;
}
}