From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Samsuik <40902469+Samsuik@users.noreply.github.com> Date: Sat, 9 Sep 2023 18:39:15 +0100 Subject: [PATCH] Merge Cannon Entities diff --git a/src/main/java/me/samsuik/sakura/entity/merge/MergeHistory.java b/src/main/java/me/samsuik/sakura/entity/merge/MergeHistory.java new file mode 100644 index 0000000000000000000000000000000000000000..7d899163b308f52b48bf72a1e9cd38d565bbe05d --- /dev/null +++ b/src/main/java/me/samsuik/sakura/entity/merge/MergeHistory.java @@ -0,0 +1,56 @@ +package me.samsuik.sakura.entity.merge; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.Entity; + +import java.util.List; + +public final class MergeHistory { + + // packed position -> known merging information + private final Long2ObjectMap mergeDataMap = new Long2ObjectOpenHashMap<>(); + private SpawnPositionData mergeData = null; + + public SpawnPositionData retrievePositions(Entity entity) { + long origin = entity.getPackedOrigin(); + + if (mergeData != null && mergeData.isPositionKnown(origin)) { + return mergeData; + } + + return mergeData = mergeDataMap.get(origin); + } + + public void markPositions(Entity entity) { + List mergeList = entity.getMergeList(); + long origin = entity.getPackedOrigin(); + + // After a merged entity has been discarded store all the position data + SpawnPositionData data = mergeDataMap.computeIfAbsent(origin, p -> new SpawnPositionData(mergeList)); + data.getExpiry().refresh(MinecraftServer.currentTickLong); + + // Collect all merge positions + LongOpenHashSet positions = new LongOpenHashSet((mergeList.size() + 1) / 2); + positions.add(entity.getPackedOrigin()); + + for (Entity mergedEntity : mergeList) { + positions.add(mergedEntity.getPackedOrigin()); + } + + // Retain existing positions or insert new positions + data.retainOrInsertPositions(positions); + } + + public void expire(long tick) { + mergeData = null; // clear this every tick + + // only expire every 20 ticks + if (tick % 20 != 0) return; + + mergeDataMap.values().removeIf(data -> data.getExpiry().isExpired(tick)); + } + +} diff --git a/src/main/java/me/samsuik/sakura/entity/merge/MergeThreshold.java b/src/main/java/me/samsuik/sakura/entity/merge/MergeThreshold.java new file mode 100644 index 0000000000000000000000000000000000000000..4d08af1a7ff0ebc2b1198513c86c08087d9d6e89 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/entity/merge/MergeThreshold.java @@ -0,0 +1,26 @@ +package me.samsuik.sakura.entity.merge; + +/** + * Threshold for AOT/on spawn merging. + *

+ * This is determined by the amount of spawn attempts over time. + */ +public final class MergeThreshold { + + private final long startingTick; + private final int thresholdAttempts; + private final long thresholdAge; + private int attempts; + + public MergeThreshold(long tick, int attempts, long age) { + startingTick = tick; + thresholdAttempts = attempts; + thresholdAge = age; + } + + public boolean hasPassed(long tick) { + return ++attempts >= thresholdAttempts + || tick - startingTick >= thresholdAge; + } + +} diff --git a/src/main/java/me/samsuik/sakura/entity/merge/SpawnPositionData.java b/src/main/java/me/samsuik/sakura/entity/merge/SpawnPositionData.java new file mode 100644 index 0000000000000000000000000000000000000000..e63935c17e213bf60571d120ad9ce311b5249d45 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/entity/merge/SpawnPositionData.java @@ -0,0 +1,64 @@ +package me.samsuik.sakura.entity.merge; + +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import me.samsuik.sakura.utils.collections.EntityTable; +import me.samsuik.sakura.utils.objects.Expiry; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.Entity; + +import java.util.List; + +/** + * Contains all the positions that past entities of the same origin have merged with. + */ +public final class SpawnPositionData { + + private final LongSet knownPositions = new LongOpenHashSet(); + private final LongSet retainedPositions = new LongOpenHashSet(); + + private final EntityTable table; + private final Expiry expiry = new Expiry(MinecraftServer.currentTickLong, 200); + private final MergeThreshold threshold = new MergeThreshold(MinecraftServer.currentTickLong, 12, 200); + + public SpawnPositionData(List mergeList) { + this.table = new EntityTable(mergeList.size()); + } + + public Expiry getExpiry() { + return expiry; + } + + public void retainOrInsertPositions(LongOpenHashSet positions) { + if (!knownPositions.isEmpty()) { + retainedPositions.addAll(positions); + } else { + retainedPositions.retainAll(positions); + } + + knownPositions.addAll(positions); + } + + public boolean isPositionKnown(long pos) { + return knownPositions.contains(pos); + } + + public boolean isPositionRetained(long pos) { + return retainedPositions.contains(pos); + } + + public boolean isAbleToOnSpawnMerge() { + return threshold.hasPassed(MinecraftServer.currentTickLong); + } + + public Entity findFirstEntityInSamePosition(Entity entity) { + Entity found = table.locate(entity); + + if (found != null && found.getId() < entity.getId() && knownPositions.contains(found.getPackedOrigin()) && !found.isRemoved() && entity.compareState(found)) { + return found; + } + + return null; + } + +} diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 3c98362624e1b02b6b028db95564254e044c7b0d..f78881fcf186c44da243c2f31ff855b878e172cf 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1748,6 +1748,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { if (!entity.isRemoved()) { if (false && this.shouldDiscardEntity(entity)) { // CraftBukkit - We prevent spawning in general, so this butchering is not needed @@ -912,6 +913,15 @@ public class ServerLevel extends Level implements WorldGenLevel { entity.stopRiding(); } + // Sakura start + Entity previous = previousEntity[0]; + if (entity.isMergeableType(previous) && entity.tryMergeInto(previous)) { + return; + } else { + previousEntity[0] = entity; + } + // Sakura end + gameprofilerfiller.push("tick"); this.guardEntityTick(this::tickNonPassenger, entity); gameprofilerfiller.pop(); diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java index e657b5f88195f1db2cb9a9a97f255ff23992cadd..5e155cdf66701f96dd0b831603c714b5a47d74c0 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -603,6 +603,116 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S return to.entityState() != null && to.entityState().isCurrentState(this); } // Sakura end - store entity data/state + // Sakura start - cannon entity merging + // List of merged entities, should be naturally sorted (oldest -> youngest) + private final List mergeList = new java.util.ArrayList<>(1); + private @Nullable me.samsuik.sakura.entity.merge.SpawnPositionData originData = null; + private me.samsuik.sakura.entity.merge.MergeLevel mergeLevel; + protected int stacked = 1; // default + + public final me.samsuik.sakura.entity.merge.MergeLevel getMergeLevel() { + return mergeLevel; + } + + public final void setMergeLevel(me.samsuik.sakura.entity.merge.MergeLevel level) { + mergeLevel = level; + } + + public final int getStacked() { + return stacked; + } + + public final void setStacked(int stack) { + stacked = stack; + } + + public final List getMergeList() { + return mergeList; + } + + public final long getPackedOrigin() { + var v = getOriginVector(); + if (v == null) return Long.MIN_VALUE; + // Note: vector#getBlockN may not be 100% exact + // If there's any future issues at let's say 0.999999... + // giving an incorrect position change it to Mth instead. + return BlockPos.asLong(v.getBlockX(), v.getBlockY(), v.getBlockZ()); + } + + private boolean isSafeToSpawnMerge(Entity entity) { + return tickCount == 1 && originData != null + && originData.isAbleToOnSpawnMerge() // on spawn safety delay has passed + && originData == entity.originData // make sure it's the same group + && originData.isPositionRetained(entity.getPackedOrigin()); + } + + public boolean isMergeableType(@Nullable Entity previous) { + return false; + } + + public final boolean tryMergeInto(@Nullable Entity entity) { + if (mergeLevel.atLeast(me.samsuik.sakura.entity.merge.MergeLevel.LENIENT) && tickCount == 0) { + originData = level.mergeHistory.retrievePositions(this); + } + + Entity mergeEntity = null; + + if (entity == null || entity.getType() != getType()) { + return false; // First entity + } else if (mergeLevel.atLeast(me.samsuik.sakura.entity.merge.MergeLevel.SPAWN) && entity.isSafeToSpawnMerge(this)) { + // "SPAWN" merges entities one gametick after they have spawned. Merging is + // only possible after it has been established that the entity is safe to + // merge by collecting information on the entities that merge together over time. + mergeEntity = entity; + } else { + // "STRICT" merges entities with the same properties, position, momentum and OOE. + // This is considered safe to use, and will not break cannon mechanics. + if (mergeLevel.atLeast(me.samsuik.sakura.entity.merge.MergeLevel.STRICT) && compareState(entity)) { + mergeEntity = entity; + } + + // "LENIENT" merges entities aggressively by tracking the entities that have + // previously merged. This is a hybrid of "SPAWN" and "STRICT" merging, with the + // visuals of "STRICT" merging and better merging potential of "SPAWN" merging. + if (mergeLevel.atLeast(me.samsuik.sakura.entity.merge.MergeLevel.LENIENT) && mergeEntity == null && originData != null) { + mergeEntity = originData.findFirstEntityInSamePosition(this); + } + } + + if (mergeEntity != null && isSafeToMergeInto(mergeEntity)) { + mergeInto(mergeEntity); + return true; + } + + return false; + } + + protected boolean respawnMerged() { + return false; + } + + public boolean isSafeToMergeInto(Entity entity) { + return false; + } + + private void mergeInto(Entity entity) { + entity.mergeList.add(this); + entity.mergeList.addAll(mergeList); + entity.stacked += stacked; + + mergeList.clear(); // clear the list to stop our tracking when merging + stacked = 0; // prevent any possible duplication + + discard(null); // MERGE is appropriate here but plugins may not expect tnt or falling blocks to merge + + // update api handle, this is so cannondebug can function + //noinspection ConstantValue + if (bukkitEntity != null) { + bukkitEntity.setHandle(entity); + bukkitEntity = entity.bukkitEntity; + } + } + // Sakura end - cannon entity merging public Entity(EntityType type, Level world) { this.id = Entity.ENTITY_COUNTER.incrementAndGet(); @@ -651,6 +761,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S this.getEntityData().registrationLocked = true; // Spigot this.setPos(0.0D, 0.0D, 0.0D); this.eyeHeight = this.getEyeHeight(net.minecraft.world.entity.Pose.STANDING, this.dimensions); + this.mergeLevel = level.sakuraConfig().cannons.mergeLevel; // Sakura } public boolean isColliding(BlockPos pos, BlockState state) { @@ -2533,6 +2644,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S nbttagcompound.putBoolean("Paper.FreezeLock", true); } // Paper end + // Sakura start + if (stacked > 0) { + nbttagcompound.putInt("Sakura.Stacked", stacked); + } + // Sakura end return nbttagcompound; } catch (Throwable throwable) { CrashReport crashreport = CrashReport.forThrowable(throwable, "Saving entity NBT"); @@ -2680,6 +2796,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S freezeLocked = nbt.getBoolean("Paper.FreezeLock"); } // Paper end + // Sakura start + if (nbt.contains("Sakura.Stacked")) { + stacked = nbt.getInt("Sakura.Stacked"); + } + // Sakura end } catch (Throwable throwable) { CrashReport crashreport = CrashReport.forThrowable(throwable, "Loading entity NBT"); @@ -4889,6 +5010,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S // Paper end - rewrite chunk system CraftEventFactory.callEntityRemoveEvent(this, cause); // CraftBukkit end + // Sakura start + if (entity_removalreason == RemovalReason.DISCARDED && !mergeList.isEmpty()) { + level.mergeHistory.markPositions(this); + } + // Sakura end final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers if (this.removalReason == null) { this.removalReason = entity_removalreason; diff --git a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java index 743aac4ba5d08ef3e6b67136bd4919b62411a7a0..99a0bec7eb5be527b41248b365b037a5e42a3270 100644 --- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java +++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java @@ -133,6 +133,59 @@ public class FallingBlockEntity extends Entity { return !this.isRemoved(); } + // Sakura start - cannon entity merging + @Override + public boolean isMergeableType(@Nullable Entity previous) { + return previous == null || !isRemoved() && !previous.isRemoved(); + } + + @Override + public boolean isSafeToMergeInto(Entity entity) { + return entity instanceof FallingBlockEntity fbe + && fbe.blockState.equals(blockState) + && fbe.time - 1 == time; // todo: special case in case on spawn isn't used + } + + @Override + protected boolean respawnMerged() { + if (stacked <= 1) return false; + + while (stacked-- >= 1) { + // Unlike PrimedTnt we have to try respawn each stacked entity + FallingBlockEntity fallingBlock = new FallingBlockEntity(EntityType.FALLING_BLOCK, level()); + + // Try to stack the falling block + this.entityState().apply(fallingBlock); + fallingBlock.spawnReason = spawnReason; + fallingBlock.time = time - 1; + fallingBlock.tick(); + + // If you horizontal stack into a moving piston block this condition will be met. + if (!fallingBlock.isRemoved()) { + stacked++; + fallingBlock.storeEntityState(); + fallingBlock.entityState().apply(this); + break; + } else if (stacked == 0) { + this.discard(EntityRemoveEvent.Cause.DESPAWN); + } + } + + return true; + } + + @Nullable + public ItemEntity spawnAtLocation(ItemLike item) { + ItemEntity itemEntity = null; + + while (stacked-- >= 1) { + itemEntity = super.spawnAtLocation(item); + } + + return itemEntity; + } + // Sakura end + @Override public void tick() { if (this.blockState.isAir()) { @@ -200,6 +253,7 @@ public class FallingBlockEntity extends Entity { return; } // CraftBukkit end + if (this.respawnMerged()) return; // Sakura if (this.level().setBlock(blockposition, this.blockState, 3)) { ((ServerLevel) this.level()).getChunkSource().chunkMap.broadcast(this, new ClientboundBlockUpdatePacket(blockposition, this.level().getBlockState(blockposition))); this.discard(EntityRemoveEvent.Cause.DESPAWN); 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 90f10473ae441d68333cd497c718a3c982544533..4f695305794c2564517d99b4edd3180d7ea07845 100644 --- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java +++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java @@ -72,6 +72,44 @@ public class PrimedTnt extends Entity implements TraceableEntity { return !this.isRemoved(); } + // Sakura start - cannon entity merging + @Override + public boolean isMergeableType(@Nullable Entity previous) { + return previous == null || !isRemoved() && !previous.isRemoved(); + } + + @Override + public boolean isSafeToMergeInto(Entity entity) { + return entity instanceof PrimedTnt tnt + && tnt.getFuse() + 1 == getFuse() + // required to prevent issues with powdered snow + && (tnt.entityState().fallDistance() == fallDistance + || tnt.entityState().fallDistance() > 2.5f && fallDistance > 2.5f); + } + + @Override + protected boolean respawnMerged() { + if (stacked <= 1) return false; + + PrimedTnt tnt = new PrimedTnt(EntityType.TNT, level()); + + while (stacked-- > 1) { + this.setFuse(100); // Prevent unwanted explosions while ticking + + // Cause an explosion to affect this entity + tnt.setPos(this.position()); + tnt.setDeltaMovement(this.getDeltaMovement()); + this.entityState().apply(this); + tnt.explode(); + this.storeEntityState(); + + this.tick(); + } + + return true; + } + // Sakura end + @Override public void tick() { if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().spigotConfig.currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot @@ -97,6 +135,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { if (i <= 0) { // CraftBukkit start - Need to reverse the order of the explosion and the entity death so we have a location for the event // this.discard(); + this.respawnMerged(); // Sakura if (!this.level().isClientSide) { this.explode(); } diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java index 684fd53a34fae43cd916a6f0a5cf38a2505d9bfe..9507b2419834ace4f71a781ad90284af5e4d8aa1 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -229,6 +229,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public abstract ResourceKey getTypeKey(); 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 - cannon entity merging 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 - create paper world config; Async-Anti-Xray: Pass executor this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java index 1359d25a32b4a5d5e8e68ce737bd19f7b5afaf69..0afa2cfb04b5097788927076669e85fe24041df9 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java @@ -14,6 +14,28 @@ public class CraftFallingBlock extends CraftEntity implements FallingBlock { super(server, entity); } + // Sakura start + @Override + public @org.jetbrains.annotations.NotNull me.samsuik.sakura.entity.merge.MergeLevel getMergeLevel() { + return getHandle().getMergeLevel(); + } + + @Override + public void setMergeLevel(@org.jetbrains.annotations.NotNull me.samsuik.sakura.entity.merge.MergeLevel level) { + getHandle().setMergeLevel(level); + } + + @Override + public int getStacked() { + return getHandle().getStacked(); + } + + @Override + public void setStacked(int stacked) { + getHandle().setStacked(stacked); + } + // Sakura end + @Override public FallingBlockEntity getHandle() { return (FallingBlockEntity) this.entity; diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java index dac3d34677688ac560bc1be2087a08479ef71b87..3e80513263236d56019e3402c52f4a3677c83c76 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java @@ -12,6 +12,28 @@ public class CraftTNTPrimed extends CraftEntity implements TNTPrimed { super(server, entity); } + // Sakura start + @Override + public @org.jetbrains.annotations.NotNull me.samsuik.sakura.entity.merge.MergeLevel getMergeLevel() { + return getHandle().getMergeLevel(); + } + + @Override + public void setMergeLevel(@org.jetbrains.annotations.NotNull me.samsuik.sakura.entity.merge.MergeLevel level) { + getHandle().setMergeLevel(level); + } + + @Override + public int getStacked() { + return getHandle().getStacked(); + } + + @Override + public void setStacked(int stacked) { + getHandle().setStacked(stacked); + } + // Sakura end + @Override public float getYield() { return this.getHandle().yield;