From 08bfa446a5702c1b83bf16b0f1ac6bdcac7e6378 Mon Sep 17 00:00:00 2001 From: MC_XiaoHei Date: Thu, 31 Jul 2025 14:29:34 +0800 Subject: [PATCH] =?UTF-8?q?Lithium=20Sleeping=20Block=20Entity=EF=BC=88#61?= =?UTF-8?q?8=EF=BC=89=20(#630)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: try to impl Lithium Sleeping Block Entity * feat: add sleepingBlockEntity config * chore: update branch * fix: lock sleepingBlockEntity config to expect hot update * chore: format code * chore: format code * fix: unapplied mixin --- .../0141-Lithium-Sleeping-Block-Entity.patch | 2419 +++++++++++++++++ .../org/leavesmc/leaves/LeavesConfig.java | 3 + .../LithiumCooldownReceivingInventory.java | 47 + .../api/inventory/LithiumDefaultedList.java | 33 + .../api/inventory/LithiumInventory.java | 78 + .../LithiumTransferConditionInventory.java | 39 + .../SetBlockStateHandlingBlockEntity.java | 23 + .../entity/SetChangedHandlingBlockEntity.java | 23 + .../ShapeUpdateHandlingBlockBehaviour.java | 29 + .../SleepUntilTimeBlockEntityTickInvoker.java | 53 + .../block/entity/SleepingBlockEntity.java | 97 + .../InventoryChangeEmitter.java | 47 + .../InventoryChangeListener.java | 32 + .../InventoryChangeTracker.java | 34 + .../ComparatorTracker.java | 26 + .../ComparatorTracking.java | 63 + .../hopper/BlockStateOnlyInventory.java | 4 + .../hopper/ComparatorUpdatePattern.java | 114 + .../common/hopper/HopperCachingState.java | 29 + .../lithium/common/hopper/HopperHelper.java | 168 ++ .../common/hopper/InventoryHelper.java | 52 + .../common/hopper/LithiumDoubleInventory.java | 190 ++ .../common/hopper/LithiumDoubleStackList.java | 162 ++ .../common/hopper/LithiumStackList.java | 304 +++ .../lithium/common/hopper/UpdateReceiver.java | 28 + .../ChunkSectionEntityMovementListener.java | 5 + .../ChunkSectionEntityMovementTracker.java | 68 + .../ChunkSectionInventoryEntityTracker.java | 67 + ...ChunkSectionItemEntityMovementTracker.java | 67 + .../common/util/DirectionConstants.java | 33 + .../util/change_tracking/ChangePublisher.java | 34 + .../change_tracking/ChangeSubscriber.java | 211 ++ .../common/util/tuples/WorldSectionBox.java | 37 + .../lithium/common/world/WorldHelper.java | 29 + 34 files changed, 4648 insertions(+) create mode 100644 leaves-server/minecraft-patches/features/0141-Lithium-Sleeping-Block-Entity.patch create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumCooldownReceivingInventory.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumDefaultedList.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumInventory.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumTransferConditionInventory.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetBlockStateHandlingBlockEntity.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetChangedHandlingBlockEntity.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/ShapeUpdateHandlingBlockBehaviour.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepUntilTimeBlockEntityTickInvoker.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepingBlockEntity.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeEmitter.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeListener.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeTracker.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracker.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracking.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/BlockStateOnlyInventory.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/ComparatorUpdatePattern.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperCachingState.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperHelper.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/InventoryHelper.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleInventory.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleStackList.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumStackList.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/UpdateReceiver.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementListener.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementTracker.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionInventoryEntityTracker.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionItemEntityMovementTracker.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/DirectionConstants.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangePublisher.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangeSubscriber.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/tuples/WorldSectionBox.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/world/WorldHelper.java diff --git a/leaves-server/minecraft-patches/features/0141-Lithium-Sleeping-Block-Entity.patch b/leaves-server/minecraft-patches/features/0141-Lithium-Sleeping-Block-Entity.patch new file mode 100644 index 00000000..61e6bb9d --- /dev/null +++ b/leaves-server/minecraft-patches/features/0141-Lithium-Sleeping-Block-Entity.patch @@ -0,0 +1,2419 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MC_XiaoHei +Date: Thu, 31 Jul 2025 09:37:26 +0800 +Subject: [PATCH] Lithium Sleeping Block Entity + +This patch is Powered by Lithium(https://github.com/CaffeineMC/lithium) + +diff --git a/net/minecraft/core/NonNullList.java b/net/minecraft/core/NonNullList.java +index 7e31c5c8659d24948fd45a2d6ee7bdeca6027d27..387325b971b7cec6aeb677bf6c49b8bf21b3a344 100644 +--- a/net/minecraft/core/NonNullList.java ++++ b/net/minecraft/core/NonNullList.java +@@ -9,7 +9,7 @@ import javax.annotation.Nullable; + import org.apache.commons.lang3.Validate; + + public class NonNullList extends AbstractList { +- private final List list; ++ public final List list; // Leaves - private -> public + @Nullable + private final E defaultValue; + +diff --git a/net/minecraft/core/component/PatchedDataComponentMap.java b/net/minecraft/core/component/PatchedDataComponentMap.java +index 3af6c1e2549ba3aeb60aa9d498a976be3680c0ee..1e7c7e242b5be6fdf52124864e1e284fd9542771 100644 +--- a/net/minecraft/core/component/PatchedDataComponentMap.java ++++ b/net/minecraft/core/component/PatchedDataComponentMap.java +@@ -14,7 +14,7 @@ import java.util.Map.Entry; + import java.util.stream.Collectors; + import javax.annotation.Nullable; + +-public final class PatchedDataComponentMap implements DataComponentMap { ++public final class PatchedDataComponentMap implements DataComponentMap, org.leavesmc.leaves.lithium.common.util.change_tracking.ChangePublisher { // Leaves - Lithium Sleeping Block Entity + private final DataComponentMap prototype; + private Reference2ObjectMap, Optional> patch; + private boolean copyOnWrite; +@@ -135,6 +135,7 @@ public final class PatchedDataComponentMap implements DataComponentMap { + } + + private void ensureMapOwnership() { ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && this.subscriber != null) this.subscriber.lithium$notify((PatchedDataComponentMap) (Object) this, 0); // Leaves - Lithium Sleeping Block Entity + if (this.copyOnWrite) { + this.patch = new Reference2ObjectArrayMap<>(this.patch); + this.copyOnWrite = false; +@@ -238,4 +239,22 @@ public final class PatchedDataComponentMap implements DataComponentMap { + public String toString() { + return "{" + this.stream().map(TypedDataComponent::toString).collect(Collectors.joining(", ")) + "}"; + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber subscriber; ++ ++ @Override ++ public void lithium$subscribe(org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber subscriber, int subscriberData) { ++ if (subscriberData != 0) { ++ throw new UnsupportedOperationException("ComponentMapImpl does not support subscriber data"); ++ } ++ this.subscriber = org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber.combine(this.subscriber, 0, subscriber, 0); ++ } ++ ++ @Override ++ public int lithium$unsubscribe(org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber subscriber) { ++ this.subscriber = org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber.without(this.subscriber, subscriber); ++ return 0; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/server/commands/data/EntityDataAccessor.java b/net/minecraft/server/commands/data/EntityDataAccessor.java +index 3092454bf7071deca75fecfc203072593fe5c7e7..c20e12726a020429f36b9fbe0d6da2afccb220a2 100644 +--- a/net/minecraft/server/commands/data/EntityDataAccessor.java ++++ b/net/minecraft/server/commands/data/EntityDataAccessor.java +@@ -55,6 +55,7 @@ public class EntityDataAccessor implements DataAccessor { + try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(this.entity.problemPath(), LOGGER)) { + this.entity.load(TagValueInput.create(scopedCollector, this.entity.registryAccess(), other)); + this.entity.setUUID(uuid); ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && this.entity instanceof net.minecraft.world.entity.item.ItemEntity itemEntity) itemEntity.levelCallback.onMove(); // Leaves - Lithium Sleeping Block Entity + } + } + } +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index f78c4f3f2047564730050f16693c00aa2c29a4e0..15a7da64e6ff4d62a76b4b3c113eb3563f621feb 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -2402,6 +2402,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + for (TickingBlockEntity tickingBlockEntity : this.blockEntityTickers) { + BlockPos pos = tickingBlockEntity.getPos(); ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && pos == null) pos = BlockPos.ZERO; // Leaves - Lithium Sleeping Block Entity + csvOutput.writeRow(pos.getX(), pos.getY(), pos.getZ(), tickingBlockEntity.getType()); + } + } +diff --git a/net/minecraft/world/Container.java b/net/minecraft/world/Container.java +index 86cac164a2bf0e76528396e6aabbfd64cfc29559..da99b4bc7fe8460945070915073be141f9bd6778 100644 +--- a/net/minecraft/world/Container.java ++++ b/net/minecraft/world/Container.java +@@ -11,7 +11,7 @@ import net.minecraft.world.item.ItemStack; + import net.minecraft.world.level.Level; + import net.minecraft.world.level.block.entity.BlockEntity; + +-public interface Container extends Clearable, Iterable { ++public interface Container extends Clearable, Iterable, org.leavesmc.leaves.lithium.api.inventory.LithiumCooldownReceivingInventory, org.leavesmc.leaves.lithium.api.inventory.LithiumTransferConditionInventory { // Leaves - Lithium Sleeping Block Entity + float DEFAULT_DISTANCE_BUFFER = 4.0F; + + int getContainerSize(); +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index ca017f5e483a4ff5bc497ad453f4cf63a0bb97f5..0260f6c1518114cca57e945423f1f671740eb802 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -309,7 +309,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + private static final EntityDataAccessor DATA_NO_GRAVITY = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.BOOLEAN); + protected static final EntityDataAccessor DATA_POSE = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.POSE); + private static final EntityDataAccessor DATA_TICKS_FROZEN = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.INT); +- private EntityInLevelCallback levelCallback = EntityInLevelCallback.NULL; ++ public EntityInLevelCallback levelCallback = EntityInLevelCallback.NULL; // Leaves - private -> public + private final VecDeltaCodec packetPositionCodec = new VecDeltaCodec(); + public boolean hasImpulse; + @Nullable +@@ -5147,6 +5147,25 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.setBoundingBox(this.makeBoundingBox()); + } + // Paper end - Block invalid positions and bounding box ++ // Leaves start - Lithium Sleeping Block Entity ++ if (!org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) return; ++ if (this instanceof ItemEntity) { ++ long sectionKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(this); ++ var tracker = org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionItemEntityMovementTracker.itemEntityMovementTrackerMap.computeIfAbsent( ++ sectionKey, ++ k -> new org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionItemEntityMovementTracker(sectionKey) ++ ); ++ tracker.notifyAllListeners(level.getGameTime()); ++ } ++ else if (this instanceof net.minecraft.world.entity.vehicle.ContainerEntity) { ++ long sectionKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(this); ++ var tracker = org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionInventoryEntityTracker.containerEntityMovementTrackerMap.computeIfAbsent( ++ sectionKey, ++ k -> new org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionInventoryEntityTracker(sectionKey) ++ ); ++ tracker.notifyAllListeners(level.getGameTime()); ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } + + public void checkDespawn() { +diff --git a/net/minecraft/world/entity/item/ItemEntity.java b/net/minecraft/world/entity/item/ItemEntity.java +index fc8d3e56771998a04d5e0b35ad7638a44def8c77..9b0625e2ee59659a68749bce5ac44bba8c038b0b 100644 +--- a/net/minecraft/world/entity/item/ItemEntity.java ++++ b/net/minecraft/world/entity/item/ItemEntity.java +@@ -34,8 +34,12 @@ import net.minecraft.world.level.portal.TeleportTransition; + import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + import net.minecraft.world.phys.Vec3; ++// Leaves start - Lithium Sleeping Block Entity ++import org.leavesmc.leaves.lithium.common.util.change_tracking.ChangePublisher; ++import org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber; ++// Leaves end - Lithium Sleeping Block Entity + +-public class ItemEntity extends Entity implements TraceableEntity { ++public class ItemEntity extends Entity implements TraceableEntity, ChangePublisher, ChangeSubscriber.CountChangeSubscriber { // Leaves - Lithium Sleeping Block Entity + private static final EntityDataAccessor DATA_ITEM = SynchedEntityData.defineId(ItemEntity.class, EntityDataSerializers.ITEM_STACK); + private static final float FLOAT_HEIGHT = 0.1F; + public static final float EYE_HEIGHT = 0.2125F; +@@ -535,6 +539,25 @@ public class ItemEntity extends Entity implements TraceableEntity { + } + + public void setItem(ItemStack stack) { ++ // Leaves start - Lithium Sleeping Block Entity ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && this.subscriber != null) { ++ ItemStack oldStack = this.getItem(); ++ if (oldStack != stack) { ++ if (!oldStack.isEmpty()) { ++ oldStack.lithium$unsubscribe(this); ++ } ++ ++ if (!stack.isEmpty()) { ++ stack.lithium$subscribe(this, this.subscriberData); ++ this.subscriber.lithium$notify((ItemEntity) (Object) this, this.subscriberData); ++ } else { ++ this.subscriber.lithium$forceUnsubscribe((ItemEntity) (Object) this, this.subscriberData); ++ this.subscriber = null; ++ this.subscriberData = 0; ++ } ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + this.getEntityData().set(DATA_ITEM, stack); + this.despawnRate = this.level().paperConfig().entities.spawning.altItemDespawnRate.enabled ? this.level().paperConfig().entities.spawning.altItemDespawnRate.items.getOrDefault(stack.getItem(), this.level().spigotConfig.itemDespawnRate) : this.level().spigotConfig.itemDespawnRate; // Paper - Alternative item-despawn-rate + } +@@ -614,4 +637,76 @@ public class ItemEntity extends Entity implements TraceableEntity { + public SlotAccess getSlot(int slot) { + return slot == 0 ? SlotAccess.of(this::getItem, this::setItem) : super.getSlot(slot); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private ChangeSubscriber subscriber; ++ //Stores the data of the subscriber, unless the subscriber is a Multi which stores the data in a list, in which case this variable stores 0 ++ private int subscriberData; ++ ++ private void startTrackingChanges() { ++ ItemStack stack = this.getItem(); ++ if (!stack.isEmpty()) { ++ stack.lithium$subscribe(this, 0); ++ } ++ } ++ ++ @Override ++ public void lithium$subscribe(ChangeSubscriber subscriber, int subscriberData) { ++ if (this.subscriber == null) { ++ this.startTrackingChanges(); ++ } ++ this.subscriber = ChangeSubscriber.combine(this.subscriber, this.subscriberData, subscriber, subscriberData); ++ if (this.subscriber instanceof ChangeSubscriber.Multi) { ++ this.subscriberData = 0; ++ } else { ++ this.subscriberData = subscriberData; ++ } ++ } ++ ++ @Override ++ public int lithium$unsubscribe(ChangeSubscriber subscriber) { ++ int retval = ChangeSubscriber.dataOf(this.subscriber, subscriber, this.subscriberData); ++ this.subscriberData = ChangeSubscriber.dataWithout(this.subscriber, subscriber, this.subscriberData); ++ this.subscriber = ChangeSubscriber.without(this.subscriber, subscriber); ++ ++ if (this.subscriber == null) { ++ ItemStack stack = this.getItem(); ++ if (!stack.isEmpty()) { ++ stack.lithium$unsubscribe(this); ++ } ++ } ++ return retval; ++ } ++ ++ @Override ++ public void lithium$notify(ItemStack publisher, int subscriberData) { ++ if (publisher != this.getItem()) { ++ throw new IllegalStateException("Received notification from an unexpected publisher"); ++ } ++ ++ if (this.subscriber != null) { ++ this.subscriber.lithium$notify(this, this.subscriberData); ++ } ++ } ++ ++ @Override ++ public void lithium$forceUnsubscribe(ItemStack publisher, int subscriberData) { ++ if (this.subscriber != null) { ++ this.subscriber.lithium$forceUnsubscribe(this, this.subscriberData); ++ this.subscriber = null; ++ this.subscriberData = 0; ++ } ++ } ++ ++ @Override ++ public void lithium$notifyCount(ItemStack publisher, int subscriberData, int newCount) { ++ if (publisher != this.getItem()) { ++ throw new IllegalStateException("Received notification from an unexpected publisher"); ++ } ++ ++ if (this.subscriber instanceof ChangeSubscriber.CountChangeSubscriber countChangeSubscriber) { ++ countChangeSubscriber.lithium$notifyCount(this, this.subscriberData, newCount); ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java b/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java +index 7781ca07a0c8fe1140f341b695e66de95802ee2e..f190e8f244d011bdb7f04ccf0f0a35cb9d2c684a 100644 +--- a/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java ++++ b/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java +@@ -21,7 +21,7 @@ import net.minecraft.world.level.storage.ValueOutput; + import net.minecraft.world.level.storage.loot.LootTable; + import net.minecraft.world.phys.Vec3; + +-public abstract class AbstractMinecartContainer extends AbstractMinecart implements ContainerEntity { ++public abstract class AbstractMinecartContainer extends AbstractMinecart implements ContainerEntity, org.leavesmc.leaves.lithium.api.inventory.LithiumInventory { // Leaves - Lithium Sleeping Block Entity + private NonNullList itemStacks = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY); // CraftBukkit - SPIGOT-3513 + @Nullable + public ResourceKey lootTable; +@@ -223,4 +223,15 @@ public abstract class AbstractMinecartContainer extends AbstractMinecart impleme + return this.getBukkitEntity().getLocation(); + } + // CraftBukkit end ++ // Leaves start - Lithium Sleeping Block Entity ++ @Override ++ public net.minecraft.core.NonNullList getInventoryLithium() { ++ return itemStacks; ++ } ++ ++ @Override ++ public void setInventoryLithium(net.minecraft.core.NonNullList inventory) { ++ itemStacks = inventory; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/inventory/AbstractContainerMenu.java b/net/minecraft/world/inventory/AbstractContainerMenu.java +index 0541ac612679fcfd49c71c005bc4fb2cc7101188..1fc56b3840c67971669885e56467c3221f931cfd 100644 +--- a/net/minecraft/world/inventory/AbstractContainerMenu.java ++++ b/net/minecraft/world/inventory/AbstractContainerMenu.java +@@ -931,6 +931,7 @@ public abstract class AbstractContainerMenu { + } else { + float f = 0.0F; + ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && container instanceof org.leavesmc.leaves.lithium.api.inventory.LithiumInventory optimizedInventory) return org.leavesmc.leaves.lithium.common.hopper.InventoryHelper.getLithiumStackList(optimizedInventory).getSignalStrength(container); // Leaves - Lithium Sleeping Block Entity + for (int i = 0; i < container.getContainerSize(); i++) { + ItemStack item = container.getItem(i); + if (!item.isEmpty()) { +diff --git a/net/minecraft/world/item/ItemStack.java b/net/minecraft/world/item/ItemStack.java +index fdc92174fcb4f56fb4ff056f04c9b2429061d887..2fe3d7dedcd1151462161ebb0cf844204354a409 100644 +--- a/net/minecraft/world/item/ItemStack.java ++++ b/net/minecraft/world/item/ItemStack.java +@@ -93,8 +93,12 @@ import net.minecraft.world.level.block.state.pattern.BlockInWorld; + import org.apache.commons.lang3.function.TriConsumer; + import org.apache.commons.lang3.mutable.MutableBoolean; + import org.slf4j.Logger; ++// Leaves start - Lithium Sleeping Block Entity ++import org.leavesmc.leaves.lithium.common.util.change_tracking.ChangePublisher; ++import org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber; ++// Leaves end - Lithium Sleeping Block Entity + +-public final class ItemStack implements DataComponentHolder { ++public final class ItemStack implements DataComponentHolder, ChangePublisher, ChangeSubscriber { // Leaves - Lithium Sleeping Block Entity + private static final List OP_NBT_WARNING = List.of( + Component.translatable("item.op_warning.line1").withStyle(ChatFormatting.RED, ChatFormatting.BOLD), + Component.translatable("item.op_warning.line2").withStyle(ChatFormatting.RED), +@@ -958,6 +962,7 @@ public final class ItemStack implements DataComponentHolder { + + @Nullable + public T set(DataComponentType component, @Nullable T value) { ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && component == DataComponents.ENCHANTMENTS && this.subscriber instanceof ChangeSubscriber.EnchantmentSubscriber enchantmentSubscriber) enchantmentSubscriber.lithium$notifyAfterEnchantmentChange(this, this.subscriberData); // Leaves - Lithium Sleeping Block Entity + return this.components.set(component, value); + } + +@@ -1302,6 +1307,23 @@ public final class ItemStack implements DataComponentHolder { + } + + public void setCount(int count) { ++ // Leaves start - Lithium Sleeping Block Entity ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && count != this.count) { ++ if (this.subscriber instanceof ChangeSubscriber.CountChangeSubscriber countChangeSubscriber) { ++ countChangeSubscriber.lithium$notifyCount(this, this.subscriberData, count); ++ } ++ ++ if (count == 0) { ++ this.components.lithium$unsubscribe(this); ++ ++ if (this.subscriber != null) { ++ this.subscriber.lithium$forceUnsubscribe(this, this.subscriberData); ++ this.subscriber = null; ++ this.subscriberData = 0; ++ } ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + this.count = count; + } + +@@ -1357,4 +1379,90 @@ public final class ItemStack implements DataComponentHolder { + public boolean canDestroyBlock(BlockState state, Level level, BlockPos pos, Player player) { + return this.getItem().canDestroyBlock(this, state, level, pos, player); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private ChangeSubscriber subscriber; ++ private int subscriberData; ++ ++ @Override ++ public void lithium$subscribe(ChangeSubscriber subscriber, int subscriberData) { ++ if (this.isEmpty()) { ++ throw new IllegalStateException("Cannot subscribe to an empty ItemStack!"); ++ } ++ ++ if (this.subscriber == null) { ++ this.startTrackingChanges(); ++ } ++ this.subscriber = ChangeSubscriber.combine(this.subscriber, this.subscriberData, subscriber, subscriberData); ++ if (this.subscriber instanceof ChangeSubscriber.Multi) { ++ this.subscriberData = 0; ++ } else { ++ this.subscriberData = subscriberData; ++ } ++ } ++ ++ @Override ++ public int lithium$unsubscribe(ChangeSubscriber subscriber) { ++ if (this.isEmpty()) { ++ throw new IllegalStateException("Cannot unsubscribe from an empty ItemStack!"); ++ } ++ ++ int retval = ChangeSubscriber.dataOf(this.subscriber, subscriber, this.subscriberData); ++ this.subscriberData = ChangeSubscriber.dataWithout(this.subscriber, subscriber, this.subscriberData); ++ this.subscriber = ChangeSubscriber.without(this.subscriber, subscriber); ++ ++ if (this.subscriber == null) { ++ this.components.lithium$unsubscribe(this); ++ } ++ return retval; ++ } ++ ++ @Override ++ public void lithium$unsubscribeWithData(ChangeSubscriber subscriber, int subscriberData) { ++ if (this.isEmpty()) { ++ throw new IllegalStateException("Cannot unsubscribe from an empty ItemStack!"); ++ } ++ ++ this.subscriberData = ChangeSubscriber.dataWithout(this.subscriber, subscriber, this.subscriberData, subscriberData, true); ++ this.subscriber = ChangeSubscriber.without(this.subscriber, subscriber, subscriberData, true); ++ ++ if (this.subscriber == null) { ++ this.components.lithium$unsubscribe(this); ++ } ++ } ++ ++ @Override ++ public boolean lithium$isSubscribedWithData(ChangeSubscriber subscriber, int subscriberData) { ++ if (this.isEmpty()) { ++ throw new IllegalStateException("Cannot be subscribed to an empty ItemStack!"); ++ } ++ ++ return ChangeSubscriber.containsSubscriber(this.subscriber, this.subscriberData, subscriber, subscriberData); ++ } ++ ++ @Override ++ public void lithium$forceUnsubscribe(PatchedDataComponentMap publisher, int subscriberData) { ++ if (publisher != this.components) { ++ throw new IllegalStateException("Invalid publisher, expected " + this.components + " but got " + publisher); ++ } ++ this.subscriber.lithium$forceUnsubscribe(this, this.subscriberData); ++ this.subscriber = null; ++ this.subscriberData = 0; ++ } ++ ++ private void startTrackingChanges() { ++ this.components.lithium$subscribe(this, 0); ++ } ++ ++ @Override ++ public void lithium$notify(PatchedDataComponentMap publisher, int subscriberData) { ++ if (publisher != this.components) { ++ throw new IllegalStateException("Invalid publisher, expected " + this.components + " but got " + publisher); ++ } ++ ++ if (this.subscriber != null) { ++ this.subscriber.lithium$notify(this, this.subscriberData); ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index f31a48530e965f01507c335f50d898ed72767e3c..07f66ff104351abd9bd3632d958752cc6bc0395d 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -1224,6 +1224,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl + int i = flags & -34; + // Leaves start - no block update + if (org.leavesmc.leaves.command.subcommands.BlockUpdateCommand.isNoBlockUpdate()) { ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) org.leavesmc.leaves.lithium.common.hopper.HopperHelper.updateHopperOnUpdateSuppression(this, pos, flags, chunkAt, oldState != currentState); // Leaves - Lithium Sleeping Block Entity + this.updatePOIOnBlockStateChange(pos, blockState, blockState1); + return; + } +@@ -1487,7 +1488,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl + // Spigot end + if (tickingBlockEntity.isRemoved()) { + toRemove.add(tickingBlockEntity); // Paper - Fix MC-117075; use removeAll +- } else if (runsNormally && this.shouldTickBlocksAt(tickingBlockEntity.getPos())) { ++ } else if (runsNormally && org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity ? this.shouldTickBlockPosFilterNull(tickingBlockEntity.getPos()) : this.shouldTickBlocksAt(tickingBlockEntity.getPos())) { // Leaves - Lithium Sleeping Block Entity + tickingBlockEntity.tick(); + // Paper start - rewrite chunk system + if ((++tickedEntities & 7) == 0) { +@@ -2149,4 +2150,25 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl + return this.id; + } + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ public BlockEntity lithium$getLoadedExistingBlockEntity(BlockPos pos) { ++ if (!this.isOutsideBuildHeight(pos)) { ++ if (this.isClientSide || Thread.currentThread() == this.thread) { ++ ChunkAccess chunk = this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()), ChunkStatus.FULL, false); ++ if (chunk != null) { ++ return chunk.getBlockEntity(pos); ++ } ++ } ++ } ++ return null; ++ } ++ ++ private boolean shouldTickBlockPosFilterNull(BlockPos pos) { ++ if (pos == null) { ++ return false; ++ } ++ return shouldTickBlocksAt(pos); ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/ComposterBlock.java b/net/minecraft/world/level/block/ComposterBlock.java +index a647d76d365a60b95a3eb7927ac426bf70d417f3..c84c8e7080cd71d19fab3dbc34ae3e83cd1fc132 100644 +--- a/net/minecraft/world/level/block/ComposterBlock.java ++++ b/net/minecraft/world/level/block/ComposterBlock.java +@@ -411,7 +411,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + } + } + +- public static class EmptyContainer extends SimpleContainer implements WorldlyContainer { ++ public static class EmptyContainer extends SimpleContainer implements WorldlyContainer, org.leavesmc.leaves.lithium.common.hopper.BlockStateOnlyInventory { // Leaves - Lithium Sleeping Block Entity + public EmptyContainer(LevelAccessor levelAccessor, BlockPos blockPos) { // CraftBukkit + super(0); + this.bukkitOwner = new org.bukkit.craftbukkit.inventory.CraftBlockInventoryHolder(levelAccessor, blockPos, this); // CraftBukkit +@@ -433,7 +433,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + } + } + +- public static class InputContainer extends SimpleContainer implements WorldlyContainer { ++ public static class InputContainer extends SimpleContainer implements WorldlyContainer, org.leavesmc.leaves.lithium.common.hopper.BlockStateOnlyInventory { // Leaves - Lithium Sleeping Block Entity + private final BlockState state; + private final LevelAccessor level; + private final BlockPos pos; +@@ -479,12 +479,13 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + } + // Paper end - Add CompostItemEvent and EntityCompostItemEvent + this.level.levelEvent(1500, this.pos, blockState != this.state ? 1 : 0); ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) this.changed = false; // Leaves - Lithium Sleeping Block Entity + this.removeItemNoUpdate(0); + } + } + } + +- public static class OutputContainer extends SimpleContainer implements WorldlyContainer { ++ public static class OutputContainer extends SimpleContainer implements WorldlyContainer, org.leavesmc.leaves.lithium.common.hopper.BlockStateOnlyInventory { // Leaves - Lithium Sleeping Block Entity + private final BlockState state; + private final LevelAccessor level; + private final BlockPos pos; +diff --git a/net/minecraft/world/level/block/DiodeBlock.java b/net/minecraft/world/level/block/DiodeBlock.java +index 94d35e4812705771756a0ee1a9b0255be75e3770..f2e19b6e89195fa99a2eca7f1c647e94b5594689 100644 +--- a/net/minecraft/world/level/block/DiodeBlock.java ++++ b/net/minecraft/world/level/block/DiodeBlock.java +@@ -173,6 +173,7 @@ public abstract class DiodeBlock extends HorizontalDirectionalBlock { + @Override + protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving) { + this.updateNeighborsInFront(level, pos, state); ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && this instanceof ComparatorBlock && !oldState.is(Blocks.COMPARATOR)) org.leavesmc.leaves.lithium.common.block.entity.inventory_comparator_tracking.ComparatorTracking.notifyNearbyBlockEntitiesAboutNewComparator(level, pos); // Leaves - Lithium Sleeping Block Entity + } + + // Leaves start - behaviour 1.21.1- +diff --git a/net/minecraft/world/level/block/HopperBlock.java b/net/minecraft/world/level/block/HopperBlock.java +index d3535a9dc462d92a306aea981d7799d8d975f59d..47377bc98eb183c2761c710575a7d811261f02e0 100644 +--- a/net/minecraft/world/level/block/HopperBlock.java ++++ b/net/minecraft/world/level/block/HopperBlock.java +@@ -38,7 +38,7 @@ import net.minecraft.world.phys.shapes.CollisionContext; + import net.minecraft.world.phys.shapes.Shapes; + import net.minecraft.world.phys.shapes.VoxelShape; + +-public class HopperBlock extends BaseEntityBlock { ++public class HopperBlock extends BaseEntityBlock implements org.leavesmc.leaves.lithium.common.block.entity.ShapeUpdateHandlingBlockBehaviour { // Leaves - Lithium Sleeping Block Entity + public static final MapCodec CODEC = simpleCodec(HopperBlock::new); + public static final EnumProperty FACING = BlockStateProperties.FACING_HOPPER; + public static final BooleanProperty ENABLED = BlockStateProperties.ENABLED; +@@ -101,6 +101,17 @@ public class HopperBlock extends BaseEntityBlock { + protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving) { + if (!oldState.is(state.getBlock())) { + this.checkPoweredState(level, pos, state); ++ // Leaves start - Lithium Sleeping Block Entity ++ //invalidate caches of nearby hoppers when placing an update suppressed hopper ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && level.getBlockState(pos) != state) { ++ for (Direction direction : UPDATE_SHAPE_ORDER) { ++ BlockEntity hopper = level.lithium$getLoadedExistingBlockEntity(pos.relative(direction)); ++ if (hopper instanceof org.leavesmc.leaves.lithium.common.hopper.UpdateReceiver updateReceiver) { ++ updateReceiver.lithium$invalidateCacheOnNeighborUpdate(direction == Direction.DOWN); ++ } ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } + } + +@@ -115,6 +126,7 @@ public class HopperBlock extends BaseEntityBlock { + + @Override + protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston) { ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && level.lithium$getLoadedExistingBlockEntity(pos) instanceof org.leavesmc.leaves.lithium.common.hopper.UpdateReceiver updateReceiver) updateReceiver.lithium$invalidateCacheOnUndirectedNeighborUpdate(); // Leaves - Lithium Sleeping Block Entity /* invalidate cache when the block is replaced */ + this.checkPoweredState(level, pos, state); + } + +@@ -176,4 +188,25 @@ public class HopperBlock extends BaseEntityBlock { + protected boolean isPathfindable(BlockState state, PathComputationType pathComputationType) { + return false; + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ @Override ++ public void lithium$handleShapeUpdate(net.minecraft.world.level.LevelReader levelReader, BlockState myBlockState, BlockPos myPos, BlockPos posFrom, BlockState newState) { ++ //invalidate cache when composters change state ++ if (newState.getBlock() instanceof net.minecraft.world.WorldlyContainerHolder) { ++ this.updateHopper(levelReader, myBlockState, myPos, posFrom); ++ } ++ } ++ ++ private void updateHopper(net.minecraft.world.level.LevelReader world, BlockState myBlockState, BlockPos myPos, BlockPos posFrom) { ++ Direction facing = myBlockState.getValue(HopperBlock.FACING); ++ boolean above = posFrom.getY() == myPos.getY() + 1; ++ if (above || posFrom.getX() == myPos.getX() + facing.getStepX() && posFrom.getY() == myPos.getY() + facing.getStepY() && posFrom.getZ() == myPos.getZ() + facing.getStepZ()) { ++ BlockEntity hopper = ((net.minecraft.world.level.Level) world).lithium$getLoadedExistingBlockEntity(myPos); ++ if (hopper instanceof org.leavesmc.leaves.lithium.common.hopper.UpdateReceiver updateReceiver) { ++ updateReceiver.lithium$invalidateCacheOnNeighborUpdate(above); ++ } ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java b/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +index 646d4c26870bb03f6d397b5e03ad97923d0928b2..77cfc97b3c8926b8126076f41c801c418f3b93fd 100644 +--- a/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +@@ -39,7 +39,7 @@ import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + import net.minecraft.world.phys.Vec3; + +-public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntity implements WorldlyContainer, RecipeCraftingHolder, StackedContentsCompatible { ++public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntity implements WorldlyContainer, RecipeCraftingHolder, StackedContentsCompatible, org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker, org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity, org.leavesmc.leaves.lithium.common.block.entity.SetChangedHandlingBlockEntity, org.leavesmc.leaves.lithium.api.inventory.LithiumInventory { // Leaves - Lithium Sleeping Block Entity + protected static final int SLOT_INPUT = 0; + protected static final int SLOT_FUEL = 1; + protected static final int SLOT_RESULT = 2; +@@ -164,6 +164,7 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + this.recipesUsed.clear(); + this.recipesUsed.putAll(input.read("RecipesUsed", RECIPES_USED_CODEC).orElse(Map.of())); + this.cookSpeedMultiplier = input.getDoubleOr("Paper.CookSpeedMultiplier", 1); // Paper - cook speed multiplier API ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && this.isSleeping() && this.level != null) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + } + + @Override +@@ -269,6 +270,7 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + if (flag) { + setChanged(level, pos, state); + } ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) furnace.checkSleep(state); // Leaves - Lithium Sleeping Block Entity + } + + private static boolean canBurn( +@@ -541,4 +543,53 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + this.getRecipesToAwardAndPopExperience(serverLevel, Vec3.atCenterOf(pos)); + } + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = null; ++ private TickingBlockEntity sleepingTicker = null; ++ ++ @Override ++ public net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { ++ return tickWrapper; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ this.lithium$setSleepingTicker(null); ++ } ++ ++ @Override ++ public TickingBlockEntity lithium$getSleepingTicker() { ++ return sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ ++ private void checkSleep(BlockState state) { ++ if (!this.isLit() && this.cookingTimer == 0 && (state.is(Blocks.FURNACE) || state.is(Blocks.BLAST_FURNACE) || state.is(Blocks.SMOKER)) && this.level != null) { ++ this.lithium$startSleeping(); ++ } ++ } ++ ++ @Override ++ public void lithium$handleSetChanged() { ++ if (this.isSleeping() && this.level != null && !this.level.isClientSide) { ++ this.wakeUpNow(); ++ } ++ } ++ ++ @Override ++ public net.minecraft.core.NonNullList getInventoryLithium() { ++ return items; ++ } ++ ++ @Override ++ public void setInventoryLithium(net.minecraft.core.NonNullList inventory) { ++ items = inventory; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/BarrelBlockEntity.java b/net/minecraft/world/level/block/entity/BarrelBlockEntity.java +index f0ba09c0edc598dfc4e501ba69016e8a2f8d3a7c..2bb30d6c5ca56b47d685e1e095dbeb046720cd04 100644 +--- a/net/minecraft/world/level/block/entity/BarrelBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BarrelBlockEntity.java +@@ -20,7 +20,7 @@ import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + +-public class BarrelBlockEntity extends RandomizableContainerBlockEntity { ++public class BarrelBlockEntity extends RandomizableContainerBlockEntity implements org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker, org.leavesmc.leaves.lithium.api.inventory.LithiumInventory { // Leaves - Lithium Sleeping Block Entity + // CraftBukkit start - add fields and methods + public java.util.List transaction = new java.util.ArrayList<>(); + private int maxStack = MAX_STACK; +@@ -119,6 +119,7 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity { + @Override + protected void setItems(NonNullList items) { + this.items = items; ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) this.lithium$emitStackListReplaced(); // Leaves - Lithium Sleeping Block Entity + } + + // Leaves start - pca +@@ -172,4 +173,18 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity { + double d2 = this.worldPosition.getZ() + 0.5 + unitVec3i.getZ() / 2.0; + this.level.playSound(null, d, d1, d2, sound, SoundSource.BLOCKS, 0.5F, this.level.random.nextFloat() * 0.1F + 0.9F); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ ++ ++ @Override ++ public net.minecraft.core.NonNullList getInventoryLithium() { ++ return items; ++ } ++ ++ @Override ++ public void setInventoryLithium(net.minecraft.core.NonNullList inventory) { ++ items = inventory; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java b/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java +index 561acadcf81dc219d88e8ec8bdbd4f5f8fcbadc3..bfa6b473d44a5494fcbcc03a690db6292ee67320 100644 +--- a/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java +@@ -23,8 +23,17 @@ import net.minecraft.world.item.component.ItemContainerContents; + import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; ++// Leaves start - Lithium Sleeping Block Entity ++import it.unimi.dsi.fastutil.objects.ReferenceArraySet; ++import org.leavesmc.leaves.lithium.api.inventory.LithiumInventory; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeEmitter; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeListener; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker; ++import org.leavesmc.leaves.lithium.common.hopper.InventoryHelper; ++import org.leavesmc.leaves.lithium.common.hopper.LithiumStackList; ++// Leaves end - Lithium Sleeping Block Entity + +-public abstract class BaseContainerBlockEntity extends BlockEntity implements Container, MenuProvider, Nameable { ++public abstract class BaseContainerBlockEntity extends BlockEntity implements Container, MenuProvider, Nameable, InventoryChangeEmitter { + public LockCode lockKey = LockCode.NO_LOCK; + @Nullable + public Component name; +@@ -38,6 +47,7 @@ public abstract class BaseContainerBlockEntity extends BlockEntity implements Co + super.loadAdditional(input); + this.lockKey = LockCode.fromTag(input); + this.name = parseCustomNameSafe(input, "CustomName"); ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && this instanceof InventoryChangeTracker inventoryChangeTracker) inventoryChangeTracker.lithium$emitStackListReplaced(); // Leaves - Lithium Sleeping Block Entity + } + + @Override +@@ -206,4 +216,97 @@ public abstract class BaseContainerBlockEntity extends BlockEntity implements Co + return org.bukkit.craftbukkit.util.CraftLocation.toBukkit(this.worldPosition, this.level); + } + // CraftBukkit end ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ ReferenceArraySet inventoryChangeListeners = null; ++ ReferenceArraySet inventoryHandlingTypeListeners = null; ++ ++ @Override ++ public void lithium$emitContentModified() { ++ ReferenceArraySet inventoryChangeListeners = this.inventoryChangeListeners; ++ if (inventoryChangeListeners != null) { ++ for (InventoryChangeListener inventoryChangeListener : inventoryChangeListeners) { ++ inventoryChangeListener.lithium$handleInventoryContentModified(this); ++ } ++ inventoryChangeListeners.clear(); ++ } ++ } ++ ++ @Override ++ public void lithium$emitStackListReplaced() { ++ ReferenceArraySet listeners = this.inventoryHandlingTypeListeners; ++ if (listeners != null && !listeners.isEmpty()) { ++ for (InventoryChangeListener inventoryChangeListener : listeners) { ++ inventoryChangeListener.handleStackListReplaced(this); ++ } ++ listeners.clear(); ++ } ++ ++ if (this instanceof InventoryChangeListener listener) { ++ listener.handleStackListReplaced(this); ++ } ++ ++ this.invalidateChangeListening(); ++ } ++ ++ @Override ++ public void lithium$emitRemoved() { ++ ReferenceArraySet listeners = this.inventoryHandlingTypeListeners; ++ if (listeners != null && !listeners.isEmpty()) { ++ for (InventoryChangeListener listener : listeners) { ++ listener.lithium$handleInventoryRemoved(this); ++ } ++ listeners.clear(); ++ } ++ ++ if (this instanceof InventoryChangeListener listener) { ++ listener.lithium$handleInventoryRemoved(this); ++ } ++ ++ this.invalidateChangeListening(); ++ } ++ ++ private void invalidateChangeListening() { ++ if (this.inventoryChangeListeners != null) { ++ this.inventoryChangeListeners.clear(); ++ } ++ ++ LithiumStackList lithiumStackList = this instanceof LithiumInventory ? InventoryHelper.getLithiumStackListOrNull((LithiumInventory) this) : null; ++ if (lithiumStackList != null && this instanceof InventoryChangeTracker inventoryChangeTracker) { ++ lithiumStackList.removeInventoryModificationCallback(inventoryChangeTracker); ++ } ++ } ++ ++ @Override ++ public void lithium$emitFirstComparatorAdded() { ++ ReferenceArraySet inventoryChangeListeners = this.inventoryChangeListeners; ++ if (inventoryChangeListeners != null && !inventoryChangeListeners.isEmpty()) { ++ inventoryChangeListeners.removeIf(inventoryChangeListener -> inventoryChangeListener.lithium$handleComparatorAdded(this)); ++ } ++ } ++ ++ @Override ++ public void lithium$forwardContentChangeOnce(InventoryChangeListener inventoryChangeListener, LithiumStackList stackList, InventoryChangeTracker thisTracker) { ++ if (this.inventoryChangeListeners == null) { ++ this.inventoryChangeListeners = new ReferenceArraySet<>(1); ++ } ++ stackList.setInventoryModificationCallback(thisTracker); ++ this.inventoryChangeListeners.add(inventoryChangeListener); ++ ++ } ++ ++ @Override ++ public void lithium$forwardMajorInventoryChanges(InventoryChangeListener inventoryChangeListener) { ++ if (this.inventoryHandlingTypeListeners == null) { ++ this.inventoryHandlingTypeListeners = new ReferenceArraySet<>(1); ++ } ++ this.inventoryHandlingTypeListeners.add(inventoryChangeListener); ++ } ++ ++ @Override ++ public void lithium$stopForwardingMajorInventoryChanges(InventoryChangeListener inventoryChangeListener) { ++ if (this.inventoryHandlingTypeListeners != null) { ++ this.inventoryHandlingTypeListeners.remove(inventoryChangeListener); ++ } ++ } + } +diff --git a/net/minecraft/world/level/block/entity/BlockEntity.java b/net/minecraft/world/level/block/entity/BlockEntity.java +index 5986825d6a381eeb445dd424dd127864aa703163..4fbdf8c3edcba37d0e31fe6404a503e8b7f54940 100644 +--- a/net/minecraft/world/level/block/entity/BlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BlockEntity.java +@@ -33,8 +33,16 @@ import net.minecraft.world.level.storage.TagValueOutput; + import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + import org.slf4j.Logger; +- +-public abstract class BlockEntity { ++// Leaves start - Lithium Sleeping Block Entity ++import net.minecraft.core.Direction; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_comparator_tracking.ComparatorTracker; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_comparator_tracking.ComparatorTracking; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker; ++import org.leavesmc.leaves.lithium.common.block.entity.SetBlockStateHandlingBlockEntity; ++import org.leavesmc.leaves.lithium.common.block.entity.SetChangedHandlingBlockEntity; ++// Leaves end - Lithium Sleeping Block Entity ++ ++public abstract class BlockEntity implements ComparatorTracker, SetBlockStateHandlingBlockEntity, SetChangedHandlingBlockEntity { // Leaves - Lithium Sleeping Block Entity + static boolean ignoreBlockEntityUpdates; // Paper - Perf: Optimize Hoppers + // CraftBukkit start - data containers + private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry(); +@@ -56,6 +64,7 @@ public abstract class BlockEntity { + this.validateBlockState(blockState); + this.blockState = blockState; + this.persistentDataContainer = new org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer(DATA_TYPE_REGISTRY); // Paper - always init ++ this.hasComparators = UNKNOWN; // Leaves - Lithium Sleeping Block Entity + } + + private void validateBlockState(BlockState state) { +@@ -228,9 +237,10 @@ public abstract class BlockEntity { + + public void setChanged() { + if (this.level != null) { +- if (ignoreBlockEntityUpdates) return; // Paper - Perf: Optimize Hoppers ++ if (ignoreBlockEntityUpdates) { if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) lithium$handleSetChanged(); return; } // Paper - Perf: Optimize Hoppers // Leaves - Lithium Sleeping Block Entity - I do not know if this is needed. + setChanged(this.level, this.worldPosition, this.blockState); + } ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) lithium$handleSetChanged(); // Leaves - Lithium Sleeping Block Entity + } + + protected static void setChanged(Level level, BlockPos pos, BlockState state) { +@@ -262,7 +272,9 @@ public abstract class BlockEntity { + } + + public void setRemoved() { ++ this.hasComparators = UNKNOWN; // Leaves - Lithium Sleeping Block Entity + this.remove = true; ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && this.level != null && !this.level.isClientSide() && this instanceof InventoryChangeTracker inventoryChangeTracker) inventoryChangeTracker.lithium$emitRemoved(); // Leaves - Lithium Sleeping Block Entity + } + + public void clearRemoved() { +@@ -302,6 +314,7 @@ public abstract class BlockEntity { + public void setBlockState(BlockState blockState) { + this.validateBlockState(blockState); + this.blockState = blockState; ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) this.lithium$handleSetBlockState(); // Leaves - Lithium Sleeping Block Entity + } + + protected void applyImplicitComponents(DataComponentGetter componentGetter) { +@@ -402,4 +415,32 @@ public abstract class BlockEntity { + return this.blockEntity.getNameForReporting() + "@" + this.blockEntity.getBlockPos(); + } + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private static final byte UNKNOWN = (byte) -1; ++ private static final byte COMPARATOR_PRESENT = (byte) 1; ++ private static final byte COMPARATOR_ABSENT = (byte) 0; ++ ++ byte hasComparators; ++ ++ @Override ++ public void lithium$onComparatorAdded(Direction direction, int offset) { ++ byte hasComparators = this.hasComparators; ++ if (direction.getAxis() != Direction.Axis.Y && hasComparators != COMPARATOR_PRESENT && offset >= 1 && offset <= 2) { ++ this.hasComparators = COMPARATOR_PRESENT; ++ ++ if (this instanceof InventoryChangeTracker inventoryChangeTracker) { ++ inventoryChangeTracker.lithium$emitFirstComparatorAdded(); ++ } ++ } ++ } ++ ++ @Override ++ public boolean lithium$hasAnyComparatorNearby() { ++ if (this.hasComparators == UNKNOWN) { ++ this.hasComparators = ComparatorTracking.findNearbyComparators(this.level, this.worldPosition) ? COMPARATOR_PRESENT : COMPARATOR_ABSENT; ++ } ++ return this.hasComparators == COMPARATOR_PRESENT; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java b/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java +index 7eac2188a38fff2ecfa4082b5d023b111cf7d8f6..d794e324f6c8d70f63798f251dce79bb6e711d50 100644 +--- a/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java +@@ -24,7 +24,7 @@ import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + +-public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements WorldlyContainer { ++public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements WorldlyContainer, org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker, org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity, org.leavesmc.leaves.lithium.common.block.entity.SetChangedHandlingBlockEntity, org.leavesmc.leaves.lithium.api.inventory.LithiumInventory { // Leaves - Lithium Sleeping Block Entity- + private static final int INGREDIENT_SLOT = 3; + private static final int FUEL_SLOT = 4; + private static final int[] SLOTS_FOR_UP = new int[]{3}; +@@ -135,6 +135,7 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements + } + + public static void serverTick(Level level, BlockPos pos, BlockState state, BrewingStandBlockEntity blockEntity) { ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) blockEntity.checkSleep(state); // Leaves - Lithium Sleeping Block Entity + ItemStack itemStack = blockEntity.items.get(4); + if (blockEntity.fuel <= 0 && itemStack.is(ItemTags.BREWING_FUEL)) { + // CraftBukkit start +@@ -153,6 +154,7 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements + } + // CraftBukkit end + setChanged(level, pos, state); ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) blockEntity.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + } + + boolean isBrewable = isBrewable(level.potionBrewing(), blockEntity.items); +@@ -285,6 +287,7 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements + } + + this.fuel = input.getByteOr("Fuel", (byte)0); ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && this.isSleeping() && this.level != null) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + } + + @Override +@@ -341,4 +344,52 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements + } + } + // Leaves end - pca ++ // Leaves start - Lithium Sleeping Block Entity ++ private net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = null; ++ private TickingBlockEntity sleepingTicker = null; ++ ++ @Override ++ public net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { ++ return tickWrapper; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ this.lithium$setSleepingTicker(null); ++ } ++ ++ @Override ++ public TickingBlockEntity lithium$getSleepingTicker() { ++ return sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ ++ private void checkSleep(BlockState state) { ++ if (this.brewTime == 0 && state.is(net.minecraft.world.level.block.Blocks.BREWING_STAND) && this.level != null) { ++ this.lithium$startSleeping(); ++ } ++ } ++ ++ @Override ++ public void lithium$handleSetChanged() { ++ if (this.isSleeping() && this.level != null && !this.level.isClientSide) { ++ this.wakeUpNow(); ++ } ++ } ++ ++ @Override ++ public net.minecraft.core.NonNullList getInventoryLithium() { ++ return items; ++ } ++ ++ @Override ++ public void setInventoryLithium(net.minecraft.core.NonNullList inventory) { ++ items = inventory; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/CampfireBlockEntity.java b/net/minecraft/world/level/block/entity/CampfireBlockEntity.java +index fb7932e17d7d00ee3050e71c88510fa23befb1bb..6e3cfa4bb776299be4faf48fe165f5120cae2dc5 100644 +--- a/net/minecraft/world/level/block/entity/CampfireBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/CampfireBlockEntity.java +@@ -38,7 +38,7 @@ import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + import org.slf4j.Logger; + +-public class CampfireBlockEntity extends BlockEntity implements Clearable { ++public class CampfireBlockEntity extends BlockEntity implements Clearable, org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity { // Leaves - Lithium Sleeping Block Entity + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int BURN_COOL_SPEED = 2; + private static final int NUM_SLOTS = 4; +@@ -112,7 +112,7 @@ public class CampfireBlockEntity extends BlockEntity implements Clearable { + + if (flag) { + setChanged(level, pos, state); +- } ++ } else if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) campfire.lithium$startSleeping(); // Leaves - Lithium Sleeping Block Entity + } + + public static void cooldownTick(Level level, BlockPos pos, BlockState state, CampfireBlockEntity blockEntity) { +@@ -127,7 +127,7 @@ public class CampfireBlockEntity extends BlockEntity implements Clearable { + + if (flag) { + setChanged(level, pos, state); +- } ++ } else if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) blockEntity.lithium$startSleeping(); // Leaves - Lithium Sleeping Block Entity + } + + public static void particleTick(Level level, BlockPos pos, BlockState state, CampfireBlockEntity blockEntity) { +@@ -183,6 +183,7 @@ public class CampfireBlockEntity extends BlockEntity implements Clearable { + System.arraycopy(cookingState, 0, this.stopCooking, 0, Math.min(this.stopCooking.length, bytes.capacity())); + }); + // Paper end - Add more Campfire API ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + } + + @Override +@@ -237,6 +238,7 @@ public class CampfireBlockEntity extends BlockEntity implements Clearable { + this.cookingTime[i] = event.getTotalCookTime(); // i -> event.getTotalCookTime() + // CraftBukkit end + this.cookingProgress[i] = 0; ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + this.items.set(i, stack.consumeAndReturn(1, entity)); + level.gameEvent(GameEvent.BLOCK_CHANGE, this.getBlockPos(), GameEvent.Context.of(entity, this.getBlockState())); + this.markUpdated(); +@@ -280,4 +282,30 @@ public class CampfireBlockEntity extends BlockEntity implements Clearable { + public void removeComponentsFromTag(ValueOutput output) { + output.discard("Items"); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = null; ++ private TickingBlockEntity sleepingTicker = null; ++ ++ @Override ++ public net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { ++ return tickWrapper; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ this.lithium$setSleepingTicker(null); ++ } ++ ++ @Override ++ public TickingBlockEntity lithium$getSleepingTicker() { ++ return sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/ChestBlockEntity.java b/net/minecraft/world/level/block/entity/ChestBlockEntity.java +index 784e146b28370dc2dac094d5f2ac654a5bc47e01..b1c851771e03a70d9c6a87173e81d6dd2e43f057 100644 +--- a/net/minecraft/world/level/block/entity/ChestBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/ChestBlockEntity.java +@@ -24,7 +24,7 @@ import net.minecraft.world.level.block.state.properties.ChestType; + import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + +-public class ChestBlockEntity extends RandomizableContainerBlockEntity implements LidBlockEntity { ++public class ChestBlockEntity extends RandomizableContainerBlockEntity implements LidBlockEntity, org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker, org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeEmitter, org.leavesmc.leaves.lithium.common.block.entity.SetBlockStateHandlingBlockEntity, org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity, org.leavesmc.leaves.lithium.api.inventory.LithiumInventory { // Leaves - Lithium Sleeping Block Entity + private static final int EVENT_SET_OPEN_COUNT = 1; + private NonNullList items = NonNullList.withSize(27, ItemStack.EMPTY); + public final ContainerOpenersCounter openersCounter = new ContainerOpenersCounter() { +@@ -127,6 +127,7 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement + + public static void lidAnimateTick(Level level, BlockPos pos, BlockState state, ChestBlockEntity blockEntity) { + blockEntity.chestLidController.tickLid(); ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) blockEntity.checkSleep(); // Leaves - Lithium Sleeping Block Entity + } + + public static void playSound(Level level, BlockPos pos, BlockState state, SoundEvent sound) { +@@ -148,6 +149,7 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement + @Override + public boolean triggerEvent(int id, int type) { + if (id == 1) { ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && this.sleepingTicker != null) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + this.chestLidController.shouldBeOpen(type > 0); + return true; + } else { +@@ -177,6 +179,7 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement + @Override + protected void setItems(NonNullList items) { + this.items = items; ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) this.lithium$emitStackListReplaced(); // Leaves - Lithium Sleeping Block Entity + } + + @Override +@@ -227,4 +230,52 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement + Block block = state.getBlock(); + level.blockEvent(pos, block, 1, eventParam); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = null; ++ private TickingBlockEntity sleepingTicker = null; ++ ++ private void checkSleep() { ++ //If the animation is finished, it will stay unchanged until the next triggerEvent, which may change shouldBeOpen ++ if (this.getOpenNess(0.0F) == this.getOpenNess(1.0F)) { ++ this.lithium$startSleeping(); ++ } ++ } ++ ++ @Override ++ public net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { ++ return this.tickWrapper; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ } ++ ++ @Override ++ public TickingBlockEntity lithium$getSleepingTicker() { ++ return this.sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$handleSetBlockState() { ++ //Handle switching double / single chest state ++ this.lithium$emitRemoved(); ++ } ++ ++ @Override ++ public net.minecraft.core.NonNullList getInventoryLithium() { ++ return items; ++ } ++ ++ @Override ++ public void setInventoryLithium(net.minecraft.core.NonNullList inventory) { ++ items = inventory; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java b/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java +index 969ac280ae563e3412dba406ba68ceaa8a75d519..b7e804676c39a26174758c9491f2ef4209b51f2e 100644 +--- a/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java +@@ -22,7 +22,7 @@ import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + import org.slf4j.Logger; + +-public class ChiseledBookShelfBlockEntity extends BlockEntity implements Container { ++public class ChiseledBookShelfBlockEntity extends BlockEntity implements Container, org.leavesmc.leaves.lithium.api.inventory.LithiumTransferConditionInventory { // Leaves - Lithium Sleeping Block Entity + public static final int MAX_BOOKS_IN_STORAGE = 6; + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int DEFAULT_LAST_INTERACTED_SLOT = -1; +@@ -195,4 +195,6 @@ public class ChiseledBookShelfBlockEntity extends BlockEntity implements Contain + public void removeComponentsFromTag(ValueOutput output) { + output.discard("Items"); + } ++ ++ @Override public boolean lithium$itemInsertionTestRequiresStackSize1() {return true;} // Leaves - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/CrafterBlockEntity.java b/net/minecraft/world/level/block/entity/CrafterBlockEntity.java +index 9ce4b5a3954eda08ef587cf95dec8ed119b7a598..46939269cfb7491fdf0be7ac74a78df5476637e8 100644 +--- a/net/minecraft/world/level/block/entity/CrafterBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/CrafterBlockEntity.java +@@ -22,7 +22,7 @@ import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + +-public class CrafterBlockEntity extends RandomizableContainerBlockEntity implements CraftingContainer { ++public class CrafterBlockEntity extends RandomizableContainerBlockEntity implements CraftingContainer, org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity, org.leavesmc.leaves.lithium.common.block.entity.SetChangedHandlingBlockEntity { + public static final int CONTAINER_WIDTH = 3; + public static final int CONTAINER_HEIGHT = 3; + public static final int CONTAINER_SIZE = 9; +@@ -169,6 +169,7 @@ public class CrafterBlockEntity extends RandomizableContainerBlockEntity impleme + } + }); + this.containerData.set(9, input.getIntOr("triggered", 0)); ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && this.isSleeping() && this.level != null) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + } + + @Override +@@ -278,10 +279,12 @@ public class CrafterBlockEntity extends RandomizableContainerBlockEntity impleme + level.setBlock(pos, state.setValue(CrafterBlock.CRAFTING, false), 3); + } + } ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && i < 0) crafter.checkSleep(); // Leaves - Lithium Sleeping Block Entity + } + + public void setCraftingTicksRemaining(int craftingTicksRemaining) { + this.craftingTicksRemaining = craftingTicksRemaining; ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && this.isSleeping() && this.level != null) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + } + + public int getRedstoneSignal() { +@@ -300,4 +303,43 @@ public class CrafterBlockEntity extends RandomizableContainerBlockEntity impleme + private boolean slotCanBeDisabled(int slot) { + return slot > -1 && slot < 9 && this.items.get(slot).isEmpty(); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = null; ++ private TickingBlockEntity sleepingTicker = null; ++ ++ @Override ++ public net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { ++ return this.tickWrapper; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ this.lithium$setSleepingTicker(null); ++ } ++ ++ @Override ++ public TickingBlockEntity lithium$getSleepingTicker() { ++ return this.sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ ++ private void checkSleep() { ++ if (this.craftingTicksRemaining == 0) { ++ this.lithium$startSleeping(); ++ } ++ } ++ ++ @Override ++ public void lithium$handleSetChanged() { ++ if (this.isSleeping() && this.level != null && !this.level.isClientSide) { ++ this.wakeUpNow(); ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/DispenserBlockEntity.java b/net/minecraft/world/level/block/entity/DispenserBlockEntity.java +index 36b9362e1ce31b63c100ec65921e095fb7871e82..fb5a7af71fee8d680eb739f45e58ec53cfc2839f 100644 +--- a/net/minecraft/world/level/block/entity/DispenserBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/DispenserBlockEntity.java +@@ -13,7 +13,7 @@ import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + +-public class DispenserBlockEntity extends RandomizableContainerBlockEntity { ++public class DispenserBlockEntity extends RandomizableContainerBlockEntity implements org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker, org.leavesmc.leaves.lithium.api.inventory.LithiumInventory { // Leaves - Lithium Sleeping Block Entity + public static final int CONTAINER_SIZE = 9; + private NonNullList items = NonNullList.withSize(9, ItemStack.EMPTY); + +@@ -144,10 +144,23 @@ public class DispenserBlockEntity extends RandomizableContainerBlockEntity { + @Override + protected void setItems(NonNullList items) { + this.items = items; ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) this.lithium$emitStackListReplaced(); // Leaves - Lithium Sleeping Block Entity + } + + @Override + protected AbstractContainerMenu createMenu(int id, Inventory player) { + return new DispenserMenu(id, player, this); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ @Override ++ public net.minecraft.core.NonNullList getInventoryLithium() { ++ return items; ++ } ++ ++ @Override ++ public void setInventoryLithium(net.minecraft.core.NonNullList inventory) { ++ items = inventory; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/EnderChestBlockEntity.java b/net/minecraft/world/level/block/entity/EnderChestBlockEntity.java +index 363d85c96bd3fb1a1945595df36e30bd6dd2fa4e..542910b3d5faa85f2b14022932c058bcd1d8d904 100644 +--- a/net/minecraft/world/level/block/entity/EnderChestBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/EnderChestBlockEntity.java +@@ -9,7 +9,7 @@ import net.minecraft.world.level.Level; + import net.minecraft.world.level.block.Blocks; + import net.minecraft.world.level.block.state.BlockState; + +-public class EnderChestBlockEntity extends BlockEntity implements LidBlockEntity { ++public class EnderChestBlockEntity extends BlockEntity implements LidBlockEntity, org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity { // Leaves - Lithium Sleeping Block Entity + private final ChestLidController chestLidController = new ChestLidController(); + public final ContainerOpenersCounter openersCounter = new ContainerOpenersCounter() { + @Override +@@ -57,11 +57,13 @@ public class EnderChestBlockEntity extends BlockEntity implements LidBlockEntity + + public static void lidAnimateTick(Level level, BlockPos pos, BlockState state, EnderChestBlockEntity blockEntity) { + blockEntity.chestLidController.tickLid(); ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) blockEntity.checkSleep(); // Leaves - Lithium Sleeping Block Entity + } + + @Override + public boolean triggerEvent(int id, int type) { + if (id == 1) { ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && this.sleepingTicker != null) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + this.chestLidController.shouldBeOpen(type > 0); + return true; + } else { +@@ -95,4 +97,36 @@ public class EnderChestBlockEntity extends BlockEntity implements LidBlockEntity + public float getOpenNess(float partialTicks) { + return this.chestLidController.getOpenness(partialTicks); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = null; ++ private TickingBlockEntity sleepingTicker = null; ++ ++ @Override ++ public net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { ++ return this.tickWrapper; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ } ++ ++ @Override ++ public TickingBlockEntity lithium$getSleepingTicker() { ++ return this.sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ ++ private void checkSleep() { ++ //If the animation is finished, it will stay unchanged until the next triggerEvent, which may change shouldBeOpen ++ if (this.getOpenNess(0.0F) == this.getOpenNess(1.0F)) { ++ this.lithium$startSleeping(); ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index a900cde0d7e6ddd6faf961e7861c3cc499164d7a..4f87c15d5ffdb23c97bbeb68bc42c029eba06277 100644 +--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -27,8 +27,30 @@ import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + import net.minecraft.world.phys.AABB; +- +-public class HopperBlockEntity extends RandomizableContainerBlockEntity implements Hopper { ++// Leaves start - Lithium Sleeping Block Entity ++import java.util.Objects; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.CompoundContainer; ++import net.minecraft.server.level.ServerLevel; ++import org.leavesmc.leaves.lithium.api.inventory.LithiumCooldownReceivingInventory; ++import org.leavesmc.leaves.lithium.api.inventory.LithiumInventory; ++import org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeListener; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_comparator_tracking.ComparatorTracker; ++import org.leavesmc.leaves.lithium.common.hopper.BlockStateOnlyInventory; ++import org.leavesmc.leaves.lithium.common.hopper.HopperCachingState; ++import org.leavesmc.leaves.lithium.common.hopper.HopperHelper; ++import org.leavesmc.leaves.lithium.common.hopper.InventoryHelper; ++import org.leavesmc.leaves.lithium.common.hopper.LithiumStackList; ++import org.leavesmc.leaves.lithium.common.hopper.UpdateReceiver; ++import org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionEntityMovementListener; ++import org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionEntityMovementTracker; ++import org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionInventoryEntityTracker; ++import org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionItemEntityMovementTracker; ++// Leaves end - Lithium Sleeping Block Entity ++ ++public class HopperBlockEntity extends RandomizableContainerBlockEntity implements Hopper, SleepingBlockEntity, ChunkSectionEntityMovementListener, LithiumInventory, InventoryChangeListener, UpdateReceiver, InventoryChangeTracker { // Leaves - Lithium Sleeping Block Entity + public static final int MOVE_ITEM_SPEED = 8; + public static final int HOPPER_CONTAINER_SIZE = 5; + private static final int[][] CACHED_SLOTS = new int[54][]; +@@ -123,6 +145,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + @Override + public void setBlockState(BlockState blockState) { ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && this.level != null && !this.level.isClientSide() && blockState.getValue(HopperBlock.FACING) != this.getBlockState().getValue(HopperBlock.FACING)) this.invalidateCachedData(); // Leaves - Lithium Sleeping Block Entity + super.setBlockState(blockState); + this.facing = blockState.getValue(HopperBlock.FACING); + } +@@ -151,6 +174,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + boolean result = tryMoveItems(level, pos, state, blockEntity, () -> { + return suckInItems(level, blockEntity); + }); ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) blockEntity.checkSleepingConditions(); // Leaves - Lithium Sleeping Block Entity + if (!result && blockEntity.level.spigotConfig.hopperCheck > 1) { + blockEntity.setCooldown(blockEntity.level.spigotConfig.hopperCheck); + } +@@ -218,6 +242,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(blockEntity); + } + // Leaves end - pca ++ // Leaves start - Lithium Sleeping Block Entity ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity ++ && !blockEntity.isOnCooldown() ++ && !blockEntity.isSleeping() ++ && !state.getValue(HopperBlock.ENABLED)) { ++ blockEntity.lithium$startSleeping(); ++ } ++ // Leaves end - Lithium Sleeping Block Entity + return true; + } + } +@@ -456,11 +488,19 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + } + } + // Leaves end - hopper counter +- Container attachedContainer = getAttachedContainer(level, pos, blockEntity); ++ Container attachedContainer = org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity ? blockEntity.getInsertInventory(level) : getAttachedContainer(level, pos, blockEntity); // Leaves - Lithium Sleeping Block Entity + if (attachedContainer == null) { + return false; + } else { + Direction opposite = blockEntity.facing.getOpposite(); ++ // Leaves start - Lithium Sleeping Block Entity ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) { ++ Boolean res = lithiumInsert(level, pos, blockEntity, attachedContainer); ++ if (res != null) { ++ return res; ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + if (isFullContainer(attachedContainer, opposite)) { + return false; + } else { +@@ -589,9 +629,17 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + public static boolean suckInItems(Level level, Hopper hopper) { + BlockPos blockPos = BlockPos.containing(hopper.getLevelX(), hopper.getLevelY() + 1.0, hopper.getLevelZ()); + BlockState blockState = level.getBlockState(blockPos); +- Container sourceContainer = getSourceContainer(level, hopper, blockPos, blockState); ++ Container sourceContainer = org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity ? getExtractInventory(level, hopper, blockPos, blockState) : getSourceContainer(level, hopper, blockPos, blockState); // Leaves - Lithium Sleeping Block Entity + if (sourceContainer != null) { + Direction direction = Direction.DOWN; ++ // Leaves start - Lithium Sleeping Block Entity ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) { ++ Boolean res = lithiumExtract(level, hopper, sourceContainer); ++ if (res != null) { ++ return res; ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + skipPullModeEventFire = skipHopperEvents; // Paper - Perf: Optimize Hoppers + + for (int i : getSlots(sourceContainer, direction)) { +@@ -604,7 +652,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + } else { + boolean flag = hopper.isGridAligned() && (!org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldHopperSuckInBehavior && blockState.isCollisionShapeFullBlock(level, blockPos)) && !blockState.is(BlockTags.DOES_NOT_BLOCK_HOPPERS); // Leaves - oldHopperSuckInBehavior + if (!flag) { +- for (ItemEntity itemEntity : getItemsAtAndAbove(level, hopper)) { ++ for (ItemEntity itemEntity : org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity ? lithiumGetInputItemEntities(level, hopper) : getItemsAtAndAbove(level, hopper)) { // Leaves - Lithium Sleeping Block Entity + if (addItem(hopper, itemEntity)) { + return true; + } +@@ -900,6 +948,19 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + } + + public void setCooldown(int cooldownTime) { ++ // Leaves start - Lithium Sleeping Block Entity ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) { ++ if (cooldownTime == 7) { ++ if (this.tickedGameTime == Long.MAX_VALUE) { ++ this.sleepOnlyCurrentTick(); ++ } else { ++ this.wakeUpNow(); ++ } ++ } else if (cooldownTime > 0 && this.sleepingTicker != null) { ++ this.wakeUpNow(); ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + this.cooldownTime = cooldownTime; + } + +@@ -919,6 +980,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + @Override + protected void setItems(NonNullList items) { + this.items = items; ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) this.lithium$emitStackListReplaced(); // Leaves - Lithium Sleeping Block Entity + } + + public static void entityInside(Level level, BlockPos pos, BlockState state, Entity entity, HopperBlockEntity blockEntity) { +@@ -933,4 +995,731 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + protected AbstractContainerMenu createMenu(int id, Inventory player) { + return new HopperMenu(id, player, this); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = null; ++ private TickingBlockEntity sleepingTicker = null; ++ private long myModCountAtLastInsert, myModCountAtLastExtract, myModCountAtLastItemCollect; ++ ++ private HopperCachingState.BlockInventory insertionMode = HopperCachingState.BlockInventory.UNKNOWN; ++ private HopperCachingState.BlockInventory extractionMode = HopperCachingState.BlockInventory.UNKNOWN; ++ ++ //The currently used block inventories ++ @Nullable ++ private Container insertBlockInventory, extractBlockInventory; ++ ++ //The currently used inventories (optimized type, if not present, skip optimizations) ++ @Nullable ++ private LithiumInventory insertInventory, extractInventory; ++ @Nullable //Null iff corresp. LithiumInventory field is null ++ private LithiumStackList insertStackList, extractStackList; ++ //Mod count used to avoid transfer attempts that are known to fail (no change since last attempt) ++ private long insertStackListModCount, extractStackListModCount; ++ ++ @Nullable ++ private List collectItemEntityTracker; ++ private boolean collectItemEntityTrackerWasEmpty; ++ @Nullable ++ private AABB collectItemEntityBox; ++ private long collectItemEntityAttemptTime; ++ ++ @Nullable ++ private List extractInventoryEntityTracker; ++ @Nullable ++ private AABB extractInventoryEntityBox; ++ private long extractInventoryEntityFailedSearchTime; ++ ++ @Nullable ++ private List insertInventoryEntityTracker; ++ @Nullable ++ private AABB insertInventoryEntityBox; ++ private long insertInventoryEntityFailedSearchTime; ++ ++ private boolean shouldCheckSleep; ++ ++ private void checkSleepingConditions() { ++ if (this.isOnCooldown() || this.getLevel() == null) { ++ return; ++ } ++ if (isSleeping()) { ++ return; ++ } ++ if (!this.shouldCheckSleep) { ++ this.shouldCheckSleep = true; ++ return; ++ } ++ boolean listenToExtractTracker = false; ++ boolean listenToInsertTracker = false; ++ boolean listenToExtractEntities = false; ++ boolean listenToItemEntities = false; ++ boolean listenToInsertEntities = false; ++ ++ LithiumStackList thisStackList = InventoryHelper.getLithiumStackList(this); ++ ++ if (this.extractionMode != HopperCachingState.BlockInventory.BLOCK_STATE && thisStackList.getFullSlots() != thisStackList.size()) { ++ if (this.extractionMode == HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY) { ++ Container blockInventory = this.extractBlockInventory; ++ if (this.extractStackList != null && ++ blockInventory instanceof InventoryChangeTracker) { ++ if (!this.extractStackList.maybeSendsComparatorUpdatesOnFailedExtract() || (blockInventory instanceof ComparatorTracker comparatorTracker && !comparatorTracker.lithium$hasAnyComparatorNearby())) { ++ listenToExtractTracker = true; ++ } else { ++ return; ++ } ++ } else { ++ return; ++ } ++ } else if (this.extractionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY) { ++ BlockState hopperState = this.getBlockState(); ++ listenToExtractEntities = true; ++ ++ BlockPos blockPos = this.getBlockPos().above(); ++ BlockState blockState = this.getLevel().getBlockState(blockPos); ++ if (!blockState.isCollisionShapeFullBlock(this.getLevel(), blockPos) || blockState.is(BlockTags.DOES_NOT_BLOCK_HOPPERS)) { ++ listenToItemEntities = true; ++ } ++ } else { ++ return; ++ } ++ } ++ if (this.insertionMode != HopperCachingState.BlockInventory.BLOCK_STATE && 0 < thisStackList.getOccupiedSlots()) { ++ if (this.insertionMode == HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY) { ++ Container blockInventory = this.insertBlockInventory; ++ if (this.insertStackList != null && blockInventory instanceof InventoryChangeTracker) { ++ listenToInsertTracker = true; ++ } else { ++ return; ++ } ++ } else if (this.insertionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY) { ++ BlockState hopperState = this.getBlockState(); ++ listenToInsertEntities = true; ++ } else { ++ return; ++ } ++ } ++ ++ if (listenToExtractTracker) { ++ ((InventoryChangeTracker) this.extractBlockInventory).listenForContentChangesOnce(this.extractStackList, this); ++ } ++ if (listenToInsertTracker) { ++ ((InventoryChangeTracker) this.insertBlockInventory).listenForContentChangesOnce(this.insertStackList, this); ++ } ++ if (listenToInsertEntities) { ++ if (this.insertInventoryEntityTracker == null || this.insertInventoryEntityTracker.isEmpty()) { ++ return; ++ } ++ ChunkSectionEntityMovementTracker.listenToEntityMovementOnce(this, insertInventoryEntityTracker); ++ } ++ if (listenToExtractEntities) { ++ if (this.extractInventoryEntityTracker == null || this.extractInventoryEntityTracker.isEmpty()) { ++ return; ++ } ++ ChunkSectionEntityMovementTracker.listenToEntityMovementOnce(this, extractInventoryEntityTracker); ++ } ++ if (listenToItemEntities) { ++ if (this.collectItemEntityTracker == null || this.collectItemEntityTracker.isEmpty()) { ++ return; ++ } ++ ChunkSectionEntityMovementTracker.listenToEntityMovementOnce(this, collectItemEntityTracker); ++ } ++ ++ this.listenForContentChangesOnce(thisStackList, this); ++ lithium$startSleeping(); ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ ++ @Override ++ public TickingBlockEntity lithium$getSleepingTicker() { ++ return sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ this.lithium$setSleepingTicker(null); ++ } ++ ++ @Override ++ public LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { ++ return tickWrapper; ++ } ++ ++ @Override ++ public boolean lithium$startSleeping() { ++ if (this.isSleeping()) { ++ return false; ++ } ++ ++ LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = this.lithium$getTickWrapper(); ++ if (tickWrapper != null) { ++ this.lithium$setSleepingTicker(tickWrapper.ticker); ++ tickWrapper.rebind(SleepingBlockEntity.SLEEPING_BLOCK_ENTITY_TICKER); ++ ++ // Set the last tick time to max value, so other hoppers transferring into this hopper will set it to 7gt ++ // cooldown. Then when waking up, we make sure to not tick this hopper in the same gametick. ++ // This makes the observable hopper cooldown not be different from vanilla. ++ this.tickedGameTime = Long.MAX_VALUE; ++ return true; ++ } ++ return false; ++ } ++ ++ @Override ++ public void handleEntityMovement() { ++ this.wakeUpNow(); ++ } ++ ++ @Override ++ public NonNullList getInventoryLithium() { ++ return items; ++ } ++ ++ @Override ++ public void setInventoryLithium(NonNullList inventory) { ++ this.items = inventory; ++ } ++ ++ @Override ++ public void lithium$handleInventoryContentModified(Container inventory) { ++ wakeUpNow(); ++ } ++ ++ @Override ++ public void lithium$handleInventoryRemoved(Container inventory) { ++ wakeUpNow(); ++ if (inventory == this.insertBlockInventory) { ++ this.invalidateBlockInsertionData(); ++ } ++ if (inventory == this.extractBlockInventory) { ++ this.invalidateBlockExtractionData(); ++ } ++ if (inventory == this) { ++ this.invalidateCachedData(); ++ } ++ } ++ ++ @Override ++ public boolean lithium$handleComparatorAdded(Container inventory) { ++ if (inventory == this.extractBlockInventory) { ++ wakeUpNow(); ++ return true; ++ } ++ return false; ++ } ++ ++ @Override ++ public void lithium$invalidateCacheOnNeighborUpdate(boolean fromAbove) { ++ //Clear the block inventory cache (composter inventories and no inventory present) on block update / observer update ++ if (fromAbove) { ++ if (this.extractionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY || this.extractionMode == HopperCachingState.BlockInventory.BLOCK_STATE) { ++ this.invalidateBlockExtractionData(); ++ } ++ } else { ++ if (this.insertionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY || this.insertionMode == HopperCachingState.BlockInventory.BLOCK_STATE) { ++ this.invalidateBlockInsertionData(); ++ } ++ } ++ } ++ ++ @Override ++ public void lithium$invalidateCacheOnUndirectedNeighborUpdate() { ++ if (this.extractionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY || this.extractionMode == HopperCachingState.BlockInventory.BLOCK_STATE) { ++ this.invalidateBlockExtractionData(); ++ } ++ if (this.insertionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY || this.insertionMode == HopperCachingState.BlockInventory.BLOCK_STATE) { ++ this.invalidateBlockInsertionData(); ++ } ++ } ++ ++ @Override ++ public void lithium$invalidateCacheOnNeighborUpdate(Direction fromDirection) { ++ boolean fromAbove = fromDirection == Direction.UP; ++ if (fromAbove || this.getBlockState().getValue(HopperBlock.FACING) == fromDirection) { ++ this.lithium$invalidateCacheOnNeighborUpdate(fromAbove); ++ } ++ } ++ ++ private void invalidateBlockInsertionData() { ++ this.insertionMode = HopperCachingState.BlockInventory.UNKNOWN; ++ this.insertBlockInventory = null; ++ this.insertInventory = null; ++ this.insertStackList = null; ++ this.insertStackListModCount = 0; ++ ++ wakeUpNow(); ++ } ++ ++ private void invalidateCachedData() { ++ this.shouldCheckSleep = false; ++ this.invalidateInsertionData(); ++ this.invalidateExtractionData(); ++ } ++ ++ private void invalidateInsertionData() { ++ if (this.level instanceof ServerLevel) { ++ if (this.insertInventoryEntityTracker != null) { ++ ChunkSectionEntityMovementTracker.unregister(this.insertInventoryEntityTracker); ++ this.insertInventoryEntityTracker = null; ++ this.insertInventoryEntityBox = null; ++ this.insertInventoryEntityFailedSearchTime = 0L; ++ } ++ } ++ ++ if (this.insertionMode == HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY) { ++ assert this.insertBlockInventory != null; ++ ((InventoryChangeTracker) this.insertBlockInventory).stopListenForMajorInventoryChanges(this); ++ } ++ this.invalidateBlockInsertionData(); ++ } ++ ++ private void invalidateExtractionData() { ++ if (this.level instanceof ServerLevel) { ++ if (this.extractInventoryEntityTracker != null) { ++ ChunkSectionEntityMovementTracker.unregister(this.extractInventoryEntityTracker); ++ this.extractInventoryEntityTracker = null; ++ this.extractInventoryEntityBox = null; ++ this.extractInventoryEntityFailedSearchTime = 0L; ++ } ++ if (this.collectItemEntityTracker != null) { ++ ChunkSectionEntityMovementTracker.unregister(this.collectItemEntityTracker); ++ this.collectItemEntityTracker = null; ++ this.collectItemEntityBox = null; ++ this.collectItemEntityTrackerWasEmpty = false; ++ } ++ } ++ if (this.extractionMode == HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY) { ++ assert this.extractBlockInventory != null; ++ ((InventoryChangeTracker) this.extractBlockInventory).stopListenForMajorInventoryChanges(this); ++ } ++ this.invalidateBlockExtractionData(); ++ } ++ ++ private void invalidateBlockExtractionData() { ++ this.extractionMode = HopperCachingState.BlockInventory.UNKNOWN; ++ this.extractBlockInventory = null; ++ this.extractInventory = null; ++ this.extractStackList = null; ++ this.extractStackListModCount = 0; ++ ++ this.wakeUpNow(); ++ } ++ ++ private static Container getExtractInventory(Level world, Hopper hopper, BlockPos extractBlockPos, BlockState extractBlockState) { ++ if (!(hopper instanceof HopperBlockEntity hopperBlockEntity)) { ++ return getSourceContainer(world, hopper, extractBlockPos, extractBlockState); //Hopper Minecarts do not cache Inventories ++ } ++ ++ Container blockInventory = hopperBlockEntity.lithium$getExtractBlockInventory(world, extractBlockPos, extractBlockState); ++ if (blockInventory != null) { ++ return blockInventory; ++ } ++ return hopperBlockEntity.lithium$getExtractEntityInventory(world); ++ } ++ ++ public Container lithium$getExtractBlockInventory(Level world, BlockPos extractBlockPos, BlockState extractBlockState) { ++ Container blockInventory = this.extractBlockInventory; ++ if (this.extractionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY) { ++ return null; ++ } else if (this.extractionMode == HopperCachingState.BlockInventory.BLOCK_STATE) { ++ return blockInventory; ++ } else if (this.extractionMode == HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY) { ++ return blockInventory; ++ } else if (this.extractionMode == HopperCachingState.BlockInventory.BLOCK_ENTITY) { ++ BlockEntity blockEntity = (BlockEntity) Objects.requireNonNull(blockInventory); ++ //Movable Block Entity compatibility - position comparison ++ BlockPos pos = blockEntity.getBlockPos(); ++ if (!(blockEntity).isRemoved() && pos.equals(extractBlockPos)) { ++ LithiumInventory optimizedInventory; ++ if ((optimizedInventory = this.extractInventory) != null) { ++ LithiumStackList insertInventoryStackList = InventoryHelper.getLithiumStackList(optimizedInventory); ++ //This check is necessary as sometimes the stacklist is silently replaced (e.g. command making furnace read inventory from nbt) ++ if (insertInventoryStackList == this.extractStackList) { ++ return optimizedInventory; ++ } else { ++ this.invalidateBlockExtractionData(); ++ } ++ } else { ++ return blockInventory; ++ } ++ } ++ } ++ ++ //No Cached Inventory: Get like vanilla and cache ++ blockInventory = getBlockContainer(world, extractBlockPos, extractBlockState); ++ blockInventory = HopperHelper.replaceDoubleInventory(blockInventory); ++ this.cacheExtractBlockInventory(blockInventory); ++ return blockInventory; ++ } ++ ++ public Container lithium$getInsertBlockInventory(Level world) { ++ Container blockInventory = this.insertBlockInventory; ++ if (this.insertionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY) { ++ return null; ++ } else if (this.insertionMode == HopperCachingState.BlockInventory.BLOCK_STATE) { ++ return blockInventory; ++ } else if (this.insertionMode == HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY) { ++ return blockInventory; ++ } else if (this.insertionMode == HopperCachingState.BlockInventory.BLOCK_ENTITY) { ++ BlockEntity blockEntity = (BlockEntity) Objects.requireNonNull(blockInventory); ++ //Movable Block Entity compatibility - position comparison ++ BlockPos pos = blockEntity.getBlockPos(); ++ Direction direction = this.facing; ++ BlockPos transferPos = this.getBlockPos().relative(direction); ++ if (!(blockEntity).isRemoved() && ++ pos.equals(transferPos)) { ++ LithiumInventory optimizedInventory; ++ if ((optimizedInventory = this.insertInventory) != null) { ++ LithiumStackList insertInventoryStackList = InventoryHelper.getLithiumStackList(optimizedInventory); ++ //This check is necessary as sometimes the stacklist is silently replaced (e.g. command making furnace read inventory from nbt) ++ if (insertInventoryStackList == this.insertStackList) { ++ return optimizedInventory; ++ } else { ++ this.invalidateBlockInsertionData(); ++ } ++ } else { ++ return blockInventory; ++ } ++ } ++ } ++ ++ //No Cached Inventory: Get like vanilla and cache ++ Direction direction = this.facing; ++ BlockPos insertBlockPos = this.getBlockPos().relative(direction); ++ BlockState blockState = world.getBlockState(insertBlockPos); ++ blockInventory = getBlockContainer(world, insertBlockPos, blockState); ++ blockInventory = HopperHelper.replaceDoubleInventory(blockInventory); ++ this.cacheInsertBlockInventory(blockInventory); ++ return blockInventory; ++ } ++ ++ ++ public Container getInsertInventory(Level world) { ++ Container blockInventory = this.lithium$getInsertBlockInventory(world); ++ if (blockInventory != null) { ++ return blockInventory; ++ } ++ ++ if (this.insertInventoryEntityTracker == null) { ++ this.initInsertInventoryTracker(world); ++ } ++ if (ChunkSectionEntityMovementTracker.isUnchangedSince(this.insertInventoryEntityFailedSearchTime, this.insertInventoryEntityTracker)) { ++ this.insertInventoryEntityFailedSearchTime = this.tickedGameTime; ++ return null; ++ } ++ this.insertInventoryEntityFailedSearchTime = Long.MIN_VALUE; ++ this.shouldCheckSleep = false; ++ ++ List inventoryEntities = ChunkSectionInventoryEntityTracker.getEntities(world, this.insertInventoryEntityBox); ++ if (inventoryEntities.isEmpty()) { ++ this.insertInventoryEntityFailedSearchTime = this.tickedGameTime; ++ //Remember failed entity search timestamp. This allows shortcutting if no entity movement happens. ++ return null; ++ } ++ Container inventory = inventoryEntities.get(world.random.nextInt(inventoryEntities.size())); ++ if (inventory instanceof LithiumInventory optimizedInventory) { ++ LithiumStackList insertInventoryStackList = InventoryHelper.getLithiumStackList(optimizedInventory); ++ if (inventory != this.insertInventory || this.insertStackList != insertInventoryStackList) { ++ this.cacheInsertLithiumInventory(optimizedInventory); ++ } ++ } ++ ++ return inventory; ++ } ++ ++ private void initCollectItemEntityTracker() { ++ assert this.level instanceof ServerLevel; ++ AABB inputBox = this.getSuckAabb().move(this.worldPosition.getX(), this.worldPosition.getY(), this.worldPosition.getZ()); ++ this.collectItemEntityBox = inputBox; ++ this.collectItemEntityTracker = ++ ChunkSectionItemEntityMovementTracker.registerAt( ++ (ServerLevel) this.level, ++ inputBox ++ ); ++ this.collectItemEntityAttemptTime = Long.MIN_VALUE; ++ } ++ ++ private void initExtractInventoryTracker(Level world) { ++ assert world instanceof ServerLevel; ++ BlockPos pos = this.worldPosition.relative(Direction.UP); ++ this.extractInventoryEntityBox = new AABB(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1); ++ this.extractInventoryEntityTracker = ++ ChunkSectionInventoryEntityTracker.registerAt( ++ (ServerLevel) this.level, ++ this.extractInventoryEntityBox ++ ); ++ this.extractInventoryEntityFailedSearchTime = Long.MIN_VALUE; ++ } ++ ++ private void initInsertInventoryTracker(Level world) { ++ assert world instanceof ServerLevel; ++ Direction direction = this.facing; ++ BlockPos pos = this.worldPosition.relative(direction); ++ this.insertInventoryEntityBox = new AABB(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1); ++ this.insertInventoryEntityTracker = ++ ChunkSectionInventoryEntityTracker.registerAt( ++ (ServerLevel) this.level, ++ this.insertInventoryEntityBox ++ ); ++ this.insertInventoryEntityFailedSearchTime = Long.MIN_VALUE; ++ } ++ ++ private @Nullable Container lithium$getExtractEntityInventory(Level world) { ++ if (this.extractInventoryEntityTracker == null) { ++ this.initExtractInventoryTracker(world); ++ } ++ if (ChunkSectionEntityMovementTracker.isUnchangedSince(this.extractInventoryEntityFailedSearchTime, this.extractInventoryEntityTracker)) { ++ this.extractInventoryEntityFailedSearchTime = this.tickedGameTime; ++ return null; ++ } ++ this.extractInventoryEntityFailedSearchTime = Long.MIN_VALUE; ++ this.shouldCheckSleep = false; ++ ++ List inventoryEntities = ChunkSectionInventoryEntityTracker.getEntities(world, this.extractInventoryEntityBox); ++ if (inventoryEntities.isEmpty()) { ++ this.extractInventoryEntityFailedSearchTime = this.tickedGameTime; ++ //only set unchanged when no entity present. this allows shortcutting this case ++ //shortcutting the entity present case requires checking its change counter ++ return null; ++ } ++ Container inventory = inventoryEntities.get(world.random.nextInt(inventoryEntities.size())); ++ if (inventory instanceof LithiumInventory optimizedInventory) { ++ LithiumStackList extractInventoryStackList = InventoryHelper.getLithiumStackList(optimizedInventory); ++ if (inventory != this.extractInventory || this.extractStackList != extractInventoryStackList) { ++ //not caching the inventory (NO_BLOCK_INVENTORY prevents it) ++ //make change counting on the entity inventory possible, without caching it as block inventory ++ this.cacheExtractLithiumInventory(optimizedInventory); ++ } ++ } ++ return inventory; ++ } ++ ++ /** ++ * Makes this hopper remember the given inventory. ++ * ++ * @param insertInventory Block inventory / Blockentity inventory to be remembered ++ */ ++ private void cacheInsertBlockInventory(Container insertInventory) { ++ assert !(insertInventory instanceof Entity); ++ if (insertInventory instanceof LithiumInventory optimizedInventory) { ++ this.cacheInsertLithiumInventory(optimizedInventory); ++ } else { ++ this.insertInventory = null; ++ this.insertStackList = null; ++ this.insertStackListModCount = 0; ++ } ++ ++ if (insertInventory instanceof BlockEntity || insertInventory instanceof CompoundContainer) { ++ this.insertBlockInventory = insertInventory; ++ if (insertInventory instanceof InventoryChangeTracker) { ++ this.insertionMode = HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY; ++ ((InventoryChangeTracker) insertInventory).listenForMajorInventoryChanges(this); ++ } else { ++ this.insertionMode = HopperCachingState.BlockInventory.BLOCK_ENTITY; ++ } ++ } else { ++ if (insertInventory == null) { ++ this.insertBlockInventory = null; ++ this.insertionMode = HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY; ++ } else { ++ this.insertBlockInventory = insertInventory; ++ this.insertionMode = insertInventory instanceof BlockStateOnlyInventory ? HopperCachingState.BlockInventory.BLOCK_STATE : HopperCachingState.BlockInventory.UNKNOWN; ++ } ++ } ++ } ++ ++ private void cacheInsertLithiumInventory(LithiumInventory optimizedInventory) { ++ LithiumStackList insertInventoryStackList = InventoryHelper.getLithiumStackList(optimizedInventory); ++ this.insertInventory = optimizedInventory; ++ this.insertStackList = insertInventoryStackList; ++ this.insertStackListModCount = insertInventoryStackList.getModCount() - 1; ++ } ++ ++ private void cacheExtractLithiumInventory(LithiumInventory optimizedInventory) { ++ LithiumStackList extractInventoryStackList = InventoryHelper.getLithiumStackList(optimizedInventory); ++ this.extractInventory = optimizedInventory; ++ this.extractStackList = extractInventoryStackList; ++ this.extractStackListModCount = extractInventoryStackList.getModCount() - 1; ++ } ++ ++ /** ++ * Makes this hopper remember the given inventory. ++ * ++ * @param extractInventory Block inventory / Blockentity inventory to be remembered ++ */ ++ private void cacheExtractBlockInventory(Container extractInventory) { ++ assert !(extractInventory instanceof Entity); ++ if (extractInventory instanceof LithiumInventory optimizedInventory) { ++ this.cacheExtractLithiumInventory(optimizedInventory); ++ } else { ++ this.extractInventory = null; ++ this.extractStackList = null; ++ this.extractStackListModCount = 0; ++ } ++ ++ if (extractInventory instanceof BlockEntity || extractInventory instanceof CompoundContainer) { ++ this.extractBlockInventory = extractInventory; ++ if (extractInventory instanceof InventoryChangeTracker) { ++ this.extractionMode = HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY; ++ ((InventoryChangeTracker) extractInventory).listenForMajorInventoryChanges(this); ++ } else { ++ this.extractionMode = HopperCachingState.BlockInventory.BLOCK_ENTITY; ++ } ++ } else { ++ if (extractInventory == null) { ++ this.extractBlockInventory = null; ++ this.extractionMode = HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY; ++ } else { ++ this.extractBlockInventory = extractInventory; ++ this.extractionMode = extractInventory instanceof BlockStateOnlyInventory ? HopperCachingState.BlockInventory.BLOCK_STATE : HopperCachingState.BlockInventory.UNKNOWN; ++ } ++ } ++ } ++ ++ private static List lithiumGetInputItemEntities(Level world, Hopper hopper) { ++ if (!(hopper instanceof HopperBlockEntity hopperBlockEntity)) { ++ return getItemsAtAndAbove(world, hopper); //optimizations not implemented for hopper minecarts ++ } ++ ++ if (hopperBlockEntity.collectItemEntityTracker == null) { ++ hopperBlockEntity.initCollectItemEntityTracker(); ++ } ++ ++ long modCount = InventoryHelper.getLithiumStackList(hopperBlockEntity).getModCount(); ++ ++ if ((hopperBlockEntity.collectItemEntityTrackerWasEmpty || hopperBlockEntity.myModCountAtLastItemCollect == modCount) && ++ ChunkSectionEntityMovementTracker.isUnchangedSince(hopperBlockEntity.collectItemEntityAttemptTime, hopperBlockEntity.collectItemEntityTracker)) { ++ hopperBlockEntity.collectItemEntityAttemptTime = hopperBlockEntity.tickedGameTime; ++ return java.util.Collections.emptyList(); ++ } ++ ++ hopperBlockEntity.myModCountAtLastItemCollect = modCount; ++ hopperBlockEntity.shouldCheckSleep = false; ++ ++ List itemEntities = ChunkSectionItemEntityMovementTracker.getEntities(world, hopperBlockEntity.collectItemEntityBox); ++ hopperBlockEntity.collectItemEntityAttemptTime = hopperBlockEntity.tickedGameTime; ++ hopperBlockEntity.collectItemEntityTrackerWasEmpty = itemEntities.isEmpty(); ++ //set unchanged so that if this extract fails and there is no other change to hoppers or items, extracting ++ // items can be skipped. ++ return itemEntities; ++ } ++ ++ private static Boolean lithiumInsert(Level world, BlockPos pos, HopperBlockEntity hopperBlockEntity, Container insertInventory) { ++ if (insertInventory == null || hopperBlockEntity instanceof net.minecraft.world.WorldlyContainer) { ++ //call the vanilla code to allow other mods inject features ++ //e.g. carpet mod allows hoppers to insert items into wool blocks ++ return null; ++ } ++ ++ LithiumStackList hopperStackList = InventoryHelper.getLithiumStackList(hopperBlockEntity); ++ if (hopperBlockEntity.insertInventory == insertInventory && hopperStackList.getModCount() == hopperBlockEntity.myModCountAtLastInsert) { ++ if (hopperBlockEntity.insertStackList != null && hopperBlockEntity.insertStackList.getModCount() == hopperBlockEntity.insertStackListModCount) { ++// ComparatorUpdatePattern.NO_UPDATE.apply(hopperBlockEntity, hopperStackList); //commented because it's a noop, Hoppers do not send useless comparator updates ++ return false; ++ } ++ } ++ ++ boolean insertInventoryWasEmptyHopperNotDisabled = insertInventory instanceof HopperBlockEntity hopperInv && ++ !hopperInv.isOnCustomCooldown() && hopperBlockEntity.insertStackList != null && ++ hopperBlockEntity.insertStackList.getOccupiedSlots() == 0; ++ ++ boolean insertInventoryHandlesModdedCooldown = ++ ((LithiumCooldownReceivingInventory) insertInventory).canReceiveTransferCooldown() && ++ hopperBlockEntity.insertStackList != null ? ++ hopperBlockEntity.insertStackList.getOccupiedSlots() == 0 : ++ insertInventory.isEmpty(); ++ ++ ++ //noinspection ConstantConditions ++ if (!(hopperBlockEntity.insertInventory == insertInventory && hopperBlockEntity.insertStackList.getFullSlots() == hopperBlockEntity.insertStackList.size())) { ++ Direction fromDirection = hopperBlockEntity.facing.getOpposite(); ++ int size = hopperStackList.size(); ++ //noinspection ForLoopReplaceableByForEach ++ for (int i = 0; i < size; ++i) { ++ ItemStack transferStack = hopperStackList.get(i); ++ if (!transferStack.isEmpty()) { ++ boolean transferSuccess = HopperHelper.tryMoveSingleItem(insertInventory, transferStack, fromDirection); ++ if (transferSuccess) { ++ if (insertInventoryWasEmptyHopperNotDisabled) { ++ HopperBlockEntity receivingHopper = (HopperBlockEntity) insertInventory; ++ int k = 8; ++ if (receivingHopper.tickedGameTime >= hopperBlockEntity.tickedGameTime) { ++ k = 7; ++ } ++ receivingHopper.setCooldown(k); ++ } ++ if (insertInventoryHandlesModdedCooldown) { ++ ((LithiumCooldownReceivingInventory) insertInventory).setTransferCooldown(hopperBlockEntity.tickedGameTime); ++ } ++ insertInventory.setChanged(); ++ return true; ++ } ++ } ++ } ++ } ++ hopperBlockEntity.myModCountAtLastInsert = hopperStackList.getModCount(); ++ if (hopperBlockEntity.insertStackList != null) { ++ hopperBlockEntity.insertStackListModCount = hopperBlockEntity.insertStackList.getModCount(); ++ } ++ return false; ++ } ++ ++ private static Boolean lithiumExtract(Level world, Hopper to, Container from) { ++ if (!(to instanceof HopperBlockEntity hopperBlockEntity)) { ++ return null; //optimizations not implemented for hopper minecarts ++ } ++ ++ if (from != hopperBlockEntity.extractInventory || hopperBlockEntity.extractStackList == null) { ++ return null; //from inventory is not an optimized inventory, vanilla fallback ++ } ++ ++ LithiumStackList hopperStackList = InventoryHelper.getLithiumStackList(hopperBlockEntity); ++ LithiumStackList fromStackList = hopperBlockEntity.extractStackList; ++ ++ if (hopperStackList.getModCount() == hopperBlockEntity.myModCountAtLastExtract) { ++ if (fromStackList.getModCount() == hopperBlockEntity.extractStackListModCount) { ++ if (!(from instanceof ComparatorTracker comparatorTracker) || comparatorTracker.lithium$hasAnyComparatorNearby()) { ++ //noinspection CollectionAddedToSelf ++ fromStackList.runComparatorUpdatePatternOnFailedExtract(fromStackList, from); ++ } ++ return false; ++ } ++ } ++ ++ int[] availableSlots = from instanceof WorldlyContainer ? ((WorldlyContainer) from).getSlotsForFace(Direction.DOWN) : null; ++ int fromSize = availableSlots != null ? availableSlots.length : from.getContainerSize(); ++ for (int i = 0; i < fromSize; i++) { ++ int fromSlot = availableSlots != null ? availableSlots[i] : i; ++ ItemStack itemStack = fromStackList.get(fromSlot); ++ if (!itemStack.isEmpty() && canTakeItemFromContainer(to, from, itemStack, fromSlot, Direction.DOWN)) { ++ //calling removeStack is necessary due to its side effects (markDirty in LootableContainerBlockEntity) ++ ItemStack takenItem = from.removeItem(fromSlot, 1); ++ assert !takenItem.isEmpty(); ++ boolean transferSuccess = HopperHelper.tryMoveSingleItem(to, takenItem, null); ++ if (transferSuccess) { ++ to.setChanged(); ++ from.setChanged(); ++ return true; ++ } ++ //put the item back similar to vanilla ++ ItemStack restoredStack = fromStackList.get(fromSlot); ++ if (restoredStack.isEmpty()) { ++ restoredStack = takenItem; ++ } else { ++ restoredStack.grow(1); ++ } ++ //calling setStack is necessary due to its side effects (markDirty in LootableContainerBlockEntity) ++ from.setItem(fromSlot, restoredStack); ++ } ++ } ++ hopperBlockEntity.myModCountAtLastExtract = hopperStackList.getModCount(); ++ if (fromStackList != null) { ++ hopperBlockEntity.extractStackListModCount = fromStackList.getModCount(); ++ } ++ return false; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java b/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java +index c1d1e28daa3b4d2a0bb359af08670f5d071e51ed..97a18901f320286f8eac2bfa698b353f94a621b3 100644 +--- a/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java +@@ -32,7 +32,7 @@ import net.minecraft.world.level.storage.ValueOutput; + import net.minecraft.world.phys.AABB; + import net.minecraft.world.phys.Vec3; + +-public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity implements WorldlyContainer { ++public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity implements WorldlyContainer, org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker, org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity, org.leavesmc.leaves.lithium.api.inventory.LithiumInventory { // Leaves - Lithium Sleeping Block Entity + public static final int COLUMNS = 9; + public static final int ROWS = 3; + public static final int CONTAINER_SIZE = 27; +@@ -134,6 +134,7 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl + doNeighborUpdates(level, pos, state); + } + } ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && this.animationStatus == ShulkerBoxBlockEntity.AnimationStatus.CLOSED && this.progressOld == 0.0f && this.progress == 0.0f) this.lithium$startSleeping(); // Leaves - Lithium Sleeping Block Entity + } + + public ShulkerBoxBlockEntity.AnimationStatus getAnimationStatus() { +@@ -174,6 +175,7 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl + + @Override + public boolean triggerEvent(int id, int type) { ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && this.sleepingTicker != null) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + if (id == 1) { + this.openCount = type; + if (type == 0) { +@@ -265,6 +267,7 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl + @Override + protected void setItems(NonNullList items) { + this.itemStacks = items; ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) this.lithium$emitStackListReplaced(); // Leaves - Lithium Sleeping Block Entity + } + + // Leaves start - pca +@@ -316,4 +319,39 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl + OPENED, + CLOSING; + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = null; ++ private TickingBlockEntity sleepingTicker = null; ++ ++ @Override ++ public net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { ++ return tickWrapper; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ } ++ ++ @Override ++ public TickingBlockEntity lithium$getSleepingTicker() { ++ return sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ ++ @Override ++ public net.minecraft.core.NonNullList getInventoryLithium() { ++ return itemStacks; ++ } ++ ++ @Override ++ public void setInventoryLithium(net.minecraft.core.NonNullList inventory) { ++ itemStacks = inventory; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/state/BlockBehaviour.java b/net/minecraft/world/level/block/state/BlockBehaviour.java +index 67719dce9017a4c86a70b62fb660bddc636d5582..c1cbb635b6c22b04ca33c68a4c9777f067b30b4b 100644 +--- a/net/minecraft/world/level/block/state/BlockBehaviour.java ++++ b/net/minecraft/world/level/block/state/BlockBehaviour.java +@@ -85,7 +85,7 @@ import net.minecraft.world.phys.shapes.CollisionContext; + import net.minecraft.world.phys.shapes.Shapes; + import net.minecraft.world.phys.shapes.VoxelShape; + +-public abstract class BlockBehaviour implements FeatureElement { ++public abstract class BlockBehaviour implements FeatureElement, org.leavesmc.leaves.lithium.common.block.entity.ShapeUpdateHandlingBlockBehaviour { // Leaves - Lithium Sleeping Block Entity + protected static final Direction[] UPDATE_SHAPE_ORDER = new Direction[]{ + Direction.WEST, Direction.EAST, Direction.NORTH, Direction.SOUTH, Direction.DOWN, Direction.UP + }; +@@ -157,6 +157,7 @@ public abstract class BlockBehaviour implements FeatureElement { + BlockState neighborState, + RandomSource random + ) { ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity) this.lithium$handleShapeUpdate(level, state, pos, neighborPos, neighborState); // Leaves - Lithium Sleeping Block Entity /* Triggers when a shape update (= update that observers can detect) is sent */ + return state; + } + +diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java +index 56e7df059cd070a8f472d6712dfb08d14ffa8819..8c263cbb348411a4f1c2ed0a3e46a494d7622f68 100644 +--- a/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/net/minecraft/world/level/chunk/LevelChunk.java +@@ -898,12 +898,14 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + (pos, ticker1) -> { + TickingBlockEntity tickingBlockEntity = this.createTicker(blockEntity, ticker); + if (ticker1 != null) { ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && blockEntity instanceof org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity sleepingBlockEntity) sleepingBlockEntity.lithium$setTickWrapper(ticker1); // Leaves - Lithium Sleeping Block Entity + ticker1.rebind(tickingBlockEntity); + return (LevelChunk.RebindableTickingBlockEntityWrapper)ticker1; + } else if (this.isInLevel()) { + LevelChunk.RebindableTickingBlockEntityWrapper rebindableTickingBlockEntityWrapper = new LevelChunk.RebindableTickingBlockEntityWrapper( + tickingBlockEntity + ); ++ if (org.leavesmc.leaves.LeavesConfig.performance.sleepingBlockEntity && blockEntity instanceof org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity sleepingBlockEntity) sleepingBlockEntity.lithium$setTickWrapper(rebindableTickingBlockEntityWrapper); // Leaves - Lithium Sleeping Block Entity + this.level.addBlockEntityTicker(rebindableTickingBlockEntityWrapper); + return rebindableTickingBlockEntityWrapper; + } else { +@@ -997,14 +999,14 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + void run(LevelChunk chunk); + } + +- static class RebindableTickingBlockEntityWrapper implements TickingBlockEntity { +- private TickingBlockEntity ticker; ++ public static class RebindableTickingBlockEntityWrapper implements TickingBlockEntity { // Leaves - default -> public ++ public TickingBlockEntity ticker; // Leaves - private -> public + + RebindableTickingBlockEntityWrapper(TickingBlockEntity ticker) { + this.ticker = ticker; + } + +- void rebind(TickingBlockEntity ticker) { ++ public void rebind(TickingBlockEntity ticker) { // Leaves - default -> public + this.ticker = ticker; + } + diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java index 0df75f34..f76ab49b 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java @@ -768,6 +768,9 @@ public final class LeavesConfig { @GlobalConfig("fix-villagers-dont-release-memory") public boolean villagersDontReleaseMemoryFix = false; + @GlobalConfig(value = "sleeping-block-entity", lock = true) + public boolean sleepingBlockEntity = false; + @RemovedConfig(name = "biome-temperatures-use-aging-cache", category = "performance") @RemovedConfig(name = "cache-world-generator-sea-level", category = "performance") @RemovedConfig(name = "cache-ominous-banner-item", category = "performance") diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumCooldownReceivingInventory.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumCooldownReceivingInventory.java new file mode 100644 index 00000000..42e73686 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumCooldownReceivingInventory.java @@ -0,0 +1,47 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.api.inventory; + +public interface LithiumCooldownReceivingInventory { + + /** + * To be implemented by modded inventories that want to receive hopper-like transfer cooldowns from lithium's (!) + * item transfers. Hopper-like transfer cooldown means a cooldown that is only set if the hopper was empty before + * the transfer. + * NOTE: Lithium does not replace all of vanilla's item transfers. Mod authors still need to implement + * their own hooks for vanilla code even when they require users to install Lithium. + * + * @param currentTime tick time of the item transfer. + */ + default void setTransferCooldown(long currentTime) { + } + + + /** + * To be implemented by modded inventories that want to receive hopper-like transfer cooldowns from lithium's (!) + * item transfers. Hopper-like transfer cooldown means a cooldown that is only set if the hopper was empty before + * the transfer. + * NOTE: Lithium does not replace all of vanilla's item transfers. Mod authors still need to implement + * their own hooks for vanilla code even when they require users to install Lithium. + * + * @return Whether this inventory wants to receive transfer cooldowns from lithium's code + */ + default boolean canReceiveTransferCooldown() { + return false; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumDefaultedList.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumDefaultedList.java new file mode 100644 index 00000000..a03e5591 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumDefaultedList.java @@ -0,0 +1,33 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.api.inventory; + +import net.minecraft.core.Direction; +import net.minecraft.world.item.ItemStack; + +public interface LithiumDefaultedList { + /** + * Call this method when the behavior of + * {@link net.minecraft.world.Container#canPlaceItem(int, ItemStack)} + * {@link net.minecraft.world.WorldlyContainer#canPlaceItemThroughFace(int, ItemStack, Direction)} + * {@link net.minecraft.world.WorldlyContainer#canTakeItemThroughFace(int, ItemStack, Direction)} + * or similar functionality changed. + * This method will not need to be called if this change in behavior is triggered by a change of the stack list contents. + */ + void changedInteractionConditions(); +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumInventory.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumInventory.java new file mode 100644 index 00000000..e92d9c25 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumInventory.java @@ -0,0 +1,78 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.api.inventory; + +import net.minecraft.core.NonNullList; +import net.minecraft.world.Container; +import net.minecraft.world.entity.vehicle.ContainerEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity; + +/** + * Provides the ability for mods to allow Lithium's hopper optimizations to access their inventories' for item transfers. + * This exists because Lithium's optimized hopper logic will only interact with inventories more efficiently than + * vanilla if the stack list can be directly accessed and replaced with Lithium's custom stack list. + * It is not required to implement this interface, but doing so will allow the mod's inventories to benefit from + * Lithium's optimizations. + *

