9
0
mirror of https://github.com/Samsuik/Sakura.git synced 2025-12-29 03:39:07 +00:00

Half way through feature patches

This commit is contained in:
Samsuik
2025-01-16 23:48:05 +00:00
parent 1e345a18b5
commit 7dbdd69077
25 changed files with 1445 additions and 4 deletions

View File

@@ -0,0 +1,62 @@
package me.samsuik.sakura.explosion.density;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
/**
* This is a replacement for papers explosion density cache to be more lenient and efficient.
*/
public final class BlockDensityCache {
public static final float UNKNOWN_DENSITY = -1.0f;
private final Int2ObjectOpenHashMap<DensityData> densityDataMap = new Int2ObjectOpenHashMap<>();
private DensityData data;
private int key;
private boolean knownSource;
public float getDensity(Vec3 explosion, Entity entity) {
int key = getKey(explosion, entity);
DensityData data = this.densityDataMap.get(key);
if (data != null && data.hasPosition(explosion, entity.getBoundingBox())) {
return data.density();
} else {
this.knownSource = data != null && data.complete() && data.isExplosionPosition(explosion);
this.data = data;
this.key = key;
return UNKNOWN_DENSITY;
}
}
public float getKnownDensity(Vec3 point) {
if (this.knownSource && this.data.isKnownPosition(point)) {
return this.data.density();
} else {
return UNKNOWN_DENSITY;
}
}
public void putDensity(Vec3 explosion, Entity entity, float density) {
if (this.data == null || !this.data.complete()) {
this.densityDataMap.put(this.key, new DensityData(explosion, entity, density));
} else if (this.data.density() == density) {
this.data.expand(explosion, entity);
}
}
public void invalidate() {
this.densityDataMap.clear();
}
private static int getKey(Vec3 explosion, Entity entity) {
int key = Mth.floor(explosion.x());
key = 31 * key + Mth.floor(explosion.y());
key = 31 * key + Mth.floor(explosion.z());
key = 31 * key + entity.getBlockX();
key = 31 * key + entity.getBlockY();
key = 31 * key + entity.getBlockZ();
return key;
}
}

View File

@@ -0,0 +1,47 @@
package me.samsuik.sakura.explosion.density;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
public final class DensityData {
private AABB source;
private AABB known;
private AABB entity;
private final float density;
private final boolean complete;
public DensityData(Vec3 explosion, Entity entity, float density) {
this.source = new AABB(explosion, explosion);
this.known = new AABB(entity.position(), entity.position());
this.entity = entity.getBoundingBox();
this.density = density;
this.complete = Math.abs(density - 0.5f) == 0.5f;
}
public float density() {
return this.density;
}
public boolean complete() {
return this.complete;
}
public boolean hasPosition(Vec3 explosion, AABB entity) {
return this.isExplosionPosition(explosion) && this.entity.isAABBInBounds(entity);
}
public boolean isKnownPosition(Vec3 point) {
return this.entity.isVec3InBounds(point);
}
public boolean isExplosionPosition(Vec3 explosion) {
return this.source.isVec3InBounds(explosion);
}
public void expand(Vec3 explosion, Entity entity) {
this.source = this.source.expand(explosion);
this.known = this.known.expand(entity.position());
this.entity = this.entity.minmax(entity.getBoundingBox());
}
}

View File

@@ -0,0 +1,43 @@
package me.samsuik.sakura.explosion.durable;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import net.minecraft.core.BlockPos;
import java.util.concurrent.TimeUnit;
public final class DurableBlockManager {
private final Cache<BlockPos, DurableBlock> durableBlocks = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.MINUTES)
.maximumSize(65534)
.build();
public boolean damage(BlockPos pos, DurableMaterial material) {
DurableBlock block = this.durableBlocks.getIfPresent(pos);
if (block == null) {
this.durableBlocks.put(pos, block = new DurableBlock(material.durability()));
}
return block.damage();
}
public int durability(BlockPos pos, DurableMaterial material) {
final DurableBlock block = this.durableBlocks.getIfPresent(pos);
return block != null ? block.durability() : material.durability();
}
private static final class DurableBlock {
private int durability;
public DurableBlock(int durability) {
this.durability = durability;
}
public int durability() {
return this.durability;
}
public boolean damage() {
return --this.durability <= 0;
}
}
}

