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..87327a63fdaf48b5a42666c36b925bda5516544e 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -603,6 +603,123 @@ 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; + + for (Entity mergedEntity : this.mergeList) { + mergedEntity.updateEntityHandle(entity); + } + + 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 + updateEntityHandle(entity); + } + + public void updateEntityHandle(Entity entity) { + // update api handle, this is so cannondebug can function + //noinspection ConstantValue + if (bukkitEntity != null) { + bukkitEntity.setHandle(entity); + } + this.bukkitEntity = entity.getBukkitEntity(); + } + // Sakura end - cannon entity merging public Entity(EntityType type, Level world) { this.id = Entity.ENTITY_COUNTER.incrementAndGet(); @@ -651,6 +768,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 +2651,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 +2803,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 +5017,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 4efdb2c114c96c32ce33593d35552c1c3da53e56..66b6250dc692e1222ac1661cdd206a1094049a15 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,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, 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(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 +254,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 7ebcd07c40e25fb909d7eec81820702181df1b5d..284ab0a2add55394f42fc0b5e62748ba1b629531 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/net/minecraft/world/level/block/BasePressurePlateBlock.java b/src/main/java/net/minecraft/world/level/block/BasePressurePlateBlock.java index 0d573c05f4f8838d4492f749ca473f7a9e8d60dd..b88dc572f6fbd7642f86196bf3525ad33686a64b 100644 --- a/src/main/java/net/minecraft/world/level/block/BasePressurePlateBlock.java +++ b/src/main/java/net/minecraft/world/level/block/BasePressurePlateBlock.java @@ -88,7 +88,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 - cannon entity merging boolean flag = output > 0; boolean flag1 = j > 0; @@ -170,6 +170,12 @@ public abstract class BasePressurePlateBlock extends Block { })); // CraftBukkit } + // 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 05bf23bd9ec951840ffceb68638458de02ad0063..ade85fb5a26da9272d392f24318ce6360ceb6414 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 - 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(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.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 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;