From c9ce8da2b09d82abdecc1d7122eea56e0cac9646 Mon Sep 17 00:00:00 2001
From: aromaa <me@joniaromaa.fi>
Date: Fri, 26 Apr 2024 02:25:17 +0300
Subject: [PATCH 1/3] Rework ChunkEvent

---
 SpongeAPI                                     |   2 +-
 .../level/chunk/storage/IOWorkerBridge.java   |   3 +-
 .../common/event/ShouldFire.java              |  10 +-
 .../chunk/storage/SpongeChunkEntities.java    | 233 ++++++++++++++++++
 .../chunk/storage/SpongeIOWorkerType.java     |  30 +++
 .../ObjectArrayMutableEntityBuffer.java       |  71 +-----
 .../level/chunk/LevelChunkMixin_API.java      |   3 +-
 .../core/server/level/ChunkHolderMixin.java   |  20 ++
 .../core/server/level/ChunkMapMixin.java      |  23 +-
 .../world/level/chunk/LevelChunkMixin.java    |   3 +-
 .../chunk/storage/EntityStorageMixin.java     | 121 +++++++++
 .../level/chunk/storage/IOWorkerMixin.java    |  30 ++-
 src/mixins/resources/mixins.sponge.core.json  |   1 +
 .../test/chunk/ChunkEventTest.java            | 213 +++++++++++++++-
 14 files changed, 657 insertions(+), 106 deletions(-)
 create mode 100644 src/main/java/org/spongepowered/common/world/level/chunk/storage/SpongeChunkEntities.java
 create mode 100644 src/main/java/org/spongepowered/common/world/level/chunk/storage/SpongeIOWorkerType.java
 create mode 100644 src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/storage/EntityStorageMixin.java

diff --git a/SpongeAPI b/SpongeAPI
index cb4ee935103..d48612a13cf 160000
--- a/SpongeAPI
+++ b/SpongeAPI
@@ -1 +1 @@
-Subproject commit cb4ee935103e4cd09e9bcb7a4e8cd649e0287fc9
+Subproject commit d48612a13cfc456b73166a8d6125d94f19f29589
diff --git a/src/main/java/org/spongepowered/common/bridge/world/level/chunk/storage/IOWorkerBridge.java b/src/main/java/org/spongepowered/common/bridge/world/level/chunk/storage/IOWorkerBridge.java
index e22178095b7..1429e65b6f1 100644
--- a/src/main/java/org/spongepowered/common/bridge/world/level/chunk/storage/IOWorkerBridge.java
+++ b/src/main/java/org/spongepowered/common/bridge/world/level/chunk/storage/IOWorkerBridge.java
@@ -26,8 +26,9 @@
 
 import net.minecraft.resources.ResourceKey;
 import net.minecraft.world.level.Level;
+import org.spongepowered.common.world.level.chunk.storage.SpongeIOWorkerType;
 
 public interface IOWorkerBridge {
 
-    void bridge$setDimension(ResourceKey<Level> dimension);
+    void bridge$setDimension(SpongeIOWorkerType type, ResourceKey<Level> dimension);
 }
