--- a/net/minecraft/world/level/block/entity/BlockEntity.java
+++ b/net/minecraft/world/level/block/entity/BlockEntity.java
@@ -3,6 +3,7 @@
 import com.mojang.logging.LogUtils;
 import com.mojang.serialization.Codec;
 import java.util.HashSet;
+import java.util.Objects;
 import java.util.Set;
 import javax.annotation.Nullable;
 import net.minecraft.CrashReportCategory;
@@ -26,7 +27,24 @@
 import net.minecraft.world.level.block.state.BlockState;
 import org.slf4j.Logger;
 
+// CraftBukkit start
+import org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer;
+import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry;
+import org.bukkit.inventory.InventoryHolder;
+// CraftBukkit end
+
+import org.spigotmc.CustomTimingsHandler; // Spigot
+import co.aikar.timings.MinecraftTimings; // Paper
+import co.aikar.timings.Timing; // Paper
+
 public abstract class BlockEntity {
+    static boolean ignoreTileUpdates; // Paper - Perf: Optimize Hoppers
+
+    public Timing tickTimer = MinecraftTimings.getTileEntityTimings(this); // Paper
+    // CraftBukkit start - data containers
+    private static final CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new CraftPersistentDataTypeRegistry();
+    public CraftPersistentDataContainer persistentDataContainer;
+    // CraftBukkit end
     private static final Logger LOGGER = LogUtils.getLogger();
     private final BlockEntityType<?> type;
     @Nullable
@@ -34,12 +52,14 @@
     protected final BlockPos worldPosition;
     protected boolean remove;
     private BlockState blockState;
-    private DataComponentMap components = DataComponentMap.EMPTY;
+    private DataComponentMap components;
 
     public BlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
+        this.components = DataComponentMap.EMPTY;
         this.type = type;
         this.worldPosition = pos.immutable();
         this.blockState = state;
+        this.persistentDataContainer = new CraftPersistentDataContainer(DATA_TYPE_REGISTRY); // Paper - always init
     }
 
     public static BlockPos getPosFromTag(CompoundTag nbt) {
@@ -59,64 +79,89 @@
         return this.level != null;
     }
 
+    // CraftBukkit start - read container
     protected void loadAdditional(CompoundTag nbt, HolderLookup.Provider registryLookup) {
+        this.persistentDataContainer.clear(); // Paper - clear instead of init
+
+        net.minecraft.nbt.Tag persistentDataTag = nbt.get("PublicBukkitValues");
+        if (persistentDataTag instanceof CompoundTag) {
+            this.persistentDataContainer.putAll((CompoundTag) persistentDataTag);
+        }
     }
+    // CraftBukkit end
 
     public final void loadWithComponents(CompoundTag nbt, HolderLookup.Provider registryLookup) {
         this.loadAdditional(nbt, registryLookup);
-        BlockEntity.ComponentHelper.COMPONENTS_CODEC
-            .parse(registryLookup.createSerializationContext(NbtOps.INSTANCE), nbt)
-            .resultOrPartial(error -> LOGGER.warn("Failed to load components: {}", error))
-            .ifPresent(components -> this.components = components);
+        BlockEntity.ComponentHelper.COMPONENTS_CODEC.parse(registryLookup.createSerializationContext(NbtOps.INSTANCE), nbt).resultOrPartial((s) -> {
+            BlockEntity.LOGGER.warn("Failed to load components: {}", s);
+        }).ifPresent((datacomponentmap) -> {
+            this.components = datacomponentmap;
+        });
     }
 
     public final void loadCustomOnly(CompoundTag nbt, HolderLookup.Provider registryLookup) {
         this.loadAdditional(nbt, registryLookup);
     }
 
