From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Samsuik <40902469+Samsuik@users.noreply.github.com> Date: Thu, 12 Oct 2023 16:23:31 +0100 Subject: [PATCH] Optimised Explosions diff --git a/src/main/java/me/samsuik/sakura/explosion/DensityCache.java b/src/main/java/me/samsuik/sakura/explosion/DensityCache.java new file mode 100644 index 0000000000000000000000000000000000000000..3f6f34cc617efaad420485a7f613cfcad88e3783 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/explosion/DensityCache.java @@ -0,0 +1,130 @@ +package me.samsuik.sakura.explosion; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +import javax.annotation.Nullable; + +public class DensityCache { + + private final Int2ObjectMap densityMap = new Int2ObjectOpenHashMap<>(); + + public @Nullable Density retrieveCache(int key) { + return densityMap.get(key); + } + + public void createCache(int key, Entity entity, Vec3 vec3d, float density) { + densityMap.put(key, new Density(entity.getBoundingBox(), vec3d, density)); + } + + public void clear() { + densityMap.clear(); + } + + public static int createKey(Entity entity, Vec3 vec3d) { + int hash = Mth.floor(vec3d.x); + hash = 31 * hash ^ Mth.floor(vec3d.y); + hash = 31 * hash ^ Mth.floor(vec3d.z); + hash = 31 * hash ^ Mth.floor(entity.getX()); + hash = 31 * hash ^ Mth.floor(entity.getY()); + hash = 31 * hash ^ Mth.floor(entity.getZ()); + return hash; + } + + public static final class Density { + private AABB source; + private AABB entity; + private AABB obstruction; + private final float density; + private final boolean expand; + + Density(AABB bb, Vec3 p, float d) { + entity = bb; + source = new AABB(p, p); + density = d; + expand = density == 0.0f || density == 1.0f; + } + + public boolean isExpandable() { + return expand; + } + + public float density() { + return density; + } + + public void obscure(Vec3 p) { + if (obstruction == null) { + obstruction = new AABB(p, p); + } else { + obstruction = expandBBWithVec3(p, obstruction); + } + } + + public boolean isObscured(Vec3 p) { + return obstruction != null + && obstruction.minX <= p.x && obstruction.maxX >= p.x + && obstruction.minY <= p.y && obstruction.maxY >= p.y + && obstruction.minZ <= p.z && obstruction.maxZ >= p.z; + } + + public void expand(AABB bb, Vec3 p) { + entity = entity.minmax(bb); + source = expandBBWithVec3(p, source); + } + + public boolean has(AABB bb, Vec3 p) { + return isBBWithinBounds(bb) && isPointWithinBounds(p); + } + + public boolean has(Vec3 p) { + return isPointWithinBounds(p); + } + + private boolean isBBWithinBounds(AABB bb) { + return entity.minX <= bb.minX && entity.maxX >= bb.maxX + && entity.minY <= bb.minY && entity.maxY >= bb.maxY + && entity.minZ <= bb.minZ && entity.maxZ >= bb.maxZ; + } + + private boolean isPointWithinBounds(Vec3 p) { + return source.minX <= p.x && source.maxX >= p.x + && source.minY <= p.y && source.maxY >= p.y + && source.minZ <= p.z && source.maxZ >= p.z; + } + + private AABB expandBBWithVec3(Vec3 point, AABB what) { + double minX = what.minX; + double minY = what.minY; + double minZ = what.minZ; + double maxX = what.maxX; + double maxY = what.maxY; + double maxZ = what.maxZ; + + if (point.x < minX) { + minX = point.x; + } else if (point.x > maxX) { + maxX = point.x; + } + + if (point.y < minY) { + minY = point.y; + } else if (point.y > maxY) { + maxY = point.y; + } + + if (point.z < minZ) { + minZ = point.z; + } else if (point.z > maxZ) { + maxZ = point.z; + } + + return new AABB(minX, minY, minZ, maxX, maxY, maxZ); + } + } + +} diff --git a/src/main/java/me/samsuik/sakura/explosion/SakuraExplosion.java b/src/main/java/me/samsuik/sakura/explosion/SakuraExplosion.java new file mode 100644 index 0000000000000000000000000000000000000000..93c7be878632296683a794235455013c33bec9e7 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/explosion/SakuraExplosion.java @@ -0,0 +1,381 @@ +package me.samsuik.sakura.explosion; + +import me.samsuik.sakura.entity.EntityState; +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.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.boss.EnderDragonPart; +import net.minecraft.world.entity.boss.enderdragon.EnderDragon; +import net.minecraft.world.entity.item.PrimedTnt; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.enchantment.ProtectionEnchantment; +import net.minecraft.world.level.Explosion; +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.craftbukkit.event.CraftEventFactory; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/* + * Special explosion implementation to take advantage of TNT merging. + * + * This allows us to introduce more optimisations + * * if we have been unable to find any blocks nearby stop searching for blocks + * * avoid trying to affect entities that are completely obscured + * * reduce range checks for out of range entities + * * take better advantage of the block cache in paper + * * special case for explosions in the same position + * + * Unfortunately, this requires duplicating the impact entity section from Explosion. + * + * This does hurt performance in a "rogue tnt" scenario, tnt that has been spawned + * by an explosion destroying a tnt block often in massive blocks of tnt. It is not + * realistic to explode a big block of tnt in survival or factions. They only cause + * harm to a server and extremely wasteful for resources with minimal impact to terrain. + */ +public class SakuraExplosion extends Explosion { + + private final Level level; + + public SakuraExplosion(Level world, @Nullable Entity 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 = world; + } + + @Override + public void explode() { + if (this.radius < 0.1F) { + for (int i = 1; i < source.getStacked(); ++i) { + getToBlow().clear(); + ((ServerLevel) level).notifyPlayersOfExplosion(x, y, z, radius, this); + finalizeExplosion(false); + } + + return; + } + + List positions = new ArrayList<>(source.getStacked()); + + EntityState entityState = null; + AABB bounds = new AABB(x, y, z, x, y, z); + Vec3 lastMovement = source.entityState().momentum(); + int wrapped = 0; + + for (int i = 0; i < source.getStacked() && !wasCanceled; ++i) { + if (i > 0) { + calculateNextPosition(entityState); + getToBlow().clear(); + } + + // keep track of positions and bounds + Vec3 position = new Vec3(x, y, z); + positions.add(position); + bounds = bounds.minmax(new AABB(position, position)); + + // search for blocks if necessary + if (wrapped < 7 + 12) { + BlockState state = level.getBlockState(BlockPos.containing(x, y, z)); + if (interactsWithBlocks() && isDestructibleBlock(state) && isRegionUnprotected()) { + searchForBlocks(); + } + } + + // Check if the explosion has wrapped around with swinging on each axis + if (wrapped < 7) { + Vec3 movement = source.entityState().momentum(); + if (movement.x == lastMovement.x || movement.x * lastMovement.x < 0) wrapped |= 1; + if (movement.y == lastMovement.y || movement.y * lastMovement.y < 0) wrapped |= 1 << 1; + if (movement.z == lastMovement.z || movement.z * lastMovement.z < 0) wrapped |= 1 << 2; + lastMovement = movement; + } + + boolean isFinalExplosion = i + 1 >= source.getStacked(); + + // Possible optimisation here is making our own finalize explosion for this special case. + // If this is after the explosion event we can take better advantage of protection plugins. + if (isFinalExplosion || !getToBlow().isEmpty()) { + locateAndImpactEntities(positions, bounds); + bounds = new AABB(position, position); + positions.clear(); + } + + if (!isFinalExplosion) { + // Calculate next source velocity + entityState = calculateNextVelocity(position); + + // Could be viable in the future to have a configuration option to reduce explosion events + super.finalizeExplosion(false); + ((ServerLevel) level).notifyPlayersOfExplosion(x, y, z, radius, this); + + // Update wrapped, this is for tracking swinging and if blocks are found + if (getToBlow().isEmpty() && level.sakuraConfig().cannons.explosion.avoidRedundantBlockSearches) { + wrapped++; + } else { + wrapped = 7; + } + } + } + } + + private void calculateNextPosition(EntityState entityState) { + if (source instanceof PrimedTnt tnt) { + tnt.setFuse(100); + } + + boolean moved = !source.entityState().position().equals(source.position()); + entityState.apply(source); + source.storeEntityState(); + + if (!getToBlow().isEmpty() || source.getDeltaMovement().lengthSqr() <= 65.16525625 || moved) { + source.tick(); + } + + // update explosion position + x = source.getX(); + y = source.getY(0.0625D); + z = source.getZ(); + } + + private EntityState calculateNextVelocity(Vec3 position) { + PrimedTnt tnt = new PrimedTnt(EntityType.TNT, level); + source.entityState().apply(tnt); + impactEntityIdle(tnt, new Entity[0], position, 1, radius * 2.0f); + return EntityState.of(tnt); + } + + private void locateAndImpactEntities(List positions, AABB bb) { + double radius = this.radius * 2.0f; + + int minSection = io.papermc.paper.util.WorldUtil.getMinSection(level); + int maxSection = io.papermc.paper.util.WorldUtil.getMaxSection(level); + + int minChunkX = Mth.floor(bb.minX - radius) >> 4; + int minChunkY = Mth.clamp(Mth.floor(bb.minY - radius) >> 4, minSection, maxSection); + int minChunkZ = Mth.floor(bb.minZ - radius) >> 4; + int maxChunkX = Mth.floor(bb.maxX + radius) >> 4; + int maxChunkY = Mth.clamp(Mth.floor(bb.maxY + radius) >> 4, minSection, maxSection); + int maxChunkZ = Mth.floor(bb.maxZ + radius) >> 4; + + Vec3 center = bb.getCenter(); + double change = Math.max(bb.maxX - bb.minX, Math.max(bb.maxY - bb.minY, bb.maxZ - bb.minZ)); + double maxDistanceSqr = Math.pow(radius + change, 2); + + boolean moved = change != 0.0; + + io.papermc.paper.chunk.system.entity.EntityLookup entityLookup = ((ServerLevel) level).getEntityLookup(); + + // impact entities already has a range check there is no reason to also + // do an intersection check when retrieving entities from the chunk. + + 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) { + if (moved) { + impactEntities(chunk.getSectionEntities(chunkY), positions, center, radius, maxDistanceSqr); + } else { + impactEntitiesIdle(chunk.getSectionEntities(chunkY), positions.get(0), positions.size(), radius); + } + } + } + } + } + + // swinging case: more than 1 position and actively changing positions. + private void impactEntities(Entity[] entities, List positions, Vec3 center, double radius, double maxDistanceSqr) { + for (int i = 0; i < entities.length; ++i) { + Entity entity = entities[i]; + if (entity == null) break; + + if (entity != source && !entity.ignoreExplosion() && entity.distanceToSqr(center.x, center.y, center.z) <= maxDistanceSqr) { + int key = DensityCache.createKey(entity, center); + DensityCache.Density data = level.densityCache.retrieveCache(key); + Vec3 position = entity.position(); + + if (data != null && data.isObscured(position)) { + continue; + } else if (impactEntity(entity, entities, positions, radius) == 1 && data != null) { + data.obscure(position); + } + } + + // chunk entities can change while we're affecting entities + if (entities[i] != entity) { + i--; + } + } + } + + private int impactEntity(Entity entity, Entity[] section, List positions, double radius) { + int found = 0; + + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < positions.size(); i++) { + Vec3 pos = positions.get(i); + + double distanceFromBottom = Math.sqrt(entity.distanceToSqr(pos)) / radius; + + if (distanceFromBottom > 1.0) continue; + + double x = entity.getX() - pos.x; + double y = (entity instanceof PrimedTnt ? entity.getY() : entity.getEyeY()) - pos.y; + double z = entity.getZ() - pos.z; + double distance = Math.sqrt(x * x + y * y + z * z); + + if (distance == 0.0D) continue; + + x /= distance; + y /= distance; + z /= distance; + double density = this.getBlockDensity(pos, entity); // Paper - Optimize explosions // Paper - optimise explosions + double exposure = (1.0D - distanceFromBottom) * density; + + int visible = density != 0.0 ? 1 : 0; + found |= (visible << 1) | 1; + + if (entity.isPrimedTNT || entity.isFallingBlock) { + entity.addDeltaMovement(x * exposure, y * exposure, z * exposure); + continue; + } + + impactLiving(entity, pos, section, x, y, z, exposure, radius); + } + + return found; + } + + // stationary case: 1 position or stationary + private void impactEntitiesIdle(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()) { + impactEntityIdle(entity, entities, position, potential, radius); + } + + // chunk entities can change while we're affecting entities + if (entities[i] != entity) { + i--; + } + } + } + + private void impactEntityIdle(Entity entity, Entity[] section, 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()) - pos.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 // Paper - optimise explosions + double exposure = (1.0D - distanceFromBottom) * density; + + if (entity.isPrimedTNT || entity.isFallingBlock) { + x *= exposure; + y *= exposure; + z *= exposure; + + if (exposure == 0.0) { + return; + } + + for (int i = 0; i < potential; ++i) { + entity.addDeltaMovement(x, y, z); + } + } else { + for (int i = 0; i < potential; ++i) { + impactLiving(entity, pos, section, x, y, z, exposure, radius); + } + } + } + } + } + + private void impactLiving(Entity entity, Vec3 pos, Entity[] section, double x, double y, double z, double exposure, double radius) { + // CraftBukkit start + + // Special case ender dragon only give knockback if no damage is cancelled + // Thinks to note: + // - Setting a velocity to a ComplexEntityPart is ignored (and therefore not needed) + // - Damaging ComplexEntityPart while forward the damage to EntityEnderDragon + // - Damaging EntityEnderDragon does nothing + // - EntityEnderDragon hitbock always covers the other parts and is therefore always present + if (entity instanceof EnderDragonPart) { + return; + } + + CraftEventFactory.entityDamage = this.source; + entity.lastDamageCancelled = false; + + if (entity instanceof EnderDragon) { + for (EnderDragonPart entityComplexPart : ((EnderDragon) entity).subEntities) { + for (Entity ent : section) { + // Calculate damage separately for each EntityComplexPart + if (ent == null) break; + if (ent == entityComplexPart) { + double d7part; + if ((d7part = Math.sqrt(entityComplexPart.distanceToSqr(pos)) / radius) <= 1.0D) { + double d13part = (1.0D - d7part) * Explosion.getSeenPercent(pos, entityComplexPart, null); // Sakura + entityComplexPart.hurt(this.getDamageSource(), (float) ((int) ((d13part * d13part + d13part) / 2.0D * 7.0D * radius + 1.0D))); + } + } + } + } + } else { + entity.hurt(this.damageSource, (float) ((int) ((exposure * exposure + exposure) / 2.0D * 7.0D * radius + 1.0D))); + } + + CraftEventFactory.entityDamage = null; + if (entity.lastDamageCancelled) { // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Skip entity if damage event was cancelled + return; + } + // CraftBukkit end + + double force; + + if (entity instanceof LivingEntity) { + LivingEntity entityliving = (LivingEntity) entity; + + force = entity instanceof Player && level.paperConfig().environment.disableExplosionKnockback ? 0 : ProtectionEnchantment.getExplosionKnockbackAfterDampener(entityliving, exposure); // Paper - disable explosion knockback + } else { + force = exposure; + } + + x *= force; + y *= force; + z *= force; + // Sakura - moved down + + entity.addDeltaMovement(x, y, z); // Sakura reduce deltamovement allocations + if (entity instanceof Player) { + Vec3 vec3d1 = new Vec3(x, y, z); // Sakura + Player entityhuman = (Player) entity; + + if (!entityhuman.isSpectator() && (!entityhuman.isCreative() || !entityhuman.getAbilities().flying) && !level.paperConfig().environment.disableExplosionKnockback) { // Paper - Disable explosion knockback + this.getHitPlayers().put(entityhuman, vec3d1); + } + } + } + +} diff --git a/src/main/java/me/samsuik/sakura/utils/ExplosionUtil.java b/src/main/java/me/samsuik/sakura/utils/ExplosionUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..e0387f16ff49031fdcbc8990613417da88d84e87 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/utils/ExplosionUtil.java @@ -0,0 +1,64 @@ +package me.samsuik.sakura.utils; + +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; + +import java.util.ArrayList; +import java.util.List; + +public class ExplosionUtil { + + private static final java.util.function.Function highestOf = (vector) -> { + double highest = 0; + + for (double v : vector) { + highest = Math.max(Math.abs(v), highest); + } + + return highest; + }; + + public static void reduceRays(DoubleArrayList rayCoords) { + // temporarily transform rayCoords into a double[] list + List vectors = new ArrayList<>(); + + for (int i = 0; i < rayCoords.size(); i += 3) { + vectors.add(new double[] { + rayCoords.getDouble(i), + rayCoords.getDouble(i + 1), + rayCoords.getDouble(i + 2) + }); + } + + vectors.sort((o1, o2) -> Double.compare(highestOf.apply(o2), highestOf.apply(o1))); + + List checked = new java.util.ArrayList<>(); + + for (double[] vector : vectors) { + boolean found = checked.stream().anyMatch((filtered) -> { + double x = (filtered[0] - vector[0]) / 0.3f; + double y = (filtered[1] - vector[1]) / 0.3f; + double z = (filtered[2] - vector[2]) / 0.3f; + double distanceSquared = x * x + y * y + z * z; + + return (distanceSquared > 0.009 && distanceSquared < 0.01) + || (distanceSquared > 0.0075 && distanceSquared < 0.008) + || (distanceSquared > 0.006 && distanceSquared < 0.00675); + }); + + if (!found) { + checked.add(vector); + } + } + + rayCoords.clear(); + + for (double[] vector : vectors) { + for (double coord : vector) { + rayCoords.add(coord); + } + } + + rayCoords.trim(); + } + +} diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 9a15ce8aef1c52b915ea9131a31e60065d667a4a..a9049e2cf29bdf90eb308f6b15a4374779032eec 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1579,6 +1579,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 2.5f && fallDistance > 2.5f); } + /* @Override protected boolean respawnMerged() { if (stacked <= 1) return false; @@ -96,6 +97,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { return true; } + */ // 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 3cb5306a2ccfc1c53f90ecd56980d17be4042093..88fccb17260c203111147a2a458d81855cf12152 100644 --- a/src/main/java/net/minecraft/world/level/Explosion.java +++ b/src/main/java/net/minecraft/world/level/Explosion.java @@ -55,14 +55,16 @@ 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 - expose fields + protected double x; + protected double y; + protected double z; @Nullable public final Entity source; - private final float radius; - private final DamageSource damageSource; - private final ExplosionDamageCalculator damageCalculator; + protected final float radius; + protected final DamageSource damageSource; + protected final ExplosionDamageCalculator damageCalculator; + // Sakura end private final ObjectArrayList toBlow; private final Map hitPlayers; public boolean wasCanceled = false; // CraftBukkit - add field @@ -96,11 +98,70 @@ public class Explosion { this.damageCalculator = behavior == null ? this.makeDamageCalculator(entity) : behavior; } + // Sakura start + private static final double[] BLOCK_RAYCAST_VECTORS; + + static { + it.unimi.dsi.fastutil.doubles.DoubleArrayList vectorList = new it.unimi.dsi.fastutil.doubles.DoubleArrayList(); + + for (int k = 0; k < 16; ++k) { + for (int i = 0; i < 16; ++i) { + for (int j = 0; j < 16; ++j) { + if (k == 0 || k == 15 || i == 0 || i == 15 || j == 0 || j == 15) { + double d0 = (double) ((float) k / 15.0F * 2.0F - 1.0F); + double d1 = (double) ((float) i / 15.0F * 2.0F - 1.0F); + double d2 = (double) ((float) j / 15.0F * 2.0F - 1.0F); + double d3 = Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2); + + d0 /= d3; + d1 /= d3; + d2 /= d3; + + vectorList.add(d0 * 0.30000001192092896D); + vectorList.add(d1 * 0.30000001192092896D); + vectorList.add(d2 * 0.30000001192092896D); + } + } + } + } + + me.samsuik.sakura.utils.ExplosionUtil.reduceRays(vectorList); + BLOCK_RAYCAST_VECTORS = vectorList.toDoubleArray(); + } + + protected boolean isDestructibleBlock(@Nullable BlockState state) { + if (state == null) { + return false; + } + + float power = radius * 1.3f; + float blockRes = state.getBlock().getExplosionResistance(); + float fluidRes = state.getFluidState().getExplosionResistance(); + + // This should be better than just checking if we're within a fluid block. + return (Math.max(blockRes, fluidRes) + 0.3f) * 0.3f <= power; + } + + protected boolean isRegionUnprotected() { + // As an optimisation, check if a plugin has cancelled or cleared the blockList. + // This is relatively sane on factions and cannon servers, but mileage may vary. + if (source != null && level.sakuraConfig().cannons.explosion.optimiseProtectedRegions) { + Location location = new Location(level.getWorld(), x, y, z); + List blocks = new ObjectArrayList<>(1); + blocks.add(location.getBlock()); + EntityExplodeEvent event = new EntityExplodeEvent(source.getBukkitEntity(), location, blocks, 0.0f); + return event.callEvent() && !event.blockList().isEmpty(); + } + + return true; + } + // Sakura end + private ExplosionDamageCalculator makeDamageCalculator(@Nullable Entity entity) { return (ExplosionDamageCalculator) (entity == null ? Explosion.EXPLOSION_DAMAGE_CALCULATOR : new EntityBasedExplosionDamageCalculator(entity)); } - public static float getSeenPercent(Vec3 source, Entity entity) { + protected static float getSeenPercent(Vec3 source, Entity entity, @Nullable me.samsuik.sakura.explosion.DensityCache.Density data) { // Sakura - protected and pass density AABB axisalignedbb = entity.getBoundingBox(); double d0 = 1.0D / ((axisalignedbb.maxX - axisalignedbb.minX) * 2.0D + 1.0D); double d1 = 1.0D / ((axisalignedbb.maxY - axisalignedbb.minY) * 2.0D + 1.0D); @@ -120,7 +181,11 @@ public class Explosion { double d10 = Mth.lerp(d7, axisalignedbb.minZ, axisalignedbb.maxZ); Vec3 vec3d1 = new Vec3(d8 + d3, d9, d10 + d4); - if (entity.level.clip(new ClipContext(vec3d1, source, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, entity)).getType() == HitResult.Type.MISS) { + // Sakura start + if (data != null && data.isExpandable() && data.has(vec3d1)) { + i += (int) data.density(); + } else if (entity.level.clip(new ClipContext(vec3d1, source, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, entity)).getType() == HitResult.Type.MISS) { + // Sakura end ++i; } @@ -141,34 +206,79 @@ public class Explosion { return; } // CraftBukkit end + // Sakura start + // block at explosion position + BlockState state = level.getBlockState(BlockPos.containing(x, y, z)); + if (interactsWithBlocks() && isDestructibleBlock(state) && isRegionUnprotected()) { + searchForBlocks(); + } + + // Checking if this explosion occurred inside a block is not a viable optimisation. + // If an entity is 1.0e-7 away from the position no matter it will be affected no matter what. + locateAndImpactEntities(); + } + + protected void searchForBlocks() { this.level.gameEvent(this.source, GameEvent.EXPLODE, new Vec3(this.x, this.y, this.z)); Set set = Sets.newHashSet(); boolean flag = true; - int i; - int j; + it.unimi.dsi.fastutil.longs.LongSet found = new it.unimi.dsi.fastutil.longs.LongOpenHashSet(); + BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos(); + net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache) level.getChunkSource(); - for (int k = 0; k < 16; ++k) { - for (i = 0; i < 16; ++i) { - for (j = 0; j < 16; ++j) { - if (k == 0 || k == 15 || i == 0 || i == 15 || j == 0 || j == 15) { - double d0 = (double) ((float) k / 15.0F * 2.0F - 1.0F); - double d1 = (double) ((float) i / 15.0F * 2.0F - 1.0F); - double d2 = (double) ((float) j / 15.0F * 2.0F - 1.0F); - double d3 = Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2); + for (int i = 0; i < BLOCK_RAYCAST_VECTORS.length; i += 3) { + { + { + { + double d0 = BLOCK_RAYCAST_VECTORS[i]; + double d1 = BLOCK_RAYCAST_VECTORS[i + 1]; + double d2 = BLOCK_RAYCAST_VECTORS[i + 2]; - d0 /= d3; - d1 /= d3; - d2 /= d3; float f = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F); double d4 = this.x; double d5 = this.y; double d6 = this.z; + float resistance = 0.0f; + int prevX = Integer.MIN_VALUE, prevY = prevX, prevZ = prevX; + int lastChunkX = Integer.MIN_VALUE, lastChunkZ = lastChunkX; + net.minecraft.world.level.chunk.LevelChunk chunk = null; + for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) { - BlockPos blockposition = BlockPos.containing(d4, d5, d6); - BlockState iblockdata = this.level.getBlockState(blockposition); - if (!iblockdata.isDestroyable()) continue; // Paper + final int blockX = Mth.floor(d4); + final int blockY = Mth.floor(d5); + final int blockZ = Mth.floor(d6); + + d4 += d0; + d5 += d1; + d6 += d2; + + if (blockX == prevX && blockY == prevY && blockZ == prevZ) { + f -= resistance; + continue; + } + + final int chunkX = blockX >> 4; + final int chunkZ = blockZ >> 4; + + if (chunkX != lastChunkX || chunkZ != lastChunkZ) { + chunk = chunkProvider.getChunk(chunkX, chunkZ, true); + + lastChunkX = chunkX; + lastChunkZ = chunkZ; + } + + prevX = blockX; + prevY = blockY; + prevZ = blockZ; + + mutableBlockPos.set(blockX, blockY, blockZ); + + BlockPos blockposition = mutableBlockPos; + BlockState iblockdata = chunk.getBlockState(blockposition); + if (!iblockdata.isDestroyable()) break; // Paper + // Sakura end FluidState fluid = iblockdata.getFluidState(); // Paper if (!this.level.isInWorldBounds(blockposition)) { @@ -181,8 +291,9 @@ public class Explosion { f -= ((Float) optional.get() + 0.3F) * 0.3F; } + resistance = optional.orElse(0.0f); // Sakura if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockposition, iblockdata, f)) { - set.add(blockposition); + found.add(mutableBlockPos.asLong()); // Sakura // Paper start - prevent headless pistons from forming if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) { BlockEntity extension = this.level.getBlockEntity(blockposition); @@ -193,21 +304,25 @@ public class Explosion { } // Paper end } - - d4 += d0 * 0.30000001192092896D; - d5 += d1 * 0.30000001192092896D; - d6 += d2 * 0.30000001192092896D; + // Sakura - move up } } } } } - this.toBlow.addAll(set); + // Sakura start + for (long position : found) { + this.toBlow.add(BlockPos.of(position)); + } + } + + protected void locateAndImpactEntities() { float f2 = this.radius * 2.0F; - i = Mth.floor(this.x - (double) f2 - 1.0D); - j = Mth.floor(this.x + (double) f2 + 1.0D); + int i = Mth.floor(this.x - (double) f2 - 1.0D); + int j = Mth.floor(this.x + (double) f2 + 1.0D); + // Sakura end int l = Mth.floor(this.y - (double) f2 - 1.0D); int i1 = Mth.floor(this.y + (double) f2 + 1.0D); int j1 = Mth.floor(this.z - (double) f2 - 1.0D); @@ -254,7 +369,7 @@ public class Explosion { // Calculate damage separately for each EntityComplexPart double d7part; if (list.contains(entityComplexPart) && (d7part = Math.sqrt(entityComplexPart.distanceToSqr(vec3d)) / f2) <= 1.0D) { - double d13part = (1.0D - d7part) * Explosion.getSeenPercent(vec3d, entityComplexPart); + double d13part = (1.0D - d7part) * Explosion.getSeenPercent(vec3d, entityComplexPart, null); // Sakura entityComplexPart.hurt(this.getDamageSource(), (float) ((int) ((d13part * d13part + d13part) / 2.0D * 7.0D * (double) f2 + 1.0D))); } } @@ -362,6 +477,12 @@ public class Explosion { this.wasCanceled = true; return; } + + // Sakura start + if (!this.level.paperConfig().environment.optimizeExplosions) { + this.level.densityCache.clear(); + } + // Sakura end // CraftBukkit end objectlistiterator = this.toBlow.iterator(); @@ -525,15 +646,22 @@ public class Explosion { private BlockInteraction() {} } // Paper start - Optimize explosions - private float getBlockDensity(Vec3 vec3d, Entity entity) { - if (!this.level.paperConfig().environment.optimizeExplosions) { - return getSeenPercent(vec3d, entity); + // Sakura start + protected float getBlockDensity(Vec3 vec3d, Entity entity) { // Paper - optimise explosions + int key = me.samsuik.sakura.explosion.DensityCache.createKey(entity, vec3d); + me.samsuik.sakura.explosion.DensityCache.Density data = level.densityCache.retrieveCache(key); + + if (data != null && data.has(entity.getBoundingBox(), vec3d)) { + return data.density(); } - CacheKey key = new CacheKey(this, entity.getBoundingBox()); - Float blockDensity = this.level.explosionDensityCache.get(key); - if (blockDensity == null) { - blockDensity = getSeenPercent(vec3d, entity); - this.level.explosionDensityCache.put(key, blockDensity); + + float blockDensity = getSeenPercent(vec3d, entity, data); // Paper - optimise explosions; + + if (data == null || !data.isExpandable() && (blockDensity == 0.0f || blockDensity == 1.0f)) { + level.densityCache.createCache(key, entity, vec3d, blockDensity); + } else if (data.isExpandable() && data.density() == blockDensity) { + data.expand(entity.getBoundingBox(), vec3d); + // Sakura end } return blockDensity; diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java index d1ca193575f531f0cb12e056bfda75b86c3940bb..c62793c50e95e2e9588e20a59a1d8fb70ddf7760 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -289,6 +289,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public final it.unimi.dsi.fastutil.longs.Long2IntMap minimalTNT = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(); // Sakura - visibility api public final me.samsuik.sakura.entity.merge.MergeHistory mergeHistory = new me.samsuik.sakura.entity.merge.MergeHistory(); // Sakura + public final me.samsuik.sakura.explosion.DensityCache densityCache = new me.samsuik.sakura.explosion.DensityCache(); // Sakura protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, RegistryAccess iregistrycustom, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, Supplier sakuraWorldConfigCreator, java.util.concurrent.Executor executor) { // Sakura // Paper - Async-Anti-Xray - Pass executor this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot @@ -1010,7 +1011,15 @@ 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); + // Sakura start + Explosion explosion; + + if (explosionSourceType == ExplosionInteraction.TNT) { + explosion = new me.samsuik.sakura.explosion.SakuraExplosion(this, entity, damageSource, behavior, x, y, z, power, createFire, explosion_effect1); + } else { + explosion = new Explosion(this, entity, damageSource, behavior, x, y, z, power, createFire, explosion_effect1); + } + // Sakura end explosion.explode(); explosion.finalizeExplosion(particles);