diff --git a/src/main/java/org/spongepowered/common/event/ShouldFire.java b/src/main/java/org/spongepowered/common/event/ShouldFire.java
index ca825f56e1d..494d84a79ed 100644
--- a/src/main/java/org/spongepowered/common/event/ShouldFire.java
+++ b/src/main/java/org/spongepowered/common/event/ShouldFire.java
@@ -116,12 +116,16 @@ public final class ShouldFire {
 
     public static boolean KICK_PLAYER_EVENT = false;
 
-    public static boolean CHUNK_EVENT_LOAD = false;
-    public static boolean CHUNK_EVENT_SAVE_PRE = false;
-    public static boolean CHUNK_EVENT_SAVE_POST = false;
+    public static boolean CHUNK_EVENT_BLOCKS_LOAD = false;
+    public static boolean CHUNK_EVENT_BLOCKS_SAVE_PRE = false;
+    public static boolean CHUNK_EVENT_BLOCKS_SAVE_POST = false;
+    public static boolean CHUNK_EVENT_ENTITIES_LOAD = false;
+    public static boolean CHUNK_EVENT_ENTITIES_SAVE_PRE = false;
+    public static boolean CHUNK_EVENT_ENTITIES_SAVE_POST = false;
     public static boolean CHUNK_EVENT_GENERATED = false;
     public static boolean CHUNK_EVENT_UNLOAD_PRE = false;
     public static boolean CHUNK_EVENT_UNLOAD_POST = false;
+    public static boolean CHUNK_EVENT_LOAD = false;
 
 
     public static boolean CHANGE_DATA_HOLDER_EVENT_VALUE_CHANGE = false;
diff --git a/src/main/java/org/spongepowered/common/world/level/chunk/storage/SpongeChunkEntities.java b/src/main/java/org/spongepowered/common/world/level/chunk/storage/SpongeChunkEntities.java
new file mode 100644
index 00000000000..0531cbf12ad
--- /dev/null
+++ b/src/main/java/org/spongepowered/common/world/level/chunk/storage/SpongeChunkEntities.java
@@ -0,0 +1,233 @@
+/*
+ * This file is part of Sponge, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) SpongePowered <https://www.spongepowered.org>
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.spongepowered.common.world.level.chunk.storage;
+
+import com.google.common.collect.ImmutableList;
+import net.minecraft.util.Tuple;
+import net.minecraft.world.level.Level;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.api.data.persistence.DataContainer;
+import org.spongepowered.api.entity.Entity;
+import org.spongepowered.api.entity.EntityType;
+import org.spongepowered.api.entity.living.player.Player;
+import org.spongepowered.api.util.AABB;
+import org.spongepowered.api.world.chunk.EntityChunk;
+import org.spongepowered.api.world.volume.stream.StreamOptions;
+import org.spongepowered.api.world.volume.stream.VolumeStream;
+import org.spongepowered.common.bridge.world.level.LevelBridge;
+import org.spongepowered.common.util.VecHelper;
+import org.spongepowered.common.world.storage.SpongeChunkLayout;
+import org.spongepowered.common.world.volume.VolumeStreamUtils;
+import org.spongepowered.common.world.volume.buffer.entity.ObjectArrayMutableEntityBuffer;
+import org.spongepowered.math.vector.Vector3d;
+import org.spongepowered.math.vector.Vector3i;
+
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+public final class SpongeChunkEntities implements EntityChunk {
+
+    private final Level level;
+    private final Vector3i chunkPosition;
+    private final Stream<net.minecraft.world.entity.Entity> entities;
+
+    private @MonotonicNonNull SpongeChunkLayout chunkLayout;
+    private @MonotonicNonNull Vector3i blockMin;
+    private @MonotonicNonNull Vector3i blockMax;
+    private @MonotonicNonNull List<net.minecraft.world.entity.Entity> newEntities;
+
+    public SpongeChunkEntities(final Level chunkLayout, final Vector3i chunkPosition, final Stream<net.minecraft.world.entity.Entity> entities) {
+        this.level = chunkLayout;
+        this.chunkPosition = chunkPosition;
+        this.entities = entities;
+    }
+
+    @Override
+    public Vector3i min() {
+        if (this.blockMin == null) {
+            if (this.chunkLayout == null) {
+                this.chunkLayout = new SpongeChunkLayout(this.level.getMinBuildHeight(), this.level.getHeight());
+            }
+            this.blockMin = this.chunkLayout.forceToWorld(this.chunkPosition);
+        }
+        return this.blockMin;
+    }
+
+    @Override
+    public Vector3i max() {
+        if (this.blockMax == null) {
+            if (this.chunkLayout == null) {
+                this.chunkLayout = new SpongeChunkLayout(this.level.getMinBuildHeight(), this.level.getHeight());
+            }
+            this.blockMax = this.min().add(this.chunkLayout.chunkSize()).sub(1, 1, 1);
+        }
+        return this.blockMax;
+    }
+
+    @Override
+    public boolean contains(final int x, final int y, final int z) {
+        return VecHelper.inBounds(x, y, z, this.min(), this.max());
+    }
+
+    @Override
+    public boolean isAreaAvailable(final int x, final int y, final int z) {
+        return VecHelper.inBounds(x, y, z, this.min(), this.max());
+    }
+
+    @Override
+    public Collection<? extends Player> players() {
+        return this.entities.filter(Player.class::isInstance).map(Player.class::cast).toList();
+    }
+
+    @Override
+    public Optional<Entity> entity(final UUID uuid) {
+        return this.entities.filter(e -> e.getUUID().equals(uuid)).map(Entity.class::cast).findFirst();
+    }
+
+    @Override
+    public Collection<? extends Entity> entities() {
+        return this.entities.map(Entity.class::cast).toList();
+    }
+
+    @Override
+    public <T extends Entity> Collection<? extends T> entities(final Class<? extends T> entityClass, final AABB box, final @Nullable Predicate<? super T> predicate) {
+        final net.minecraft.world.phys.AABB mcAabb = VecHelper.toMinecraftAABB(box);
+        return this.entities
+                .filter(e -> entityClass.isInstance(e) && e.getBoundingBox().intersects(mcAabb))
+                .map(entityClass::cast)
+                .filter(e -> predicate.test(e))
+                .toList();
+    }
+
+    @Override
+    public Collection<? extends Entity> entities(final AABB box, final Predicate<? super Entity> filter) {
+        final net.minecraft.world.phys.AABB mcAabb = VecHelper.toMinecraftAABB(box);
+        return this.entities.map(Entity.class::cast)
+                .filter(e -> ((net.minecraft.world.entity.Entity) e).getBoundingBox().intersects(mcAabb) && filter.test(e))
+                .toList();
+    }
+
+    @Override
+    public VolumeStream<EntityChunk, Entity> entityStream(final Vector3i min, final Vector3i max, final StreamOptions options) {
+        VolumeStreamUtils.validateStreamArgs(
+                Objects.requireNonNull(min, "min"), Objects.requireNonNull(max, "max"),
+                Objects.requireNonNull(options, "options"));
+
+        final boolean shouldCarbonCopy = options.carbonCopy();
+        final Vector3i size = max.sub(min).add(1, 1 ,1);
+        final @MonotonicNonNull ObjectArrayMutableEntityBuffer backingVolume;
+        if (shouldCarbonCopy) {
+            backingVolume = new ObjectArrayMutableEntityBuffer(min, size);
+        } else {
+            backingVolume = null;
+        }
+
+        return VolumeStreamUtils.generateStream(options,
+                this,
+                this,
+                // Entity Accessor
+                (chunk) -> chunk.entities.filter(entity -> VecHelper.inBounds(entity.blockPosition(), min, max))
+                        .map(entity -> new AbstractMap.SimpleEntry<>(entity.blockPosition(), entity)),
+                // Entity Identity Function
+                VolumeStreamUtils.getOrCloneEntityWithVolume(shouldCarbonCopy, backingVolume, this.level),
+                (key, entity) -> entity.getUUID(),
+                // Filtered Position Entity Accessor
+                (entityUuid, chunk) -> {
+                    final net.minecraft.world.entity.@Nullable Entity entity = shouldCarbonCopy
+                            ? (net.minecraft.world.entity.Entity) backingVolume.entity(entityUuid).orElse(null)
+                            : (net.minecraft.world.entity.Entity) chunk.entity(entityUuid).orElse(null);
+                    if (entity == null) {
+                        return null;
+                    }
+                    return new Tuple<>(entity.blockPosition(), entity);
+                }
+        );
+    }
+
+    @Override
+    public <E extends Entity> E createEntity(final EntityType<E> type, final Vector3d position) throws IllegalArgumentException, IllegalStateException {
+        this.checkPositionInChunk(position);
+        return ((LevelBridge) this.level).bridge$createEntity(type, position, false);
+    }
+
+    @Override
+    public <E extends Entity> E createEntityNaturally(final EntityType<E> type, final Vector3d position) throws IllegalArgumentException, IllegalStateException {
+        this.checkPositionInChunk(position);
+        return ((LevelBridge) this.level).bridge$createEntity(type, position, true);
+    }
+
+    @Override
+    public Optional<Entity> createEntity(final DataContainer container) {
+        return Optional.ofNullable(((LevelBridge) this.level).bridge$createEntity(container, null,
+                position -> VecHelper.inBounds(position, this.min(), this.max())));
+    }
+
+    @Override
+    public Optional<Entity> createEntity(final DataContainer container, final Vector3d position) {
+        this.checkPositionInChunk(position);
+        return Optional.ofNullable(((LevelBridge) this.level).bridge$createEntity(container, position, null));
+    }
+
+    @Override
+    public boolean spawnEntity(final Entity entity) {
+        if (this.newEntities == null) {
+            this.newEntities = new ArrayList<>();
+        }
+        this.newEntities.add((net.minecraft.world.entity.Entity) entity);
+        return true;
+    }
+
+    @Override
+    public Collection<Entity> spawnEntities(final Iterable<? extends Entity> entities) {
+        final List<Entity> list = new ArrayList<>();
+        for (final Entity entity : entities) {
+            this.spawnEntity(entity);
+            list.add(entity);
+        }
+        return list;
+    }
+
+    private void checkPositionInChunk(final Vector3d position) {
+        if (!VecHelper.inBounds(position, this.min(), this.max())) {
+            throw new IllegalArgumentException("Supplied bounds are not within this chunk.");
+        }
+    }
+
+    public @Nullable List<net.minecraft.world.entity.Entity> buildIfChanged() {
+        if (this.newEntities == null) {
+            return null;
+        }
+
+        return Stream.concat(this.entities, this.newEntities.stream()).collect(ImmutableList.toImmutableList());
+    }
+}
diff --git a/src/main/java/org/spongepowered/common/world/level/chunk/storage/SpongeIOWorkerType.java b/src/main/java/org/spongepowered/common/world/level/chunk/storage/SpongeIOWorkerType.java
new file mode 100644
index 00000000000..66f252528cf
--- /dev/null
+++ b/src/main/java/org/spongepowered/common/world/level/chunk/storage/SpongeIOWorkerType.java
@@ -0,0 +1,30 @@
+/*
+ * This file is part of Sponge, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) SpongePowered <https://www.spongepowered.org>
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.spongepowered.common.world.level.chunk.storage;
+
+public enum SpongeIOWorkerType {
+    CHUNK,
+    ENTITY
+}
diff --git a/src/main/java/org/spongepowered/common/world/volume/buffer/entity/ObjectArrayMutableEntityBuffer.java b/src/main/java/org/spongepowered/common/world/volume/buffer/entity/ObjectArrayMutableEntityBuffer.java
index c99032a7a87..940b5d0af3d 100644
--- a/src/main/java/org/spongepowered/common/world/volume/buffer/entity/ObjectArrayMutableEntityBuffer.java
+++ b/src/main/java/org/spongepowered/common/world/volume/buffer/entity/ObjectArrayMutableEntityBuffer.java
@@ -26,23 +26,18 @@
 
 import com.google.common.collect.ImmutableList;
 import org.checkerframework.checker.nullness.qual.Nullable;
-import org.spongepowered.api.block.BlockState;
-import org.spongepowered.api.block.BlockType;
 import org.spongepowered.api.data.persistence.DataContainer;
 import org.spongepowered.api.entity.Entity;
 import org.spongepowered.api.entity.EntityType;
 import org.spongepowered.api.entity.living.player.Player;
-import org.spongepowered.api.fluid.FluidState;
 import org.spongepowered.api.util.AABB;
-import org.spongepowered.api.world.schematic.Palette;
 import org.spongepowered.api.world.volume.entity.EntityVolume;
 import org.spongepowered.api.world.volume.stream.StreamOptions;
 import org.spongepowered.api.world.volume.stream.VolumeElement;
 import org.spongepowered.api.world.volume.stream.VolumeStream;
 import org.spongepowered.common.world.volume.SpongeVolumeStream;
 import org.spongepowered.common.world.volume.VolumeStreamUtils;
-import org.spongepowered.common.world.volume.buffer.block.AbstractBlockBuffer;
-import org.spongepowered.common.world.volume.buffer.block.ArrayMutableBlockBuffer;
+import org.spongepowered.common.world.volume.buffer.AbstractVolumeBuffer;
 import org.spongepowered.math.vector.Vector3d;
 import org.spongepowered.math.vector.Vector3i;
 
@@ -52,81 +47,19 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.UUID;
-import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
-import java.util.stream.IntStream;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
-public class ObjectArrayMutableEntityBuffer extends AbstractBlockBuffer implements EntityVolume.Mutable {
-    // This is our backing block buffer
-    private final ArrayMutableBlockBuffer blockBuffer;
+public class ObjectArrayMutableEntityBuffer extends AbstractVolumeBuffer implements EntityVolume.Mutable {
     private final List<Entity> entities;
 
     public ObjectArrayMutableEntityBuffer(final Vector3i start, final Vector3i size) {
         super(start, size);
-        this.blockBuffer = new ArrayMutableBlockBuffer(start, size);
         this.entities = new ArrayList<>();
     }
 
-    public ObjectArrayMutableEntityBuffer(final Vector3i start, final Vector3i size,
-        final ArrayMutableBlockBuffer blockBuffer
-    ) {
-        super(start, size);
-        this.blockBuffer = blockBuffer;
-        this.entities = new ArrayList<>();
-    }
-
-    @Override
-    public Palette<BlockState, BlockType> getPalette() {
-        return this.blockBuffer.getPalette();
-    }
-
-    @Override
-    public BlockState block(final int x, final int y, final int z) {
-        return this.blockBuffer.block(x, y, z);
-    }
-
-    @Override
-    public FluidState fluid(final int x, final int y, final int z) {
-        return this.blockBuffer.fluid(x, y, z);
-    }
-
-    @Override
-    public int highestYAt(final int x, final int z) {
-        return this.blockBuffer.highestYAt(x, z);
-    }
-
-    @Override
-    public VolumeStream<EntityVolume.Mutable, BlockState> blockStateStream(final Vector3i min, final Vector3i max,
-        final StreamOptions options) {
-        VolumeStreamUtils.validateStreamArgs(min, max, this.min(), this.max(), options);
-        final ArrayMutableBlockBuffer buffer;
-        if (options.carbonCopy()) {
-            buffer = this.blockBuffer.copy();
-        } else {
-            buffer = this.blockBuffer;
-        }
-        final Stream<VolumeElement<EntityVolume.Mutable, BlockState>> stateStream = IntStream.rangeClosed(min.x(), max.x())
-            .mapToObj(x -> IntStream.rangeClosed(min.z(), max.z())
-                .mapToObj(z -> IntStream.rangeClosed(min.y(), max.y())
-                    .mapToObj(y -> VolumeElement.of((EntityVolume.Mutable) this, () -> buffer.block(x, y, z), new Vector3d(x, y, z)))
-                ).flatMap(Function.identity())
-            ).flatMap(Function.identity());
-        return new SpongeVolumeStream<>(stateStream, () -> this);
-    }
-
-    @Override
-    public boolean setBlock(final int x, final int y, final int z, final BlockState block) {
-        return this.blockBuffer.setBlock(x, y, z, block);
-    }
-
-    @Override
-    public boolean removeBlock(final int x, final int y, final int z) {
-        return this.blockBuffer.removeBlock(x, y, z);
-    }
-
     @Override
     public <E extends Entity> E createEntity(final EntityType<E> type, final Vector3d position) throws IllegalArgumentException, IllegalStateException {
         throw new UnsupportedOperationException("Cannot create entities without a world, can only add to a volume");
diff --git a/src/mixins/java/org/spongepowered/common/mixin/api/minecraft/world/level/chunk/LevelChunkMixin_API.java b/src/mixins/java/org/spongepowered/common/mixin/api/minecraft/world/level/chunk/LevelChunkMixin_API.java
index 52d1fb1335a..a16b1cc68ff 100644
--- a/src/mixins/java/org/spongepowered/common/mixin/api/minecraft/world/level/chunk/LevelChunkMixin_API.java
+++ b/src/mixins/java/org/spongepowered/common/mixin/api/minecraft/world/level/chunk/LevelChunkMixin_API.java
@@ -29,6 +29,7 @@
 import net.minecraft.core.registries.Registries;
 import net.minecraft.util.Tuple;
 import net.minecraft.world.DifficultyInstance;
+import net.minecraft.world.level.BlockGetter;
 import net.minecraft.world.level.ChunkPos;
 import net.minecraft.world.level.Level;
 import net.minecraft.world.level.LevelHeightAccessor;
@@ -248,7 +249,7 @@ public VolumeStream<WorldChunk, BlockState> blockStateStream(
             (blockPos, world) -> {
                 final net.minecraft.world.level.block.state.BlockState tileEntity = shouldCarbonCopy
                     ? backingVolume.getBlock(blockPos)
-                    : ((LevelReader) world).getBlockState(blockPos);
+                    : ((BlockGetter) world).getBlockState(blockPos);
                 return new Tuple<>(blockPos, tileEntity);
             }
         );
diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkHolderMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkHolderMixin.java
index ca887304b96..31ef0eac30e 100644
--- a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkHolderMixin.java
+++ b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkHolderMixin.java
@@ -27,6 +27,7 @@
 import com.mojang.datafixers.util.Either;
 import net.minecraft.server.level.ChunkHolder;
 import net.minecraft.server.level.ChunkMap;
+import net.minecraft.server.level.FullChunkStatus;
 import net.minecraft.world.level.chunk.ChunkAccess;
 import net.minecraft.world.level.chunk.ChunkStatus;
 import net.minecraft.world.level.chunk.ImposterProtoChunk;
@@ -34,7 +35,9 @@
 import org.spongepowered.api.ResourceKey;
 import org.spongepowered.api.event.SpongeEventFactory;
 import org.spongepowered.api.event.world.chunk.ChunkEvent;
+import org.spongepowered.api.world.chunk.WorldChunk;
 import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
 import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@@ -51,6 +54,11 @@
 
 @Mixin(ChunkHolder.class)
 abstract class ChunkHolderMixin {
+
+    // @formatter:off
+    @Shadow public abstract CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> shadow$getEntityTickingChunkFuture();
+    // @formatter:on
+
     @Inject(
         method = "replaceProtoChunk(Lnet/minecraft/world/level/chunk/ImposterProtoChunk;)V",
         at = @At("TAIL")
@@ -74,4 +82,16 @@ abstract class ChunkHolderMixin {
             cir.setReturnValue(ChunkHolder.UNLOADED_CHUNK_FUTURE);
         }
     }
+
+    @Inject(method = "lambda$scheduleFullChunkPromotion$7", at = @At("TAIL"))
+    private void impl$onScheduleFullChunkPromotion(final ChunkMap $$0x, final FullChunkStatus $$1x, final CallbackInfo ci) {
+        if ($$1x == FullChunkStatus.ENTITY_TICKING && ShouldFire.CHUNK_EVENT_LOAD) {
+            this.shadow$getEntityTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).ifLeft(chunk -> {
+                final Vector3i chunkPos = VecHelper.toVector3i(chunk.getPos());
+                final ChunkEvent.Load event = SpongeEventFactory.createChunkEventLoad(PhaseTracker.getInstance().currentCause(),
+                        (WorldChunk) chunk, chunkPos, (ResourceKey) (Object) chunk.getLevel().dimension().location());
+                SpongeCommon.post(event);
+            });
+        }
+    }
 }
diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkMapMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkMapMixin.java
index 32201337e48..5dc931f3f25 100644
--- a/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkMapMixin.java
+++ b/src/mixins/java/org/spongepowered/common/mixin/core/server/level/ChunkMapMixin.java
@@ -41,6 +41,7 @@
 import org.spongepowered.api.event.world.chunk.ChunkEvent;
 import org.spongepowered.api.util.Direction;
 import org.spongepowered.api.world.SerializationBehavior;
+import org.spongepowered.api.world.chunk.BlockChunk;
 import org.spongepowered.api.world.chunk.WorldChunk;
 import org.spongepowered.asm.mixin.Final;
 import org.spongepowered.asm.mixin.Mixin;
@@ -64,6 +65,8 @@
 import org.spongepowered.common.event.tracking.PhaseTracker;
 import org.spongepowered.common.util.Constants;
 import org.spongepowered.common.util.DirectionUtil;
+import org.spongepowered.common.util.VecHelper;
+import org.spongepowered.common.world.level.chunk.storage.SpongeIOWorkerType;
 import org.spongepowered.math.vector.Vector3i;
 
 import java.util.concurrent.CompletableFuture;
@@ -84,7 +87,7 @@ public abstract class ChunkMapMixin implements ChunkMapBridge {
 
     @Inject(method = "<init>", at = @At("RETURN"))
     private void impl$setIOWorkerDimension(final CallbackInfo ci) {
-        ((IOWorkerBridge) ((ChunkStorageAccessor) this).accessor$worker()).bridge$setDimension(this.level.dimension());
+        ((IOWorkerBridge) ((ChunkStorageAccessor) this).accessor$worker()).bridge$setDimension(SpongeIOWorkerType.CHUNK, this.level.dimension());
     }
 
     @Redirect(method = "save",
@@ -127,7 +130,7 @@ public abstract class ChunkMapMixin implements ChunkMapBridge {
             )
     )
     private void impl$onSetUnloaded(final ServerLevel level, final LevelChunk chunk) {
-        final Vector3i chunkPos = new Vector3i(chunk.getPos().x, 0, chunk.getPos().z);
+        final Vector3i chunkPos = VecHelper.toVector3i(chunk.getPos());
 
         if (ShouldFire.CHUNK_EVENT_UNLOAD_PRE) {
             final ChunkEvent.Unload event = SpongeEventFactory.createChunkEventUnloadPre(PhaseTracker.getInstance().currentCause(),
@@ -157,10 +160,10 @@ public abstract class ChunkMapMixin implements ChunkMapBridge {
     @Inject(method = "save", at = @At(value = "HEAD"), cancellable = true)
     private void impl$onSave(final ChunkAccess var1, final CallbackInfoReturnable<Boolean> cir) {
         if (var1 instanceof WorldChunk) {
-            if (ShouldFire.CHUNK_EVENT_SAVE_PRE) {
-                final Vector3i chunkPos = new Vector3i(var1.getPos().x, 0, var1.getPos().z);
-                final ChunkEvent.Save.Pre postSave = SpongeEventFactory.createChunkEventSavePre(PhaseTracker.getInstance().currentCause(),
-                    ((WorldChunk) var1), chunkPos, (ResourceKey) (Object) this.level.dimension().location());
+            if (ShouldFire.CHUNK_EVENT_BLOCKS_SAVE_PRE) {
+                final Vector3i chunkPos = VecHelper.toVector3i(var1.getPos());
+                final ChunkEvent.Blocks.Save.Pre postSave = SpongeEventFactory.createChunkEventBlocksSavePre(PhaseTracker.getInstance().currentCause(),
+                    ((BlockChunk) var1), chunkPos, (ResourceKey) (Object) this.level.dimension().location());
                 SpongeCommon.post(postSave);
                 if (postSave.isCancelled()) {
                     cir.setReturnValue(false);
@@ -178,10 +181,10 @@ public abstract class ChunkMapMixin implements ChunkMapBridge {
     )
     private void impl$onLoad(final LevelChunk levelChunk, final boolean loaded) {
         levelChunk.setLoaded(true);
-        final Vector3i chunkPos = new Vector3i(levelChunk.getPos().x, 0, levelChunk.getPos().z);
-        if (ShouldFire.CHUNK_EVENT_LOAD) {
-            final ChunkEvent.Load loadEvent = SpongeEventFactory.createChunkEventLoad(PhaseTracker.getInstance().currentCause(),
-                ((WorldChunk) levelChunk), chunkPos, (ResourceKey) (Object) this.level.dimension().location());
+        final Vector3i chunkPos = VecHelper.toVector3i(levelChunk.getPos());
+        if (ShouldFire.CHUNK_EVENT_BLOCKS_LOAD) {
+            final ChunkEvent.Blocks.Load loadEvent = SpongeEventFactory.createChunkEventBlocksLoad(PhaseTracker.getInstance().currentCause(),
+                ((BlockChunk) levelChunk), chunkPos, (ResourceKey) (Object) this.level.dimension().location());
             SpongeCommon.post(loadEvent);
         }
 
diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/LevelChunkMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/LevelChunkMixin.java
index f62daa6549a..07a4474a29c 100644
--- a/src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/LevelChunkMixin.java
+++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/LevelChunkMixin.java
@@ -47,6 +47,7 @@
 import org.spongepowered.api.data.Key;
 import org.spongepowered.api.data.value.Value;
 import org.spongepowered.api.util.Direction;
+import org.spongepowered.api.world.chunk.BlockChunk;
 import org.spongepowered.asm.mixin.Final;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.Shadow;
@@ -82,7 +83,7 @@
 import java.util.function.Function;
 
 @Mixin(net.minecraft.world.level.chunk.LevelChunk.class)
-public abstract class LevelChunkMixin extends ChunkAccess implements LevelChunkBridge, CacheKeyBridge, SpongeMutableDataHolder, SpongeDataHolderBridge, DataCompoundHolder {
+public abstract class LevelChunkMixin extends ChunkAccess implements LevelChunkBridge, CacheKeyBridge, SpongeMutableDataHolder, SpongeDataHolderBridge, DataCompoundHolder, BlockChunk {
 
     // @formatter:off
     @Shadow @Final private Level level;
diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/storage/EntityStorageMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/storage/EntityStorageMixin.java
new file mode 100644
index 00000000000..3e0a944afb6
--- /dev/null
+++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/storage/EntityStorageMixin.java
@@ -0,0 +1,121 @@
+/*
+ * This file is part of Sponge, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) SpongePowered <https://www.spongepowered.org>
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.spongepowered.common.mixin.core.world.level.chunk.storage;
+
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.storage.EntityStorage;
+import net.minecraft.world.level.chunk.storage.IOWorker;
+import net.minecraft.world.level.entity.ChunkEntities;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.api.ResourceKey;
+import org.spongepowered.api.event.SpongeEventFactory;
+import org.spongepowered.api.event.world.chunk.ChunkEvent;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+import org.spongepowered.common.SpongeCommon;
+import org.spongepowered.common.bridge.world.level.chunk.storage.IOWorkerBridge;
+import org.spongepowered.common.event.ShouldFire;
+import org.spongepowered.common.event.tracking.PhaseTracker;
+import org.spongepowered.common.util.VecHelper;
+import org.spongepowered.common.world.level.chunk.storage.SpongeChunkEntities;
+import org.spongepowered.common.world.level.chunk.storage.SpongeIOWorkerType;
+import org.spongepowered.math.vector.Vector3i;
+
+import java.util.List;
+import java.util.Optional;
+
+@Mixin(EntityStorage.class)
+public abstract class EntityStorageMixin {
+
+    // @formatter:off
+    @Shadow @Final private IOWorker worker;
+    @Shadow @Final private ServerLevel level;
+    // @formatter:on
+
+    @Inject(method = "<init>", at = @At("RETURN"))
+    private void impl$setIOWorkerDimension(final CallbackInfo ci) {
+        ((IOWorkerBridge) this.worker).bridge$setDimension(SpongeIOWorkerType.ENTITY, this.level.dimension());
+    }
+
+    @Inject(method = "lambda$loadEntities$0", at = @At("RETURN"), cancellable = true)
+    private void impl$onLoadEntities(final ChunkPos $$0x, final Optional<CompoundTag> $$1, final CallbackInfoReturnable<ChunkEntities<Entity>> cir) {
+        if (!ShouldFire.CHUNK_EVENT_ENTITIES_LOAD) {
+            return;
+        }
+
+        final Vector3i chunkPos = VecHelper.toVector3i($$0x);
+        final SpongeChunkEntities entities = new SpongeChunkEntities(this.level, chunkPos, cir.getReturnValue().getEntities());
+        final ChunkEvent.Entities.Load loadEvent = SpongeEventFactory.createChunkEventEntitiesLoad(PhaseTracker.getInstance().currentCause(),
+                chunkPos, entities, (ResourceKey) (Object) this.level.dimension().location());
+
+        SpongeCommon.post(loadEvent);
+
+        final @Nullable List<Entity> newList = entities.buildIfChanged();
+        if (newList != null) {
+            cir.setReturnValue(new ChunkEntities<>($$0x, newList));
+        }
+    }
+
+    @ModifyVariable(method = "storeEntities", at = @At("HEAD"), argsOnly = true)
+    private ChunkEntities<Entity> impl$onStoreEntities(final ChunkEntities<Entity> $$0) {
+        if (!ShouldFire.CHUNK_EVENT_ENTITIES_SAVE_PRE) {
+            return $$0;
+        }
+
+        final Vector3i chunkPos = VecHelper.toVector3i($$0.getPos());
+
+        final SpongeChunkEntities entities = new SpongeChunkEntities(this.level, chunkPos, $$0.getEntities());
+        final ChunkEvent.Entities.Save.Pre saveEvent = SpongeEventFactory.createChunkEventEntitiesSavePre(PhaseTracker.getInstance().currentCause(),
+                chunkPos, entities, (ResourceKey) (Object) this.level.dimension().location());
+
+        if (SpongeCommon.post(saveEvent)) {
+            return null;
+        }
+
+        final @Nullable List<Entity> newList = entities.buildIfChanged();
+        if (newList != null) {
+            return new ChunkEntities<>($$0.getPos(), newList);
+        }
+
+        return $$0;
+    }
+
+    @Inject(method = "storeEntities",
+            at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/entity/ChunkEntities;getPos()Lnet/minecraft/world/level/ChunkPos;"), cancellable = true)
+    private void impl$onCancelledEntitySave(final ChunkEntities<Entity> $$0, final CallbackInfo ci) {
+        if ($$0 == null) {
+            ci.cancel();
+        }
+    }
+}
diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/storage/IOWorkerMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/storage/IOWorkerMixin.java
index f053b41f2c8..e94a7e7fddf 100644
--- a/src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/storage/IOWorkerMixin.java
+++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/storage/IOWorkerMixin.java
@@ -55,6 +55,7 @@
 import org.spongepowered.common.event.ShouldFire;
 import org.spongepowered.common.event.tracking.PhaseTracker;
 import org.spongepowered.common.world.level.chunk.SpongeUnloadedChunkException;
+import org.spongepowered.common.world.level.chunk.storage.SpongeIOWorkerType;
 import org.spongepowered.math.vector.Vector3i;
 
 import java.util.BitSet;
@@ -78,24 +79,33 @@ public abstract class IOWorkerMixin implements IOWorkerBridge {
     @Shadow protected abstract void shadow$tellStorePending();
     // @formatter:on
 
-    @MonotonicNonNull private ResourceKey<Level> impl$dimension; //We only set this for chunk related IO workers
+    // We only set these for chunk and entity related IO workers
+    @MonotonicNonNull private SpongeIOWorkerType impl$type;
+    @MonotonicNonNull private ResourceKey<Level> impl$dimension;
 
     @Override
-    public void bridge$setDimension(ResourceKey<Level> dimension) {
+    public void bridge$setDimension(final SpongeIOWorkerType type, final ResourceKey<Level> dimension) {
+        this.impl$type = type;
         this.impl$dimension = dimension;
     }
 
     @Inject(method = "runStore", at = @At(value = "INVOKE", shift = At.Shift.AFTER, target = "Ljava/util/concurrent/CompletableFuture;complete(Ljava/lang/Object;)Z"))
     private void impl$onSaved(final ChunkPos param0, final @Coerce Object param1, final CallbackInfo ci) {
-        if (this.impl$dimension == null) {
-            return;
+        if (this.impl$type == SpongeIOWorkerType.CHUNK) {
+            if (ShouldFire.CHUNK_EVENT_BLOCKS_SAVE_POST) {
+                final Vector3i chunkPos = new Vector3i(param0.x, 0, param0.z);
+                final ChunkEvent.Blocks.Save.Post postSave = SpongeEventFactory.createChunkEventBlocksSavePost(PhaseTracker.getInstance().currentCause(), chunkPos,
+                        (org.spongepowered.api.ResourceKey) (Object) this.impl$dimension.location());
+                SpongeCommon.post(postSave);
+            }
         }
-
-        if (ShouldFire.CHUNK_EVENT_SAVE_POST) {
-            final Vector3i chunkPos = new Vector3i(param0.x, 0, param0.z);
-            final ChunkEvent.Save.Post postSave = SpongeEventFactory.createChunkEventSavePost(PhaseTracker.getInstance().currentCause(), chunkPos,
-                    (org.spongepowered.api.ResourceKey) (Object) this.impl$dimension.location());
-            SpongeCommon.post(postSave);
+        else if (this.impl$type == SpongeIOWorkerType.ENTITY) {
+            if (ShouldFire.CHUNK_EVENT_ENTITIES_SAVE_POST) {
+                final Vector3i chunkPos = new Vector3i(param0.x, 0, param0.z);
+                final ChunkEvent.Entities.Save.Post postSave = SpongeEventFactory.createChunkEventEntitiesSavePost(PhaseTracker.getInstance().currentCause(), chunkPos,
+                        (org.spongepowered.api.ResourceKey) (Object) this.impl$dimension.location());
+                SpongeCommon.post(postSave);
+            }
         }
     }
 
diff --git a/src/mixins/resources/mixins.sponge.core.json b/src/mixins/resources/mixins.sponge.core.json
index b0947d8b34b..8f2336ef4aa 100644
--- a/src/mixins/resources/mixins.sponge.core.json
+++ b/src/mixins/resources/mixins.sponge.core.json
@@ -237,6 +237,7 @@
         "world.level.chunk.ChunkSerializerMixin",
         "world.level.chunk.ChunkStatusMixin",
         "world.level.chunk.LevelChunkMixin",
+        "world.level.chunk.storage.EntityStorageMixin",
         "world.level.chunk.storage.IOWorkerMixin",
         "world.level.dimension.DimensionTypeMixin",
         "world.level.dimension.LevelStemMixin",
diff --git a/testplugins/src/main/java/org/spongepowered/test/chunk/ChunkEventTest.java b/testplugins/src/main/java/org/spongepowered/test/chunk/ChunkEventTest.java
index 347e8a8483a..3e102e5f47f 100644
--- a/testplugins/src/main/java/org/spongepowered/test/chunk/ChunkEventTest.java
+++ b/testplugins/src/main/java/org/spongepowered/test/chunk/ChunkEventTest.java
@@ -25,16 +25,32 @@
 package org.spongepowered.test.chunk;
 
 import com.google.inject.Inject;
+import io.leangen.geantyref.TypeToken;
+import net.kyori.adventure.identity.Identity;
 import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.checkerframework.checker.nullness.qual.NonNull;
 import org.spongepowered.api.Game;
 import org.spongepowered.api.Sponge;
+import org.spongepowered.api.block.BlockType;
+import org.spongepowered.api.command.Command;
+import org.spongepowered.api.command.CommandResult;
 import org.spongepowered.api.command.parameter.CommandContext;
+import org.spongepowered.api.command.parameter.Parameter;
+import org.spongepowered.api.data.Keys;
+import org.spongepowered.api.entity.Entity;
+import org.spongepowered.api.entity.EntityType;
 import org.spongepowered.api.event.Listener;
+import org.spongepowered.api.event.lifecycle.RegisterCommandEvent;
 import org.spongepowered.api.event.world.chunk.ChunkEvent;
+import org.spongepowered.api.registry.RegistryTypes;
 import org.spongepowered.plugin.PluginContainer;
 import org.spongepowered.plugin.builtin.jvm.Plugin;
 import org.spongepowered.test.LoadableModule;
 
+import java.util.HashSet;
+import java.util.Set;
+
 @Plugin("chunkeventtest")
 public final class ChunkEventTest implements LoadableModule {
 
@@ -42,6 +58,14 @@ public final class ChunkEventTest implements LoadableModule {
 
     private static final boolean LOG_CHUNK_EVENTS = Boolean.getBoolean("sponge.logChunkEvents");
 
+    private boolean logEvents = ChunkEventTest.LOG_CHUNK_EVENTS;
+    private boolean cancelBlockSave = false;
+    private boolean cancelEntitySave = false;
+    private final Set<EntityType<?>> filterEntitySave = new HashSet<>();
+    private final Set<EntityType<?>> addEntityLoad = new HashSet<>();
+    private final Set<BlockType> filterBlockSave = new HashSet<>();
+    private final Set<BlockType> addBlockLoad = new HashSet<>();
+
     @Inject
     public ChunkEventTest(final Game game, final PluginContainer plugin) {
         this.plugin = plugin;
@@ -50,38 +74,207 @@ public ChunkEventTest(final Game game, final PluginContainer plugin) {
         }
     }
 
+    @Listener
+    private void registerCommands(final RegisterCommandEvent<Command.Parameterized> event) {
+        final Parameter.Value<EntityType<@NonNull ?>> entityTypeParam =
+                Parameter.registryElement(new TypeToken<>() {}, RegistryTypes.ENTITY_TYPE, "minecraft").key("entityType").build();
+
+        final Parameter.Value<BlockType> blockTypeParam =
+                Parameter.registryElement(TypeToken.get(BlockType.class), RegistryTypes.BLOCK_TYPE, "minecraft").key("blockType").build();
+
+        event.register(this.plugin, Command.builder()
+                .executor(context -> {
+                    this.logEvents = !this.logEvents;
+                    final Component newState = Component.text(
+                            this.logEvents ? "ON" : "OFF", this.logEvents ? NamedTextColor.GREEN : NamedTextColor.RED);
+                    context.sendMessage(Identity.nil(), Component.text("Turning Chunk Log: ").append(newState));
+                    return CommandResult.success();
+                })
+                .build(), "logChunkEvents"
+        );
+        event.register(this.plugin, Command.builder()
+                .executor(context -> {
+                    this.cancelBlockSave = !this.cancelBlockSave;
+                    final Component newState = Component.text(
+                            this.cancelBlockSave ? "OFF" : "ON", this.cancelBlockSave ? NamedTextColor.RED : NamedTextColor.GREEN);
+                    context.sendMessage(Identity.nil(), Component.text("Turning Block Save: ").append(newState));
+                    return CommandResult.success();
+                })
+                .build(), "toggleChunkBlockSave"
+        );
+        event.register(this.plugin, Command.builder()
+                .executor(context -> {
+                    this.cancelEntitySave = !this.cancelEntitySave;
+                    final Component newState = Component.text(
+                            this.cancelEntitySave ? "OFF" : "ON", this.cancelEntitySave ? NamedTextColor.RED : NamedTextColor.GREEN);
+                    context.sendMessage(Identity.nil(), Component.text("Turning Entity Save: ").append(newState));
+                    return CommandResult.success();
+                })
+                .build(), "toggleChunkEntitySave"
+        );
+        event.register(this.plugin, Command.builder()
+                .addParameter(entityTypeParam)
+                .executor(context -> {
+                    final EntityType<?> entityType = context.requireOne(entityTypeParam);
+                    if (!this.filterEntitySave.contains(entityType)) {
+                        this.filterEntitySave.add(entityType);
+                        context.sendMessage(Identity.nil(), Component.text("Filtering entity: " + entityType.key(RegistryTypes.ENTITY_TYPE), NamedTextColor.GREEN));
+                    } else {
+                        this.filterEntitySave.remove(entityType);
+                        context.sendMessage(Identity.nil(), Component.text("Removed entity from filter: " + entityType.key(RegistryTypes.ENTITY_TYPE), NamedTextColor.RED));
+                    }
+                    return CommandResult.success();
+                })
+                .build(), "chunkSaveEntityFilter"
+        );
+        event.register(this.plugin, Command.builder()
+                .addParameter(entityTypeParam)
+                .executor(context -> {
+                    final EntityType<?> entityType = context.requireOne(entityTypeParam);
+                    if (!this.addEntityLoad.contains(entityType)) {
+                        this.addEntityLoad.add(entityType);
+                        context.sendMessage(Identity.nil(), Component.text("Adding entity: " + entityType.key(RegistryTypes.ENTITY_TYPE), NamedTextColor.GREEN));
+                    } else {
+                        this.addEntityLoad.remove(entityType);
+                        context.sendMessage(Identity.nil(), Component.text("No longer adding entity: " + entityType.key(RegistryTypes.ENTITY_TYPE), NamedTextColor.RED));
+                    }
+                    return CommandResult.success();
+                })
+                .build(), "chunkLoadExtraEntity"
+        );
+        event.register(this.plugin, Command.builder()
+                .addParameter(blockTypeParam)
+                .executor(context -> {
+                    final BlockType blockType = context.requireOne(blockTypeParam);
+                    if (!this.filterBlockSave.contains(blockType)) {
+                        this.filterBlockSave.add(blockType);
+                        context.sendMessage(Identity.nil(), Component.text("Filtering block: " + blockType.key(RegistryTypes.BLOCK_TYPE), NamedTextColor.GREEN));
+                    } else {
+                        this.filterBlockSave.remove(blockType);
+                        context.sendMessage(Identity.nil(), Component.text("Removed block from filter: " + blockType.key(RegistryTypes.BLOCK_TYPE), NamedTextColor.RED));
+                    }
+                    return CommandResult.success();
+                })
+                .build(), "chunkSaveBlockFilter"
+        );
+        event.register(this.plugin, Command.builder()
+                .addParameter(blockTypeParam)
+                .executor(context -> {
+                    final BlockType blockType = context.requireOne(blockTypeParam);
+                    if (!this.addBlockLoad.contains(blockType)) {
+                        this.addBlockLoad.add(blockType);
+                        context.sendMessage(Identity.nil(), Component.text("Adding block: " + blockType.key(RegistryTypes.BLOCK_TYPE), NamedTextColor.GREEN));
+                    } else {
+                        this.addBlockLoad.remove(blockType);
+                        context.sendMessage(Identity.nil(), Component.text("No longer adding block: " + blockType.key(RegistryTypes.BLOCK_TYPE), NamedTextColor.RED));
+                    }
+                    return CommandResult.success();
+                })
+                .build(), "chunkLoadExtraBlock"
+        );
+    }
+
     @Override
     public void enable(final CommandContext ctx) {
         Sponge.eventManager().registerListeners(this.plugin, new ChunkListener());
     }
 
 
-    static class ChunkListener {
+    class ChunkListener {
 
         @Listener
         public void onChunkGenerated(final ChunkEvent.Generated event) {
-            Sponge.game().systemSubject().sendMessage(Component.text("Generated Chunk " + event.chunkPosition() + " in " + event.worldKey().asString()));
+            if (ChunkEventTest.this.logEvents) {
+                Sponge.game().systemSubject().sendMessage(Component.text("Generated Chunk " + event.chunkPosition() + " in " + event.worldKey().asString()));
+            }
         }
 
         @Listener
         public void onChunkLoad(final ChunkEvent.Load event) {
-            Sponge.game().systemSubject().sendMessage(Component.text("Load Chunk " + event.chunk().chunkPosition() + " in " + event.worldKey().asString()));
+            if (ChunkEventTest.this.logEvents) {
+                Sponge.game().systemSubject().sendMessage(Component.text("Load Chunk " + event.chunkPosition() + " in " + event.worldKey().asString()));
+            }
         }
 
         @Listener
-        public void onChunkSave(final ChunkEvent.Save.Pre event) {
-            event.setCancelled(true);
-            Sponge.game().systemSubject().sendMessage(Component.text("Pre Save Chunk " + event.chunkPosition() + " in " + event.worldKey().asString()));
+        public void onChunkUnload(final ChunkEvent.Unload event) {
+            if (ChunkEventTest.this.logEvents) {
+                Sponge.game().systemSubject().sendMessage(Component.text("Unload Chunk " + event.chunkPosition() + " in " + event.worldKey().asString()));
+            }
         }
 
         @Listener
-        public void onChunkSave(final ChunkEvent.Save.Post event) {
-            Sponge.game().systemSubject().sendMessage(Component.text("Post Save Chunk " + event.chunkPosition() + " in " + event.worldKey().asString()));
+        public void onChunkBlocksLoad(final ChunkEvent.Blocks.Load event) {
+            /*ChunkEventTest.this.addBlockLoad.forEach(b ->
+                    event.blockVolume().blockStateStream(event.blockVolume().min(), event.blockVolume().max(), StreamOptions.lazily())
+                            .filter(e -> ((BlockState) e.type()).type().equals(b))
+                            .transform(e -> VolumeElement.of(e.volume(), BlockTypes.PINK_WOOL.get().defaultState(), e.position()))
+                            .apply(VolumeCollectors.of(event.blockVolume(), VolumePositionTranslators.identity(), VolumeApplicators.applyBlocks())));*/
+            if (ChunkEventTest.this.logEvents) {
+                Sponge.game().systemSubject().sendMessage(Component.text("Load Chunk Blocks " + event.chunkPosition() + " in " + event.worldKey().asString()));
+            }
         }
 
         @Listener