View File

@@ -0,0 +1,203 @@
package me.samsuik.sakura.explosion.special;
import ca.spottedleaf.moonrise.common.util.WorldUtil;
import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
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.level.ExplosionDamageCalculator;
import net.minecraft.world.level.ServerExplosion;
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<T extends Entity> extends ServerExplosion {
private static final double ENTITY_DISPATCH_DISTANCE = Math.pow(32.0, 2.0);
protected final T cause; // preferred over source
private Vec3 impactPosition;
protected final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
private final Consumer<SpecialisedExplosion<T>> applyEffects;
public SpecialisedExplosion(ServerLevel level, T entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, Vec3 center, float power, boolean createFire, BlockInteraction destructionType, Consumer<SpecialisedExplosion<T>> applyEffects) {
super(level, entity, damageSource, behavior, center, power, createFire, destructionType);
this.cause = entity;
this.impactPosition = center;
this.applyEffects = applyEffects;
}
protected double getExplosionOffset() {
return (double) this.cause.getBbHeight() * 0.0625D;
}
protected abstract int getExplosionCount();
protected abstract void startExplosion();
@Override
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 = this.getExplosionCount() - 1; i >= 0; --i) {
this.finalizeExplosionAndParticles(List.of());
}
} else {
this.createBlockCache();
this.startExplosion(); // search for blocks, impact entities, finalise if necessary
this.clearBlockCache();
}
}
protected final boolean requiresImpactEntities(List<BlockPos> blocks, Vec3 center) {
if (this.impactPosition.distanceToSqr(center) > ENTITY_DISPATCH_DISTANCE) {
this.impactPosition = center;
return true;
}
return !blocks.isEmpty();
}
protected final boolean finalizeExplosionAndParticles(List<BlockPos> blocks) {
this.wasCanceled = false;
List<BlockPos> explodedPositions = new ObjectArrayList<>(blocks);
this.interactWithBlocks(explodedPositions);
if (!this.wasCanceled) {
this.applyEffects.accept(this);
this.getHitPlayers().clear();
}
return !explodedPositions.isEmpty();
}
protected void postExplosion(List<BlockPos> 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.directMappedBlockCache, null);
}
protected final void recalculateExplosionPosition() {
this.recalculateExplosionPosition(this.cause);
}
protected final void recalculateExplosionPosition(T entity) {
double x = entity.getX();
double y = entity.getY() + this.getExplosionOffset();
double z = entity.getZ();
this.center = new Vec3(x, y, z);
}
protected final void forEachEntitySliceInBounds(AABB bb, Consumer<Entity[]> 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;
EntityLookup entityLookup = this.level().moonrise$getEntityLookup();
for (int chunkX = minChunkX; chunkX <= maxChunkX; ++chunkX) {
for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; ++chunkZ) {
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 != this.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 (this.excludeSourceFromDamage && entity == this.source) {
return; // for paper api
}
if (entity.isPrimedTNT || entity.isFallingBlock) {
this.impactCannonEntity(entity, pos, potential, radius);
} else {
for (int i = 0; i < potential; ++i) {
super.impactEntity((float) radius, 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()) - 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
double exposure = (1.0D - distanceFromBottom) * density;
if (exposure == 0.0) {
return;
}
x *= exposure;
y *= exposure;
z *= exposure;
this.applyEntityVelocity(entity, x, y, z, potential);
}
}
}
protected final void applyEntityVelocity(Entity entity, double x, double y, double z, int potential) {
Vec3 movement = entity.getDeltaMovement();
double moveX = movement.x();
double moveY = movement.y();
double moveZ = movement.z();
for (int i = 0; i < potential; ++i) {
moveX += x;
moveY += y;
moveZ += z;
}
entity.setDeltaMovement(moveX, moveY, moveZ);
}
}

View File

