From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Samsuik Date: Fri, 3 May 2024 15:04:31 +0100 Subject: [PATCH] Specialised Explosions diff --git a/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java b/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java index 0fd814f1d65c111266a2b20f86561839a4cef755..932f7a0d030d2d4932e6e6d4a5805e9b683cce67 100644 --- a/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java +++ b/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java @@ -125,6 +125,12 @@ public final class IteratorSafeOrderedReferenceSet { } } + // Sakura start - add indexOf method + public int indexOf(final E element) { + return this.indexMap.getInt(element); + } + // Sakura end - add indexOf method + public boolean remove(final E element) { final int index = this.indexMap.removeInt(element); if (index >= 0) { diff --git a/src/main/java/me/samsuik/sakura/explosion/special/SpecialisedExplosion.java b/src/main/java/me/samsuik/sakura/explosion/special/SpecialisedExplosion.java new file mode 100644 index 0000000000000000000000000000000000000000..0611555b4afb461e2045585e3d81601420792c7c --- /dev/null +++ b/src/main/java/me/samsuik/sakura/explosion/special/SpecialisedExplosion.java @@ -0,0 +1,199 @@ +package me.samsuik.sakura.explosion.special; + +import io.papermc.paper.util.WorldUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.item.PrimedTnt; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.ExplosionDamageCalculator; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.function.Consumer; + +public abstract class SpecialisedExplosion extends Explosion { + private static final double ENTITY_DISPATCH_DISTANCE = Math.pow(32.0, 2.0); + + protected final ServerLevel level; + protected Vec3 position; + protected final T cause; // preferred over source + private Vec3 impactPosition; + protected ExplosionBlockCache[] recentBlockCache; + protected final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); + + public SpecialisedExplosion(Level world, T entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, double x, double y, double z, float power, boolean createFire, BlockInteraction destructionType, ParticleOptions particle, ParticleOptions emitterParticle, SoundEvent soundEvent) { + super(world, entity, damageSource, behavior, x, y, z, power, createFire, destructionType, particle, emitterParticle, soundEvent); + this.level = (ServerLevel) world; + this.position = new Vec3(x, y, z); + this.cause = entity; + this.impactPosition = this.position; + } + + protected double getExplosionOffset() { + return (double) this.cause.getBbHeight() * 0.0625D; + } + + protected abstract void startExplosion(); + + @Override + protected final void clearBlockCache() { + super.clearBlockCache(); + this.recentBlockCache = null; + } + + @Override + @Deprecated + public final void explode() { + if (this.radius() < 0.1F) { + // (radius < 0.1F) in bukkit is assumed to not be able to find any blocks or entities. + for (int i = 1; i < this.cause.getStacked(); ++i) { + this.finalizeExplosionAndParticles(); + } + } else { + this.recentBlockCache = this.createBlockCache(); + this.startExplosion(); // search for blocks, impact entities, finalise if necessary + this.clearBlockCache(); + } + } + + protected final boolean requiresImpactEntities() { + if (this.impactPosition.distanceToSqr(this.position) > ENTITY_DISPATCH_DISTANCE) { + this.impactPosition = this.position; + return true; + } + return !this.getToBlow().isEmpty(); + } + + protected final boolean finalizeExplosionAndParticles() { + this.wasCanceled = false; + super.finalizeExplosion(false); + boolean destroyedBlocks = !this.getToBlow().isEmpty(); + + if (!this.interactsWithBlocks()) { + this.getToBlow().clear(); // server sends block changes in the explosion packet + } + + if (!this.wasCanceled) { + this.level.notifyPlayersOfExplosion(this.x, this.y, this.z, this.radius(), this); + } + + this.getToBlow().clear(); + return destroyedBlocks; + } + + protected void postExplosion(List foundBlocks, boolean destroyedBlocks) { + // optimisation: Keep the block cache across explosions and invalidate any found blocks. + // This can help reduce block retrievals while block searching when there's a durable block, + // and when ray tracing for obstructions. This is disabled by default because plugins can + // change blocks in the explosion event. + if (this.level.sakuraConfig().cannons.explosion.useBlockCacheAcrossExplosions && !foundBlocks.isEmpty() && !destroyedBlocks) { + this.markBlocksInCacheAsExplodable(foundBlocks); + } else { + super.blockCache.clear(); + } + + java.util.Arrays.fill(this.recentBlockCache, null); + } + + protected final void recalculateExplosionPosition() { + this.recalculateExplosionPosition(this.cause); + } + + protected final void recalculateExplosionPosition(T entity) { + this.x = entity.getX(); + this.y = entity.getY() + this.getExplosionOffset(); + this.z = entity.getZ(); + this.position = new Vec3(this.x, this.y, this.z); + } + + protected final void forEachEntitySliceInBounds(AABB bb, Consumer sliceConsumer) { + int minSection = WorldUtil.getMinSection(this.level); + int maxSection = WorldUtil.getMaxSection(this.level); + + int minChunkX = Mth.floor(bb.minX) >> 4; + int minChunkY = Mth.clamp(Mth.floor(bb.minY) >> 4, minSection, maxSection); + int minChunkZ = Mth.floor(bb.minZ) >> 4; + int maxChunkX = Mth.floor(bb.maxX) >> 4; + int maxChunkY = Mth.clamp(Mth.floor(bb.maxY) >> 4, minSection, maxSection); + int maxChunkZ = Mth.floor(bb.maxZ) >> 4; + + io.papermc.paper.chunk.system.entity.EntityLookup entityLookup = this.level.getEntityLookup(); + for (int chunkX = minChunkX; chunkX <= maxChunkX; ++chunkX) { + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; ++chunkZ) { + io.papermc.paper.world.ChunkEntitySlices chunk = entityLookup.getChunk(chunkX, chunkZ); + + if (chunk == null) { + continue; + } + + for (int chunkY = minChunkY; chunkY <= maxChunkY; ++chunkY) { + sliceConsumer.accept(chunk.getSectionEntities(chunkY)); + } + } + } + } + + protected final void impactEntitiesFromPosition(Entity[] entities, Vec3 position, int potential, double radius) { + for (int i = 0; i < entities.length; ++i) { + Entity entity = entities[i]; + if (entity == null) break; + + if (entity != source && !entity.ignoreExplosion(this)) { + this.impactEntity(entity, position, potential, radius); + } + + if (entities[i] != entity) { + i--; + } + } + } + + protected final void impactEntity(Entity entity, Vec3 pos, int potential, double radius) { + if (entity.isPrimedTNT || entity.isFallingBlock) { + this.impactCannonEntity(entity, pos, potential, radius); + } else { + for (int i = 0; i < potential; ++i) { + super.impactEntity(this.recentBlockCache, this.mutablePos, (float) radius, pos, entity); + } + } + } + + protected final void impactCannonEntity(Entity entity, Vec3 pos, int potential, double radius) { + double distanceFromBottom = Math.sqrt(entity.distanceToSqr(pos)) / radius; + + if (distanceFromBottom <= 1.0) { + double x = entity.getX() - pos.x; + double y = (entity instanceof PrimedTnt ? entity.getY() : entity.getEyeY()) - this.y; + double z = entity.getZ() - pos.z; + double distance = Math.sqrt(x * x + y * y + z * z); + + if (distance != 0.0D) { + x /= distance; + y /= distance; + z /= distance; + double density = this.getBlockDensity(pos, entity, this.recentBlockCache, this.mutablePos); // Paper - Optimize explosions // Paper - optimise explosions + double exposure = (1.0D - distanceFromBottom) * density; + + if (exposure == 0.0) { + return; + } + + x *= exposure; + y *= exposure; + z *= exposure; + + for (int i = 0; i < potential; ++i) { + entity.addDeltaMovement(x, y, z); + } + } + } + } +} diff --git a/src/main/java/me/samsuik/sakura/explosion/special/TntExplosion.java b/src/main/java/me/samsuik/sakura/explosion/special/TntExplosion.java new file mode 100644 index 0000000000000000000000000000000000000000..39cd91c1e19585d24bbe1333bf15985b0d1a0dab --- /dev/null +++ b/src/main/java/me/samsuik/sakura/explosion/special/TntExplosion.java @@ -0,0 +1,186 @@ +package me.samsuik.sakura.explosion.special; + +import io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import me.samsuik.sakura.entity.EntityState; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.item.PrimedTnt; +import net.minecraft.world.level.ExplosionDamageCalculator; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public final class TntExplosion extends SpecialisedExplosion { + private static final int ALL_DIRECTIONS = 0b111; + private static final int FOUND_ALL_BLOCKS = ALL_DIRECTIONS + 12; + + private final Vec3 originalPosition; + private final List explosions = new ObjectArrayList<>(); + private AABB bounds; + private int wrapped = 0; + private int movement = 0; + + public TntExplosion(Level world, PrimedTnt tnt, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, double x, double y, double z, float power, boolean createFire, BlockInteraction destructionType, ParticleOptions particle, ParticleOptions emitterParticle, SoundEvent soundEvent) { + super(world, tnt, damageSource, behavior, x, y, z, power, createFire, destructionType, particle, emitterParticle, soundEvent); + this.originalPosition = this.position; + this.bounds = new AABB(x, y, z, x, y, z); + } + + @Override + protected void startExplosion() { + for (int i = this.calculateExplosionPotential() - 1; i >= 0; --i) { + Vec3 previousMomentum = this.cause.entityState().momentum(); + boolean lastCycle = (i == 0); + this.midExplosion(previousMomentum, lastCycle); // search for blocks and impact entities + + if (!lastCycle) { + EntityState entityState = this.nextSourceVelocity(); + List foundBlocks = new ObjectArrayList<>(this.getToBlow()); + boolean destroyedBlocks = this.finalizeExplosionAndParticles(); + this.postExplosion(foundBlocks, destroyedBlocks); // update wrapped, clear recent block cache + this.updateExplosionPosition(entityState, destroyedBlocks); + } + } + } + + private void midExplosion(Vec3 previousMomentum, boolean lastCycle) { + if (this.wrapped < FOUND_ALL_BLOCKS) { + final int blockX = Mth.floor(this.x); + final int blockY = Mth.floor(this.y); + final int blockZ = Mth.floor(this.z); + + final long key = BlockPos.asLong(blockX, blockY, blockZ); + + final ExplosionBlockCache initialCache = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); + + if (initialCache.resistance <= (this.radius() * 1.3f) && this.interactsWithBlocks() && this.isRegionUnprotected()) { // Sakura - optimise protected explosions + this.searchForBlocks(this.recentBlockCache, initialCache); + } + } + + if (this.wrapped < ALL_DIRECTIONS) { + Vec3 momentum = this.cause.entityState().momentum(); + for (Direction.Axis axis : Direction.Axis.VALUES) { + double current = momentum.get(axis); + double previous = previousMomentum.get(axis); + if (current == previous || current * previous < 0) { + this.wrapped |= 1 << axis.ordinal(); + } + } + } + + this.bounds = this.bounds.expand(this.position); + this.explosions.add(this.position); + + if (lastCycle || this.requiresImpactEntities()) { + this.locateAndImpactEntitiesInBounds(); + this.bounds = new AABB(this.position, this.position); + this.explosions.clear(); + } + } + + @Override + protected void postExplosion(List foundBlocks, boolean destroyedBlocks) { + super.postExplosion(foundBlocks, destroyedBlocks); + // Update wrapped, this is for tracking swinging and if blocks are found + if (this.wrapped >= 7) { + if (!destroyedBlocks && this.level.sakuraConfig().cannons.explosion.avoidRedundantBlockSearches) { + this.wrapped++; + } else { + this.wrapped = 7; + } + } + } + + private Vector getCauseOrigin() { + Vector origin = this.cause.getOriginVector(); + return origin == null ? new Vector(this.x, this.y, this.z) : origin; + } + + private EntityState nextSourceVelocity() { + Vector origin = this.getCauseOrigin(); // valid position to use while creating a temporary entity + PrimedTnt tnt = new PrimedTnt(this.level, origin.getX(), origin.getY(), origin.getZ(), null); + this.cause.entityState().apply(tnt); + this.impactCannonEntity(tnt, this.position, 1, this.radius() * 2.0f); + return EntityState.of(tnt); + } + + private void updateExplosionPosition(EntityState entityState, boolean destroyedBlocks) { + // Ticking is always required after destroying a block. + if (destroyedBlocks || !this.position.equals(this.cause.position()) && (this.movement != 1 || !this.originalPosition.equals(this.position))) { + entityState.apply(this.cause); + this.cause.storeEntityState(); + this.cause.setFuse(100); + this.cause.tick(); + this.movement++; + this.recalculateExplosionPosition(); + } + } + + private int calculateExplosionPotential() { + IteratorSafeOrderedReferenceSet entities = this.level.entityTickList.entities; + int base = this.cause.getStacked(); + int index = entities.indexOf(this.cause); + // iterate over the entityTickList to find entities that are exploding in the same position. + while ((index = entities.advanceRawIterator(index)) != -1) { + Entity foundEntity = entities.rawGet(index); + if (foundEntity.isRemoved() || !foundEntity.compareState(this.cause) || !foundEntity.isSafeToMergeInto(this.cause)) + break; + base += foundEntity.getStacked(); + foundEntity.discard(); + } + + return base; + } + + private void locateAndImpactEntitiesInBounds() { + double radius = this.radius() * 2.0f; + AABB bb = this.bounds; + + Vec3 center = bb.getCenter(); + double change = Math.max(bb.getXsize(), Math.max(bb.getYsize(), bb.getZsize())); + double maxDistanceSqr = Math.pow(radius + change, 2.0); + boolean moved = (change != 0.0); + + this.forEachEntitySliceInBounds(bb.inflate(radius), entities -> { + if (moved) { + this.impactEntitiesSwinging(entities, center, radius, maxDistanceSqr); + } else { + this.impactEntitiesFromPosition(entities, this.explosions.get(0), this.explosions.size(), radius); + } + }); + } + + private void impactEntitiesSwinging(Entity[] entities, Vec3 center, double radius, double maxDistanceSqr) { + for (int i = 0; i < entities.length; ++i) { + Entity entity = entities[i]; + if (entity == null) break; + + if (entity != this.source && !entity.ignoreExplosion(this) && entity.distanceToSqr(center.x, center.y, center.z) <= maxDistanceSqr) { + this.impactEntity(entity, radius); + } + + if (entities[i] != entity) { + i--; + } + } + } + + private void impactEntity(Entity entity, double radius) { + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < this.explosions.size(); i++) { + this.impactEntity(entity, this.explosions.get(i), 1, radius); + } + } +} diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java index 51fa57e8b9d5c9ee563ec3608a437c69da08d32c..ec7d7f22b267ae6572e6005f10221755cbb1a480 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -1957,6 +1957,12 @@ public class ServerLevel extends Level implements WorldGenLevel { explosion.clearToBlow(); } + // Sakura start - specialised explosions + this.notifyPlayersOfExplosion(x, y, z, power, explosion); + return explosion; + } + public final void notifyPlayersOfExplosion(double x, double y, double z, float power, Explosion explosion) { + // Sakura end - specialised explosions Iterator iterator = this.players.iterator(); while (iterator.hasNext()) { @@ -1967,7 +1973,7 @@ public class ServerLevel extends Level implements WorldGenLevel { } } - return explosion; + // Sakura - return moved up into explode } @Override diff --git a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java index 4f695305794c2564517d99b4edd3180d7ea07845..8acef487b0579febb3497cc88b23c4fc74492571 100644 --- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java +++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java @@ -86,28 +86,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { && (tnt.entityState().fallDistance() == fallDistance || tnt.entityState().fallDistance() > 2.5f && fallDistance > 2.5f); } - - @Override - protected boolean respawnMerged() { - if (stacked <= 1) return false; - - PrimedTnt tnt = new PrimedTnt(EntityType.TNT, level()); - - while (stacked-- > 1) { - this.setFuse(100); // Prevent unwanted explosions while ticking - - // Cause an explosion to affect this entity - tnt.setPos(this.position()); - tnt.setDeltaMovement(this.getDeltaMovement()); - this.entityState().apply(this); - tnt.explode(); - this.storeEntityState(); - - this.tick(); - } - - return true; - } + // Sakura - specialised explosions // Sakura end @Override diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java index c9cf9373d3eae4e54ae27fbb8993f858e91e98cc..5ab0de07b1da8c14b62044747bf6e4288ecc7506 100644 --- a/src/main/java/net/minecraft/world/level/Explosion.java +++ b/src/main/java/net/minecraft/world/level/Explosion.java @@ -55,9 +55,11 @@ public class Explosion { private final Explosion.BlockInteraction blockInteraction; private final RandomSource random; private final Level level; - private final double x; - private final double y; - private final double z; + // Sakura start - private -> protected + protected double x; + protected double y; + protected double z; + // Sakura end - private -> protected @Nullable public final Entity source; private final float radius; @@ -185,7 +187,7 @@ public class Explosion { // resistance = (res + 0.3F) * 0.3F; // so for resistance = 0, we need res = -0.3F private static final Float ZERO_RESISTANCE = Float.valueOf(-0.3f); - private it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap blockCache = null; + protected it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap blockCache = null; // Sakura - private -> protected public static final class ExplosionBlockCache { @@ -212,7 +214,29 @@ public class Explosion { private long[] chunkPosCache = null; private net.minecraft.world.level.chunk.LevelChunk[] chunkCache = null; - private ExplosionBlockCache getOrCacheExplosionBlock(final int x, final int y, final int z, + // Sakura start - specialised explosions + protected final ExplosionBlockCache[] createBlockCache() { + this.blockCache = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(); + + this.chunkPosCache = new long[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; + java.util.Arrays.fill(this.chunkPosCache, ChunkPos.INVALID_CHUNK_POS); + + this.chunkCache = new net.minecraft.world.level.chunk.LevelChunk[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; + + return new ExplosionBlockCache[BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH]; + } + + protected final void markBlocksInCacheAsExplodable(List blocks) { + for (BlockPos blow : blocks) { + ExplosionBlockCache cache = this.blockCache.get(blow.asLong()); + // May be null if the blockCache is cleared then retrieved from the recent block cache + if (cache != null) { + cache.shouldExplode = null; + } + } + } + // Sakura end - specialised explosions + protected final ExplosionBlockCache getOrCacheExplosionBlock(final int x, final int y, final int z, // Sakura - private -> protected final long key, final boolean calculateResistance) { ExplosionBlockCache ret = this.blockCache.get(key); if (ret != null) { @@ -310,7 +334,7 @@ public class Explosion { (currZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); ExplosionBlockCache cachedBlock = blockCache[cacheKey]; if (cachedBlock == null || cachedBlock.key != key) { - blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(currX, currY, currZ, key, false); + blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(currX, currY, currZ, key, this.level.sakuraConfig().cannons.explosion.useBlockCacheAcrossExplosions); // Sakura - specialised explosions } final BlockState blockState = cachedBlock.blockState; @@ -501,14 +525,7 @@ public class Explosion { int j; // Paper start - optimise explosions - this.blockCache = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(); - - this.chunkPosCache = new long[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; - java.util.Arrays.fill(this.chunkPosCache, ChunkPos.INVALID_CHUNK_POS); - - this.chunkCache = new net.minecraft.world.level.chunk.LevelChunk[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; - - final ExplosionBlockCache[] blockCache = new ExplosionBlockCache[BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH]; + final ExplosionBlockCache[] blockCache = this.createBlockCache(); // use initial cache value that is most likely to be used: the source position final ExplosionBlockCache initialCache; { @@ -998,7 +1015,7 @@ public class Explosion { private BlockInteraction() {} } // Paper start - Optimize explosions - private float getBlockDensity(Vec3 vec3d, Entity entity, ExplosionBlockCache[] blockCache, BlockPos.MutableBlockPos blockPos) { // Paper - optimise explosions + protected final float getBlockDensity(Vec3 vec3d, Entity entity, ExplosionBlockCache[] blockCache, BlockPos.MutableBlockPos blockPos) { // Sakura - private -> protected // Paper - optimise explosions // Sakura start - replace density cache float blockDensity = this.level.densityCache.getDensity(vec3d, entity); if (blockDensity == me.samsuik.sakura.explosion.density.BlockDensityCache.UNKNOWN_DENSITY) { diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java index d555ea21a6e53938003a45d5de2ce97c4986f9e7..ad4076dd0352eb4f62588e3c83ffb2c42b07a3e0 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -1419,7 +1419,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable { } Explosion.BlockInteraction explosion_effect1 = explosion_effect; - Explosion explosion = new Explosion(this, entity, damageSource, behavior, x, y, z, power, createFire, explosion_effect1, particle, emitterParticle, soundEvent); + // Sakura start - specialised explosions + final Explosion explosion; + if (entity instanceof net.minecraft.world.entity.item.PrimedTnt tnt) { + explosion = new me.samsuik.sakura.explosion.special.TntExplosion(this, tnt, damageSource, behavior, x, y, z, power, createFire, explosion_effect1, particle, emitterParticle, soundEvent); + } else { + explosion = new Explosion(this, entity, damageSource, behavior, x, y, z, power, createFire, explosion_effect1, particle, emitterParticle, soundEvent); + } + // Sakura end - specialised explosions explosion.explode(); explosion.finalizeExplosion(particles);