-        public void onChunkUnload(final ChunkEvent.Unload event) {
-            Sponge.game().systemSubject().sendMessage(Component.text("Unload Chunk " + event.chunkPosition() + " in " + event.worldKey().asString()));
+        public void onChunkBlocksSavePre(final ChunkEvent.Blocks.Save.Pre event) {
+            if (ChunkEventTest.this.cancelBlockSave) {
+                event.setCancelled(true);
+                return;
+            }
+            /*ChunkEventTest.this.filterBlockSave.forEach(b ->
+                    event.blockVolume().blockStateStream(event.blockVolume().min(), event.blockVolume().max(), StreamOptions.lazily())
+                            .filter(e -> ((BlockState) e.type()).type().equals(b))
+                            .transform(e -> VolumeElement.of(e.volume(), BlockTypes.AIR.get().defaultState(), e.position()))
+                            .apply(VolumeCollectors.of(event.blockVolume(), VolumePositionTranslators.identity(), VolumeApplicators.applyBlocks())));*/
+            if (ChunkEventTest.this.logEvents) {
+                Sponge.game().systemSubject().sendMessage(Component.text("Pre Save Chunk Blocks " + event.chunkPosition() + " in " + event.worldKey().asString()));
+            }
+        }
+
+        @Listener
+        public void onChunkBlocksSavePost(final ChunkEvent.Blocks.Save.Post event) {
+            if (ChunkEventTest.this.logEvents) {
+                Sponge.game().systemSubject().sendMessage(Component.text("Post Save Chunk Blocks " + event.chunkPosition() + " in " + event.worldKey().asString()));
+            }
+        }
+
+        @Listener
+        public void onChunkEntitiesLoad(final ChunkEvent.Entities.Load event) {
+            ChunkEventTest.this.addEntityLoad.forEach(e -> {
+                final Entity entity = event.entityVolume().createEntity(e, event.entityVolume().min().add(8, event.entityVolume().size().y() / 2, 8));
+                entity.offer(Keys.TRANSIENT, true);
+                entity.offer(Keys.IS_GRAVITY_AFFECTED, false);
+                entity.offer(Keys.IS_AI_ENABLED, false);
+                event.entityVolume().spawnEntity(entity);
+            });
+            if (ChunkEventTest.this.logEvents) {
+                Sponge.game().systemSubject().sendMessage(Component.text("Load Chunk Entities " + event.chunkPosition() + " in " + event.worldKey().asString()));
+            }
+        }
+
+        @Listener
+        public void onChunkEntitiesSavePre(final ChunkEvent.Entities.Save.Pre event) {
+            if (ChunkEventTest.this.cancelEntitySave) {
+                event.setCancelled(true);
+                return;
+            }
+            if (!ChunkEventTest.this.filterEntitySave.isEmpty()) {
+                event.entityVolume().entities().forEach(e -> {
+                    if (ChunkEventTest.this.filterEntitySave.contains(e.type())) {
+                        e.remove();
+                    }
+                });
+            }
+            if (ChunkEventTest.this.logEvents) {
+                Sponge.game().systemSubject().sendMessage(Component.text("Pre Save Chunk Entities " + event.chunkPosition() + " in " + event.worldKey().asString()));
+            }
+        }
+
+        @Listener
+        public void onChunkEntitiesSavePost(final ChunkEvent.Entities.Save.Post event) {
+            if (ChunkEventTest.this.logEvents) {
+                Sponge.game().systemSubject().sendMessage(Component.text("Post Save Chunk Entities " + event.chunkPosition() + " in " + event.worldKey().asString()));
+            }
         }
     }
 }