+ * This interface should be implemented by your {@link net.minecraft.world.Container} or + * {@link net.minecraft.world.WorldlyContainer} type to access the stack list. + *

+ * An inventory must not extend {@link net.minecraft.world.level.block.entity.BlockEntity} if it has a supporting block that + * implements {@link net.minecraft.world.entity.vehicle.ContainerEntity}. + *

+ * The hopper interaction behavior of a LithiumInventory should only change if the content of the inventory + * stack list also changes. For example, an inventory which only accepts an item if it already contains an item of the + * same type would work fine (changing the acceptance condition only happens when changing the inventory contents here). + * However, an inventory which accepts an item only if a certain block is near its position will need to signal this + * change to hoppers by calling {@link LithiumDefaultedList#changedInteractionConditions()}. + * + * @author 2No2Name + */ +public interface LithiumInventory extends Container { + + /** + * Getter for the inventory stack list of this inventory. + * + * @return inventory stack list + */ + NonNullList getInventoryLithium(); + + /** + * Setter for the inventory stack list of this inventory. + * Used to replace the stack list with Lithium's custom stack list. + * + * @param inventory inventory stack list + */ + void setInventoryLithium(NonNullList inventory); + + /** + * Generates the loot like a hopper access would do in vanilla. + *

+ * If a modded inventory has custom loot generation code, it will be required to override this + * loot generation method. Otherwise, its loot may be generated too late. + */ + default void generateLootLithium() { + if (this instanceof RandomizableContainerBlockEntity) { + ((RandomizableContainerBlockEntity) this).unpackLootTable(null); + } + if (this instanceof ContainerEntity) { + ((ContainerEntity) this).unpackChestVehicleLootTable(null); + } + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumTransferConditionInventory.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumTransferConditionInventory.java new file mode 100644 index 00000000..f30d34b9 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumTransferConditionInventory.java @@ -0,0 +1,39 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.api.inventory; + +public interface LithiumTransferConditionInventory { + + /** + * Implement this method to signal that the inventory requires a stack size of 1 for item insertion tests. + * Lithium's hopper optimization transfers a single item, but to avoid excessive copying of item stacks, it passes + * the original stack to the inventory's insertion test. If the inventory requires a stack size of 1 for this test, + * the stack should be copied. However, lithium cannot detect whether the copy is necessary and this method is meant + * to signal this requirement. When the method is not implemented even though it is required, Lithium's hopper + * optimizations may not transfer items correctly to this inventory. + *

+ * The only vanilla inventory that requires this is the Chiseled Bookshelf. Mods with such special inventories + * should implement this method in the inventories' class. + * (It is not required to implement this interface, just the method is enough.) + * + * @return whether the inventory requires a stack size of 1 for item insertion tests + */ + default boolean lithium$itemInsertionTestRequiresStackSize1() { + return false; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetBlockStateHandlingBlockEntity.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetBlockStateHandlingBlockEntity.java new file mode 100644 index 00000000..fa291e4f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetBlockStateHandlingBlockEntity.java @@ -0,0 +1,23 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity; + +public interface SetBlockStateHandlingBlockEntity { + default void lithium$handleSetBlockState() { + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetChangedHandlingBlockEntity.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetChangedHandlingBlockEntity.java new file mode 100644 index 00000000..b520461c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetChangedHandlingBlockEntity.java @@ -0,0 +1,23 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity; + +public interface SetChangedHandlingBlockEntity { + default void lithium$handleSetChanged() { + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/ShapeUpdateHandlingBlockBehaviour.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/ShapeUpdateHandlingBlockBehaviour.java new file mode 100644 index 00000000..1f65c347 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/ShapeUpdateHandlingBlockBehaviour.java @@ -0,0 +1,29 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.state.BlockState; + +public interface ShapeUpdateHandlingBlockBehaviour { + + default void lithium$handleShapeUpdate(LevelReader world, BlockState myBlockState, BlockPos myPos, BlockPos posFrom, BlockState newState) { + } + +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepUntilTimeBlockEntityTickInvoker.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepUntilTimeBlockEntityTickInvoker.java new file mode 100644 index 00000000..9f38c9d4 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepUntilTimeBlockEntityTickInvoker.java @@ -0,0 +1,53 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.entity.TickingBlockEntity; + +public record SleepUntilTimeBlockEntityTickInvoker(BlockEntity sleepingBlockEntity, long sleepUntilTickExclusive, + TickingBlockEntity delegate) implements TickingBlockEntity { + + @Override + public void tick() { + //noinspection ConstantConditions + long tickTime = this.sleepingBlockEntity.getLevel().getGameTime(); + if (tickTime >= this.sleepUntilTickExclusive) { + ((SleepingBlockEntity) this.sleepingBlockEntity).setTicker(this.delegate); + this.delegate.tick(); + } + } + + @Override + public boolean isRemoved() { + return this.sleepingBlockEntity.isRemoved(); + } + + @Override + public BlockPos getPos() { + return this.sleepingBlockEntity.getBlockPos(); + } + + @Override + public String getType() { + //noinspection ConstantConditions + return BlockEntityType.getKey(this.sleepingBlockEntity.getType()).toString(); + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepingBlockEntity.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepingBlockEntity.java new file mode 100644 index 00000000..f10ec39b --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepingBlockEntity.java @@ -0,0 +1,97 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.TickingBlockEntity; +import net.minecraft.world.level.chunk.LevelChunk; + +public interface SleepingBlockEntity { + TickingBlockEntity SLEEPING_BLOCK_ENTITY_TICKER = new TickingBlockEntity() { + public void tick() { + } + + public boolean isRemoved() { + return false; + } + + public BlockPos getPos() { + return null; + } + + public String getType() { + return ""; + } + }; + + LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper(); + + void lithium$setTickWrapper(LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper); + + TickingBlockEntity lithium$getSleepingTicker(); + + void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker); + + default boolean lithium$startSleeping() { + if (this.isSleeping()) { + return false; + } + + LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = this.lithium$getTickWrapper(); + if (tickWrapper == null) { + return false; + } + this.lithium$setSleepingTicker(tickWrapper.ticker); + tickWrapper.rebind(SleepingBlockEntity.SLEEPING_BLOCK_ENTITY_TICKER); + return true; + } + + default void sleepOnlyCurrentTick() { + TickingBlockEntity sleepingTicker = this.lithium$getSleepingTicker(); + LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = this.lithium$getTickWrapper(); + if (sleepingTicker == null) { + sleepingTicker = tickWrapper.ticker; + } + Level world = ((BlockEntity) this).getLevel(); + tickWrapper.rebind(new SleepUntilTimeBlockEntityTickInvoker((BlockEntity) this, world.getGameTime() + 1, sleepingTicker)); + this.lithium$setSleepingTicker(null); + } + + default void wakeUpNow() { + TickingBlockEntity sleepingTicker = this.lithium$getSleepingTicker(); + if (sleepingTicker == null) { + return; + } + this.setTicker(sleepingTicker); + this.lithium$setSleepingTicker(null); + } + + default void setTicker(TickingBlockEntity delegate) { + LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = this.lithium$getTickWrapper(); + if (tickWrapper == null) { + return; + } + tickWrapper.rebind(delegate); + } + + default boolean isSleeping() { + return this.lithium$getSleepingTicker() != null; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeEmitter.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeEmitter.java new file mode 100644 index 00000000..e65b7f86 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeEmitter.java @@ -0,0 +1,47 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking; + +import org.leavesmc.leaves.lithium.common.hopper.LithiumStackList; + +/** + * Interface for Objects that can emit various inventory change events. This does not mean that the inventory + * creates those events - this requirement is met by InventoryChangeTracker. This distinction is needed due + * to modded inventories being able to inherit from BaseContainerBlockEntity, which does not guarantee the creation + * of the required events but implements most of the inventory change listening. + * The forwarding methods below are helpers, it is not recommended to call them from outside InventoryChangeTracker.java + */ +public interface InventoryChangeEmitter { + void lithium$emitStackListReplaced(); + + void lithium$emitRemoved(); + + void lithium$emitContentModified(); + + void lithium$emitFirstComparatorAdded(); + + void lithium$forwardContentChangeOnce(InventoryChangeListener inventoryChangeListener, LithiumStackList stackList, InventoryChangeTracker thisTracker); + + void lithium$forwardMajorInventoryChanges(InventoryChangeListener inventoryChangeListener); + + void lithium$stopForwardingMajorInventoryChanges(InventoryChangeListener inventoryChangeListener); + + default void emitCallbackReplaced() { + this.lithium$emitRemoved(); + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeListener.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeListener.java new file mode 100644 index 00000000..041b2eee --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeListener.java @@ -0,0 +1,32 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking; + +import net.minecraft.world.Container; + +public interface InventoryChangeListener { + default void handleStackListReplaced(Container inventory) { + this.lithium$handleInventoryRemoved(inventory); + } + + void lithium$handleInventoryContentModified(Container inventory); + + void lithium$handleInventoryRemoved(Container inventory); + + boolean lithium$handleComparatorAdded(Container inventory); +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeTracker.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeTracker.java new file mode 100644 index 00000000..455f758e --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeTracker.java @@ -0,0 +1,34 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking; + +import org.leavesmc.leaves.lithium.common.hopper.LithiumStackList; + +public interface InventoryChangeTracker extends InventoryChangeEmitter { + default void listenForContentChangesOnce(LithiumStackList stackList, InventoryChangeListener inventoryChangeListener) { + this.lithium$forwardContentChangeOnce(inventoryChangeListener, stackList, this); + } + + default void listenForMajorInventoryChanges(InventoryChangeListener inventoryChangeListener) { + this.lithium$forwardMajorInventoryChanges(inventoryChangeListener); + } + + default void stopListenForMajorInventoryChanges(InventoryChangeListener inventoryChangeListener) { + this.lithium$stopForwardingMajorInventoryChanges(inventoryChangeListener); + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracker.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracker.java new file mode 100644 index 00000000..6c69839a --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracker.java @@ -0,0 +1,26 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity.inventory_comparator_tracking; + +import net.minecraft.core.Direction; + +public interface ComparatorTracker { + void lithium$onComparatorAdded(Direction direction, int offset); + + boolean lithium$hasAnyComparatorNearby(); +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracking.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracking.java new file mode 100644 index 00000000..09ed7dea --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracking.java @@ -0,0 +1,63 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity.inventory_comparator_tracking; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.Container; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.leavesmc.leaves.lithium.common.util.DirectionConstants; + +public class ComparatorTracking { + + public static void notifyNearbyBlockEntitiesAboutNewComparator(Level world, BlockPos pos) { + BlockPos.MutableBlockPos searchPos = new BlockPos.MutableBlockPos(); + for (Direction searchDirection : DirectionConstants.HORIZONTAL) { + for (int searchOffset = 1; searchOffset <= 2; searchOffset++) { + searchPos.set(pos); + searchPos.move(searchDirection, searchOffset); + BlockState blockState = world.getBlockState(searchPos); + if (blockState.getBlock() instanceof EntityBlock) { + BlockEntity blockEntity = world.lithium$getLoadedExistingBlockEntity(searchPos); + if (blockEntity instanceof Container) { + blockEntity.lithium$onComparatorAdded(searchDirection, searchOffset); + } + } + } + } + } + + public static boolean findNearbyComparators(Level world, BlockPos pos) { + BlockPos.MutableBlockPos searchPos = new BlockPos.MutableBlockPos(); + for (Direction searchDirection : DirectionConstants.HORIZONTAL) { + for (int searchOffset = 1; searchOffset <= 2; searchOffset++) { + searchPos.set(pos); + searchPos.move(searchDirection, searchOffset); + BlockState blockState = world.getBlockState(searchPos); + if (blockState.is(Blocks.COMPARATOR)) { + return true; + } + } + } + return false; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/BlockStateOnlyInventory.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/BlockStateOnlyInventory.java new file mode 100644 index 00000000..4889fd06 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/BlockStateOnlyInventory.java @@ -0,0 +1,4 @@ +package org.leavesmc.leaves.lithium.common.hopper; + +public interface BlockStateOnlyInventory { +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/ComparatorUpdatePattern.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/ComparatorUpdatePattern.java new file mode 100644 index 00000000..7890837b --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/ComparatorUpdatePattern.java @@ -0,0 +1,114 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +import net.minecraft.world.level.block.entity.BlockEntity; + +/** + * Pattern of comparator updates that the given inventory is sending when a hopper + * unsuccessfully attempts to take items from it. This pattern is independent from + * the hopper, given that it fails to take items. + * Background: A hopper trying to take items will take one item out of each slot and + * put it back right after. Some types of inventories will send comparator updates + * every time a hopper does that. Multiple consecutive comparator updates + * are not distinguishable from a single one and can therefore be omitted. + *

+ * A hopper failing to take items can be predicted by testing whether the inventory + * modification counter {@link LithiumStackList} of the hopper or the inventory above + * have changed since just before the previous attempt to take items. + *

+ * Decrementing and immediately incrementing the signal strength of an inventory + * cannot be distinguished from setting the signal strength to 0 temporarily. + * + * @author 2No2Name + */ +public enum ComparatorUpdatePattern { + NO_UPDATE { + //example: empty inventory, inventory that does not send comparator updates on hopper pull attempt + + @Override + public ComparatorUpdatePattern thenUpdate() { + return UPDATE; + } + + @Override + public ComparatorUpdatePattern thenDecrementUpdateIncrementUpdate() { + return DECREMENT_UPDATE_INCREMENT_UPDATE; + } + }, + UPDATE { + //example: inventory with items, but removing any single item does not change the signal strength + @Override + public void apply(BlockEntity blockEntity, LithiumStackList stackList) { + blockEntity.setChanged(); + } + + @Override + public ComparatorUpdatePattern thenDecrementUpdateIncrementUpdate() { + return UPDATE_DECREMENT_UPDATE_INCREMENT_UPDATE; + } + }, + DECREMENT_UPDATE_INCREMENT_UPDATE { + //example: inventory with items, but removing the first item reduces the signal strength + @Override + public void apply(BlockEntity blockEntity, LithiumStackList stackList) { + stackList.setReducedSignalStrengthOverride(); + blockEntity.setChanged(); + stackList.clearSignalStrengthOverride(); + blockEntity.setChanged(); + } + + @Override + public boolean isChainable() { + return false; + } + }, + UPDATE_DECREMENT_UPDATE_INCREMENT_UPDATE { + //example: inventory with items, removing the first item does not reduce the signal strength, + // but there is another item that will reduce the signal strength when removed + @Override + public void apply(BlockEntity blockEntity, LithiumStackList stackList) { + blockEntity.setChanged(); + stackList.setReducedSignalStrengthOverride(); + blockEntity.setChanged(); + stackList.clearSignalStrengthOverride(); + blockEntity.setChanged(); + } + + @Override + public boolean isChainable() { + return false; + } + }; + + public void apply(BlockEntity blockEntity, LithiumStackList stackList) { + + } + + public ComparatorUpdatePattern thenUpdate() { + return this; + } + + public ComparatorUpdatePattern thenDecrementUpdateIncrementUpdate() { + return this; + } + + public boolean isChainable() { + return true; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperCachingState.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperCachingState.java new file mode 100644 index 00000000..b5212f88 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperCachingState.java @@ -0,0 +1,29 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +public class HopperCachingState { + + public enum BlockInventory { + UNKNOWN, // No information cached + BLOCK_STATE, // Known to be Composter-like inventory (inventory from block, but not block entity, only depends on block state) + BLOCK_ENTITY, // Known to be BlockEntity inventory without removal tracking capability + REMOVAL_TRACKING_BLOCK_ENTITY, // Known to be BlockEntity inventory with removal tracking capability + NO_BLOCK_INVENTORY // Known to be a block without hopper interaction (-> interact with entities instead) + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperHelper.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperHelper.java new file mode 100644 index 00000000..f85a1a22 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperHelper.java @@ -0,0 +1,168 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.Mth; +import net.minecraft.world.CompoundContainer; +import net.minecraft.world.Container; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.HopperBlockEntity; +import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity; +import net.minecraft.world.level.chunk.LevelChunk; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.lithium.common.util.DirectionConstants; +import org.leavesmc.leaves.lithium.common.world.WorldHelper; + +import java.util.Map; + +public class HopperHelper { + + public static boolean tryMoveSingleItem(Container to, ItemStack stack, @Nullable Direction fromDirection) { + ItemStack transferChecker; + if (to.lithium$itemInsertionTestRequiresStackSize1()) { + transferChecker = stack.copy(); + transferChecker.setCount(1); + } else { + transferChecker = stack; + } + + WorldlyContainer toSided = to instanceof WorldlyContainer ? ((WorldlyContainer) to) : null; + if (toSided != null && fromDirection != null) { + int[] slots = toSided.getSlotsForFace(fromDirection); + + for (int slotIndex = 0; slotIndex < slots.length; ++slotIndex) { + if (tryMoveSingleItem(to, toSided, stack, transferChecker, slots[slotIndex], fromDirection)) { + return true; //caller needs to take the item from the original inventory and call to.markDirty() + } + } + } else { + int j = to.getContainerSize(); + for (int slot = 0; slot < j; ++slot) { + if (tryMoveSingleItem(to, toSided, stack, transferChecker, slot, fromDirection)) { + return true; //caller needs to take the item from the original inventory and call to.markDirty() + } + } + } + return false; + } + + public static boolean tryMoveSingleItem(Container to, @Nullable WorldlyContainer toSided, ItemStack transferStack, ItemStack transferChecker, int targetSlot, @Nullable Direction fromDirection) { + ItemStack toStack = to.getItem(targetSlot); + if (to.canPlaceItem(targetSlot, transferChecker) && (toSided == null || toSided.canPlaceItemThroughFace(targetSlot, transferChecker, fromDirection))) { + int toCount; + if (toStack.isEmpty()) { + ItemStack singleItem = transferStack.split(1); + to.setItem(targetSlot, singleItem); + return true; //caller needs to call to.markDirty() + } else if (toStack.getMaxStackSize() > (toCount = toStack.getCount()) && to.getMaxStackSize() > toCount && ItemStack.isSameItemSameComponents(toStack, transferStack)) { + transferStack.shrink(1); + toStack.grow(1); + return true; //caller needs to call to.markDirty() + } + } + return false; + } + + private static int calculateReducedSignalStrength(float contentWeight, int inventorySize, int inventoryMaxCountPerStack, int numOccupiedSlots, int itemStackCount, int itemStackMaxCount) { + //contentWeight adaption can include rounding error for non-power of 2 max stack sizes, which do not exist in vanilla anyways + int maxStackSize = Math.min(inventoryMaxCountPerStack, itemStackMaxCount); + int newNumOccupiedSlots = numOccupiedSlots - (itemStackCount == 1 ? 1 : 0); + float newContentWeight = contentWeight - (1f / (float) maxStackSize); + newContentWeight /= (float) inventorySize; + return Mth.floor(newContentWeight * 14.0F) + (newNumOccupiedSlots > 0 ? 1 : 0); + } + + public static ComparatorUpdatePattern determineComparatorUpdatePattern(Container from, LithiumStackList fromStackList) { + if ((from instanceof HopperBlockEntity) || !(from instanceof RandomizableContainerBlockEntity)) { + return ComparatorUpdatePattern.NO_UPDATE; + } + //calculate the signal strength of the inventory, but also keep the content weight variable + float contentWeight = 0f; + int numOccupiedSlots = 0; + + for (int j = 0; j < from.getContainerSize(); ++j) { + ItemStack itemStack = from.getItem(j); + if (!itemStack.isEmpty()) { + int maxStackSize = Math.min(from.getMaxStackSize(), itemStack.getMaxStackSize()); + contentWeight += itemStack.getCount() / (float) maxStackSize; + ++numOccupiedSlots; + } + } + float f = contentWeight; + f /= (float) from.getContainerSize(); + int originalSignalStrength = Mth.floor(f * 14.0F) + (numOccupiedSlots > 0 ? 1 : 0); + + + ComparatorUpdatePattern updatePattern = ComparatorUpdatePattern.NO_UPDATE; + //check the signal strength change when failing to extract from each slot + int[] availableSlots = from instanceof WorldlyContainer ? ((WorldlyContainer) from).getSlotsForFace(Direction.DOWN) : null; + WorldlyContainer sidedInventory = from instanceof WorldlyContainer ? (WorldlyContainer) from : null; + int fromSize = availableSlots != null ? availableSlots.length : from.getContainerSize(); + for (int i = 0; i < fromSize; i++) { + int fromSlot = availableSlots != null ? availableSlots[i] : i; + ItemStack itemStack = fromStackList.get(fromSlot); + if (!itemStack.isEmpty() && (sidedInventory == null || sidedInventory.canTakeItemThroughFace(fromSlot, itemStack, Direction.DOWN))) { + int newSignalStrength = calculateReducedSignalStrength(contentWeight, from.getContainerSize(), from.getMaxStackSize(), numOccupiedSlots, itemStack.getCount(), itemStack.getMaxStackSize()); + if (newSignalStrength != originalSignalStrength) { + updatePattern = updatePattern.thenDecrementUpdateIncrementUpdate(); + } else { + updatePattern = updatePattern.thenUpdate(); + } + if (!updatePattern.isChainable()) { + break; //if the pattern is indistinguishable from all extensions of the pattern, stop iterating + } + } + } + return updatePattern; + } + + public static Container replaceDoubleInventory(Container blockInventory) { + if (blockInventory instanceof CompoundContainer doubleInventory) { + doubleInventory = LithiumDoubleInventory.getLithiumInventory(doubleInventory); + if (doubleInventory != null) { + return doubleInventory; + } + } + return blockInventory; + } + + public static void updateHopperOnUpdateSuppression(Level level, BlockPos pos, int flags, LevelChunk worldChunk, boolean stateChange) { + if ((flags & Block.UPDATE_NEIGHBORS) == 0 && stateChange) { + //No block updates were sent. We need to update nearby hoppers to avoid outdated inventory caches being used + + //Small performance improvement when getting block entities within the same chunk. + Map blockEntities = WorldHelper.areNeighborsWithinSameChunk(pos) ? worldChunk.getBlockEntities() : null; + if (blockEntities == null || !blockEntities.isEmpty()) { + for (Direction direction : DirectionConstants.ALL) { + BlockPos offsetPos = pos.relative(direction); + //Directly get the block entity instead of getting the block state first. Maybe that is faster, maybe not. + BlockEntity hopper = blockEntities != null ? blockEntities.get(offsetPos) : level.lithium$getLoadedExistingBlockEntity(offsetPos); + if (hopper instanceof HopperBlockEntity hopperBlockEntity) { + hopperBlockEntity.lithium$invalidateCacheOnNeighborUpdate(direction == Direction.DOWN); + } + } + } + } + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/InventoryHelper.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/InventoryHelper.java new file mode 100644 index 00000000..897d3928 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/InventoryHelper.java @@ -0,0 +1,52 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +import net.minecraft.core.NonNullList; +import net.minecraft.world.item.ItemStack; +import org.leavesmc.leaves.lithium.api.inventory.LithiumInventory; + +public class InventoryHelper { + public static LithiumStackList getLithiumStackList(LithiumInventory inventory) { + NonNullList stackList = inventory.getInventoryLithium(); + if (stackList instanceof LithiumStackList lithiumStackList) { + return lithiumStackList; + } + return upgradeToLithiumStackList(inventory); + } + + public static LithiumStackList getLithiumStackListOrNull(LithiumInventory inventory) { + NonNullList stackList = inventory.getInventoryLithium(); + if (stackList instanceof LithiumStackList lithiumStackList) { + return lithiumStackList; + } + return null; + } + + private static LithiumStackList upgradeToLithiumStackList(LithiumInventory inventory) { + //generate loot to avoid any problems with directly accessing the inventory slots + //the loot that is generated here is not generated earlier than in vanilla, because vanilla generates loot + //when the hopper checks whether the inventory is empty or full + inventory.generateLootLithium(); + //get the stack list after generating loot, just in case generating loot creates a new stack list + NonNullList stackList = inventory.getInventoryLithium(); + LithiumStackList lithiumStackList = new LithiumStackList(stackList, inventory.getMaxStackSize()); + inventory.setInventoryLithium(lithiumStackList); + return lithiumStackList; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleInventory.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleInventory.java new file mode 100644 index 00000000..8ed221a8 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleInventory.java @@ -0,0 +1,190 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.core.Direction; +import net.minecraft.core.NonNullList; +import net.minecraft.world.CompoundContainer; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import org.leavesmc.leaves.lithium.api.inventory.LithiumInventory; +import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeEmitter; +import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeListener; +import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker; +import org.leavesmc.leaves.lithium.common.block.entity.inventory_comparator_tracking.ComparatorTracker; + +public class LithiumDoubleInventory extends CompoundContainer implements LithiumInventory, InventoryChangeTracker, InventoryChangeEmitter, InventoryChangeListener, ComparatorTracker { + + private final LithiumInventory first; + private final LithiumInventory second; + + private LithiumStackList doubleStackList; + + ReferenceOpenHashSet inventoryChangeListeners = null; + ReferenceOpenHashSet inventoryHandlingTypeListeners = null; + + /** + * This method returns the same LithiumDoubleInventory instance for equal (same children in same order) + * doubleInventory parameters until {@link #lithium$emitRemoved()} is called. After that a new LithiumDoubleInventory object + * may be in use. + * + * @param doubleInventory A double inventory + * @return The only non-removed LithiumDoubleInventory instance for the double inventory. Null if not compatible + */ + public static LithiumDoubleInventory getLithiumInventory(CompoundContainer doubleInventory) { + Container vanillaFirst = doubleInventory.container1; + Container vanillaSecond = doubleInventory.container2; + if (vanillaFirst != vanillaSecond && vanillaFirst instanceof LithiumInventory first && vanillaSecond instanceof LithiumInventory second) { + LithiumDoubleInventory newDoubleInventory = new LithiumDoubleInventory(first, second); + LithiumDoubleStackList doubleStackList = LithiumDoubleStackList.getOrCreate( + newDoubleInventory, + InventoryHelper.getLithiumStackList(first), + InventoryHelper.getLithiumStackList(second), + newDoubleInventory.getMaxStackSize() + ); + newDoubleInventory.doubleStackList = doubleStackList; + return doubleStackList.doubleInventory; + } + return null; + } + + private LithiumDoubleInventory(LithiumInventory first, LithiumInventory second) { + super(first, second); + this.first = first; + this.second = second; + } + + @Override + public void lithium$emitContentModified() { + ReferenceOpenHashSet inventoryChangeListeners = this.inventoryChangeListeners; + if (inventoryChangeListeners != null) { + for (InventoryChangeListener inventoryChangeListener : inventoryChangeListeners) { + inventoryChangeListener.lithium$handleInventoryContentModified(this); + } + inventoryChangeListeners.clear(); + } + } + + @Override + public void lithium$emitStackListReplaced() { + ReferenceOpenHashSet listeners = this.inventoryHandlingTypeListeners; + if (listeners != null && !listeners.isEmpty()) { + listeners.forEach(inventoryChangeListener -> inventoryChangeListener.handleStackListReplaced(this)); + } + + this.invalidateChangeListening(); + } + + @Override + public void lithium$emitRemoved() { + ReferenceOpenHashSet listeners = this.inventoryHandlingTypeListeners; + if (listeners != null && !listeners.isEmpty()) { + listeners.forEach(listener -> listener.lithium$handleInventoryRemoved(this)); + } + + this.invalidateChangeListening(); + } + + private void invalidateChangeListening() { + if (this.inventoryChangeListeners != null) { + this.inventoryChangeListeners.clear(); + } + + LithiumStackList lithiumStackList = InventoryHelper.getLithiumStackListOrNull(this); + if (lithiumStackList != null) { + lithiumStackList.removeInventoryModificationCallback(this); + } + } + + @Override + public void lithium$emitFirstComparatorAdded() { + ReferenceOpenHashSet inventoryChangeListeners = this.inventoryChangeListeners; + if (inventoryChangeListeners != null && !inventoryChangeListeners.isEmpty()) { + inventoryChangeListeners.removeIf(inventoryChangeListener -> inventoryChangeListener.lithium$handleComparatorAdded(this)); + } + } + + @Override + public void lithium$forwardContentChangeOnce(InventoryChangeListener inventoryChangeListener, LithiumStackList stackList, InventoryChangeTracker thisTracker) { + if (this.inventoryChangeListeners == null) { + this.inventoryChangeListeners = new ReferenceOpenHashSet<>(1); + } + stackList.setInventoryModificationCallback(thisTracker); + this.inventoryChangeListeners.add(inventoryChangeListener); + + } + + @Override + public void lithium$forwardMajorInventoryChanges(InventoryChangeListener inventoryChangeListener) { + if (this.inventoryHandlingTypeListeners == null) { + this.inventoryHandlingTypeListeners = new ReferenceOpenHashSet<>(1); + + ((InventoryChangeTracker) this.first).listenForMajorInventoryChanges(this); + ((InventoryChangeTracker) this.second).listenForMajorInventoryChanges(this); + } + this.inventoryHandlingTypeListeners.add(inventoryChangeListener); + } + + @Override + public void lithium$stopForwardingMajorInventoryChanges(InventoryChangeListener inventoryChangeListener) { + if (this.inventoryHandlingTypeListeners != null) { + this.inventoryHandlingTypeListeners.remove(inventoryChangeListener); + if (this.inventoryHandlingTypeListeners.isEmpty()) { + ((InventoryChangeTracker) this.first).stopListenForMajorInventoryChanges(this); + ((InventoryChangeTracker) this.second).stopListenForMajorInventoryChanges(this); + } + } + } + + @Override + public NonNullList getInventoryLithium() { + return this.doubleStackList; + } + + @Override + public void setInventoryLithium(NonNullList inventory) { + throw new UnsupportedOperationException(); + } + + @Override + public void lithium$handleInventoryContentModified(Container inventory) { + this.lithium$emitContentModified(); + } + + @Override + public void lithium$handleInventoryRemoved(Container inventory) { + this.lithium$emitRemoved(); + } + + @Override + public boolean lithium$handleComparatorAdded(Container inventory) { + this.lithium$emitFirstComparatorAdded(); + return this.inventoryChangeListeners.isEmpty(); + } + + @Override + public void lithium$onComparatorAdded(Direction direction, int offset) { + throw new UnsupportedOperationException("Call onComparatorAdded(Direction direction, int offset) on the inventory half only!"); + } + + @Override + public boolean lithium$hasAnyComparatorNearby() { + return ((ComparatorTracker) this.first).lithium$hasAnyComparatorNearby() || ((ComparatorTracker) this.second).lithium$hasAnyComparatorNearby(); + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleStackList.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleStackList.java new file mode 100644 index 00000000..40fd1f39 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleStackList.java @@ -0,0 +1,162 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +import net.minecraft.world.CompoundContainer; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker; + +/** + * Class to allow DoubleInventory to have LithiumStackList optimizations. + * The objects should be immutable and their state should be limited to the first and second inventory. + * Other state must be managed carefully, as at any time objects of this class may be replaced with new instances. + */ +public class LithiumDoubleStackList extends LithiumStackList { + private final LithiumStackList first; + private final LithiumStackList second; + final LithiumDoubleInventory doubleInventory; + + private long signalStrengthChangeCount; + + public LithiumDoubleStackList(LithiumDoubleInventory doubleInventory, LithiumStackList first, LithiumStackList second, int maxCountPerStack) { + super(maxCountPerStack); + this.first = first; + this.second = second; + this.doubleInventory = doubleInventory; + } + + public static LithiumDoubleStackList getOrCreate(LithiumDoubleInventory doubleInventory, LithiumStackList first, LithiumStackList second, int maxCountPerStack) { + LithiumDoubleStackList parentStackList = first.parent; + if (parentStackList == null || parentStackList != second.parent || parentStackList.first != first || parentStackList.second != second) { + if (parentStackList != null) { + parentStackList.doubleInventory.lithium$emitRemoved(); + } + parentStackList = new LithiumDoubleStackList(doubleInventory, first, second, maxCountPerStack); + first.parent = parentStackList; + second.parent = parentStackList; + } + return parentStackList; + } + + @Override + public long getModCount() { + return this.first.getModCount() + this.second.getModCount(); + } + + @Override + public void changedALot() { + throw new UnsupportedOperationException("Call changed() on the inventory half only!"); + } + + @Override + public void changed() { + throw new UnsupportedOperationException("Call changed() on the inventory half only!"); + } + + @Override + public ItemStack set(int index, ItemStack element) { + if (index >= this.first.size()) { + return this.second.set(index - this.first.size(), element); + } else { + return this.first.set(index, element); + } + } + + @Override + public void add(int slot, ItemStack element) { + throw new UnsupportedOperationException("Call add(int value, ItemStack element) on the inventory half only!"); + } + + @Override + public ItemStack remove(int index) { + throw new UnsupportedOperationException("Call remove(int value, ItemStack element) on the inventory half only!"); + } + + @Override + public void clear() { + this.first.clear(); + this.second.clear(); + } + + @Override + public int getSignalStrength(Container inventory) { + //signal strength override state has to be stored in the halves, because this object may be replaced with a copy at any time + boolean signalStrengthOverride = this.first.hasSignalStrengthOverride() || this.second.hasSignalStrengthOverride(); + if (signalStrengthOverride) { + return 0; + } + int cachedSignalStrength = this.cachedSignalStrength; + if (cachedSignalStrength == -1 || this.getModCount() != this.signalStrengthChangeCount) { + cachedSignalStrength = this.calculateSignalStrength(Integer.MAX_VALUE); + this.signalStrengthChangeCount = this.getModCount(); + this.cachedSignalStrength = cachedSignalStrength; + return cachedSignalStrength; + } + return cachedSignalStrength; + } + + @Override + public void setReducedSignalStrengthOverride() { + this.first.setReducedSignalStrengthOverride(); + this.second.setReducedSignalStrengthOverride(); + } + + @Override + public void clearSignalStrengthOverride() { + this.first.clearSignalStrengthOverride(); + this.second.clearSignalStrengthOverride(); + } + + /** + * @param masterStackList the stacklist of the inventory that comparators read from (double inventory for double chests) + * @param inventory the blockentity / inventory that this stacklist is inside + */ + public void runComparatorUpdatePatternOnFailedExtract(LithiumStackList masterStackList, Container inventory) { + if (inventory instanceof CompoundContainer compoundContainer) { + this.first.runComparatorUpdatePatternOnFailedExtract( + this, compoundContainer.container1 + ); + this.second.runComparatorUpdatePatternOnFailedExtract( + this, compoundContainer.container2 + ); + } + } + + @NotNull + @Override + public ItemStack get(int index) { + return index >= this.first.size() ? this.second.get(index - this.first.size()) : this.first.get(index); + } + + @Override + public int size() { + return this.first.size() + this.second.size(); + } + + public void setInventoryModificationCallback(@NotNull InventoryChangeTracker inventoryModificationCallback) { + this.first.setInventoryModificationCallback(inventoryModificationCallback); + this.second.setInventoryModificationCallback(inventoryModificationCallback); + } + + public void removeInventoryModificationCallback(@NotNull InventoryChangeTracker inventoryModificationCallback) { + this.first.removeInventoryModificationCallback(inventoryModificationCallback); + this.second.removeInventoryModificationCallback(inventoryModificationCallback); + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumStackList.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumStackList.java new file mode 100644 index 00000000..bca3898f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumStackList.java @@ -0,0 +1,304 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +import net.minecraft.core.NonNullList; +import net.minecraft.util.Mth; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.lithium.api.inventory.LithiumDefaultedList; +import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker; +import org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber; + +public class LithiumStackList extends NonNullList implements LithiumDefaultedList, ChangeSubscriber.CountChangeSubscriber { + final int maxCountPerStack; + + protected int cachedSignalStrength; + private ComparatorUpdatePattern cachedComparatorUpdatePattern; + + private boolean signalStrengthOverride; + + private long modCount; + private int occupiedSlots; + private int fullSlots; + + LithiumDoubleStackList parent; //only used for double chests + + InventoryChangeTracker inventoryModificationCallback; + + public LithiumStackList(NonNullList original, int maxCountPerStack) { + super(original.list, ItemStack.EMPTY); + this.maxCountPerStack = maxCountPerStack; + + this.cachedSignalStrength = -1; + this.cachedComparatorUpdatePattern = null; + this.modCount = 0; + this.signalStrengthOverride = false; + + this.occupiedSlots = 0; + this.fullSlots = 0; + int size = this.size(); + for (int i = 0; i < size; i++) { + ItemStack stack = this.get(i); + if (!stack.isEmpty()) { + this.occupiedSlots++; + if (stack.getMaxStackSize() <= stack.getCount()) { + this.fullSlots++; + } + stack.lithium$subscribe(this, i); + } + } + + this.inventoryModificationCallback = null; + } + + public LithiumStackList(int maxCountPerStack) { + super(null, ItemStack.EMPTY); + this.maxCountPerStack = maxCountPerStack; + this.cachedSignalStrength = -1; + this.inventoryModificationCallback = null; + } + + public long getModCount() { + return this.modCount; + } + + public void changedALot() { + this.changed(); + + //fix the slot mapping of all stacks in the inventory + //fix occupied/full slot counters + this.occupiedSlots = 0; + this.fullSlots = 0; + int size = this.size(); + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < size; i++) { + ItemStack stack = this.get(i); + if (!stack.isEmpty()) { + this.occupiedSlots++; + if (stack.getMaxStackSize() <= stack.getCount()) { + this.fullSlots++; + } + stack.lithium$unsubscribe(this); + } + } + for (int i = 0; i < size; i++) { + ItemStack stack = this.get(i); + if (!stack.isEmpty()) { + stack.lithium$subscribe(this, i); + } + } + + } + + /** + * Method that must be invoked before or after a change of the inventory to update important values. If done too + * early or too late, behavior might be incorrect. + */ + public void changed() { + this.cachedSignalStrength = -1; + this.cachedComparatorUpdatePattern = null; + this.modCount++; + + InventoryChangeTracker inventoryModificationCallback = this.inventoryModificationCallback; + if (inventoryModificationCallback != null) { + this.inventoryModificationCallback = null; + inventoryModificationCallback.lithium$emitContentModified(); + } + } + + @Override + public ItemStack set(int index, ItemStack element) { + ItemStack previous = super.set(index, element); + + //Handle vanilla's item stack resurrection in HopperBlockEntity extract(Hopper hopper, Inventory inventory, int slot, Direction side): + // Item stacks are set to 0 items, then back to 1. Then inventory.set(index, element) is called. + // At this point, the LithiumStackList unsubscribed from the stack when it reached 0. + // Handle: If the previous == element, and the stack is not subscribed, we handle it as if an empty stack was replaced. + if (previous == element && !element.isEmpty()) { + boolean notSubscribed = previous.lithium$isSubscribedWithData(this, index); + if (!notSubscribed) { + previous = ItemStack.EMPTY; + } + } + + if (previous != element) { + if (!previous.isEmpty()) { + previous.lithium$unsubscribeWithData(this, index); + } + if (!element.isEmpty()) { + element.lithium$subscribe(this, index); + } + + this.occupiedSlots += (previous.isEmpty() ? 1 : 0) - (element.isEmpty() ? 1 : 0); + this.fullSlots += (element.getCount() >= element.getMaxStackSize() ? 1 : 0) - (previous.getCount() >= previous.getMaxStackSize() ? 1 : 0); + this.changed(); + } + + return previous; + } + + @Override + public void add(int slot, ItemStack element) { + super.add(slot, element); + if (!element.isEmpty()) { + element.lithium$subscribe(this, this.indexOf(element)); + } + this.changedALot(); + } + + @Override + public ItemStack remove(int index) { + ItemStack previous = super.remove(index); + if (!previous.isEmpty()) { + previous.lithium$unsubscribeWithData(this, index); + } + this.changedALot(); + return previous; + } + + @Override + public void clear() { + int size = this.size(); + for (int i = 0; i < size; i++) { + ItemStack stack = this.get(i); + if (!stack.isEmpty()) { + stack.lithium$unsubscribeWithData(this, i); + } + } + super.clear(); + this.changedALot(); + } + + public boolean hasSignalStrengthOverride() { + return this.signalStrengthOverride; + } + + public int getSignalStrength(Container inventory) { + if (this.signalStrengthOverride) { + return 0; + } + int signalStrength = this.cachedSignalStrength; + if (signalStrength == -1) { + return this.cachedSignalStrength = this.calculateSignalStrength(inventory.getContainerSize()); + } + return signalStrength; + } + + /** + * [VanillaCopy] {@link net.minecraft.world.inventory.AbstractContainerMenu#getRedstoneSignalFromContainer(Container)} + * + * @return the signal strength for this inventory + */ + int calculateSignalStrength(int inventorySize) { + int i = 0; + float f = 0.0F; + + inventorySize = Math.min(inventorySize, this.size()); + for (int j = 0; j < inventorySize; ++j) { + ItemStack itemStack = this.get(j); + if (!itemStack.isEmpty()) { + f += (float) itemStack.getCount() / (float) Math.min(this.maxCountPerStack, itemStack.getMaxStackSize()); + ++i; + } + } + + f /= (float) inventorySize; + return Mth.floor(f * 14.0F) + (i > 0 ? 1 : 0); + } + + public void setReducedSignalStrengthOverride() { + this.signalStrengthOverride = true; + } + + public void clearSignalStrengthOverride() { + this.signalStrengthOverride = false; + } + + + /** + * @param masterStackList the stacklist of the inventory that comparators read from (double inventory for double chests) + * @param inventory the blockentity / inventory that this stacklist is inside + */ + public void runComparatorUpdatePatternOnFailedExtract(LithiumStackList masterStackList, Container inventory) { + if (inventory instanceof BlockEntity) { + if (this.cachedComparatorUpdatePattern == null) { + this.cachedComparatorUpdatePattern = HopperHelper.determineComparatorUpdatePattern(inventory, masterStackList); + } + this.cachedComparatorUpdatePattern.apply((BlockEntity) inventory, masterStackList); + } + } + + public boolean maybeSendsComparatorUpdatesOnFailedExtract() { + return this.cachedComparatorUpdatePattern == null || this.cachedComparatorUpdatePattern != ComparatorUpdatePattern.NO_UPDATE; + } + + public int getOccupiedSlots() { + return this.occupiedSlots; + } + + public int getFullSlots() { + return this.fullSlots; + } + + @Override + public void changedInteractionConditions() { + this.changed(); + } + + + public void setInventoryModificationCallback(@NotNull InventoryChangeTracker inventoryModificationCallback) { + if (this.inventoryModificationCallback != null && this.inventoryModificationCallback != inventoryModificationCallback) { + this.inventoryModificationCallback.emitCallbackReplaced(); + } + this.inventoryModificationCallback = inventoryModificationCallback; + } + + public void removeInventoryModificationCallback(@NotNull InventoryChangeTracker inventoryModificationCallback) { + if (this.inventoryModificationCallback != null && this.inventoryModificationCallback == inventoryModificationCallback) { + this.inventoryModificationCallback = null; + } + } + + @Override + public void lithium$notify(@Nullable ItemStack publisher, int subscriberData) { + //Item component changes: LithiumStackList does not care about this + } + + @Override + public void lithium$forceUnsubscribe(ItemStack publisher, int subscriberData) { + throw new UnsupportedOperationException("Cannot force unsubscribe on a LithiumStackList!"); + } + + @Override + public void lithium$notifyCount(ItemStack stack, int index, int newCount) { + assert stack == this.get(index); + int count = stack.getCount(); + if (newCount <= 0) { + stack.lithium$unsubscribeWithData(this, index); + } + int maxCount = stack.getMaxStackSize(); + this.occupiedSlots -= newCount <= 0 ? 1 : 0; + this.fullSlots += (newCount >= maxCount ? 1 : 0) - (count >= maxCount ? 1 : 0); + + this.changed(); + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/UpdateReceiver.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/UpdateReceiver.java new file mode 100644 index 00000000..ab96fa2f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/UpdateReceiver.java @@ -0,0 +1,28 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +import net.minecraft.core.Direction; + +public interface UpdateReceiver { + void lithium$invalidateCacheOnNeighborUpdate(boolean above); + + void lithium$invalidateCacheOnUndirectedNeighborUpdate(); + + void lithium$invalidateCacheOnNeighborUpdate(Direction fromDirection); +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementListener.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementListener.java new file mode 100644 index 00000000..16ba4dd1 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementListener.java @@ -0,0 +1,5 @@ +package org.leavesmc.leaves.lithium.common.tracking.entity; + +public interface ChunkSectionEntityMovementListener { + void handleEntityMovement(); +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementTracker.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementTracker.java new file mode 100644 index 00000000..23941cd0 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementTracker.java @@ -0,0 +1,68 @@ +package org.leavesmc.leaves.lithium.common.tracking.entity; + +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public abstract class ChunkSectionEntityMovementTracker { + protected long lastChangeTime = 0; + protected final ReferenceOpenHashSet listeners = new ReferenceOpenHashSet<>(); + protected final long sectionKey; + protected int userCount = 0; + + public ChunkSectionEntityMovementTracker(long sectionKey) { + this.sectionKey = sectionKey; + } + + public void register() { + this.userCount++; + } + + public abstract void unregister(); + + public static void unregister(@NotNull List trackers) { + for (ChunkSectionEntityMovementTracker tracker : trackers) { + tracker.unregister(); + } + } + + public boolean isUnchangedSince(long lastCheckedTime) { + return this.lastChangeTime <= lastCheckedTime; + } + + public static boolean isUnchangedSince(long lastCheckedTime, @NotNull List trackers) { + for (ChunkSectionEntityMovementTracker tracker : trackers) { + if (!tracker.isUnchangedSince(lastCheckedTime)) { + return false; + } + } + return true; + } + + public void listenToEntityMovementOnce(ChunkSectionEntityMovementListener listener) { + this.listeners.add(listener); + } + + public static void listenToEntityMovementOnce(ChunkSectionEntityMovementListener listener, @NotNull List trackers) { + for (ChunkSectionEntityMovementTracker tracker : trackers) { + tracker.listenToEntityMovementOnce(listener); + } + } + + private void setChanged(long atTime) { + if (atTime > this.lastChangeTime) { + this.lastChangeTime = atTime; + } + } + + public void notifyAllListeners(long time) { + if (!listeners.isEmpty()) { + for (ChunkSectionEntityMovementListener listener : listeners) { + listener.handleEntityMovement(); + } + listeners.clear(); + } + setChanged(time); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionInventoryEntityTracker.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionInventoryEntityTracker.java new file mode 100644 index 00000000..4b6ad915 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionInventoryEntityTracker.java @@ -0,0 +1,67 @@ +package org.leavesmc.leaves.lithium.common.tracking.entity; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.Container; +import net.minecraft.world.entity.EntitySelector; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.lithium.common.util.tuples.WorldSectionBox; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class ChunkSectionInventoryEntityTracker extends ChunkSectionEntityMovementTracker { + public static final Map containerEntityMovementTrackerMap = new java.util.HashMap<>(); + + public ChunkSectionInventoryEntityTracker(long sectionKey) { + super(sectionKey); + } + + @Override + public void unregister() { + this.userCount--; + if (this.userCount <= 0) { + containerEntityMovementTrackerMap.remove(sectionKey); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public static @NotNull List getEntities(@NotNull Level level, AABB boundingBox) { + return level.getEntitiesOfClass((Class) Container.class, boundingBox, EntitySelector.CONTAINER_ENTITY_SELECTOR); + } + + public static @NotNull List registerAt(ServerLevel world, AABB interactionArea) { + WorldSectionBox worldSectionBox = WorldSectionBox.entityAccessBox(world, interactionArea); + + if (worldSectionBox.chunkX1() == worldSectionBox.chunkX2() && + worldSectionBox.chunkY1() == worldSectionBox.chunkY2() && + worldSectionBox.chunkZ1() == worldSectionBox.chunkZ2()) { + return Collections.singletonList(registerAt(CoordinateUtils.getChunkSectionKey(worldSectionBox.chunkX1(), worldSectionBox.chunkY1(), worldSectionBox.chunkZ1()))); + } + + List trackers = new ArrayList<>(); + + for (int x = worldSectionBox.chunkX1(); x <= worldSectionBox.chunkX2(); x++) { + for (int y = worldSectionBox.chunkY1(); y <= worldSectionBox.chunkY2(); y++) { + for (int z = worldSectionBox.chunkZ1(); z <= worldSectionBox.chunkZ2(); z++) { + trackers.add(registerAt(CoordinateUtils.getChunkSectionKey(x, y, z))); + } + } + } + + return trackers; + } + + private static @NotNull ChunkSectionInventoryEntityTracker registerAt(long key) { + ChunkSectionInventoryEntityTracker tracker = containerEntityMovementTrackerMap.computeIfAbsent( + key, + k -> new ChunkSectionInventoryEntityTracker(key) + ); + tracker.register(); + return tracker; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionItemEntityMovementTracker.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionItemEntityMovementTracker.java new file mode 100644 index 00000000..f4f9e6a3 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionItemEntityMovementTracker.java @@ -0,0 +1,67 @@ +package org.leavesmc.leaves.lithium.common.tracking.entity; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.EntitySelector; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.lithium.common.util.tuples.WorldSectionBox; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class ChunkSectionItemEntityMovementTracker extends ChunkSectionEntityMovementTracker { + public static final Map itemEntityMovementTrackerMap = new java.util.HashMap<>(); + + public ChunkSectionItemEntityMovementTracker(long sectionKey) { + super(sectionKey); + } + + @Override + public void unregister() { + this.userCount--; + if (this.userCount <= 0) { + itemEntityMovementTrackerMap.remove(sectionKey); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public static @NotNull List getEntities(@NotNull Level level, AABB boundingBox) { + return level.getEntitiesOfClass((Class) ItemEntity.class, boundingBox, EntitySelector.ENTITY_STILL_ALIVE); + } + + public static @NotNull List registerAt(ServerLevel world, AABB interactionArea) { + WorldSectionBox worldSectionBox = WorldSectionBox.entityAccessBox(world, interactionArea); + + if (worldSectionBox.chunkX1() == worldSectionBox.chunkX2() && + worldSectionBox.chunkY1() == worldSectionBox.chunkY2() && + worldSectionBox.chunkZ1() == worldSectionBox.chunkZ2()) { + return Collections.singletonList(registerAt(CoordinateUtils.getChunkSectionKey(worldSectionBox.chunkX1(), worldSectionBox.chunkY1(), worldSectionBox.chunkZ1()))); + } + + List trackers = new ArrayList<>(); + + for (int x = worldSectionBox.chunkX1(); x <= worldSectionBox.chunkX2(); x++) { + for (int y = worldSectionBox.chunkY1(); y <= worldSectionBox.chunkY2(); y++) { + for (int z = worldSectionBox.chunkZ1(); z <= worldSectionBox.chunkZ2(); z++) { + trackers.add(registerAt(CoordinateUtils.getChunkSectionKey(x, y, z))); + } + } + } + + return trackers; + } + + private static @NotNull ChunkSectionItemEntityMovementTracker registerAt(long key) { + ChunkSectionItemEntityMovementTracker tracker = itemEntityMovementTrackerMap.computeIfAbsent( + key, + k -> new ChunkSectionItemEntityMovementTracker(key) + ); + tracker.register(); + return tracker; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/DirectionConstants.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/DirectionConstants.java new file mode 100644 index 00000000..817be4be --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/DirectionConstants.java @@ -0,0 +1,33 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.util; + +import net.minecraft.core.Direction; + +/** + * Pre-initialized constants to avoid unnecessary allocations. + */ +public final class DirectionConstants { + private DirectionConstants() { + } + + public static final Direction[] ALL = Direction.values(); + public static final Direction[] VERTICAL = {Direction.DOWN, Direction.UP}; + public static final Direction[] HORIZONTAL = {Direction.WEST, Direction.EAST, Direction.NORTH, Direction.SOUTH}; + public static final byte[] HORIZONTAL_OPPOSITE_INDICES = {1, 0, 3, 2}; +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangePublisher.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangePublisher.java new file mode 100644 index 00000000..cd0a9878 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangePublisher.java @@ -0,0 +1,34 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.util.change_tracking; + +import net.minecraft.world.item.ItemStack; + +public interface ChangePublisher { + void lithium$subscribe(ChangeSubscriber subscriber, int subscriberData); + + int lithium$unsubscribe(ChangeSubscriber subscriber); + + default void lithium$unsubscribeWithData(ChangeSubscriber subscriber, int index) { + throw new UnsupportedOperationException("Only implemented for ItemStacks"); + } + + default boolean lithium$isSubscribedWithData(ChangeSubscriber subscriber, int subscriberData) { + throw new UnsupportedOperationException("Only implemented for ItemStacks"); + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangeSubscriber.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangeSubscriber.java new file mode 100644 index 00000000..3cbb2758 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangeSubscriber.java @@ -0,0 +1,211 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.util.change_tracking; + +import it.unimi.dsi.fastutil.ints.IntArrayList; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; + +public interface ChangeSubscriber { + + static ChangeSubscriber combine(ChangeSubscriber prevSubscriber, int prevSData, @NotNull ChangeSubscriber newSubscriber, int newSData) { + if (prevSubscriber == null) { + return newSubscriber; + } else if (prevSubscriber instanceof Multi) { + ArrayList> subscribers = new ArrayList<>(((Multi) prevSubscriber).subscribers); + IntArrayList subscriberDatas = new IntArrayList(((Multi) prevSubscriber).subscriberDatas); + subscribers.add(newSubscriber); + subscriberDatas.add(newSData); + return new Multi<>(subscribers, subscriberDatas); + } else { + ArrayList> subscribers = new ArrayList<>(); + IntArrayList subscriberDatas = new IntArrayList(); + subscribers.add(prevSubscriber); + subscriberDatas.add(prevSData); + subscribers.add(newSubscriber); + subscriberDatas.add(newSData); + return new Multi<>(subscribers, subscriberDatas); + } + } + + static ChangeSubscriber without(ChangeSubscriber prevSubscriber, ChangeSubscriber removedSubscriber) { + return without(prevSubscriber, removedSubscriber, 0, false); + } + + static ChangeSubscriber without(ChangeSubscriber prevSubscriber, ChangeSubscriber removedSubscriber, int removedSubscriberData, boolean matchData) { + if (prevSubscriber == removedSubscriber) { + return null; + } else if (prevSubscriber instanceof Multi multi) { + int index = multi.indexOf(removedSubscriber, removedSubscriberData, matchData); + if (index != -1) { + if (multi.subscribers.size() == 2) { + return multi.subscribers.get(1 - index); + } else { + ArrayList> subscribers = new ArrayList<>(multi.subscribers); + IntArrayList subscriberDatas = new IntArrayList(multi.subscriberDatas); + subscribers.remove(index); + subscriberDatas.removeInt(index); + + return new Multi<>(subscribers, subscriberDatas); + } + } else { + return prevSubscriber; + } + } else { + return prevSubscriber; + } + } + + static int dataWithout(ChangeSubscriber prevSubscriber, ChangeSubscriber removedSubscriber, int subscriberData) { + return dataWithout(prevSubscriber, removedSubscriber, subscriberData, 0, false); + } + + static int dataWithout(ChangeSubscriber prevSubscriber, ChangeSubscriber removedSubscriber, int subscriberData, int removedSubscriberData, boolean matchData) { + if (prevSubscriber instanceof Multi multi) { + int index = multi.indexOf(removedSubscriber, removedSubscriberData, matchData); + if (index != -1) { + if (multi.subscribers.size() == 2) { + return multi.subscriberDatas.getInt(1 - index); + } else { + return subscriberData; + } + } else { + return subscriberData; + } + } + return prevSubscriber == removedSubscriber ? 0 : subscriberData; + } + + static int dataOf(ChangeSubscriber subscribers, ChangeSubscriber subscriber, int subscriberData) { + return subscribers instanceof Multi multi ? multi.subscriberDatas.getInt(multi.subscribers.indexOf(subscriber)) : subscriberData; + } + + static boolean containsSubscriber(ChangeSubscriber subscriber, int subscriberData, ChangeSubscriber subscriber1, int subscriberData1) { + if (subscriber instanceof Multi multi) { + return multi.indexOf(subscriber1, subscriberData1, true) != -1; + } + return subscriber == subscriber1 && subscriberData == subscriberData1; + } + + + /** + * Notify the subscriber that the publisher will be changed immediately after this call. + * + * @param publisher The publisher that is about to change + * @param subscriberData The data associated with the subscriber, given when the subscriber was added + */ + void lithium$notify(@Nullable T publisher, int subscriberData); + + /** + * Notify the subscriber about being unsubscribed from the publisher. Used when the publisher becomes invalid. + * The subscriber should not attempt to unsubscribe itself from the publisher in this method. + * + * @param publisher The publisher unsubscribed from + * @param subscriberData The data associated with the subscriber, given when the subscriber was added + */ + void lithium$forceUnsubscribe(T publisher, int subscriberData); + + interface CountChangeSubscriber extends ChangeSubscriber { + + /** + * Notify the subscriber that the publisher's count data will be changed immediately after this call. + * + * @param publisher The publisher that is about to change + * @param subscriberData The data associated with the subscriber, given when the subscriber was added + * @param newCount The new count of the publisher + */ + void lithium$notifyCount(T publisher, int subscriberData, int newCount); + } + + interface EnchantmentSubscriber extends ChangeSubscriber { + + /** + * Notify the subscriber that the publisher's enchantment data has been changed immediately before this call. + * + * @param publisher The publisher that has changed + * @param subscriberData The data associated with the subscriber, given when the subscriber was added + */ + void lithium$notifyAfterEnchantmentChange(T publisher, int subscriberData); + } + + class Multi implements CountChangeSubscriber, EnchantmentSubscriber { + private final ArrayList> subscribers; + private final IntArrayList subscriberDatas; + + public Multi(ArrayList> subscribers, IntArrayList subscriberDatas) { + this.subscribers = subscribers; + this.subscriberDatas = subscriberDatas; + } + + @Override + public void lithium$notify(T publisher, int subscriberData) { + ArrayList> changeSubscribers = this.subscribers; + for (int i = 0; i < changeSubscribers.size(); i++) { + ChangeSubscriber subscriber = changeSubscribers.get(i); + subscriber.lithium$notify(publisher, this.subscriberDatas.getInt(i)); + } + } + + @Override + public void lithium$forceUnsubscribe(T publisher, int subscriberData) { + ArrayList> changeSubscribers = this.subscribers; + for (int i = 0; i < changeSubscribers.size(); i++) { + ChangeSubscriber subscriber = changeSubscribers.get(i); + subscriber.lithium$forceUnsubscribe(publisher, this.subscriberDatas.getInt(i)); + } + } + + @Override + public void lithium$notifyCount(T publisher, int subscriberData, int newCount) { + ArrayList> changeSubscribers = this.subscribers; + for (int i = 0; i < changeSubscribers.size(); i++) { + ChangeSubscriber subscriber = changeSubscribers.get(i); + if (subscriber instanceof ChangeSubscriber.CountChangeSubscriber countChangeSubscriber) { + countChangeSubscriber.lithium$notifyCount(publisher, this.subscriberDatas.getInt(i), newCount); + } + } + } + + int indexOf(ChangeSubscriber subscriber, int subscriberData, boolean matchData) { + if (!matchData) { + return this.subscribers.indexOf(subscriber); + } else { + for (int i = 0; i < this.subscribers.size(); i++) { + if (this.subscribers.get(i) == subscriber && this.subscriberDatas.getInt(i) == subscriberData) { + return i; + } + } + return -1; + } + } + + @Override + public void lithium$notifyAfterEnchantmentChange(T publisher, int subscriberData) { + ArrayList> changeSubscribers = this.subscribers; + for (int i = 0; i < changeSubscribers.size(); i++) { + ChangeSubscriber subscriber = changeSubscribers.get(i); + if (subscriber instanceof ChangeSubscriber.EnchantmentSubscriber enchantmentSubscriber) { + enchantmentSubscriber.lithium$notifyAfterEnchantmentChange(publisher, this.subscriberDatas.getInt(i)); + } + } + } + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/tuples/WorldSectionBox.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/tuples/WorldSectionBox.java new file mode 100644 index 00000000..c93b0807 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/util/tuples/WorldSectionBox.java @@ -0,0 +1,37 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.util.tuples; + +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; + +//Y values use coordinates, not indices (y=0 -> chunkY=0) +//upper bounds are EXCLUSIVE +public record WorldSectionBox(Level world, int chunkX1, int chunkY1, int chunkZ1, int chunkX2, int chunkY2, + int chunkZ2) { + public static WorldSectionBox entityAccessBox(Level world, AABB box) { + int minX = SectionPos.posToSectionCoord(box.minX - 2.0D); + int minY = SectionPos.posToSectionCoord(box.minY - 4.0D); + int minZ = SectionPos.posToSectionCoord(box.minZ - 2.0D); + int maxX = SectionPos.posToSectionCoord(box.maxX + 2.0D) + 1; + int maxY = SectionPos.posToSectionCoord(box.maxY) + 1; + int maxZ = SectionPos.posToSectionCoord(box.maxZ + 2.0D) + 1; + return new WorldSectionBox(world, minX, minY, minZ, maxX, maxY, maxZ); + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/world/WorldHelper.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/world/WorldHelper.java new file mode 100644 index 00000000..7a467f4b --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/world/WorldHelper.java @@ -0,0 +1,29 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.world; + +import net.minecraft.core.BlockPos; + +public class WorldHelper { + public static boolean areNeighborsWithinSameChunk(BlockPos pos) { + int localX = pos.getX() & 15; + int localZ = pos.getZ() & 15; + + return localX > 0 && localZ > 0 && localX < 15 && localZ < 15; + } +} \ No newline at end of file