diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/BlockEntityTickersList.java b/core/src/main/java/net/momirealms/craftengine/core/util/BlockEntityTickersList.java
new file mode 100644
index 000000000..124269086
--- /dev/null
+++ b/core/src/main/java/net/momirealms/craftengine/core/util/BlockEntityTickersList.java
@@ -0,0 +1,111 @@
+/**
+ * This implementation references the BlockEntityTickersList implementation by Winds Studio,
+ * available at: https://github.com/Winds-Studio/Leaf/blob/b9ebff/leaf-server/src/main/java/org/dreeam/leaf/util/list/BlockEntityTickersList.java
+ *
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3)
+ */
+package net.momirealms.craftengine.core.util;
+
+import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import net.momirealms.craftengine.core.block.entity.tick.TickingBlockEntity;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+/**
+ * A list for ServerLevel's blockEntityTickers
+ *
+ * This list behaves identically to ObjectArrayList, but it has an additional method, `removeAllByIndex`, that allows a list of integers to be passed indicating what
+ * indexes should be deleted from the list
+ *
+ * This is faster than using removeAll, since we don't need to compare the identity of each block entity, and faster than looping thru each index manually and deleting with remove,
+ * since we don't need to resize the array every single remove.
+ */
+public final class BlockEntityTickersList extends ObjectArrayList {
+
+ private final IntOpenHashSet toRemove = new IntOpenHashSet();
+ private int startSearchFromIndex = -1;
+
+ /**
+ * Creates a new array list with {@link #DEFAULT_INITIAL_CAPACITY} capacity.
+ */
+ public BlockEntityTickersList() {
+ super();
+ }
+
+ /**
+ * Creates a new array list and fills it with a given collection.
+ *
+ * @param c a collection that will be used to fill the array list.
+ */
+ public BlockEntityTickersList(final Collection extends TickingBlockEntity> c) {
+ super(c);
+ }
+
+ /**
+ * Marks an entry as removed
+ *
+ * @param index the index of the item on the list to be marked as removed
+ */
+ public void markAsRemoved(final int index) {
+ // The block entities list always loop starting from 0, so we only need to check if the startSearchFromIndex is -1 and that's it
+ if (this.startSearchFromIndex == -1)
+ this.startSearchFromIndex = index;
+
+ this.toRemove.add(index);
+ }
+
+ /**
+ * Removes elements that have been marked as removed.
+ */
+ public void removeMarkedEntries() {
+ if (this.startSearchFromIndex == -1) // No entries in the list, skip
+ return;
+
+ removeAllByIndex(startSearchFromIndex, toRemove);
+ toRemove.clear();
+ this.startSearchFromIndex = -1; // Reset the start search index
+ }
+
+ /**
+ * Removes elements by their index.
+ */
+ private void removeAllByIndex(final int startSearchFromIndex, final IntOpenHashSet c) { // can't use Set because we want to avoid autoboxing when using contains
+ final int requiredMatches = c.size();
+ if (requiredMatches == 0)
+ return; // exit early, we don't need to do anything
+
+ final Object[] a = this.a;
+ int writeIndex = startSearchFromIndex;
+ int lastCopyIndex = startSearchFromIndex;
+ int matches = 0;
+
+ for (int readIndex = startSearchFromIndex; readIndex < size; readIndex++) {
+ if (c.contains(readIndex)) {
+ matches++;
+ final int blockLength = readIndex - lastCopyIndex;
+ if (blockLength > 0) {
+ System.arraycopy(a, lastCopyIndex, a, writeIndex, blockLength);
+ writeIndex += blockLength;
+ }
+ lastCopyIndex = readIndex + 1;
+
+ if (matches == requiredMatches) {
+ break;
+ }
+ }
+ }
+
+ final int finalBlockLength = size - lastCopyIndex;
+ if (finalBlockLength > 0) {
+ System.arraycopy(a, lastCopyIndex, a, writeIndex, finalBlockLength);
+ writeIndex += finalBlockLength;
+ }
+
+ if (writeIndex < size) {
+ Arrays.fill(a, writeIndex, size, null);
+ }
+ size = writeIndex;
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/net/momirealms/craftengine/core/world/CEWorld.java b/core/src/main/java/net/momirealms/craftengine/core/world/CEWorld.java
index 38b3985ec..5d35506a7 100644
--- a/core/src/main/java/net/momirealms/craftengine/core/world/CEWorld.java
+++ b/core/src/main/java/net/momirealms/craftengine/core/world/CEWorld.java
@@ -1,13 +1,13 @@
package net.momirealms.craftengine.core.world;
import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
-import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.entity.BlockEntity;
import net.momirealms.craftengine.core.block.entity.tick.TickingBlockEntity;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.scheduler.SchedulerTask;
+import net.momirealms.craftengine.core.util.BlockEntityTickersList;
import net.momirealms.craftengine.core.world.chunk.CEChunk;
import net.momirealms.craftengine.core.world.chunk.storage.StorageAdaptor;
import net.momirealms.craftengine.core.world.chunk.storage.WorldDataStorage;
@@ -25,12 +25,14 @@ public abstract class CEWorld {
protected final WorldHeight worldHeightAccessor;
protected List pendingLightSections = new ArrayList<>();
protected final Set lightSections = ConcurrentHashMap.newKeySet(128);
- protected final List tickingBlockEntities = new ArrayList<>();
+ protected final BlockEntityTickersList tickingBlockEntities = new BlockEntityTickersList();
protected final List pendingTickingBlockEntities = new ArrayList<>();
protected volatile boolean isTickingBlockEntities = false;
protected volatile boolean isUpdatingLights = false;
protected SchedulerTask syncTickTask;
protected SchedulerTask asyncTickTask;
+ @SuppressWarnings("FieldCanBeLocal")
+ private int tileTickPosition;
public CEWorld(World world, StorageAdaptor adaptor) {
this.world = world;
@@ -205,15 +207,15 @@ public abstract class CEWorld {
this.pendingTickingBlockEntities.clear();
}
if (!this.tickingBlockEntities.isEmpty()) {
- ReferenceOpenHashSet toRemove = new ReferenceOpenHashSet<>();
- for (TickingBlockEntity blockEntity : this.tickingBlockEntities) {
+ for (this.tileTickPosition = 0; this.tileTickPosition < this.tickingBlockEntities.size(); this.tileTickPosition++) {
+ TickingBlockEntity blockEntity = this.tickingBlockEntities.get(this.tileTickPosition);
if (blockEntity.isValid()) {
blockEntity.tick();
} else {
- toRemove.add(blockEntity);
+ this.tickingBlockEntities.markAsRemoved(this.tileTickPosition);
}
}
- this.tickingBlockEntities.removeAll(toRemove);
+ this.tickingBlockEntities.removeMarkedEntries();
}
this.isTickingBlockEntities = false;
}