diff --git a/src/main/java/com/vinlanx/explosionoverhaul/AsyncCraterManager.java b/src/main/java/com/vinlanx/explosionoverhaul/AsyncCraterManager.java index 1a1474f..12679e9 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/AsyncCraterManager.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/AsyncCraterManager.java @@ -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 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 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 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 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 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; } } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/Config.java b/src/main/java/com/vinlanx/explosionoverhaul/Config.java index ce1007c..7d4a009 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/Config.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/Config.java @@ -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(); diff --git a/src/main/java/com/vinlanx/explosionoverhaul/CraterDeformer.java b/src/main/java/com/vinlanx/explosionoverhaul/CraterDeformer.java index 071907b..b0ca199 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/CraterDeformer.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/CraterDeformer.java @@ -13,50 +13,63 @@ public class CraterDeformer { public static Set getCraterBlocks(ServerLevel level, Vec3 explosionPos, float power) { Set 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) { diff --git a/src/main/java/com/vinlanx/explosionoverhaul/ExplosionClusterHandler.java b/src/main/java/com/vinlanx/explosionoverhaul/ExplosionClusterHandler.java index b43db33..f79e189 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/ExplosionClusterHandler.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/ExplosionClusterHandler.java @@ -39,17 +39,17 @@ public class ExplosionClusterHandler { List 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(); } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/ServerExplosionHandler.java b/src/main/java/com/vinlanx/explosionoverhaul/ServerExplosionHandler.java index e3f03f0..2af9f47 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/ServerExplosionHandler.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/ServerExplosionHandler.java @@ -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 RECENT_EXPLOSIONS = new ArrayList<>(); public static void handleExplosion(ServerLevel level, Explosion explosion, List 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 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) { + } } diff --git a/src/main/resources/assets/explosionoverhaul/particles/custom_glow.json b/src/main/resources/assets/explosionoverhaul/particles/custom_glow.json index abe91f7..4d367e3 100644 --- a/src/main/resources/assets/explosionoverhaul/particles/custom_glow.json +++ b/src/main/resources/assets/explosionoverhaul/particles/custom_glow.json @@ -1,6 +1,6 @@ { "textures": [ - "explosionoverhaul:soft_glow", - "explosionoverhaul:soft_glow_e" + "explosionoverhaul:plasma", + "explosionoverhaul:plasma_e" ] }