From bdf3f4b889930b93b6c95cd51f96a4d530533914 Mon Sep 17 00:00:00 2001
From: aromaa <me@joniaromaa.fi>
Date: Mon, 29 Apr 2024 18:34:14 +0300
Subject: [PATCH 2/3] PR feedback

---
 SpongeAPI                                              |  2 +-
 ...SpongeChunkEntities.java => SpongeEntityChunk.java} | 10 +++++-----
 .../world/level/chunk/storage/EntityStorageMixin.java  | 10 +++++-----
 .../org/spongepowered/test/chunk/ChunkEventTest.java   |  6 +++---
 4 files changed, 14 insertions(+), 14 deletions(-)
 rename src/main/java/org/spongepowered/common/world/level/chunk/storage/{SpongeChunkEntities.java => SpongeEntityChunk.java} (96%)

diff --git a/SpongeAPI b/SpongeAPI
index d48612a13cf..fa3b4aa3edc 160000
--- a/SpongeAPI
+++ b/SpongeAPI
@@ -1 +1 @@
-Subproject commit d48612a13cfc456b73166a8d6125d94f19f29589
+Subproject commit fa3b4aa3edc47813813bfe3170d21b894d067aeb
diff --git a/src/main/java/org/spongepowered/common/world/level/chunk/storage/SpongeChunkEntities.java b/src/main/java/org/spongepowered/common/world/level/chunk/storage/SpongeEntityChunk.java
similarity index 96%
rename from src/main/java/org/spongepowered/common/world/level/chunk/storage/SpongeChunkEntities.java
rename to src/main/java/org/spongepowered/common/world/level/chunk/storage/SpongeEntityChunk.java
index 0531cbf12ad..67f5bce8b7c 100644
--- a/src/main/java/org/spongepowered/common/world/level/chunk/storage/SpongeChunkEntities.java
+++ b/src/main/java/org/spongepowered/common/world/level/chunk/storage/SpongeEntityChunk.java
@@ -25,8 +25,8 @@
 package org.spongepowered.common.world.level.chunk.storage;
 
 import com.google.common.collect.ImmutableList;
