generated from MrSphay/codex-agent-repository-kit
Stabilize clustered TNT explosions
All checks were successful
Build / build (push) Successful in 13m47s
All checks were successful
Build / build (push) Successful in 13m47s
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"textures": [
|
||||
"explosionoverhaul:soft_glow",
|
||||
"explosionoverhaul:soft_glow_e"
|
||||
"explosionoverhaul:plasma",
|
||||
"explosionoverhaul:plasma_e"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user