diff --git a/leaf-server/minecraft-patches/features/0277-Custom-NonNullList.patch b/leaf-server/minecraft-patches/features/0277-Custom-NonNullList.patch new file mode 100644 index 00000000..8dd790bb --- /dev/null +++ b/leaf-server/minecraft-patches/features/0277-Custom-NonNullList.patch @@ -0,0 +1,64 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Wed, 23 Jul 2025 15:51:59 +0200 +Subject: [PATCH] Custom NonNullList + + +diff --git a/net/minecraft/core/NonNullList.java b/net/minecraft/core/NonNullList.java +index 7e31c5c8659d24948fd45a2d6ee7bdeca6027d27..9d39369f298d53031b118c71f52471b0239a804b 100644 +--- a/net/minecraft/core/NonNullList.java ++++ b/net/minecraft/core/NonNullList.java +@@ -14,23 +14,28 @@ public class NonNullList extends AbstractList { + private final E defaultValue; + + public static NonNullList create() { +- return new NonNullList<>(Lists.newArrayList(), null); ++ return new NonNullList<>(new org.dreeam.leaf.util.list.OptimizedNonNullListArrayList<>(null), null); + } + + public static NonNullList createWithCapacity(int initialCapacity) { +- return new NonNullList<>(Lists.newArrayListWithCapacity(initialCapacity), null); ++ return new NonNullList<>(new org.dreeam.leaf.util.list.OptimizedNonNullListArrayList<>(initialCapacity, null), null); + } + + public static NonNullList withSize(int size, E defaultValue) { + Validate.notNull(defaultValue); +- Object[] objects = new Object[size]; ++ ++ @SuppressWarnings("unchecked") ++ E[] objects = (E[])new Object[size]; + Arrays.fill(objects, defaultValue); +- return new NonNullList<>(Arrays.asList((E[])objects), defaultValue); ++ return new NonNullList<>(new org.dreeam.leaf.util.list.OptimizedNonNullListArrayList<>(objects, defaultValue), defaultValue); + } + + @SafeVarargs + public static NonNullList of(E defaultValue, E... elements) { +- return new NonNullList<>(Arrays.asList(elements), defaultValue); ++ Validate.notNull(elements); ++ // Directly create the custom list from the array. ++ // This avoids the problematic, fixed-size `Arrays.asList()` and is more efficient. ++ return new NonNullList<>(new org.dreeam.leaf.util.list.OptimizedNonNullListArrayList<>(elements, defaultValue), defaultValue); + } + + protected NonNullList(List list, @Nullable E defaultValue) { +@@ -69,10 +74,16 @@ public class NonNullList extends AbstractList { + @Override + public void clear() { + if (this.defaultValue == null) { +- super.clear(); ++ this.list.clear(); + } else { +- for (int i = 0; i < this.size(); i++) { +- this.set(i, this.defaultValue); ++ // If the backing list is our custom type, use its fast fill method. ++ if (this.list instanceof org.dreeam.leaf.util.list.OptimizedNonNullListArrayList) { ++ ((org.dreeam.leaf.util.list.OptimizedNonNullListArrayList) this.list).fillWithDefault(); ++ } else { ++ // Fallback for any other List impl ++ for (int i = 0; i < this.size(); i++) { ++ this.list.set(i, this.defaultValue); ++ } + } + } + } diff --git a/leaf-server/minecraft-patches/features/0277-Optimise-TextColor.patch b/leaf-server/minecraft-patches/features/0278-Optimise-TextColor.patch similarity index 100% rename from leaf-server/minecraft-patches/features/0277-Optimise-TextColor.patch rename to leaf-server/minecraft-patches/features/0278-Optimise-TextColor.patch diff --git a/leaf-server/src/main/java/org/dreeam/leaf/util/list/OptimizedNonNullListArrayList.java b/leaf-server/src/main/java/org/dreeam/leaf/util/list/OptimizedNonNullListArrayList.java new file mode 100644 index 00000000..4d1b9f8b --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/util/list/OptimizedNonNullListArrayList.java @@ -0,0 +1,442 @@ +package org.dreeam.leaf.util.list; + +import it.unimi.dsi.fastutil.objects.AbstractObjectList; +import it.unimi.dsi.fastutil.objects.ObjectArrays; +import it.unimi.dsi.fastutil.objects.ObjectSpliterator; +import it.unimi.dsi.fastutil.objects.ObjectSpliterators; + +import java.io.*; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.RandomAccess; +import java.util.function.Consumer; +import javax.annotation.Nullable; + +/** + * optimized version of ObjectArrayList for NonNullList + * Licensed under: LGPL-3.0 (https://www.gnu.org/licenses/lgpl-3.0.html) + * By: @taiyouh at discord + */ +public class OptimizedNonNullListArrayList extends AbstractObjectList + implements RandomAccess, Cloneable, Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private static final int DEFAULT_INITIAL_CAPACITY = 10; + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // JVM limit + private static final int GROWTH_SHIFT = 1; // equivalent to /2 but faster + private static final int CAPACITY_THRESHOLD = 1 << 30; + + private static final int HASH_VALID_FLAG = 1; + private static final int HAS_DEFAULT_FLAG = 2; + + /** Packed flags for various boolean states */ + private int flags; + + /** The backing array */ + protected transient E[] a; + + /** The current actual size - kept as power of 2 when possible for faster operations */ + protected transient int size; + + /** The default value */ + @Nullable + private final E defaultValue; + + /** Cached hash code for performance (invalidated on modifications) */ + private transient int cachedHashCode = 0; + + // === CONSTRUCTORS === + + public OptimizedNonNullListArrayList(int capacity, @Nullable E defaultValue) { + if (capacity < 0) throw new IllegalArgumentException("Initial capacity (" + capacity + ") is negative"); + + // round up to next power of 2 for better cache perf + capacity = nextPowerOfTwo(capacity); + + if (capacity == 0) { + this.a = (E[])ObjectArrays.EMPTY_ARRAY; + } else { + this.a = (E[])new Object[capacity]; + } + + this.defaultValue = defaultValue; + this.flags = defaultValue != null ? HAS_DEFAULT_FLAG : 0; + } + + public OptimizedNonNullListArrayList(@Nullable E defaultValue) { + this.a = (E[])ObjectArrays.DEFAULT_EMPTY_ARRAY; + this.defaultValue = defaultValue; + this.flags = defaultValue != null ? HAS_DEFAULT_FLAG : 0; + } + + public OptimizedNonNullListArrayList(final E[] sourceArray, @Nullable E defaultValue) { + this(sourceArray.length, defaultValue); + System.arraycopy(sourceArray, 0, this.a, 0, sourceArray.length); + this.size = sourceArray.length; + } + + // === BIT MANIPULATION UTILITIES === + + /** + * Fast power-of-2 calculation + */ + private static int nextPowerOfTwo(int n) { + if (n <= 1) return 1; + if ((n & (n - 1)) == 0) return n; // Already power of 2 + + n--; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + return ++n; + } + + /** + * Check if number is power of 2 + */ + private static boolean isPowerOfTwo(int n) { + return n > 0 && (n & (n - 1)) == 0; + } + + // === FLAG UTILITIES === + + private boolean hasFlag(int flag) { + return (flags & flag) != 0; + } + + private void setFlag(int flag) { + flags |= flag; + } + + private void clearFlag(int flag) { + flags &= ~flag; + } + + // === GROWTH LOGIC === + + private void grow(int minCapacity) { + final int oldCapacity = a.length; + if (minCapacity <= oldCapacity) return; + + int newCapacity; + + if (a == ObjectArrays.DEFAULT_EMPTY_ARRAY) { + newCapacity = Math.max(DEFAULT_INITIAL_CAPACITY, minCapacity); + } else { + // use shifts instead of division/multiplication + if (oldCapacity < CAPACITY_THRESHOLD) { + // For smaller arrays, use 1.5x growth + newCapacity = oldCapacity + (oldCapacity >>> GROWTH_SHIFT); + } else { + // For larger arrays, use more conservative growth to avoid OOM + newCapacity = oldCapacity + (oldCapacity >>> 2); // 1.25x growth + } + + newCapacity = Math.max(newCapacity, minCapacity); + + if (newCapacity > MAX_ARRAY_SIZE) { + newCapacity = hugeCapacity(minCapacity); + } + if (newCapacity < 1024) { + newCapacity = nextPowerOfTwo(newCapacity); + } + } + + final Object[] newArray = new Object[newCapacity]; + System.arraycopy(a, 0, newArray, 0, size); + a = (E[])newArray; + } + + private static int hugeCapacity(int minCapacity) { + if (minCapacity < 0) throw new OutOfMemoryError(); + return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; + } + + // === CORE OPERATIONS === + + @Override + public void add(final int index, final E k) { + Objects.requireNonNull(k, "Cannot add null element to this list"); + + if ((index >>> 31) != 0 || index > size) { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); + } + + grow(size + 1); + + if (index != size) { + System.arraycopy(a, index, a, index + 1, size - index); + } + + a[index] = k; + size++; + invalidateHash(); + } + + @Override + public boolean add(final E k) { + Objects.requireNonNull(k, "Cannot add null element to this list"); + grow(size + 1); + a[size++] = k; + invalidateHash(); + return true; + } + + @Override + public E get(final int index) { + if ((index | (size - 1 - index)) < 0) { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); + } + return a[index]; + } + + @Override + public E remove(final int index) { + if ((index | (size - 1 - index)) < 0) { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); + } + + final E old = a[index]; + final int numMoved = size - index - 1; + + if (numMoved > 0) { + System.arraycopy(a, index + 1, a, index, numMoved); + } + + a[--size] = null; // Clear reference for GC + invalidateHash(); + return old; + } + + @Override + public E set(final int index, final E k) { + Objects.requireNonNull(k, "Cannot set null element in this list"); + + if ((index | (size - 1 - index)) < 0) { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); + } + + final E old = a[index]; + a[index] = k; + invalidateHash(); + return old; + } + + @Override + public void clear() { + if (defaultValue != null && size > 0) { + Arrays.fill(a, 0, size, defaultValue); + } + size = 0; + invalidateHash(); + } + + /** + * fast fill with default value + */ + public void fillWithDefault() { + if (hasFlag(HAS_DEFAULT_FLAG) && size > 0) { + Arrays.fill(a, 0, size, defaultValue); + invalidateHash(); + } + } + + // === SEARCH OPERATIONS === + + @Override + public int indexOf(final Object k) { + if (k == null) return -1; + + final Object[] array = this.a; + final int length = this.size; + + int i = 0; + for (; i < length - 3; i += 4) { + if (k.equals(array[i])) return i; + if (k.equals(array[i + 1])) return i + 1; + if (k.equals(array[i + 2])) return i + 2; + if (k.equals(array[i + 3])) return i + 3; + } + + for (; i < length; i++) { + if (k.equals(array[i])) return i; + } + + return -1; + } + + @Override + public int lastIndexOf(final Object k) { + if (k == null) return -1; + + final Object[] array = this.a; + + for (int i = size - 1; i >= 3; i -= 4) { + if (k.equals(array[i])) return i; + if (k.equals(array[i - 1])) return i - 1; + if (k.equals(array[i - 2])) return i - 2; + if (k.equals(array[i - 3])) return i - 3; + } + + for (int i = Math.min(size - 1, 3); i >= 0; i--) { + if (k.equals(array[i])) return i; + } + + return -1; + } + + @Override + public int hashCode() { + if (hasFlag(HASH_VALID_FLAG)) return cachedHashCode; + + final Object[] array = this.a; + final int length = this.size; + int h; + int i = 0; + if (length >=4) { + int h1 = 1; + int h2 = 3; + int h3 = 7; + int h4 = 11; + for (; i < length - 3; i += 4) { + h1 = 31 * h1 + array[i].hashCode(); + h2 = 31 * h2 + array[i + 1].hashCode(); + h3 = 31 * h3 + array[i + 2].hashCode(); + h4 = 31 * h4 + array[i + 3].hashCode(); + } + h = h1 + h2 + h3 + h4; + } else { + h = 1; + } + for (; i < length; i++) { + h = 31 * h + array[i].hashCode(); + } + cachedHashCode = h; + setFlag(HASH_VALID_FLAG); + return h; + } + + private void invalidateHash() { + clearFlag(HASH_VALID_FLAG); + } + + @Override + public boolean equals(final Object o) { + if (o == this) return true; + if (!(o instanceof List)) return false; + + if (o instanceof OptimizedNonNullListArrayList) { + final OptimizedNonNullListArrayList other = (OptimizedNonNullListArrayList)o; + if (size != other.size) return false; + if (hasFlag(HASH_VALID_FLAG) && other.hasFlag(HASH_VALID_FLAG) && cachedHashCode != other.cachedHashCode) { + return false; + } + return Arrays.equals(a, 0, size, other.a, 0, size); + } + + return super.equals(o); + } + + @Override public int size() { return size; } + @Override public boolean isEmpty() { return size == 0; } + + // === SPLITERATOR WITH PREFETCH HINTS === + + @Override + public ObjectSpliterator spliterator() { + return new OptimizedSpliterator(); + } + + private final class OptimizedSpliterator implements ObjectSpliterator { + private int pos, max; + private boolean hasSplit = false; + + public OptimizedSpliterator() { + this(0, OptimizedNonNullListArrayList.this.size, false); + } + + private OptimizedSpliterator(int pos, int max, boolean hasSplit) { + this.pos = pos; + this.max = max; + this.hasSplit = hasSplit; + } + + @Override + public int characteristics() { + return ObjectSpliterators.LIST_SPLITERATOR_CHARACTERISTICS; + } + + @Override + public long estimateSize() { + return getWorkingMax() - pos; + } + + private int getWorkingMax() { + return hasSplit ? max : OptimizedNonNullListArrayList.this.size; + } + + @Override + public boolean tryAdvance(final Consumer action) { + if (pos >= getWorkingMax()) return false; + action.accept(a[pos++]); + return true; + } + + @Override + public ObjectSpliterator trySplit() { + final int max = getWorkingMax(); + final int remaining = max - pos; + + int retLen = remaining >>> 1; + if (retLen <= 1) return null; + + this.max = max; + int myNewPos = pos + retLen; + int oldPos = pos; + this.pos = myNewPos; + this.hasSplit = true; + + return new OptimizedSpliterator(oldPos, myNewPos, true); + } + } + + @Override + public OptimizedNonNullListArrayList clone() { + OptimizedNonNullListArrayList cloned; + try { + cloned = (OptimizedNonNullListArrayList)super.clone(); + } catch (CloneNotSupportedException err) { + throw new InternalError(err); + } + + cloned.a = Arrays.copyOf(this.a, this.size); + cloned.clearFlag(HASH_VALID_FLAG); + return cloned; + } + + @Serial + private void writeObject(ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + s.writeInt(size); + for (int i = 0; i < size; i++) { + s.writeObject(a[i]); + } + } + + @Serial + private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { + s.defaultReadObject(); + this.size = s.readInt(); + this.a = (E[])new Object[this.size]; + for (int i = 0; i < size; i++) { + a[i] = (E)s.readObject(); + } + this.flags = (defaultValue != null) ? HAS_DEFAULT_FLAG : 0; + // Hash is not valid after deserialization (HASH_VALID_FLAG remains cleared) + } +}