+import net.minecraft.server.level.ServerLevel;
 import net.minecraft.util.Tuple;
-import net.minecraft.world.level.Level;
 import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
 import org.checkerframework.checker.nullness.qual.Nullable;
 import org.spongepowered.api.data.persistence.DataContainer;
@@ -55,9 +55,9 @@
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
-public final class SpongeChunkEntities implements EntityChunk {
+public final class SpongeEntityChunk implements EntityChunk {
 
-    private final Level level;
+    private final ServerLevel level;
     private final Vector3i chunkPosition;
     private final Stream<net.minecraft.world.entity.Entity> entities;
 
@@ -66,8 +66,8 @@ public final class SpongeChunkEntities implements EntityChunk {
     private @MonotonicNonNull Vector3i blockMax;
     private @MonotonicNonNull List<net.minecraft.world.entity.Entity> newEntities;
 
-    public SpongeChunkEntities(final Level chunkLayout, final Vector3i chunkPosition, final Stream<net.minecraft.world.entity.Entity> entities) {
-        this.level = chunkLayout;
+    public SpongeEntityChunk(final ServerLevel level, final Vector3i chunkPosition, final Stream<net.minecraft.world.entity.Entity> entities) {
+        this.level = level;
         this.chunkPosition = chunkPosition;
         this.entities = entities;
     }
diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/storage/EntityStorageMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/storage/EntityStorageMixin.java
index 3e0a944afb6..7e95447bd35 100644
--- a/src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/storage/EntityStorageMixin.java
+++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/level/chunk/storage/EntityStorageMixin.java
@@ -48,7 +48,7 @@
 import org.spongepowered.common.event.ShouldFire;
 import org.spongepowered.common.event.tracking.PhaseTracker;
 import org.spongepowered.common.util.VecHelper;
-import org.spongepowered.common.world.level.chunk.storage.SpongeChunkEntities;
+import org.spongepowered.common.world.level.chunk.storage.SpongeEntityChunk;
 import org.spongepowered.common.world.level.chunk.storage.SpongeIOWorkerType;
 import org.spongepowered.math.vector.Vector3i;
 
@@ -75,9 +75,9 @@ public abstract class EntityStorageMixin {
         }
 
         final Vector3i chunkPos = VecHelper.toVector3i($$0x);
-        final SpongeChunkEntities entities = new SpongeChunkEntities(this.level, chunkPos, cir.getReturnValue().getEntities());
+        final SpongeEntityChunk entities = new SpongeEntityChunk(this.level, chunkPos, cir.getReturnValue().getEntities());
         final ChunkEvent.Entities.Load loadEvent = SpongeEventFactory.createChunkEventEntitiesLoad(PhaseTracker.getInstance().currentCause(),
-                chunkPos, entities, (ResourceKey) (Object) this.level.dimension().location());
+                entities, chunkPos, (ResourceKey) (Object) this.level.dimension().location());
 
         SpongeCommon.post(loadEvent);
 
@@ -95,9 +95,9 @@ public abstract class EntityStorageMixin {
 
         final Vector3i chunkPos = VecHelper.toVector3i($$0.getPos());
 
-        final SpongeChunkEntities entities = new SpongeChunkEntities(this.level, chunkPos, $$0.getEntities());
+        final SpongeEntityChunk entities = new SpongeEntityChunk(this.level, chunkPos, $$0.getEntities());
         final ChunkEvent.Entities.Save.Pre saveEvent = SpongeEventFactory.createChunkEventEntitiesSavePre(PhaseTracker.getInstance().currentCause(),
-                chunkPos, entities, (ResourceKey) (Object) this.level.dimension().location());
+                entities, chunkPos, (ResourceKey) (Object) this.level.dimension().location());
 
         if (SpongeCommon.post(saveEvent)) {
             return null;
diff --git a/testplugins/src/main/java/org/spongepowered/test/chunk/ChunkEventTest.java b/testplugins/src/main/java/org/spongepowered/test/chunk/ChunkEventTest.java
index 3e102e5f47f..3bc485f4129 100644
--- a/testplugins/src/main/java/org/spongepowered/test/chunk/ChunkEventTest.java
+++ b/testplugins/src/main/java/org/spongepowered/test/chunk/ChunkEventTest.java
@@ -241,11 +241,11 @@ public void onChunkBlocksSavePost(final ChunkEvent.Blocks.Save.Post event) {
         @Listener
         public void onChunkEntitiesLoad(final ChunkEvent.Entities.Load event) {
             ChunkEventTest.this.addEntityLoad.forEach(e -> {
-                final Entity entity = event.entityVolume().createEntity(e, event.entityVolume().min().add(8, event.entityVolume().size().y() / 2, 8));
+                final Entity entity = event.chunk().createEntity(e, event.chunk().min().add(8, event.chunk().size().y() / 2, 8));
                 entity.offer(Keys.TRANSIENT, true);
                 entity.offer(Keys.IS_GRAVITY_AFFECTED, false);
                 entity.offer(Keys.IS_AI_ENABLED, false);
-                event.entityVolume().spawnEntity(entity);
+                event.chunk().spawnEntity(entity);
             });
             if (ChunkEventTest.this.logEvents) {
                 Sponge.game().systemSubject().sendMessage(Component.text("Load Chunk Entities " + event.chunkPosition() + " in " + event.worldKey().asString()));
@@ -259,7 +259,7 @@ public void onChunkEntitiesSavePre(final ChunkEvent.Entities.Save.Pre event) {
                 return;
             }
             if (!ChunkEventTest.this.filterEntitySave.isEmpty()) {
-                event.entityVolume().entities().forEach(e -> {
+                event.chunk().entities().forEach(e -> {
                     if (ChunkEventTest.this.filterEntitySave.contains(e.type())) {
                         e.remove();
                     }

From 9433bd8fce6d04d3029c8106eca7b1fd293178f2 Mon Sep 17 00:00:00 2001
From: aromaa <me@joniaromaa.fi>
Date: Mon, 29 Apr 2024 19:24:21 +0300
Subject: [PATCH 3/3] Update API ref

---
 SpongeAPI | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/SpongeAPI b/SpongeAPI
index fa3b4aa3edc..d5a46ad7fd4 160000
--- a/SpongeAPI
+++ b/SpongeAPI
@@ -1 +1 @@
-Subproject commit fa3b4aa3edc47813813bfe3170d21b894d067aeb
+Subproject commit d5a46ad7fd4e4757fd50778122c92b7fed9d0aa0