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..e654f48386e21e049facb6d90b356117af2bace6 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/explosion/special/SpecialisedExplosion.java @@ -0,0 +1,176 @@ +package me.samsuik.sakura.explosion.special; + +import io.papermc.paper.util.WorldUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +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; + + public SpecialisedExplosion(Level world, T entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, double x, double y, double z, float power, boolean createFire, BlockInteraction destructionType) { + super(world, entity, damageSource, behavior, x, y, z, power, createFire, destructionType); + 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 + @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.startExplosion(); // search for blocks, impact entities, finalise if necessary + } + } + + 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.getHitPlayers().clear(); + } + + this.getToBlow().clear(); + return destroyedBlocks; + } + + protected abstract void postExplosion(List foundBlocks, boolean destroyedBlocks); + + 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.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(entity, pos, (float) radius); + } + } + } + + 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); // Paper - Optimize 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..2e46df5cda2dc2812a10cd54ff7b128f9c50b361 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/explosion/special/TntExplosion.java @@ -0,0 +1,192 @@ +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.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.item.PrimedTnt; +import net.minecraft.world.level.ExplosionDamageCalculator; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +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 boolean moved = false; + + public TntExplosion(Level world, PrimedTnt tnt, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, double x, double y, double z, float power, boolean createFire, BlockInteraction destructionType) { + super(world, tnt, damageSource, behavior, x, y, z, power, createFire, destructionType); + 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) { + BlockPos explosionBlockPos = new BlockPos(this.x, this.y, this.z); + BlockState blockstate = this.level.getBlockState(explosionBlockPos); + float resistance = blockstate.getBlock().getExplosionResistance(); + if ((resistance + 0.3f) * 0.3f < (this.radius * 1.3f) && this.interactsWithBlocks() && this.isRegionUnprotected()) { // Sakura - optimise protected explosions + this.searchForBlocks(); + } + } + + 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) { + // Update wrapped, this is for tracking swinging and if blocks are found + if (this.wrapped >= 7) { + if (!destroyedBlocks && this.level.sakuraConfig.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) { + // Before setting entity state, otherwise we might cause issues. + final boolean hasMoved; + if (this.moved) { + hasMoved = true; + } else if (this.position.equals(this.cause.position())) { + hasMoved = false; + } else { + double newMomentum = entityState.momentum().lengthSqr(); + double oldMomentum = this.cause.entityState().momentum().lengthSqr(); + hasMoved = oldMomentum <= Math.pow(this.radius * 2.0 + 1.0, 2.0) || newMomentum <= oldMomentum; + } + + // Keep track of entity state + entityState.apply(this.cause); + this.cause.storeEntityState(); + + // Ticking is always required after destroying a block. + if (destroyedBlocks || hasMoved) { + this.cause.setFuse(100); + this.cause.tick(); + this.moved |= !this.position.equals(this.originalPosition); + 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(); + foundEntity.updateEntityHandle(this.cause); + } + + 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() && 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 451f7facb950c98b67713d6e7d012bf25bb89e31..71f96f0716e6001ea74690d43160c08ca6a3024e 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -1657,6 +1657,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()) { @@ -1676,7 +1682,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 0c75fe840119040898a10a931dcb9b3e51c8cdcf..88f83ef060b818e31bbb636a45048b2cdb1d4fec 100644 --- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java +++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java @@ -76,28 +76,7 @@ public class PrimedTnt extends Entity { && (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, this.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 cf9dd1ffdacdfe1c26ca15ca17a20fe46a81b20d..a10d18a7573f9174708be0bf9dd912b8ce7eb0f5 100644 --- a/src/main/java/net/minecraft/world/level/Explosion.java +++ b/src/main/java/net/minecraft/world/level/Explosion.java @@ -59,12 +59,14 @@ public class Explosion { private final Explosion.BlockInteraction blockInteraction; private final Random 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; + protected final float radius; // Sakura - private -> protected private final DamageSource damageSource; private final ExplosionDamageCalculator damageCalculator; private final List toBlow; @@ -455,7 +457,10 @@ public class Explosion { Player entityhuman = (Player) entity; if (!entityhuman.isSpectator() && (!entityhuman.isCreative() || !entityhuman.getAbilities().flying) && !level.paperConfig.disableExplosionKnockback) { // Paper - Disable explosion knockback - this.hitPlayers.put(entityhuman, new Vec3(d8 * d13, d9 * d13, d10 * d13)); + // Sakura start - specialised explosions; tally player velocity + final Vec3 explosionImpact = new Vec3(d8 * d13, d9 * d13, d10 * d13); + this.hitPlayers.compute(entityhuman, (p, v) -> v != null ? v.add(explosionImpact) : explosionImpact); + // Sakura end - specialised explosions; tally player velocity } } } @@ -656,7 +661,7 @@ public class Explosion { private BlockInteraction() {} } // Paper start - Optimize explosions - private float getBlockDensity(Vec3 vec3d, Entity entity) { + protected final float getBlockDensity(Vec3 vec3d, Entity entity) { // Sakura - private -> protected // 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 b7407df002b7addf854a321f62c26003f7fdc86b..cd770809cc3720b63681d447f3970ff59b6b4c61 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -982,7 +982,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable { } public Explosion explode(@Nullable Entity entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, double x, double y, double z, float power, boolean createFire, Explosion.BlockInteraction destructionType) { - Explosion explosion = new Explosion(this, entity, damageSource, behavior, x, y, z, power, createFire, destructionType); + // 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, destructionType); + } else { + explosion = new Explosion(this, entity, damageSource, behavior, x, y, z, power, createFire, destructionType); + } + // Sakura end - specialised explosions explosion.explode(); explosion.finalizeExplosion(true);