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/EntityMergeHandler.java b/src/main/java/me/samsuik/sakura/entity/merge/EntityMergeHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..dbd38e3b5b32c324eff6875269f09131425796a4
--- /dev/null
+++ b/src/main/java/me/samsuik/sakura/entity/merge/EntityMergeHandler.java
@@ -0,0 +1,76 @@
+package me.samsuik.sakura.entity.merge;
+
+import net.minecraft.world.entity.Entity;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public final class EntityMergeHandler {
+ private final TrackedMergeHistory trackedHistory = new TrackedMergeHistory();
+
+ /**
+ * Tries to merge the provided entities using the {@link MergeStrategy}.
+ *
+ * @param previous the last entity to tick
+ * @param entity the entity being merged
+ * @return success
+ */
+ public boolean tryMerge(@Nullable Entity entity, @Nullable Entity previous) {
+ if (entity instanceof MergeableEntity mergeEntity && previous instanceof MergeableEntity) {
+ MergeEntityData mergeEntityData = mergeEntity.getMergeEntityData();
+ MergeStrategy strategy = MergeStrategy.from(mergeEntityData.getMergeLevel());
+ Entity into = strategy.mergeEntity(entity, previous, this.trackedHistory);
+ if (into instanceof MergeableEntity intoEntity && !into.isRemoved() && mergeEntity.isSafeToMergeInto(intoEntity, strategy.trackHistory())) {
+ return this.mergeEntity(mergeEntity, intoEntity);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Stores the merged data of the provided entities if the {@link MergeStrategy} requires it.
+ *
+ * @param entity provided entity
+ */
+ public void removeEntity(@Nullable Entity entity) {
+ if (entity instanceof MergeableEntity mergeEntity) {
+ MergeEntityData mergeEntityData = mergeEntity.getMergeEntityData();
+ MergeStrategy strategy = MergeStrategy.from(mergeEntityData.getMergeLevel());
+ if (mergeEntityData.hasMerged() /*&& strategy.trackHistory()*/) {
+ this.trackedHistory.trackHistory(entity, mergeEntityData);
+ }
+ }
+ }
+
+ /**
+ * Called every tick and provided the current server tick to remove any unneeded merge history.
+ *
+ * @param tick server tick
+ */
+ public void expire(int tick) {
+ if (tick % 200 == 0) {
+ this.trackedHistory.expire(tick);
+ }
+ }
+
+ /**
+ * Merges the first entity into the second. The entity merge count can be retrieved through the {@link MergeEntityData}.
+ *
+ * This method also updates the bukkit handle so that plugins reference the first entity after the second entity has been removed.
+ *
+ * @param mergeEntity the first entity
+ * @param into the entity to merge into
+ * @return if successful
+ */
+ public boolean mergeEntity(@NotNull MergeableEntity mergeEntity, @NotNull MergeableEntity into) {
+ MergeEntityData entities = mergeEntity.getMergeEntityData();
+ MergeEntityData mergeInto = into.getMergeEntityData();
+ mergeInto.mergeWith(entities); // merge entities together
+
+ // discard the entity and update the bukkit handle
+ Entity nmsEntity = (Entity) mergeEntity;
+ nmsEntity.discard();
+ nmsEntity.updateBukkitHandle((Entity) into);
+ return true;
+ }
+}
diff --git a/src/main/java/me/samsuik/sakura/entity/merge/MergeCondition.java b/src/main/java/me/samsuik/sakura/entity/merge/MergeCondition.java
new file mode 100644
index 0000000000000000000000000000000000000000..6c0c224bc30c9c75d4b82508661117ce16197680
--- /dev/null
+++ b/src/main/java/me/samsuik/sakura/entity/merge/MergeCondition.java
@@ -0,0 +1,16 @@
+package me.samsuik.sakura.entity.merge;
+
+import net.minecraft.world.entity.Entity;
+import org.jetbrains.annotations.NotNull;
+
+public interface MergeCondition {
+ default MergeCondition and(@NotNull MergeCondition condition) {
+ return (e,c,t) -> this.accept(e,c,t) && condition.accept(e,c,t);
+ }
+
+ default MergeCondition or(@NotNull MergeCondition condition) {
+ return (e,c,t) -> this.accept(e,c,t) || condition.accept(e,c,t);
+ }
+
+ boolean accept(@NotNull Entity entity, int attempts, long sinceCreation);
+}
diff --git a/src/main/java/me/samsuik/sakura/entity/merge/MergeEntityData.java b/src/main/java/me/samsuik/sakura/entity/merge/MergeEntityData.java
new file mode 100644
index 0000000000000000000000000000000000000000..b13e3dcc333bcb53b4493e7087765fcca0a88604
--- /dev/null
+++ b/src/main/java/me/samsuik/sakura/entity/merge/MergeEntityData.java
@@ -0,0 +1,52 @@
+package me.samsuik.sakura.entity.merge;
+
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import net.minecraft.world.entity.Entity;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public final class MergeEntityData {
+ private final Entity entity;
+ private List connected = new ObjectArrayList<>();
+ private int count = 1;
+ private MergeLevel mergeLevel = MergeLevel.NONE;
+
+ public MergeEntityData(Entity entity) {
+ this.entity = entity;
+ }
+
+ public void mergeWith(@NotNull MergeEntityData mergeEntityData) {
+ this.connected.add(mergeEntityData);
+ this.connected.addAll(mergeEntityData.connected);
+ this.count += mergeEntityData.getCount();
+ mergeEntityData.setCount(0);
+ }
+
+ public LongOpenHashSet getOriginPositions() {
+ LongOpenHashSet positions = new LongOpenHashSet();
+ this.connected.forEach(entityData -> positions.add(entityData.entity.getPackedOriginPosition()));
+ return positions;
+ }
+
+ public boolean hasMerged() {
+ return !this.connected.isEmpty() && this.count != 0;
+ }
+
+ public void setMergeLevel(MergeLevel mergeLevel) {
+ this.mergeLevel = mergeLevel;
+ }
+
+ public MergeLevel getMergeLevel() {
+ return mergeLevel;
+ }
+
+ public void setCount(int count) {
+ this.count = count;
+ }
+
+ public int getCount() {
+ return this.count;
+ }
+}
diff --git a/src/main/java/me/samsuik/sakura/entity/merge/MergeStrategy.java b/src/main/java/me/samsuik/sakura/entity/merge/MergeStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ce8b8a54f80039c2f50496021494fbd0d81a82f
--- /dev/null
+++ b/src/main/java/me/samsuik/sakura/entity/merge/MergeStrategy.java
@@ -0,0 +1,118 @@
+package me.samsuik.sakura.entity.merge;
+
+import me.samsuik.sakura.utils.collections.FixedSizeCustomObjectTable;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.entity.EntityTickList;
+import org.jetbrains.annotations.NotNull;
+
+public interface MergeStrategy {
+ /**
+ * If this merge strategy requires the merge history to be tracked.
+ *
+ * @return should track history
+ */
+ boolean trackHistory();
+
+ /**
+ * Tries to merge the first entity into another entity.
+ *
+ * The first entity should always be positioned right after the second entity in the
+ * {@link EntityTickList}. This method should only
+ * be called before the first entity and after the second entity has ticked.
+ *
+ * @param entity current entity
+ * @param previous last entity to tick
+ * @return success
+ */
+ Entity mergeEntity(@NotNull Entity entity, @NotNull Entity previous, @NotNull TrackedMergeHistory mergeHistory);
+
+ /**
+ * Gets the {@link MergeStrategy} for the {@link MergeLevel}.
+ *
+ * @param level provided level
+ * @return strategy
+ */
+ static MergeStrategy from(MergeLevel level) {
+ return switch (level) {
+ case NONE -> None.INSTANCE;
+ case STRICT -> Strict.INSTANCE;
+ case LENIENT -> Lenient.INSTANCE;
+ case SPAWN -> Spawn.INSTANCE;
+ };
+ }
+
+ final class None implements MergeStrategy {
+ private static final None INSTANCE = new None();
+
+ @Override
+ public boolean trackHistory() {
+ return false;
+ }
+
+ @Override
+ public Entity mergeEntity(@NotNull Entity entity, @NotNull Entity previous, @NotNull TrackedMergeHistory mergeHistory) {
+ return null;
+ }
+ }
+
+ final class Strict implements MergeStrategy {
+ private static final Strict INSTANCE = new Strict();
+
+ @Override
+ public boolean trackHistory() {
+ return false;
+ }
+
+ @Override
+ public Entity mergeEntity(@NotNull Entity entity, @NotNull Entity previous, @NotNull TrackedMergeHistory mergeHistory) {
+ return entity.compareState(previous) ? previous : null;
+ }
+ }
+
+ final class Lenient implements MergeStrategy {
+ private static final Lenient INSTANCE = new Lenient();
+ private final FixedSizeCustomObjectTable entityTable = new FixedSizeCustomObjectTable<>(512, entity -> {
+ return entity.blockPosition().hashCode();
+ });
+
+ @Override
+ public boolean trackHistory() {
+ return true;
+ }
+
+ @Override
+ public Entity mergeEntity(@NotNull Entity entity, @NotNull Entity previous, @NotNull TrackedMergeHistory mergeHistory) {
+ if (entity.compareState(previous)) {
+ return previous;
+ }
+
+ Entity nextEntity = this.entityTable.getAndWrite(entity);
+ if (nextEntity == null || !nextEntity.level().equals(entity.level())) {
+ return null;
+ }
+
+ return mergeHistory.hasPreviousMerged(entity, nextEntity) && entity.compareState(nextEntity) ? nextEntity : null;
+ }
+ }
+
+ final class Spawn implements MergeStrategy {
+ private static final Spawn INSTANCE = new Spawn();
+ private static final MergeCondition CONDITION = (e, shots, time) -> (shots > 16 || time >= 200);
+
+ @Override
+ public boolean trackHistory() {
+ return true;
+ }
+
+ @Override
+ public Entity mergeEntity(@NotNull Entity entity, @NotNull Entity previous, @NotNull TrackedMergeHistory mergeHistory) {
+ final Entity mergeInto;
+ if (entity.tickCount == 1 && mergeHistory.hasMetCondition(entity, CONDITION) && mergeHistory.hasPreviousMerged(entity, previous)) {
+ mergeInto = previous;
+ } else {
+ mergeInto = entity.compareState(previous) ? previous : null;
+ }
+ return mergeInto;
+ }
+ }
+}
diff --git a/src/main/java/me/samsuik/sakura/entity/merge/MergeableEntity.java b/src/main/java/me/samsuik/sakura/entity/merge/MergeableEntity.java
new file mode 100644
index 0000000000000000000000000000000000000000..3061c3a48f7c68f64a3348b9583f4b41c16429a9
--- /dev/null
+++ b/src/main/java/me/samsuik/sakura/entity/merge/MergeableEntity.java
@@ -0,0 +1,22 @@
+package me.samsuik.sakura.entity.merge;
+
+import org.jetbrains.annotations.NotNull;
+
+public interface MergeableEntity {
+ @NotNull MergeEntityData getMergeEntityData();
+
+ boolean isSafeToMergeInto(@NotNull MergeableEntity entity, boolean ticksLived);
+
+ default boolean respawnEntity() {
+ MergeEntityData mergeData = this.getMergeEntityData();
+ int count = mergeData.getCount();
+ if (count > 1) {
+ mergeData.setCount(0);
+ this.respawnEntity(count);
+ return true;
+ }
+ return false;
+ }
+
+ void respawnEntity(int count);
+}
diff --git a/src/main/java/me/samsuik/sakura/entity/merge/TrackedMergeHistory.java b/src/main/java/me/samsuik/sakura/entity/merge/TrackedMergeHistory.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c2a26fdb3081ced574070e5e6f3a1b4c09b578d
--- /dev/null
+++ b/src/main/java/me/samsuik/sakura/entity/merge/TrackedMergeHistory.java
@@ -0,0 +1,73 @@
+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 me.samsuik.sakura.utils.objects.Expiry;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.world.entity.Entity;
+import org.jetbrains.annotations.NotNull;
+
+public final class TrackedMergeHistory {
+ private final Long2ObjectMap historyMap = new Long2ObjectOpenHashMap<>();
+
+ public boolean hasPreviousMerged(@NotNull Entity entity, @NotNull Entity into) {
+ PositionHistory positions = this.getHistory(into);
+ return positions != null && positions.has(entity.getPackedOriginPosition());
+ }
+
+ public void trackHistory(@NotNull Entity entity, @NotNull MergeEntityData mergeEntityData) {
+ long originPosition = entity.getPackedOriginPosition();
+ boolean createHistory = !this.historyMap.containsKey(originPosition);
+ PositionHistory positions = this.historyMap.computeIfAbsent(originPosition, p -> new PositionHistory());
+ LongOpenHashSet originPositions = mergeEntityData.getOriginPositions();
+ if (createHistory) {
+ originPositions.forEach(pos -> this.historyMap.put(pos, positions));
+ }
+ positions.trackPositions(originPositions, !createHistory);
+ }
+
+ public boolean hasMetCondition(@NotNull Entity entity, MergeCondition condition) {
+ PositionHistory positions = this.getHistory(entity);
+ return positions != null && positions.hasMetConditions(entity, condition);
+ }
+
+ private PositionHistory getHistory(Entity entity) {
+ long originPosition = entity.getPackedOriginPosition();
+ return this.historyMap.get(originPosition);
+ }
+
+ public void expire(int tick) {
+ this.historyMap.values().removeIf(p -> p.expiry().isExpired(tick));
+ }
+
+ private static final class PositionHistory {
+ private final LongOpenHashSet positions = new LongOpenHashSet();
+ private final Expiry expiry = new Expiry(MinecraftServer.currentTick, 200);
+ private final long created = MinecraftServer.currentTick;
+ private int cycles = 0;
+
+ public Expiry expiry() {
+ return this.expiry;
+ }
+
+ public boolean has(long position) {
+ this.expiry.refresh(MinecraftServer.currentTick);
+ return this.positions.contains(position);
+ }
+
+ public void trackPositions(LongOpenHashSet positions, boolean retain) {
+ if (retain) {
+ this.positions.retainAll(positions);
+ } else {
+ this.positions.addAll(positions);
+ }
+ this.cycles++;
+ }
+
+ public boolean hasMetConditions(@NotNull Entity entity, @NotNull MergeCondition condition) {
+ long timeSinceCreation = MinecraftServer.currentTick - this.created;
+ return condition.accept(entity, this.cycles, timeSinceCreation);
+ }
+ }
+}
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 873f37587a850fab964c11e96411252fd01e29f9..c871ef194962d1fc574a8d3d9f27dd817ead5fc9 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -1890,6 +1890,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {
if (!entity.isRemoved()) {
if (!tickratemanager.isEntityFrozen(entity)) {
@@ -801,6 +802,15 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
entity.stopRiding();
}
+ // Sakura start - merge cannon entities
+ Entity previous = previousEntity[0];
+ if (this.mergeHandler.tryMerge(entity, previous)) {
+ return;
+ } else {
+ previousEntity[0] = entity;
+ }
+ // Sakura end - merge cannon entities
+
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 4a295091629acd9a540f1194b006f445e4432d4f..c8b33f6d5b820da4b9e16b7d251ca4aad1875aa8 100644
--- a/src/main/java/net/minecraft/world/entity/Entity.java
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
@@ -647,6 +647,23 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
return to.entityState() != null && to.entityState().isCurrentState(this);
}
// Sakura end - store entity data/state
+ // Sakura start - merge cannon entities
+ public final void updateBukkitHandle(Entity entity) {
+ if (this.bukkitEntity != null) {
+ this.bukkitEntity.setHandle(entity);
+ }
+ this.bukkitEntity = entity.getBukkitEntity();
+ }
+
+ public final long getPackedOriginPosition() {
+ org.bukkit.util.Vector origin = this.getOriginVector();
+ if (origin != null) {
+ return BlockPos.asLong(origin.getBlockX(), origin.getBlockY(), origin.getBlockZ());
+ } else {
+ return Long.MIN_VALUE;
+ }
+ }
+ // Sakura end - merge cannon entities
public Entity(EntityType> type, Level world) {
this.id = Entity.ENTITY_COUNTER.incrementAndGet();
@@ -5209,6 +5226,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
if (this.removalReason != Entity.RemovalReason.UNLOADED_TO_CHUNK) { this.getPassengers().forEach(Entity::stopRiding); } // Paper - rewrite chunk system
this.levelCallback.onRemove(entity_removalreason);
this.onRemoval(entity_removalreason);
+ // Sakura start - merge cannon entities
+ if (entity_removalreason == RemovalReason.DISCARDED) {
+ this.level.mergeHandler.removeEntity(this);
+ }
+ // Sakura end - merge cannon entities
// Paper start - Folia schedulers
if (!(this instanceof ServerPlayer) && entity_removalreason != RemovalReason.CHANGED_DIMENSION && !alreadyRemoved) {
// Players need to be special cased, because they are regularly removed from the world
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 35d8422ea3bcdc52db6a7f6702fd593346b30c2c..f0617e9d71fa47dfc0566f31d85b8a05ba3b16fc 100644
--- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java
+++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java
@@ -57,7 +57,7 @@ import org.bukkit.craftbukkit.event.CraftEventFactory;
import org.bukkit.event.entity.EntityRemoveEvent;
// CraftBukkit end
-public class FallingBlockEntity extends Entity {
+public class FallingBlockEntity extends Entity implements me.samsuik.sakura.entity.merge.MergeableEntity { // Sakura - merge cannon entities
private static final Logger LOGGER = LogUtils.getLogger();
public BlockState blockState;
@@ -73,6 +73,58 @@ public class FallingBlockEntity extends Entity {
protected static final EntityDataAccessor DATA_START_POS = SynchedEntityData.defineId(FallingBlockEntity.class, EntityDataSerializers.BLOCK_POS);
public boolean autoExpire = true; // Paper - Expand FallingBlock API
+ // Sakura start - merge cannon entities
+ private final me.samsuik.sakura.entity.merge.MergeEntityData mergeData = new me.samsuik.sakura.entity.merge.MergeEntityData(this);
+
+ @Override
+ public final me.samsuik.sakura.entity.merge.MergeEntityData getMergeEntityData() {
+ return this.mergeData;
+ }
+
+ @Override
+ public final boolean isSafeToMergeInto(me.samsuik.sakura.entity.merge.MergeableEntity entity, boolean ticksLived) {
+ return entity instanceof FallingBlockEntity fbe
+ && fbe.blockState.equals(this.blockState)
+ && (!ticksLived || fbe.time - 1 == this.time);
+ }
+
+ @Override
+ public final void respawnEntity(int count) {
+ while (count-- >= 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 = this.blockState;
+ fallingBlock.spawnReason = this.spawnReason;
+ fallingBlock.time = this.time - 1;
+ fallingBlock.tick();
+
+ // If you horizontal stack into a moving piston block this condition will be met.
+ if (!fallingBlock.isRemoved()) {
+ this.mergeData.setCount(count + 1);
+ fallingBlock.storeEntityState();
+ fallingBlock.entityState().apply(this);
+ break;
+ } else if (count == 0) {
+ this.discard(EntityRemoveEvent.Cause.DESPAWN);
+ }
+ }
+ }
+
+ @Nullable
+ public ItemEntity spawnAtLocation(ServerLevel level, ItemLike item) { // may be overridden by plugins
+ ItemEntity itemEntity = null;
+
+ for (int i = 0; i < this.mergeData.getCount(); ++i) {
+ itemEntity = super.spawnAtLocation(level, item);
+ }
+
+ return itemEntity;
+ }
+ // Sakura end - merge cannon entities
+
public FallingBlockEntity(EntityType extends FallingBlockEntity> type, Level world) {
super(type, world);
this.blockState = Blocks.SAND.defaultBlockState();
@@ -80,6 +132,7 @@ public class FallingBlockEntity extends Entity {
this.fallDamageMax = 40;
this.isFallingBlock = true; // Sakura
this.loadChunks = world.sakuraConfig().cannons.loadChunks; // Sakura - load chunks on movement
+ this.mergeData.setMergeLevel(world.sakuraConfig().cannons.mergeLevel); // Sakura - merge cannon entities
}
public FallingBlockEntity(Level world, double x, double y, double z, BlockState block) {
@@ -222,6 +275,7 @@ public class FallingBlockEntity extends Entity {
return;
}
// CraftBukkit end
+ if (this.respawnEntity()) return; // Sakura - merge cannon entities
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); // CraftBukkit - add Bukkit remove cause
@@ -342,6 +396,7 @@ public class FallingBlockEntity extends Entity {
nbt.putBoolean("CancelDrop", this.cancelDrop);
if (!autoExpire) {nbt.putBoolean("Paper.AutoExpire", false);} // Paper - Expand FallingBlock API
+ nbt.putInt("merge_count", this.mergeData.getCount()); // Sakura - merge cannon entities
}
@Override
@@ -374,6 +429,11 @@ public class FallingBlockEntity extends Entity {
this.autoExpire = nbt.getBoolean("Paper.AutoExpire");
}
// Paper end - Expand FallingBlock API
+ // Sakura start - merge cannon entities
+ if (nbt.contains("merge_count", 3)) {
+ this.mergeData.setCount(nbt.getInt("merge_count"));
+ }
+ // Sakura end - merge cannon entities
}
public void setHurtsEntities(float fallHurtAmount, int fallHurtMax) {
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 25a25b5c6497c9823a41e6bd8fd22f0841377c62..6b739e820af6333c1ff3a4b87154d4c00c9a5559 100644
--- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java
+++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java
@@ -33,7 +33,7 @@ import org.bukkit.event.entity.EntityRemoveEvent;
import org.bukkit.event.entity.ExplosionPrimeEvent;
// CraftBukkit end
-public class PrimedTnt extends Entity implements TraceableEntity {
+public class PrimedTnt extends Entity implements TraceableEntity, me.samsuik.sakura.entity.merge.MergeableEntity { // Sakura - merge cannon entities
private static final EntityDataAccessor DATA_FUSE_ID = SynchedEntityData.defineId(PrimedTnt.class, EntityDataSerializers.INT);
private static final EntityDataAccessor DATA_BLOCK_STATE_ID = SynchedEntityData.defineId(PrimedTnt.class, EntityDataSerializers.BLOCK_STATE);
@@ -59,12 +59,49 @@ public class PrimedTnt extends Entity implements TraceableEntity {
public float explosionPower;
public boolean isIncendiary = false; // CraftBukkit - add field
+ // Sakura start - merge cannon entities
+ private final me.samsuik.sakura.entity.merge.MergeEntityData mergeData = new me.samsuik.sakura.entity.merge.MergeEntityData(this);
+
+ @Override
+ public final me.samsuik.sakura.entity.merge.MergeEntityData getMergeEntityData() {
+ return this.mergeData;
+ }
+
+ @Override
+ public final boolean isSafeToMergeInto(me.samsuik.sakura.entity.merge.MergeableEntity entity, boolean ticksLived) {
+ return entity instanceof PrimedTnt tnt
+ && tnt.getFuse() + 1 == this.getFuse()
+ // required to prevent issues with powdered snow
+ && (tnt.entityState().fallDistance() == this.fallDistance
+ || tnt.entityState().fallDistance() > 2.5f && this.fallDistance > 2.5f);
+ }
+
+ @Override
+ public final void respawnEntity(int count) {
+ PrimedTnt tnt = new PrimedTnt(EntityType.TNT, this.level());
+ tnt.updateBukkitHandle(this); // update handle for plugins
+ while (count-- > 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();
+ }
+ }
+ // Sakura end - merge cannon entities
+
public PrimedTnt(EntityType extends PrimedTnt> type, Level world) {
super(type, world);
this.explosionPower = 4.0F;
this.blocksBuilding = true;
this.isPrimedTNT = true; // Sakura
this.loadChunks = world.sakuraConfig().cannons.loadChunks; // Sakura - load chunks on movement
+ this.mergeData.setMergeLevel(world.sakuraConfig().cannons.mergeLevel); // Sakura - merge cannon entities
}
public PrimedTnt(Level world, double x, double y, double z, @Nullable LivingEntity igniter) {
@@ -125,6 +162,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.respawnEntity(); // Sakura - merge cannon entities
if (!this.level().isClientSide) {
this.explode();
}
@@ -185,7 +223,7 @@ public class PrimedTnt extends Entity implements TraceableEntity {
if (this.explosionPower != 4.0F) {
nbt.putFloat("explosion_power", this.explosionPower);
}
-
+ nbt.putInt("merge_count", this.mergeData.getCount()); // Sakura - merge cannon entities
}
@Override
@@ -194,6 +232,11 @@ public class PrimedTnt extends Entity implements TraceableEntity {
if (nbt.contains("block_state", 10)) {
this.setBlockState(NbtUtils.readBlockState(this.level().holderLookup(Registries.BLOCK), nbt.getCompound("block_state")));
}
+ // Sakura start - merge cannon entities
+ if (nbt.contains("merge_count", 3)) {
+ this.mergeData.setCount(nbt.getInt("merge_count"));
+ }
+ // Sakura end - merge cannon entities
if (nbt.contains("explosion_power", 99)) {
this.explosionPower = Mth.clamp(nbt.getFloat("explosion_power"), 0.0F, 128.0F);
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
index 4696506402ab92439033cf01007fd78a543b5b37..d036cdcc17b8429c986946a020cb34cd6ced322e 100644
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
@@ -841,6 +841,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
return chunk != null ? chunk.getNoiseBiome(x, y, z) : this.getUncachedNoiseBiome(x, y, z);
}
// Paper end - optimise random ticking
+ public final me.samsuik.sakura.entity.merge.EntityMergeHandler mergeHandler = new me.samsuik.sakura.entity.merge.EntityMergeHandler(); // Sakura - merge cannon entities
protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, RegistryAccess iregistrycustom, Holder holder, 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, java.util.function.Supplier sakuraWorldConfigCreator, java.util.concurrent.Executor executor) { // Sakura - sakura configuration files // Paper - create paper world config & Anti-Xray
// Paper start - getblock optimisations - cache world height/sections
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 9afa811579ac2e556b5c5c23b3b49587439dfadc..c2eb63de04fc48bd2cc1aad8d9cba272c0829c80 100644
--- a/src/main/java/net/minecraft/world/level/block/BasePressurePlateBlock.java
+++ b/src/main/java/net/minecraft/world/level/block/BasePressurePlateBlock.java
@@ -89,7 +89,7 @@ public abstract class BasePressurePlateBlock extends Block {
}
private 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 - merge cannon entities
boolean flag = output > 0;
boolean flag1 = j > 0;
@@ -171,6 +171,12 @@ public abstract class BasePressurePlateBlock extends Block {
})); // CraftBukkit
}
+ // Sakura start - merge cannon entities
+ protected int getSignalStrength(Level world, BlockPos pos, boolean entityInside) {
+ return this.getSignalStrength(world, pos);
+ }
+ // Sakura end - merge cannon entities
+
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 05bf23bd9ec951840ffceb68638458de02ad0063..f9c800e912ce966dba4f1026f2d5b702ae6549cd 100644
--- a/src/main/java/net/minecraft/world/level/block/WeightedPressurePlateBlock.java
+++ b/src/main/java/net/minecraft/world/level/block/WeightedPressurePlateBlock.java
@@ -42,6 +42,11 @@ public class WeightedPressurePlateBlock extends BasePressurePlateBlock {
@Override
protected int getSignalStrength(Level world, BlockPos pos) {
+ // Sakura start - merge cannon entities
+ return this.getSignalStrength(world, pos, false);
+ }
+ protected final int getSignalStrength(Level world, BlockPos pos, boolean entityInside) {
+ // Sakura end - merge cannon entities
// CraftBukkit start
// int i = Math.min(getEntityCount(world, BlockPressurePlateWeighted.TOUCH_AABB.move(blockposition), Entity.class), this.maxWeight);
int i = 0;
@@ -57,7 +62,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 instanceof me.samsuik.sakura.entity.merge.MergeableEntity mergeEntity ? mergeEntity.getMergeEntityData().getCount() : 1; // Sakura - merge cannon entities
}
}
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java
index 1359d25a32b4a5d5e8e68ce737bd19f7b5afaf69..55f67c2ca07eca0d3e1522eebbb4ce37704bdd04 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 - merge cannon entities
+ @Override
+ public @org.jetbrains.annotations.NotNull me.samsuik.sakura.entity.merge.MergeLevel getMergeLevel() {
+ return this.getHandle().getMergeEntityData().getMergeLevel();
+ }
+
+ @Override
+ public void setMergeLevel(@org.jetbrains.annotations.NotNull me.samsuik.sakura.entity.merge.MergeLevel level) {
+ this.getHandle().getMergeEntityData().setMergeLevel(level);
+ }
+
+ @Override
+ public int getStacked() {
+ return this.getHandle().getMergeEntityData().getCount();
+ }
+
+ @Override
+ public void setStacked(int stacked) {
+ this.getHandle().getMergeEntityData().setCount(stacked);
+ }
+ // Sakura end - merge cannon entities
+
@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 a61aec087fa7cec27a803668bdc1b9e6eb336755..c6f36ab2368d0e2e4555d5f8edc0132dcb61a53c 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 - merge cannon entities
+ @Override
+ public @org.jetbrains.annotations.NotNull me.samsuik.sakura.entity.merge.MergeLevel getMergeLevel() {
+ return this.getHandle().getMergeEntityData().getMergeLevel();
+ }
+
+ @Override
+ public void setMergeLevel(@org.jetbrains.annotations.NotNull me.samsuik.sakura.entity.merge.MergeLevel level) {
+ this.getHandle().getMergeEntityData().setMergeLevel(level);
+ }
+
+ @Override
+ public int getStacked() {
+ return this.getHandle().getMergeEntityData().getCount();
+ }
+
+ @Override
+ public void setStacked(int stacked) {
+ this.getHandle().getMergeEntityData().setCount(stacked);
+ }
+ // Sakura end - merge cannon entities
+
@Override
public float getYield() {
return this.getHandle().explosionPower;