-    protected void saveAdditional(CompoundTag nbt, HolderLookup.Provider registryLookup) {
-    }
+    protected void saveAdditional(CompoundTag nbt, HolderLookup.Provider registryLookup) {}
 
     public final CompoundTag saveWithFullMetadata(HolderLookup.Provider registryLookup) {
-        CompoundTag compoundTag = this.saveWithoutMetadata(registryLookup);
-        this.saveMetadata(compoundTag);
-        return compoundTag;
+        CompoundTag nbttagcompound = this.saveWithoutMetadata(registryLookup);
+
+        this.saveMetadata(nbttagcompound);
+        return nbttagcompound;
     }
 
     public final CompoundTag saveWithId(HolderLookup.Provider registryLookup) {
-        CompoundTag compoundTag = this.saveWithoutMetadata(registryLookup);
-        this.saveId(compoundTag);
-        return compoundTag;
+        CompoundTag nbttagcompound = this.saveWithoutMetadata(registryLookup);
+
+        this.saveId(nbttagcompound);
+        return nbttagcompound;
     }
 
     public final CompoundTag saveWithoutMetadata(HolderLookup.Provider registryLookup) {
-        CompoundTag compoundTag = new CompoundTag();
-        this.saveAdditional(compoundTag, registryLookup);
-        BlockEntity.ComponentHelper.COMPONENTS_CODEC
-            .encodeStart(registryLookup.createSerializationContext(NbtOps.INSTANCE), this.components)
-            .resultOrPartial(snbt -> LOGGER.warn("Failed to save components: {}", snbt))
-            .ifPresent(nbt -> compoundTag.merge((CompoundTag)nbt));
-        return compoundTag;
+        CompoundTag nbttagcompound = new CompoundTag();
+
+        this.saveAdditional(nbttagcompound, registryLookup);
+        BlockEntity.ComponentHelper.COMPONENTS_CODEC.encodeStart(registryLookup.createSerializationContext(NbtOps.INSTANCE), this.components).resultOrPartial((s) -> {
+            BlockEntity.LOGGER.warn("Failed to save components: {}", s);
+        }).ifPresent((nbtbase) -> {
+            nbttagcompound.merge((CompoundTag) nbtbase);
+        });
+        // CraftBukkit start - store container
+        if (this.persistentDataContainer != null && !this.persistentDataContainer.isEmpty()) {
+            nbttagcompound.put("PublicBukkitValues", this.persistentDataContainer.toTagCompound());
+        }
+        // CraftBukkit end
+        return nbttagcompound;
     }
 
     public final CompoundTag saveCustomOnly(HolderLookup.Provider registryLookup) {
-        CompoundTag compoundTag = new CompoundTag();
-        this.saveAdditional(compoundTag, registryLookup);
-        return compoundTag;
+        CompoundTag nbttagcompound = new CompoundTag();
+
+        this.saveAdditional(nbttagcompound, registryLookup);
+        // Paper start - store PDC here as well
+        if (this.persistentDataContainer != null && !this.persistentDataContainer.isEmpty()) {
+            nbttagcompound.put("PublicBukkitValues", this.persistentDataContainer.toTagCompound());
+        }
+        // Paper end
+        return nbttagcompound;
     }
 
     public final CompoundTag saveCustomAndMetadata(HolderLookup.Provider registryLookup) {
-        CompoundTag compoundTag = this.saveCustomOnly(registryLookup);
-        this.saveMetadata(compoundTag);
-        return compoundTag;
+        CompoundTag nbttagcompound = this.saveCustomOnly(registryLookup);
+
+        this.saveMetadata(nbttagcompound);
+        return nbttagcompound;
     }
 
     public void saveId(CompoundTag nbt) {
-        ResourceLocation resourceLocation = BlockEntityType.getKey(this.getType());
-        if (resourceLocation == null) {
-            throw new RuntimeException(this.getClass() + " is missing a mapping! This is a bug!");
+        ResourceLocation minecraftkey = BlockEntityType.getKey(this.getType());
+
+        if (minecraftkey == null) {
+            throw new RuntimeException(String.valueOf(this.getClass()) + " is missing a mapping! This is a bug!");
         } else {
-            nbt.putString("id", resourceLocation.toString());
+            nbt.putString("id", minecraftkey.toString());
         }
     }
 
@@ -125,9 +170,10 @@
     }
 
     public void saveToItem(ItemStack stack, HolderLookup.Provider registries) {
-        CompoundTag compoundTag = this.saveCustomOnly(registries);
-        this.removeComponentsFromTag(compoundTag);
-        BlockItem.setBlockEntityData(stack, this.getType(), compoundTag);
+        CompoundTag nbttagcompound = this.saveCustomOnly(registries);
+
+        this.removeComponentsFromTag(nbttagcompound);
+        BlockItem.setBlockEntityData(stack, this.getType(), nbttagcompound);
         stack.applyComponents(this.collectComponents());
     }
 
