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..2749e740d043ecaf8cca78c4527a8aab51548611 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/explosion/SakuraExplosion.java @@ -0,0 +1,383 @@ +package me.samsuik.sakura.explosion; + +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.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.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() { + PrimedTnt origin = (PrimedTnt) source; + List positions = new ArrayList<>(origin.getStacked()); + + // This is a temporary entity that will be used for movement. + PrimedTnt tnt = new PrimedTnt(level, 0, 0, 0, null); + AABB bounds = new AABB(x, y, z, x, y, z); + + origin.entityState().apply(tnt); + + Vec3 lastMovement = tnt.getDeltaMovement(); + ExplosionBlockCache[] blockCache = createBlockCache(); + int wrapped = 0; + + for (int i = 0; i < origin.getStacked() && !wasCanceled; ++i) { + if (i > 0) { + updatePosition(origin, tnt); + } + + // block at explosion position + int blockX = Mth.floor(x); + int blockY = Mth.floor(y); + int blockZ = Mth.floor(z); + Vec3 position = new Vec3(x, y, z); + + // search for blocks if necessary + if (wrapped < 7 + 12) { + getToBlow().clear(); + + long key = BlockPos.asLong(blockX, blockY, blockZ); + ExplosionBlockCache center = getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); + + if (interactsWithBlocks() && isDestructibleBlock(center.blockState) && isRegionUnprotected()) { + searchForBlocks(blockCache); + } + } + + // keep track of positions and bounds + positions.add(position); + bounds = bounds.minmax(new AABB(position, position)); + + Vec3 movement = tnt.getDeltaMovement(); + + if (wrapped < 7) { + // Check if the explosion has wrapped around with swinging on each axis + 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; + } else if (getToBlow().isEmpty() && level.sakuraConfig().cannons.explosion.avoidRedundantBlockSearches) { + wrapped++; + } else { + wrapped = 7; + } + + lastMovement = tnt.getDeltaMovement(); + + if (i + 1 < origin.getStacked()) { + BlockPos.MutableBlockPos mbp = new BlockPos.MutableBlockPos(); + impactEntityIdle(tnt, new Entity[0], position, 1, radius * 2.0f, mbp, blockCache); + + // The purpose of this is to make sure papers blockCache doesn't become + // outdated by flushing the map and removing stale entries from the recent + // cache array. If there is any case where tnt can provide its self delta + // movement and then start moving without blocks this may break stuff to + // fix it see the note above or add a boolean to mark the cache as dirty + // outside this loop and then invalidate before the final impact entities. + if (!getToBlow().isEmpty() && tnt.getDeltaMovement().lengthSqr() <= 64.0) { + invalidateBlockCache(blockCache); + } + + // could it be viable to have a configuration option to only + // call finalize explosion when blocks are found + // may affect plugins that need exact explosion positions + super.finalizeExplosion(false); + ((ServerLevel) level).notifyPlayersOfExplosion(x, y, z, radius, this); + } else { + locateAndImpactEntities(positions, bounds, blockCache); + } + } + + clearBlockCache(); + } + + private void updatePosition(PrimedTnt origin, PrimedTnt tnt) { + boolean originMoved = !origin.position().equals(tnt.position()); + + origin.setFuse(100); + tnt.storeEntityState(); + tnt.entityState().apply(origin); + + // We have to check delta movement otherwise this optimisation can break reversing tnt. + // If origin was shot upwards to a block then falls in the explosion tick it will swing + // and origin and tnt will be in the same position every other tnt while swinging. + if (!getToBlow().isEmpty() || tnt.getDeltaMovement().lengthSqr() <= 64.0 || originMoved) { + origin.tick(); + } + + // update explosion position + x = origin.getX(); + y = origin.getY(0.0625); + z = origin.getZ(); + } + + private void locateAndImpactEntities(List positions, AABB bb, ExplosionBlockCache[] blockCache) { + 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; + + BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos(); // Paper - optimise explosions + + 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, blockPos, blockCache, radius, maxDistanceSqr); + } else { + impactEntitiesIdle(chunk.getSectionEntities(chunkY), positions.get(0), positions.size(), blockPos, blockCache, radius); + } + } + } + } + } + + // swinging case: more than 1 position and actively changing positions. + private void impactEntities(Entity[] entities, List positions, Vec3 center, BlockPos.MutableBlockPos blockPos, ExplosionBlockCache[] blockCache, 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, blockPos, blockCache) == 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, BlockPos.MutableBlockPos blockPos, ExplosionBlockCache[] blockCache) { + 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, blockCache, blockPos); // 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; + } + + impactNonLiving(entity, pos, section, x, y, z, exposure, blockPos, blockCache); + } + + return found; + } + + // stationary case: 1 position or stationary + private void impactEntitiesIdle(Entity[] entities, Vec3 position, int potential, BlockPos.MutableBlockPos blockPos, ExplosionBlockCache[] blockCache, 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, blockPos, blockCache); + } + + // 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, BlockPos.MutableBlockPos blockPos, ExplosionBlockCache[] blockCache) { + 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, blockCache, blockPos); // 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) { + impactNonLiving(entity, pos, section, x, y, z, exposure, blockPos, blockCache); + } + } + } + } + } + + private void impactNonLiving(Entity entity, Vec3 pos, Entity[] section, double d8, double d9, double d10, double d13, BlockPos.MutableBlockPos blockPos, ExplosionBlockCache[] blockCache) { + // 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) { + if (ent == null) break; + // Calculate damage separately for each EntityComplexPart + double d7part; + if (ent == entityComplexPart && (d7part = Math.sqrt(entityComplexPart.distanceToSqr(pos)) / radius) <= 1.0D) { + double d13part = (1.0D - d7part) * this.getSeenFraction(pos, entityComplexPart, null, blockCache, blockPos); // Sakura // Paper - optimise explosions + entityComplexPart.hurt(this.getDamageSource(), (float) ((int) ((d13part * d13part + d13part) / 2.0D * 7.0D * (double) radius + 1.0D))); + } + } + } + } else { + entity.hurt(this.getDamageSource(), (float) ((int) ((d13 * d13 + d13) / 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 d14; + + if (entity instanceof LivingEntity) { + LivingEntity entityliving = (LivingEntity) entity; + + d14 = entity instanceof Player && level.paperConfig().environment.disableExplosionKnockback ? 0 : ProtectionEnchantment.getExplosionKnockbackAfterDampener(entityliving, d13); // Paper - disable explosion knockback + } else { + d14 = d13; + } + + d8 *= d14; + d9 *= d14; + d10 *= d14; + + // Sakura start - reduce deltamovement allocations + entity.addDeltaMovement(d8, d9, d10); + if (entity instanceof Player) { + Vec3 vec3d1 = new Vec3(d8, d9, d10); + // Sakura end + 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 457ccc9934fc6563a1b260ec9d12f0c875a4bd37..d2b12606c4bd7edec4061f67e79bdedcee147fca 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1581,6 +1581,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 2.5f && fallDistance > 2.5f); } + /* @Override protected void respawn() { if (stacked <= 1) return; @@ -115,6 +116,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { tnt.discard(); } + */ // 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 1b335111bd9eb90bbda87225b740768705f26193..11ce5591f5f7eb487323e2c828218af2461fca09 100644 --- a/src/main/java/net/minecraft/world/level/Explosion.java +++ b/src/main/java/net/minecraft/world/level/Explosion.java @@ -56,12 +56,14 @@ 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 - final private -> protected + protected double x; + protected double y; + protected double z; + // Sakura end @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 ObjectArrayList toBlow; @@ -122,6 +124,12 @@ public class Explosion { } } + // Sakura start + if (me.samsuik.sakura.configuration.GlobalConfiguration.get().cannons.explosion.reducedSearchRays) { + me.samsuik.sakura.utils.ExplosionUtil.reduceRays(rayCoords); + } + // Sakura end + CACHED_RAYS = rayCoords.toDoubleArray(); } @@ -129,14 +137,14 @@ public class Explosion { private static final int CHUNK_CACHE_MASK = (1 << CHUNK_CACHE_SHIFT) - 1; private static final int CHUNK_CACHE_WIDTH = 1 << CHUNK_CACHE_SHIFT; - private static final int BLOCK_EXPLOSION_CACHE_SHIFT = 3; - private static final int BLOCK_EXPLOSION_CACHE_MASK = (1 << BLOCK_EXPLOSION_CACHE_SHIFT) - 1; + protected static final int BLOCK_EXPLOSION_CACHE_SHIFT = 3; // Sakura - protected + protected static final int BLOCK_EXPLOSION_CACHE_MASK = (1 << BLOCK_EXPLOSION_CACHE_SHIFT) - 1; // Sakura - protected private static final int BLOCK_EXPLOSION_CACHE_WIDTH = 1 << BLOCK_EXPLOSION_CACHE_SHIFT; // 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 - protected public static final class ExplosionBlockCache { @@ -163,7 +171,7 @@ 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, + protected 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) { @@ -313,7 +321,8 @@ public class Explosion { } } - private float getSeenFraction(final Vec3 source, final Entity target, + protected float getSeenFraction(final Vec3 source, final Entity target, // Sakura - protected + final @Nullable me.samsuik.sakura.explosion.DensityCache.Density data, // Sakura final ExplosionBlockCache[] blockCache, final BlockPos.MutableBlockPos blockPos) { final AABB boundingBox = target.getBoundingBox(); @@ -351,7 +360,11 @@ public class Explosion { Math.fma(dz, diffZ, offZ) ); - if (!this.clipsAnything(from, source, context, blockCache, blockPos)) { + // Sakura start + if (data != null && data.isExpandable() && data.has(from)) { + missedRays += (int) data.density(); + } else if (!this.clipsAnything(from, source, context, blockCache, blockPos)) { + // Sakura end ++missedRays; } } @@ -361,12 +374,72 @@ public class Explosion { return (float)missedRays / (float)totalRays; } // Paper end - optimise collisions + // Sakura start + protected 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 void clearBlockCache() { + this.blockCache = null; // Paper - optimise explosions + this.chunkPosCache = null; // Paper - optimise explosions + this.chunkCache = null; // Paper - optimise explosions + } + + protected void invalidateBlockCache(ExplosionBlockCache[] blockCaches) { + for (BlockPos blow : getToBlow()) { + final int cacheKey = + (blow.getX() & BLOCK_EXPLOSION_CACHE_MASK) | + (blow.getY() & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | + (blow.getZ() & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); + + blockCaches[cacheKey] = null; + } + + blockCache.clear(); + } + + protected boolean isDestructibleBlock(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) <= power; + } + + protected boolean isRegionUnprotected() { + // check if there is a plugin cancelling or clearing the block list. + // for our use case on factions and cannon servers this is a relatively + // sane optimisation, this may not be the case for other servers. + if (source != null && level.sakuraConfig().cannons.explosion.optimiseProtectedRegions) { + Location location = new Location(level.getWorld(), x, y, z); + java.util.ArrayList blocks = new java.util.ArrayList<>(1); + blocks.add(location.getBlock()); + EntityExplodeEvent event = new EntityExplodeEvent(source.getBukkitEntity(), location, blocks, 0.0f); + event.callEvent(); + return !event.isCancelled() && !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 float getSeenPercent(Vec3 source, Entity entity, me.samsuik.sakura.explosion.DensityCache.Density data) { // Sakura - protected 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); @@ -386,7 +459,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; } @@ -407,7 +484,29 @@ public class Explosion { return; } // CraftBukkit end + // Sakura start + final ExplosionBlockCache[] blockCache = createBlockCache(); + + // block at explosion position + int blockX = Mth.floor(x); + int blockY = Mth.floor(y); + int blockZ = Mth.floor(z); + long key = BlockPos.asLong(blockX, blockY, blockZ); + ExplosionBlockCache center = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); + + if (interactsWithBlocks() && isDestructibleBlock(center.blockState) && isRegionUnprotected()) { + searchForBlocks(blockCache); + } + + // 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(blockCache); + clearBlockCache(); + } + + protected void searchForBlocks(ExplosionBlockCache[] blockCache) { this.level.gameEvent(this.source, GameEvent.EXPLODE, new Vec3(this.x, this.y, this.z)); + // Sakura end Set set = Sets.newHashSet(); boolean flag = true; @@ -415,14 +514,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]; + // Sakura - move up // use initial cache value that is most likely to be used: the source position final ExplosionBlockCache initialCache; { @@ -521,10 +613,15 @@ public class Explosion { } this.toBlow.addAll(set); + // Sakura start + } + + protected void locateAndImpactEntities(ExplosionBlockCache[] blockCache) { 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); @@ -574,7 +671,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) * this.getSeenFraction(vec3d, entityComplexPart, blockCache, blockPos); // Paper - optimise explosions + double d13part = (1.0D - d7part) * this.getSeenFraction(vec3d, entityComplexPart, null, blockCache, blockPos); // Sakura // Paper - optimise explosions entityComplexPart.hurt(this.getDamageSource(), (float) ((int) ((d13part * d13part + d13part) / 2.0D * 7.0D * (double) f2 + 1.0D))); } } @@ -617,9 +714,7 @@ public class Explosion { } } - this.blockCache = null; // Paper - optimise explosions - this.chunkPosCache = null; // Paper - optimise explosions - this.chunkCache = null; // Paper - optimise explosions + // Sakura - move up } @@ -683,6 +778,12 @@ public class Explosion { this.toBlow.add(coords); } + // Sakura start + if (!level.paperConfig().environment.optimizeExplosions) { + level.densityCache.clear(); + } + // Sakura end + if (cancelled) { this.wasCanceled = true; return; @@ -850,15 +951,22 @@ public class Explosion { private BlockInteraction() {} } // Paper start - Optimize explosions - private float getBlockDensity(Vec3 vec3d, Entity entity, ExplosionBlockCache[] blockCache, BlockPos.MutableBlockPos blockPos) { // Paper - optimise explosions - if (!this.level.paperConfig().environment.optimizeExplosions) { - return this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise explosions + // Sakura start + protected float getBlockDensity(Vec3 vec3d, Entity entity, ExplosionBlockCache[] blockCache, BlockPos.MutableBlockPos blockPos) { // 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 = this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise explosions; - this.level.explosionDensityCache.put(key, blockDensity); + + float blockDensity = this.getSeenFraction(vec3d, entity, data, blockCache, blockPos); // 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 c7defb4555edd792c83ec001c8dfbf604376b190..3e1271d07883f05fff21d0ec6088fe42a579a706 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -235,6 +235,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { } // Sakura end 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(); 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 @@ -1413,7 +1414,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);