@@ -0,0 +1,201 @@
package me.samsuik.sakura.explosion.special;
import ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import me.samsuik.sakura.entity.EntityState;
import me.samsuik.sakura.entity.merge.MergeLevel;
import me.samsuik.sakura.entity.merge.MergeableEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
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.phys.AABB;
import net.minecraft.world.phys.Vec3;
import org.bukkit.craftbukkit.util.CraftVector;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.function.Consumer;
public final class TntExplosion extends SpecialisedExplosion<PrimedTnt> {
private static final int ALL_DIRECTIONS = 0b111;
private static final int FOUND_ALL_BLOCKS = ALL_DIRECTIONS + 12;
private final Vec3 originalPosition;
private final List<Vec3> explosions = new ObjectArrayList<>();
private AABB bounds;
private int wrapped = 0;
private boolean moved = false;
public TntExplosion(ServerLevel level, PrimedTnt tnt, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, Vec3 center, float power, boolean createFire, BlockInteraction destructionType, Consumer<SpecialisedExplosion<PrimedTnt>> applyEffects) {
super(level, tnt, damageSource, behavior, center, power, createFire, destructionType, applyEffects);
this.originalPosition = center;
this.bounds = new AABB(center, center);
}
@Override
protected int getExplosionCount() {
if (this.cause.getMergeEntityData().getMergeLevel() == MergeLevel.NONE) {
this.mergeEntitiesBeforeExplosion();
}
return this.cause.getMergeEntityData().getCount();
}
@Override
protected void startExplosion() {
for (int i = this.getExplosionCount() - 1; i >= 0; --i) {
Vec3 previousMomentum = this.cause.entityState().momentum();
boolean lastCycle = i == 0;
List<BlockPos> toBlow = this.midExplosion(previousMomentum, lastCycle); // search for blocks and impact entities
boolean destroyedBlocks = this.finalizeExplosionAndParticles(toBlow);
if (!lastCycle) {
EntityState entityState = this.nextSourceVelocity();
this.postExplosion(toBlow, destroyedBlocks); // update wrapped, clear recent block cache
this.updateExplosionPosition(entityState, destroyedBlocks);
}
}
}
private List<BlockPos> midExplosion(Vec3 previousMomentum, boolean lastCycle) {
final List<BlockPos> explodedPositions;
if (this.wrapped < FOUND_ALL_BLOCKS) {
explodedPositions = this.calculateExplodedPositions();
} else {
explodedPositions = List.of();
}
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.center);
this.explosions.add(this.center);
if (lastCycle || this.requiresImpactEntities(explodedPositions, this.center)) {
this.locateAndImpactEntitiesInBounds();
this.bounds = new AABB(this.center, this.center);
this.explosions.clear();
}
return explodedPositions;
}
@Override
protected void postExplosion(List<BlockPos> foundBlocks, boolean destroyedBlocks) {
super.postExplosion(foundBlocks, destroyedBlocks);
// Update wrapped, this is for tracking swinging and if blocks are found
if (this.wrapped >= ALL_DIRECTIONS) {
if (!destroyedBlocks && this.level().sakuraConfig().cannons.explosion.avoidRedundantBlockSearches) {
this.wrapped++;
} else {
this.wrapped = ALL_DIRECTIONS;
}
}
}
private Vector getCauseOrigin() {
Vector origin = this.cause.getOriginVector();
return origin == null ? CraftVector.toBukkit(this.center) : 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.center, 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.center.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.center.equals(this.originalPosition);
this.recalculateExplosionPosition();
}
}
private void mergeEntitiesBeforeExplosion() {
IteratorSafeOrderedReferenceSet<Entity> entities = this.level().entityTickList.entities;
int index = entities.indexOf(this.cause);
entities.createRawIterator();
// 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 instanceof MergeableEntity mergeEntity) || foundEntity.isRemoved() || !foundEntity.compareState(this.cause) || !mergeEntity.isSafeToMergeInto(this.cause, true))
break;
this.level().mergeHandler.mergeEntity(mergeEntity, this.cause);
}
entities.finishRawIterator();
}
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.getFirst(), 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);
}
}
}