@@ -140,29 +186,30 @@
 
     @Nullable
     public static BlockEntity loadStatic(BlockPos pos, BlockState state, CompoundTag nbt, HolderLookup.Provider registryLookup) {
-        String string = nbt.getString("id");
-        ResourceLocation resourceLocation = ResourceLocation.tryParse(string);
-        if (resourceLocation == null) {
-            LOGGER.error("Block entity has invalid type: {}", string);
+        String s = nbt.getString("id");
+        ResourceLocation minecraftkey = ResourceLocation.tryParse(s);
+
+        if (minecraftkey == null) {
+            BlockEntity.LOGGER.error("Block entity has invalid type: {}", s);
             return null;
         } else {
-            return BuiltInRegistries.BLOCK_ENTITY_TYPE.getOptional(resourceLocation).map(type -> {
+            return (BlockEntity) BuiltInRegistries.BLOCK_ENTITY_TYPE.getOptional(minecraftkey).map((tileentitytypes) -> {
                 try {
-                    return type.create(pos, state);
-                } catch (Throwable var5x) {
-                    LOGGER.error("Failed to create block entity {}", string, var5x);
+                    return tileentitytypes.create(pos, state);
+                } catch (Throwable throwable) {
+                    BlockEntity.LOGGER.error("Failed to create block entity {}", s, throwable);
                     return null;
                 }
-            }).map(blockEntity -> {
+            }).map((tileentity) -> {
                 try {
-                    blockEntity.loadWithComponents(nbt, registryLookup);
-                    return (BlockEntity)blockEntity;
-                } catch (Throwable var5x) {
-                    LOGGER.error("Failed to load data for block entity {}", string, var5x);
+                    tileentity.loadWithComponents(nbt, registryLookup);
+                    return tileentity;
+                } catch (Throwable throwable) {
+                    BlockEntity.LOGGER.error("Failed to load data for block entity {}", s, throwable);
                     return null;
                 }
             }).orElseGet(() -> {
-                LOGGER.warn("Skipping BlockEntity with id {}", string);
+                BlockEntity.LOGGER.warn("Skipping BlockEntity with id {}", s);
                 return null;
             });
         }
@@ -170,8 +217,10 @@
 
     public void setChanged() {
         if (this.level != null) {
-            setChanged(this.level, this.worldPosition, this.blockState);
+            if (ignoreTileUpdates) return; // Paper - Perf: Optimize Hoppers
+            BlockEntity.setChanged(this.level, this.worldPosition, this.blockState);
         }
+
     }
 
     protected static void setChanged(Level world, BlockPos pos, BlockState state) {
@@ -179,6 +228,7 @@
         if (!state.isAir()) {
             world.updateNeighbourForOutputSignal(pos, state.getBlock());
         }
+
     }
 
     public BlockPos getBlockPos() {
@@ -215,9 +265,18 @@
     }
 
     public void fillCrashReportCategory(CrashReportCategory crashReportSection) {
-        crashReportSection.setDetail("Name", () -> BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(this.getType()) + " // " + this.getClass().getCanonicalName());
+        crashReportSection.setDetail("Name", () -> {
+            String s = String.valueOf(BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(this.getType()));
+
+            return s + " // " + this.getClass().getCanonicalName();
+        });
         if (this.level != null) {
-            CrashReportCategory.populateBlockDetails(crashReportSection, this.level, this.worldPosition, this.getBlockState());
+            // Paper start - Prevent block entity and entity crashes
+            BlockState block = this.getBlockState();
+            if (block != null) {
+                CrashReportCategory.populateBlockDetails(crashReportSection, this.level, this.worldPosition, block);
+            }
+            // Paper end - Prevent block entity and entity crashes
             CrashReportCategory.populateBlockDetails(crashReportSection, this.level, this.worldPosition, this.level.getBlockState(this.worldPosition));
         }
     }
@@ -230,52 +289,66 @@
         return this.type;
     }
 
+    /** @deprecated */
     @Deprecated
     public void setBlockState(BlockState state) {
         this.blockState = state;
     }
 
-    protected void applyImplicitComponents(BlockEntity.DataComponentInput components) {
-    }
+    protected void applyImplicitComponents(BlockEntity.DataComponentInput components) {}
 
     public final void applyComponentsFromItemStack(ItemStack stack) {
         this.applyComponents(stack.getPrototype(), stack.getComponentsPatch());
     }
 
     public final void applyComponents(DataComponentMap defaultComponents, DataComponentPatch components) {
-        final Set<DataComponentType<?>> set = new HashSet<>();
+        // CraftBukkit start
+        this.applyComponentsSet(defaultComponents, components);
+    }
+
+    public final Set<DataComponentType<?>> applyComponentsSet(DataComponentMap datacomponentmap, DataComponentPatch datacomponentpatch) {
+        // CraftBukkit end
+        final Set<DataComponentType<?>> set = new HashSet();
+
         set.add(DataComponents.BLOCK_ENTITY_DATA);
-        final DataComponentMap dataComponentMap = PatchedDataComponentMap.fromPatch(defaultComponents, components);
-        this.applyImplicitComponents(new BlockEntity.DataComponentInput() {
+        final PatchedDataComponentMap patcheddatacomponentmap = PatchedDataComponentMap.fromPatch(datacomponentmap, datacomponentpatch);
+
+        this.applyImplicitComponents(new BlockEntity.DataComponentInput() { // CraftBukkit - decompile error
             @Nullable
             @Override
             public <T> T get(DataComponentType<T> type) {
                 set.add(type);
-                return dataComponentMap.get(type);
+                return patcheddatacomponentmap.get(type);
             }
 
             @Override
             public <T> T getOrDefault(DataComponentType<? extends T> type, T fallback) {
                 set.add(type);
-                return dataComponentMap.getOrDefault(type, fallback);
+                return patcheddatacomponentmap.getOrDefault(type, fallback);
             }
         });
-        DataComponentPatch dataComponentPatch = components.forget(set::contains);
-        this.components = dataComponentPatch.split().added();
-    }
+        Objects.requireNonNull(set);
+        DataComponentPatch datacomponentpatch1 = datacomponentpatch.forget(set::contains);
 
-    protected void collectImplicitComponents(DataComponentMap.Builder componentMapBuilder) {
+        this.components = datacomponentpatch1.split().added();
+        // CraftBukkit start
+        set.remove(DataComponents.BLOCK_ENTITY_DATA); // Remove as never actually added by applyImplicitComponents
+        return set;
+        // CraftBukkit end
     }
 
+    protected void collectImplicitComponents(DataComponentMap.Builder componentMapBuilder) {}
+
+    /** @deprecated */
     @Deprecated
-    public void removeComponentsFromTag(CompoundTag nbt) {
-    }
+    public void removeComponentsFromTag(CompoundTag nbt) {}
 
     public final DataComponentMap collectComponents() {
-        DataComponentMap.Builder builder = DataComponentMap.builder();
-        builder.addAll(this.components);
-        this.collectImplicitComponents(builder);
-        return builder.build();
+        DataComponentMap.Builder datacomponentmap_a = DataComponentMap.builder();
+
+        datacomponentmap_a.addAll(this.components);
+        this.collectImplicitComponents(datacomponentmap_a);
+        return datacomponentmap_a.build();
     }
 
     public DataComponentMap components() {
@@ -290,20 +363,45 @@
     public static Component parseCustomNameSafe(String json, HolderLookup.Provider registryLookup) {
         try {
             return Component.Serializer.fromJson(json, registryLookup);
-        } catch (Exception var3) {
-            LOGGER.warn("Failed to parse custom name from string '{}', discarding", json, var3);
+        } catch (Exception exception) {
+            BlockEntity.LOGGER.warn("Failed to parse custom name from string '{}', discarding", json, exception);
             return null;
         }
     }
 
-    static class ComponentHelper {
+    // CraftBukkit start - add method
+    public InventoryHolder getOwner() {
+        // Paper start
+        return getOwner(true);
+    }
+    public InventoryHolder getOwner(boolean useSnapshot) {
+        // Paper end
+        if (this.level == null) return null;
+        org.bukkit.block.Block block = this.level.getWorld().getBlockAt(this.worldPosition.getX(), this.worldPosition.getY(), this.worldPosition.getZ());
+        // if (block.getType() == org.bukkit.Material.AIR) return null; // Paper - actually get the tile entity if it still exists
+        org.bukkit.block.BlockState state = block.getState(useSnapshot); // Paper
+        if (state instanceof InventoryHolder) return (InventoryHolder) state;
+        return null;
+    }
+    // CraftBukkit end
+
+    // Paper start - Sanitize sent data
+    public CompoundTag sanitizeSentNbt(CompoundTag tag) {
+        tag.remove("PublicBukkitValues");
+
+        return tag;
+    }
+    // Paper end - Sanitize sent data
+
+    private static class ComponentHelper {
+
         public static final Codec<DataComponentMap> COMPONENTS_CODEC = DataComponentMap.CODEC.optionalFieldOf("components", DataComponentMap.EMPTY).codec();
 
-        private ComponentHelper() {
-        }
+        private ComponentHelper() {}
     }
 
     protected interface DataComponentInput {
+
         @Nullable
         <T> T get(DataComponentType<T> type);
 
