Stabilize clustered TNT explosions
All checks were successful
Build / build (push) Successful in 13m47s

This commit is contained in:
MrSphay
2026-05-09 15:29:59 +02:00
parent 0c57ad000a
commit b9e06a6a19
6 changed files with 152 additions and 48 deletions

View File

@@ -1,9 +1,7 @@
package com.vinlanx.explosionoverhaul;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
@@ -15,16 +13,25 @@ import net.minecraft.world.phys.Vec3;
public class AsyncCraterManager {
private static final Queue<CraterJob> JOBS = new ArrayDeque<>();
private static final int MAX_SCAN_POSITIONS_PER_TICK = 4096;
private static final int MAX_BREAK_EVENTS_PER_TICK = 96;
private static final int MAX_DEBRIS_PER_TICK = 4;
private static final int MAX_DEBRIS_PER_EXPLOSION = 64;
public static void submit(ServerLevel level, Vec3 pos, float power) {
List<BlockPos> blocks = new ArrayList<>(CraterDeformer.getCraterBlocks(level, pos, power));
synchronized (JOBS) {
JOBS.add(new CraterJob(level, pos, power, blocks));
for (CraterJob job : JOBS) {
if (job.overlaps(level, pos, power)) {
return;
}
}
JOBS.add(new CraterJob(level, pos, power));
}
}
public static void onServerTick(MinecraftServer server) {
int budget = Math.max(1, (Integer)Config.COMMON.craterApplyBlocksPerTick.get());
int configuredBudget = Math.max(1, (Integer)Config.COMMON.craterApplyBlocksPerTick.get());
int budget = Math.max(256, Math.min(configuredBudget, MAX_SCAN_POSITIONS_PER_TICK));
synchronized (JOBS) {
Iterator<CraterJob> iterator = JOBS.iterator();
while (iterator.hasNext() && budget > 0) {
@@ -53,37 +60,89 @@ public class AsyncCraterManager {
private final ServerLevel level;
private final Vec3 origin;
private final float power;
private final List<BlockPos> blocks;
private int index;
private final float radius;
private final int minX;
private final int maxX;
private final int minY;
private final int maxY;
private final int minZ;
private final int maxZ;
private final int maxDebris;
private int x;
private int y;
private int z;
private int debrisSpawned;
private CraterJob(ServerLevel level, Vec3 origin, float power, List<BlockPos> blocks) {
private CraterJob(ServerLevel level, Vec3 origin, float power) {
this.level = level;
this.origin = origin;
this.power = power;
this.blocks = blocks;
this.radius = CraterDeformer.calculateRadius(power);
int scanRadius = CraterDeformer.calculateScanRadius(power);
BlockPos center = BlockPos.containing(origin);
this.minX = center.getX() - scanRadius;
this.maxX = center.getX() + scanRadius;
this.minY = Math.max(level.getMinBuildHeight(), center.getY() - scanRadius);
this.maxY = Math.min(level.getMaxBuildHeight() - 1, center.getY() + scanRadius);
this.minZ = center.getZ() - scanRadius;
this.maxZ = center.getZ() + scanRadius;
this.x = this.minX;
this.y = this.minY;
this.z = this.minZ;
this.maxDebris = Math.min(MAX_DEBRIS_PER_EXPLOSION, Math.max(4, Math.round(power * 2.5f)));
}
private int apply(int budget) {
int debrisBudget = Math.max(0, (Integer)Config.COMMON.craterMaxFallingBlocksPerTick.get());
while (this.index < this.blocks.size() && budget > 0) {
BlockPos pos = this.blocks.get(this.index++);
int breakEvents = 0;
int debrisThisTick = 0;
int configuredDebris = Math.max(0, (Integer)Config.COMMON.craterMaxFallingBlocksPerTick.get());
int debrisBudget = Math.min(configuredDebris, MAX_DEBRIS_PER_TICK);
while (!this.done() && budget > 0) {
BlockPos pos = new BlockPos(this.x, this.y, this.z);
this.advance();
--budget;
if (!CraterDeformer.shouldDestroyCraterBlock(this.level, pos, this.origin, this.power, this.radius)) {
continue;
}
BlockState state = this.level.getBlockState(pos);
if (!state.isAir() && state.getDestroySpeed(this.level, pos) >= 0.0f && !ExplosionOverhaul.isBlockStateBlacklisted(state)) {
if (this.debrisSpawned < debrisBudget && this.level.random.nextFloat() < 0.035f) {
if (this.debrisSpawned < this.maxDebris && debrisThisTick < debrisBudget && this.level.random.nextFloat() < 0.006f) {
CraterDeformer.spawnDebris(this.level, this.origin, this.power, 1);
++this.debrisSpawned;
++debrisThisTick;
}
if (breakEvents < MAX_BREAK_EVENTS_PER_TICK && this.level.random.nextFloat() < 0.035f) {
this.level.levelEvent(2001, pos, Block.getId(state));
++breakEvents;
}
this.level.levelEvent(2001, pos, Block.getId(state));
this.level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3);
}
--budget;
}
return budget;
}
private boolean overlaps(ServerLevel level, Vec3 pos, float power) {
if (this.level != level || this.done()) {
return false;
}
double threshold = Math.max(3.0, Math.min(this.radius, CraterDeformer.calculateRadius(power)) * 0.45);
return this.origin.distanceToSqr(pos) <= threshold * threshold;
}
private void advance() {
if (++this.x <= this.maxX) {
return;
}
this.x = this.minX;
if (++this.y <= this.maxY) {
return;
}
this.y = this.minY;
++this.z;
}
private boolean done() {
return this.index >= this.blocks.size();
return this.z > this.maxZ;
}
}
}

View File

@@ -249,8 +249,8 @@ public class Config {
builder.comment("\nAsync crater pipeline (off-thread ray geometry + main-thread batched application)").push("Async Crater");
this.enableAsyncCrater = builder.comment(new String[]{"Enable the asynchronous crater pipeline.", "Compute crater geometry off-thread and apply block changes in small batches per tick to keep TPS smooth on large explosions.", "", "WARNING: When asynchronous and multi-threaded mode is enabled - destruction of VS objects - does not work."}).define("enableAsyncCrater", true);
this.craterMaxThreads = builder.comment(new String[]{"Maximum number of threads for off-thread crater geometry precomputation.", "0 = Auto (use all available: " + availableThreadsForCrater + ")", "1 = Single-threaded", "2-" + maxThreadsForSystemCrater + " = Custom thread count for this system"}).defineInRange("craterMaxThreads", 0, 0, maxThreadsForSystemCrater);
this.craterApplyBlocksPerTick = builder.comment(new String[]{"Maximum number of blocks to evaluate/apply per server tick when building a crater.", "Higher values complete faster but can cause small TPS dips.", "Lower values keep TPS flatter but take longer."}).defineInRange("craterApplyBlocksPerTick", 50000, 0, 150000);
this.craterMaxFallingBlocksPerTick = builder.comment(new String[]{"Limit of falling block entities to spawn per tick during crater application (visual debris).", "Set to 0 to disable spawning via the async pipeline."}).defineInRange("craterMaxFallingBlocksPerTick", 500, 0, 2000);
this.craterApplyBlocksPerTick = builder.comment(new String[]{"Maximum number of crater positions to scan/apply per server tick.", "Higher values complete faster but can cause TPS and render spikes.", "The runtime clamps this to a safe maximum for clustered TNT stability."}).defineInRange("craterApplyBlocksPerTick", 4096, 0, 150000);
this.craterMaxFallingBlocksPerTick = builder.comment(new String[]{"Limit of falling block entities to spawn per tick during crater application (visual debris).", "Set to 0 to disable spawning via the async pipeline.", "The runtime clamps this to a safe maximum for clustered TNT stability."}).defineInRange("craterMaxFallingBlocksPerTick", 4, 0, 2000);
this.enableDirectChunkWrites = builder.comment(new String[]{"Directly write block states into chunk sections during crater application (much faster).", "When enabled, bypasses Level#setBlock and updates heightmaps/light manually.", "Note: This skips Forge block events and neighbor updates."}).define("enableDirectChunkWrites", true);
this.craterChunksPerTick = builder.comment(new String[]{"Maximum number of chunks to process per tick when direct writes are enabled.", "Used along with block budget to smooth TPS."}).defineInRange("craterChunksPerTick", 120, 0, 500);
builder.pop();

View File

@@ -13,50 +13,63 @@ public class CraterDeformer {
public static Set<BlockPos> getCraterBlocks(ServerLevel level, Vec3 explosionPos, float power) {
Set<BlockPos> blocks = new LinkedHashSet<>();
float radius = calculateRadius(power);
float coreRatio = ((Double)Config.COMMON.craterCoreRatio.get()).floatValue();
float coreRadius = radius * Mth.clamp(coreRatio, 0.1f, 0.95f);
float maxResistance = calculateMaxResistance(power);
BlockPos origin = BlockPos.containing(explosionPos);
int r = Mth.ceil(radius + 2.0f);
int r = calculateScanRadius(power);
for (BlockPos cursor : BlockPos.betweenClosed(origin.offset(-r, -r, -r), origin.offset(r, r, r))) {
BlockPos pos = cursor.immutable();
BlockState state = level.getBlockState(pos);
if (state.isAir() || state.getDestroySpeed(level, pos) < 0.0f || ExplosionOverhaul.isBlockStateBlacklisted(state)) {
continue;
}
float resistance = state.getBlock().getExplosionResistance();
if (resistance > maxResistance && pos.distToCenterSqr(explosionPos) > coreRadius * coreRadius) {
continue;
}
double dx = (pos.getX() + 0.5 - explosionPos.x()) / radius;
double dy = (pos.getY() + 0.5 - explosionPos.y()) / (radius * 0.78);
double dz = (pos.getZ() + 0.5 - explosionPos.z()) / radius;
double normalized = Math.sqrt(dx * dx + dy * dy + dz * dz);
double noise = roughness(pos, explosionPos, power);
double shell = 0.82 + noise * 0.22;
double undercut = pos.getY() + 0.5 < explosionPos.y() ? 0.06 : -0.03;
if (normalized <= shell + undercut || Math.sqrt(pos.distToCenterSqr(explosionPos)) <= coreRadius * (0.82 + noise * 0.12)) {
if (shouldDestroyCraterBlock(level, pos, explosionPos, power, radius)) {
blocks.add(pos);
}
}
return blocks;
}
public static boolean shouldDestroyCraterBlock(ServerLevel level, BlockPos pos, Vec3 explosionPos, float power) {
return shouldDestroyCraterBlock(level, pos, explosionPos, power, calculateRadius(power));
}
static boolean shouldDestroyCraterBlock(ServerLevel level, BlockPos pos, Vec3 explosionPos, float power, float radius) {
float coreRatio = ((Double)Config.COMMON.craterCoreRatio.get()).floatValue();
float coreRadius = radius * Mth.clamp(coreRatio, 0.1f, 0.95f);
float maxResistance = calculateMaxResistance(power);
BlockState state = level.getBlockState(pos);
if (state.isAir() || state.getDestroySpeed(level, pos) < 0.0f || ExplosionOverhaul.isBlockStateBlacklisted(state)) {
return false;
}
float resistance = state.getBlock().getExplosionResistance();
double distanceSqr = pos.distToCenterSqr(explosionPos);
if (resistance > maxResistance && distanceSqr > coreRadius * coreRadius) {
return false;
}
double dx = (pos.getX() + 0.5 - explosionPos.x()) / radius;
double dy = (pos.getY() + 0.5 - explosionPos.y()) / (radius * 0.78);
double dz = (pos.getZ() + 0.5 - explosionPos.z()) / radius;
double normalized = Math.sqrt(dx * dx + dy * dy + dz * dz);
double noise = roughness(pos, explosionPos, power);
double shell = 0.82 + noise * 0.22;
double undercut = pos.getY() + 0.5 < explosionPos.y() ? 0.06 : -0.03;
return normalized <= shell + undercut || Math.sqrt(distanceSqr) <= coreRadius * (0.82 + noise * 0.12);
}
public static float calculateRadius(float power) {
float multiplier = ((Double)Config.COMMON.craterSizeMultiplier.get()).floatValue();
return Math.max(2.5f, Math.min(70.0f, power * 2.15f * multiplier));
}
public static int calculateScanRadius(float power) {
return Mth.ceil(calculateRadius(power) + 2.0f);
}
public static float calculateMaxResistance(float power) {
return Math.max(8.0f, power * 6.0f);
}
public static void applyLargeExplosionLogic(ServerLevel level, Vec3 explosionPos, float power) {
spawnDebris(level, explosionPos, power, Math.min(180, Math.round(power * 8.0f)));
spawnDebris(level, explosionPos, power, Math.min(72, Math.round(power * 3.0f)));
}
public static void applySmallExplosionLogic(ServerLevel level, Vec3 explosionPos, float power) {
spawnDebris(level, explosionPos, power, Math.min(48, Math.round(power * 5.0f)));
spawnDebris(level, explosionPos, power, Math.min(24, Math.round(power * 3.0f)));
}
public static void spawnDebris(ServerLevel level, Vec3 explosionPos, float power, int maxDebris) {

View File

@@ -39,17 +39,17 @@ public class ExplosionClusterHandler {
List<ExplosionSource> sources = new ArrayList<>();
sources.add(new ExplosionSource(pos, initialPower));
for (PrimedTnt tnt : tntEntities) {
if (!tnt.position().equals(pos)) {
if (tnt.position().distanceToSqr(pos) > 0.0001) {
sources.add(new ExplosionSource(tnt.position(), 4.0f));
}
}
for (Creeper creeper : creepers) {
if (!creeper.position().equals(pos)) {
if (creeper.position().distanceToSqr(pos) > 0.0001) {
sources.add(new ExplosionSource(creeper.position(), 3.0f));
}
}
for (AbstractMinecart cart : minecarts) {
if (cart.getType() == EntityType.TNT_MINECART && !cart.position().equals(pos)) {
if (cart.getType() == EntityType.TNT_MINECART && cart.position().distanceToSqr(pos) > 0.0001) {
sources.add(new ExplosionSource(cart.position(), 4.0f));
}
}
@@ -67,17 +67,17 @@ public class ExplosionClusterHandler {
}
}
for (PrimedTnt tnt : tntEntities) {
if (!tnt.position().equals(pos)) {
if (tnt.position().distanceToSqr(pos) > 0.0001) {
tnt.discard();
}
}
for (Creeper creeper : creepers) {
if (!creeper.position().equals(pos)) {
if (creeper.position().distanceToSqr(pos) > 0.0001) {
creeper.discard();
}
}
for (AbstractMinecart cart : minecarts) {
if (cart.getType() == EntityType.TNT_MINECART && !cart.position().equals(pos)) {
if (cart.getType() == EntityType.TNT_MINECART && cart.position().distanceToSqr(pos) > 0.0001) {
cart.discard();
}
}

View File

@@ -2,7 +2,9 @@ package com.vinlanx.explosionoverhaul;
import com.vinlanx.explosionoverhaul.api.IExplosionPower;
import com.vinlanx.explosionoverhaul.mixinhelper.ExplosionAccessor;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import net.minecraft.core.BlockPos;
@@ -20,6 +22,8 @@ import net.minecraft.world.phys.shapes.CollisionContext;
public class ServerExplosionHandler {
private static final double SOUND_SPEED_BLOCKS_PER_TICK = 343.0 / 20.0;
private static final int DUPLICATE_WINDOW_TICKS = 6;
private static final List<RecentExplosion> RECENT_EXPLOSIONS = new ArrayList<>();
public static void handleExplosion(ServerLevel level, Explosion explosion, List<BlockPos> affectedBlocks) {
float power = 4.0f;
@@ -34,6 +38,10 @@ public class ServerExplosionHandler {
}
power = ExplosionClusterHandler.calculateClusteredPower(level, center, power);
if (shouldSkipDuplicate(level, center, power)) {
affectedBlocks.clear();
return;
}
ExplosionOverhaul.LOGGER.info("Explosion Overhaul handling explosion at {} with power {}", center, power);
dispatchPlayerEffects(level, center, power);
@@ -240,9 +248,33 @@ public class ServerExplosionHandler {
return nearest;
}
private static boolean shouldSkipDuplicate(ServerLevel level, Vec3 center, float power) {
long now = level.getGameTime();
synchronized (RECENT_EXPLOSIONS) {
Iterator<RecentExplosion> iterator = RECENT_EXPLOSIONS.iterator();
while (iterator.hasNext()) {
RecentExplosion recent = iterator.next();
if (recent.level != level || now - recent.gameTime > DUPLICATE_WINDOW_TICKS) {
iterator.remove();
}
}
double duplicateRadius = Math.max(3.0, Math.min(10.0, CraterDeformer.calculateRadius(power) * 0.25));
for (RecentExplosion recent : RECENT_EXPLOSIONS) {
if (recent.level == level && recent.center.distanceToSqr(center) <= duplicateRadius * duplicateRadius) {
return true;
}
}
RECENT_EXPLOSIONS.add(new RecentExplosion(level, center, power, now));
return false;
}
}
public static void register() {
}
public record CameraShakeProfile(float intensity, int durationTicks, float pushIntensity) {
}
private record RecentExplosion(ServerLevel level, Vec3 center, float power, long gameTime) {
}
}

View File

@@ -1,6 +1,6 @@
{
"textures": [
"explosionoverhaul:soft_glow",
"explosionoverhaul:soft_glow_e"
"explosionoverhaul:plasma",
"explosionoverhaul:plasma_e"
]
}