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 321bd1d6c2cbf8384b08708abb837ed3ad4a0078..1358f800a29ef15163442331100816bb418ae3f6 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1567,6 +1567,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 @@ -727,6 +728,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 106e03e0eae24f47a49d2495e98cb30195b27013..c34e4077ba1c8768d3ed8f32a14df46a32009ab8 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -565,6 +565,119 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { 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 + + this.discard(); // MERGE is appropriate here but plugins may not expect tnt or falling blocks to merge + this.updateEntityHandle(entity); + } + + public final void updateEntityHandle(Entity entity) { + // update api handle, this is so cannondebug can function + //noinspection ConstantValue + if (this.bukkitEntity != null) { + this.bukkitEntity.setHandle(entity); + this.bukkitEntity = entity.bukkitEntity; + } + } + // Sakura end - cannon entity merging public Entity(EntityType type, Level world) { this.id = Entity.ENTITY_COUNTER.incrementAndGet(); @@ -611,6 +724,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { 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) { @@ -2317,6 +2431,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { nbt.putBoolean("Paper.FreezeLock", true); } // Paper end + // Sakura start + if (stacked > 0) { + nbt.putInt("Sakura.Stacked", stacked); + } + // Sakura end return nbt; } catch (Throwable throwable) { CrashReport crashreport = CrashReport.forThrowable(throwable, "Saving entity NBT"); @@ -2484,6 +2603,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { 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"); @@ -4529,6 +4653,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { return; } // Paper end - rewrite chunk system + // Sakura start + if (reason == RemovalReason.DISCARDED && !mergeList.isEmpty()) { + level.mergeHistory.markPositions(this); + } + // Sakura end if (this.removalReason == null) { this.removalReason = reason; } 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 6b85eecde57a809391d531bb11d189d0a25ccfdc..2965d963ca7eaf7bfd010094c08aa8b80af79a13 100644 --- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java +++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java @@ -129,6 +129,60 @@ 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, this.level); + + // Try to stack the falling block + this.entityState().apply(fallingBlock); + fallingBlock.blockState = blockState; + 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(); + } + } + + 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() { // Paper start - fix sand duping @@ -208,6 +262,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(); 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 c2f04fca63ebef48a3092b60c703277abcb9b586..b9ee3115ea6f2929d8cd66ab189acc1614126574 100644 --- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java +++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java @@ -59,6 +59,44 @@ public class PrimedTnt 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 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, this.level); + + while (stacked-- > 1) { + this.setFuse(100); // Prevent unwanted explosions while ticking + + // Cause an explosion to affect this entity + tnt.setPos(this.position()); + tnt.setDeltaMovement(this.getDeltaMovement()); + this.entityState().apply(this); + tnt.explode(); + this.storeEntityState(); + + this.tick(); + } + + return true; + } + // Sakura end + @Override public void tick() { if (level.spigotConfig.maxTntTicksPerTick > 0 && ++level.spigotConfig.currentPrimedTnt > level.spigotConfig.maxTntTicksPerTick) { @@ -86,6 +124,7 @@ public class PrimedTnt extends Entity { 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 d80cdbbd9cd569e6db31950f05695c1cf476be3c..05ee07f06721b16801ae7420da7705b64db52eed 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -283,6 +283,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, 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 diff --git a/src/main/java/net/minecraft/world/level/block/BasePressurePlateBlock.java b/src/main/java/net/minecraft/world/level/block/BasePressurePlateBlock.java index caa0fbe5cad9cdc11d6e4af92bc627990cd8204c..6923a34e82e49004b29a77e57fea8c2a00180299 100644 --- a/src/main/java/net/minecraft/world/level/block/BasePressurePlateBlock.java +++ b/src/main/java/net/minecraft/world/level/block/BasePressurePlateBlock.java @@ -79,7 +79,7 @@ public abstract class BasePressurePlateBlock extends Block { } protected void checkPressed(@Nullable Entity entity, Level world, BlockPos pos, BlockState state, int output) { - int j = this.getSignalStrength(world, pos); + int j = this.getSignalStrength(world, pos, output == 0); // Sakura - cannon entity merging boolean flag = output > 0; boolean flag1 = j > 0; @@ -158,6 +158,12 @@ public abstract class BasePressurePlateBlock extends Block { return PushReaction.DESTROY; } + // Sakura start - cannon entity merging + protected int getSignalStrength(Level world, BlockPos pos, boolean entityInside) { + return this.getSignalStrength(world, pos); + } + // Sakura end - cannon entity merging + protected abstract int getSignalStrength(Level world, BlockPos pos); protected abstract int getSignalForState(BlockState state); diff --git a/src/main/java/net/minecraft/world/level/block/WeightedPressurePlateBlock.java b/src/main/java/net/minecraft/world/level/block/WeightedPressurePlateBlock.java index e3bcb7fc520abcb1854d89b65ebc947cc885caa6..f9786ba3bead763e4f880b99e0e0cae64fb62613 100644 --- a/src/main/java/net/minecraft/world/level/block/WeightedPressurePlateBlock.java +++ b/src/main/java/net/minecraft/world/level/block/WeightedPressurePlateBlock.java @@ -32,6 +32,11 @@ public class WeightedPressurePlateBlock extends BasePressurePlateBlock { @Override protected int getSignalStrength(Level world, BlockPos pos) { + // Sakura start - cannon entity merging + return this.getSignalStrength(world, pos, false); + } + protected final int getSignalStrength(Level world, BlockPos pos, boolean entityInside) { + // Sakura end - cannon entity merging // CraftBukkit start // int i = Math.min(world.getEntitiesOfClass(Entity.class, BlockPressurePlateWeighted.TOUCH_AABB.move(blockposition)).size(), this.maxWeight); int i = 0; @@ -51,7 +56,7 @@ public class WeightedPressurePlateBlock extends BasePressurePlateBlock { // We only want to block turning the plate on if all events are cancelled if (!cancellable.isCancelled()) { - i++; + i += !entityInside ? entity.getStacked() : 1; // Sakura - cannon entity merging } } diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java index 87c413c2f3b59ae9ef36e5becc10b29a81348022..65bac64214112ebd9e067202fe13293abe10559d 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) entity; diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java index 06540d3771949daff641e518219090559f363959..9b03ba0b3a7b491fafad5afae606cb6b8a6b53e6 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java @@ -13,6 +13,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;