From bf2a4b776fbf0dda1e95a17b0b464bb789761626 Mon Sep 17 00:00:00 2001 From: mlbv <51232730+mlbv@users.noreply.github.com> Date: Fri, 23 May 2025 01:01:34 +0800 Subject: [PATCH 1/5] feat: parallelized explosion calculation and is configurable --- src/main/java/com/hbm/config/BombConfig.java | 6 +- .../entity/logic/EntityNukeExplosionMK5.java | 90 ++-- .../explosion/ExplosionNukeRayBatched.java | 78 ++- .../ExplosionNukeRayParallelized.java | 497 ++++++++++++++++++ .../com/hbm/interfaces/IExplosionRay.java | 9 + 5 files changed, 608 insertions(+), 72 deletions(-) create mode 100644 src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java create mode 100644 src/main/java/com/hbm/interfaces/IExplosionRay.java diff --git a/src/main/java/com/hbm/config/BombConfig.java b/src/main/java/com/hbm/config/BombConfig.java index b49253e35..927ebd3ba 100644 --- a/src/main/java/com/hbm/config/BombConfig.java +++ b/src/main/java/com/hbm/config/BombConfig.java @@ -26,7 +26,8 @@ public class BombConfig { public static int fDelay = 4; public static int limitExplosionLifespan = 0; public static boolean chunkloading = true; - + public static boolean parallelization = true; + public static void loadFromConfig(Configuration config) { final String CATEGORY_NUKES = CommonConfig.CATEGORY_NUKES; @@ -92,7 +93,8 @@ public class BombConfig { Property falloutDelayProp = config.get(CATEGORY_NUKE, "6.04_falloutDelay", 4); falloutDelayProp.comment = "How many ticks to wait for the next fallout chunk computation"; fDelay = falloutDelayProp.getInt(); - + chunkloading = CommonConfig.createConfigBool(config, CATEGORY_NUKE, "6.XX_enableChunkLoading", "Allows all types of procedural explosions to keep the central chunk loaded.", true); + parallelization = CommonConfig.createConfigBool(config, CATEGORY_NUKE, "6.XX_enableParallelization", "Allows explosions to use multiple threads.", true); } } diff --git a/src/main/java/com/hbm/entity/logic/EntityNukeExplosionMK5.java b/src/main/java/com/hbm/entity/logic/EntityNukeExplosionMK5.java index 3dc6451a8..def573ad7 100644 --- a/src/main/java/com/hbm/entity/logic/EntityNukeExplosionMK5.java +++ b/src/main/java/com/hbm/entity/logic/EntityNukeExplosionMK5.java @@ -2,6 +2,7 @@ package com.hbm.entity.logic; import java.util.List; +import com.hbm.interfaces.IExplosionRay; import org.apache.logging.log4j.Level; import com.hbm.config.BombConfig; @@ -9,6 +10,7 @@ import com.hbm.config.GeneralConfig; import com.hbm.entity.effect.EntityFalloutRain; import com.hbm.explosion.ExplosionNukeGeneric; import com.hbm.explosion.ExplosionNukeRayBatched; +import com.hbm.explosion.ExplosionNukeRayParallelized; import com.hbm.main.MainRegistry; import com.hbm.util.ContaminationUtil; import com.hbm.util.ContaminationUtil.ContaminationType; @@ -22,32 +24,32 @@ import net.minecraft.util.Vec3; import net.minecraft.world.World; public class EntityNukeExplosionMK5 extends EntityExplosionChunkloading { - + //Strength of the blast public int strength; //How many rays are calculated per tick public int speed; public int length; - + private long explosionStart; public boolean fallout = true; private int falloutAdd = 0; - - ExplosionNukeRayBatched explosion; + + private IExplosionRay explosion; public EntityNukeExplosionMK5(World p_i1582_1_) { super(p_i1582_1_); } - + public EntityNukeExplosionMK5(World world, int strength, int speed, int length) { super(world); this.strength = strength; this.speed = speed; this.length = length; } - + @Override public void onUpdate() { - + if(strength == 0) { this.clearChunkLoader(); this.setDead(); @@ -55,30 +57,34 @@ public class EntityNukeExplosionMK5 extends EntityExplosionChunkloading { } if(!worldObj.isRemote) loadChunk((int) Math.floor(posX / 16D), (int) Math.floor(posZ / 16D)); - + for(Object player : this.worldObj.playerEntities) { ((EntityPlayer)player).triggerAchievement(MainRegistry.achManhattan); } - + if(!worldObj.isRemote && fallout && explosion != null && this.ticksExisted < 10 && strength >= 75) { radiate(2_500_000F / (this.ticksExisted * 5 + 1), this.length * 2); } - - ExplosionNukeGeneric.dealDamage(this.worldObj, this.posX, this.posY, this.posZ, this.length * 2); - - if(explosion == null) { - explosion = new ExplosionNukeRayBatched(worldObj, (int)this.posX, (int)this.posY, (int)this.posZ, this.strength, this.speed, this.length); - } - - if(!explosion.isAusf3Complete) { - explosion.collectTip(speed * 10); - } else if(explosion.perChunk.size() > 0) { - long start = System.currentTimeMillis(); - - while(explosion.perChunk.size() > 0 && System.currentTimeMillis() < start + BombConfig.mk5) explosion.processChunk(); - - } else if(fallout) { + ExplosionNukeGeneric.dealDamage(this.worldObj, this.posX, this.posY, this.posZ, this.length * 2); + + if(explosion == null) { + explosionStart = System.currentTimeMillis(); + if (BombConfig.parallelization) { + explosion = new ExplosionNukeRayParallelized(worldObj, posX, posY, posZ, + strength, speed, length); + } else { + explosion = new ExplosionNukeRayBatched(worldObj, (int) posX, (int) posY, (int) posZ, + strength, speed, length); + } + } + + if(!explosion.isComplete()) { + explosion.cacheChunksTick(BombConfig.mk5); + explosion.destructionTick(BombConfig.mk5); + } else if(fallout) { + if(GeneralConfig.enableExtendedLogging && explosionStart != 0) + MainRegistry.logger.log(Level.INFO, "[NUKE] Explosion complete. Time elapsed: " + (System.currentTimeMillis() - explosionStart) + "ms"); EntityFalloutRain fallout = new EntityFalloutRain(this.worldObj); fallout.posX = this.posX; fallout.posY = this.posY; @@ -94,35 +100,35 @@ public class EntityNukeExplosionMK5 extends EntityExplosionChunkloading { this.setDead(); } } - + private void radiate(float rads, double range) { - + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, AxisAlignedBB.getBoundingBox(posX, posY, posZ, posX, posY, posZ).expand(range, range, range)); - + for(EntityLivingBase e : entities) { - + Vec3 vec = Vec3.createVectorHelper(e.posX - posX, (e.posY + e.getEyeHeight()) - posY, e.posZ - posZ); double len = vec.lengthVector(); vec = vec.normalize(); - + float res = 0; - + for(int i = 1; i < len; i++) { int ix = (int)Math.floor(posX + vec.xCoord * i); int iy = (int)Math.floor(posY + vec.yCoord * i); int iz = (int)Math.floor(posZ + vec.zCoord * i); - + res += worldObj.getBlock(ix, iy, iz).getExplosionResistance(null); } - + if(res < 1) res = 1; - + float eRads = rads; eRads /= (float)res; eRads /= (float)(len * len); - + ContaminationUtil.contaminate(e, HazardType.RADIATION, ContaminationType.RAD_BYPASS, eRads); } } @@ -136,17 +142,17 @@ public class EntityNukeExplosionMK5 extends EntityExplosionChunkloading { protected void writeEntityToNBT(NBTTagCompound nbt) { nbt.setInteger("ticksExisted", this.ticksExisted); } - + public static EntityNukeExplosionMK5 statFac(World world, int r, double x, double y, double z) { - + if(GeneralConfig.enableExtendedLogging && !world.isRemote) MainRegistry.logger.log(Level.INFO, "[NUKE] Initialized explosion at " + x + " / " + y + " / " + z + " with strength " + r + "!"); - + if(r == 0) r = 25; - + r *= 2; - + EntityNukeExplosionMK5 mk5 = new EntityNukeExplosionMK5(world); mk5.strength = (int)(r); mk5.speed = (int)Math.ceil(100000 / mk5.strength); @@ -154,14 +160,14 @@ public class EntityNukeExplosionMK5 extends EntityExplosionChunkloading { mk5.length = mk5.strength / 2; return mk5; } - + public static EntityNukeExplosionMK5 statFacNoRad(World world, int r, double x, double y, double z) { - + EntityNukeExplosionMK5 mk5 = statFac(world, r, x, y ,z); mk5.fallout = false; return mk5; } - + public EntityNukeExplosionMK5 moreFallout(int fallout) { falloutAdd = fallout; return this; diff --git a/src/main/java/com/hbm/explosion/ExplosionNukeRayBatched.java b/src/main/java/com/hbm/explosion/ExplosionNukeRayBatched.java index de45a8741..6f7af5e35 100644 --- a/src/main/java/com/hbm/explosion/ExplosionNukeRayBatched.java +++ b/src/main/java/com/hbm/explosion/ExplosionNukeRayBatched.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; +import com.hbm.interfaces.IExplosionRay; import com.hbm.util.fauxpointtwelve.BlockPos; import net.minecraft.block.Block; @@ -14,7 +15,7 @@ import net.minecraft.util.Vec3; import net.minecraft.world.ChunkCoordIntPair; import net.minecraft.world.World; -public class ExplosionNukeRayBatched { +public class ExplosionNukeRayBatched implements IExplosionRay { public HashMap> perChunk = new HashMap(); //for future: optimize blockmap further by using sub-chunks instead of chunks public List orderedChunks = new ArrayList(); @@ -26,7 +27,7 @@ public class ExplosionNukeRayBatched { int strength; int length; - + int speed; int gspNumMax; int gspNum; double gspX; @@ -40,8 +41,8 @@ public class ExplosionNukeRayBatched { this.posY = y; this.posZ = z; this.strength = strength; + this.speed = speed; this.length = length; - // Total number of points this.gspNumMax = (int)(2.5 * Math.PI * Math.pow(this.strength,2)); this.gspNum = 1; @@ -76,7 +77,7 @@ public class ExplosionNukeRayBatched { } public void collectTip(int count) { - + //count = Math.min(count, 10); int amountProcessed = 0; @@ -106,7 +107,7 @@ public class ExplosionNukeRayBatched { double fac = 100 - ((double) i) / ((double) length) * 100; fac *= 0.07D; - + Block block = world.getBlock(iX, iY, iZ); if(!block.getMaterial().isLiquid()) @@ -125,18 +126,18 @@ public class ExplosionNukeRayBatched { break; } } - + for(ChunkCoordIntPair pos : chunkCoords) { List triplets = perChunk.get(pos); - + if(triplets == null) { triplets = new ArrayList(); perChunk.put(pos, triplets); //we re-use the same pos instead of using individualized per-chunk ones to save on RAM } - + triplets.add(lastPos); } - + // Raise one generalized spiral points this.generateGspUp(); @@ -145,20 +146,20 @@ public class ExplosionNukeRayBatched { return; } } - + orderedChunks.addAll(perChunk.keySet()); orderedChunks.sort(comparator); - + isAusf3Complete = true; } - + public static float masqueradeResistance(Block block) { if(block == Blocks.sandstone) return Blocks.stone.getExplosionResistance(null); if(block == Blocks.obsidian) return Blocks.stone.getExplosionResistance(null) * 3; return block.getExplosionResistance(null); } - + /** little comparator for roughly sorting chunks by distance to the center */ public class CoordComparator implements Comparator { @@ -170,15 +171,15 @@ public class ExplosionNukeRayBatched { int diff1 = Math.abs((chunkX - o1.chunkXPos)) + Math.abs((chunkZ - o1.chunkZPos)); int diff2 = Math.abs((chunkX - o2.chunkXPos)) + Math.abs((chunkZ - o2.chunkZPos)); - + return diff1 - diff2; } } public void processChunk() { - + if(this.perChunk.isEmpty()) return; - + ChunkCoordIntPair coord = orderedChunks.get(0); List list = perChunk.get(coord); HashSet toRem = new HashSet(); @@ -186,13 +187,13 @@ public class ExplosionNukeRayBatched { //List toRem = new ArrayList(); int chunkX = coord.chunkXPos; int chunkZ = coord.chunkZPos; - + int enter = (int) (Math.min( Math.abs(posX - (chunkX << 4)), Math.abs(posZ - (chunkZ << 4)))) - 16; //jump ahead to cut back on NOPs - + enter = Math.max(enter, 0); - + for(FloatTriplet triplet : list) { float x = triplet.xCoord; float y = triplet.yCoord; @@ -205,13 +206,13 @@ public class ExplosionNukeRayBatched { int tipX = (int) Math.floor(x); int tipY = (int) Math.floor(y); int tipZ = (int) Math.floor(z); - + boolean inChunk = false; for(int i = enter; i < vec.lengthVector(); i++) { int x0 = (int) Math.floor(posX + pX * i); int y0 = (int) Math.floor(posY + pY * i); int z0 = (int) Math.floor(posZ + pZ * i); - + if(x0 >> 4 != chunkX || z0 >> 4 != chunkZ) { if(inChunk) { break; @@ -219,13 +220,13 @@ public class ExplosionNukeRayBatched { continue; } } - + inChunk = true; if(!world.isAirBlock(x0, y0, z0)) { - + BlockPos pos = new BlockPos(x0, y0, z0); - + if(x0 == tipX && y0 == tipY && z0 == tipZ) { toRemTips.add(pos); } @@ -241,20 +242,41 @@ public class ExplosionNukeRayBatched { world.setBlock(pos.getX(), pos.getY(), pos.getZ(), Blocks.air, 0, 2); } } - + perChunk.remove(coord); orderedChunks.remove(0); } - + protected void handleTip(int x, int y, int z) { world.setBlock(x, y, z, Blocks.air, 0, 3); } - + + @Override + public boolean isComplete() { + return isAusf3Complete && perChunk.isEmpty(); + } + + @Override + public void cacheChunksTick(int time) { + if (!isAusf3Complete) { + // time ignored here since collectTip() did not implement a time limit + collectTip(speed*10); + } + } + + @Override + public void destructionTick(int time) { + if (!isAusf3Complete) return; + long start = System.currentTimeMillis(); + while(!perChunk.isEmpty() && System.currentTimeMillis() < start + time) + processChunk(); + } + public class FloatTriplet { public float xCoord; public float yCoord; public float zCoord; - + public FloatTriplet(float x, float y, float z) { xCoord = x; yCoord = y; diff --git a/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java b/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java new file mode 100644 index 000000000..bc0cf57a8 --- /dev/null +++ b/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java @@ -0,0 +1,497 @@ +package com.hbm.explosion; + +import com.hbm.interfaces.IExplosionRay; +import com.hbm.main.MainRegistry; +import net.minecraft.block.Block; +import net.minecraft.init.Blocks; +import net.minecraft.util.Vec3; +import net.minecraft.world.ChunkCoordIntPair; +import net.minecraft.world.EnumSkyBlock; +import net.minecraft.world.World; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.storage.ExtendedBlockStorage; +import org.apache.logging.log4j.Level; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLongArray; + +public class ExplosionNukeRayParallelized implements IExplosionRay { + + private static final int WORLD_HEIGHT = 256; + private static final int BITSET_SIZE = 16 * WORLD_HEIGHT * 16; + private static final int WORDS_PER_SET = BITSET_SIZE >>> 6; // (16*256*16)/64 + + protected final World world; + private final double explosionX, explosionY, explosionZ; + private final int originX, originY, originZ; + private final int strength; + private final int radius; + + private final List directions; + private final ConcurrentMap destructionMap; + private final ConcurrentMap snapshots; + + private final BlockingQueue rayQueue; + private final BlockingQueue cacheQueue; + private final ExecutorService pool; + private final CountDownLatch latch; + private final Thread latchWatcherThread; + private final List orderedChunks; + private volatile boolean collectFinished = false; + private volatile boolean destroyFinished = false; + + + public ExplosionNukeRayParallelized(World world, double x, double y, double z, int strength, int speed, int radius) { + this.world = world; + this.explosionX = x; + this.explosionY = y; + this.explosionZ = z; + + this.originX = (int) Math.floor(x); + this.originY = (int) Math.floor(y); + this.originZ = (int) Math.floor(z); + + this.strength = strength; + this.radius = radius; + + int rayCount = Math.max(0, (int) (2.5 * Math.PI * strength * strength)); + + this.latch = new CountDownLatch(rayCount); + this.destructionMap = new ConcurrentHashMap<>(); + this.snapshots = new ConcurrentHashMap<>(); + this.orderedChunks = new ArrayList<>(); + + this.rayQueue = new LinkedBlockingQueue<>(); + this.cacheQueue = new LinkedBlockingQueue<>(); + + int workers = Math.max(1, Runtime.getRuntime().availableProcessors() - 1); + this.pool = Executors.newWorkStealingPool(workers); + this.directions = generateSphereRays(rayCount); + + for (int i = 0; i < rayCount; i++) rayQueue.add(new RayTask(i)); + for (int i = 0; i < workers; i++) pool.submit(new Worker()); + + this.latchWatcherThread = new Thread(() -> { + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + collectFinished = true; + } + }, "ExplosionNuke-LatchWatcher-" + System.nanoTime()); + this.latchWatcherThread.setDaemon(true); + this.latchWatcherThread.start(); + } + + private static float getNukeResistance(Block b) { + // if (b.getMaterial().isLiquid()) return 0.1F; + if (b == Blocks.sandstone) return Blocks.stone.getExplosionResistance(null); + if (b == Blocks.obsidian) return Blocks.stone.getExplosionResistance(null) * 3; + return b.getExplosionResistance(null); + } + + @Override + public void cacheChunksTick(int timeBudgetMs) { + if (collectFinished || this.cacheQueue == null) return; + + final long deadline = System.nanoTime() + (timeBudgetMs * 1_000_000L); + while (System.nanoTime() < deadline) { + ChunkKey ck = cacheQueue.poll(); + if (ck == null) break; + snapshots.computeIfAbsent(ck, key -> { + SubChunkSnapshot snap = createSubChunk(key.pos, key.subY); + return snap == null ? SubChunkSnapshot.EMPTY : snap; + }); + } + } + + @Override + public void destructionTick(int timeBudgetMs) { + if (!collectFinished || destroyFinished) return; + final long deadline = System.nanoTime() + timeBudgetMs * 1_000_000L; + + if (orderedChunks.isEmpty() && !destructionMap.isEmpty()) { + orderedChunks.addAll(destructionMap.keySet()); + orderedChunks.sort(Comparator.comparingInt(c -> Math.abs((originX >> 4) - c.chunkXPos) + Math.abs((originZ >> 4) - c.chunkZPos))); + } + + Iterator it = orderedChunks.iterator(); + while (it.hasNext() && System.nanoTime() < deadline) { + ChunkCoordIntPair cp = it.next(); + ConcurrentBitSet bs = destructionMap.get(cp); + if (bs == null) { + it.remove(); + continue; + } + + Chunk chunk = world.getChunkFromChunkCoords(cp.chunkXPos, cp.chunkZPos); + ExtendedBlockStorage[] storages = chunk.getBlockStorageArray(); + boolean ChunkModified = false; + + for (int subY = 0; subY < storages.length; subY++) { + ExtendedBlockStorage storage = storages[subY]; + if (storage == null) continue; + + int yPrimeMin = 255 - ((subY << 4) + 15); + int startBit = yPrimeMin << 8; + int yPrimeMax = 255 - (subY << 4); + int endBit = (yPrimeMax << 8) | 0xFF; + + int bit = bs.nextSetBit(startBit); + if (bit < 0 || bit > endBit) continue; + + while (bit >= 0 && bit <= endBit && System.nanoTime() < deadline) { + int yGlobal = 255 - (bit >>> 8); + int xGlobal = (cp.chunkXPos << 4) | ((bit >>> 4) & 0xF); + int zGlobal = (cp.chunkZPos << 4) | (bit & 0xF); + + if (world.getTileEntity(xGlobal, yGlobal, zGlobal) != null) { + chunk.removeTileEntity(xGlobal & 0xF, yGlobal, zGlobal & 0xF); // world Y + world.removeTileEntity(xGlobal, yGlobal, zGlobal); + } + + int xLocal = xGlobal & 0xF; + int yLocal = yGlobal & 0xF; + int zLocal = zGlobal & 0xF; + storage.func_150818_a(xLocal, yLocal, zLocal, Blocks.air); + storage.setExtBlockMetadata(xLocal, yLocal, zLocal, 0); + ChunkModified = true; + + world.notifyBlocksOfNeighborChange(xGlobal, yGlobal, zGlobal, Blocks.air); + world.markBlockForUpdate(xGlobal, yGlobal, zGlobal); + + world.updateLightByType(EnumSkyBlock.Sky, xGlobal, yGlobal, zGlobal); + world.updateLightByType(EnumSkyBlock.Block, xGlobal, yGlobal, zGlobal); + + bs.clear(bit); + bit = bs.nextSetBit(bit + 1); + } + } + + if (ChunkModified) { + chunk.setChunkModified(); + world.markBlockRangeForRenderUpdate(cp.chunkXPos << 4, 0, cp.chunkZPos << 4, (cp.chunkXPos << 4) | 15, WORLD_HEIGHT - 1, (cp.chunkZPos << 4) | 15); + } + + if (bs.isEmpty()) { + destructionMap.remove(cp); + for (int sy = 0; sy < (WORLD_HEIGHT >> 4); sy++) { + snapshots.remove(new ChunkKey(cp.chunkXPos, cp.chunkZPos, sy)); + } + it.remove(); + } + } + + if (orderedChunks.isEmpty() && destructionMap.isEmpty()) { + destroyFinished = true; + if (pool != null) pool.shutdown(); + } + } + + @Override + public boolean isComplete() { + return collectFinished && destroyFinished; + } + + public void cancel() { + this.collectFinished = true; + this.destroyFinished = true; + + if (this.rayQueue != null) this.rayQueue.clear(); + if (this.cacheQueue != null) this.cacheQueue.clear(); + + if (this.latch != null) { + while (this.latch.getCount() > 0) { + this.latch.countDown(); + } + } + if (this.latchWatcherThread != null && this.latchWatcherThread.isAlive()) { + this.latchWatcherThread.interrupt(); + } + + if (this.pool != null && !this.pool.isShutdown()) { + this.pool.shutdownNow(); + try { + if (!this.pool.awaitTermination(100, TimeUnit.MILLISECONDS)) { + MainRegistry.logger.log(Level.ERROR, "ExplosionNukeRayParallelized thread pool did not terminate promptly on cancel."); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + if (!this.pool.isShutdown()) { + this.pool.shutdownNow(); + } + } + } + if (this.destructionMap != null) this.destructionMap.clear(); + if (this.snapshots != null) this.snapshots.clear(); + if (this.orderedChunks != null) this.orderedChunks.clear(); + } + + private SubChunkSnapshot createSubChunk(ChunkCoordIntPair cpos, int subY) { + if (!world.getChunkProvider().chunkExists(cpos.chunkXPos, cpos.chunkZPos)) { + return SubChunkSnapshot.EMPTY; + } + Chunk chunk = world.getChunkFromChunkCoords(cpos.chunkXPos, cpos.chunkZPos); + ExtendedBlockStorage ebs = chunk.getBlockStorageArray()[subY]; + if (ebs == null || ebs.isEmpty()) { + return SubChunkSnapshot.EMPTY; + } + + short[] data = new short[16 * 16 * 16]; + List palette = new ArrayList<>(); + palette.add(Blocks.air); + Map idxMap = new HashMap<>(); + idxMap.put(Blocks.air, (short) 0); + boolean allAir = true; + + for (int ly = 0; ly < 16; ly++) { + for (int lz = 0; lz < 16; lz++) { + for (int lx = 0; lx < 16; lx++) { + Block block = ebs.getBlockByExtId(lx, ly, lz); + int idx; + if (block == Blocks.air) { + idx = 0; + } else { + allAir = false; + Short e = idxMap.get(block); + if (e == null) { + idxMap.put(block, (short) palette.size()); + palette.add(block); + idx = palette.size() - 1; + } else { + idx = e; + } + } + data[(ly << 8) | (lz << 4) | lx] = (short) idx; + } + } + } + if (allAir) return SubChunkSnapshot.EMPTY; + return new SubChunkSnapshot(palette.toArray(new Block[0]), data); + } + + private List generateSphereRays(int count) { + List list = new ArrayList<>(count); + if (count <= 0) return list; + if (count == 1) { + list.add(Vec3.createVectorHelper(1, 0, 0).normalize()); + return list; + } + double phi = Math.PI * (3.0 - Math.sqrt(5.0)); + for (int i = 0; i < count; i++) { + double y = 1.0 - (i / (double) (count - 1)) * 2.0; + double r = Math.sqrt(1.0 - y * y); + double t = phi * i; + list.add(Vec3.createVectorHelper(Math.cos(t) * r, y, Math.sin(t) * r)); + } + return list; + } + + private static class ChunkKey { + final ChunkCoordIntPair pos; + final int subY; + + ChunkKey(int cx, int cz, int sy) { + this.pos = new ChunkCoordIntPair(cx, cz); + this.subY = sy; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ChunkKey)) return false; + ChunkKey k = (ChunkKey) o; + return subY == k.subY && pos.equals(k.pos); + } + + @Override + public int hashCode() { + return Objects.hash(pos.chunkXPos, pos.chunkZPos, subY); + } + } + + private static class SubChunkSnapshot { + private static final SubChunkSnapshot EMPTY = new SubChunkSnapshot(new Block[]{Blocks.air}, null); + private final Block[] palette; + private final short[] data; + + SubChunkSnapshot(Block[] p, short[] d) { + this.palette = p; + this.data = d; + } + + Block getBlock(int x, int y, int z) { + if (this == EMPTY || data == null) return Blocks.air; + short idx = data[(y << 8) | (z << 4) | x]; + return (idx >= 0 && idx < palette.length) ? palette[idx] : Blocks.air; + } + } + + private static final class ConcurrentBitSet { + private final AtomicLongArray words = new AtomicLongArray(WORDS_PER_SET); + + void set(int bit) { + if (bit < 0 || bit >= BITSET_SIZE) return; + int wd = bit >>> 6; + long m = 1L << (bit & 63); + while (true) { + long o = words.get(wd); + long u = o | m; + if (o == u || words.compareAndSet(wd, o, u)) return; + } + } + + void clear(int bit) { + if (bit < 0 || bit >= BITSET_SIZE) return; + int wd = bit >>> 6; + long m = ~(1L << (bit & 63)); + words.set(wd, words.get(wd) & m); + } + + int nextSetBit(int from) { + if (from < 0) from = 0; + int wd = from >>> 6; + if (wd >= WORDS_PER_SET) return -1; + long w = words.get(wd) & (~0L << (from & 63)); + while (true) { + if (w != 0) return (wd << 6) + Long.numberOfTrailingZeros(w); + if (++wd == WORDS_PER_SET) return -1; + w = words.get(wd); + } + } + + boolean isEmpty() { + for (int i = 0; i < WORDS_PER_SET; i++) if (words.get(i) != 0) return false; + return true; + } + } + + private class Worker implements Runnable { + @Override + public void run() { + try { + while (true) { + if (collectFinished && rayQueue.isEmpty()) break; + RayTask task = rayQueue.poll(100, TimeUnit.MILLISECONDS); + if (task == null) { + if (collectFinished && rayQueue.isEmpty()) break; + continue; + } + task.trace(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + private class RayTask { + final int dirIndex; + double px, py, pz; + int x, y, z; + float energy; + double tMaxX, tMaxY, tMaxZ, tDeltaX, tDeltaY, tDeltaZ; + int stepX, stepY, stepZ; + boolean initialised = false; + + RayTask(int dirIdx) { + this.dirIndex = dirIdx; + } + + void init() { + Vec3 dir = directions.get(dirIndex); + px = explosionX; + py = explosionY; + pz = explosionZ; + x = originX; + y = originY; + z = originZ; + energy = strength; + + + final double EPS = 1e-6; + double ax = Math.abs(dir.xCoord); + stepX = ax < EPS ? 0 : (dir.xCoord > 0 ? 1 : -1); + double invDx = stepX == 0 ? Double.POSITIVE_INFINITY : 1.0 / ax; + double ay = Math.abs(dir.yCoord); + stepY = ay < EPS ? 0 : (dir.yCoord > 0 ? 1 : -1); + double invDy = stepY == 0 ? Double.POSITIVE_INFINITY : 1.0 / ay; + double az = Math.abs(dir.zCoord); + stepZ = az < EPS ? 0 : (dir.zCoord > 0 ? 1 : -1); + double invDz = stepZ == 0 ? Double.POSITIVE_INFINITY : 1.0 / az; + + tDeltaX = invDx; + tDeltaY = invDy; + tDeltaZ = invDz; + tMaxX = stepX == 0 ? Double.POSITIVE_INFINITY : ((stepX > 0 ? (x + 1 - px) : (px - x)) * invDx); + tMaxY = stepY == 0 ? Double.POSITIVE_INFINITY : ((stepY > 0 ? (y + 1 - py) : (py - y)) * invDy); + tMaxZ = stepZ == 0 ? Double.POSITIVE_INFINITY : ((stepZ > 0 ? (z + 1 - pz) : (pz - z)) * invDz); + initialised = true; + } + + void trace() { + if (!initialised) init(); + if (energy <= 0) { + latch.countDown(); + return; + } + + while (energy > 0) { + if (y < 0 || y >= WORLD_HEIGHT) { + break; + } + double dxBlock = x + 0.5 - explosionX; + double dyBlock = y + 0.5 - explosionY; + double dzBlock = z + 0.5 - explosionZ; + if (dxBlock * dxBlock + dyBlock * dyBlock + dzBlock * dzBlock > radius * radius) { + break; + } + + ChunkKey ck = new ChunkKey(x >> 4, z >> 4, y >> 4); + SubChunkSnapshot snap = snapshots.get(ck); + + if (snap == null) { + cacheQueue.offer(ck); + rayQueue.offer(this); + return; + } + if (snap != SubChunkSnapshot.EMPTY) { + Block block = snap.getBlock(x & 15, y & 15, z & 15); + if (block != Blocks.air) { + float res = getNukeResistance(block); + float resistanceCutoff = 2_000_000F; + if (res >= resistanceCutoff) break; + double distToBlock = Math.sqrt(dxBlock * dxBlock + dyBlock * dyBlock + dzBlock * dzBlock); + double effectiveDist = Math.max(distToBlock, 0.01); + energy -= (float) (Math.pow(res + 1.0, 3.0 * (effectiveDist / radius)) - 1.0); + if (energy > 0) { + ConcurrentBitSet bs = destructionMap.computeIfAbsent(ck.pos, posKey -> new ConcurrentBitSet()); + bs.set(((255 - y) << 8) | ((x & 15) << 4) | (z & 15)); + } else break; + } + } + + if (tMaxX < tMaxY) { + if (tMaxX < tMaxZ) { + x += stepX; + tMaxX += tDeltaX; + } else { + z += stepZ; + tMaxZ += tDeltaZ; + } + } else { + if (tMaxY < tMaxZ) { + y += stepY; + tMaxY += tDeltaY; + } else { + z += stepZ; + tMaxZ += tDeltaZ; + } + } + } + latch.countDown(); + } + } +} diff --git a/src/main/java/com/hbm/interfaces/IExplosionRay.java b/src/main/java/com/hbm/interfaces/IExplosionRay.java new file mode 100644 index 000000000..2ee6ac78b --- /dev/null +++ b/src/main/java/com/hbm/interfaces/IExplosionRay.java @@ -0,0 +1,9 @@ +package com.hbm.interfaces; + +public interface IExplosionRay { + boolean isComplete(); + + void cacheChunksTick(int processTime); + + void destructionTick(int processTime); +} From 9bb9a56452771f7e27ce1bce1490be8c2d18f4d6 Mon Sep 17 00:00:00 2001 From: mlbv <51232730+mlbv@users.noreply.github.com> Date: Fri, 23 May 2025 01:47:29 +0800 Subject: [PATCH 2/5] Fix: setDead() now correctly calls cancel() --- .../java/com/hbm/entity/logic/EntityNukeExplosionMK5.java | 7 +++++++ .../java/com/hbm/explosion/ExplosionNukeRayBatched.java | 7 +++++++ .../com/hbm/explosion/ExplosionNukeRayParallelized.java | 1 + src/main/java/com/hbm/interfaces/IExplosionRay.java | 2 ++ 4 files changed, 17 insertions(+) diff --git a/src/main/java/com/hbm/entity/logic/EntityNukeExplosionMK5.java b/src/main/java/com/hbm/entity/logic/EntityNukeExplosionMK5.java index def573ad7..95965c95c 100644 --- a/src/main/java/com/hbm/entity/logic/EntityNukeExplosionMK5.java +++ b/src/main/java/com/hbm/entity/logic/EntityNukeExplosionMK5.java @@ -133,6 +133,13 @@ public class EntityNukeExplosionMK5 extends EntityExplosionChunkloading { } } + @Override + public void setDead(){ + if(explosion != null) + explosion.cancel(); + super.setDead(); + } + @Override protected void readEntityFromNBT(NBTTagCompound nbt) { this.ticksExisted = nbt.getInteger("ticksExisted"); diff --git a/src/main/java/com/hbm/explosion/ExplosionNukeRayBatched.java b/src/main/java/com/hbm/explosion/ExplosionNukeRayBatched.java index 6f7af5e35..1d97dfa38 100644 --- a/src/main/java/com/hbm/explosion/ExplosionNukeRayBatched.java +++ b/src/main/java/com/hbm/explosion/ExplosionNukeRayBatched.java @@ -272,6 +272,13 @@ public class ExplosionNukeRayBatched implements IExplosionRay { processChunk(); } + @Override + public void cancel() { + isAusf3Complete = true; + if (perChunk != null) perChunk.clear(); + if (orderedChunks != null) orderedChunks.clear(); + } + public class FloatTriplet { public float xCoord; public float yCoord; diff --git a/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java b/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java index bc0cf57a8..2c8eb6761 100644 --- a/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java +++ b/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java @@ -195,6 +195,7 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { return collectFinished && destroyFinished; } + @Override public void cancel() { this.collectFinished = true; this.destroyFinished = true; diff --git a/src/main/java/com/hbm/interfaces/IExplosionRay.java b/src/main/java/com/hbm/interfaces/IExplosionRay.java index 2ee6ac78b..d164b6b27 100644 --- a/src/main/java/com/hbm/interfaces/IExplosionRay.java +++ b/src/main/java/com/hbm/interfaces/IExplosionRay.java @@ -6,4 +6,6 @@ public interface IExplosionRay { void cacheChunksTick(int processTime); void destructionTick(int processTime); + + void cancel(); } From ea430afe71c5bcd6d94aa2214bb6d6d492a7b50b Mon Sep 17 00:00:00 2001 From: mlbv <51232730+mlbv@users.noreply.github.com> Date: Fri, 23 May 2025 19:40:45 +0800 Subject: [PATCH 3/5] Fix: Make crate look normal --- .../ExplosionNukeRayParallelized.java | 137 +++++++++++------- 1 file changed, 86 insertions(+), 51 deletions(-) diff --git a/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java b/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java index 2c8eb6761..81e28cb63 100644 --- a/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java +++ b/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java @@ -86,7 +86,7 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { } private static float getNukeResistance(Block b) { - // if (b.getMaterial().isLiquid()) return 0.1F; + if (b.getMaterial().isLiquid()) return 0.1F; if (b == Blocks.sandstone) return Blocks.stone.getExplosionResistance(null); if (b == Blocks.obsidian) return Blocks.stone.getExplosionResistance(null) * 3; return b.getExplosionResistance(null); @@ -128,7 +128,7 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { Chunk chunk = world.getChunkFromChunkCoords(cp.chunkXPos, cp.chunkZPos); ExtendedBlockStorage[] storages = chunk.getBlockStorageArray(); - boolean ChunkModified = false; + boolean chunkModified = false; for (int subY = 0; subY < storages.length; subY++) { ExtendedBlockStorage storage = storages[subY]; @@ -157,7 +157,7 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { int zLocal = zGlobal & 0xF; storage.func_150818_a(xLocal, yLocal, zLocal, Blocks.air); storage.setExtBlockMetadata(xLocal, yLocal, zLocal, 0); - ChunkModified = true; + chunkModified = true; world.notifyBlocksOfNeighborChange(xGlobal, yGlobal, zGlobal, Blocks.air); world.markBlockForUpdate(xGlobal, yGlobal, zGlobal); @@ -170,7 +170,7 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { } } - if (ChunkModified) { + if (chunkModified) { chunk.setChunkModified(); world.markBlockRangeForRenderUpdate(cp.chunkXPos << 4, 0, cp.chunkZPos << 4, (cp.chunkXPos << 4) | 15, WORLD_HEIGHT - 1, (cp.chunkZPos << 4) | 15); } @@ -387,7 +387,6 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { } } } - private class RayTask { final int dirIndex; double px, py, pz; @@ -396,40 +395,51 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { double tMaxX, tMaxY, tMaxZ, tDeltaX, tDeltaY, tDeltaZ; int stepX, stepY, stepZ; boolean initialised = false; + double currentRayPosition; + + private static final double RAY_DIRECTION_EPSILON = 1e-6; + private static final double PROCESSING_EPSILON = 1e-9; + private static final float MIN_EFFECTIVE_DIST_FOR_ENERGY_CALC = 0.01f; RayTask(int dirIdx) { this.dirIndex = dirIdx; } void init() { - Vec3 dir = directions.get(dirIndex); - px = explosionX; - py = explosionY; - pz = explosionZ; - x = originX; - y = originY; - z = originZ; - energy = strength; + Vec3 dir = directions.get(this.dirIndex); + this.px = explosionX; + this.py = explosionY; + this.pz = explosionZ; + this.x = originX; + this.y = originY; + this.z = originZ; + // This scales the crate. higher = bigger crate. Adjust if the radius of the crate deviates from expected. + this.energy = strength * 0.3F; + this.currentRayPosition = 0.0; + double dirX = dir.xCoord; + double dirY = dir.yCoord; + double dirZ = dir.zCoord; - final double EPS = 1e-6; - double ax = Math.abs(dir.xCoord); - stepX = ax < EPS ? 0 : (dir.xCoord > 0 ? 1 : -1); - double invDx = stepX == 0 ? Double.POSITIVE_INFINITY : 1.0 / ax; - double ay = Math.abs(dir.yCoord); - stepY = ay < EPS ? 0 : (dir.yCoord > 0 ? 1 : -1); - double invDy = stepY == 0 ? Double.POSITIVE_INFINITY : 1.0 / ay; - double az = Math.abs(dir.zCoord); - stepZ = az < EPS ? 0 : (dir.zCoord > 0 ? 1 : -1); - double invDz = stepZ == 0 ? Double.POSITIVE_INFINITY : 1.0 / az; + double absDirX = Math.abs(dirX); + this.stepX = (absDirX < RAY_DIRECTION_EPSILON) ? 0 : (dirX > 0 ? 1 : -1); + this.tDeltaX = (stepX == 0) ? Double.POSITIVE_INFINITY : 1.0 / absDirX; + this.tMaxX = (stepX == 0) ? Double.POSITIVE_INFINITY : + ((stepX > 0 ? (this.x + 1 - this.px) : (this.px - this.x)) * this.tDeltaX); - tDeltaX = invDx; - tDeltaY = invDy; - tDeltaZ = invDz; - tMaxX = stepX == 0 ? Double.POSITIVE_INFINITY : ((stepX > 0 ? (x + 1 - px) : (px - x)) * invDx); - tMaxY = stepY == 0 ? Double.POSITIVE_INFINITY : ((stepY > 0 ? (y + 1 - py) : (py - y)) * invDy); - tMaxZ = stepZ == 0 ? Double.POSITIVE_INFINITY : ((stepZ > 0 ? (z + 1 - pz) : (pz - z)) * invDz); - initialised = true; + double absDirY = Math.abs(dirY); + this.stepY = (absDirY < RAY_DIRECTION_EPSILON) ? 0 : (dirY > 0 ? 1 : -1); + this.tDeltaY = (stepY == 0) ? Double.POSITIVE_INFINITY : 1.0 / absDirY; + this.tMaxY = (stepY == 0) ? Double.POSITIVE_INFINITY : + ((stepY > 0 ? (this.y + 1 - this.py) : (this.py - this.y)) * this.tDeltaY); + + double absDirZ = Math.abs(dirZ); + this.stepZ = (absDirZ < RAY_DIRECTION_EPSILON) ? 0 : (dirZ > 0 ? 1 : -1); + this.tDeltaZ = (stepZ == 0) ? Double.POSITIVE_INFINITY : 1.0 / absDirZ; + this.tMaxZ = (stepZ == 0) ? Double.POSITIVE_INFINITY : + ((stepZ > 0 ? (this.z + 1 - this.pz) : (this.pz - this.z)) * this.tDeltaZ); + + this.initialised = true; } void trace() { @@ -440,15 +450,8 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { } while (energy > 0) { - if (y < 0 || y >= WORLD_HEIGHT) { - break; - } - double dxBlock = x + 0.5 - explosionX; - double dyBlock = y + 0.5 - explosionY; - double dzBlock = z + 0.5 - explosionZ; - if (dxBlock * dxBlock + dyBlock * dyBlock + dzBlock * dzBlock > radius * radius) { - break; - } + if (y < 0 || y >= WORLD_HEIGHT) break; + if (currentRayPosition >= radius - PROCESSING_EPSILON) break; ChunkKey ck = new ChunkKey(x >> 4, z >> 4, y >> 4); SubChunkSnapshot snap = snapshots.get(ck); @@ -458,21 +461,40 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { rayQueue.offer(this); return; } - if (snap != SubChunkSnapshot.EMPTY) { - Block block = snap.getBlock(x & 15, y & 15, z & 15); + double t_exit_voxel = Math.min(tMaxX, Math.min(tMaxY, tMaxZ)); + double segmentLenInVoxel = t_exit_voxel - this.currentRayPosition; + double segmentLenForProcessing; + boolean stopAfterThisSegment = false; + + if (this.currentRayPosition + segmentLenInVoxel > radius - PROCESSING_EPSILON) { + segmentLenForProcessing = Math.max(0.0, radius - this.currentRayPosition); + stopAfterThisSegment = true; + } else { + segmentLenForProcessing = segmentLenInVoxel; + } + + if (snap != SubChunkSnapshot.EMPTY && segmentLenForProcessing > PROCESSING_EPSILON) { + Block block = snap.getBlock(x & 0xF, y & 0xF, z & 0xF); if (block != Blocks.air) { - float res = getNukeResistance(block); - float resistanceCutoff = 2_000_000F; - if (res >= resistanceCutoff) break; - double distToBlock = Math.sqrt(dxBlock * dxBlock + dyBlock * dyBlock + dzBlock * dzBlock); - double effectiveDist = Math.max(distToBlock, 0.01); - energy -= (float) (Math.pow(res + 1.0, 3.0 * (effectiveDist / radius)) - 1.0); - if (energy > 0) { - ConcurrentBitSet bs = destructionMap.computeIfAbsent(ck.pos, posKey -> new ConcurrentBitSet()); - bs.set(((255 - y) << 8) | ((x & 15) << 4) | (z & 15)); - } else break; + float resistance = getNukeResistance(block); + if (resistance >= 2_000_000F) { // cutoff + energy = 0; + } else { + double energyLossFactor = getEnergyLossFactor(resistance); + energy -= (float) (energyLossFactor * segmentLenForProcessing); + if (energy > 0) { + ConcurrentBitSet bs = destructionMap.computeIfAbsent( + ck.pos, + posKey -> new ConcurrentBitSet() + ); + int bitIndex = ((WORLD_HEIGHT - 1 - y) << 8) | ((x & 0xF) << 4) | (z & 0xF); + bs.set(bitIndex); + } + } } } + this.currentRayPosition = t_exit_voxel; + if (energy <= 0 || stopAfterThisSegment || this.currentRayPosition >= radius - PROCESSING_EPSILON) break; if (tMaxX < tMaxY) { if (tMaxX < tMaxZ) { @@ -494,5 +516,18 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { } latch.countDown(); } + + private double getEnergyLossFactor(float resistance) { + double dxBlockToCenter = (this.x + 0.5) - explosionX; + double dyBlockToCenter = (this.y + 0.5) - explosionY; + double dzBlockToCenter = (this.z + 0.5) - explosionZ; + double distToBlockCenterSq = dxBlockToCenter * dxBlockToCenter + + dyBlockToCenter * dyBlockToCenter + + dzBlockToCenter * dzBlockToCenter; + double distToBlockCenter = Math.sqrt(distToBlockCenterSq); + + double effectiveDist = Math.max(distToBlockCenter, MIN_EFFECTIVE_DIST_FOR_ENERGY_CALC); + return (Math.pow(resistance + 1.0, 3.0 * (effectiveDist / radius)) - 1.0); + } } } From 9d3860df64f779af50d95455a4ea840b395c7a4a Mon Sep 17 00:00:00 2001 From: mlbv <51232730+mlbv@users.noreply.github.com> Date: Fri, 23 May 2025 21:47:03 +0800 Subject: [PATCH 4/5] Fix: Make generateSphereRays run asynchronously --- .../com/hbm/explosion/ExplosionNukeRayParallelized.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java b/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java index 81e28cb63..ab0527d25 100644 --- a/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java +++ b/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java @@ -28,7 +28,8 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { private final int strength; private final int radius; - private final List directions; + private volatile List directions; + private final CompletableFuture> directionsFuture; private final ConcurrentMap destructionMap; private final ConcurrentMap snapshots; @@ -67,7 +68,7 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { int workers = Math.max(1, Runtime.getRuntime().availableProcessors() - 1); this.pool = Executors.newWorkStealingPool(workers); - this.directions = generateSphereRays(rayCount); + this.directionsFuture = CompletableFuture.supplyAsync(() -> generateSphereRays(rayCount)); for (int i = 0; i < rayCount; i++) rayQueue.add(new RayTask(i)); for (int i = 0; i < workers; i++) pool.submit(new Worker()); @@ -406,6 +407,7 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { } void init() { + if (directions == null) directions = directionsFuture.join(); Vec3 dir = directions.get(this.dirIndex); this.px = explosionX; this.py = explosionY; @@ -413,7 +415,8 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { this.x = originX; this.y = originY; this.z = originZ; - // This scales the crate. higher = bigger crate. Adjust if the radius of the crate deviates from expected. + // This scales the crater. higher = bigger. + // Currently the crater is a little bit bigger than the original implementation this.energy = strength * 0.3F; this.currentRayPosition = 0.0; From 1a5fda1847138e137ebf74a95abb0dda3ebdca21 Mon Sep 17 00:00:00 2001 From: mlbv <51232730+mlbv@users.noreply.github.com> Date: Sat, 24 May 2025 09:00:15 +0800 Subject: [PATCH 5/5] Feat: accumulatedDestruction --- src/main/java/com/hbm/config/BombConfig.java | 2 + .../ExplosionNukeRayParallelized.java | 152 ++++++++++++++++-- 2 files changed, 140 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/hbm/config/BombConfig.java b/src/main/java/com/hbm/config/BombConfig.java index 927ebd3ba..64ee42417 100644 --- a/src/main/java/com/hbm/config/BombConfig.java +++ b/src/main/java/com/hbm/config/BombConfig.java @@ -27,6 +27,7 @@ public class BombConfig { public static int limitExplosionLifespan = 0; public static boolean chunkloading = true; public static boolean parallelization = true; + public static boolean accumulatedDestruction = true; public static void loadFromConfig(Configuration config) { @@ -96,5 +97,6 @@ public class BombConfig { chunkloading = CommonConfig.createConfigBool(config, CATEGORY_NUKE, "6.XX_enableChunkLoading", "Allows all types of procedural explosions to keep the central chunk loaded.", true); parallelization = CommonConfig.createConfigBool(config, CATEGORY_NUKE, "6.XX_enableParallelization", "Allows explosions to use multiple threads.", true); + accumulatedDestruction = CommonConfig.createConfigBool(config, CATEGORY_NUKE, "6.XX_enableAccumulatedDestruction", "Enables the accumulated destruction model for explosions. Blocks accumulate damage and are only destroyed once their resistance is exceeded.\nMore physically accurate, slightly slower. Requires enableParallelization = true.", true); } } diff --git a/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java b/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java index ab0527d25..6cab5a01c 100644 --- a/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java +++ b/src/main/java/com/hbm/explosion/ExplosionNukeRayParallelized.java @@ -1,5 +1,6 @@ package com.hbm.explosion; +import com.hbm.config.BombConfig; import com.hbm.interfaces.IExplosionRay; import com.hbm.main.MainRegistry; import net.minecraft.block.Block; @@ -15,6 +16,7 @@ import org.apache.logging.log4j.Level; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLongArray; +import java.util.concurrent.atomic.DoubleAdder; public class ExplosionNukeRayParallelized implements IExplosionRay { @@ -31,6 +33,8 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { private volatile List directions; private final CompletableFuture> directionsFuture; private final ConcurrentMap destructionMap; + private final ConcurrentMap accumulatedDamageMap; + private final ConcurrentMap snapshots; private final BlockingQueue rayQueue; @@ -40,9 +44,9 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { private final Thread latchWatcherThread; private final List orderedChunks; private volatile boolean collectFinished = false; + private volatile boolean consolidationFinished = false; private volatile boolean destroyFinished = false; - public ExplosionNukeRayParallelized(World world, double x, double y, double z, int strength, int speed, int radius) { this.world = world; this.explosionX = x; @@ -60,6 +64,7 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { this.latch = new CountDownLatch(rayCount); this.destructionMap = new ConcurrentHashMap<>(); + this.accumulatedDamageMap = new ConcurrentHashMap<>(); this.snapshots = new ConcurrentHashMap<>(); this.orderedChunks = new ArrayList<>(); @@ -80,6 +85,11 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { Thread.currentThread().interrupt(); } finally { collectFinished = true; + if (BombConfig.accumulatedDestruction) { + pool.submit(this::runConsolidation); + } else { + consolidationFinished = true; + } } }, "ExplosionNuke-LatchWatcher-" + System.nanoTime()); this.latchWatcherThread.setDaemon(true); @@ -110,7 +120,8 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { @Override public void destructionTick(int timeBudgetMs) { - if (!collectFinished || destroyFinished) return; + if (!collectFinished || !consolidationFinished || destroyFinished) return; // Added consolidationFinished check + final long deadline = System.nanoTime() + timeBudgetMs * 1_000_000L; if (orderedChunks.isEmpty() && !destructionMap.isEmpty()) { @@ -193,12 +204,13 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { @Override public boolean isComplete() { - return collectFinished && destroyFinished; + return collectFinished && consolidationFinished && destroyFinished; } @Override public void cancel() { this.collectFinished = true; + this.consolidationFinished = true; this.destroyFinished = true; if (this.rayQueue != null) this.rayQueue.clear(); @@ -227,6 +239,7 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { } } if (this.destructionMap != null) this.destructionMap.clear(); + if (this.accumulatedDamageMap != null) this.accumulatedDamageMap.clear(); if (this.snapshots != null) this.snapshots.clear(); if (this.orderedChunks != null) this.orderedChunks.clear(); } @@ -291,6 +304,72 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { return list; } + private void runConsolidation() { + Iterator> chunkEntryIterator = accumulatedDamageMap.entrySet().iterator(); + while (chunkEntryIterator.hasNext()) { + Map.Entry entry = chunkEntryIterator.next(); + ChunkCoordIntPair cp = entry.getKey(); + ChunkDamageAccumulator accumulator = entry.getValue(); + + if (accumulator.isEmpty()) { + chunkEntryIterator.remove(); + continue; + } + + ConcurrentBitSet chunkDestructionBitSet = destructionMap.computeIfAbsent(cp, k -> new ConcurrentBitSet()); + + Iterator> damageEntryIterator = accumulator.entrySet().iterator(); + while (damageEntryIterator.hasNext()) { + Map.Entry damageEntry = damageEntryIterator.next(); + int bitIndex = damageEntry.getKey(); + + float accumulatedDamage = (float) damageEntry.getValue().sum(); + + if (accumulatedDamage <= 0.0f) { + damageEntryIterator.remove(); + continue; + } + + int yGlobal = 255 - (bitIndex >>> 8); + int subY = yGlobal >> 4; + + if (subY < 0) { + damageEntryIterator.remove(); + continue; + } + + ChunkKey snapshotKey = new ChunkKey(cp.chunkXPos, cp.chunkZPos, subY); + SubChunkSnapshot snap = snapshots.get(snapshotKey); + Block originalBlock; + + if (snap == null || snap == SubChunkSnapshot.EMPTY) { + damageEntryIterator.remove(); + continue; + } else { + int xLocal = (bitIndex >>> 4) & 0xF; + int zLocal = bitIndex & 0xF; + originalBlock = snap.getBlock(xLocal, yGlobal & 0xF, zLocal); + if (originalBlock == Blocks.air) { + damageEntryIterator.remove(); + continue; + } + } + + float resistance = getNukeResistance(originalBlock); + if (accumulatedDamage >= resistance) { + chunkDestructionBitSet.set(bitIndex); + damageEntryIterator.remove(); + } + } + + if (accumulator.isEmpty()) { + chunkEntryIterator.remove(); + } + } + accumulatedDamageMap.clear(); + consolidationFinished = true; + } + private static class ChunkKey { final ChunkCoordIntPair pos; final int subY; @@ -349,7 +428,13 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { if (bit < 0 || bit >= BITSET_SIZE) return; int wd = bit >>> 6; long m = ~(1L << (bit & 63)); - words.set(wd, words.get(wd) & m); + while (true) { + long oldWord = words.get(wd); + long newWord = oldWord & m; + if (oldWord == newWord || words.compareAndSet(wd, oldWord, newWord)) { + return; + } + } } int nextSetBit(int from) { @@ -370,6 +455,35 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { } } + private static class ChunkDamageAccumulator { + // key = bitIndex, value = total accumulated damage + private final ConcurrentHashMap damageMap = new ConcurrentHashMap<>(); + + public void addDamage(int bitIndex, float damageAmount) { + if (damageAmount <= 0) return; + DoubleAdder adder = damageMap.computeIfAbsent(bitIndex, k -> new DoubleAdder()); + adder.add(damageAmount); + } + + public float getDamage(int bitIndex) { + DoubleAdder adder = damageMap.get(bitIndex); + return adder == null ? 0f : (float) adder.sum(); + } + + public void clearDamage(int bitIndex) { + damageMap.remove(bitIndex); + } + + public Set> entrySet() { + return damageMap.entrySet(); + } + + + public boolean isEmpty() { + return damageMap.isEmpty(); + } + } + private class Worker implements Runnable { @Override public void run() { @@ -409,15 +523,15 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { void init() { if (directions == null) directions = directionsFuture.join(); Vec3 dir = directions.get(this.dirIndex); + // This scales the crater. Higher = bigger. + // Currently the crater is a little bit bigger than the original implementation + this.energy = strength * 0.3F; this.px = explosionX; this.py = explosionY; this.pz = explosionZ; this.x = originX; this.y = originY; this.z = originZ; - // This scales the crater. higher = bigger. - // Currently the crater is a little bit bigger than the original implementation - this.energy = strength * 0.3F; this.currentRayPosition = 0.0; double dirX = dir.xCoord; @@ -484,14 +598,24 @@ public class ExplosionNukeRayParallelized implements IExplosionRay { energy = 0; } else { double energyLossFactor = getEnergyLossFactor(resistance); - energy -= (float) (energyLossFactor * segmentLenForProcessing); - if (energy > 0) { - ConcurrentBitSet bs = destructionMap.computeIfAbsent( - ck.pos, - posKey -> new ConcurrentBitSet() - ); + float damageDealt = (float) (energyLossFactor * segmentLenForProcessing); + energy -= damageDealt; + if (damageDealt > 0) { int bitIndex = ((WORLD_HEIGHT - 1 - y) << 8) | ((x & 0xF) << 4) | (z & 0xF); - bs.set(bitIndex); + if (BombConfig.accumulatedDestruction) { + ChunkCoordIntPair chunkPos = ck.pos; + ChunkDamageAccumulator chunkAccumulator = + accumulatedDamageMap.computeIfAbsent(chunkPos, k -> new ChunkDamageAccumulator()); + chunkAccumulator.addDamage(bitIndex, damageDealt); + } else { + if (energy > 0) { + ConcurrentBitSet bs = destructionMap.computeIfAbsent( + ck.pos, + posKey -> new ConcurrentBitSet() + ); + bs.set(bitIndex); + } + } } } }