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 783367f380ad2a21418d102fbb6bc49a71148ee8..2db7dee8589425c805b148ef46827ce88ec38c4e 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1807,6 +1807,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 @@ -918,6 +919,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 b87a07a5ac1da6803c7b61ef248a3dbecaedc3de..f08a3969c85f4ca1470c6fe9bcc3b4ae0e6920a3 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -605,6 +605,116 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess 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(); @@ -654,6 +764,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess this.entityData = datawatcher_a.build(); this.setPos(0.0D, 0.0D, 0.0D); this.eyeHeight = this.dimensions.eyeHeight(); + this.mergeLevel = level.sakuraConfig().cannons.mergeLevel; // Sakura } public boolean isColliding(BlockPos pos, BlockState state) { @@ -2592,6 +2703,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess 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"); @@ -2739,6 +2855,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess 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"); @@ -4944,6 +5065,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess // 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 3e80788374f03269067137582f6b6816821f7cac..f3c0e3efdcdda4f9772499a7fdc78f0046e89a16 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 protected double getDefaultGravity() { return 0.04D; @@ -202,6 +255,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 a2e8a95ba53953192db1889c686c507321666b03..1860ffd3e11601983abbb85962dd9ef56992c9f5 100644 --- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java +++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java @@ -70,6 +70,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 protected double getDefaultGravity() { return 0.04D; @@ -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 a2a2747dbddb819cfd7d29a552824d9d88fca744..1b14de4b22bd6f7103be55af20df6f5d7a149e0b 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -226,6 +226,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;