diff options
Diffstat (limited to 'patches')
6 files changed, 21301 insertions, 323 deletions
diff --git a/patches/server/0489-Improve-ServerGUI.patch b/patches/server/0489-Improve-ServerGUI.patch index 8dde1d1a9b..48a151cd82 100644 --- a/patches/server/0489-Improve-ServerGUI.patch +++ b/patches/server/0489-Improve-ServerGUI.patch @@ -102,329 +102,330 @@ new file mode 100644 index 0000000000000000000000000000000000000000..8b924977b7886df9ab8790b1e4ff9b1c04a2af45 GIT binary patch literal 16900 -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 -LFIp>X81%mY^Bg|U +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 +XXyugpZ1#qMfq}u()z4*}Q$iB}^Bg|U literal 0 HcmV?d00001 diff --git a/patches/server/1069-fixup-ConcurrentUtil.patch b/patches/server/1069-fixup-ConcurrentUtil.patch new file mode 100644 index 0000000000..60d57a1656 --- /dev/null +++ b/patches/server/1069-fixup-ConcurrentUtil.patch @@ -0,0 +1,6177 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf <[email protected]> +Date: Mon, 21 Oct 2024 11:47:34 -0700 +Subject: [PATCH] fixup! ConcurrentUtil + + +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java +deleted file mode 100644 +index 094eff418b4e3bffce020d650931b4d9e58fa9ed..0000000000000000000000000000000000000000 +--- a/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java ++++ /dev/null +@@ -1,149 +0,0 @@ +-package ca.spottedleaf.concurrentutil.collection; +- +-import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +-import ca.spottedleaf.concurrentutil.util.Validate; +- +-import java.lang.invoke.VarHandle; +-import java.util.ConcurrentModificationException; +- +-/** +- * Single reader thread single writer thread queue. The reader side of the queue is ordered by acquire semantics, +- * and the writer side of the queue is ordered by release semantics. +- */ +-// TODO test +-public class SRSWLinkedQueue<E> { +- +- // always non-null +- protected LinkedNode<E> head; +- +- // always non-null +- protected LinkedNode<E> tail; +- +- /* IMPL NOTE: Leave hashCode and equals to their defaults */ +- +- public SRSWLinkedQueue() { +- final LinkedNode<E> dummy = new LinkedNode<>(null, null); +- this.head = this.tail = dummy; +- } +- +- /** +- * Must be the reader thread. +- * +- * <p> +- * Returns, without removing, the first element of this queue. +- * </p> +- * @return Returns, without removing, the first element of this queue. +- */ +- public E peekFirst() { +- LinkedNode<E> head = this.head; +- E ret = head.getElementPlain(); +- if (ret == null) { +- head = head.getNextAcquire(); +- if (head == null) { +- // empty +- return null; +- } +- // update head reference for next poll() call +- this.head = head; +- // guaranteed to be non-null +- ret = head.getElementPlain(); +- if (ret == null) { +- throw new ConcurrentModificationException("Multiple reader threads"); +- } +- } +- +- return ret; +- } +- +- /** +- * Must be the reader thread. +- * +- * <p> +- * Returns and removes the first element of this queue. +- * </p> +- * @return Returns and removes the first element of this queue. +- */ +- public E poll() { +- LinkedNode<E> head = this.head; +- E ret = head.getElementPlain(); +- if (ret == null) { +- head = head.getNextAcquire(); +- if (head == null) { +- // empty +- return null; +- } +- // guaranteed to be non-null +- ret = head.getElementPlain(); +- if (ret == null) { +- throw new ConcurrentModificationException("Multiple reader threads"); +- } +- } +- +- head.setElementPlain(null); +- LinkedNode<E> next = head.getNextAcquire(); +- this.head = next == null ? head : next; +- +- return ret; +- } +- +- /** +- * Must be the writer thread. +- * +- * <p> +- * Adds the element to the end of the queue. +- * </p> +- * +- * @throws NullPointerException If the provided element is null +- */ +- public void addLast(final E element) { +- Validate.notNull(element, "Provided element cannot be null"); +- final LinkedNode<E> append = new LinkedNode<>(element, null); +- +- this.tail.setNextRelease(append); +- this.tail = append; +- } +- +- protected static final class LinkedNode<E> { +- +- protected volatile Object element; +- protected volatile LinkedNode<E> next; +- +- protected static final VarHandle ELEMENT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "element", Object.class); +- protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "next", LinkedNode.class); +- +- protected LinkedNode(final Object element, final LinkedNode<E> next) { +- ELEMENT_HANDLE.set(this, element); +- NEXT_HANDLE.set(this, next); +- } +- +- /* element */ +- +- @SuppressWarnings("unchecked") +- protected final E getElementPlain() { +- return (E)ELEMENT_HANDLE.get(this); +- } +- +- protected final void setElementPlain(final E update) { +- ELEMENT_HANDLE.set(this, (Object)update); +- } +- /* next */ +- +- @SuppressWarnings("unchecked") +- protected final LinkedNode<E> getNextPlain() { +- return (LinkedNode<E>)NEXT_HANDLE.get(this); +- } +- +- @SuppressWarnings("unchecked") +- protected final LinkedNode<E> getNextAcquire() { +- return (LinkedNode<E>)NEXT_HANDLE.getAcquire(this); +- } +- +- protected final void setNextPlain(final LinkedNode<E> next) { +- NEXT_HANDLE.set(this, next); +- } +- +- protected final void setNextRelease(final LinkedNode<E> next) { +- NEXT_HANDLE.setRelease(this, next); +- } +- } +-} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/completable/CallbackCompletable.java b/src/main/java/ca/spottedleaf/concurrentutil/completable/CallbackCompletable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6bad6f8ecc0944d2f406924c7de7e227ff1e70fa +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/completable/CallbackCompletable.java +@@ -0,0 +1,110 @@ ++package ca.spottedleaf.concurrentutil.completable; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.Cancellable; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; ++import java.util.function.BiConsumer; ++ ++public final class CallbackCompletable<T> { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(CallbackCompletable.class); ++ ++ private final MultiThreadedQueue<BiConsumer<T, Throwable>> waiters = new MultiThreadedQueue<>(); ++ private T result; ++ private Throwable throwable; ++ private volatile boolean completed; ++ ++ public boolean isCompleted() { ++ return this.completed; ++ } ++ ++ /** ++ * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero ++ * synchronisation ++ */ ++ public T getResult() { ++ return this.result; ++ } ++ ++ /** ++ * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero ++ * synchronisation ++ */ ++ public Throwable getThrowable() { ++ return this.throwable; ++ } ++ ++ /** ++ * Adds a waiter that should only be completed asynchronously by the complete() calls. If complete() ++ * has already been called, returns {@code null} and does not invoke the specified consumer. ++ * @param consumer Consumer to be executed on completion ++ * @throws NullPointerException If consumer is null ++ * @return A cancellable which will control the execution of the specified consumer ++ */ ++ public Cancellable addAsynchronousWaiter(final BiConsumer<T, Throwable> consumer) { ++ if (this.waiters.add(consumer)) { ++ return new CancellableImpl(consumer); ++ } ++ return null; ++ } ++ ++ private void completeAllWaiters(final T result, final Throwable throwable) { ++ this.completed = true; ++ BiConsumer<T, Throwable> waiter; ++ while ((waiter = this.waiters.pollOrBlockAdds()) != null) { ++ this.completeWaiter(waiter, result, throwable); ++ } ++ } ++ ++ private void completeWaiter(final BiConsumer<T, Throwable> consumer, final T result, final Throwable throwable) { ++ try { ++ consumer.accept(result, throwable); ++ } catch (final Throwable throwable2) { ++ LOGGER.error("Failed to complete callback " + ConcurrentUtil.genericToString(consumer), throwable2); ++ } ++ } ++ ++ /** ++ * Adds a waiter that will be completed asynchronously by the complete() calls. If complete() ++ * has already been called, then invokes the consumer synchronously with the completed result. ++ * @param consumer Consumer to be executed on completion ++ * @throws NullPointerException If consumer is null ++ * @return A cancellable which will control the execution of the specified consumer ++ */ ++ public Cancellable addWaiter(final BiConsumer<T, Throwable> consumer) { ++ if (this.waiters.add(consumer)) { ++ return new CancellableImpl(consumer); ++ } ++ this.completeWaiter(consumer, this.result, this.throwable); ++ return new CancellableImpl(consumer); ++ } ++ ++ public void complete(final T result) { ++ this.result = result; ++ this.completeAllWaiters(result, null); ++ } ++ ++ public void completeWithThrowable(final Throwable throwable) { ++ if (throwable == null) { ++ throw new NullPointerException("Throwable cannot be null"); ++ } ++ this.throwable = throwable; ++ this.completeAllWaiters(null, throwable); ++ } ++ ++ private final class CancellableImpl implements Cancellable { ++ ++ private final BiConsumer<T, Throwable> waiter; ++ ++ private CancellableImpl(final BiConsumer<T, Throwable> waiter) { ++ this.waiter = waiter; ++ } ++ ++ @Override ++ public boolean cancel() { ++ return CallbackCompletable.this.waiters.remove(this.waiter); ++ } ++ } ++} +\ No newline at end of file +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java +index 46d1bd01542ebeeffc0006a5c585a50dbbbff907..365616439fa079017d648ed7f6ddf6950a691adf 100644 +--- a/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java ++++ b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java +@@ -1,112 +1,737 @@ + package ca.spottedleaf.concurrentutil.completable; + +-import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +-import ca.spottedleaf.concurrentutil.executor.Cancellable; + import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Validate; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; ++import java.lang.invoke.VarHandle; ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.CompletionException; ++import java.util.concurrent.CompletionStage; ++import java.util.concurrent.Executor; ++import java.util.concurrent.ForkJoinPool; ++import java.util.concurrent.locks.LockSupport; + import java.util.function.BiConsumer; ++import java.util.function.BiFunction; ++import java.util.function.Consumer; ++import java.util.function.Function; ++import java.util.function.Supplier; + + public final class Completable<T> { + + private static final Logger LOGGER = LoggerFactory.getLogger(Completable.class); ++ private static final Function<? super Throwable, ? extends Throwable> DEFAULT_EXCEPTION_HANDLER = (final Throwable thr) -> { ++ LOGGER.error("Unhandled exception during Completable operation", thr); ++ return thr; ++ }; + +- private final MultiThreadedQueue<BiConsumer<T, Throwable>> waiters = new MultiThreadedQueue<>(); +- private T result; +- private Throwable throwable; +- private volatile boolean completed; ++ public static Executor getDefaultExecutor() { ++ return ForkJoinPool.commonPool(); ++ } ++ ++ private static final Transform<?, ?> COMPLETED_STACK = new Transform<>(null, null, null, null) { ++ @Override ++ public void run() {} ++ }; ++ private volatile Transform<?, T> completeStack; ++ private static final VarHandle COMPLETE_STACK_HANDLE = ConcurrentUtil.getVarHandle(Completable.class, "completeStack", Transform.class); ++ ++ private static final Object NULL_MASK = new Object(); ++ private volatile Object result; ++ private static final VarHandle RESULT_HANDLE = ConcurrentUtil.getVarHandle(Completable.class, "result", Object.class); + +- public boolean isCompleted() { +- return this.completed; ++ private Object getResultPlain() { ++ return (Object)RESULT_HANDLE.get(this); + } + +- /** +- * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero +- * synchronisation +- */ +- public T getResult() { +- return this.result; ++ private Object getResultVolatile() { ++ return (Object)RESULT_HANDLE.getVolatile(this); + } + +- /** +- * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero +- * synchronisation +- */ +- public Throwable getThrowable() { +- return this.throwable; ++ private void pushStackOrRun(final Transform<?, T> push) { ++ int failures = 0; ++ for (Transform<?, T> curr = (Transform<?, T>)COMPLETE_STACK_HANDLE.getVolatile(this);;) { ++ if (curr == COMPLETED_STACK) { ++ push.execute(); ++ return; ++ } ++ ++ push.next = curr; ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = (Transform<?, T>)COMPLETE_STACK_HANDLE.compareAndExchange(this, curr, push))) { ++ return; ++ } ++ push.next = null; ++ ++failures; ++ } + } + +- /** +- * Adds a waiter that should only be completed asynchronously by the complete() calls. If complete() +- * has already been called, returns {@code null} and does not invoke the specified consumer. +- * @param consumer Consumer to be executed on completion +- * @throws NullPointerException If consumer is null +- * @return A cancellable which will control the execution of the specified consumer +- */ +- public Cancellable addAsynchronousWaiter(final BiConsumer<T, Throwable> consumer) { +- if (this.waiters.add(consumer)) { +- return new CancellableImpl(consumer); ++ private void propagateStack() { ++ Transform<?, T> topStack = (Transform<?, T>)COMPLETE_STACK_HANDLE.getAndSet(this, COMPLETED_STACK); ++ while (topStack != null) { ++ topStack.execute(); ++ topStack = topStack.next; + } +- return null; + } + +- private void completeAllWaiters(final T result, final Throwable throwable) { +- this.completed = true; +- BiConsumer<T, Throwable> waiter; +- while ((waiter = this.waiters.pollOrBlockAdds()) != null) { +- this.completeWaiter(waiter, result, throwable); ++ private static Object maskNull(final Object res) { ++ return res == null ? NULL_MASK : res; ++ } ++ ++ private static Object unmaskNull(final Object res) { ++ return res == NULL_MASK ? null : res; ++ } ++ ++ private static Executor checkExecutor(final Executor executor) { ++ return Validate.notNull(executor, "Executor may not be null"); ++ } ++ ++ public Completable() {} ++ ++ private Completable(final Object complete) { ++ COMPLETE_STACK_HANDLE.set(this, COMPLETED_STACK); ++ RESULT_HANDLE.setRelease(this, complete); ++ } ++ ++ public static <T> Completable<T> completed(final T value) { ++ return new Completable<>(maskNull(value)); ++ } ++ ++ public static <T> Completable<T> failed(final Throwable ex) { ++ Validate.notNull(ex, "Exception may not be null"); ++ ++ return new Completable<>(new ExceptionResult(ex)); ++ } ++ ++ public static <T> Completable<T> supplied(final Supplier<T> supplier) { ++ return supplied(supplier, DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public static <T> Completable<T> supplied(final Supplier<T> supplier, final Function<? super Throwable, ? extends Throwable> exceptionHandler) { ++ try { ++ return completed(supplier.get()); ++ } catch (final Throwable throwable) { ++ Throwable complete; ++ try { ++ complete = exceptionHandler.apply(throwable); ++ } catch (final Throwable thr2) { ++ throwable.addSuppressed(thr2); ++ complete = throwable; ++ } ++ return failed(complete); + } + } + +- private void completeWaiter(final BiConsumer<T, Throwable> consumer, final T result, final Throwable throwable) { ++ public static <T> Completable<T> suppliedAsync(final Supplier<T> supplier, final Executor executor) { ++ return suppliedAsync(supplier, executor, DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public static <T> Completable<T> suppliedAsync(final Supplier<T> supplier, final Executor executor, final Function<? super Throwable, ? extends Throwable> exceptionHandler) { ++ final Completable<T> ret = new Completable<>(); ++ ++ class AsyncSuppliedCompletable implements Runnable, CompletableFuture.AsynchronousCompletionTask { ++ @Override ++ public void run() { ++ try { ++ ret.complete(supplier.get()); ++ } catch (final Throwable throwable) { ++ Throwable complete; ++ try { ++ complete = exceptionHandler.apply(throwable); ++ } catch (final Throwable thr2) { ++ throwable.addSuppressed(thr2); ++ complete = throwable; ++ } ++ ret.completeExceptionally(complete); ++ } ++ } ++ } ++ + try { +- consumer.accept(result, throwable); +- } catch (final ThreadDeath death) { +- throw death; +- } catch (final Throwable throwable2) { +- LOGGER.error("Failed to complete callback " + ConcurrentUtil.genericToString(consumer), throwable2); ++ executor.execute(new AsyncSuppliedCompletable()); ++ } catch (final Throwable throwable) { ++ Throwable complete; ++ try { ++ complete = exceptionHandler.apply(throwable); ++ } catch (final Throwable thr2) { ++ throwable.addSuppressed(thr2); ++ complete = throwable; ++ } ++ ret.completeExceptionally(complete); ++ } ++ ++ return ret; ++ } ++ ++ private boolean completeRaw(final Object value) { ++ if ((Object)RESULT_HANDLE.getVolatile(this) != null || !(boolean)RESULT_HANDLE.compareAndSet(this, (Object)null, value)) { ++ return false; ++ } ++ ++ this.propagateStack(); ++ return true; ++ } ++ ++ public boolean complete(final T result) { ++ return this.completeRaw(maskNull(result)); ++ } ++ ++ public boolean completeExceptionally(final Throwable exception) { ++ Validate.notNull(exception, "Exception may not be null"); ++ ++ return this.completeRaw(new ExceptionResult(exception)); ++ } ++ ++ public boolean isDone() { ++ return this.getResultVolatile() != null; ++ } ++ ++ public boolean isNormallyComplete() { ++ return this.getResultVolatile() != null && !(this.getResultVolatile() instanceof ExceptionResult); ++ } ++ ++ public boolean isExceptionallyComplete() { ++ return this.getResultVolatile() instanceof ExceptionResult; ++ } ++ ++ public Throwable getException() { ++ final Object res = this.getResultVolatile(); ++ if (res == null) { ++ return null; ++ } ++ ++ if (!(res instanceof ExceptionResult exRes)) { ++ throw new IllegalStateException("Not completed exceptionally"); ++ } ++ ++ return exRes.ex; ++ } ++ ++ public T getNow(final T dfl) throws CompletionException { ++ final Object res = this.getResultVolatile(); ++ if (res == null) { ++ return dfl; ++ } ++ ++ if (res instanceof ExceptionResult exRes) { ++ throw new CompletionException(exRes.ex); ++ } ++ ++ return (T)unmaskNull(res); ++ } ++ ++ public T join() throws CompletionException { ++ if (this.isDone()) { ++ return this.getNow(null); ++ } ++ ++ final UnparkTransform<T> unparkTransform = new UnparkTransform<>(this, Thread.currentThread()); ++ ++ this.pushStackOrRun(unparkTransform); ++ ++ boolean interuptted = false; ++ while (!unparkTransform.isReleasable()) { ++ try { ++ ForkJoinPool.managedBlock(unparkTransform); ++ } catch (final InterruptedException ex) { ++ interuptted = true; ++ } ++ } ++ ++ if (interuptted) { ++ Thread.currentThread().interrupt(); ++ } ++ ++ return this.getNow(null); ++ } ++ ++ public CompletableFuture<T> toFuture() { ++ final Object rawResult = this.getResultVolatile(); ++ if (rawResult != null) { ++ if (rawResult instanceof ExceptionResult exRes) { ++ return CompletableFuture.failedFuture(exRes.ex); ++ } else { ++ return CompletableFuture.completedFuture((T)unmaskNull(rawResult)); ++ } ++ } ++ ++ final CompletableFuture<T> ret = new CompletableFuture<>(); ++ ++ class ToFuture implements BiConsumer<T, Throwable> { ++ ++ @Override ++ public void accept(final T res, final Throwable ex) { ++ if (ex != null) { ++ ret.completeExceptionally(ex); ++ } else { ++ ret.complete(res); ++ } ++ } ++ } ++ ++ this.whenComplete(new ToFuture()); ++ ++ return ret; ++ } ++ ++ public static <T> Completable<T> fromFuture(final CompletionStage<T> stage) { ++ final Completable<T> ret = new Completable<>(); ++ ++ class FromFuture implements BiConsumer<T, Throwable> { ++ @Override ++ public void accept(final T res, final Throwable ex) { ++ if (ex != null) { ++ ret.completeExceptionally(ex); ++ } else { ++ ret.complete(res); ++ } ++ } ++ } ++ ++ stage.whenComplete(new FromFuture()); ++ ++ return ret; ++ } ++ ++ ++ public <U> Completable<U> thenApply(final Function<? super T, ? extends U> function) { ++ return this.thenApply(function, DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public <U> Completable<U> thenApply(final Function<? super T, ? extends U> function, final Function<? super Throwable, ? extends Throwable> exceptionHandler) { ++ Validate.notNull(function, "Function may not be null"); ++ Validate.notNull(exceptionHandler, "Exception handler may not be null"); ++ ++ final Completable<U> ret = new Completable<>(); ++ this.pushStackOrRun(new ApplyTransform<>(null, this, ret, exceptionHandler, function)); ++ return ret; ++ } ++ ++ public <U> Completable<U> thenApplyAsync(final Function<? super T, ? extends U> function) { ++ return this.thenApplyAsync(function, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public <U> Completable<U> thenApplyAsync(final Function<? super T, ? extends U> function, final Executor executor) { ++ return this.thenApplyAsync(function, executor, DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public <U> Completable<U> thenApplyAsync(final Function<? super T, ? extends U> function, final Executor executor, final Function<? super Throwable, ? extends Throwable> exceptionHandler) { ++ Validate.notNull(function, "Function may not be null"); ++ Validate.notNull(exceptionHandler, "Exception handler may not be null"); ++ ++ final Completable<U> ret = new Completable<>(); ++ this.pushStackOrRun(new ApplyTransform<>(checkExecutor(executor), this, ret, exceptionHandler, function)); ++ return ret; ++ } ++ ++ ++ public Completable<Void> thenAccept(final Consumer<? super T> consumer) { ++ return this.thenAccept(consumer, DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public Completable<Void> thenAccept(final Consumer<? super T> consumer, final Function<? super Throwable, ? extends Throwable> exceptionHandler) { ++ Validate.notNull(consumer, "Consumer may not be null"); ++ Validate.notNull(exceptionHandler, "Exception handler may not be null"); ++ ++ final Completable<Void> ret = new Completable<>(); ++ this.pushStackOrRun(new AcceptTransform<>(null, this, ret, exceptionHandler, consumer)); ++ return ret; ++ } ++ ++ public Completable<Void> thenAcceptAsync(final Consumer<? super T> consumer) { ++ return this.thenAcceptAsync(consumer, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public Completable<Void> thenAcceptAsync(final Consumer<? super T> consumer, final Executor executor) { ++ return this.thenAcceptAsync(consumer, executor, DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public Completable<Void> thenAcceptAsync(final Consumer<? super T> consumer, final Executor executor, final Function<? super Throwable, ? extends Throwable> exceptionHandler) { ++ Validate.notNull(consumer, "Consumer may not be null"); ++ Validate.notNull(exceptionHandler, "Exception handler may not be null"); ++ ++ final Completable<Void> ret = new Completable<>(); ++ this.pushStackOrRun(new AcceptTransform<>(checkExecutor(executor), this, ret, exceptionHandler, consumer)); ++ return ret; ++ } ++ ++ ++ public Completable<Void> thenRun(final Runnable run) { ++ return this.thenRun(run, DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public Completable<Void> thenRun(final Runnable run, final Function<? super Throwable, ? extends Throwable> exceptionHandler) { ++ Validate.notNull(run, "Run may not be null"); ++ Validate.notNull(exceptionHandler, "Exception handler may not be null"); ++ ++ final Completable<Void> ret = new Completable<>(); ++ this.pushStackOrRun(new RunTransform<>(null, this, ret, exceptionHandler, run)); ++ return ret; ++ } ++ ++ public Completable<Void> thenRunAsync(final Runnable run) { ++ return this.thenRunAsync(run, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public Completable<Void> thenRunAsync(final Runnable run, final Executor executor) { ++ return this.thenRunAsync(run, executor, DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public Completable<Void> thenRunAsync(final Runnable run, final Executor executor, final Function<? super Throwable, ? extends Throwable> exceptionHandler) { ++ Validate.notNull(run, "Run may not be null"); ++ Validate.notNull(exceptionHandler, "Exception handler may not be null"); ++ ++ final Completable<Void> ret = new Completable<>(); ++ this.pushStackOrRun(new RunTransform<>(checkExecutor(executor), this, ret, exceptionHandler, run)); ++ return ret; ++ } ++ ++ ++ public <U> Completable<U> handle(final BiFunction<? super T, ? super Throwable, ? extends U> function) { ++ return this.handle(function, DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public <U> Completable<U> handle(final BiFunction<? super T, ? super Throwable, ? extends U> function, ++ final Function<? super Throwable, ? extends Throwable> exceptionHandler) { ++ Validate.notNull(function, "Function may not be null"); ++ Validate.notNull(exceptionHandler, "Exception handler may not be null"); ++ ++ final Completable<U> ret = new Completable<>(); ++ this.pushStackOrRun(new HandleTransform<>(null, this, ret, exceptionHandler, function)); ++ return ret; ++ } ++ ++ public <U> Completable<U> handleAsync(final BiFunction<? super T, ? super Throwable, ? extends U> function) { ++ return this.handleAsync(function, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public <U> Completable<U> handleAsync(final BiFunction<? super T, ? super Throwable, ? extends U> function, ++ final Executor executor) { ++ return this.handleAsync(function, executor, DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public <U> Completable<U> handleAsync(final BiFunction<? super T, ? super Throwable, ? extends U> function, ++ final Executor executor, ++ final Function<? super Throwable, ? extends Throwable> exceptionHandler) { ++ Validate.notNull(function, "Function may not be null"); ++ Validate.notNull(exceptionHandler, "Exception handler may not be null"); ++ ++ final Completable<U> ret = new Completable<>(); ++ this.pushStackOrRun(new HandleTransform<>(checkExecutor(executor), this, ret, exceptionHandler, function)); ++ return ret; ++ } ++ ++ ++ public Completable<T> whenComplete(final BiConsumer<? super T, ? super Throwable> consumer) { ++ return this.whenComplete(consumer, DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public Completable<T> whenComplete(final BiConsumer<? super T, ? super Throwable> consumer, final Function<? super Throwable, ? extends Throwable> exceptionHandler) { ++ Validate.notNull(consumer, "Consumer may not be null"); ++ Validate.notNull(exceptionHandler, "Exception handler may not be null"); ++ ++ final Completable<T> ret = new Completable<>(); ++ this.pushStackOrRun(new WhenTransform<>(null, this, ret, exceptionHandler, consumer)); ++ return ret; ++ } ++ ++ public Completable<T> whenCompleteAsync(final BiConsumer<? super T, ? super Throwable> consumer) { ++ return this.whenCompleteAsync(consumer, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public Completable<T> whenCompleteAsync(final BiConsumer<? super T, ? super Throwable> consumer, final Executor executor) { ++ return this.whenCompleteAsync(consumer, executor, DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public Completable<T> whenCompleteAsync(final BiConsumer<? super T, ? super Throwable> consumer, final Executor executor, ++ final Function<? super Throwable, ? extends Throwable> exceptionHandler) { ++ Validate.notNull(consumer, "Consumer may not be null"); ++ Validate.notNull(exceptionHandler, "Exception handler may not be null"); ++ ++ final Completable<T> ret = new Completable<>(); ++ this.pushStackOrRun(new WhenTransform<>(checkExecutor(executor), this, ret, exceptionHandler, consumer)); ++ return ret; ++ } ++ ++ ++ public Completable<T> exceptionally(final Function<Throwable, ? extends T> function) { ++ return this.exceptionally(function, DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public Completable<T> exceptionally(final Function<Throwable, ? extends T> function, final Function<? super Throwable, ? extends Throwable> exceptionHandler) { ++ Validate.notNull(function, "Function may not be null"); ++ Validate.notNull(exceptionHandler, "Exception handler may not be null"); ++ ++ final Completable<T> ret = new Completable<>(); ++ this.pushStackOrRun(new ExceptionallyTransform<>(null, this, ret, exceptionHandler, function)); ++ return ret; ++ } ++ ++ public Completable<T> exceptionallyAsync(final Function<Throwable, ? extends T> function) { ++ return this.exceptionallyAsync(function, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public Completable<T> exceptionallyAsync(final Function<Throwable, ? extends T> function, final Executor executor) { ++ return this.exceptionallyAsync(function, executor, DEFAULT_EXCEPTION_HANDLER); ++ } ++ ++ public Completable<T> exceptionallyAsync(final Function<Throwable, ? extends T> function, final Executor executor, ++ final Function<? super Throwable, ? extends Throwable> exceptionHandler) { ++ Validate.notNull(function, "Function may not be null"); ++ Validate.notNull(exceptionHandler, "Exception handler may not be null"); ++ ++ final Completable<T> ret = new Completable<>(); ++ this.pushStackOrRun(new ExceptionallyTransform<>(checkExecutor(executor), this, ret, exceptionHandler, function)); ++ return ret; ++ } ++ ++ private static final class ExceptionResult { ++ public final Throwable ex; ++ ++ public ExceptionResult(final Throwable ex) { ++ this.ex = ex; + } + } + +- /** +- * Adds a waiter that will be completed asynchronously by the complete() calls. If complete() +- * has already been called, then invokes the consumer synchronously with the completed result. +- * @param consumer Consumer to be executed on completion +- * @throws NullPointerException If consumer is null +- * @return A cancellable which will control the execution of the specified consumer +- */ +- public Cancellable addWaiter(final BiConsumer<T, Throwable> consumer) { +- if (this.waiters.add(consumer)) { +- return new CancellableImpl(consumer); ++ private static abstract class Transform<U, T> implements Runnable, CompletableFuture.AsynchronousCompletionTask { ++ ++ private Transform<?, T> next; ++ ++ private final Executor executor; ++ protected final Completable<T> from; ++ protected final Completable<U> to; ++ protected final Function<? super Throwable, ? extends Throwable> exceptionHandler; ++ ++ protected Transform(final Executor executor, final Completable<T> from, final Completable<U> to, ++ final Function<? super Throwable, ? extends Throwable> exceptionHandler) { ++ this.executor = executor; ++ this.from = from; ++ this.to = to; ++ this.exceptionHandler = exceptionHandler; ++ } ++ ++ // force interface call to become virtual call ++ @Override ++ public abstract void run(); ++ ++ protected void failed(final Throwable throwable) { ++ Throwable complete; ++ try { ++ complete = this.exceptionHandler.apply(throwable); ++ } catch (final Throwable thr2) { ++ throwable.addSuppressed(thr2); ++ complete = throwable; ++ } ++ this.to.completeExceptionally(complete); ++ } ++ ++ public void execute() { ++ if (this.executor == null) { ++ this.run(); ++ return; ++ } ++ ++ try { ++ this.executor.execute(this); ++ } catch (final Throwable throwable) { ++ this.failed(throwable); ++ } ++ } ++ } ++ ++ private static final class ApplyTransform<U, T> extends Transform<U, T> { ++ ++ private final Function<? super T, ? extends U> function; ++ ++ public ApplyTransform(final Executor executor, final Completable<T> from, final Completable<U> to, ++ final Function<? super Throwable, ? extends Throwable> exceptionHandler, ++ final Function<? super T, ? extends U> function) { ++ super(executor, from, to, exceptionHandler); ++ this.function = function; ++ } ++ ++ @Override ++ public void run() { ++ final Object result = this.from.getResultPlain(); ++ try { ++ if (result instanceof ExceptionResult exRes) { ++ this.to.completeExceptionally(exRes.ex); ++ } else { ++ this.to.complete(this.function.apply((T)unmaskNull(result))); ++ } ++ } catch (final Throwable throwable) { ++ this.failed(throwable); ++ } + } +- this.completeWaiter(consumer, this.result, this.throwable); +- return new CancellableImpl(consumer); + } + +- public void complete(final T result) { +- this.result = result; +- this.completeAllWaiters(result, null); ++ private static final class AcceptTransform<T> extends Transform<Void, T> { ++ private final Consumer<? super T> consumer; ++ ++ public AcceptTransform(final Executor executor, final Completable<T> from, final Completable<Void> to, ++ final Function<? super Throwable, ? extends Throwable> exceptionHandler, ++ final Consumer<? super T> consumer) { ++ super(executor, from, to, exceptionHandler); ++ this.consumer = consumer; ++ } ++ ++ @Override ++ public void run() { ++ final Object result = this.from.getResultPlain(); ++ try { ++ if (result instanceof ExceptionResult exRes) { ++ this.to.completeExceptionally(exRes.ex); ++ } else { ++ this.consumer.accept((T)unmaskNull(result)); ++ this.to.complete(null); ++ } ++ } catch (final Throwable throwable) { ++ this.failed(throwable); ++ } ++ } + } + +- public void completeWithThrowable(final Throwable throwable) { +- if (throwable == null) { +- throw new NullPointerException("Throwable cannot be null"); ++ private static final class RunTransform<T> extends Transform<Void, T> { ++ private final Runnable run; ++ ++ public RunTransform(final Executor executor, final Completable<T> from, final Completable<Void> to, ++ final Function<? super Throwable, ? extends Throwable> exceptionHandler, ++ final Runnable run) { ++ super(executor, from, to, exceptionHandler); ++ this.run = run; ++ } ++ ++ @Override ++ public void run() { ++ final Object result = this.from.getResultPlain(); ++ try { ++ if (result instanceof ExceptionResult exRes) { ++ this.to.completeExceptionally(exRes.ex); ++ } else { ++ this.run.run(); ++ this.to.complete(null); ++ } ++ } catch (final Throwable throwable) { ++ this.failed(throwable); ++ } + } +- this.throwable = throwable; +- this.completeAllWaiters(null, throwable); + } + +- private final class CancellableImpl implements Cancellable { ++ private static final class HandleTransform<U, T> extends Transform<U, T> { ++ ++ private final BiFunction<? super T, ? super Throwable, ? extends U> function; ++ ++ public HandleTransform(final Executor executor, final Completable<T> from, final Completable<U> to, ++ final Function<? super Throwable, ? extends Throwable> exceptionHandler, ++ final BiFunction<? super T, ? super Throwable, ? extends U> function) { ++ super(executor, from, to, exceptionHandler); ++ this.function = function; ++ } + +- private final BiConsumer<T, Throwable> waiter; ++ @Override ++ public void run() { ++ final Object result = this.from.getResultPlain(); ++ try { ++ if (result instanceof ExceptionResult exRes) { ++ this.to.complete(this.function.apply(null, exRes.ex)); ++ } else { ++ this.to.complete(this.function.apply((T)unmaskNull(result), null)); ++ } ++ } catch (final Throwable throwable) { ++ this.failed(throwable); ++ } ++ } ++ } ++ ++ private static final class WhenTransform<T> extends Transform<T, T> { ++ ++ private final BiConsumer<? super T, ? super Throwable> consumer; ++ ++ public WhenTransform(final Executor executor, final Completable<T> from, final Completable<T> to, ++ final Function<? super Throwable, ? extends Throwable> exceptionHandler, ++ final BiConsumer<? super T, ? super Throwable> consumer) { ++ super(executor, from, to, exceptionHandler); ++ this.consumer = consumer; ++ } ++ ++ @Override ++ public void run() { ++ final Object result = this.from.getResultPlain(); ++ try { ++ if (result instanceof ExceptionResult exRes) { ++ this.consumer.accept(null, exRes.ex); ++ this.to.completeExceptionally(exRes.ex); ++ } else { ++ final T unmasked = (T)unmaskNull(result); ++ this.consumer.accept(unmasked, null); ++ this.to.complete(unmasked); ++ } ++ } catch (final Throwable throwable) { ++ this.failed(throwable); ++ } ++ } ++ } ++ ++ private static final class ExceptionallyTransform<T> extends Transform<T, T> { ++ private final Function<Throwable, ? extends T> function; ++ ++ public ExceptionallyTransform(final Executor executor, final Completable<T> from, final Completable<T> to, ++ final Function<? super Throwable, ? extends Throwable> exceptionHandler, ++ final Function<Throwable, ? extends T> function) { ++ super(executor, from, to, exceptionHandler); ++ this.function = function; ++ } ++ ++ @Override ++ public void run() { ++ final Object result = this.from.getResultPlain(); ++ try { ++ if (result instanceof ExceptionResult exRes) { ++ this.to.complete(this.function.apply(exRes.ex)); ++ } else { ++ this.to.complete((T)unmaskNull(result)); ++ } ++ } catch (final Throwable throwable) { ++ this.failed(throwable); ++ } ++ } ++ } ++ ++ private static final class UnparkTransform<T> extends Transform<Void, T> implements ForkJoinPool.ManagedBlocker { ++ ++ private volatile Thread thread; ++ ++ public UnparkTransform(final Completable<T> from, final Thread target) { ++ super(null, from, null, null); ++ this.thread = target; ++ } ++ ++ @Override ++ public void run() { ++ final Thread t = this.thread; ++ this.thread = null; ++ LockSupport.unpark(t); ++ } ++ ++ @Override ++ public boolean block() throws InterruptedException { ++ while (!this.isReleasable()) { ++ if (Thread.interrupted()) { ++ throw new InterruptedException(); ++ } ++ LockSupport.park(this); ++ } + +- private CancellableImpl(final BiConsumer<T, Throwable> waiter) { +- this.waiter = waiter; ++ return true; + } + + @Override +- public boolean cancel() { +- return Completable.this.waiters.remove(this.waiter); ++ public boolean isReleasable() { ++ return this.thread == null; + } + } + } +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java +deleted file mode 100644 +index 18d646676fd022afd64afaac30ec1bd283a73b0e..0000000000000000000000000000000000000000 +--- a/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java ++++ /dev/null +@@ -1,208 +0,0 @@ +-package ca.spottedleaf.concurrentutil.executor; +- +-import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +-import java.util.function.BooleanSupplier; +- +-/** +- * Base implementation for an abstract queue of tasks which are executed either synchronously or asynchronously. +- * +- * <p> +- * The implementation supports tracking task executions using {@link #getTotalTasksScheduled()} and +- * {@link #getTotalTasksExecuted()}, and optionally shutting down the executor using {@link #shutdown()} +- * </p> +- * +- * <p> +- * The base implementation does not provide a method to queue a task for execution, rather that is specified in +- * the specific implementation. However, it is required that a specific implementation provides a method to +- * <i>queue</i> a task or <i>create</i> a task. A <i>queued</i> task is one which will eventually be executed, +- * and a <i>created</i> task must be queued to execute via {@link BaseTask#queue()} or be executed manually via +- * {@link BaseTask#execute()}. This choice of delaying the queueing of a task may be useful to provide a task handle +- * which may be cancelled or adjusted before the actual real task logic is ready to be executed. +- * </p> +- */ +-public interface BaseExecutor { +- +- /** +- * Returns whether every task scheduled to this queue has been removed and executed or cancelled. If no tasks have been queued, +- * returns {@code true}. +- * +- * @return {@code true} if all tasks that have been queued have finished executing or no tasks have been queued, {@code false} otherwise. +- */ +- public default boolean haveAllTasksExecuted() { +- // order is important +- // if new tasks are scheduled between the reading of these variables, scheduled is guaranteed to be higher - +- // so our check fails, and we try again +- final long completed = this.getTotalTasksExecuted(); +- final long scheduled = this.getTotalTasksScheduled(); +- +- return completed == scheduled; +- } +- +- /** +- * Returns the number of tasks that have been scheduled or execute or are pending to be scheduled. +- */ +- public long getTotalTasksScheduled(); +- +- /** +- * Returns the number of tasks that have fully been executed. +- */ +- public long getTotalTasksExecuted(); +- +- /** +- * Waits until this queue has had all of its tasks executed (NOT removed). See {@link #haveAllTasksExecuted()} +- * <p> +- * This call is most effective after a {@link #shutdown()} call, as the shutdown call guarantees no tasks can +- * be executed and the waitUntilAllExecuted call makes sure the queue is empty. Effectively, using shutdown then using +- * waitUntilAllExecuted ensures this queue is empty - and most importantly, will remain empty. +- * </p> +- * <p> +- * This method is not guaranteed to be immediately responsive to queue state, so calls may take significantly more +- * time than expected. Effectively, do not rely on this call being fast - even if there are few tasks scheduled. +- * </p> +- * <p> +- * Note: Interruptions to the the current thread have no effect. Interrupt status is also not affected by this call. +- * </p> +- * +- * @throws IllegalStateException If the current thread is not allowed to wait +- */ +- public default void waitUntilAllExecuted() throws IllegalStateException { +- long failures = 1L; // start at 0.25ms +- +- while (!this.haveAllTasksExecuted()) { +- Thread.yield(); +- failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms +- } +- } +- +- /** +- * Executes the next available task. +- * +- * @return {@code true} if a task was executed, {@code false} otherwise +- * @throws IllegalStateException If the current thread is not allowed to execute a task +- */ +- public boolean executeTask() throws IllegalStateException; +- +- /** +- * Executes all queued tasks. +- * +- * @return {@code true} if a task was executed, {@code false} otherwise +- * @throws IllegalStateException If the current thread is not allowed to execute a task +- */ +- public default boolean executeAll() { +- if (!this.executeTask()) { +- return false; +- } +- +- while (this.executeTask()); +- +- return true; +- } +- +- /** +- * Waits and executes tasks until the condition returns {@code true}. +- * <p> +- * WARNING: This function is <i>not</i> suitable for waiting until a deadline! +- * Use {@link #executeUntil(long)} or {@link #executeConditionally(BooleanSupplier, long)} instead. +- * </p> +- */ +- public default void executeConditionally(final BooleanSupplier condition) { +- long failures = 0; +- while (!condition.getAsBoolean()) { +- if (this.executeTask()) { +- failures = failures >>> 2; +- } else { +- failures = ConcurrentUtil.linearLongBackoff(failures, 100_000L, 10_000_000L); // 100us, 10ms +- } +- } +- } +- +- /** +- * Waits and executes tasks until the condition returns {@code true} or {@code System.nanoTime() - deadline >= 0}. +- */ +- public default void executeConditionally(final BooleanSupplier condition, final long deadline) { +- long failures = 0; +- // double check deadline; we don't know how expensive the condition is +- while ((System.nanoTime() - deadline < 0L) && !condition.getAsBoolean() && (System.nanoTime() - deadline < 0L)) { +- if (this.executeTask()) { +- failures = failures >>> 2; +- } else { +- failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms +- } +- } +- } +- +- /** +- * Waits and executes tasks until {@code System.nanoTime() - deadline >= 0}. +- */ +- public default void executeUntil(final long deadline) { +- long failures = 0; +- while (System.nanoTime() - deadline < 0L) { +- if (this.executeTask()) { +- failures = failures >>> 2; +- } else { +- failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms +- } +- } +- } +- +- /** +- * Prevent further additions to this queue. Attempts to add after this call has completed (potentially during) will +- * result in {@link IllegalStateException} being thrown. +- * <p> +- * This operation is atomic with respect to other shutdown calls +- * </p> +- * <p> +- * After this call has completed, regardless of return value, this queue will be shutdown. +- * </p> +- * +- * @return {@code true} if the queue was shutdown, {@code false} if it has shut down already +- * @throws UnsupportedOperationException If this queue does not support shutdown +- * @see #isShutdown() +- */ +- public default boolean shutdown() throws UnsupportedOperationException { +- throw new UnsupportedOperationException(); +- } +- +- /** +- * Returns whether this queue has shut down. Effectively, whether new tasks will be rejected - this method +- * does not indicate whether all the tasks scheduled have been executed. +- * @return Returns whether this queue has shut down. +- * @see #waitUntilAllExecuted() +- */ +- public default boolean isShutdown() { +- return false; +- } +- +- /** +- * Task object returned for any {@link BaseExecutor} scheduled task. +- * @see BaseExecutor +- */ +- public static interface BaseTask extends Cancellable { +- +- /** +- * Causes a lazily queued task to become queued or executed +- * +- * @throws IllegalStateException If the backing queue has shutdown +- * @return {@code true} If the task was queued, {@code false} if the task was already queued/cancelled/executed +- */ +- public boolean queue(); +- +- /** +- * Forces this task to be marked as completed. +- * +- * @return {@code true} if the task was cancelled, {@code false} if the task has already completed or is being completed. +- */ +- @Override +- public boolean cancel(); +- +- /** +- * Executes this task. This will also mark the task as completing. +- * <p> +- * Exceptions thrown from the runnable will be rethrown. +- * </p> +- * +- * @return {@code true} if this task was executed, {@code false} if it was already marked as completed. +- */ +- public boolean execute(); +- } +-} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/PrioritisedExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/PrioritisedExecutor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..17cbaee1e89bd3f6d905e640d20d0119ab0570a0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/PrioritisedExecutor.java +@@ -0,0 +1,271 @@ ++package ca.spottedleaf.concurrentutil.executor; ++ ++import ca.spottedleaf.concurrentutil.util.Priority; ++ ++public interface PrioritisedExecutor { ++ ++ /** ++ * Returns the number of tasks that have been scheduled are pending to be scheduled. ++ */ ++ public long getTotalTasksScheduled(); ++ ++ /** ++ * Returns the number of tasks that have been executed. ++ */ ++ public long getTotalTasksExecuted(); ++ ++ /** ++ * Generates the next suborder id. ++ * @return The next suborder id. ++ */ ++ public long generateNextSubOrder(); ++ ++ /** ++ * Executes the next available task. ++ * <p> ++ * If there is a task with priority {@link Priority#BLOCKING} available, then that such task is executed. ++ * </p> ++ * <p> ++ * If there is a task with priority {@link Priority#IDLE} available then that task is only executed ++ * when there are no other tasks available with a higher priority. ++ * </p> ++ * <p> ++ * If there are no tasks that have priority {@link Priority#BLOCKING} or {@link Priority#IDLE}, then ++ * this function will be biased to execute tasks that have higher priorities. ++ * </p> ++ * ++ * @return {@code true} if a task was executed, {@code false} otherwise ++ * @throws IllegalStateException If the current thread is not allowed to execute a task ++ */ ++ public boolean executeTask() throws IllegalStateException; ++ ++ /** ++ * Prevent further additions to this executor. Attempts to add after this call has completed (potentially during) will ++ * result in {@link IllegalStateException} being thrown. ++ * <p> ++ * This operation is atomic with respect to other shutdown calls ++ * </p> ++ * <p> ++ * After this call has completed, regardless of return value, this executor will be shutdown. ++ * </p> ++ * ++ * @return {@code true} if the executor was shutdown, {@code false} if it has shut down already ++ * @see #isShutdown() ++ */ ++ public boolean shutdown(); ++ ++ /** ++ * Returns whether this executor has shut down. Effectively, returns whether new tasks will be rejected. ++ * This method does not indicate whether all the tasks scheduled have been executed. ++ * @return Returns whether this executor has shut down. ++ */ ++ public boolean isShutdown(); ++ ++ /** ++ * Queues or executes a task at {@link Priority#NORMAL} priority. ++ * @param task The task to run. ++ * ++ * @throws IllegalStateException If this executor has shutdown. ++ * @throws NullPointerException If the task is null ++ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task ++ * associated with the parameter ++ */ ++ public PrioritisedTask queueTask(final Runnable task); ++ ++ /** ++ * Queues or executes a task. ++ * ++ * @param task The task to run. ++ * @param priority The priority for the task. ++ * ++ * @throws IllegalStateException If this executor has shutdown. ++ * @throws NullPointerException If the task is null ++ * @throws IllegalArgumentException If the priority is invalid. ++ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task ++ * associated with the parameter ++ */ ++ public PrioritisedTask queueTask(final Runnable task, final Priority priority); ++ ++ /** ++ * Queues or executes a task. ++ * ++ * @param task The task to run. ++ * @param priority The priority for the task. ++ * @param subOrder The task's suborder. ++ * ++ * @throws IllegalStateException If this executor has shutdown. ++ * @throws NullPointerException If the task is null ++ * @throws IllegalArgumentException If the priority is invalid. ++ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task ++ * associated with the parameter ++ */ ++ public PrioritisedTask queueTask(final Runnable task, final Priority priority, final long subOrder); ++ ++ /** ++ * Creates, but does not queue or execute, a task at {@link Priority#NORMAL} priority. ++ * @param task The task to run. ++ * ++ * @throws NullPointerException If the task is null ++ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task ++ * associated with the parameter ++ */ ++ public PrioritisedTask createTask(final Runnable task); ++ ++ /** ++ * Creates, but does not queue or execute, a task at {@link Priority#NORMAL} priority. ++ * ++ * @param task The task to run. ++ * @param priority The priority for the task. ++ * ++ * @throws NullPointerException If the task is null ++ * @throws IllegalArgumentException If the priority is invalid. ++ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task ++ * associated with the parameter ++ */ ++ public PrioritisedTask createTask(final Runnable task, final Priority priority); ++ ++ /** ++ * Creates, but does not queue or execute, a task at {@link Priority#NORMAL} priority. ++ * ++ * @param task The task to run. ++ * @param priority The priority for the task. ++ * @param subOrder The task's suborder. ++ * ++ * @throws NullPointerException If the task is null ++ * @throws IllegalArgumentException If the priority is invalid. ++ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task ++ * associated with the parameter ++ */ ++ public PrioritisedTask createTask(final Runnable task, final Priority priority, final long subOrder); ++ ++ public static interface PrioritisedTask extends Cancellable { ++ ++ /** ++ * Returns the executor associated with this task. ++ * @return The executor associated with this task. ++ */ ++ public PrioritisedExecutor getExecutor(); ++ ++ /** ++ * Causes a lazily queued task to become queued or executed ++ * ++ * @throws IllegalStateException If the backing executor has shutdown ++ * @return {@code true} If the task was queued, {@code false} if the task was already queued/cancelled/executed ++ */ ++ public boolean queue(); ++ ++ /** ++ * Returns whether this task has been queued and is not completing. ++ * @return {@code true} If the task has been queued, {@code false} if the task has not been queued or is marked ++ * as completing. ++ */ ++ public boolean isQueued(); ++ ++ /** ++ * Forces this task to be marked as completed. ++ * ++ * @return {@code true} if the task was cancelled, {@code false} if the task has already completed ++ * or is being completed. ++ */ ++ @Override ++ public boolean cancel(); ++ ++ /** ++ * Executes this task. This will also mark the task as completing. ++ * <p> ++ * Exceptions thrown from the runnable will be rethrown. ++ * </p> ++ * ++ * @return {@code true} if this task was executed, {@code false} if it was already marked as completed. ++ */ ++ public boolean execute(); ++ ++ /** ++ * Returns the current priority. Note that {@link Priority#COMPLETING} will be returned ++ * if this task is completing or has completed. ++ */ ++ public Priority getPriority(); ++ ++ /** ++ * Attempts to set this task's priority level to the level specified. ++ * ++ * @param priority Specified priority level. ++ * ++ * @throws IllegalArgumentException If the priority is invalid ++ * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue ++ * this task was scheduled on was shutdown, or if the priority was already at the specified level. ++ */ ++ public boolean setPriority(final Priority priority); ++ ++ /** ++ * Attempts to raise the priority to the priority level specified. ++ * ++ * @param priority Priority specified ++ * ++ * @throws IllegalArgumentException If the priority is invalid ++ * @return {@code false} if the current task is completing, {@code true} if the priority was raised to the ++ * specified level or was already at the specified level or higher. ++ */ ++ public boolean raisePriority(final Priority priority); ++ ++ /** ++ * Attempts to lower the priority to the priority level specified. ++ * ++ * @param priority Priority specified ++ * ++ * @throws IllegalArgumentException If the priority is invalid ++ * @return {@code false} if the current task is completing, {@code true} if the priority was lowered to the ++ * specified level or was already at the specified level or lower. ++ */ ++ public boolean lowerPriority(final Priority priority); ++ ++ /** ++ * Returns the suborder id associated with this task. ++ * @return The suborder id associated with this task. ++ */ ++ public long getSubOrder(); ++ ++ /** ++ * Sets the suborder id associated with this task. Ths function has no effect when this task ++ * is completing or is completed. ++ * ++ * @param subOrder Specified new sub order. ++ * ++ * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue ++ * this task was scheduled on was shutdown, or if the current suborder is the same as the new sub order. ++ */ ++ public boolean setSubOrder(final long subOrder); ++ ++ /** ++ * Attempts to raise the suborder to the suborder specified. ++ * ++ * @param subOrder Specified new sub order. ++ * ++ * @return {@code false} if the current task is completing, {@code true} if the suborder was raised to the ++ * specified suborder or was already at the specified suborder or higher. ++ */ ++ public boolean raiseSubOrder(final long subOrder); ++ ++ /** ++ * Attempts to lower the suborder to the suborder specified. ++ * ++ * @param subOrder Specified new sub order. ++ * ++ * @return {@code false} if the current task is completing, {@code true} if the suborder was lowered to the ++ * specified suborder or was already at the specified suborder or lower. ++ */ ++ public boolean lowerSubOrder(final long subOrder); ++ ++ /** ++ * Sets the priority and suborder id associated with this task. Ths function has no effect when this task ++ * is completing or is completed. ++ * ++ * @param priority Priority specified ++ * @param subOrder Specified new sub order. ++ * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue ++ * this task was scheduled on was shutdown, or if the current priority and suborder are the same as ++ * the parameters. ++ */ ++ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/queue/PrioritisedTaskQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/queue/PrioritisedTaskQueue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..edb8c6611bdc9aced2714b963e00bbb7829603d2 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/queue/PrioritisedTaskQueue.java +@@ -0,0 +1,454 @@ ++package ca.spottedleaf.concurrentutil.executor.queue; ++ ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import java.lang.invoke.VarHandle; ++import java.util.Comparator; ++import java.util.Map; ++import java.util.concurrent.ConcurrentSkipListMap; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicLong; ++ ++public final class PrioritisedTaskQueue implements PrioritisedExecutor { ++ ++ /** ++ * Required for tie-breaking in the queue ++ */ ++ private final AtomicLong taskIdGenerator = new AtomicLong(); ++ private final AtomicLong scheduledTasks = new AtomicLong(); ++ private final AtomicLong executedTasks = new AtomicLong(); ++ private final AtomicLong subOrderGenerator = new AtomicLong(); ++ private final AtomicBoolean shutdown = new AtomicBoolean(); ++ private final ConcurrentSkipListMap<PrioritisedQueuedTask.Holder, Boolean> tasks = new ConcurrentSkipListMap<>(PrioritisedQueuedTask.COMPARATOR); ++ ++ @Override ++ public long getTotalTasksScheduled() { ++ return this.scheduledTasks.get(); ++ } ++ ++ @Override ++ public long getTotalTasksExecuted() { ++ return this.executedTasks.get(); ++ } ++ ++ @Override ++ public long generateNextSubOrder() { ++ return this.subOrderGenerator.getAndIncrement(); ++ } ++ ++ @Override ++ public boolean shutdown() { ++ return !this.shutdown.getAndSet(true); ++ } ++ ++ @Override ++ public boolean isShutdown() { ++ return this.shutdown.get(); ++ } ++ ++ public PrioritisedTask peekFirst() { ++ final Map.Entry<PrioritisedQueuedTask.Holder, Boolean> firstEntry = this.tasks.firstEntry(); ++ return firstEntry == null ? null : firstEntry.getKey().task; ++ } ++ ++ public Priority getHighestPriority() { ++ final Map.Entry<PrioritisedQueuedTask.Holder, Boolean> firstEntry = this.tasks.firstEntry(); ++ return firstEntry == null ? null : Priority.getPriority(firstEntry.getKey().priority); ++ } ++ ++ public boolean hasNoScheduledTasks() { ++ final long executedTasks = this.executedTasks.get(); ++ final long scheduledTasks = this.scheduledTasks.get(); ++ ++ return executedTasks == scheduledTasks; ++ } ++ ++ public PrioritySubOrderPair getHighestPrioritySubOrder() { ++ final Map.Entry<PrioritisedQueuedTask.Holder, Boolean> firstEntry = this.tasks.firstEntry(); ++ if (firstEntry == null) { ++ return null; ++ } ++ ++ final PrioritisedQueuedTask.Holder holder = firstEntry.getKey(); ++ ++ return new PrioritySubOrderPair(Priority.getPriority(holder.priority), holder.subOrder); ++ } ++ ++ public Runnable pollTask() { ++ for (;;) { ++ final Map.Entry<PrioritisedQueuedTask.Holder, Boolean> firstEntry = this.tasks.pollFirstEntry(); ++ if (firstEntry != null) { ++ final PrioritisedQueuedTask.Holder task = firstEntry.getKey(); ++ task.markRemoved(); ++ if (!task.task.cancel()) { ++ continue; ++ } ++ return task.task.execute; ++ } ++ ++ return null; ++ } ++ } ++ ++ @Override ++ public boolean executeTask() { ++ for (;;) { ++ final Map.Entry<PrioritisedQueuedTask.Holder, Boolean> firstEntry = this.tasks.pollFirstEntry(); ++ if (firstEntry != null) { ++ final PrioritisedQueuedTask.Holder task = firstEntry.getKey(); ++ task.markRemoved(); ++ if (!task.task.execute()) { ++ continue; ++ } ++ return true; ++ } ++ ++ return false; ++ } ++ } ++ ++ @Override ++ public PrioritisedTask createTask(final Runnable task) { ++ return this.createTask(task, Priority.NORMAL, this.generateNextSubOrder()); ++ } ++ ++ @Override ++ public PrioritisedTask createTask(final Runnable task, final Priority priority) { ++ return this.createTask(task, priority, this.generateNextSubOrder()); ++ } ++ ++ @Override ++ public PrioritisedTask createTask(final Runnable task, final Priority priority, final long subOrder) { ++ return new PrioritisedQueuedTask(task, priority, subOrder); ++ } ++ ++ @Override ++ public PrioritisedTask queueTask(final Runnable task) { ++ return this.queueTask(task, Priority.NORMAL, this.generateNextSubOrder()); ++ } ++ ++ @Override ++ public PrioritisedTask queueTask(final Runnable task, final Priority priority) { ++ return this.queueTask(task, priority, this.generateNextSubOrder()); ++ } ++ ++ @Override ++ public PrioritisedTask queueTask(final Runnable task, final Priority priority, final long subOrder) { ++ final PrioritisedQueuedTask ret = new PrioritisedQueuedTask(task, priority, subOrder); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ private final class PrioritisedQueuedTask implements PrioritisedExecutor.PrioritisedTask { ++ public static final Comparator<PrioritisedQueuedTask.Holder> COMPARATOR = (final PrioritisedQueuedTask.Holder t1, final PrioritisedQueuedTask.Holder t2) -> { ++ final int priorityCompare = t1.priority - t2.priority; ++ if (priorityCompare != 0) { ++ return priorityCompare; ++ } ++ ++ final int subOrderCompare = Long.compare(t1.subOrder, t2.subOrder); ++ if (subOrderCompare != 0) { ++ return subOrderCompare; ++ } ++ ++ return Long.compare(t1.id, t2.id); ++ }; ++ ++ private static final class Holder { ++ private final PrioritisedQueuedTask task; ++ private final int priority; ++ private final long subOrder; ++ private final long id; ++ ++ private volatile boolean removed; ++ private static final VarHandle REMOVED_HANDLE = ConcurrentUtil.getVarHandle(Holder.class, "removed", boolean.class); ++ ++ private Holder(final PrioritisedQueuedTask task, final int priority, final long subOrder, ++ final long id) { ++ this.task = task; ++ this.priority = priority; ++ this.subOrder = subOrder; ++ this.id = id; ++ } ++ ++ /** ++ * Returns true if marked as removed ++ */ ++ public boolean markRemoved() { ++ return !(boolean)REMOVED_HANDLE.getAndSet((Holder)this, (boolean)true); ++ } ++ } ++ ++ private final long id; ++ private final Runnable execute; ++ ++ private Priority priority; ++ private long subOrder; ++ private Holder holder; ++ ++ public PrioritisedQueuedTask(final Runnable execute, final Priority priority, final long subOrder) { ++ if (!Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ this.execute = execute; ++ this.priority = priority; ++ this.subOrder = subOrder; ++ this.id = PrioritisedTaskQueue.this.taskIdGenerator.getAndIncrement(); ++ } ++ ++ @Override ++ public PrioritisedExecutor getExecutor() { ++ return PrioritisedTaskQueue.this; ++ } ++ ++ @Override ++ public boolean queue() { ++ synchronized (this) { ++ if (this.holder != null || this.priority == Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (PrioritisedTaskQueue.this.isShutdown()) { ++ throw new IllegalStateException("Queue is shutdown"); ++ } ++ ++ final Holder holder = new Holder(this, this.priority.priority, this.subOrder, this.id); ++ this.holder = holder; ++ ++ PrioritisedTaskQueue.this.scheduledTasks.getAndIncrement(); ++ PrioritisedTaskQueue.this.tasks.put(holder, Boolean.TRUE); ++ } ++ ++ if (PrioritisedTaskQueue.this.isShutdown()) { ++ this.cancel(); ++ throw new IllegalStateException("Queue is shutdown"); ++ } ++ ++ ++ return true; ++ } ++ ++ @Override ++ public boolean isQueued() { ++ synchronized (this) { ++ return this.holder != null && this.priority != Priority.COMPLETING; ++ } ++ } ++ ++ @Override ++ public boolean cancel() { ++ synchronized (this) { ++ if (this.priority == Priority.COMPLETING) { ++ return false; ++ } ++ ++ this.priority = Priority.COMPLETING; ++ ++ if (this.holder != null) { ++ if (this.holder.markRemoved()) { ++ PrioritisedTaskQueue.this.tasks.remove(this.holder); ++ } ++ PrioritisedTaskQueue.this.executedTasks.getAndIncrement(); ++ } ++ ++ return true; ++ } ++ } ++ ++ @Override ++ public boolean execute() { ++ final boolean increaseExecuted; ++ ++ synchronized (this) { ++ if (this.priority == Priority.COMPLETING) { ++ return false; ++ } ++ ++ this.priority = Priority.COMPLETING; ++ ++ if (increaseExecuted = (this.holder != null)) { ++ if (this.holder.markRemoved()) { ++ PrioritisedTaskQueue.this.tasks.remove(this.holder); ++ } ++ } ++ } ++ ++ try { ++ this.execute.run(); ++ return true; ++ } finally { ++ if (increaseExecuted) { ++ PrioritisedTaskQueue.this.executedTasks.getAndIncrement(); ++ } ++ } ++ } ++ ++ @Override ++ public Priority getPriority() { ++ synchronized (this) { ++ return this.priority; ++ } ++ } ++ ++ @Override ++ public boolean setPriority(final Priority priority) { ++ synchronized (this) { ++ if (this.priority == Priority.COMPLETING || this.priority == priority) { ++ return false; ++ } ++ ++ this.priority = priority; ++ ++ if (this.holder != null) { ++ if (this.holder.markRemoved()) { ++ PrioritisedTaskQueue.this.tasks.remove(this.holder); ++ } ++ this.holder = new Holder(this, priority.priority, this.subOrder, this.id); ++ PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE); ++ } ++ ++ return true; ++ } ++ } ++ ++ @Override ++ public boolean raisePriority(final Priority priority) { ++ synchronized (this) { ++ if (this.priority == Priority.COMPLETING || this.priority.isHigherOrEqualPriority(priority)) { ++ return false; ++ } ++ ++ this.priority = priority; ++ ++ if (this.holder != null) { ++ if (this.holder.markRemoved()) { ++ PrioritisedTaskQueue.this.tasks.remove(this.holder); ++ } ++ this.holder = new Holder(this, priority.priority, this.subOrder, this.id); ++ PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE); ++ } ++ ++ return true; ++ } ++ } ++ ++ @Override ++ public boolean lowerPriority(Priority priority) { ++ synchronized (this) { ++ if (this.priority == Priority.COMPLETING || this.priority.isLowerOrEqualPriority(priority)) { ++ return false; ++ } ++ ++ this.priority = priority; ++ ++ if (this.holder != null) { ++ if (this.holder.markRemoved()) { ++ PrioritisedTaskQueue.this.tasks.remove(this.holder); ++ } ++ this.holder = new Holder(this, priority.priority, this.subOrder, this.id); ++ PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE); ++ } ++ ++ return true; ++ } ++ } ++ ++ @Override ++ public long getSubOrder() { ++ synchronized (this) { ++ return this.subOrder; ++ } ++ } ++ ++ @Override ++ public boolean setSubOrder(final long subOrder) { ++ synchronized (this) { ++ if (this.priority == Priority.COMPLETING || this.subOrder == subOrder) { ++ return false; ++ } ++ ++ this.subOrder = subOrder; ++ ++ if (this.holder != null) { ++ if (this.holder.markRemoved()) { ++ PrioritisedTaskQueue.this.tasks.remove(this.holder); ++ } ++ this.holder = new Holder(this, priority.priority, this.subOrder, this.id); ++ PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE); ++ } ++ ++ return true; ++ } ++ } ++ ++ @Override ++ public boolean raiseSubOrder(long subOrder) { ++ synchronized (this) { ++ if (this.priority == Priority.COMPLETING || this.subOrder >= subOrder) { ++ return false; ++ } ++ ++ this.subOrder = subOrder; ++ ++ if (this.holder != null) { ++ if (this.holder.markRemoved()) { ++ PrioritisedTaskQueue.this.tasks.remove(this.holder); ++ } ++ this.holder = new Holder(this, priority.priority, this.subOrder, this.id); ++ PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE); ++ } ++ ++ return true; ++ } ++ } ++ ++ @Override ++ public boolean lowerSubOrder(final long subOrder) { ++ synchronized (this) { ++ if (this.priority == Priority.COMPLETING || this.subOrder <= subOrder) { ++ return false; ++ } ++ ++ this.subOrder = subOrder; ++ ++ if (this.holder != null) { ++ if (this.holder.markRemoved()) { ++ PrioritisedTaskQueue.this.tasks.remove(this.holder); ++ } ++ this.holder = new Holder(this, priority.priority, this.subOrder, this.id); ++ PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE); ++ } ++ ++ return true; ++ } ++ } ++ ++ @Override ++ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) { ++ synchronized (this) { ++ if (this.priority == Priority.COMPLETING || (this.priority == priority && this.subOrder == subOrder)) { ++ return false; ++ } ++ ++ this.priority = priority; ++ this.subOrder = subOrder; ++ ++ if (this.holder != null) { ++ if (this.holder.markRemoved()) { ++ PrioritisedTaskQueue.this.tasks.remove(this.holder); ++ } ++ this.holder = new Holder(this, priority.priority, this.subOrder, this.id); ++ PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE); ++ } ++ ++ return true; ++ } ++ } ++ } ++ ++ public static record PrioritySubOrderPair(Priority priority, long subOrder) {} ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java +deleted file mode 100644 +index 3ce10053d4ec51855ad7012abb5d97df1c0e557a..0000000000000000000000000000000000000000 +--- a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java ++++ /dev/null +@@ -1,170 +0,0 @@ +-package ca.spottedleaf.concurrentutil.executor.standard; +- +-import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +-import java.lang.invoke.VarHandle; +- +-public class DelayedPrioritisedTask { +- +- protected volatile int priority; +- protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "priority", int.class); +- +- protected static final int PRIORITY_SET = Integer.MIN_VALUE >>> 0; +- +- protected final int getPriorityVolatile() { +- return (int)PRIORITY_HANDLE.getVolatile((DelayedPrioritisedTask)this); +- } +- +- protected final int compareAndExchangePriorityVolatile(final int expect, final int update) { +- return (int)PRIORITY_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (int)expect, (int)update); +- } +- +- protected final int getAndOrPriorityVolatile(final int val) { +- return (int)PRIORITY_HANDLE.getAndBitwiseOr((DelayedPrioritisedTask)this, (int)val); +- } +- +- protected final void setPriorityPlain(final int val) { +- PRIORITY_HANDLE.set((DelayedPrioritisedTask)this, (int)val); +- } +- +- protected volatile PrioritisedExecutor.PrioritisedTask task; +- protected static final VarHandle TASK_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "task", PrioritisedExecutor.PrioritisedTask.class); +- +- protected PrioritisedExecutor.PrioritisedTask getTaskPlain() { +- return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.get((DelayedPrioritisedTask)this); +- } +- +- protected PrioritisedExecutor.PrioritisedTask getTaskVolatile() { +- return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.getVolatile((DelayedPrioritisedTask)this); +- } +- +- protected final PrioritisedExecutor.PrioritisedTask compareAndExchangeTaskVolatile(final PrioritisedExecutor.PrioritisedTask expect, final PrioritisedExecutor.PrioritisedTask update) { +- return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (PrioritisedExecutor.PrioritisedTask)expect, (PrioritisedExecutor.PrioritisedTask)update); +- } +- +- public DelayedPrioritisedTask(final PrioritisedExecutor.Priority priority) { +- this.setPriorityPlain(priority.priority); +- } +- +- // only public for debugging +- public int getPriorityInternal() { +- return this.getPriorityVolatile(); +- } +- +- public PrioritisedExecutor.PrioritisedTask getTask() { +- return this.getTaskVolatile(); +- } +- +- public void setTask(final PrioritisedExecutor.PrioritisedTask task) { +- int priority = this.getPriorityVolatile(); +- +- if (this.compareAndExchangeTaskVolatile(null, task) != null) { +- throw new IllegalStateException("setTask() called twice"); +- } +- +- int failures = 0; +- for (;;) { +- task.setPriority(PrioritisedExecutor.Priority.getPriority(priority)); +- +- if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SET))) { +- return; +- } +- +- ++failures; +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- } +- } +- +- public PrioritisedExecutor.Priority getPriority() { +- final int priority = this.getPriorityVolatile(); +- if ((priority & PRIORITY_SET) != 0) { +- return this.task.getPriority(); +- } +- +- return PrioritisedExecutor.Priority.getPriority(priority); +- } +- +- public void raisePriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { +- throw new IllegalArgumentException("Invalid priority " + priority); +- } +- +- int failures = 0; +- for (int curr = this.getPriorityVolatile();;) { +- if ((curr & PRIORITY_SET) != 0) { +- this.getTaskPlain().raisePriority(priority); +- return; +- } +- +- if (!priority.isLowerPriority(curr)) { +- return; +- } +- +- if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { +- return; +- } +- +- // failed, retry +- +- ++failures; +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- } +- } +- +- public void setPriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { +- throw new IllegalArgumentException("Invalid priority " + priority); +- } +- +- int failures = 0; +- for (int curr = this.getPriorityVolatile();;) { +- if ((curr & PRIORITY_SET) != 0) { +- this.getTaskPlain().setPriority(priority); +- return; +- } +- +- if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { +- return; +- } +- +- // failed, retry +- +- ++failures; +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- } +- } +- +- public void lowerPriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { +- throw new IllegalArgumentException("Invalid priority " + priority); +- } +- +- int failures = 0; +- for (int curr = this.getPriorityVolatile();;) { +- if ((curr & PRIORITY_SET) != 0) { +- this.getTaskPlain().lowerPriority(priority); +- return; +- } +- +- if (!priority.isHigherPriority(curr)) { +- return; +- } +- +- if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { +- return; +- } +- +- // failed, retry +- +- ++failures; +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- } +- } +-} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java +deleted file mode 100644 +index 91beb6f23f257cf265fe3150f760892e605f217a..0000000000000000000000000000000000000000 +--- a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java ++++ /dev/null +@@ -1,276 +0,0 @@ +-package ca.spottedleaf.concurrentutil.executor.standard; +- +-import ca.spottedleaf.concurrentutil.executor.BaseExecutor; +- +-/** +- * Implementation of {@link BaseExecutor} which schedules tasks to be executed by a given priority. +- * @see BaseExecutor +- */ +-public interface PrioritisedExecutor extends BaseExecutor { +- +- public static enum Priority { +- +- /** +- * Priority value indicating the task has completed or is being completed. +- * This priority cannot be used to schedule tasks. +- */ +- COMPLETING(-1), +- +- /** +- * Absolute highest priority, should only be used for when a task is blocking a time-critical thread. +- */ +- BLOCKING(), +- +- /** +- * Should only be used for urgent but not time-critical tasks. +- */ +- HIGHEST(), +- +- /** +- * Two priorities above normal. +- */ +- HIGHER(), +- +- /** +- * One priority above normal. +- */ +- HIGH(), +- +- /** +- * Default priority. +- */ +- NORMAL(), +- +- /** +- * One priority below normal. +- */ +- LOW(), +- +- /** +- * Two priorities below normal. +- */ +- LOWER(), +- +- /** +- * Use for tasks that should eventually execute, but are not needed to. +- */ +- LOWEST(), +- +- /** +- * Use for tasks that can be delayed indefinitely. +- */ +- IDLE(); +- +- // returns whether the priority can be scheduled +- public static boolean isValidPriority(final Priority priority) { +- return priority != null && priority != Priority.COMPLETING; +- } +- +- // returns the higher priority of the two +- public static Priority max(final Priority p1, final Priority p2) { +- return p1.isHigherOrEqualPriority(p2) ? p1 : p2; +- } +- +- // returns the lower priroity of the two +- public static Priority min(final Priority p1, final Priority p2) { +- return p1.isLowerOrEqualPriority(p2) ? p1 : p2; +- } +- +- public boolean isHigherOrEqualPriority(final Priority than) { +- return this.priority <= than.priority; +- } +- +- public boolean isHigherPriority(final Priority than) { +- return this.priority < than.priority; +- } +- +- public boolean isLowerOrEqualPriority(final Priority than) { +- return this.priority >= than.priority; +- } +- +- public boolean isLowerPriority(final Priority than) { +- return this.priority > than.priority; +- } +- +- public boolean isHigherOrEqualPriority(final int than) { +- return this.priority <= than; +- } +- +- public boolean isHigherPriority(final int than) { +- return this.priority < than; +- } +- +- public boolean isLowerOrEqualPriority(final int than) { +- return this.priority >= than; +- } +- +- public boolean isLowerPriority(final int than) { +- return this.priority > than; +- } +- +- public static boolean isHigherOrEqualPriority(final int priority, final int than) { +- return priority <= than; +- } +- +- public static boolean isHigherPriority(final int priority, final int than) { +- return priority < than; +- } +- +- public static boolean isLowerOrEqualPriority(final int priority, final int than) { +- return priority >= than; +- } +- +- public static boolean isLowerPriority(final int priority, final int than) { +- return priority > than; +- } +- +- static final Priority[] PRIORITIES = Priority.values(); +- +- /** includes special priorities */ +- public static final int TOTAL_PRIORITIES = PRIORITIES.length; +- +- public static final int TOTAL_SCHEDULABLE_PRIORITIES = TOTAL_PRIORITIES - 1; +- +- public static Priority getPriority(final int priority) { +- return PRIORITIES[priority + 1]; +- } +- +- private static int priorityCounter; +- +- private static int nextCounter() { +- return priorityCounter++; +- } +- +- public final int priority; +- +- Priority() { +- this(nextCounter()); +- } +- +- Priority(final int priority) { +- this.priority = priority; +- } +- } +- +- /** +- * Executes the next available task. +- * <p> +- * If there is a task with priority {@link PrioritisedExecutor.Priority#BLOCKING} available, then that such task is executed. +- * </p> +- * <p> +- * If there is a task with priority {@link PrioritisedExecutor.Priority#IDLE} available then that task is only executed +- * when there are no other tasks available with a higher priority. +- * </p> +- * <p> +- * If there are no tasks that have priority {@link PrioritisedExecutor.Priority#BLOCKING} or {@link PrioritisedExecutor.Priority#IDLE}, then +- * this function will be biased to execute tasks that have higher priorities. +- * </p> +- * +- * @return {@code true} if a task was executed, {@code false} otherwise +- * @throws IllegalStateException If the current thread is not allowed to execute a task +- */ +- @Override +- public boolean executeTask() throws IllegalStateException; +- +- /** +- * Queues or executes a task at {@link Priority#NORMAL} priority. +- * @param task The task to run. +- * +- * @throws IllegalStateException If this queue has shutdown. +- * @throws NullPointerException If the task is null +- * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task +- * associated with the parameter +- */ +- public default PrioritisedTask queueRunnable(final Runnable task) { +- return this.queueRunnable(task, Priority.NORMAL); +- } +- +- /** +- * Queues or executes a task. +- * +- * @param task The task to run. +- * @param priority The priority for the task. +- * +- * @throws IllegalStateException If this queue has shutdown. +- * @throws NullPointerException If the task is null +- * @throws IllegalArgumentException If the priority is invalid. +- * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task +- * associated with the parameter +- */ +- public PrioritisedTask queueRunnable(final Runnable task, final Priority priority); +- +- /** +- * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseTask#queue()}. +- * +- * @param task The task to run. +- * +- * @throws IllegalStateException If this queue has shutdown. +- * @throws NullPointerException If the task is null +- * @throws IllegalArgumentException If the priority is invalid. +- * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks +- * @return The prioritised task associated with the parameters +- */ +- public default PrioritisedTask createTask(final Runnable task) { +- return this.createTask(task, Priority.NORMAL); +- } +- +- /** +- * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseTask#queue()}. +- * +- * @param task The task to run. +- * @param priority The priority for the task. +- * +- * @throws IllegalStateException If this queue has shutdown. +- * @throws NullPointerException If the task is null +- * @throws IllegalArgumentException If the priority is invalid. +- * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks +- * @return The prioritised task associated with the parameters +- */ +- public PrioritisedTask createTask(final Runnable task, final Priority priority); +- +- /** +- * Extension of {@link ca.spottedleaf.concurrentutil.executor.BaseExecutor.BaseTask} which adds functions +- * to retrieve and modify the task's associated priority. +- * +- * @see ca.spottedleaf.concurrentutil.executor.BaseExecutor.BaseTask +- */ +- public static interface PrioritisedTask extends BaseTask { +- +- /** +- * Returns the current priority. Note that {@link Priority#COMPLETING} will be returned +- * if this task is completing or has completed. +- */ +- public Priority getPriority(); +- +- /** +- * Attempts to set this task's priority level to the level specified. +- * +- * @param priority Specified priority level. +- * +- * @throws IllegalArgumentException If the priority is invalid +- * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue +- * this task was scheduled on was shutdown, or if the priority was already at the specified level. +- */ +- public boolean setPriority(final Priority priority); +- +- /** +- * Attempts to raise the priority to the priority level specified. +- * +- * @param priority Priority specified +- * +- * @throws IllegalArgumentException If the priority is invalid +- * @return {@code false} if the current task is completing, {@code true} if the priority was raised to the specified level or was already at the specified level or higher. +- */ +- public boolean raisePriority(final Priority priority); +- +- /** +- * Attempts to lower the priority to the priority level specified. +- * +- * @param priority Priority specified +- * +- * @throws IllegalArgumentException If the priority is invalid +- * @return {@code false} if the current task is completing, {@code true} if the priority was lowered to the specified level or was already at the specified level or lower. +- */ +- public boolean lowerPriority(final Priority priority); +- } +-} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java +deleted file mode 100644 +index 2ba36e29d0d8693f2f5e6c6d195ca27f2a5099aa..0000000000000000000000000000000000000000 +--- a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java ++++ /dev/null +@@ -1,632 +0,0 @@ +-package ca.spottedleaf.concurrentutil.executor.standard; +- +-import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +-import org.slf4j.Logger; +-import org.slf4j.LoggerFactory; +-import java.util.ArrayList; +-import java.util.Arrays; +-import java.util.Comparator; +-import java.util.TreeSet; +-import java.util.concurrent.atomic.AtomicBoolean; +-import java.util.function.BiConsumer; +- +-public final class PrioritisedThreadPool { +- +- private static final Logger LOGGER = LoggerFactory.getLogger(PrioritisedThreadPool.class); +- +- private final PrioritisedThread[] threads; +- private final TreeSet<PrioritisedPoolExecutorImpl> queues = new TreeSet<>(PrioritisedPoolExecutorImpl.comparator()); +- private final String name; +- private final long queueMaxHoldTime; +- +- private final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> nonShutdownQueues = new ReferenceOpenHashSet<>(); +- private final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> activeQueues = new ReferenceOpenHashSet<>(); +- +- private boolean shutdown; +- +- private long schedulingIdGenerator; +- +- private static final long DEFAULT_QUEUE_HOLD_TIME = (long)(5.0e6); +- +- /** +- * @param name Specified debug name of this thread pool +- * @param threads The number of threads to use +- */ +- public PrioritisedThreadPool(final String name, final int threads) { +- this(name, threads, null); +- } +- +- /** +- * @param name Specified debug name of this thread pool +- * @param threads The number of threads to use +- * @param threadModifier Invoked for each created thread with its incremental id before starting them +- */ +- public PrioritisedThreadPool(final String name, final int threads, final BiConsumer<Thread, Integer> threadModifier) { +- this(name, threads, threadModifier, DEFAULT_QUEUE_HOLD_TIME); // 5ms +- } +- +- /** +- * @param name Specified debug name of this thread pool +- * @param threads The number of threads to use +- * @param threadModifier Invoked for each created thread with its incremental id before starting them +- * @param queueHoldTime The maximum amount of time to spend executing tasks in a specific queue before attempting +- * to switch to another queue, per thread +- */ +- public PrioritisedThreadPool(final String name, final int threads, final BiConsumer<Thread, Integer> threadModifier, +- final long queueHoldTime) { // in ns +- if (threads <= 0) { +- throw new IllegalArgumentException("Thread count must be > 0, not " + threads); +- } +- if (name == null) { +- throw new IllegalArgumentException("Name cannot be null"); +- } +- this.name = name; +- this.queueMaxHoldTime = queueHoldTime; +- +- this.threads = new PrioritisedThread[threads]; +- for (int i = 0; i < threads; ++i) { +- this.threads[i] = new PrioritisedThread(this); +- +- // set default attributes +- this.threads[i].setName("Prioritised thread for pool '" + name + "' #" + i); +- this.threads[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> { +- LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable); +- }); +- +- // let thread modifier override defaults +- if (threadModifier != null) { +- threadModifier.accept(this.threads[i], Integer.valueOf(i)); +- } +- +- // now the thread can start +- this.threads[i].start(); +- } +- } +- +- /** +- * Returns an array representing the threads backing this thread pool. +- */ +- public Thread[] getThreads() { +- return Arrays.copyOf(this.threads, this.threads.length, Thread[].class); +- } +- +- /** +- * Creates and returns a {@link PrioritisedPoolExecutor} to schedule tasks onto. The returned executor will execute +- * tasks on this thread pool only. +- * @param name The debug name of the executor. +- * @param minParallelism The minimum number of threads to be executing tasks from the returned executor +- * before threads may be allocated to other queues in this thread pool. +- * @param parallelism The maximum number of threads which may be executing tasks from the returned executor. +- * @throws IllegalStateException If this thread pool is shut down +- */ +- public PrioritisedPoolExecutor createExecutor(final String name, final int minParallelism, final int parallelism) { +- synchronized (this.nonShutdownQueues) { +- if (this.shutdown) { +- throw new IllegalStateException("Queue is shutdown: " + this.toString()); +- } +- final PrioritisedPoolExecutorImpl ret = new PrioritisedPoolExecutorImpl( +- this, name, +- Math.min(Math.max(1, parallelism), this.threads.length), +- Math.min(Math.max(0, minParallelism), this.threads.length) +- ); +- +- this.nonShutdownQueues.add(ret); +- +- synchronized (this.activeQueues) { +- this.activeQueues.add(ret); +- } +- +- return ret; +- } +- } +- +- /** +- * Prevents creation of new queues, shutdowns all non-shutdown queues if specified +- */ +- public void halt(final boolean shutdownQueues) { +- synchronized (this.nonShutdownQueues) { +- this.shutdown = true; +- } +- if (shutdownQueues) { +- final ArrayList<PrioritisedPoolExecutorImpl> queuesToShutdown; +- synchronized (this.nonShutdownQueues) { +- this.shutdown = true; +- queuesToShutdown = new ArrayList<>(this.nonShutdownQueues); +- } +- +- for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) { +- queue.shutdown(); +- } +- } +- +- +- for (final PrioritisedThread thread : this.threads) { +- // can't kill queue, queue is null +- thread.halt(false); +- } +- } +- +- /** +- * Waits until all threads in this pool have shutdown, or until the specified time has passed. +- * @param msToWait Maximum time to wait. +- * @return {@code false} if the maximum time passed, {@code true} otherwise. +- */ +- public boolean join(final long msToWait) { +- try { +- return this.join(msToWait, false); +- } catch (final InterruptedException ex) { +- throw new IllegalStateException(ex); +- } +- } +- +- /** +- * Waits until all threads in this pool have shutdown, or until the specified time has passed. +- * @param msToWait Maximum time to wait. +- * @return {@code false} if the maximum time passed, {@code true} otherwise. +- * @throws InterruptedException If this thread is interrupted. +- */ +- public boolean joinInterruptable(final long msToWait) throws InterruptedException { +- return this.join(msToWait, true); +- } +- +- protected final boolean join(final long msToWait, final boolean interruptable) throws InterruptedException { +- final long nsToWait = msToWait * (1000 * 1000); +- final long start = System.nanoTime(); +- final long deadline = start + nsToWait; +- boolean interrupted = false; +- try { +- for (final PrioritisedThread thread : this.threads) { +- for (;;) { +- if (!thread.isAlive()) { +- break; +- } +- final long current = System.nanoTime(); +- if (current >= deadline) { +- return false; +- } +- +- try { +- thread.join(Math.max(1L, (deadline - current) / (1000 * 1000))); +- } catch (final InterruptedException ex) { +- if (interruptable) { +- throw ex; +- } +- interrupted = true; +- } +- } +- } +- +- return true; +- } finally { +- if (interrupted) { +- Thread.currentThread().interrupt(); +- } +- } +- } +- +- /** +- * Shuts down this thread pool, optionally waiting for all tasks to be executed. +- * This function will invoke {@link PrioritisedPoolExecutor#shutdown()} on all created executors on this +- * thread pool. +- * @param wait Whether to wait for tasks to be executed +- */ +- public void shutdown(final boolean wait) { +- final ArrayList<PrioritisedPoolExecutorImpl> queuesToShutdown; +- synchronized (this.nonShutdownQueues) { +- this.shutdown = true; +- queuesToShutdown = new ArrayList<>(this.nonShutdownQueues); +- } +- +- for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) { +- queue.shutdown(); +- } +- +- for (final PrioritisedThread thread : this.threads) { +- // none of these can be true or else NPE +- thread.close(false, false); +- } +- +- if (wait) { +- final ArrayList<PrioritisedPoolExecutorImpl> queues; +- synchronized (this.activeQueues) { +- queues = new ArrayList<>(this.activeQueues); +- } +- for (final PrioritisedPoolExecutorImpl queue : queues) { +- queue.waitUntilAllExecuted(); +- } +- } +- } +- +- protected static final class PrioritisedThread extends PrioritisedQueueExecutorThread { +- +- protected final PrioritisedThreadPool pool; +- protected final AtomicBoolean alertedHighPriority = new AtomicBoolean(); +- +- public PrioritisedThread(final PrioritisedThreadPool pool) { +- super(null); +- this.pool = pool; +- } +- +- public boolean alertHighPriorityExecutor() { +- if (!this.notifyTasks()) { +- if (!this.alertedHighPriority.get()) { +- this.alertedHighPriority.set(true); +- } +- return false; +- } +- +- return true; +- } +- +- private boolean isAlertedHighPriority() { +- return this.alertedHighPriority.get() && this.alertedHighPriority.getAndSet(false); +- } +- +- @Override +- protected boolean pollTasks() { +- final PrioritisedThreadPool pool = this.pool; +- final TreeSet<PrioritisedPoolExecutorImpl> queues = this.pool.queues; +- +- boolean ret = false; +- for (;;) { +- if (this.halted) { +- break; +- } +- // try to find a queue +- // note that if and ONLY IF the queues set is empty, this means there are no tasks for us to execute. +- // so we can only break when it's empty +- final PrioritisedPoolExecutorImpl queue; +- // select queue +- synchronized (queues) { +- queue = queues.pollFirst(); +- if (queue == null) { +- // no tasks to execute +- break; +- } +- +- queue.schedulingId = ++pool.schedulingIdGenerator; +- // we own this queue now, so increment the executor count +- // do we also need to push this queue up for grabs for another executor? +- if (++queue.concurrentExecutors < queue.maximumExecutors) { +- // re-add to queues +- // it's very important this is done in the same synchronised block for polling, as this prevents +- // us from possibly later adding a queue that should not exist in the set +- queues.add(queue); +- queue.isQueued = true; +- } else { +- queue.isQueued = false; +- } +- // note: we cannot drain entries from the queue while holding this lock, as it will cause deadlock +- // the queue addition holds the per-queue lock first then acquires the lock we have now, but if we +- // try to poll now we don't hold the per queue lock but we do hold the global lock... +- } +- +- // parse tasks as long as we are allowed +- final long start = System.nanoTime(); +- final long deadline = start + pool.queueMaxHoldTime; +- do { +- try { +- if (this.halted) { +- break; +- } +- if (!queue.executeTask()) { +- // no more tasks, try next queue +- break; +- } +- ret = true; +- } catch (final ThreadDeath death) { +- throw death; // goodbye world... +- } catch (final Throwable throwable) { +- LOGGER.error("Exception thrown from thread '" + this.getName() + "' in queue '" + queue.toString() + "'", throwable); +- } +- } while (!this.isAlertedHighPriority() && System.nanoTime() <= deadline); +- +- synchronized (queues) { +- // decrement executors, we are no longer executing +- if (queue.isQueued) { +- queues.remove(queue); +- queue.isQueued = false; +- } +- if (--queue.concurrentExecutors == 0 && queue.scheduledPriority == null) { +- // reset scheduling id once the queue is empty again +- // this will ensure empty queues are not prioritised suddenly over active queues once tasks are +- // queued +- queue.schedulingId = 0L; +- } +- +- // ensure the executor is queued for execution again +- if (!queue.isHalted && queue.scheduledPriority != null) { // make sure it actually has tasks +- queues.add(queue); +- queue.isQueued = true; +- } +- } +- } +- +- return ret; +- } +- } +- +- public interface PrioritisedPoolExecutor extends PrioritisedExecutor { +- +- /** +- * Removes this queue from the thread pool without shutting the queue down or waiting for queued tasks to be executed +- */ +- public void halt(); +- +- /** +- * Returns whether this executor is scheduled to run tasks or is running tasks, otherwise it returns whether +- * this queue is not halted and not shutdown. +- */ +- public boolean isActive(); +- } +- +- protected static final class PrioritisedPoolExecutorImpl extends PrioritisedThreadedTaskQueue implements PrioritisedPoolExecutor { +- +- protected final PrioritisedThreadPool pool; +- protected final long[] priorityCounts = new long[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; +- protected long schedulingId; +- protected int concurrentExecutors; +- protected Priority scheduledPriority; +- +- protected final String name; +- protected final int maximumExecutors; +- protected final int minimumExecutors; +- protected boolean isQueued; +- +- public PrioritisedPoolExecutorImpl(final PrioritisedThreadPool pool, final String name, final int maximumExecutors, final int minimumExecutors) { +- this.pool = pool; +- this.name = name; +- this.maximumExecutors = maximumExecutors; +- this.minimumExecutors = minimumExecutors; +- } +- +- public static Comparator<PrioritisedPoolExecutorImpl> comparator() { +- return (final PrioritisedPoolExecutorImpl p1, final PrioritisedPoolExecutorImpl p2) -> { +- if (p1 == p2) { +- return 0; +- } +- +- final int belowMin1 = p1.minimumExecutors - p1.concurrentExecutors; +- final int belowMin2 = p2.minimumExecutors - p2.concurrentExecutors; +- +- // test minimum executors +- if (belowMin1 > 0 || belowMin2 > 0) { +- // want the largest belowMin to be first +- final int minCompare = Integer.compare(belowMin2, belowMin1); +- +- if (minCompare != 0) { +- return minCompare; +- } +- } +- +- // prefer higher priority +- final int priorityCompare = p1.scheduledPriority.ordinal() - p2.scheduledPriority.ordinal(); +- if (priorityCompare != 0) { +- return priorityCompare; +- } +- +- // try to spread out the executors so that each can have threads executing +- final int executorCompare = p1.concurrentExecutors - p2.concurrentExecutors; +- if (executorCompare != 0) { +- return executorCompare; +- } +- +- // if all else fails here we just choose whichever executor was queued first +- return Long.compare(p1.schedulingId, p2.schedulingId); +- }; +- } +- +- private boolean isHalted; +- +- @Override +- public void halt() { +- final PrioritisedThreadPool pool = this.pool; +- final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues; +- synchronized (queues) { +- if (this.isHalted) { +- return; +- } +- this.isHalted = true; +- if (this.isQueued) { +- queues.remove(this); +- this.isQueued = false; +- } +- } +- synchronized (pool.nonShutdownQueues) { +- pool.nonShutdownQueues.remove(this); +- } +- synchronized (pool.activeQueues) { +- pool.activeQueues.remove(this); +- } +- } +- +- @Override +- public boolean isActive() { +- final PrioritisedThreadPool pool = this.pool; +- final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues; +- +- synchronized (queues) { +- if (this.concurrentExecutors != 0) { +- return true; +- } +- synchronized (pool.activeQueues) { +- if (pool.activeQueues.contains(this)) { +- return true; +- } +- } +- } +- +- return false; +- } +- +- private long totalQueuedTasks = 0L; +- +- @Override +- protected void priorityChange(final PrioritisedThreadedTaskQueue.PrioritisedTask task, final Priority from, final Priority to) { +- // Note: The superclass' queue lock is ALWAYS held when inside this method. So we do NOT need to do any additional synchronisation +- // for accessing this queue's state. +- final long[] priorityCounts = this.priorityCounts; +- final boolean shutdown = this.isShutdown(); +- +- if (from == null && to == Priority.COMPLETING) { +- throw new IllegalStateException("Cannot complete task without queueing it first"); +- } +- +- // we should only notify for queueing of tasks, not changing priorities +- final boolean shouldNotifyTasks = from == null; +- +- final Priority scheduledPriority = this.scheduledPriority; +- if (from != null) { +- --priorityCounts[from.priority]; +- } +- if (to != Priority.COMPLETING) { +- ++priorityCounts[to.priority]; +- } +- final long totalQueuedTasks; +- if (to == Priority.COMPLETING) { +- totalQueuedTasks = --this.totalQueuedTasks; +- } else if (from == null) { +- totalQueuedTasks = ++this.totalQueuedTasks; +- } else { +- totalQueuedTasks = this.totalQueuedTasks; +- } +- +- // find new highest priority +- int highest = Math.min(to == Priority.COMPLETING ? Priority.IDLE.priority : to.priority, scheduledPriority == null ? Priority.IDLE.priority : scheduledPriority.priority); +- int lowestPriority = priorityCounts.length; // exclusive +- for (;highest < lowestPriority; ++highest) { +- final long count = priorityCounts[highest]; +- if (count < 0) { +- throw new IllegalStateException("Priority " + highest + " has " + count + " scheduled tasks"); +- } +- +- if (count != 0) { +- break; +- } +- } +- +- final Priority newPriority; +- if (highest == lowestPriority) { +- // no tasks left +- newPriority = null; +- } else if (shutdown) { +- // whichever is lower, the actual greatest priority or simply HIGHEST +- // this is so shutdown automatically gets priority +- newPriority = Priority.getPriority(Math.min(highest, Priority.HIGHEST.priority)); +- } else { +- newPriority = Priority.getPriority(highest); +- } +- +- final int executorsWanted; +- boolean shouldNotifyHighPriority = false; +- +- final PrioritisedThreadPool pool = this.pool; +- final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues; +- +- synchronized (queues) { +- if (!this.isQueued) { +- // see if we need to be queued +- if (newPriority != null) { +- if (this.schedulingId == 0L) { +- this.schedulingId = ++pool.schedulingIdGenerator; +- } +- this.scheduledPriority = newPriority; // must be updated before queue add +- if (!this.isHalted && this.concurrentExecutors < this.maximumExecutors) { +- shouldNotifyHighPriority = newPriority.isHigherOrEqualPriority(Priority.HIGH); +- queues.add(this); +- this.isQueued = true; +- } +- } else { +- // do not queue +- this.scheduledPriority = null; +- } +- } else { +- // see if we need to NOT be queued +- if (newPriority == null) { +- queues.remove(this); +- this.scheduledPriority = null; +- this.isQueued = false; +- } else if (scheduledPriority != newPriority) { +- // if our priority changed, we need to update it - which means removing and re-adding into the queue +- queues.remove(this); +- // only now can we update scheduledPriority, since we are no longer in queue +- this.scheduledPriority = newPriority; +- queues.add(this); +- shouldNotifyHighPriority = (scheduledPriority == null || scheduledPriority.isLowerPriority(Priority.HIGH)) && newPriority.isHigherOrEqualPriority(Priority.HIGH); +- } +- } +- +- if (this.isQueued) { +- executorsWanted = Math.min(this.maximumExecutors - this.concurrentExecutors, (int)totalQueuedTasks); +- } else { +- executorsWanted = 0; +- } +- } +- +- if (newPriority == null && shutdown) { +- synchronized (pool.activeQueues) { +- pool.activeQueues.remove(this); +- } +- } +- +- // Wake up the number of executors we want +- if (executorsWanted > 0 || (shouldNotifyTasks | shouldNotifyHighPriority)) { +- int notified = 0; +- for (final PrioritisedThread thread : pool.threads) { +- if ((shouldNotifyHighPriority ? thread.alertHighPriorityExecutor() : thread.notifyTasks()) +- && (++notified >= executorsWanted)) { +- break; +- } +- } +- } +- } +- +- @Override +- public boolean shutdown() { +- final boolean ret = super.shutdown(); +- if (!ret) { +- return ret; +- } +- +- final PrioritisedThreadPool pool = this.pool; +- +- // remove from active queues +- synchronized (pool.nonShutdownQueues) { +- pool.nonShutdownQueues.remove(this); +- } +- +- final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues; +- +- // try and shift around our priority +- synchronized (queues) { +- if (this.scheduledPriority == null) { +- // no tasks are queued, ensure we aren't in activeQueues +- synchronized (pool.activeQueues) { +- pool.activeQueues.remove(this); +- } +- +- return ret; +- } +- +- // try to set scheduled priority to HIGHEST so it drains faster +- +- if (this.scheduledPriority.isHigherOrEqualPriority(Priority.HIGHEST)) { +- // already at target priority (highest or above) +- return ret; +- } +- +- // shift priority to HIGHEST +- +- if (this.isQueued) { +- queues.remove(this); +- this.scheduledPriority = Priority.HIGHEST; +- queues.add(this); +- } else { +- this.scheduledPriority = Priority.HIGHEST; +- } +- } +- +- return ret; +- } +- } +-} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java +deleted file mode 100644 +index 3e8401b1b1f833c4f01bc87059a2f48d761d989f..0000000000000000000000000000000000000000 +--- a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java ++++ /dev/null +@@ -1,378 +0,0 @@ +-package ca.spottedleaf.concurrentutil.executor.standard; +- +-import java.util.ArrayDeque; +-import java.util.concurrent.atomic.AtomicLong; +- +-public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor { +- +- protected final ArrayDeque<PrioritisedTask>[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; { +- for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) { +- this.queues[i] = new ArrayDeque<>(); +- } +- } +- +- // Use AtomicLong to separate from the queue field, we don't want false sharing here. +- protected final AtomicLong totalScheduledTasks = new AtomicLong(); +- protected final AtomicLong totalCompletedTasks = new AtomicLong(); +- +- // this is here to prevent failures to queue stalling flush() calls (as the schedule calls would increment totalScheduledTasks without this check) +- protected volatile boolean hasShutdown; +- +- protected long taskIdGenerator = 0; +- +- @Override +- public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final Priority priority) throws IllegalStateException, IllegalArgumentException { +- if (!Priority.isValidPriority(priority)) { +- throw new IllegalArgumentException("Priority " + priority + " is invalid"); +- } +- if (task == null) { +- throw new NullPointerException("Task cannot be null"); +- } +- +- if (this.hasShutdown) { +- // prevent us from stalling flush() calls by incrementing scheduled tasks when we really didn't schedule something +- throw new IllegalStateException("Queue has shutdown"); +- } +- +- final PrioritisedTask ret; +- +- synchronized (this.queues) { +- if (this.hasShutdown) { +- throw new IllegalStateException("Queue has shutdown"); +- } +- this.getAndAddTotalScheduledTasksVolatile(1L); +- +- ret = new PrioritisedTask(this.taskIdGenerator++, task, priority, this); +- +- this.queues[ret.priority.priority].add(ret); +- +- // call priority change callback (note: only after we successfully queue!) +- this.priorityChange(ret, null, priority); +- } +- +- return ret; +- } +- +- @Override +- public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final Priority priority) { +- if (!Priority.isValidPriority(priority)) { +- throw new IllegalArgumentException("Priority " + priority + " is invalid"); +- } +- if (task == null) { +- throw new NullPointerException("Task cannot be null"); +- } +- +- return new PrioritisedTask(task, priority, this); +- } +- +- @Override +- public long getTotalTasksScheduled() { +- return this.totalScheduledTasks.get(); +- } +- +- @Override +- public long getTotalTasksExecuted() { +- return this.totalCompletedTasks.get(); +- } +- +- // callback method for subclasses to override +- // from is null when a task is immediately created +- protected void priorityChange(final PrioritisedTask task, final Priority from, final Priority to) {} +- +- /** +- * Polls the highest priority task currently available. {@code null} if none. This will mark the +- * returned task as completed. +- */ +- protected PrioritisedTask poll() { +- return this.poll(Priority.IDLE); +- } +- +- protected PrioritisedTask poll(final Priority minPriority) { +- final ArrayDeque<PrioritisedTask>[] queues = this.queues; +- synchronized (queues) { +- final int max = minPriority.priority; +- for (int i = 0; i <= max; ++i) { +- final ArrayDeque<PrioritisedTask> queue = queues[i]; +- PrioritisedTask task; +- while ((task = queue.pollFirst()) != null) { +- if (task.trySetCompleting(i)) { +- return task; +- } +- } +- } +- } +- +- return null; +- } +- +- /** +- * Polls and executes the highest priority task currently available. Exceptions thrown during task execution will +- * be rethrown. +- * @return {@code true} if a task was executed, {@code false} otherwise. +- */ +- @Override +- public boolean executeTask() { +- final PrioritisedTask task = this.poll(); +- +- if (task != null) { +- task.executeInternal(); +- return true; +- } +- +- return false; +- } +- +- @Override +- public boolean shutdown() { +- synchronized (this.queues) { +- if (this.hasShutdown) { +- return false; +- } +- this.hasShutdown = true; +- } +- return true; +- } +- +- @Override +- public boolean isShutdown() { +- return this.hasShutdown; +- } +- +- /* totalScheduledTasks */ +- +- protected final long getTotalScheduledTasksVolatile() { +- return this.totalScheduledTasks.get(); +- } +- +- protected final long getAndAddTotalScheduledTasksVolatile(final long value) { +- return this.totalScheduledTasks.getAndAdd(value); +- } +- +- /* totalCompletedTasks */ +- +- protected final long getTotalCompletedTasksVolatile() { +- return this.totalCompletedTasks.get(); +- } +- +- protected final long getAndAddTotalCompletedTasksVolatile(final long value) { +- return this.totalCompletedTasks.getAndAdd(value); +- } +- +- protected static final class PrioritisedTask implements PrioritisedExecutor.PrioritisedTask { +- protected final PrioritisedThreadedTaskQueue queue; +- protected long id; +- protected static final long NOT_SCHEDULED_ID = -1L; +- +- protected Runnable runnable; +- protected volatile Priority priority; +- +- protected PrioritisedTask(final long id, final Runnable runnable, final Priority priority, final PrioritisedThreadedTaskQueue queue) { +- if (!Priority.isValidPriority(priority)) { +- throw new IllegalArgumentException("Invalid priority " + priority); +- } +- +- this.priority = priority; +- this.runnable = runnable; +- this.queue = queue; +- this.id = id; +- } +- +- protected PrioritisedTask(final Runnable runnable, final Priority priority, final PrioritisedThreadedTaskQueue queue) { +- if (!Priority.isValidPriority(priority)) { +- throw new IllegalArgumentException("Invalid priority " + priority); +- } +- +- this.priority = priority; +- this.runnable = runnable; +- this.queue = queue; +- this.id = NOT_SCHEDULED_ID; +- } +- +- @Override +- public boolean queue() { +- if (this.queue.hasShutdown) { +- throw new IllegalStateException("Queue has shutdown"); +- } +- +- synchronized (this.queue.queues) { +- if (this.queue.hasShutdown) { +- throw new IllegalStateException("Queue has shutdown"); +- } +- +- final Priority priority = this.priority; +- if (priority == Priority.COMPLETING) { +- return false; +- } +- +- if (this.id != NOT_SCHEDULED_ID) { +- return false; +- } +- +- this.queue.getAndAddTotalScheduledTasksVolatile(1L); +- this.id = this.queue.taskIdGenerator++; +- this.queue.queues[priority.priority].add(this); +- +- this.queue.priorityChange(this, null, priority); +- +- return true; +- } +- } +- +- protected boolean trySetCompleting(final int minPriority) { +- final Priority oldPriority = this.priority; +- if (oldPriority != Priority.COMPLETING && oldPriority.isHigherOrEqualPriority(minPriority)) { +- this.priority = Priority.COMPLETING; +- if (this.id != NOT_SCHEDULED_ID) { +- this.queue.priorityChange(this, oldPriority, Priority.COMPLETING); +- } +- return true; +- } +- +- return false; +- } +- +- @Override +- public Priority getPriority() { +- return this.priority; +- } +- +- @Override +- public boolean setPriority(final Priority priority) { +- if (!Priority.isValidPriority(priority)) { +- throw new IllegalArgumentException("Invalid priority " + priority); +- } +- synchronized (this.queue.queues) { +- final Priority curr = this.priority; +- +- if (curr == Priority.COMPLETING) { +- return false; +- } +- +- if (curr == priority) { +- return true; +- } +- +- this.priority = priority; +- if (this.id != NOT_SCHEDULED_ID) { +- this.queue.queues[priority.priority].add(this); +- +- // call priority change callback +- this.queue.priorityChange(this, curr, priority); +- } +- } +- +- return true; +- } +- +- @Override +- public boolean raisePriority(final Priority priority) { +- if (!Priority.isValidPriority(priority)) { +- throw new IllegalArgumentException("Invalid priority " + priority); +- } +- +- synchronized (this.queue.queues) { +- final Priority curr = this.priority; +- +- if (curr == Priority.COMPLETING) { +- return false; +- } +- +- if (curr.isHigherOrEqualPriority(priority)) { +- return true; +- } +- +- this.priority = priority; +- if (this.id != NOT_SCHEDULED_ID) { +- this.queue.queues[priority.priority].add(this); +- +- // call priority change callback +- this.queue.priorityChange(this, curr, priority); +- } +- } +- +- return true; +- } +- +- @Override +- public boolean lowerPriority(final Priority priority) { +- if (!Priority.isValidPriority(priority)) { +- throw new IllegalArgumentException("Invalid priority " + priority); +- } +- +- synchronized (this.queue.queues) { +- final Priority curr = this.priority; +- +- if (curr == Priority.COMPLETING) { +- return false; +- } +- +- if (curr.isLowerOrEqualPriority(priority)) { +- return true; +- } +- +- this.priority = priority; +- if (this.id != NOT_SCHEDULED_ID) { +- this.queue.queues[priority.priority].add(this); +- +- // call priority change callback +- this.queue.priorityChange(this, curr, priority); +- } +- } +- +- return true; +- } +- +- @Override +- public boolean cancel() { +- final long id; +- synchronized (this.queue.queues) { +- final Priority oldPriority = this.priority; +- if (oldPriority == Priority.COMPLETING) { +- return false; +- } +- +- this.priority = Priority.COMPLETING; +- // call priority change callback +- if ((id = this.id) != NOT_SCHEDULED_ID) { +- this.queue.priorityChange(this, oldPriority, Priority.COMPLETING); +- } +- } +- this.runnable = null; +- if (id != NOT_SCHEDULED_ID) { +- this.queue.getAndAddTotalCompletedTasksVolatile(1L); +- } +- return true; +- } +- +- protected void executeInternal() { +- try { +- final Runnable execute = this.runnable; +- this.runnable = null; +- execute.run(); +- } finally { +- if (this.id != NOT_SCHEDULED_ID) { +- this.queue.getAndAddTotalCompletedTasksVolatile(1L); +- } +- } +- } +- +- @Override +- public boolean execute() { +- synchronized (this.queue.queues) { +- final Priority oldPriority = this.priority; +- if (oldPriority == Priority.COMPLETING) { +- return false; +- } +- +- this.priority = Priority.COMPLETING; +- // call priority change callback +- if (this.id != NOT_SCHEDULED_ID) { +- this.queue.priorityChange(this, oldPriority, Priority.COMPLETING); +- } +- } +- +- this.executeInternal(); +- return true; +- } +- } +-} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedQueueExecutorThread.java +similarity index 60% +rename from src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java +rename to src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedQueueExecutorThread.java +index d1683ba6350e530373944f98192c0f2baf241e70..f5367a13aaa02f0f929813c00a67e6ac7c8652cb 100644 +--- a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java ++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedQueueExecutorThread.java +@@ -1,6 +1,8 @@ +-package ca.spottedleaf.concurrentutil.executor.standard; ++package ca.spottedleaf.concurrentutil.executor.thread; + ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; + import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + import java.lang.invoke.VarHandle; +@@ -11,8 +13,7 @@ import java.util.concurrent.locks.LockSupport; + * <p> + * Note: When using this thread, queue additions to the underlying {@link #queue} are not sufficient to get this thread + * to execute the task. The function {@link #notifyTasks()} must be used after scheduling a task. For expected behaviour +- * of task scheduling (thread wakes up after tasks are scheduled), use the methods provided on {@link PrioritisedExecutor} +- * methods. ++ * of task scheduling, use the methods provided on this class to schedule tasks. + * </p> + */ + public class PrioritisedQueueExecutorThread extends Thread implements PrioritisedExecutor { +@@ -30,7 +31,7 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise + + protected final long spinWaitTime; + +- static final long DEFAULT_SPINWAIT_TIME = (long)(0.1e6);// 0.1ms ++ protected static final long DEFAULT_SPINWAIT_TIME = (long)(0.1e6);// 0.1ms + + public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue) { + this(queue, DEFAULT_SPINWAIT_TIME); // 0.1ms +@@ -42,7 +43,16 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise + } + + @Override +- public void run() { ++ public final void run() { ++ try { ++ this.begin(); ++ this.doRun(); ++ } finally { ++ this.die(); ++ } ++ } ++ ++ public final void doRun() { + final long spinWaitTime = this.spinWaitTime; + + main_loop: +@@ -80,7 +90,7 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise + this.setThreadParkedVolatile(true); + + // We need to parse here to avoid a race condition where a thread queues a task before we set parked to true +- // (i.e it will not notify us) ++ // (i.e. it will not notify us) + if (this.pollTasks()) { + this.setThreadParkedVolatile(false); + continue; +@@ -99,6 +109,10 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise + } + } + ++ protected void begin() {} ++ ++ protected void die() {} ++ + /** + * Attempts to poll as many tasks as possible, returning when finished. + * @return Whether any tasks were executed. +@@ -115,8 +129,6 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise + break; + } + ret = true; +- } catch (final ThreadDeath death) { +- throw death; // goodbye world... + } catch (final Throwable throwable) { + LOGGER.error("Exception thrown from prioritized runnable task in thread '" + this.getName() + "'", throwable); + } +@@ -146,95 +158,86 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise + } + + @Override +- public PrioritisedTask createTask(final Runnable task, final Priority priority) { +- final PrioritisedTask queueTask = this.queue.createTask(task, priority); +- +- // need to override queue() to notify us of tasks +- return new PrioritisedTask() { +- @Override +- public Priority getPriority() { +- return queueTask.getPriority(); +- } +- +- @Override +- public boolean setPriority(final Priority priority) { +- return queueTask.setPriority(priority); +- } ++ public long getTotalTasksExecuted() { ++ return this.queue.getTotalTasksExecuted(); ++ } + +- @Override +- public boolean raisePriority(final Priority priority) { +- return queueTask.raisePriority(priority); +- } ++ @Override ++ public long getTotalTasksScheduled() { ++ return this.queue.getTotalTasksScheduled(); ++ } + +- @Override +- public boolean lowerPriority(final Priority priority) { +- return queueTask.lowerPriority(priority); +- } ++ @Override ++ public long generateNextSubOrder() { ++ return this.queue.generateNextSubOrder(); ++ } + +- @Override +- public boolean queue() { +- final boolean ret = queueTask.queue(); +- if (ret) { +- PrioritisedQueueExecutorThread.this.notifyTasks(); +- } +- return ret; +- } ++ @Override ++ public boolean shutdown() { ++ throw new UnsupportedOperationException(); ++ } + +- @Override +- public boolean cancel() { +- return queueTask.cancel(); +- } ++ @Override ++ public boolean isShutdown() { ++ return false; ++ } + +- @Override +- public boolean execute() { +- return queueTask.execute(); +- } +- }; ++ /** ++ * {@inheritDoc} ++ * @throws IllegalStateException Always ++ */ ++ @Override ++ public boolean executeTask() throws IllegalStateException { ++ throw new IllegalStateException(); + } + + @Override +- public PrioritisedTask queueRunnable(final Runnable task, final Priority priority) { +- final PrioritisedTask ret = this.queue.queueRunnable(task, priority); ++ public PrioritisedTask queueTask(final Runnable task) { ++ final PrioritisedTask ret = this.createTask(task); + +- this.notifyTasks(); ++ ret.queue(); + + return ret; + } + + @Override +- public boolean haveAllTasksExecuted() { +- return this.queue.haveAllTasksExecuted(); ++ public PrioritisedTask queueTask(final Runnable task, final Priority priority) { ++ final PrioritisedTask ret = this.createTask(task, priority); ++ ++ ret.queue(); ++ ++ return ret; + } + + @Override +- public long getTotalTasksExecuted() { +- return this.queue.getTotalTasksExecuted(); ++ public PrioritisedTask queueTask(final Runnable task, final Priority priority, final long subOrder) { ++ final PrioritisedTask ret = this.createTask(task, priority, subOrder); ++ ++ ret.queue(); ++ ++ return ret; + } + ++ + @Override +- public long getTotalTasksScheduled() { +- return this.queue.getTotalTasksScheduled(); ++ public PrioritisedTask createTask(Runnable task) { ++ final PrioritisedTask queueTask = this.queue.createTask(task); ++ ++ return new WrappedTask(queueTask); + } + +- /** +- * {@inheritDoc} +- * @throws IllegalStateException If the current thread is {@code this} thread, or the underlying queue throws this exception. +- */ + @Override +- public void waitUntilAllExecuted() throws IllegalStateException { +- if (Thread.currentThread() == this) { +- throw new IllegalStateException("Cannot block on our own queue"); +- } +- this.queue.waitUntilAllExecuted(); ++ public PrioritisedTask createTask(final Runnable task, final Priority priority) { ++ final PrioritisedTask queueTask = this.queue.createTask(task, priority); ++ ++ return new WrappedTask(queueTask); + } + +- /** +- * {@inheritDoc} +- * @throws IllegalStateException Always +- */ + @Override +- public boolean executeTask() throws IllegalStateException { +- throw new IllegalStateException(); ++ public PrioritisedTask createTask(final Runnable task, final Priority priority, final long subOrder) { ++ final PrioritisedTask queueTask = this.queue.createTask(task, priority, subOrder); ++ ++ return new WrappedTask(queueTask); + } + + /** +@@ -242,7 +245,7 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise + * <p> + * This function is MT-Safe. + * </p> +- * @param wait If this call is to wait until the queue is empty and there are no tasks executing in the queue. ++ * @param wait If this call is to wait until this thread shuts down. + * @param killQueue Whether to shutdown this thread's queue + * @return whether this thread shut down the queue + * @see #halt(boolean) +@@ -256,7 +259,20 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise + LockSupport.unpark(this); + + if (wait) { +- this.waitUntilAllExecuted(); ++ boolean interrupted = false; ++ for (;;) { ++ if (this.isAlive()) { ++ if (interrupted) { ++ Thread.currentThread().interrupt(); ++ } ++ break; ++ } ++ try { ++ this.join(); ++ } catch (final InterruptedException ex) { ++ interrupted = true; ++ } ++ } + } + + return ret; +@@ -298,4 +314,89 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise + protected final void setThreadParkedVolatile(final boolean value) { + THREAD_PARKED_HANDLE.setVolatile(this, value); + } ++ ++ /** ++ * Required so that queue() can notify (unpark) this thread ++ */ ++ private final class WrappedTask implements PrioritisedTask { ++ private final PrioritisedTask queueTask; ++ ++ public WrappedTask(final PrioritisedTask queueTask) { ++ this.queueTask = queueTask; ++ } ++ ++ @Override ++ public PrioritisedExecutor getExecutor() { ++ return PrioritisedQueueExecutorThread.this; ++ } ++ ++ @Override ++ public boolean queue() { ++ final boolean ret = this.queueTask.queue(); ++ if (ret) { ++ PrioritisedQueueExecutorThread.this.notifyTasks(); ++ } ++ return ret; ++ } ++ ++ @Override ++ public boolean isQueued() { ++ return this.queueTask.isQueued(); ++ } ++ ++ @Override ++ public boolean cancel() { ++ return this.queueTask.cancel(); ++ } ++ ++ @Override ++ public boolean execute() { ++ return this.queueTask.execute(); ++ } ++ ++ @Override ++ public Priority getPriority() { ++ return this.queueTask.getPriority(); ++ } ++ ++ @Override ++ public boolean setPriority(final Priority priority) { ++ return this.queueTask.setPriority(priority); ++ } ++ ++ @Override ++ public boolean raisePriority(final Priority priority) { ++ return this.queueTask.raisePriority(priority); ++ } ++ ++ @Override ++ public boolean lowerPriority(final Priority priority) { ++ return this.queueTask.lowerPriority(priority); ++ } ++ ++ @Override ++ public long getSubOrder() { ++ return this.queueTask.getSubOrder(); ++ } ++ ++ @Override ++ public boolean setSubOrder(final long subOrder) { ++ return this.queueTask.setSubOrder(subOrder); ++ } ++ ++ @Override ++ public boolean raiseSubOrder(final long subOrder) { ++ return this.queueTask.raiseSubOrder(subOrder); ++ } ++ ++ @Override ++ public boolean lowerSubOrder(final long subOrder) { ++ return this.queueTask.lowerSubOrder(subOrder); ++ } ++ ++ @Override ++ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) { ++ return this.queueTask.setPriorityAndSubOrder(priority, subOrder); ++ } ++ } + } +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedThreadPool.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cb9df914a9a6d0d3f58fa58d8c93f4f583416cd1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedThreadPool.java +@@ -0,0 +1,741 @@ ++package ca.spottedleaf.concurrentutil.executor.thread; ++ ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.queue.PrioritisedTaskQueue; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.concurrentutil.util.TimeUtil; ++import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; ++import java.lang.reflect.Array; ++import java.util.Arrays; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.function.Consumer; ++ ++public final class PrioritisedThreadPool { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(PrioritisedThreadPool.class); ++ ++ private final Consumer<Thread> threadModifier; ++ private final COWArrayList<ExecutorGroup> executors = new COWArrayList<>(ExecutorGroup.class); ++ private final COWArrayList<PrioritisedThread> threads = new COWArrayList<>(PrioritisedThread.class); ++ private final COWArrayList<PrioritisedThread> aliveThreads = new COWArrayList<>(PrioritisedThread.class); ++ ++ private static final Priority HIGH_PRIORITY_NOTIFY_THRESHOLD = Priority.HIGH; ++ private static final Priority QUEUE_SHUTDOWN_PRIORITY = Priority.HIGH; ++ ++ private boolean shutdown; ++ ++ public PrioritisedThreadPool(final Consumer<Thread> threadModifier) { ++ this.threadModifier = threadModifier; ++ ++ if (threadModifier == null) { ++ throw new NullPointerException("Thread factory may not be null"); ++ } ++ } ++ ++ public Thread[] getAliveThreads() { ++ final PrioritisedThread[] threads = this.aliveThreads.getArray(); ++ ++ return Arrays.copyOf(threads, threads.length, Thread[].class); ++ } ++ ++ public Thread[] getCoreThreads() { ++ final PrioritisedThread[] threads = this.threads.getArray(); ++ ++ return Arrays.copyOf(threads, threads.length, Thread[].class); ++ } ++ ++ /** ++ * Prevents creation of new queues, shutdowns all non-shutdown queues if specified ++ */ ++ public void halt(final boolean shutdownQueues) { ++ synchronized (this) { ++ this.shutdown = true; ++ } ++ ++ if (shutdownQueues) { ++ for (final ExecutorGroup group : this.executors.getArray()) { ++ for (final ExecutorGroup.ThreadPoolExecutor executor : group.executors.getArray()) { ++ executor.shutdown(); ++ } ++ } ++ } ++ ++ for (final PrioritisedThread thread : this.threads.getArray()) { ++ thread.halt(false); ++ } ++ } ++ ++ /** ++ * Waits until all threads in this pool have shutdown, or until the specified time has passed. ++ * @param msToWait Maximum time to wait. ++ * @return {@code false} if the maximum time passed, {@code true} otherwise. ++ */ ++ public boolean join(final long msToWait) { ++ try { ++ return this.join(msToWait, false); ++ } catch (final InterruptedException ex) { ++ throw new IllegalStateException(ex); ++ } ++ } ++ ++ /** ++ * Waits until all threads in this pool have shutdown, or until the specified time has passed. ++ * @param msToWait Maximum time to wait. ++ * @return {@code false} if the maximum time passed, {@code true} otherwise. ++ * @throws InterruptedException If this thread is interrupted. ++ */ ++ public boolean joinInterruptable(final long msToWait) throws InterruptedException { ++ return this.join(msToWait, true); ++ } ++ ++ protected final boolean join(final long msToWait, final boolean interruptable) throws InterruptedException { ++ final long nsToWait = msToWait * (1000 * 1000); ++ final long start = System.nanoTime(); ++ final long deadline = start + nsToWait; ++ boolean interrupted = false; ++ try { ++ for (final PrioritisedThread thread : this.aliveThreads.getArray()) { ++ for (;;) { ++ if (!thread.isAlive()) { ++ break; ++ } ++ final long current = System.nanoTime(); ++ if (current >= deadline && msToWait > 0L) { ++ return false; ++ } ++ ++ try { ++ thread.join(msToWait <= 0L ? 0L : Math.max(1L, (deadline - current) / (1000 * 1000))); ++ } catch (final InterruptedException ex) { ++ if (interruptable) { ++ throw ex; ++ } ++ interrupted = true; ++ } ++ } ++ } ++ ++ return true; ++ } finally { ++ if (interrupted) { ++ Thread.currentThread().interrupt(); ++ } ++ } ++ } ++ ++ /** ++ * Shuts down this thread pool, optionally waiting for all tasks to be executed. ++ * This function will invoke {@link PrioritisedExecutor#shutdown()} on all created executors on this ++ * thread pool. ++ * @param wait Whether to wait for tasks to be executed ++ */ ++ public void shutdown(final boolean wait) { ++ synchronized (this) { ++ this.shutdown = true; ++ } ++ ++ for (final ExecutorGroup group : this.executors.getArray()) { ++ for (final ExecutorGroup.ThreadPoolExecutor executor : group.executors.getArray()) { ++ executor.shutdown(); ++ } ++ } ++ ++ ++ for (final PrioritisedThread thread : this.threads.getArray()) { ++ // none of these can be true or else NPE ++ thread.close(false, false); ++ } ++ ++ if (wait) { ++ this.join(0L); ++ } ++ } ++ ++ private void die(final PrioritisedThread thread) { ++ this.aliveThreads.remove(thread); ++ } ++ ++ public void adjustThreadCount(final int threads) { ++ synchronized (this) { ++ if (this.shutdown) { ++ return; ++ } ++ ++ final PrioritisedThread[] currentThreads = this.threads.getArray(); ++ if (threads == currentThreads.length) { ++ // no adjustment needed ++ return; ++ } ++ ++ if (threads < currentThreads.length) { ++ // we need to trim threads ++ for (int i = 0, difference = currentThreads.length - threads; i < difference; ++i) { ++ final PrioritisedThread remove = currentThreads[currentThreads.length - i - 1]; ++ ++ remove.halt(false); ++ this.threads.remove(remove); ++ } ++ } else { ++ // we need to add threads ++ for (int i = 0, difference = threads - currentThreads.length; i < difference; ++i) { ++ final PrioritisedThread thread = new PrioritisedThread(); ++ ++ this.threadModifier.accept(thread); ++ this.aliveThreads.add(thread); ++ this.threads.add(thread); ++ ++ thread.start(); ++ } ++ } ++ } ++ } ++ ++ private static int compareInsideGroup(final ExecutorGroup.ThreadPoolExecutor src, final Priority srcPriority, ++ final ExecutorGroup.ThreadPoolExecutor dst, final Priority dstPriority) { ++ final int priorityCompare = srcPriority.ordinal() - dstPriority.ordinal(); ++ if (priorityCompare != 0) { ++ return priorityCompare; ++ } ++ ++ final int parallelismCompare = src.currentParallelism - dst.currentParallelism; ++ if (parallelismCompare != 0) { ++ return parallelismCompare; ++ } ++ ++ return TimeUtil.compareTimes(src.lastRetrieved, dst.lastRetrieved); ++ } ++ ++ private static int compareOutsideGroup(final ExecutorGroup.ThreadPoolExecutor src, final Priority srcPriority, ++ final ExecutorGroup.ThreadPoolExecutor dst, final Priority dstPriority) { ++ if (src.getGroup().division == dst.getGroup().division) { ++ // can only compare priorities inside the same division ++ final int priorityCompare = srcPriority.ordinal() - dstPriority.ordinal(); ++ if (priorityCompare != 0) { ++ return priorityCompare; ++ } ++ } ++ ++ final int parallelismCompare = src.getGroup().currentParallelism - dst.getGroup().currentParallelism; ++ if (parallelismCompare != 0) { ++ return parallelismCompare; ++ } ++ ++ return TimeUtil.compareTimes(src.lastRetrieved, dst.lastRetrieved); ++ } ++ ++ private ExecutorGroup.ThreadPoolExecutor obtainQueue() { ++ final long time = System.nanoTime(); ++ synchronized (this) { ++ ExecutorGroup.ThreadPoolExecutor ret = null; ++ Priority retPriority = null; ++ ++ for (final ExecutorGroup executorGroup : this.executors.getArray()) { ++ ExecutorGroup.ThreadPoolExecutor highest = null; ++ Priority highestPriority = null; ++ for (final ExecutorGroup.ThreadPoolExecutor executor : executorGroup.executors.getArray()) { ++ final int maxParallelism = executor.maxParallelism; ++ if (maxParallelism > 0 && executor.currentParallelism >= maxParallelism) { ++ continue; ++ } ++ ++ final Priority priority = executor.getTargetPriority(); ++ ++ if (priority == null) { ++ continue; ++ } ++ ++ if (highestPriority == null || compareInsideGroup(highest, highestPriority, executor, priority) > 0) { ++ highest = executor; ++ highestPriority = priority; ++ } ++ } ++ ++ if (highest == null) { ++ continue; ++ } ++ ++ if (ret == null || compareOutsideGroup(ret, retPriority, highest, highestPriority) > 0) { ++ ret = highest; ++ retPriority = highestPriority; ++ } ++ } ++ ++ if (ret != null) { ++ ret.lastRetrieved = time; ++ ++ret.currentParallelism; ++ ++ret.getGroup().currentParallelism; ++ return ret; ++ } ++ ++ return ret; ++ } ++ } ++ ++ private void returnQueue(final ExecutorGroup.ThreadPoolExecutor executor) { ++ synchronized (this) { ++ --executor.currentParallelism; ++ --executor.getGroup().currentParallelism; ++ } ++ ++ if (executor.isShutdown() && executor.queue.hasNoScheduledTasks()) { ++ executor.getGroup().executors.remove(executor); ++ } ++ } ++ ++ private void notifyAllThreads() { ++ for (final PrioritisedThread thread : this.threads.getArray()) { ++ thread.notifyTasks(); ++ } ++ } ++ ++ public ExecutorGroup createExecutorGroup(final int division, final int flags) { ++ synchronized (this) { ++ if (this.shutdown) { ++ throw new IllegalStateException("Queue is shutdown: " + this.toString()); ++ } ++ ++ final ExecutorGroup ret = new ExecutorGroup(division, flags); ++ ++ this.executors.add(ret); ++ ++ return ret; ++ } ++ } ++ ++ private final class PrioritisedThread extends PrioritisedQueueExecutorThread { ++ ++ private final AtomicBoolean alertedHighPriority = new AtomicBoolean(); ++ ++ public PrioritisedThread() { ++ super(null); ++ } ++ ++ public boolean alertHighPriorityExecutor() { ++ if (!this.notifyTasks()) { ++ if (!this.alertedHighPriority.get()) { ++ this.alertedHighPriority.set(true); ++ } ++ return false; ++ } ++ ++ return true; ++ } ++ ++ private boolean isAlertedHighPriority() { ++ return this.alertedHighPriority.get() && this.alertedHighPriority.getAndSet(false); ++ } ++ ++ @Override ++ protected void die() { ++ PrioritisedThreadPool.this.die(this); ++ } ++ ++ @Override ++ protected boolean pollTasks() { ++ boolean ret = false; ++ ++ for (;;) { ++ if (this.halted) { ++ break; ++ } ++ ++ final ExecutorGroup.ThreadPoolExecutor executor = PrioritisedThreadPool.this.obtainQueue(); ++ if (executor == null) { ++ break; ++ } ++ final long deadline = System.nanoTime() + executor.queueMaxHoldTime; ++ do { ++ try { ++ if (this.halted || executor.halt) { ++ break; ++ } ++ if (!executor.executeTask()) { ++ // no more tasks, try next queue ++ break; ++ } ++ ret = true; ++ } catch (final Throwable throwable) { ++ LOGGER.error("Exception thrown from thread '" + this.getName() + "' in queue '" + executor.toString() + "'", throwable); ++ } ++ } while (!this.isAlertedHighPriority() && System.nanoTime() <= deadline); ++ ++ PrioritisedThreadPool.this.returnQueue(executor); ++ } ++ ++ ++ return ret; ++ } ++ } ++ ++ public final class ExecutorGroup { ++ ++ private final AtomicLong subOrderGenerator = new AtomicLong(); ++ private final COWArrayList<ThreadPoolExecutor> executors = new COWArrayList<>(ThreadPoolExecutor.class); ++ ++ private final int division; ++ private int currentParallelism; ++ ++ private ExecutorGroup(final int division, final int flags) { ++ this.division = division; ++ } ++ ++ public ThreadPoolExecutor[] getAllExecutors() { ++ return this.executors.getArray().clone(); ++ } ++ ++ private PrioritisedThreadPool getThreadPool() { ++ return PrioritisedThreadPool.this; ++ } ++ ++ public ThreadPoolExecutor createExecutor(final int maxParallelism, final long queueMaxHoldTime, final int flags) { ++ synchronized (PrioritisedThreadPool.this) { ++ if (PrioritisedThreadPool.this.shutdown) { ++ throw new IllegalStateException("Queue is shutdown: " + PrioritisedThreadPool.this.toString()); ++ } ++ ++ final ThreadPoolExecutor ret = new ThreadPoolExecutor(maxParallelism, queueMaxHoldTime, flags); ++ ++ this.executors.add(ret); ++ ++ return ret; ++ } ++ } ++ ++ public final class ThreadPoolExecutor implements PrioritisedExecutor { ++ ++ private final PrioritisedTaskQueue queue = new PrioritisedTaskQueue(); ++ ++ private volatile int maxParallelism; ++ private final long queueMaxHoldTime; ++ private volatile int currentParallelism; ++ private volatile boolean halt; ++ private long lastRetrieved = System.nanoTime(); ++ ++ private ThreadPoolExecutor(final int maxParallelism, final long queueMaxHoldTime, final int flags) { ++ this.maxParallelism = maxParallelism; ++ this.queueMaxHoldTime = queueMaxHoldTime; ++ } ++ ++ private ExecutorGroup getGroup() { ++ return ExecutorGroup.this; ++ } ++ ++ private boolean canNotify() { ++ if (this.halt) { ++ return false; ++ } ++ ++ final int max = this.maxParallelism; ++ return max < 0 || this.currentParallelism < max; ++ } ++ ++ private void notifyHighPriority() { ++ if (!this.canNotify()) { ++ return; ++ } ++ for (final PrioritisedThread thread : this.getGroup().getThreadPool().threads.getArray()) { ++ if (thread.alertHighPriorityExecutor()) { ++ return; ++ } ++ } ++ } ++ ++ private void notifyScheduled() { ++ if (!this.canNotify()) { ++ return; ++ } ++ for (final PrioritisedThread thread : this.getGroup().getThreadPool().threads.getArray()) { ++ if (thread.notifyTasks()) { ++ return; ++ } ++ } ++ } ++ ++ /** ++ * Removes this queue from the thread pool without shutting the queue down or waiting for queued tasks to be executed ++ */ ++ public void halt() { ++ this.halt = true; ++ ++ ExecutorGroup.this.executors.remove(this); ++ } ++ ++ /** ++ * Returns whether this executor is scheduled to run tasks or is running tasks, otherwise it returns whether ++ * this queue is not halted and not shutdown. ++ */ ++ public boolean isActive() { ++ if (this.halt) { ++ return this.currentParallelism > 0; ++ } else { ++ if (!this.isShutdown()) { ++ return true; ++ } ++ ++ return !this.queue.hasNoScheduledTasks(); ++ } ++ } ++ ++ @Override ++ public boolean shutdown() { ++ if (!this.queue.shutdown()) { ++ return false; ++ } ++ ++ if (this.queue.hasNoScheduledTasks()) { ++ ExecutorGroup.this.executors.remove(this); ++ } ++ ++ return true; ++ } ++ ++ @Override ++ public boolean isShutdown() { ++ return this.queue.isShutdown(); ++ } ++ ++ public void setMaxParallelism(final int maxParallelism) { ++ this.maxParallelism = maxParallelism; ++ // assume that we could have increased the parallelism ++ if (this.getTargetPriority() != null) { ++ ExecutorGroup.this.getThreadPool().notifyAllThreads(); ++ } ++ } ++ ++ Priority getTargetPriority() { ++ final Priority ret = this.queue.getHighestPriority(); ++ if (!this.isShutdown()) { ++ return ret; ++ } ++ ++ return ret == null ? QUEUE_SHUTDOWN_PRIORITY : Priority.max(ret, QUEUE_SHUTDOWN_PRIORITY); ++ } ++ ++ @Override ++ public long getTotalTasksScheduled() { ++ return this.queue.getTotalTasksScheduled(); ++ } ++ ++ @Override ++ public long getTotalTasksExecuted() { ++ return this.queue.getTotalTasksExecuted(); ++ } ++ ++ @Override ++ public long generateNextSubOrder() { ++ return ExecutorGroup.this.subOrderGenerator.getAndIncrement(); ++ } ++ ++ @Override ++ public boolean executeTask() { ++ return this.queue.executeTask(); ++ } ++ ++ @Override ++ public PrioritisedTask queueTask(final Runnable task) { ++ final PrioritisedTask ret = this.createTask(task); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ @Override ++ public PrioritisedTask queueTask(final Runnable task, final Priority priority) { ++ final PrioritisedTask ret = this.createTask(task, priority); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ @Override ++ public PrioritisedTask queueTask(final Runnable task, final Priority priority, final long subOrder) { ++ final PrioritisedTask ret = this.createTask(task, priority, subOrder); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ @Override ++ public PrioritisedTask createTask(final Runnable task) { ++ return this.createTask(task, Priority.NORMAL); ++ } ++ ++ @Override ++ public PrioritisedTask createTask(final Runnable task, final Priority priority) { ++ return this.createTask(task, priority, this.generateNextSubOrder()); ++ } ++ ++ @Override ++ public PrioritisedTask createTask(final Runnable task, final Priority priority, final long subOrder) { ++ return new WrappedTask(this.queue.createTask(task, priority, subOrder)); ++ } ++ ++ private final class WrappedTask implements PrioritisedTask { ++ ++ private final PrioritisedTask wrapped; ++ ++ private WrappedTask(final PrioritisedTask wrapped) { ++ this.wrapped = wrapped; ++ } ++ ++ @Override ++ public PrioritisedExecutor getExecutor() { ++ return ThreadPoolExecutor.this; ++ } ++ ++ @Override ++ public boolean queue() { ++ if (this.wrapped.queue()) { ++ final Priority priority = this.getPriority(); ++ if (priority != Priority.COMPLETING) { ++ if (priority.isHigherOrEqualPriority(HIGH_PRIORITY_NOTIFY_THRESHOLD)) { ++ ThreadPoolExecutor.this.notifyHighPriority(); ++ } else { ++ ThreadPoolExecutor.this.notifyScheduled(); ++ } ++ } ++ return true; ++ } ++ ++ return false; ++ } ++ ++ @Override ++ public boolean isQueued() { ++ return this.wrapped.isQueued(); ++ } ++ ++ @Override ++ public boolean cancel() { ++ return this.wrapped.cancel(); ++ } ++ ++ @Override ++ public boolean execute() { ++ return this.wrapped.execute(); ++ } ++ ++ @Override ++ public Priority getPriority() { ++ return this.wrapped.getPriority(); ++ } ++ ++ @Override ++ public boolean setPriority(final Priority priority) { ++ if (this.wrapped.setPriority(priority)) { ++ if (priority.isHigherOrEqualPriority(HIGH_PRIORITY_NOTIFY_THRESHOLD)) { ++ ThreadPoolExecutor.this.notifyHighPriority(); ++ } ++ return true; ++ } ++ ++ return false; ++ } ++ ++ @Override ++ public boolean raisePriority(final Priority priority) { ++ if (this.wrapped.raisePriority(priority)) { ++ if (priority.isHigherOrEqualPriority(HIGH_PRIORITY_NOTIFY_THRESHOLD)) { ++ ThreadPoolExecutor.this.notifyHighPriority(); ++ } ++ return true; ++ } ++ ++ return false; ++ } ++ ++ @Override ++ public boolean lowerPriority(final Priority priority) { ++ return this.wrapped.lowerPriority(priority); ++ } ++ ++ @Override ++ public long getSubOrder() { ++ return this.wrapped.getSubOrder(); ++ } ++ ++ @Override ++ public boolean setSubOrder(final long subOrder) { ++ return this.wrapped.setSubOrder(subOrder); ++ } ++ ++ @Override ++ public boolean raiseSubOrder(final long subOrder) { ++ return this.wrapped.raiseSubOrder(subOrder); ++ } ++ ++ @Override ++ public boolean lowerSubOrder(final long subOrder) { ++ return this.wrapped.lowerSubOrder(subOrder); ++ } ++ ++ @Override ++ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) { ++ if (this.wrapped.setPriorityAndSubOrder(priority, subOrder)) { ++ if (priority.isHigherOrEqualPriority(HIGH_PRIORITY_NOTIFY_THRESHOLD)) { ++ ThreadPoolExecutor.this.notifyHighPriority(); ++ } ++ return true; ++ } ++ ++ return false; ++ } ++ } ++ } ++ } ++ ++ private static final class COWArrayList<E> { ++ ++ private volatile E[] array; ++ ++ public COWArrayList(final Class<E> clazz) { ++ this.array = (E[])Array.newInstance(clazz, 0); ++ } ++ ++ public E[] getArray() { ++ return this.array; ++ } ++ ++ public void add(final E element) { ++ synchronized (this) { ++ final E[] array = this.array; ++ ++ final E[] copy = Arrays.copyOf(array, array.length + 1); ++ copy[array.length] = element; ++ ++ this.array = copy; ++ } ++ } ++ ++ public boolean remove(final E element) { ++ synchronized (this) { ++ final E[] array = this.array; ++ int index = -1; ++ for (int i = 0, len = array.length; i < len; ++i) { ++ if (array[i] == element) { ++ index = i; ++ break; ++ } ++ } ++ ++ if (index == -1) { ++ return false; ++ } ++ ++ final E[] copy = (E[])Array.newInstance(array.getClass().getComponentType(), array.length - 1); ++ ++ System.arraycopy(array, 0, copy, 0, index); ++ System.arraycopy(array, index + 1, copy, index, (array.length - 1) - index); ++ ++ this.array = copy; ++ } ++ ++ return true; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java +index d701998b376579ec652fb94823befa3cc0bc4090..6918f130099e6c19e20a47bfdb54915cdd13732a 100644 +--- a/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java ++++ b/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java +@@ -43,7 +43,7 @@ import java.util.function.Predicate; + * @param <V> + * @see java.util.concurrent.ConcurrentMap + */ +-public class ConcurrentLong2ReferenceChainedHashTable<V> { ++public class ConcurrentLong2ReferenceChainedHashTable<V> implements Iterable<ConcurrentLong2ReferenceChainedHashTable.TableEntry<V>> { + + protected static final int DEFAULT_CAPACITY = 16; + protected static final float DEFAULT_LOAD_FACTOR = 0.75f; +@@ -192,6 +192,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + } + + if (node.resize) { ++ // noinspection unchecked + table = (TableEntry<V>[])node.getValuePlain(); + continue; + } +@@ -274,10 +275,10 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + public int size() { + final long ret = this.size.sum(); + +- if (ret <= 0L) { ++ if (ret < 0L) { + return 0; + } +- if (ret >= (long)Integer.MAX_VALUE) { ++ if (ret > (long)Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + +@@ -341,6 +342,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + + // create new table data + ++ // noinspection unchecked + final TableEntry<V>[] newTable = new TableEntry[capacity]; + // noinspection unchecked + final TableEntry<V> resizeNode = new TableEntry<>(0L, (V)newTable, true); +@@ -365,6 +367,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + throw new IllegalStateException("Resizing to same size"); + } + ++ // noinspection unchecked + final TableEntry<V>[] work = new TableEntry[1 << capDiffShift]; // typically, capDiffShift = 1 + + for (int i = 0, len = oldTable.length; i < len; ++i) { +@@ -538,6 +541,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + } + + if (node.resize) { ++ // noinspection unchecked + table = (TableEntry<V>[])node.getValuePlain(); + continue table_loop; + } +@@ -599,6 +603,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + } + + if (node.resize) { ++ // noinspection unchecked + table = (TableEntry<V>[])node.getValuePlain(); + continue table_loop; + } +@@ -653,6 +658,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + } + + if (node.resize) { ++ // noinspection unchecked + table = (TableEntry<V>[])node.getValuePlain(); + continue table_loop; + } +@@ -704,6 +710,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + } + + if (node.resize) { ++ // noinspection unchecked + table = (TableEntry<V>[])node.getValuePlain(); + continue table_loop; + } +@@ -772,6 +779,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + } + + if (node.resize) { ++ // noinspection unchecked + table = (TableEntry<V>[])node.getValuePlain(); + continue table_loop; + } +@@ -848,6 +856,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + } + + if (node.resize) { ++ // noinspection unchecked + table = (TableEntry<V>[])node.getValuePlain(); + continue table_loop; + } +@@ -944,6 +953,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + } + + if (node.resize) { ++ // noinspection unchecked + table = (TableEntry<V>[])node.getValuePlain(); + continue table_loop; + } +@@ -1058,6 +1068,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + } + + if (node.resize) { ++ // noinspection unchecked + table = (TableEntry<V>[])node.getValuePlain(); + continue table_loop; + } +@@ -1126,6 +1137,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + } + + if (node.resize) { ++ // noinspection unchecked + table = (TableEntry<V>[])node.getValuePlain(); + continue table_loop; + } +@@ -1200,6 +1212,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + } + + if (node.resize) { ++ // noinspection unchecked + table = (TableEntry<V>[])node.getValuePlain(); + continue table_loop; + } +@@ -1288,6 +1301,11 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + return new EntryIterator<>(this); + } + ++ @Override ++ public final Iterator<TableEntry<V>> iterator() { ++ return this.entryIterator(); ++ } ++ + /** + * Returns an iterator over the keys in this map. The iterator is only guaranteed to see keys that were + * added before the beginning of this call, but it may see keys added during. +@@ -1306,7 +1324,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + + protected static final class EntryIterator<V> extends BaseIteratorImpl<V, TableEntry<V>> { + +- protected EntryIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) { ++ public EntryIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) { + super(map); + } + +@@ -1326,7 +1344,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + + protected static final class KeyIterator<V> extends BaseIteratorImpl<V, Long> implements PrimitiveIterator.OfLong { + +- protected KeyIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) { ++ public KeyIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) { + super(map); + } + +@@ -1365,7 +1383,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + + protected static final class ValueIterator<V> extends BaseIteratorImpl<V, V> { + +- protected ValueIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) { ++ public ValueIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) { + super(map); + } + +@@ -1420,7 +1438,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + + @Override + public final void remove() { +- final TableEntry<V> lastReturned = this.nextToReturn; ++ final TableEntry<V> lastReturned = this.lastReturned; + if (lastReturned == null) { + throw new NoSuchElementException(); + } +@@ -1492,6 +1510,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + final ResizeChain<V> chain = this.resizeChain; + + if (chain == null) { ++ // noinspection unchecked + final TableEntry<V>[] nextTable = (TableEntry<V>[])entry.getValuePlain(); + + final ResizeChain<V> oldChain = new ResizeChain<>(table, null, null); +@@ -1506,6 +1525,7 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + } else { + ResizeChain<V> currChain = chain.next; + if (currChain == null) { ++ // noinspection unchecked + final TableEntry<V>[] ret = (TableEntry<V>[])entry.getValuePlain(); + currChain = new ResizeChain<>(ret, chain, null); + chain.next = currChain; +@@ -1586,11 +1606,11 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + + protected static final class ResizeChain<V> { + +- protected final TableEntry<V>[] table; +- protected final ResizeChain<V> prev; +- protected ResizeChain<V> next; ++ public final TableEntry<V>[] table; ++ public final ResizeChain<V> prev; ++ public ResizeChain<V> next; + +- protected ResizeChain(final TableEntry<V>[] table, final ResizeChain<V> prev, final ResizeChain<V> next) { ++ public ResizeChain(final TableEntry<V>[] table, final ResizeChain<V> prev, final ResizeChain<V> next) { + this.table = table; + this.prev = prev; + this.next = next; +@@ -1600,64 +1620,64 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> { + + public static final class TableEntry<V> { + +- protected static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class); ++ private static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class); + +- protected final boolean resize; ++ private final boolean resize; + +- protected final long key; ++ private final long key; + +- protected volatile V value; +- protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class); ++ private volatile V value; ++ private static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class); + +- protected final V getValuePlain() { ++ private V getValuePlain() { + //noinspection unchecked + return (V)VALUE_HANDLE.get(this); + } + +- protected final V getValueAcquire() { ++ private V getValueAcquire() { + //noinspection unchecked + return (V)VALUE_HANDLE.getAcquire(this); + } + +- protected final V getValueVolatile() { ++ private V getValueVolatile() { + //noinspection unchecked + return (V)VALUE_HANDLE.getVolatile(this); + } + +- protected final void setValuePlain(final V value) { ++ private void setValuePlain(final V value) { + VALUE_HANDLE.set(this, (Object)value); + } + +- protected final void setValueRelease(final V value) { ++ private void setValueRelease(final V value) { + VALUE_HANDLE.setRelease(this, (Object)value); + } + +- protected final void setValueVolatile(final V value) { ++ private void setValueVolatile(final V value) { + VALUE_HANDLE.setVolatile(this, (Object)value); + } + +- protected volatile TableEntry<V> next; +- protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class); ++ private volatile TableEntry<V> next; ++ private static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class); + +- protected final TableEntry<V> getNextPlain() { ++ private TableEntry<V> getNextPlain() { + //noinspection unchecked + return (TableEntry<V>)NEXT_HANDLE.get(this); + } + +- protected final TableEntry<V> getNextVolatile() { ++ private TableEntry<V> getNextVolatile() { + //noinspection unchecked + return (TableEntry<V>)NEXT_HANDLE.getVolatile(this); + } + +- protected final void setNextPlain(final TableEntry<V> next) { ++ private void setNextPlain(final TableEntry<V> next) { + NEXT_HANDLE.set(this, next); + } + +- protected final void setNextRelease(final TableEntry<V> next) { ++ private void setNextRelease(final TableEntry<V> next) { + NEXT_HANDLE.setRelease(this, next); + } + +- protected final void setNextVolatile(final TableEntry<V> next) { ++ private void setNextVolatile(final TableEntry<V> next) { + NEXT_HANDLE.setVolatile(this, next); + } + +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java +index 8197ccb1c4e5878dbd8007b5fb514640765ec8e4..85e6ef636d435a0ee4bf3e0760b0c87422c520a1 100644 +--- a/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java ++++ b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java +@@ -13,6 +13,10 @@ import java.util.concurrent.atomic.AtomicLong; + import java.util.concurrent.locks.LockSupport; + import java.util.function.BooleanSupplier; + ++/** ++ * @deprecated To be replaced ++ */ ++@Deprecated + public class SchedulerThreadPool { + + public static final long DEADLINE_NOT_SET = Long.MIN_VALUE; +@@ -297,7 +301,9 @@ public class SchedulerThreadPool { + * is invoked for any scheduled task - otherwise, {@link #runTasks(BooleanSupplier)} may not be invoked to + * parse intermediate tasks. + * </p> ++ * @deprecated To be replaced + */ ++ @Deprecated + public static abstract class SchedulableTick { + private static final AtomicLong ID_GENERATOR = new AtomicLong(); + public final long id = ID_GENERATOR.getAndIncrement(); +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java +index 212bc9ae2fc7d37d4a089a2921b00de1e97f7cc1..82c4c11b0b564c97ac92bd5f54e3754a7ba95184 100644 +--- a/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java ++++ b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java +@@ -8,8 +8,8 @@ public final class LinkedSortedSet<E> implements Iterable<E> { + + public final Comparator<? super E> comparator; + +- protected Link<E> head; +- protected Link<E> tail; ++ private Link<E> head; ++ private Link<E> tail; + + public LinkedSortedSet() { + this((Comparator)Comparator.naturalOrder()); +@@ -257,8 +257,6 @@ public final class LinkedSortedSet<E> implements Iterable<E> { + private Link<E> prev; + private Link<E> next; + +- private Link() {} +- + private Link(final E element) { + this.element = element; + } +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedUnsortedList.java b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedUnsortedList.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bd8eb4f25d1dee00fbf9c05c14b0d94c5c641a55 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedUnsortedList.java +@@ -0,0 +1,204 @@ ++package ca.spottedleaf.concurrentutil.set; ++ ++import java.util.Iterator; ++import java.util.NoSuchElementException; ++import java.util.Objects; ++ ++public final class LinkedUnsortedList<E> implements Iterable<E> { ++ ++ private Link<E> head; ++ private Link<E> tail; ++ ++ public LinkedUnsortedList() {} ++ ++ public void clear() { ++ this.head = this.tail = null; ++ } ++ ++ public boolean isEmpty() { ++ return this.head == null; ++ } ++ ++ public E first() { ++ final Link<E> head = this.head; ++ return head == null ? null : head.element; ++ } ++ ++ public E last() { ++ final Link<E> tail = this.tail; ++ return tail == null ? null : tail.element; ++ } ++ ++ public boolean containsFirst(final E element) { ++ for (Link<E> curr = this.head; curr != null; curr = curr.next) { ++ if (Objects.equals(element, curr.element)) { ++ return true; ++ } ++ } ++ return false; ++ } ++ ++ public boolean containsLast(final E element) { ++ for (Link<E> curr = this.tail; curr != null; curr = curr.prev) { ++ if (Objects.equals(element, curr.element)) { ++ return true; ++ } ++ } ++ return false; ++ } ++ ++ private void removeNode(final Link<E> node) { ++ final Link<E> prev = node.prev; ++ final Link<E> next = node.next; ++ ++ // help GC ++ node.element = null; ++ node.prev = null; ++ node.next = null; ++ ++ if (prev == null) { ++ this.head = next; ++ } else { ++ prev.next = next; ++ } ++ ++ if (next == null) { ++ this.tail = prev; ++ } else { ++ next.prev = prev; ++ } ++ } ++ ++ public boolean remove(final Link<E> link) { ++ if (link.element == null) { ++ return false; ++ } ++ ++ this.removeNode(link); ++ return true; ++ } ++ ++ public boolean removeFirst(final E element) { ++ for (Link<E> curr = this.head; curr != null; curr = curr.next) { ++ if (Objects.equals(element, curr.element)) { ++ this.removeNode(curr); ++ return true; ++ } ++ } ++ return false; ++ } ++ ++ public boolean removeLast(final E element) { ++ for (Link<E> curr = this.tail; curr != null; curr = curr.prev) { ++ if (Objects.equals(element, curr.element)) { ++ this.removeNode(curr); ++ return true; ++ } ++ } ++ return false; ++ } ++ ++ @Override ++ public Iterator<E> iterator() { ++ return new Iterator<>() { ++ private Link<E> next = LinkedUnsortedList.this.head; ++ ++ @Override ++ public boolean hasNext() { ++ return this.next != null; ++ } ++ ++ @Override ++ public E next() { ++ final Link<E> next = this.next; ++ if (next == null) { ++ throw new NoSuchElementException(); ++ } ++ this.next = next.next; ++ return next.element; ++ } ++ }; ++ } ++ ++ public E pollFirst() { ++ final Link<E> head = this.head; ++ if (head == null) { ++ return null; ++ } ++ ++ final E ret = head.element; ++ final Link<E> next = head.next; ++ ++ // unlink head ++ this.head = next; ++ if (next == null) { ++ this.tail = null; ++ } else { ++ next.prev = null; ++ } ++ ++ // help GC ++ head.element = null; ++ head.next = null; ++ ++ return ret; ++ } ++ ++ public E pollLast() { ++ final Link<E> tail = this.tail; ++ if (tail == null) { ++ return null; ++ } ++ ++ final E ret = tail.element; ++ final Link<E> prev = tail.prev; ++ ++ // unlink tail ++ this.tail = prev; ++ if (prev == null) { ++ this.head = null; ++ } else { ++ prev.next = null; ++ } ++ ++ // help GC ++ tail.element = null; ++ tail.prev = null; ++ ++ return ret; ++ } ++ ++ public Link<E> addLast(final E element) { ++ final Link<E> curr = this.tail; ++ if (curr != null) { ++ return this.tail = new Link<>(element, curr, null); ++ } else { ++ return this.head = this.tail = new Link<>(element); ++ } ++ } ++ ++ public Link<E> addFirst(final E element) { ++ final Link<E> curr = this.head; ++ if (curr != null) { ++ return this.head = new Link<>(element, null, curr); ++ } else { ++ return this.head = this.tail = new Link<>(element); ++ } ++ } ++ ++ public static final class Link<E> { ++ private E element; ++ private Link<E> prev; ++ private Link<E> next; ++ ++ private Link(final E element) { ++ this.element = element; ++ } ++ ++ private Link(final E element, final Link<E> prev, final Link<E> next) { ++ this.element = element; ++ this.prev = prev; ++ this.next = next; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java +deleted file mode 100644 +index ebb1ab06165addb173fea4d295001fe37f4e79d3..0000000000000000000000000000000000000000 +--- a/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java ++++ /dev/null +@@ -1,816 +0,0 @@ +-package ca.spottedleaf.concurrentutil.util; +- +-import java.lang.invoke.VarHandle; +- +-public final class ArrayUtil { +- +- public static final VarHandle BOOLEAN_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(boolean[].class); +- +- public static final VarHandle BYTE_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(byte[].class); +- +- public static final VarHandle SHORT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(short[].class); +- +- public static final VarHandle INT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(int[].class); +- +- public static final VarHandle LONG_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(long[].class); +- +- public static final VarHandle OBJECT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(Object[].class); +- +- private ArrayUtil() { +- throw new RuntimeException(); +- } +- +- /* byte array */ +- +- public static byte getPlain(final byte[] array, final int index) { +- return (byte)BYTE_ARRAY_HANDLE.get(array, index); +- } +- +- public static byte getOpaque(final byte[] array, final int index) { +- return (byte)BYTE_ARRAY_HANDLE.getOpaque(array, index); +- } +- +- public static byte getAcquire(final byte[] array, final int index) { +- return (byte)BYTE_ARRAY_HANDLE.getAcquire(array, index); +- } +- +- public static byte getVolatile(final byte[] array, final int index) { +- return (byte)BYTE_ARRAY_HANDLE.getVolatile(array, index); +- } +- +- public static void setPlain(final byte[] array, final int index, final byte value) { +- BYTE_ARRAY_HANDLE.set(array, index, value); +- } +- +- public static void setOpaque(final byte[] array, final int index, final byte value) { +- BYTE_ARRAY_HANDLE.setOpaque(array, index, value); +- } +- +- public static void setRelease(final byte[] array, final int index, final byte value) { +- BYTE_ARRAY_HANDLE.setRelease(array, index, value); +- } +- +- public static void setVolatile(final byte[] array, final int index, final byte value) { +- BYTE_ARRAY_HANDLE.setVolatile(array, index, value); +- } +- +- public static void setVolatileContended(final byte[] array, final int index, final byte param) { +- int failures = 0; +- +- for (byte curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { +- return; +- } +- } +- } +- +- public static byte compareAndExchangeVolatile(final byte[] array, final int index, final byte expect, final byte update) { +- return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); +- } +- +- public static byte getAndAddVolatile(final byte[] array, final int index, final byte param) { +- return (byte)BYTE_ARRAY_HANDLE.getAndAdd(array, index, param); +- } +- +- public static byte getAndAndVolatile(final byte[] array, final int index, final byte param) { +- return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); +- } +- +- public static byte getAndOrVolatile(final byte[] array, final int index, final byte param) { +- return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); +- } +- +- public static byte getAndXorVolatile(final byte[] array, final int index, final byte param) { +- return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); +- } +- +- public static byte getAndSetVolatile(final byte[] array, final int index, final byte param) { +- return (byte)BYTE_ARRAY_HANDLE.getAndSet(array, index, param); +- } +- +- public static byte compareAndExchangeVolatileContended(final byte[] array, final int index, final byte expect, final byte update) { +- return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); +- } +- +- public static byte getAndAddVolatileContended(final byte[] array, final int index, final byte param) { +- int failures = 0; +- +- for (byte curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr + param)))) { +- return curr; +- } +- } +- } +- +- public static byte getAndAndVolatileContended(final byte[] array, final int index, final byte param) { +- int failures = 0; +- +- for (byte curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr & param)))) { +- return curr; +- } +- } +- } +- +- public static byte getAndOrVolatileContended(final byte[] array, final int index, final byte param) { +- int failures = 0; +- +- for (byte curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr | param)))) { +- return curr; +- } +- } +- } +- +- public static byte getAndXorVolatileContended(final byte[] array, final int index, final byte param) { +- int failures = 0; +- +- for (byte curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr ^ param)))) { +- return curr; +- } +- } +- } +- +- public static byte getAndSetVolatileContended(final byte[] array, final int index, final byte param) { +- int failures = 0; +- +- for (byte curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { +- return curr; +- } +- } +- } +- +- /* short array */ +- +- public static short getPlain(final short[] array, final int index) { +- return (short)SHORT_ARRAY_HANDLE.get(array, index); +- } +- +- public static short getOpaque(final short[] array, final int index) { +- return (short)SHORT_ARRAY_HANDLE.getOpaque(array, index); +- } +- +- public static short getAcquire(final short[] array, final int index) { +- return (short)SHORT_ARRAY_HANDLE.getAcquire(array, index); +- } +- +- public static short getVolatile(final short[] array, final int index) { +- return (short)SHORT_ARRAY_HANDLE.getVolatile(array, index); +- } +- +- public static void setPlain(final short[] array, final int index, final short value) { +- SHORT_ARRAY_HANDLE.set(array, index, value); +- } +- +- public static void setOpaque(final short[] array, final int index, final short value) { +- SHORT_ARRAY_HANDLE.setOpaque(array, index, value); +- } +- +- public static void setRelease(final short[] array, final int index, final short value) { +- SHORT_ARRAY_HANDLE.setRelease(array, index, value); +- } +- +- public static void setVolatile(final short[] array, final int index, final short value) { +- SHORT_ARRAY_HANDLE.setVolatile(array, index, value); +- } +- +- public static void setVolatileContended(final short[] array, final int index, final short param) { +- int failures = 0; +- +- for (short curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { +- return; +- } +- } +- } +- +- public static short compareAndExchangeVolatile(final short[] array, final int index, final short expect, final short update) { +- return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); +- } +- +- public static short getAndAddVolatile(final short[] array, final int index, final short param) { +- return (short)SHORT_ARRAY_HANDLE.getAndAdd(array, index, param); +- } +- +- public static short getAndAndVolatile(final short[] array, final int index, final short param) { +- return (short)SHORT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); +- } +- +- public static short getAndOrVolatile(final short[] array, final int index, final short param) { +- return (short)SHORT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); +- } +- +- public static short getAndXorVolatile(final short[] array, final int index, final short param) { +- return (short)SHORT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); +- } +- +- public static short getAndSetVolatile(final short[] array, final int index, final short param) { +- return (short)SHORT_ARRAY_HANDLE.getAndSet(array, index, param); +- } +- +- public static short compareAndExchangeVolatileContended(final short[] array, final int index, final short expect, final short update) { +- return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); +- } +- +- public static short getAndAddVolatileContended(final short[] array, final int index, final short param) { +- int failures = 0; +- +- for (short curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr + param)))) { +- return curr; +- } +- } +- } +- +- public static short getAndAndVolatileContended(final short[] array, final int index, final short param) { +- int failures = 0; +- +- for (short curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr & param)))) { +- return curr; +- } +- } +- } +- +- public static short getAndOrVolatileContended(final short[] array, final int index, final short param) { +- int failures = 0; +- +- for (short curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr | param)))) { +- return curr; +- } +- } +- } +- +- public static short getAndXorVolatileContended(final short[] array, final int index, final short param) { +- int failures = 0; +- +- for (short curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr ^ param)))) { +- return curr; +- } +- } +- } +- +- public static short getAndSetVolatileContended(final short[] array, final int index, final short param) { +- int failures = 0; +- +- for (short curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { +- return curr; +- } +- } +- } +- +- /* int array */ +- +- public static int getPlain(final int[] array, final int index) { +- return (int)INT_ARRAY_HANDLE.get(array, index); +- } +- +- public static int getOpaque(final int[] array, final int index) { +- return (int)INT_ARRAY_HANDLE.getOpaque(array, index); +- } +- +- public static int getAcquire(final int[] array, final int index) { +- return (int)INT_ARRAY_HANDLE.getAcquire(array, index); +- } +- +- public static int getVolatile(final int[] array, final int index) { +- return (int)INT_ARRAY_HANDLE.getVolatile(array, index); +- } +- +- public static void setPlain(final int[] array, final int index, final int value) { +- INT_ARRAY_HANDLE.set(array, index, value); +- } +- +- public static void setOpaque(final int[] array, final int index, final int value) { +- INT_ARRAY_HANDLE.setOpaque(array, index, value); +- } +- +- public static void setRelease(final int[] array, final int index, final int value) { +- INT_ARRAY_HANDLE.setRelease(array, index, value); +- } +- +- public static void setVolatile(final int[] array, final int index, final int value) { +- INT_ARRAY_HANDLE.setVolatile(array, index, value); +- } +- +- public static void setVolatileContended(final int[] array, final int index, final int param) { +- int failures = 0; +- +- for (int curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { +- return; +- } +- } +- } +- +- public static int compareAndExchangeVolatile(final int[] array, final int index, final int expect, final int update) { +- return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); +- } +- +- public static int getAndAddVolatile(final int[] array, final int index, final int param) { +- return (int)INT_ARRAY_HANDLE.getAndAdd(array, index, param); +- } +- +- public static int getAndAndVolatile(final int[] array, final int index, final int param) { +- return (int)INT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); +- } +- +- public static int getAndOrVolatile(final int[] array, final int index, final int param) { +- return (int)INT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); +- } +- +- public static int getAndXorVolatile(final int[] array, final int index, final int param) { +- return (int)INT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); +- } +- +- public static int getAndSetVolatile(final int[] array, final int index, final int param) { +- return (int)INT_ARRAY_HANDLE.getAndSet(array, index, param); +- } +- +- public static int compareAndExchangeVolatileContended(final int[] array, final int index, final int expect, final int update) { +- return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); +- } +- +- public static int getAndAddVolatileContended(final int[] array, final int index, final int param) { +- int failures = 0; +- +- for (int curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr + param)))) { +- return curr; +- } +- } +- } +- +- public static int getAndAndVolatileContended(final int[] array, final int index, final int param) { +- int failures = 0; +- +- for (int curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr & param)))) { +- return curr; +- } +- } +- } +- +- public static int getAndOrVolatileContended(final int[] array, final int index, final int param) { +- int failures = 0; +- +- for (int curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr | param)))) { +- return curr; +- } +- } +- } +- +- public static int getAndXorVolatileContended(final int[] array, final int index, final int param) { +- int failures = 0; +- +- for (int curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr ^ param)))) { +- return curr; +- } +- } +- } +- +- public static int getAndSetVolatileContended(final int[] array, final int index, final int param) { +- int failures = 0; +- +- for (int curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { +- return curr; +- } +- } +- } +- +- /* long array */ +- +- public static long getPlain(final long[] array, final int index) { +- return (long)LONG_ARRAY_HANDLE.get(array, index); +- } +- +- public static long getOpaque(final long[] array, final int index) { +- return (long)LONG_ARRAY_HANDLE.getOpaque(array, index); +- } +- +- public static long getAcquire(final long[] array, final int index) { +- return (long)LONG_ARRAY_HANDLE.getAcquire(array, index); +- } +- +- public static long getVolatile(final long[] array, final int index) { +- return (long)LONG_ARRAY_HANDLE.getVolatile(array, index); +- } +- +- public static void setPlain(final long[] array, final int index, final long value) { +- LONG_ARRAY_HANDLE.set(array, index, value); +- } +- +- public static void setOpaque(final long[] array, final int index, final long value) { +- LONG_ARRAY_HANDLE.setOpaque(array, index, value); +- } +- +- public static void setRelease(final long[] array, final int index, final long value) { +- LONG_ARRAY_HANDLE.setRelease(array, index, value); +- } +- +- public static void setVolatile(final long[] array, final int index, final long value) { +- LONG_ARRAY_HANDLE.setVolatile(array, index, value); +- } +- +- public static void setVolatileContended(final long[] array, final int index, final long param) { +- int failures = 0; +- +- for (long curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { +- return; +- } +- } +- } +- +- public static long compareAndExchangeVolatile(final long[] array, final int index, final long expect, final long update) { +- return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); +- } +- +- public static long getAndAddVolatile(final long[] array, final int index, final long param) { +- return (long)LONG_ARRAY_HANDLE.getAndAdd(array, index, param); +- } +- +- public static long getAndAndVolatile(final long[] array, final int index, final long param) { +- return (long)LONG_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); +- } +- +- public static long getAndOrVolatile(final long[] array, final int index, final long param) { +- return (long)LONG_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); +- } +- +- public static long getAndXorVolatile(final long[] array, final int index, final long param) { +- return (long)LONG_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); +- } +- +- public static long getAndSetVolatile(final long[] array, final int index, final long param) { +- return (long)LONG_ARRAY_HANDLE.getAndSet(array, index, param); +- } +- +- public static long compareAndExchangeVolatileContended(final long[] array, final int index, final long expect, final long update) { +- return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); +- } +- +- public static long getAndAddVolatileContended(final long[] array, final int index, final long param) { +- int failures = 0; +- +- for (long curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr + param)))) { +- return curr; +- } +- } +- } +- +- public static long getAndAndVolatileContended(final long[] array, final int index, final long param) { +- int failures = 0; +- +- for (long curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr & param)))) { +- return curr; +- } +- } +- } +- +- public static long getAndOrVolatileContended(final long[] array, final int index, final long param) { +- int failures = 0; +- +- for (long curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr | param)))) { +- return curr; +- } +- } +- } +- +- public static long getAndXorVolatileContended(final long[] array, final int index, final long param) { +- int failures = 0; +- +- for (long curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr ^ param)))) { +- return curr; +- } +- } +- } +- +- public static long getAndSetVolatileContended(final long[] array, final int index, final long param) { +- int failures = 0; +- +- for (long curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { +- return curr; +- } +- } +- } +- +- /* boolean array */ +- +- public static boolean getPlain(final boolean[] array, final int index) { +- return (boolean)BOOLEAN_ARRAY_HANDLE.get(array, index); +- } +- +- public static boolean getOpaque(final boolean[] array, final int index) { +- return (boolean)BOOLEAN_ARRAY_HANDLE.getOpaque(array, index); +- } +- +- public static boolean getAcquire(final boolean[] array, final int index) { +- return (boolean)BOOLEAN_ARRAY_HANDLE.getAcquire(array, index); +- } +- +- public static boolean getVolatile(final boolean[] array, final int index) { +- return (boolean)BOOLEAN_ARRAY_HANDLE.getVolatile(array, index); +- } +- +- public static void setPlain(final boolean[] array, final int index, final boolean value) { +- BOOLEAN_ARRAY_HANDLE.set(array, index, value); +- } +- +- public static void setOpaque(final boolean[] array, final int index, final boolean value) { +- BOOLEAN_ARRAY_HANDLE.setOpaque(array, index, value); +- } +- +- public static void setRelease(final boolean[] array, final int index, final boolean value) { +- BOOLEAN_ARRAY_HANDLE.setRelease(array, index, value); +- } +- +- public static void setVolatile(final boolean[] array, final int index, final boolean value) { +- BOOLEAN_ARRAY_HANDLE.setVolatile(array, index, value); +- } +- +- public static void setVolatileContended(final boolean[] array, final int index, final boolean param) { +- int failures = 0; +- +- for (boolean curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { +- return; +- } +- } +- } +- +- public static boolean compareAndExchangeVolatile(final boolean[] array, final int index, final boolean expect, final boolean update) { +- return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); +- } +- +- public static boolean getAndOrVolatile(final boolean[] array, final int index, final boolean param) { +- return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); +- } +- +- public static boolean getAndXorVolatile(final boolean[] array, final int index, final boolean param) { +- return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); +- } +- +- public static boolean getAndSetVolatile(final boolean[] array, final int index, final boolean param) { +- return (boolean)BOOLEAN_ARRAY_HANDLE.getAndSet(array, index, param); +- } +- +- public static boolean compareAndExchangeVolatileContended(final boolean[] array, final int index, final boolean expect, final boolean update) { +- return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); +- } +- +- public static boolean getAndAndVolatileContended(final boolean[] array, final int index, final boolean param) { +- int failures = 0; +- +- for (boolean curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr & param)))) { +- return curr; +- } +- } +- } +- +- public static boolean getAndOrVolatileContended(final boolean[] array, final int index, final boolean param) { +- int failures = 0; +- +- for (boolean curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr | param)))) { +- return curr; +- } +- } +- } +- +- public static boolean getAndXorVolatileContended(final boolean[] array, final int index, final boolean param) { +- int failures = 0; +- +- for (boolean curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr ^ param)))) { +- return curr; +- } +- } +- } +- +- public static boolean getAndSetVolatileContended(final boolean[] array, final int index, final boolean param) { +- int failures = 0; +- +- for (boolean curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { +- return curr; +- } +- } +- } +- +- @SuppressWarnings("unchecked") +- public static <T> T getPlain(final T[] array, final int index) { +- final Object ret = OBJECT_ARRAY_HANDLE.get((Object[])array, index); +- return (T)ret; +- } +- +- @SuppressWarnings("unchecked") +- public static <T> T getOpaque(final T[] array, final int index) { +- final Object ret = OBJECT_ARRAY_HANDLE.getOpaque((Object[])array, index); +- return (T)ret; +- } +- +- @SuppressWarnings("unchecked") +- public static <T> T getAcquire(final T[] array, final int index) { +- final Object ret = OBJECT_ARRAY_HANDLE.getAcquire((Object[])array, index); +- return (T)ret; +- } +- +- @SuppressWarnings("unchecked") +- public static <T> T getVolatile(final T[] array, final int index) { +- final Object ret = OBJECT_ARRAY_HANDLE.getVolatile((Object[])array, index); +- return (T)ret; +- } +- +- public static <T> void setPlain(final T[] array, final int index, final T value) { +- OBJECT_ARRAY_HANDLE.set((Object[])array, index, (Object)value); +- } +- +- public static <T> void setOpaque(final T[] array, final int index, final T value) { +- OBJECT_ARRAY_HANDLE.setOpaque((Object[])array, index, (Object)value); +- } +- +- public static <T> void setRelease(final T[] array, final int index, final T value) { +- OBJECT_ARRAY_HANDLE.setRelease((Object[])array, index, (Object)value); +- } +- +- public static <T> void setVolatile(final T[] array, final int index, final T value) { +- OBJECT_ARRAY_HANDLE.setVolatile((Object[])array, index, (Object)value); +- } +- +- public static <T> void setVolatileContended(final T[] array, final int index, final T param) { +- int failures = 0; +- +- for (T curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { +- return; +- } +- } +- } +- +- @SuppressWarnings("unchecked") +- public static <T> T compareAndExchangeVolatile(final T[] array, final int index, final T expect, final T update) { +- final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update); +- return (T)ret; +- } +- +- @SuppressWarnings("unchecked") +- public static <T> T getAndSetVolatile(final T[] array, final int index, final T param) { +- final Object ret = BYTE_ARRAY_HANDLE.getAndSet((Object[])array, index, (Object)param); +- return (T)ret; +- } +- +- @SuppressWarnings("unchecked") +- public static <T> T compareAndExchangeVolatileContended(final T[] array, final int index, final T expect, final T update) { +- final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update); +- return (T)ret; +- } +- +- public static <T> T getAndSetVolatileContended(final T[] array, final int index, final T param) { +- int failures = 0; +- +- for (T curr = getVolatile(array, index);;++failures) { +- for (int i = 0; i < failures; ++i) { +- ConcurrentUtil.backoff(); +- } +- +- if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { +- return curr; +- } +- } +- } +-} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java +index 77699c5fa9681f9ec7aa05cbb50eb60828e193ab..9d7b9b8158cd01d12adbd7896ff77bee9828e101 100644 +--- a/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java ++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java +@@ -170,6 +170,26 @@ public final class IntegerUtil { + return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1 + } + ++ // https://lemire.me/blog/2019/02/08/faster-remainders-when-the-divisor-is-a-constant-beating-compilers-and-libdivide ++ /** ++ * ++ * Usage: ++ * <pre> ++ * {@code ++ * static final long mult = getSimpleMultiplier(divisor, bits); ++ * long x = ...; ++ * long magic = x * mult; ++ * long divQ = magic >>> bits; ++ * long divR = ((magic & ((1 << bits) - 1)) * divisor) >>> bits; ++ * } ++ * </pre> ++ * ++ * @param bits The number of bits of precision for the returned result ++ */ ++ public static long getUnsignedDivisorMagic(final long divisor, final int bits) { ++ return (((1L << bits) - 1L) / divisor) + 1; ++ } ++ + private IntegerUtil() { + throw new RuntimeException(); + } +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/Priority.java b/src/main/java/ca/spottedleaf/concurrentutil/util/Priority.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2919bbaa07b70f182438c3be8f9ebbe0649809b6 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/Priority.java +@@ -0,0 +1,145 @@ ++package ca.spottedleaf.concurrentutil.util; ++ ++public enum Priority { ++ ++ /** ++ * Priority value indicating the task has completed or is being completed. ++ * This priority cannot be used to schedule tasks. ++ */ ++ COMPLETING(-1), ++ ++ /** ++ * Absolute highest priority, should only be used for when a task is blocking a time-critical thread. ++ */ ++ BLOCKING(), ++ ++ /** ++ * Should only be used for urgent but not time-critical tasks. ++ */ ++ HIGHEST(), ++ ++ /** ++ * Two priorities above normal. ++ */ ++ HIGHER(), ++ ++ /** ++ * One priority above normal. ++ */ ++ HIGH(), ++ ++ /** ++ * Default priority. ++ */ ++ NORMAL(), ++ ++ /** ++ * One priority below normal. ++ */ ++ LOW(), ++ ++ /** ++ * Two priorities below normal. ++ */ ++ LOWER(), ++ ++ /** ++ * Use for tasks that should eventually execute, but are not needed to. ++ */ ++ LOWEST(), ++ ++ /** ++ * Use for tasks that can be delayed indefinitely. ++ */ ++ IDLE(); ++ ++ // returns whether the priority can be scheduled ++ public static boolean isValidPriority(final Priority priority) { ++ return priority != null && priority != priority.COMPLETING; ++ } ++ ++ // returns the higher priority of the two ++ public static Priority max(final Priority p1, final Priority p2) { ++ return p1.isHigherOrEqualPriority(p2) ? p1 : p2; ++ } ++ ++ // returns the lower priroity of the two ++ public static Priority min(final Priority p1, final Priority p2) { ++ return p1.isLowerOrEqualPriority(p2) ? p1 : p2; ++ } ++ ++ public boolean isHigherOrEqualPriority(final Priority than) { ++ return this.priority <= than.priority; ++ } ++ ++ public boolean isHigherPriority(final Priority than) { ++ return this.priority < than.priority; ++ } ++ ++ public boolean isLowerOrEqualPriority(final Priority than) { ++ return this.priority >= than.priority; ++ } ++ ++ public boolean isLowerPriority(final Priority than) { ++ return this.priority > than.priority; ++ } ++ ++ public boolean isHigherOrEqualPriority(final int than) { ++ return this.priority <= than; ++ } ++ ++ public boolean isHigherPriority(final int than) { ++ return this.priority < than; ++ } ++ ++ public boolean isLowerOrEqualPriority(final int than) { ++ return this.priority >= than; ++ } ++ ++ public boolean isLowerPriority(final int than) { ++ return this.priority > than; ++ } ++ ++ public static boolean isHigherOrEqualPriority(final int priority, final int than) { ++ return priority <= than; ++ } ++ ++ public static boolean isHigherPriority(final int priority, final int than) { ++ return priority < than; ++ } ++ ++ public static boolean isLowerOrEqualPriority(final int priority, final int than) { ++ return priority >= than; ++ } ++ ++ public static boolean isLowerPriority(final int priority, final int than) { ++ return priority > than; ++ } ++ ++ static final Priority[] PRIORITIES = Priority.values(); ++ ++ /** includes special priorities */ ++ public static final int TOTAL_PRIORITIES = PRIORITIES.length; ++ ++ public static final int TOTAL_SCHEDULABLE_PRIORITIES = TOTAL_PRIORITIES - 1; ++ ++ public static Priority getPriority(final int priority) { ++ return PRIORITIES[priority + 1]; ++ } ++ ++ private static int priorityCounter; ++ ++ private static int nextCounter() { ++ return priorityCounter++; ++ } ++ ++ public final int priority; ++ ++ private Priority() { ++ this(nextCounter()); ++ } ++ ++ private Priority(final int priority) { ++ this.priority = priority; ++ } ++} +\ No newline at end of file diff --git a/patches/server/1070-fixup-MC-Utils.patch b/patches/server/1070-fixup-MC-Utils.patch new file mode 100644 index 0000000000..6bca72238a --- /dev/null +++ b/patches/server/1070-fixup-MC-Utils.patch @@ -0,0 +1,87 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf <[email protected]> +Date: Mon, 21 Oct 2024 12:21:54 -0700 +Subject: [PATCH] fixup! MC Utils + + +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java +index 0abba00741b39b69a7f167e5d2670f2565c9a752..96bbab283eb6c7e7863383fea0ddc62510391091 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java +@@ -1,6 +1,6 @@ + package ca.spottedleaf.moonrise.common.util; + +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.Priority; + import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; + import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk; + import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader; +@@ -23,27 +23,27 @@ public final class ChunkSystem { + private static final Logger LOGGER = LogUtils.getLogger(); + + public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) { +- scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); ++ scheduleChunkTask(level, chunkX, chunkZ, run, Priority.NORMAL); + } + +- public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) { ++ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final Priority priority) { + ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkTask(chunkX, chunkZ, run, priority); + } + + public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen, +- final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority, ++ final ChunkStatus toStatus, final boolean addTicket, final Priority priority, + final Consumer<ChunkAccess> onComplete) { + ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete); + } + + public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, +- final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) { ++ final boolean addTicket, final Priority priority, final Consumer<ChunkAccess> onComplete) { + ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + } + + public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, + final FullChunkStatus toStatus, final boolean addTicket, +- final PrioritisedExecutor.Priority priority, final Consumer<LevelChunk> onComplete) { ++ final Priority priority, final Consumer<LevelChunk> onComplete) { + ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + } + +@@ -67,7 +67,7 @@ public final class ChunkSystem { + return getUpdatingChunkHolderCount(level) != 0; + } + +- public static boolean screenEntity(final ServerLevel level, final Entity entity) { ++ public static boolean screenEntity(final ServerLevel level, final Entity entity, final boolean fromDisk, final boolean event) { + return true; + } + +diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java +index 6baa313b8201ed23193d7885c85606b0899ade3c..3eb38271b6ca26099b2da04c2d969e32fd72b2af 100644 +--- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java ++++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java +@@ -94,15 +94,13 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A + private boolean addEntity(T entity, boolean existing) { + org.spigotmc.AsyncCatcher.catchOp("Entity add"); // Paper + // Paper start - chunk system hooks +- if (existing) { +- // I don't want to know why this is a generic type. +- Entity entityCasted = (Entity)entity; +- boolean wasRemoved = entityCasted.isRemoved(); +- boolean screened = ca.spottedleaf.moonrise.common.util.ChunkSystem.screenEntity((net.minecraft.server.level.ServerLevel)entityCasted.level(), entityCasted); +- if ((!wasRemoved && entityCasted.isRemoved()) || !screened) { +- // removed by callback +- return false; +- } ++ // I don't want to know why this is a generic type. ++ Entity entityCasted = (Entity)entity; ++ boolean wasRemoved = entityCasted.isRemoved(); ++ boolean screened = ca.spottedleaf.moonrise.common.util.ChunkSystem.screenEntity((net.minecraft.server.level.ServerLevel)entityCasted.level(), entityCasted, existing, false); ++ if ((!wasRemoved && entityCasted.isRemoved()) || !screened) { ++ // removed by callback ++ return false; + } + // Paper end - chunk system hooks + if (!this.addEntityUuid(entity)) { diff --git a/patches/server/1071-Revert-Custom-table-implementation-for-blockstate-st.patch b/patches/server/1071-Revert-Custom-table-implementation-for-blockstate-st.patch new file mode 100644 index 0000000000..fa362b310f --- /dev/null +++ b/patches/server/1071-Revert-Custom-table-implementation-for-blockstate-st.patch @@ -0,0 +1,345 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf <[email protected]> +Date: Mon, 21 Oct 2024 12:52:44 -0700 +Subject: [PATCH] Revert "Custom table implementation for blockstate state + lookups" + +This reverts commit 14a7e6521ba0ce6dc8ebf98a1ccce59a5ec6a194. + +TODO Replace via deleting the patch + +diff --git a/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java b/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java +deleted file mode 100644 +index 57d0cd3ad6f972e986c72a57f1a6e36003f190c2..0000000000000000000000000000000000000000 +--- a/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java ++++ /dev/null +@@ -1,160 +0,0 @@ +-package io.papermc.paper.util.table; +- +-import com.google.common.collect.Table; +-import net.minecraft.world.level.block.state.StateHolder; +-import net.minecraft.world.level.block.state.properties.Property; +-import java.util.Collection; +-import java.util.HashSet; +-import java.util.Map; +-import java.util.Set; +- +-public final class ZeroCollidingReferenceStateTable { +- +- // upper 32 bits: starting index +- // lower 32 bits: bitset for contained ids +- protected final long[] this_index_table; +- protected final Comparable<?>[] this_table; +- protected final StateHolder<?, ?> this_state; +- +- protected long[] index_table; +- protected StateHolder<?, ?>[][] value_table; +- +- public ZeroCollidingReferenceStateTable(final StateHolder<?, ?> state, final Map<Property<?>, Comparable<?>> this_map) { +- this.this_state = state; +- this.this_index_table = this.create_table(this_map.keySet()); +- +- int max_id = -1; +- for (final Property<?> property : this_map.keySet()) { +- final int id = lookup_vindex(property, this.this_index_table); +- if (id > max_id) { +- max_id = id; +- } +- } +- +- this.this_table = new Comparable[max_id + 1]; +- for (final Map.Entry<Property<?>, Comparable<?>> entry : this_map.entrySet()) { +- this.this_table[lookup_vindex(entry.getKey(), this.this_index_table)] = entry.getValue(); +- } +- } +- +- public void loadInTable(final Table<Property<?>, Comparable<?>, StateHolder<?, ?>> table, +- final Map<Property<?>, Comparable<?>> this_map) { +- final Set<Property<?>> combined = new HashSet<>(table.rowKeySet()); +- combined.addAll(this_map.keySet()); +- +- this.index_table = this.create_table(combined); +- +- int max_id = -1; +- for (final Property<?> property : combined) { +- final int id = lookup_vindex(property, this.index_table); +- if (id > max_id) { +- max_id = id; +- } +- } +- +- this.value_table = new StateHolder[max_id + 1][]; +- +- final Map<Property<?>, Map<Comparable<?>, StateHolder<?, ?>>> map = table.rowMap(); +- for (final Property<?> property : map.keySet()) { +- final Map<Comparable<?>, StateHolder<?, ?>> propertyMap = map.get(property); +- +- final int id = lookup_vindex(property, this.index_table); +- final StateHolder<?, ?>[] states = this.value_table[id] = new StateHolder[property.getPossibleValues().size()]; +- +- for (final Map.Entry<Comparable<?>, StateHolder<?, ?>> entry : propertyMap.entrySet()) { +- if (entry.getValue() == null) { +- // TODO what +- continue; +- } +- +- states[((Property)property).getIdFor(entry.getKey())] = entry.getValue(); +- } +- } +- +- +- for (final Map.Entry<Property<?>, Comparable<?>> entry : this_map.entrySet()) { +- final Property<?> property = entry.getKey(); +- final int index = lookup_vindex(property, this.index_table); +- +- if (this.value_table[index] == null) { +- this.value_table[index] = new StateHolder[property.getPossibleValues().size()]; +- } +- +- this.value_table[index][((Property)property).getIdFor(entry.getValue())] = this.this_state; +- } +- } +- +- +- protected long[] create_table(final Collection<Property<?>> collection) { +- int max_id = -1; +- for (final Property<?> property : collection) { +- final int id = property.getId(); +- if (id > max_id) { +- max_id = id; +- } +- } +- +- final long[] ret = new long[((max_id + 1) + 31) >>> 5]; // ceil((max_id + 1) / 32) +- +- for (final Property<?> property : collection) { +- final int id = property.getId(); +- +- ret[id >>> 5] |= (1L << (id & 31)); +- } +- +- int total = 0; +- for (int i = 1, len = ret.length; i < len; ++i) { +- ret[i] |= (long)(total += Long.bitCount(ret[i - 1] & 0xFFFFFFFFL)) << 32; +- } +- +- return ret; +- } +- +- public Comparable<?> get(final Property<?> state) { +- final Comparable<?>[] table = this.this_table; +- final int index = lookup_vindex(state, this.this_index_table); +- +- if (index < 0 || index >= table.length) { +- return null; +- } +- return table[index]; +- } +- +- public StateHolder<?, ?> get(final Property<?> property, final Comparable<?> with) { +- final int withId = ((Property)property).getIdFor(with); +- if (withId < 0) { +- return null; +- } +- +- final int index = lookup_vindex(property, this.index_table); +- final StateHolder<?, ?>[][] table = this.value_table; +- if (index < 0 || index >= table.length) { +- return null; +- } +- +- final StateHolder<?, ?>[] values = table[index]; +- +- if (withId >= values.length) { +- return null; +- } +- +- return values[withId]; +- } +- +- protected static int lookup_vindex(final Property<?> property, final long[] index_table) { +- final int id = property.getId(); +- final long bitset_mask = (1L << (id & 31)); +- final long lower_mask = bitset_mask - 1; +- final int index = id >>> 5; +- if (index >= index_table.length) { +- return -1; +- } +- final long index_value = index_table[index]; +- final long contains_check = ((index_value & bitset_mask) - 1) >> (Long.SIZE - 1); // -1L if doesn't contain +- +- // index = total bits set in lower table values (upper 32 bits of index_value) plus total bits set in lower indices below id +- // contains_check is 0 if the bitset had id set, else it's -1: so index is unaffected if contains_check == 0, +- // otherwise it comes out as -1. +- return (int)(((index_value >>> 32) + Long.bitCount(index_value & lower_mask)) | contains_check); +- } +-} +diff --git a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java +index 45744d86e9582a93a0cec26009deea091080fbbe..daedcfd867ed6171fb61bdcbded417a11c8a5b0f 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java ++++ b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java +@@ -39,13 +39,11 @@ public abstract class StateHolder<O, S> { + private final Reference2ObjectArrayMap<Property<?>, Comparable<?>> values; + private Table<Property<?>, Comparable<?>, S> neighbours; + protected final MapCodec<S> propertiesCodec; +- protected final io.papermc.paper.util.table.ZeroCollidingReferenceStateTable optimisedTable; // Paper - optimise state lookup + + protected StateHolder(O owner, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<S> codec) { + this.owner = owner; + this.values = propertyMap; + this.propertiesCodec = codec; +- this.optimisedTable = new io.papermc.paper.util.table.ZeroCollidingReferenceStateTable(this, propertyMap); // Paper - optimise state lookup + } + + public <T extends Comparable<T>> S cycle(Property<T> property) { +@@ -86,11 +84,11 @@ public abstract class StateHolder<O, S> { + } + + public <T extends Comparable<T>> boolean hasProperty(Property<T> property) { +- return this.optimisedTable.get(property) != null; // Paper - optimise state lookup ++ return this.values.containsKey(property); + } + + public <T extends Comparable<T>> T getValue(Property<T> property) { +- Comparable<?> comparable = this.optimisedTable.get(property); // Paper - optimise state lookup ++ Comparable<?> comparable = this.values.get(property); + if (comparable == null) { + throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner); + } else { +@@ -99,18 +97,24 @@ public abstract class StateHolder<O, S> { + } + + public <T extends Comparable<T>> Optional<T> getOptionalValue(Property<T> property) { +- Comparable<?> comparable = this.optimisedTable.get(property); // Paper - optimise state lookup ++ Comparable<?> comparable = this.values.get(property); + return comparable == null ? Optional.empty() : Optional.of(property.getValueClass().cast(comparable)); + } + + public <T extends Comparable<T>, V extends T> S setValue(Property<T> property, V value) { +- // Paper start - optimise state lookup +- final S ret = (S)this.optimisedTable.get(property, value); +- if (ret == null) { +- throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); ++ Comparable<?> comparable = this.values.get(property); ++ if (comparable == null) { ++ throw new IllegalArgumentException("Cannot set property " + property + " as it does not exist in " + this.owner); ++ } else if (comparable.equals(value)) { ++ return (S)this; ++ } else { ++ S object = this.neighbours.get(property, value); ++ if (object == null) { ++ throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); ++ } else { ++ return object; ++ } + } +- return ret; +- // Paper end - optimise state lookup + } + + public <T extends Comparable<T>, V extends T> S trySetValue(Property<T> property, V value) { +@@ -143,7 +147,7 @@ public abstract class StateHolder<O, S> { + } + } + +- this.neighbours = (Table<Property<?>, Comparable<?>, S>)(table.isEmpty() ? table : ArrayTable.create(table)); this.optimisedTable.loadInTable((Table)this.neighbours, this.values); // Paper - optimise state lookup ++ this.neighbours = (Table<Property<?>, Comparable<?>, S>)(table.isEmpty() ? table : ArrayTable.create(table)); + } + } + +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java +index ff5fd91257c4554c523682009efe1db83f53fd5b..b63116b333b6e06494091a82588acfb639bddb71 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java +@@ -7,13 +7,6 @@ import java.util.Optional; + public class BooleanProperty extends Property<Boolean> { + private final ImmutableSet<Boolean> values = ImmutableSet.of(true, false); + +- // Paper start - optimise iblockdata state lookup +- @Override +- public final int getIdFor(final Boolean value) { +- return value.booleanValue() ? 1 : 0; +- } +- // Paper end - optimise iblockdata state lookup +- + protected BooleanProperty(String name) { + super(name, Boolean.class); + } +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java +index 498c5abe0a9d024d77029719c621c1c8485791f3..3097298fe356df98967cf4bdeaaede69dfe8a441 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java +@@ -15,15 +15,6 @@ public class EnumProperty<T extends Enum<T> & StringRepresentable> extends Prope + private final ImmutableSet<T> values; + private final Map<String, T> names = Maps.newHashMap(); + +- // Paper start - optimise iblockdata state lookup +- private int[] idLookupTable; +- +- @Override +- public final int getIdFor(final T value) { +- return this.idLookupTable[value.ordinal()]; +- } +- // Paper end - optimise iblockdata state lookup +- + protected EnumProperty(String name, Class<T> type, Collection<T> values) { + super(name, type); + this.values = ImmutableSet.copyOf(values); +@@ -36,14 +27,6 @@ public class EnumProperty<T extends Enum<T> & StringRepresentable> extends Prope + + this.names.put(string, enum_); + } +- // Paper start - optimise BlockState lookup +- int id = 0; +- this.idLookupTable = new int[type.getEnumConstants().length]; +- java.util.Arrays.fill(this.idLookupTable, -1); +- for (final T value : this.getPossibleValues()) { +- this.idLookupTable[value.ordinal()] = id++; +- } +- // Paper end - optimise BlockState lookup + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java +index 977504f2641d0133a572b0d5de85d058609343bb..3a850321a4bcc68058483b5fd53e829c425a68af 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java +@@ -11,16 +11,6 @@ public class IntegerProperty extends Property<Integer> { + public final int min; + public final int max; + +- // Paper start - optimise iblockdata state lookup +- @Override +- public final int getIdFor(final Integer value) { +- final int val = value.intValue(); +- final int ret = val - this.min; +- +- return ret | ((this.max - ret) >> 31); +- } +- // Paper end - optimise iblockdata state lookup +- + protected IntegerProperty(String name, int min, int max) { + super(name, Integer.class); + if (min < 0) { +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java +index b9493e3762410aca8e683c32b5aef187c0bee082..9055f15af0cae55effa6942913a9d7edf3857e07 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java +@@ -24,17 +24,6 @@ public abstract class Property<T extends Comparable<T>> { + ); + private final Codec<Property.Value<T>> valueCodec = this.codec.xmap(this::value, Property.Value::value); + +- // Paper start - optimise iblockdata state lookup +- private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger(); +- private final int id = ID_GENERATOR.getAndIncrement(); +- +- public final int getId() { +- return this.id; +- } +- +- public abstract int getIdFor(final T value); +- // Paper end - optimise state lookup +- + protected Property(String name, Class<T> type) { + this.clazz = type; + this.name = name; diff --git a/patches/server/1072-fixup-Optimize-BlockPosition-helper-methods.patch b/patches/server/1072-fixup-Optimize-BlockPosition-helper-methods.patch new file mode 100644 index 0000000000..f68e9eae92 --- /dev/null +++ b/patches/server/1072-fixup-Optimize-BlockPosition-helper-methods.patch @@ -0,0 +1,64 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf <[email protected]> +Date: Mon, 21 Oct 2024 19:13:43 -0700 +Subject: [PATCH] fixup! Optimize BlockPosition helper methods + + +diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java +index 2767d6f97e8b314d23a8e62f22dfd396f5660d31..7362d471f9c1858e0dd7832e1ff59e3e51d7f249 100644 +--- a/src/main/java/net/minecraft/core/BlockPos.java ++++ b/src/main/java/net/minecraft/core/BlockPos.java +@@ -161,7 +161,7 @@ public class BlockPos extends Vec3i { + + @Override + public BlockPos above(int distance) { +- return distance == 0 ? this : new BlockPos(this.getX(), this.getY() + distance, this.getZ()); // Paper - Perf: Optimize BlockPosition ++ return distance == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY() + distance, this.getZ()); // Paper - Perf: Optimize BlockPosition + } + + @Override +@@ -171,7 +171,7 @@ public class BlockPos extends Vec3i { + + @Override + public BlockPos below(int i) { +- return i == 0 ? this : new BlockPos(this.getX(), this.getY() - i, this.getZ()); // Paper - Perf: Optimize BlockPosition ++ return i == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY() - i, this.getZ()); // Paper - Perf: Optimize BlockPosition + } + + @Override +@@ -181,7 +181,7 @@ public class BlockPos extends Vec3i { + + @Override + public BlockPos north(int distance) { +- return distance == 0 ? this : new BlockPos(this.getX(), this.getY(), this.getZ() - distance); // Paper - Perf: Optimize BlockPosition ++ return distance == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY(), this.getZ() - distance); // Paper - Perf: Optimize BlockPosition + } + + @Override +@@ -191,7 +191,7 @@ public class BlockPos extends Vec3i { + + @Override + public BlockPos south(int distance) { +- return distance == 0 ? this : new BlockPos(this.getX(), this.getY(), this.getZ() + distance); // Paper - Perf: Optimize BlockPosition ++ return distance == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY(), this.getZ() + distance); // Paper - Perf: Optimize BlockPosition + } + + @Override +@@ -201,7 +201,7 @@ public class BlockPos extends Vec3i { + + @Override + public BlockPos west(int distance) { +- return distance == 0 ? this : new BlockPos(this.getX() - distance, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition ++ return distance == 0 ? this.immutable() : new BlockPos(this.getX() - distance, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition + } + + @Override +@@ -211,7 +211,7 @@ public class BlockPos extends Vec3i { + + @Override + public BlockPos east(int distance) { +- return distance == 0 ? this : new BlockPos(this.getX() + distance, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition ++ return distance == 0 ? this.immutable() : new BlockPos(this.getX() + distance, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition + } + + @Override diff --git a/patches/server/1073-fixup-Moonrise-optimisation-patches.patch b/patches/server/1073-fixup-Moonrise-optimisation-patches.patch new file mode 100644 index 0000000000..42a3be5122 --- /dev/null +++ b/patches/server/1073-fixup-Moonrise-optimisation-patches.patch @@ -0,0 +1,14304 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf <[email protected]> +Date: Mon, 21 Oct 2024 11:06:24 -0700 +Subject: [PATCH] fixup! Moonrise optimisation patches + + +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java b/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java +new file mode 100644 +index 0000000000000000000000000000000000000000..69a20e9d72b02f28b349f24fd0ea08736888e642 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java +@@ -0,0 +1,115 @@ ++package ca.spottedleaf.moonrise.common; ++ ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; ++import com.mojang.datafixers.DataFixer; ++import net.minecraft.core.BlockPos; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.GenerationChunkHolder; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.util.datafix.DataFixTypes; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.BlockGetter; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.ProtoChunk; ++import net.minecraft.world.level.chunk.storage.SerializableChunkData; ++import net.minecraft.world.level.entity.EntityTypeTest; ++import net.minecraft.world.phys.AABB; ++import java.util.List; ++import java.util.ServiceLoader; ++import java.util.function.Predicate; ++ ++public interface PlatformHooks { ++ public static PlatformHooks get() { ++ return Holder.INSTANCE; ++ } ++ ++ public String getBrand(); ++ ++ public int getLightEmission(final BlockState blockState, final BlockGetter world, final BlockPos pos); ++ ++ public Predicate<BlockState> maybeHasLightEmission(); ++ ++ public boolean hasCurrentlyLoadingChunk(); ++ ++ public LevelChunk getCurrentlyLoadingChunk(final GenerationChunkHolder holder); ++ ++ public void setCurrentlyLoading(final GenerationChunkHolder holder, final LevelChunk levelChunk); ++ ++ public void chunkFullStatusComplete(final LevelChunk newChunk, final ProtoChunk original); ++ ++ public boolean allowAsyncTicketUpdates(); ++ ++ public void onChunkHolderTicketChange(final ServerLevel world, final NewChunkHolder holder, final int oldLevel, final int newLevel); ++ ++ public void chunkUnloadFromWorld(final LevelChunk chunk); ++ ++ public void chunkSyncSave(final ServerLevel world, final ChunkAccess chunk, final SerializableChunkData data); ++ ++ public void onChunkWatch(final ServerLevel world, final LevelChunk chunk, final ServerPlayer player); ++ ++ public void onChunkUnWatch(final ServerLevel world, final ChunkPos chunk, final ServerPlayer player); ++ ++ public void addToGetEntities(final Level world, final Entity entity, final AABB boundingBox, final Predicate<? super Entity> predicate, ++ final List<Entity> into); ++ ++ public <T extends Entity> void addToGetEntities(final Level world, final EntityTypeTest<Entity, T> entityTypeTest, ++ final AABB boundingBox, final Predicate<? super T> predicate, ++ final List<? super T> into, final int maxCount); ++ ++ public void entityMove(final Entity entity, final long oldSection, final long newSection); ++ ++ public boolean screenEntity(final ServerLevel world, final Entity entity, final boolean fromDisk, final boolean event); ++ ++ public boolean configFixMC224294(); ++ ++ public boolean configAutoConfigSendDistance(); ++ ++ public double configPlayerMaxLoadRate(); ++ ++ public double configPlayerMaxGenRate(); ++ ++ public double configPlayerMaxSendRate(); ++ ++ public int configPlayerMaxConcurrentLoads(); ++ ++ public int configPlayerMaxConcurrentGens(); ++ ++ public long configAutoSaveInterval(); ++ ++ public int configMaxAutoSavePerTick(); ++ ++ public boolean configFixMC159283(); ++ ++ // support for CB chunk mustNotSave ++ public boolean forceNoSave(final ChunkAccess chunk); ++ ++ public CompoundTag convertNBT(final DataFixTypes type, final DataFixer dataFixer, final CompoundTag nbt, ++ final int fromVersion, final int toVersion); ++ ++ public boolean hasMainChunkLoadHook(); ++ ++ public void mainChunkLoad(final ChunkAccess chunk, final SerializableChunkData chunkData); ++ ++ public List<Entity> modifySavedEntities(final ServerLevel world, final int chunkX, final int chunkZ, final List<Entity> entities); ++ ++ public void unloadEntity(final Entity entity); ++ ++ public int modifyEntityTrackingRange(final Entity entity, final int currentRange); ++ ++ public static final class Holder { ++ private Holder() { ++ } ++ ++ private static final PlatformHooks INSTANCE; ++ ++ static { ++ INSTANCE = ServiceLoader.load(PlatformHooks.class, PlatformHooks.class.getClassLoader()).findFirst() ++ .orElseThrow(() -> new RuntimeException("Failed to locate PlatformHooks")); ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java +index ba68998f6ef57b24c72fd833bd7de440de9501cc..7fed43a1e7bcf35c4d7fd3224837a47fedd59860 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java +@@ -13,15 +13,15 @@ import java.util.NoSuchElementException; + */ + public final class EntityList implements Iterable<Entity> { + +- protected final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f); ++ private final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f); + { + this.entityToIndex.defaultReturnValue(Integer.MIN_VALUE); + } + +- protected static final Entity[] EMPTY_LIST = new Entity[0]; ++ private static final Entity[] EMPTY_LIST = new Entity[0]; + +- protected Entity[] entities = EMPTY_LIST; +- protected int count; ++ private Entity[] entities = EMPTY_LIST; ++ private int count; + + public int size() { + return this.count; +@@ -94,10 +94,9 @@ public final class EntityList implements Iterable<Entity> { + + @Override + public Iterator<Entity> iterator() { +- return new Iterator<Entity>() { +- +- Entity lastRet; +- int current; ++ return new Iterator<>() { ++ private Entity lastRet; ++ private int current; + + @Override + public boolean hasNext() { +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java +deleted file mode 100644 +index fcfbca333234c09f7c056bbfcd9ac8860b20a8db..0000000000000000000000000000000000000000 +--- a/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java ++++ /dev/null +@@ -1,125 +0,0 @@ +-package ca.spottedleaf.moonrise.common.list; +- +-import it.unimi.dsi.fastutil.longs.LongIterator; +-import it.unimi.dsi.fastutil.shorts.Short2LongOpenHashMap; +-import java.util.Arrays; +-import net.minecraft.world.level.block.Block; +-import net.minecraft.world.level.block.state.BlockState; +-import net.minecraft.world.level.chunk.GlobalPalette; +- +-public final class IBlockDataList { +- +- private static final GlobalPalette<BlockState> GLOBAL_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY); +- +- // map of location -> (index | (location << 16) | (palette id << 32)) +- private final Short2LongOpenHashMap map = new Short2LongOpenHashMap(2, 0.8f); +- { +- this.map.defaultReturnValue(Long.MAX_VALUE); +- } +- +- private static final long[] EMPTY_LIST = new long[0]; +- +- private long[] byIndex = EMPTY_LIST; +- private int size; +- +- public static int getLocationKey(final int x, final int y, final int z) { +- return (x & 15) | (((z & 15) << 4)) | ((y & 255) << (4 + 4)); +- } +- +- public static BlockState getBlockDataFromRaw(final long raw) { +- return GLOBAL_PALETTE.valueFor((int)(raw >>> 32)); +- } +- +- public static int getIndexFromRaw(final long raw) { +- return (int)(raw & 0xFFFF); +- } +- +- public static int getLocationFromRaw(final long raw) { +- return (int)((raw >>> 16) & 0xFFFF); +- } +- +- public static long getRawFromValues(final int index, final int location, final BlockState data) { +- return (long)index | ((long)location << 16) | (((long)GLOBAL_PALETTE.idFor(data)) << 32); +- } +- +- public static long setIndexRawValues(final long value, final int index) { +- return value & ~(0xFFFF) | (index); +- } +- +- public long add(final int x, final int y, final int z, final BlockState data) { +- return this.add(getLocationKey(x, y, z), data); +- } +- +- public long add(final int location, final BlockState data) { +- final long curr = this.map.get((short)location); +- +- if (curr == Long.MAX_VALUE) { +- final int index = this.size++; +- final long raw = getRawFromValues(index, location, data); +- this.map.put((short)location, raw); +- +- if (index >= this.byIndex.length) { +- this.byIndex = Arrays.copyOf(this.byIndex, (int)Math.max(4L, this.byIndex.length * 2L)); +- } +- +- this.byIndex[index] = raw; +- return raw; +- } else { +- final int index = getIndexFromRaw(curr); +- final long raw = this.byIndex[index] = getRawFromValues(index, location, data); +- +- this.map.put((short)location, raw); +- +- return raw; +- } +- } +- +- public long remove(final int x, final int y, final int z) { +- return this.remove(getLocationKey(x, y, z)); +- } +- +- public long remove(final int location) { +- final long ret = this.map.remove((short)location); +- final int index = getIndexFromRaw(ret); +- if (ret == Long.MAX_VALUE) { +- return ret; +- } +- +- // move the entry at the end to this index +- final int endIndex = --this.size; +- final long end = this.byIndex[endIndex]; +- if (index != endIndex) { +- // not empty after this call +- this.map.put((short)getLocationFromRaw(end), setIndexRawValues(end, index)); +- } +- this.byIndex[index] = end; +- this.byIndex[endIndex] = 0L; +- +- return ret; +- } +- +- public int size() { +- return this.size; +- } +- +- public long getRaw(final int index) { +- return this.byIndex[index]; +- } +- +- public int getLocation(final int index) { +- return getLocationFromRaw(this.getRaw(index)); +- } +- +- public BlockState getData(final int index) { +- return getBlockDataFromRaw(this.getRaw(index)); +- } +- +- public void clear() { +- this.size = 0; +- this.map.clear(); +- } +- +- public LongIterator getRawIterator() { +- return this.map.values().iterator(); +- } +-} +\ No newline at end of file +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9f3b25bb2439f283f878db93973a02fcdcd14eed +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java +@@ -0,0 +1,77 @@ ++package ca.spottedleaf.moonrise.common.list; ++ ++import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; ++import java.util.Arrays; ++ ++public final class IntList { ++ ++ private final Int2IntOpenHashMap map = new Int2IntOpenHashMap(); ++ { ++ this.map.defaultReturnValue(Integer.MIN_VALUE); ++ } ++ ++ private static final int[] EMPTY_LIST = new int[0]; ++ ++ private int[] byIndex = EMPTY_LIST; ++ private int count; ++ ++ public int size() { ++ return this.count; ++ } ++ ++ public void setMinCapacity(final int len) { ++ final int[] byIndex = this.byIndex; ++ if (byIndex.length < len) { ++ this.byIndex = Arrays.copyOf(byIndex, len); ++ } ++ } ++ ++ public int getRaw(final int index) { ++ return this.byIndex[index]; ++ } ++ ++ public boolean add(final int value) { ++ final int count = this.count; ++ final int currIndex = this.map.putIfAbsent(value, count); ++ ++ if (currIndex != Integer.MIN_VALUE) { ++ return false; // already in this list ++ } ++ ++ int[] list = this.byIndex; ++ ++ if (list.length == count) { ++ // resize required ++ list = this.byIndex = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative ++ } ++ ++ list[count] = value; ++ this.count = count + 1; ++ ++ return true; ++ } ++ ++ public boolean remove(final int value) { ++ final int index = this.map.remove(value); ++ if (index == Integer.MIN_VALUE) { ++ return false; ++ } ++ ++ // move the entry at the end to this index ++ final int endIndex = --this.count; ++ final int end = this.byIndex[endIndex]; ++ if (index != endIndex) { ++ // not empty after this call ++ this.map.put(end, index); ++ } ++ this.byIndex[index] = end; ++ this.byIndex[endIndex] = 0; ++ ++ return true; ++ } ++ ++ public void clear() { ++ this.count = 0; ++ this.map.clear(); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2bae9949ef325d0001aa638150fbbdf968367e75 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java +@@ -0,0 +1,77 @@ ++package ca.spottedleaf.moonrise.common.list; ++ ++import it.unimi.dsi.fastutil.shorts.Short2ShortOpenHashMap; ++import java.util.Arrays; ++ ++public final class ShortList { ++ ++ private final Short2ShortOpenHashMap map = new Short2ShortOpenHashMap(); ++ { ++ this.map.defaultReturnValue(Short.MIN_VALUE); ++ } ++ ++ private static final short[] EMPTY_LIST = new short[0]; ++ ++ private short[] byIndex = EMPTY_LIST; ++ private short count; ++ ++ public int size() { ++ return (int)this.count; ++ } ++ ++ public short getRaw(final int index) { ++ return this.byIndex[index]; ++ } ++ ++ public void setMinCapacity(final int len) { ++ final short[] byIndex = this.byIndex; ++ if (byIndex.length < len) { ++ this.byIndex = Arrays.copyOf(byIndex, len); ++ } ++ } ++ ++ public boolean add(final short value) { ++ final int count = (int)this.count; ++ final short currIndex = this.map.putIfAbsent(value, (short)count); ++ ++ if (currIndex != Short.MIN_VALUE) { ++ return false; // already in this list ++ } ++ ++ short[] list = this.byIndex; ++ ++ if (list.length == count) { ++ // resize required ++ list = this.byIndex = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative ++ } ++ ++ list[count] = value; ++ this.count = (short)(count + 1); ++ ++ return true; ++ } ++ ++ public boolean remove(final short value) { ++ final short index = this.map.remove(value); ++ if (index == Short.MIN_VALUE) { ++ return false; ++ } ++ ++ // move the entry at the end to this index ++ final short endIndex = --this.count; ++ final short end = this.byIndex[endIndex]; ++ if (index != endIndex) { ++ // not empty after this call ++ this.map.put(end, index); ++ } ++ this.byIndex[(int)index] = end; ++ this.byIndex[(int)endIndex] = (short)0; ++ ++ return true; ++ } ++ ++ public void clear() { ++ this.count = (short)0; ++ this.map.clear(); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/LazyRunnable.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/LazyRunnable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c2d917c2eac55b8a4411a6e159f177f9428b1150 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/LazyRunnable.java +@@ -0,0 +1,22 @@ ++package ca.spottedleaf.moonrise.common.misc; ++ ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import java.lang.invoke.VarHandle; ++ ++public final class LazyRunnable implements Runnable { ++ ++ private volatile Runnable toRun; ++ private static final VarHandle TO_RUN_HANDLE = ConcurrentUtil.getVarHandle(LazyRunnable.class, "toRun", Runnable.class); ++ ++ public void setRunnable(final Runnable run) { ++ final Runnable prev = (Runnable)TO_RUN_HANDLE.compareAndExchange(this, (Runnable)null, run); ++ if (prev != null) { ++ throw new IllegalStateException("Runnable already set"); ++ } ++ } ++ ++ @Override ++ public void run() { ++ ((Runnable)TO_RUN_HANDLE.getVolatile(this)).run(); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java +index ab093b0e8ac6f762921eb1d15f5217345c4eba05..bb44de17a37082e57f2292a4f470740be1d09b11 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java +@@ -4,13 +4,17 @@ import ca.spottedleaf.moonrise.common.list.ReferenceList; + import ca.spottedleaf.moonrise.common.util.CoordinateUtils; + import ca.spottedleaf.moonrise.common.util.MoonriseConstants; + import ca.spottedleaf.moonrise.common.util.ChunkSystem; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; + import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants; ++import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel; + import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; + import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; + import net.minecraft.core.BlockPos; + import net.minecraft.server.level.ServerLevel; + import net.minecraft.server.level.ServerPlayer; + import net.minecraft.world.level.ChunkPos; ++import java.util.ArrayList; + + public final class NearbyPlayers { + +@@ -20,7 +24,27 @@ public final class NearbyPlayers { + GENERAL_REALLY_SMALL, + TICK_VIEW_DISTANCE, + VIEW_DISTANCE, +- SPAWN_RANGE, // Moonrise - chunk tick iteration ++ // Moonrise start - chunk tick iteration ++ SPAWN_RANGE { ++ @Override ++ void addTo(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { ++ ((ChunkTickServerLevel)world).moonrise$addPlayerTickingRequest(chunkX, chunkZ); ++ } ++ ++ @Override ++ void removeFrom(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { ++ ((ChunkTickServerLevel)world).moonrise$removePlayerTickingRequest(chunkX, chunkZ); ++ } ++ }; ++ // Moonrise end - chunk tick iteration ++ ++ void addTo(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { ++ ++ } ++ ++ void removeFrom(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { ++ ++ } + } + + private static final NearbyMapType[] MAP_TYPES = NearbyMapType.values(); +@@ -37,6 +61,12 @@ public final class NearbyPlayers { + private final ServerLevel world; + private final Reference2ReferenceOpenHashMap<ServerPlayer, TrackedPlayer[]> players = new Reference2ReferenceOpenHashMap<>(); + private final Long2ReferenceOpenHashMap<TrackedChunk> byChunk = new Long2ReferenceOpenHashMap<>(); ++ private final Long2ReferenceOpenHashMap<ReferenceList<ServerPlayer>>[] directByChunk = new Long2ReferenceOpenHashMap[TOTAL_MAP_TYPES]; ++ { ++ for (int i = 0; i < this.directByChunk.length; ++i) { ++ this.directByChunk[i] = new Long2ReferenceOpenHashMap<>(); ++ } ++ } + + public NearbyPlayers(final ServerLevel world) { + this.world = world; +@@ -70,6 +100,16 @@ public final class NearbyPlayers { + } + } + ++ public void clear() { ++ if (this.players.isEmpty()) { ++ return; ++ } ++ ++ for (final ServerPlayer player : new ArrayList<>(this.players.keySet())) { ++ this.removePlayer(player); ++ } ++ } ++ + public void tickPlayer(final ServerPlayer player) { + final TrackedPlayer[] players = this.players.get(player); + if (players == null) { +@@ -94,38 +134,41 @@ public final class NearbyPlayers { + return this.byChunk.get(CoordinateUtils.getChunkKey(pos)); + } + +- public ReferenceList<ServerPlayer> getPlayers(final BlockPos pos, final NearbyMapType type) { +- final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos)); ++ public TrackedChunk getChunk(final int chunkX, final int chunkZ) { ++ return this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } + +- return chunk == null ? null : chunk.players[type.ordinal()]; ++ public ReferenceList<ServerPlayer> getPlayers(final BlockPos pos, final NearbyMapType type) { ++ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos)); + } + + public ReferenceList<ServerPlayer> getPlayers(final ChunkPos pos, final NearbyMapType type) { +- final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos)); +- +- return chunk == null ? null : chunk.players[type.ordinal()]; ++ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos)); + } + + public ReferenceList<ServerPlayer> getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) { +- final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); +- +- return chunk == null ? null : chunk.players[type.ordinal()]; ++ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } + + public ReferenceList<ServerPlayer> getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) { +- final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4)); +- +- return chunk == null ? null : chunk.players[type.ordinal()]; ++ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4)); + } + + public static final class TrackedChunk { + + private static final ServerPlayer[] EMPTY_PLAYERS_ARRAY = new ServerPlayer[0]; + ++ private final long chunkKey; ++ private final NearbyPlayers nearbyPlayers; + private final ReferenceList<ServerPlayer>[] players = new ReferenceList[TOTAL_MAP_TYPES]; + private int nonEmptyLists; + private long updateCount; + ++ public TrackedChunk(final long chunkKey, final NearbyPlayers nearbyPlayers) { ++ this.chunkKey = chunkKey; ++ this.nearbyPlayers = nearbyPlayers; ++ } ++ + public boolean isEmpty() { + return this.nonEmptyLists == 0; + } +@@ -145,7 +188,9 @@ public final class NearbyPlayers { + final ReferenceList<ServerPlayer> list = this.players[idx]; + if (list == null) { + ++this.nonEmptyLists; +- (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY)).add(player); ++ final ReferenceList<ServerPlayer> players = (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY)); ++ this.nearbyPlayers.directByChunk[idx].put(this.chunkKey, players); ++ players.add(player); + return; + } + +@@ -169,6 +214,7 @@ public final class NearbyPlayers { + + if (list.size() == 0) { + this.players[idx] = null; ++ this.nearbyPlayers.directByChunk[idx].remove(this.chunkKey); + --this.nonEmptyLists; + } + } +@@ -187,9 +233,19 @@ public final class NearbyPlayers { + protected void addCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) { + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + +- NearbyPlayers.this.byChunk.computeIfAbsent(chunkKey, (final long keyInMap) -> { +- return new TrackedChunk(); +- }).addPlayer(parameter, this.type); ++ final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey); ++ final NearbyMapType type = this.type; ++ if (chunk != null) { ++ chunk.addPlayer(parameter, type); ++ type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ); ++ } else { ++ final TrackedChunk created = new TrackedChunk(chunkKey, NearbyPlayers.this); ++ NearbyPlayers.this.byChunk.put(chunkKey, created); ++ created.addPlayer(parameter, type); ++ type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ); ++ ++ ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$requestChunkData(chunkKey).nearbyPlayers = created; ++ } + } + + @Override +@@ -201,10 +257,16 @@ public final class NearbyPlayers { + throw new IllegalStateException("Chunk should exist at " + new ChunkPos(chunkKey)); + } + +- chunk.removePlayer(parameter, this.type); ++ final NearbyMapType type = this.type; ++ chunk.removePlayer(parameter, type); ++ type.removeFrom(parameter, NearbyPlayers.this.world, chunkX, chunkZ); + + if (chunk.isEmpty()) { + NearbyPlayers.this.byChunk.remove(chunkKey); ++ final ChunkData chunkData = ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$releaseChunkData(chunkKey); ++ if (chunkData != null) { ++ chunkData.nearbyPlayers = null; ++ } + } + } + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java +index 96bbab283eb6c7e7863383fea0ddc62510391091..fc029c8fb22a7c8eeb23bfc171812f6da91c60fa 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java +@@ -1,15 +1,18 @@ + package ca.spottedleaf.moonrise.common.util; + + import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.PlatformHooks; + import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; + import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk; + import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader; + import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache; ++import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel; + import com.mojang.logging.LogUtils; + import net.minecraft.server.level.ChunkHolder; + import net.minecraft.server.level.FullChunkStatus; + import net.minecraft.server.level.ServerLevel; + import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.level.progress.ChunkProgressListener; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.level.chunk.ChunkAccess; + import net.minecraft.world.level.chunk.LevelChunk; +@@ -68,6 +71,9 @@ public final class ChunkSystem { + } + + public static boolean screenEntity(final ServerLevel level, final Entity entity, final boolean fromDisk, final boolean event) { ++ if (!PlatformHooks.get().screenEntity(level, entity, fromDisk, event)) { ++ return false; ++ } + return true; + } + +@@ -76,7 +82,13 @@ public final class ChunkSystem { + } + + public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { +- ++ // Update progress listener for LevelLoadingScreen ++ final ChunkProgressListener progressListener = level.getChunkSource().chunkMap.progressListener; ++ if (progressListener != null) { ++ ChunkSystem.scheduleChunkTask(level, holder.getPos().x, holder.getPos().z, () -> { ++ progressListener.onStatusChange(holder.getPos(), null); ++ }); ++ } + } + + public static void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) { +@@ -108,16 +120,18 @@ public final class ChunkSystem { + ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() + ); + if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) { +- chunk.postProcessGeneration(); ++ chunk.postProcessGeneration((ServerLevel)chunk.getLevel()); + } + ((ServerLevel)chunk.getLevel()).startTickingChunk(chunk); + ((ServerLevel)chunk.getLevel()).getChunkSource().chunkMap.tickingGenerated.incrementAndGet(); ++ ((ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$markChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration + } + + public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { + ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove( + ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() + ); ++ ((ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$removeChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration + } + + public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java +index ac6f284ee4469d16c5655328b2488d7612832353..97848869df61648fc415e4d39f409f433202c274 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java +@@ -3,8 +3,12 @@ package ca.spottedleaf.moonrise.common.util; + public final class MixinWorkarounds { + + // mixins tries to find the owner of the clone() method, which doesn't exist and NPEs ++ // https://github.com/FabricMC/Mixin/pull/147 + public static long[] clone(final long[] values) { + return values.clone(); + } + ++ public static byte[] clone(final byte[] values) { ++ return values.clone(); ++ } + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java +index 3abe0bd2a820352b85306d554bf14a4cf6123091..c125c70a68130be373acc989053a6c0e487be924 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java +@@ -1,45 +1,100 @@ + package ca.spottedleaf.moonrise.common.util; + +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool; ++import ca.spottedleaf.concurrentutil.executor.thread.PrioritisedThreadPool; ++import ca.spottedleaf.moonrise.common.PlatformHooks; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; +-import java.io.File; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.function.Consumer; + + public final class MoonriseCommon { + + private static final Logger LOGGER = LoggerFactory.getLogger(MoonriseCommon.class); + +- // Paper start +- public static PrioritisedThreadPool WORKER_POOL; +- public static int WORKER_THREADS; +- public static void init(io.papermc.paper.configuration.GlobalConfiguration.ChunkSystem chunkSystem) { +- // Paper end ++ public static final PrioritisedThreadPool WORKER_POOL = new PrioritisedThreadPool( ++ new Consumer<>() { ++ private final AtomicInteger idGenerator = new AtomicInteger(); ++ ++ @Override ++ public void accept(Thread thread) { ++ thread.setDaemon(true); ++ thread.setName(PlatformHooks.get().getBrand() + " Common Worker #" + this.idGenerator.getAndIncrement()); ++ thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { ++ @Override ++ public void uncaughtException(final Thread thread, final Throwable throwable) { ++ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable); ++ } ++ }); ++ } ++ } ++ ); ++ public static final long WORKER_QUEUE_HOLD_TIME = (long)(20.0e6); // 20ms ++ public static final int CLIENT_DIVISION = 0; ++ public static final PrioritisedThreadPool.ExecutorGroup RENDER_EXECUTOR_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(CLIENT_DIVISION, 0); ++ public static final int SERVER_DIVISION = 1; ++ public static final PrioritisedThreadPool.ExecutorGroup PARALLEL_GEN_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(SERVER_DIVISION, 0); ++ public static final PrioritisedThreadPool.ExecutorGroup RADIUS_AWARE_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(SERVER_DIVISION, 0); ++ public static final PrioritisedThreadPool.ExecutorGroup LOAD_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(SERVER_DIVISION, 0); ++ ++ public static void adjustWorkerThreads(final int configWorkerThreads, final int configIoThreads) { + int defaultWorkerThreads = Runtime.getRuntime().availableProcessors() / 2; + if (defaultWorkerThreads <= 4) { + defaultWorkerThreads = defaultWorkerThreads <= 3 ? 1 : 2; + } else { + defaultWorkerThreads = defaultWorkerThreads / 2; + } +- defaultWorkerThreads = Integer.getInteger("Paper.WorkerThreadCount", Integer.valueOf(defaultWorkerThreads)); // Paper ++ defaultWorkerThreads = Integer.getInteger(PlatformHooks.get().getBrand() + ".WorkerThreadCount", Integer.valueOf(defaultWorkerThreads)); + +- int workerThreads = chunkSystem.workerThreads; // Paper ++ int workerThreads = configWorkerThreads; + + if (workerThreads <= 0) { + workerThreads = defaultWorkerThreads; + } + +- WORKER_POOL = new PrioritisedThreadPool( +- "Paper Worker Pool", workerThreads, // Paper +- (final Thread thread, final Integer id) -> { +- thread.setName("Paper Common Worker #" + id.intValue()); // Paper ++ final int ioThreads = Math.max(1, configIoThreads); ++ ++ WORKER_POOL.adjustThreadCount(workerThreads); ++ IO_POOL.adjustThreadCount(ioThreads); ++ ++ LOGGER.info(PlatformHooks.get().getBrand() + " is using " + workerThreads + " worker threads, " + ioThreads + " I/O threads"); ++ } ++ ++ public static final PrioritisedThreadPool IO_POOL = new PrioritisedThreadPool( ++ new Consumer<>() { ++ private final AtomicInteger idGenerator = new AtomicInteger(); ++ ++ @Override ++ public void accept(final Thread thread) { ++ thread.setDaemon(true); ++ thread.setName(PlatformHooks.get().getBrand() + " I/O Worker #" + this.idGenerator.getAndIncrement()); + thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(final Thread thread, final Throwable throwable) { + LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable); + } + }); +- }, (long)(20.0e6)); // 20ms +- WORKER_THREADS = workerThreads; ++ } ++ } ++ ); ++ public static final long IO_QUEUE_HOLD_TIME = (long)(100.0e6); // 100ms ++ public static final PrioritisedThreadPool.ExecutorGroup CLIENT_PROFILER_IO_GROUP = IO_POOL.createExecutorGroup(CLIENT_DIVISION, 0); ++ public static final PrioritisedThreadPool.ExecutorGroup SERVER_REGION_IO_GROUP = IO_POOL.createExecutorGroup(SERVER_DIVISION, 0); ++ ++ public static void haltExecutors() { ++ MoonriseCommon.WORKER_POOL.shutdown(false); ++ LOGGER.info("Awaiting termination of worker pool for up to 60s..."); ++ if (!MoonriseCommon.WORKER_POOL.join(TimeUnit.SECONDS.toMillis(60L))) { ++ LOGGER.error("Worker pool did not shut down in time!"); ++ MoonriseCommon.WORKER_POOL.halt(false); ++ } ++ ++ MoonriseCommon.IO_POOL.shutdown(false); ++ LOGGER.info("Awaiting termination of I/O pool for up to 60s..."); ++ if (!MoonriseCommon.IO_POOL.join(TimeUnit.SECONDS.toMillis(60L))) { ++ LOGGER.error("I/O pool did not shut down in time!"); ++ MoonriseCommon.IO_POOL.halt(false); ++ } + } + + private MoonriseCommon() {} +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java +index 1cf32d7d1bbc8a0a3f7cb9024c793f6744199f64..559c959aff3c9deef867b9e425fba3e2e669cac6 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java +@@ -1,8 +1,10 @@ + package ca.spottedleaf.moonrise.common.util; + ++import ca.spottedleaf.moonrise.common.PlatformHooks; ++ + public final class MoonriseConstants { + +- public static final int MAX_VIEW_DISTANCE = 32; ++ public static final int MAX_VIEW_DISTANCE = Integer.getInteger(PlatformHooks.get().getBrand() + ".MaxViewDistance", 32); + + private MoonriseConstants() {} + +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleRandom.java b/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleRandom.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a9ff1c1a70faf4b7a64b265932f07a8b8f00c1ff +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleRandom.java +@@ -0,0 +1,52 @@ ++package ca.spottedleaf.moonrise.common.util; ++ ++import net.minecraft.world.level.levelgen.LegacyRandomSource; ++ ++/** ++ * Avoid costly CAS of superclass ++ */ ++public final class SimpleRandom extends LegacyRandomSource { ++ ++ private static final long MULTIPLIER = 25214903917L; ++ private static final long ADDEND = 11L; ++ private static final int BITS = 48; ++ private static final long MASK = (1L << BITS) - 1; ++ ++ private long value; ++ ++ public SimpleRandom(final long seed) { ++ super(0L); ++ this.value = seed; ++ } ++ ++ @Override ++ public void setSeed(final long seed) { ++ this.value = (seed ^ MULTIPLIER) & MASK; ++ } ++ ++ private long advanceSeed() { ++ return this.value = ((this.value * MULTIPLIER) + ADDEND) & MASK; ++ } ++ ++ @Override ++ public int next(final int bits) { ++ return (int)(this.advanceSeed() >>> (BITS - bits)); ++ } ++ ++ @Override ++ public int nextInt() { ++ final long seed = this.advanceSeed(); ++ return (int)(seed >>> (BITS - Integer.SIZE)); ++ } ++ ++ @Override ++ public int nextInt(final int bound) { ++ if (bound <= 0) { ++ throw new IllegalArgumentException(); ++ } ++ ++ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ ++ final long value = this.advanceSeed() >>> (BITS - Integer.SIZE); ++ return (int)((value * (long)bound) >>> Integer.SIZE); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java +index 11b7f15755dde766140c29bedca456c80d53293f..217d1f908a36a5177ba3cbb80a33f73d4dab0fa0 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java +@@ -77,11 +77,15 @@ public class TickThread extends Thread { + } + + public TickThread(final Runnable run, final String name) { +- this(run, name, ID_GENERATOR.incrementAndGet()); ++ this(null, run, name); + } + +- private TickThread(final Runnable run, final String name, final int id) { +- super(run, name); ++ public TickThread(final ThreadGroup group, final Runnable run, final String name) { ++ this(group, run, name, ID_GENERATOR.incrementAndGet()); ++ } ++ ++ private TickThread(final ThreadGroup group, final Runnable run, final String name, final int id) { ++ super(group, run, name); + this.id = id; + } + +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java +index af9623240ff2d389aa7090623f507720e7dbab7d..efda2688ae1254a82ba7f6bf8bf597ef224cbb86 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java +@@ -8,11 +8,19 @@ public final class WorldUtil { + // min, max are inclusive + + public static int getMaxSection(final LevelHeightAccessor world) { +- return world.getMaxSection() - 1; // getMaxSection() is exclusive ++ return world.getMaxSectionY(); ++ } ++ ++ public static int getMaxSection(final Level world) { ++ return world.getMaxSectionY(); + } + + public static int getMinSection(final LevelHeightAccessor world) { +- return world.getMinSection(); ++ return world.getMinSectionY(); ++ } ++ ++ public static int getMinSection(final Level world) { ++ return world.getMinSectionY(); + } + + public static int getMaxLightSection(final LevelHeightAccessor world) { +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java +index aef4fc0d3c272febe675d1ac846b88e58b4e7533..93bc56daec4526f373c84763b8c7ccb4a30e800b 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java +@@ -1,10 +1,10 @@ + package ca.spottedleaf.moonrise.patches.block_counting; + + import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +-import it.unimi.dsi.fastutil.ints.IntArrayList; ++import it.unimi.dsi.fastutil.shorts.ShortArrayList; + + public interface BlockCountingBitStorage { + +- public Int2ObjectOpenHashMap<IntArrayList> moonrise$countEntries(); ++ public Int2ObjectOpenHashMap<ShortArrayList> moonrise$countEntries(); + + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java +index a08ddb0598d44368af5b6bace971ee31edf9919e..0d1443a113c07d7655e7b927a899447f70db8fa9 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java +@@ -1,11 +1,11 @@ + package ca.spottedleaf.moonrise.patches.block_counting; + +-import ca.spottedleaf.moonrise.common.list.IBlockDataList; ++import ca.spottedleaf.moonrise.common.list.ShortList; + + public interface BlockCountingChunkSection { + +- public int moonrise$getSpecialCollidingBlocks(); ++ public boolean moonrise$hasSpecialCollidingBlocks(); + +- public IBlockDataList moonrise$getTickingBlockList(); ++ public ShortList moonrise$getTickingBlockList(); + + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java +new file mode 100644 +index 0000000000000000000000000000000000000000..89e75b454695e174c5619104eeb15eb923a2d9a7 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java +@@ -0,0 +1,12 @@ ++package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess; ++ ++public interface PropertyAccess<T> { ++ ++ public int moonrise$getId(); ++ ++ public int moonrise$getIdFor(final T value); ++ ++ public T moonrise$getById(final int id); ++ ++ public void moonrise$setById(final T[] values); ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..01da52b9e8a786824f199a057b62ce0431ecbc43 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java +@@ -0,0 +1,7 @@ ++package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess; ++ ++public interface PropertyAccessStateHolder { ++ ++ public long moonrise$getTableIndex(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b5335a2a8cb5dc7637c7112c8f7193389d726489 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java +@@ -0,0 +1,230 @@ ++package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util; ++ ++import ca.spottedleaf.concurrentutil.util.IntegerUtil; ++import ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess; ++import ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccessStateHolder; ++import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.objects.AbstractObjectSet; ++import it.unimi.dsi.fastutil.objects.AbstractReference2ObjectMap; ++import it.unimi.dsi.fastutil.objects.ObjectIterator; ++import it.unimi.dsi.fastutil.objects.ObjectSet; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import java.util.ArrayList; ++import java.util.Collection; ++import java.util.Collections; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Map; ++import net.minecraft.world.level.block.state.StateHolder; ++import net.minecraft.world.level.block.state.properties.Property; ++ ++public final class ZeroCollidingReferenceStateTable<O, S> { ++ ++ private final Int2ObjectOpenHashMap<Indexer> propertyToIndexer; ++ private S[] lookup; ++ private final Collection<Property<?>> properties; ++ ++ public ZeroCollidingReferenceStateTable(final Collection<Property<?>> properties) { ++ this.propertyToIndexer = new Int2ObjectOpenHashMap<>(properties.size()); ++ this.properties = new ReferenceOpenHashSet<>(properties); ++ ++ final List<Property<?>> sortedProperties = new ArrayList<>(properties); ++ ++ // important that each table sees the same property order given the same _set_ of properties, ++ // as each table will calculate the index for the block state ++ sortedProperties.sort((final Property<?> p1, final Property<?> p2) -> { ++ return Integer.compare( ++ ((PropertyAccess<?>)p1).moonrise$getId(), ++ ((PropertyAccess<?>)p2).moonrise$getId() ++ ); ++ }); ++ ++ int currentMultiple = 1; ++ for (final Property<?> property : sortedProperties) { ++ final int totalValues = property.getPossibleValues().size(); ++ ++ this.propertyToIndexer.put( ++ ((PropertyAccess<?>)property).moonrise$getId(), ++ new Indexer( ++ totalValues, ++ currentMultiple, ++ IntegerUtil.getUnsignedDivisorMagic((long)currentMultiple, 32), ++ IntegerUtil.getUnsignedDivisorMagic((long)totalValues, 32) ++ ) ++ ); ++ ++ currentMultiple *= totalValues; ++ } ++ } ++ ++ public <T extends Comparable<T>> boolean hasProperty(final Property<T> property) { ++ return this.propertyToIndexer.containsKey(((PropertyAccess<T>)property).moonrise$getId()); ++ } ++ ++ public long getIndex(final StateHolder<O, S> stateHolder) { ++ long ret = 0L; ++ ++ for (final Map.Entry<Property<?>, Comparable<?>> entry : stateHolder.getValues().entrySet()) { ++ final Property<?> property = entry.getKey(); ++ final Comparable<?> value = entry.getValue(); ++ ++ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<?>)property).moonrise$getId()); ++ ++ ret += (((PropertyAccess)property).moonrise$getIdFor(value)) * indexer.multiple; ++ } ++ ++ return ret; ++ } ++ ++ public boolean isLoaded() { ++ return this.lookup != null; ++ } ++ ++ public void loadInTable(final Map<Map<Property<?>, Comparable<?>>, S> universe) { ++ if (this.lookup != null) { ++ throw new IllegalStateException(); ++ } ++ ++ this.lookup = (S[])new StateHolder[universe.size()]; ++ ++ for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : universe.entrySet()) { ++ final S value = entry.getValue(); ++ if (value == null) { ++ continue; ++ } ++ this.lookup[(int)((PropertyAccessStateHolder)(StateHolder<O, S>)value).moonrise$getTableIndex()] = value; ++ } ++ ++ for (final S value : this.lookup) { ++ if (value == null) { ++ throw new IllegalStateException(); ++ } ++ } ++ } ++ ++ public <T extends Comparable<T>> T get(final long index, final Property<T> property) { ++ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<T>)property).moonrise$getId()); ++ if (indexer == null) { ++ return null; ++ } ++ ++ final long divided = (index * indexer.multipleDivMagic) >>> 32; ++ final long modded = (((divided * indexer.modMagic) & 0xFFFFFFFFL) * indexer.totalValues) >>> 32; ++ // equiv to: divided = index / multiple ++ // modded = divided % totalValues ++ ++ return ((PropertyAccess<T>)property).moonrise$getById((int)modded); ++ } ++ ++ public <T extends Comparable<T>> S set(final long index, final Property<T> property, final T with) { ++ final int newValueId = ((PropertyAccess<T>)property).moonrise$getIdFor(with); ++ if (newValueId < 0) { ++ return null; ++ } ++ ++ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<T>)property).moonrise$getId()); ++ if (indexer == null) { ++ return null; ++ } ++ ++ final long divided = (index * indexer.multipleDivMagic) >>> 32; ++ final long modded = (((divided * indexer.modMagic) & 0xFFFFFFFFL) * indexer.totalValues) >>> 32; ++ // equiv to: divided = index / multiple ++ // modded = divided % totalValues ++ ++ // subtract out the old value, add in the new ++ final long newIndex = (((long)newValueId - modded) * indexer.multiple) + index; ++ ++ return this.lookup[(int)newIndex]; ++ } ++ ++ public <T extends Comparable<T>> S trySet(final long index, final Property<T> property, final T with, final S dfl) { ++ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<T>)property).moonrise$getId()); ++ if (indexer == null) { ++ return dfl; ++ } ++ ++ final int newValueId = ((PropertyAccess<T>)property).moonrise$getIdFor(with); ++ if (newValueId < 0) { ++ return null; ++ } ++ ++ final long divided = (index * indexer.multipleDivMagic) >>> 32; ++ final long modded = (((divided * indexer.modMagic) & 0xFFFFFFFFL) * indexer.totalValues) >>> 32; ++ // equiv to: divided = index / multiple ++ // modded = divided % totalValues ++ ++ // subtract out the old value, add in the new ++ final long newIndex = (((long)newValueId - modded) * indexer.multiple) + index; ++ ++ return this.lookup[(int)newIndex]; ++ } ++ ++ public Collection<Property<?>> getProperties() { ++ return Collections.unmodifiableCollection(this.properties); ++ } ++ ++ public Map<Property<?>, Comparable<?>> getMapView(final long stateIndex) { ++ return new MapView(stateIndex); ++ } ++ ++ private static final record Indexer( ++ int totalValues, int multiple, long multipleDivMagic, long modMagic ++ ) {} ++ ++ private class MapView extends AbstractReference2ObjectMap<Property<?>, Comparable<?>> { ++ private final long stateIndex; ++ private EntrySet entrySet; ++ ++ MapView(final long stateIndex) { ++ this.stateIndex = stateIndex; ++ } ++ ++ @Override ++ public boolean containsKey(final Object key) { ++ return key instanceof Property<?> prop && ZeroCollidingReferenceStateTable.this.hasProperty(prop); ++ } ++ ++ @Override ++ public int size() { ++ return ZeroCollidingReferenceStateTable.this.properties.size(); ++ } ++ ++ @Override ++ public ObjectSet<Entry<Property<?>, Comparable<?>>> reference2ObjectEntrySet() { ++ if (this.entrySet == null) ++ this.entrySet = new EntrySet(); ++ return this.entrySet; ++ } ++ ++ @Override ++ public Comparable<?> get(final Object key) { ++ return key instanceof Property<?> prop ? ZeroCollidingReferenceStateTable.this.get(this.stateIndex, prop) : null; ++ } ++ ++ class EntrySet extends AbstractObjectSet<Entry<Property<?>, Comparable<?>>> { ++ @Override ++ public ObjectIterator<Reference2ObjectMap.Entry<Property<?>, Comparable<?>>> iterator() { ++ final Iterator<Property<?>> propIterator = ZeroCollidingReferenceStateTable.this.properties.iterator(); ++ return new ObjectIterator<>() { ++ @Override ++ public boolean hasNext() { ++ return propIterator.hasNext(); ++ } ++ ++ @Override ++ public Entry<Property<?>, Comparable<?>> next() { ++ Property<?> prop = propIterator.next(); ++ return new AbstractReference2ObjectMap.BasicEntry<>(prop, ZeroCollidingReferenceStateTable.this.get(MapView.this.stateIndex, prop)); ++ } ++ }; ++ } ++ ++ @Override ++ public int size() { ++ return ZeroCollidingReferenceStateTable.this.properties.size(); ++ } ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java +index 5fca5dc7cdfa976bcc58dfcf0d14abb78a931475..cee2c8efbe280e20c63e2d66464dc835cd328e4a 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java +@@ -1,5 +1,6 @@ + package ca.spottedleaf.moonrise.patches.chunk_system; + ++import ca.spottedleaf.moonrise.common.PlatformHooks; + import net.minecraft.SharedConstants; + import net.minecraft.nbt.CompoundTag; + import net.minecraft.nbt.Tag; +@@ -25,21 +26,13 @@ public final class ChunkSystemConverters { + public static CompoundTag convertPoiCompoundTag(final CompoundTag data, final ServerLevel world) { + final int dataVersion = getDataVersion(data, DEFAULT_POI_DATA_VERSION); + +- // Paper start - dataconverter +- return ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag( +- ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.POI_CHUNK, data, dataVersion, getCurrentVersion() +- ); +- // Paper end - dataconverter ++ return PlatformHooks.get().convertNBT(DataFixTypes.POI_CHUNK, world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion()); + } + + public static CompoundTag convertEntityChunkCompoundTag(final CompoundTag data, final ServerLevel world) { + final int dataVersion = getDataVersion(data, DEFAULT_ENTITY_CHUNK_DATA_VERSION); + +- // Paper start - dataconverter +- return ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag( +- ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ENTITY_CHUNK, data, dataVersion, getCurrentVersion() +- ); +- // Paper end - dataconverter ++ return PlatformHooks.get().convertNBT(DataFixTypes.ENTITY_CHUNK, world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion()); + } + + private ChunkSystemConverters() {} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java +deleted file mode 100644 +index 67f6dd9a4855611cfe242c2e37e90f6d27d4c823..0000000000000000000000000000000000000000 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java ++++ /dev/null +@@ -1,36 +0,0 @@ +-package ca.spottedleaf.moonrise.patches.chunk_system; +- +-import ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData; +-import net.minecraft.nbt.CompoundTag; +-import net.minecraft.server.level.ServerLevel; +-import net.minecraft.world.level.chunk.ChunkAccess; +- +-public final class ChunkSystemFeatures { +- +- public static boolean supportsAsyncChunkSave() { +- // uncertain how to properly pass AsyncSaveData to ChunkSerializer#write +- // additionally, there may be mods hooking into the write() call which may not be thread-safe to call +- return true; +- } +- +- public static AsyncChunkSaveData getAsyncSaveData(final ServerLevel world, final ChunkAccess chunk) { +- return net.minecraft.world.level.chunk.storage.ChunkSerializer.getAsyncSaveData(world, chunk); +- } +- +- public static CompoundTag saveChunkAsync(final ServerLevel world, final ChunkAccess chunk, final AsyncChunkSaveData asyncSaveData) { +- return net.minecraft.world.level.chunk.storage.ChunkSerializer.saveChunk(world, chunk, asyncSaveData); +- } +- +- public static boolean forceNoSave(final ChunkAccess chunk) { +- // support for CB chunk mustNotSave +- return chunk instanceof net.minecraft.world.level.chunk.LevelChunk levelChunk && levelChunk.mustNotSave; +- } +- +- public static boolean supportsAsyncChunkDeserialization() { +- // as it stands, the current problem with supporting this in Moonrise is that we are unsure that any mods +- // hooking into ChunkSerializer#read() are thread-safe to call +- return true; +- } +- +- private ChunkSystemFeatures() {} +-} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java +deleted file mode 100644 +index becd1c6d54ed6c912aee3a9178a970e2751d3694..0000000000000000000000000000000000000000 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java ++++ /dev/null +@@ -1,11 +0,0 @@ +-package ca.spottedleaf.moonrise.patches.chunk_system.async_save; +- +-import net.minecraft.nbt.ListTag; +-import net.minecraft.nbt.Tag; +- +-public record AsyncChunkSaveData( +- Tag blockTickList, // non-null if we had to go to the server's tick list +- Tag fluidTickList, // non-null if we had to go to the server's tick list +- ListTag blockEntities, +- long worldTime +-) {} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java +index 2c279854bdf214538380fa354e4298ec4bd9ac4e..c7da23900228aab3a5673eb5adfada5091140319 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java +@@ -1,5 +1,6 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.entity; + ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; + import net.minecraft.server.level.FullChunkStatus; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.entity.monster.Shulker; +@@ -19,6 +20,10 @@ public interface ChunkSystemEntity { + + public void moonrise$setChunkStatus(final FullChunkStatus status); + ++ public ChunkData moonrise$getChunkData(); ++ ++ public void moonrise$setChunkData(final ChunkData chunkData); ++ + public int moonrise$getSectionX(); + + public void moonrise$setSectionX(final int x); +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java +index 73df26b27146bbad2106d57b22dd3c792ed3dd1d..a814512fcfb85312474ae2c2c21443843bf57831 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java +@@ -1,5 +1,6 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.io; + ++import net.minecraft.nbt.CompoundTag; + import net.minecraft.world.level.chunk.storage.RegionFile; + import java.io.IOException; + +@@ -11,4 +12,20 @@ public interface ChunkSystemRegionFileStorage { + + public RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; + ++ public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite( ++ final int chunkX, final int chunkZ, final CompoundTag compound ++ ) throws IOException; ++ ++ public void moonrise$finishWrite( ++ final int chunkX, final int chunkZ, final MoonriseRegionFileIO.RegionDataController.WriteData writeData ++ ) throws IOException; ++ ++ public MoonriseRegionFileIO.RegionDataController.ReadData moonrise$readData( ++ final int chunkX, final int chunkZ ++ ) throws IOException; ++ ++ // if the return value is null, then the caller needs to re-try with a new call to readData() ++ public CompoundTag moonrise$finishRead( ++ final int chunkX, final int chunkZ, final MoonriseRegionFileIO.RegionDataController.ReadData readData ++ ) throws IOException; + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java +new file mode 100644 +index 0000000000000000000000000000000000000000..99f6f3e58b11b8967e6f1c3391c190d9a860ab7f +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java +@@ -0,0 +1,1707 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.io; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.completable.CallbackCompletable; ++import ca.spottedleaf.concurrentutil.completable.Completable; ++import ca.spottedleaf.concurrentutil.executor.Cancellable; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.queue.PrioritisedTaskQueue; ++import ca.spottedleaf.concurrentutil.function.BiLong1Function; ++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.TickThread; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.chunk.storage.RegionFile; ++import net.minecraft.world.level.chunk.storage.RegionFileStorage; ++import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; ++import java.io.DataInputStream; ++import java.io.DataOutputStream; ++import java.io.IOException; ++import java.lang.invoke.VarHandle; ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.CompletionException; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.function.BiConsumer; ++import java.util.function.Consumer; ++ ++public final class MoonriseRegionFileIO { ++ ++ private static final int REGION_FILE_SHIFT = 5; ++ private static final Logger LOGGER = LoggerFactory.getLogger(MoonriseRegionFileIO.class); ++ ++ /** ++ * The types of RegionFiles controlled by the I/O thread(s). ++ */ ++ public static enum RegionFileType { ++ CHUNK_DATA, ++ POI_DATA, ++ ENTITY_DATA; ++ } ++ ++ public static RegionDataController getControllerFor(final ServerLevel world, final RegionFileType type) { ++ switch (type) { ++ case CHUNK_DATA: ++ return ((ChunkSystemServerLevel)world).moonrise$getChunkDataController(); ++ case POI_DATA: ++ return ((ChunkSystemServerLevel)world).moonrise$getPoiChunkDataController(); ++ case ENTITY_DATA: ++ return ((ChunkSystemServerLevel)world).moonrise$getEntityChunkDataController(); ++ default: ++ throw new IllegalStateException("Unknown controller type " + type); ++ } ++ } ++ ++ private static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values(); ++ ++ /** ++ * Collects RegionFile data for a certain chunk. ++ */ ++ public static final class RegionFileData { ++ ++ private final boolean[] hasResult = new boolean[CACHED_REGIONFILE_TYPES.length]; ++ private final CompoundTag[] data = new CompoundTag[CACHED_REGIONFILE_TYPES.length]; ++ private final Throwable[] throwables = new Throwable[CACHED_REGIONFILE_TYPES.length]; ++ ++ /** ++ * Sets the result associated with the specified RegionFile type. Note that ++ * results can only be set once per RegionFile type. ++ * ++ * @param type The RegionFile type. ++ * @param data The result to set. ++ */ ++ public void setData(final MoonriseRegionFileIO.RegionFileType type, final CompoundTag data) { ++ final int index = type.ordinal(); ++ ++ if (this.hasResult[index]) { ++ throw new IllegalArgumentException("Result already exists for type " + type); ++ } ++ this.hasResult[index] = true; ++ this.data[index] = data; ++ } ++ ++ /** ++ * Sets the result associated with the specified RegionFile type. Note that ++ * results can only be set once per RegionFile type. ++ * ++ * @param type The RegionFile type. ++ * @param throwable The result to set. ++ */ ++ public void setThrowable(final MoonriseRegionFileIO.RegionFileType type, final Throwable throwable) { ++ final int index = type.ordinal(); ++ ++ if (this.hasResult[index]) { ++ throw new IllegalArgumentException("Result already exists for type " + type); ++ } ++ this.hasResult[index] = true; ++ this.throwables[index] = throwable; ++ } ++ ++ /** ++ * Returns whether there is a result for the specified RegionFile type. ++ * ++ * @param type Specified RegionFile type. ++ * ++ * @return Whether a result exists for {@code type}. ++ */ ++ public boolean hasResult(final MoonriseRegionFileIO.RegionFileType type) { ++ return this.hasResult[type.ordinal()]; ++ } ++ ++ /** ++ * Returns the data result for the RegionFile type. ++ * ++ * @param type Specified RegionFile type. ++ * ++ * @throws IllegalArgumentException If the result has not been set for {@code type}. ++ * @return The data result for the specified type. If the result is a {@code Throwable}, ++ * then returns {@code null}. ++ */ ++ public CompoundTag getData(final MoonriseRegionFileIO.RegionFileType type) { ++ final int index = type.ordinal(); ++ ++ if (!this.hasResult[index]) { ++ throw new IllegalArgumentException("Result does not exist for type " + type); ++ } ++ ++ return this.data[index]; ++ } ++ ++ /** ++ * Returns the throwable result for the RegionFile type. ++ * ++ * @param type Specified RegionFile type. ++ * ++ * @throws IllegalArgumentException If the result has not been set for {@code type}. ++ * @return The throwable result for the specified type. If the result is an {@code CompoundTag}, ++ * then returns {@code null}. ++ */ ++ public Throwable getThrowable(final MoonriseRegionFileIO.RegionFileType type) { ++ final int index = type.ordinal(); ++ ++ if (!this.hasResult[index]) { ++ throw new IllegalArgumentException("Result does not exist for type " + type); ++ } ++ ++ return this.throwables[index]; ++ } ++ } ++ ++ public static void flushRegionStorages(final ServerLevel world) throws IOException { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ flushRegionStorages(world, type); ++ } ++ } ++ ++ public static void flushRegionStorages(final ServerLevel world, final RegionFileType type) throws IOException { ++ getControllerFor(world, type).getCache().flush(); ++ } ++ ++ public static void flush(final MinecraftServer server) { ++ for (final ServerLevel world : server.getAllLevels()) { ++ flush(world); ++ } ++ } ++ ++ public static void flush(final ServerLevel world) { ++ for (final RegionFileType regionFileType : CACHED_REGIONFILE_TYPES) { ++ flush(world, regionFileType); ++ } ++ } ++ ++ public static void flush(final ServerLevel world, final RegionFileType type) { ++ final RegionDataController taskController = getControllerFor(world, type); ++ ++ long failures = 1L; // start at 0.13ms ++ ++ while (taskController.hasTasks()) { ++ Thread.yield(); ++ failures = ConcurrentUtil.linearLongBackoff(failures, 125_000L, 5_000_000L); // 125us, 5ms ++ } ++ } ++ ++ public static void partialFlush(final ServerLevel world, final int tasksRemaining) { ++ for (long failures = 1L;;) { // start at 0.13ms ++ long totalTasks = 0L; ++ for (final RegionFileType regionFileType : CACHED_REGIONFILE_TYPES) { ++ totalTasks += getControllerFor(world, regionFileType).getTotalWorkingTasks(); ++ } ++ ++ if (totalTasks > (long)tasksRemaining) { ++ Thread.yield(); ++ failures = ConcurrentUtil.linearLongBackoff(failures, 125_000L, 5_000_000L); // 125us, 5ms ++ } else { ++ return; ++ } ++ } ++ } ++ ++ /** ++ * Returns the priority associated with blocking I/O based on the current thread. The goal is to avoid ++ * dumb plugins from taking away priority from threads we consider crucial. ++ * @return The priroity to use with blocking I/O on the current thread. ++ */ ++ public static Priority getIOBlockingPriorityForCurrentThread() { ++ if (TickThread.isTickThread()) { ++ return Priority.BLOCKING; ++ } ++ return Priority.HIGHEST; ++ } ++ ++ /** ++ * Returns the priority for the specified regionfile type for the specified chunk. ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @return The priority for the chunk ++ */ ++ public static Priority getPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ final RegionDataController taskController = getControllerFor(world, type); ++ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (task == null) { ++ return Priority.COMPLETING; ++ } ++ ++ return task.getPriority(); ++ } ++ ++ /** ++ * Sets the priority for all regionfile types for the specified chunk. Note that great care should ++ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different ++ * priorities. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Priority priority) { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ MoonriseRegionFileIO.setPriority(world, chunkX, chunkZ, type, priority); ++ } ++ } ++ ++ /** ++ * Sets the priority for the specified regionfile type for the specified chunk. Note that great care should ++ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different ++ * priorities. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final Priority priority) { ++ final RegionDataController taskController = getControllerFor(world, type); ++ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (task != null) { ++ task.setPriority(priority); ++ } ++ } ++ ++ /** ++ * Raises the priority for all regionfile types for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param priority New priority. ++ * ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Priority priority) { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ MoonriseRegionFileIO.raisePriority(world, chunkX, chunkZ, type, priority); ++ } ++ } ++ ++ /** ++ * Raises the priority for the specified regionfile type for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @param priority New priority. ++ * ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final Priority priority) { ++ final RegionDataController taskController = getControllerFor(world, type); ++ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (task != null) { ++ task.raisePriority(priority); ++ } ++ } ++ ++ /** ++ * Lowers the priority for all regionfile types for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Priority priority) { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ MoonriseRegionFileIO.lowerPriority(world, chunkX, chunkZ, type, priority); ++ } ++ } ++ ++ /** ++ * Lowers the priority for the specified regionfile type for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final Priority priority) { ++ final RegionDataController taskController = getControllerFor(world, type); ++ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (task != null) { ++ task.lowerPriority(priority); ++ } ++ } ++ ++ /** ++ * Schedules the chunk data to be written asynchronously. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means ++ * saves must be scheduled before a chunk is unloaded. ++ * </li> ++ * <li> ++ * Writes may be called concurrently, although only the "later" write will go through. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param data Chunk's data ++ * @param type The regionfile type to write to. ++ * ++ * @throws IllegalStateException If the file io thread has shutdown. ++ */ ++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, ++ final RegionFileType type) { ++ MoonriseRegionFileIO.scheduleSave(world, chunkX, chunkZ, data, type, Priority.NORMAL); ++ } ++ ++ /** ++ * Schedules the chunk data to be written asynchronously. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means ++ * saves must be scheduled before a chunk is unloaded. ++ * </li> ++ * <li> ++ * Writes may be called concurrently, although only the "later" write will go through. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param data Chunk's data ++ * @param type The regionfile type to write to. ++ * @param priority The minimum priority to schedule at. ++ * ++ * @throws IllegalStateException If the file io thread has shutdown. ++ */ ++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, ++ final RegionFileType type, final Priority priority) { ++ scheduleSave( ++ world, chunkX, chunkZ, ++ (final BiConsumer<CompoundTag, Throwable> consumer) -> { ++ consumer.accept(data, null); ++ }, null, type, priority ++ ); ++ } ++ ++ /** ++ * Schedules the chunk data to be written asynchronously. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means ++ * saves must be scheduled before a chunk is unloaded. ++ * </li> ++ * <li> ++ * Writes may be called concurrently, although only the "later" write will go through. ++ * </li> ++ * <li> ++ * The specified write task, if not null, will have its priority controlled by the scheduler. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param completable Chunk's pending data ++ * @param writeTask The task responsible for completing the pending chunk data ++ * @param type The regionfile type to write to. ++ * @param priority The minimum priority to schedule at. ++ * ++ * @throws IllegalStateException If the file io thread has shutdown. ++ */ ++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CallbackCompletable<CompoundTag> completable, ++ final PrioritisedExecutor.PrioritisedTask writeTask, final RegionFileType type, final Priority priority) { ++ scheduleSave(world, chunkX, chunkZ, completable::addWaiter, writeTask, type, priority); ++ } ++ ++ /** ++ * Schedules the chunk data to be written asynchronously. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means ++ * saves must be scheduled before a chunk is unloaded. ++ * </li> ++ * <li> ++ * Writes may be called concurrently, although only the "later" write will go through. ++ * </li> ++ * <li> ++ * The specified write task, if not null, will have its priority controlled by the scheduler. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param completable Chunk's pending data ++ * @param writeTask The task responsible for completing the pending chunk data ++ * @param type The regionfile type to write to. ++ * @param priority The minimum priority to schedule at. ++ * ++ * @throws IllegalStateException If the file io thread has shutdown. ++ */ ++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final Completable<CompoundTag> completable, ++ final PrioritisedExecutor.PrioritisedTask writeTask, final RegionFileType type, final Priority priority) { ++ scheduleSave(world, chunkX, chunkZ, completable::whenComplete, writeTask, type, priority); ++ } ++ ++ private static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final Consumer<BiConsumer<CompoundTag, Throwable>> scheduler, ++ final PrioritisedExecutor.PrioritisedTask writeTask, final RegionFileType type, final Priority priority) { ++ final RegionDataController taskController = getControllerFor(world, type); ++ ++ final boolean[] created = new boolean[1]; ++ final ChunkIOTask.InProgressWrite write = new ChunkIOTask.InProgressWrite(writeTask); ++ final ChunkIOTask task = taskController.chunkTasks.compute(CoordinateUtils.getChunkKey(chunkX, chunkZ), ++ (final long keyInMap, final ChunkIOTask taskRunning) -> { ++ if (taskRunning == null || taskRunning.failedWrite) { ++ // no task is scheduled or the previous write failed - meaning we need to overwrite it ++ ++ // create task ++ final ChunkIOTask newTask = new ChunkIOTask( ++ world, taskController, chunkX, chunkZ, priority, new ChunkIOTask.InProgressRead() ++ ); ++ ++ newTask.pushPendingWrite(write); ++ ++ created[0] = true; ++ ++ return newTask; ++ } ++ ++ taskRunning.pushPendingWrite(write); ++ ++ return taskRunning; ++ } ++ ); ++ ++ write.schedule(task, scheduler); ++ ++ if (created[0]) { ++ taskController.startTask(task); ++ task.scheduleWriteCompress(); ++ } else { ++ task.raisePriority(priority); ++ } ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call ++ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} ++ * for single load. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ */ ++ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock) { ++ return MoonriseRegionFileIO.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call ++ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} ++ * for single load. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param priority The minimum priority to load the data at. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ */ ++ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock, ++ final Priority priority) { ++ return MoonriseRegionFileIO.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and ++ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} ++ * for single load. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param types The regionfile type(s) to load. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock, ++ final RegionFileType... types) { ++ return MoonriseRegionFileIO.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL, types); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and ++ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} ++ * for single load. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param types The regionfile type(s) to load. ++ * @param priority The minimum priority to load the data at. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock, ++ final Priority priority, final RegionFileType... types) { ++ if (types == null) { ++ throw new NullPointerException("Types cannot be null"); ++ } ++ if (types.length == 0) { ++ throw new IllegalArgumentException("Types cannot be empty"); ++ } ++ ++ final RegionFileData ret = new RegionFileData(); ++ ++ final Cancellable[] reads = new CancellableRead[types.length]; ++ final AtomicInteger completions = new AtomicInteger(); ++ final int expectedCompletions = types.length; ++ ++ for (int i = 0; i < expectedCompletions; ++i) { ++ final RegionFileType type = types[i]; ++ reads[i] = MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, ++ (final CompoundTag data, final Throwable throwable) -> { ++ if (throwable != null) { ++ ret.setThrowable(type, throwable); ++ } else { ++ ret.setData(type, data); ++ } ++ ++ if (completions.incrementAndGet() == expectedCompletions) { ++ onComplete.accept(ret); ++ } ++ }, intendingToBlock, priority); ++ } ++ ++ return new CancellableReads(reads); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call ++ * {@code onComplete}. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete, ++ final boolean intendingToBlock) { ++ return MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, Priority.NORMAL); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call ++ * {@code onComplete}. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param priority Minimum priority to load the data at. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete, ++ final boolean intendingToBlock, final Priority priority) { ++ final RegionDataController taskController = getControllerFor(world, type); ++ ++ final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion(); ++ ++ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final BiLong1Function<ChunkIOTask, ChunkIOTask> compute = (final long keyInMap, final ChunkIOTask running) -> { ++ if (running == null) { ++ // not scheduled ++ ++ // set up task ++ final ChunkIOTask newTask = new ChunkIOTask( ++ world, taskController, chunkX, chunkZ, priority, new ChunkIOTask.InProgressRead() ++ ); ++ newTask.inProgressRead.addToAsyncWaiters(onComplete); ++ ++ callbackInfo.tasksNeedReadScheduling = true; ++ return newTask; ++ } ++ ++ final ChunkIOTask.InProgressWrite pendingWrite = running.inProgressWrite; ++ ++ if (pendingWrite == null) { ++ // need to add to waiters here, because the regionfile thread will use compute() to lock and check for cancellations ++ if (!running.inProgressRead.addToAsyncWaiters(onComplete)) { ++ callbackInfo.data = running.inProgressRead.value; ++ callbackInfo.throwable = running.inProgressRead.throwable; ++ callbackInfo.completeNow = true; ++ return running; ++ } ++ ++ callbackInfo.read = running.inProgressRead; ++ ++ return running; ++ } ++ ++ // at this stage we have to use the in progress write's data to avoid an order issue ++ ++ if (!pendingWrite.addToAsyncWaiters(onComplete)) { ++ // data is ready now ++ callbackInfo.data = pendingWrite.value; ++ callbackInfo.throwable = pendingWrite.throwable; ++ callbackInfo.completeNow = true; ++ return running; ++ } ++ ++ callbackInfo.write = pendingWrite; ++ ++ return running; ++ }; ++ ++ final ChunkIOTask ret = taskController.chunkTasks.compute(key, compute); ++ ++ // needs to be scheduled ++ if (callbackInfo.tasksNeedReadScheduling) { ++ taskController.startTask(ret); ++ ret.scheduleReadIO(); ++ } else if (callbackInfo.completeNow) { ++ try { ++ onComplete.accept(callbackInfo.data == null ? null : callbackInfo.data.copy(), callbackInfo.throwable); ++ } catch (final Throwable thr) { ++ LOGGER.error("Callback " + ConcurrentUtil.genericToString(onComplete) + " synchronously failed to handle chunk data for task " + ret.toString(), thr); ++ } ++ } else { ++ // we're waiting on a task we didn't schedule, so raise its priority to what we want ++ ret.raisePriority(priority); ++ } ++ ++ return new CancellableRead(onComplete, callbackInfo.read, callbackInfo.write); ++ } ++ ++ private static final class ImmediateCallbackCompletion { ++ ++ private CompoundTag data; ++ private Throwable throwable; ++ private boolean completeNow; ++ private boolean tasksNeedReadScheduling; ++ private ChunkIOTask.InProgressRead read; ++ private ChunkIOTask.InProgressWrite write; ++ ++ } ++ ++ /** ++ * Schedules a load task to be executed asynchronously, and blocks on that task. ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param type Regionfile type ++ * @param priority Minimum priority to load the data at. ++ * ++ * @return The chunk data for the chunk. Note that a {@code null} result means the chunk or regionfile does not exist on disk. ++ * ++ * @throws IOException If the load fails for any reason ++ */ ++ public static CompoundTag loadData(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final Priority priority) throws IOException { ++ final CompletableFuture<CompoundTag> ret = new CompletableFuture<>(); ++ ++ MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> { ++ if (thr != null) { ++ ret.completeExceptionally(thr); ++ } else { ++ ret.complete(compound); ++ } ++ }, true, priority); ++ ++ try { ++ return ret.join(); ++ } catch (final CompletionException ex) { ++ throw new IOException(ex); ++ } ++ } ++ ++ private static final class CancellableRead implements Cancellable { ++ ++ private BiConsumer<CompoundTag, Throwable> callback; ++ private ChunkIOTask.InProgressRead read; ++ private ChunkIOTask.InProgressWrite write; ++ ++ private CancellableRead(final BiConsumer<CompoundTag, Throwable> callback, ++ final ChunkIOTask.InProgressRead read, ++ final ChunkIOTask.InProgressWrite write) { ++ this.callback = callback; ++ this.read = read; ++ this.write = write; ++ } ++ ++ @Override ++ public boolean cancel() { ++ final BiConsumer<CompoundTag, Throwable> callback = this.callback; ++ final ChunkIOTask.InProgressRead read = this.read; ++ final ChunkIOTask.InProgressWrite write = this.write; ++ ++ if (callback == null || (read == null && write == null)) { ++ return false; ++ } ++ ++ this.callback = null; ++ this.read = null; ++ this.write = null; ++ ++ if (read != null) { ++ return read.cancel(callback); ++ } ++ if (write != null) { ++ return write.cancel(callback); ++ } ++ ++ // unreachable ++ throw new InternalError(); ++ } ++ } ++ ++ private static final class CancellableReads implements Cancellable { ++ ++ private Cancellable[] reads; ++ private static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class); ++ ++ private CancellableReads(final Cancellable[] reads) { ++ this.reads = reads; ++ } ++ ++ @Override ++ public boolean cancel() { ++ final Cancellable[] reads = (Cancellable[])READS_HANDLE.getAndSet((CancellableReads)this, (Cancellable[])null); ++ ++ if (reads == null) { ++ return false; ++ } ++ ++ boolean ret = false; ++ ++ for (final Cancellable read : reads) { ++ ret |= read.cancel(); ++ } ++ ++ return ret; ++ } ++ } ++ ++ private static final class ChunkIOTask { ++ ++ private final ServerLevel world; ++ private final RegionDataController regionDataController; ++ private final int chunkX; ++ private final int chunkZ; ++ private Priority priority; ++ private PrioritisedExecutor.PrioritisedTask currentTask; ++ ++ private final InProgressRead inProgressRead; ++ private volatile InProgressWrite inProgressWrite; ++ private final ReferenceOpenHashSet<InProgressWrite> allPendingWrites = new ReferenceOpenHashSet<>(); ++ ++ private RegionDataController.ReadData readData; ++ private RegionDataController.WriteData writeData; ++ private boolean failedWrite; ++ ++ public ChunkIOTask(final ServerLevel world, final RegionDataController regionDataController, ++ final int chunkX, final int chunkZ, final Priority priority, final InProgressRead inProgressRead) { ++ this.world = world; ++ this.regionDataController = regionDataController; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.priority = priority; ++ this.inProgressRead = inProgressRead; ++ } ++ ++ public Priority getPriority() { ++ synchronized (this) { ++ return this.priority; ++ } ++ } ++ ++ // must hold lock on this object ++ private void updatePriority(final Priority priority) { ++ this.priority = priority; ++ if (this.currentTask != null) { ++ this.currentTask.setPriority(priority); ++ } ++ for (final InProgressWrite write : this.allPendingWrites) { ++ if (write.writeTask != null) { ++ write.writeTask.setPriority(priority); ++ } ++ } ++ } ++ ++ public boolean setPriority(final Priority priority) { ++ synchronized (this) { ++ if (this.priority == priority) { ++ return false; ++ } ++ ++ this.updatePriority(priority); ++ ++ return true; ++ } ++ } ++ ++ public boolean raisePriority(final Priority priority) { ++ synchronized (this) { ++ if (this.priority.isHigherOrEqualPriority(priority)) { ++ return false; ++ } ++ ++ this.updatePriority(priority); ++ ++ return true; ++ } ++ } ++ ++ public boolean lowerPriority(final Priority priority) { ++ synchronized (this) { ++ if (this.priority.isLowerOrEqualPriority(priority)) { ++ return false; ++ } ++ ++ this.updatePriority(priority); ++ ++ return true; ++ } ++ } ++ ++ private void pushPendingWrite(final InProgressWrite write) { ++ this.inProgressWrite = write; ++ synchronized (this) { ++ this.allPendingWrites.add(write); ++ if (write.writeTask != null) { ++ write.writeTask.setPriority(this.priority); ++ } ++ } ++ } ++ ++ private void pendingWriteComplete(final InProgressWrite write) { ++ synchronized (this) { ++ this.allPendingWrites.remove(write); ++ } ++ } ++ ++ public void scheduleReadIO() { ++ final PrioritisedExecutor.PrioritisedTask task; ++ synchronized (this) { ++ task = this.regionDataController.ioScheduler.createTask(this.chunkX, this.chunkZ, this::performReadIO, this.priority); ++ this.currentTask = task; ++ } ++ task.queue(); ++ } ++ ++ private void performReadIO() { ++ final InProgressRead read = this.inProgressRead; ++ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ); ++ ++ final boolean[] canRead = new boolean[] { true }; ++ ++ if (read.hasNoWaiters()) { ++ // cancelled read? go to task controller to confirm ++ final ChunkIOTask inMap = this.regionDataController.chunkTasks.compute(chunkKey, (final long keyInMap, final ChunkIOTask valueInMap) -> { ++ if (valueInMap == null) { ++ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkIOTask.this.toString() + ", report this!"); ++ } ++ if (valueInMap != ChunkIOTask.this) { ++ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkIOTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); ++ } ++ ++ if (!read.hasNoWaiters()) { ++ return valueInMap; ++ } else { ++ canRead[0] = false; ++ } ++ ++ if (valueInMap.inProgressWrite != null) { ++ return valueInMap; ++ } ++ ++ return null; ++ }); ++ ++ if (inMap == null) { ++ this.regionDataController.endTask(this); ++ // read is cancelled - and no write pending, so we're done ++ return; ++ } ++ // if there is a write in progress, we don't actually have to worry about waiters gaining new entries - ++ // the readers will just use the in progress write, so the value in canRead is good to use without ++ // further synchronisation. ++ } ++ ++ if (canRead[0]) { ++ RegionDataController.ReadData readData = null; ++ Throwable throwable = null; ++ ++ try { ++ readData = this.regionDataController.readData(this.chunkX, this.chunkZ); ++ } catch (final Throwable thr) { ++ throwable = thr; ++ LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr); ++ } ++ ++ if (throwable != null) { ++ this.finishRead(null, throwable); ++ } else { ++ switch (readData.result()) { ++ case NO_DATA: ++ case SYNC_READ: { ++ this.finishRead(readData.syncRead(), null); ++ break; ++ } ++ case HAS_DATA: { ++ this.readData = readData; ++ this.scheduleReadDecompress(); ++ // read will handle write scheduling ++ return; ++ } ++ default: { ++ throw new IllegalStateException("Unknown state: " + readData.result()); ++ } ++ } ++ } ++ } ++ ++ if (!this.tryAbortWrite()) { ++ this.scheduleWriteCompress(); ++ } ++ } ++ ++ private void scheduleReadDecompress() { ++ final PrioritisedExecutor.PrioritisedTask task; ++ synchronized (this) { ++ task = this.regionDataController.compressionExecutor.createTask(this::performReadDecompress, this.priority); ++ this.currentTask = task; ++ } ++ task.queue(); ++ } ++ ++ private void performReadDecompress() { ++ final RegionDataController.ReadData readData = this.readData; ++ this.readData = null; ++ ++ CompoundTag compoundTag = null; ++ Throwable throwable = null; ++ ++ try { ++ compoundTag = this.regionDataController.finishRead(this.chunkX, this.chunkZ, readData); ++ } catch (final Throwable thr) { ++ throwable = thr; ++ LOGGER.error("Failed to decompress chunk data for task: " + this.toString(), thr); ++ } ++ ++ if (compoundTag == null) { ++ // need to re-try from the start ++ this.scheduleReadIO(); ++ return; ++ } ++ ++ this.finishRead(compoundTag, throwable); ++ if (!this.tryAbortWrite()) { ++ this.scheduleWriteCompress(); ++ } ++ } ++ ++ private void finishRead(final CompoundTag compoundTag, final Throwable throwable) { ++ this.inProgressRead.complete(this, compoundTag, throwable); ++ } ++ ++ public void scheduleWriteCompress() { ++ final InProgressWrite inProgressWrite = this.inProgressWrite; ++ ++ final PrioritisedExecutor.PrioritisedTask task; ++ synchronized (this) { ++ task = this.regionDataController.compressionExecutor.createTask(() -> { ++ ChunkIOTask.this.performWriteCompress(inProgressWrite); ++ }, this.priority); ++ this.currentTask = task; ++ } ++ ++ inProgressWrite.addToWaiters(this, (final CompoundTag data, final Throwable throwable) -> { ++ task.queue(); ++ }); ++ } ++ ++ private boolean tryAbortWrite() { ++ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ); ++ if (this.inProgressWrite == null) { ++ final ChunkIOTask inMap = this.regionDataController.chunkTasks.compute(chunkKey, (final long keyInMap, final ChunkIOTask valueInMap) -> { ++ if (valueInMap == null) { ++ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkIOTask.this.toString() + ", report this!"); ++ } ++ if (valueInMap != ChunkIOTask.this) { ++ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkIOTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); ++ } ++ ++ if (valueInMap.inProgressWrite != null) { ++ return valueInMap; ++ } ++ ++ return null; ++ }); ++ ++ if (inMap == null) { ++ this.regionDataController.endTask(this); ++ return true; // set the task value to null, indicating we're done ++ } // else: inProgressWrite changed, so now we have something to write ++ } ++ ++ return false; ++ } ++ ++ private void performWriteCompress(final InProgressWrite inProgressWrite) { ++ final CompoundTag write = inProgressWrite.value; ++ if (!inProgressWrite.isComplete()) { ++ throw new IllegalStateException("Should be writable"); ++ } ++ ++ RegionDataController.WriteData writeData = null; ++ boolean failedWrite = false; ++ ++ try { ++ writeData = this.regionDataController.startWrite(this.chunkX, this.chunkZ, write); ++ } catch (final Throwable thr) { ++ // TODO implement this? ++ /*if (thr instanceof RegionFileStorage.RegionFileSizeException) { ++ final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024); ++ LOGGER.error("Chunk at (" + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk."); ++ } else */ ++ { ++ failedWrite = thr instanceof IOException; ++ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr); ++ } ++ } ++ ++ if (writeData == null) { ++ // null if a throwable was encountered ++ ++ // we cannot continue to the I/O stage here, so try to complete ++ ++ if (this.tryCompleteWrite(inProgressWrite, failedWrite)) { ++ return; ++ } else { ++ // fetch new data and try again ++ this.scheduleWriteCompress(); ++ return; ++ } ++ } else { ++ // writeData != null && !failedWrite ++ // we can continue to I/O stage ++ this.writeData = writeData; ++ this.scheduleWriteIO(inProgressWrite); ++ return; ++ } ++ } ++ ++ private void scheduleWriteIO(final InProgressWrite inProgressWrite) { ++ final PrioritisedExecutor.PrioritisedTask task; ++ synchronized (this) { ++ task = this.regionDataController.ioScheduler.createTask(this.chunkX, this.chunkZ, () -> { ++ ChunkIOTask.this.runWriteIO(inProgressWrite); ++ }, this.priority); ++ this.currentTask = task; ++ } ++ task.queue(); ++ } ++ ++ private void runWriteIO(final InProgressWrite inProgressWrite) { ++ RegionDataController.WriteData writeData = this.writeData; ++ this.writeData = null; ++ ++ boolean failedWrite = false; ++ ++ try { ++ this.regionDataController.finishWrite(this.chunkX, this.chunkZ, writeData); ++ } catch (final Throwable thr) { ++ failedWrite = thr instanceof IOException; ++ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr); ++ } ++ ++ if (!this.tryCompleteWrite(inProgressWrite, failedWrite)) { ++ // fetch new data and try again ++ this.scheduleWriteCompress(); ++ } ++ return; ++ } ++ ++ private boolean tryCompleteWrite(final InProgressWrite written, final boolean failedWrite) { ++ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ); ++ ++ final boolean[] done = new boolean[] { false }; ++ ++ this.regionDataController.chunkTasks.compute(chunkKey, (final long keyInMap, final ChunkIOTask valueInMap) -> { ++ if (valueInMap == null) { ++ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkIOTask.this.toString() + ", report this!"); ++ } ++ if (valueInMap != ChunkIOTask.this) { ++ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkIOTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); ++ } ++ if (valueInMap.inProgressWrite == written) { ++ valueInMap.failedWrite = failedWrite; ++ done[0] = true; ++ // keep the data in map if we failed the write so we can try to prevent data loss ++ return failedWrite ? valueInMap : null; ++ } ++ // different data than expected, means we need to retry write ++ return valueInMap; ++ }); ++ ++ if (done[0]) { ++ this.regionDataController.endTask(this); ++ return true; ++ } ++ return false; ++ } ++ ++ @Override ++ public String toString() { ++ return "Task for world: '" + WorldUtil.getWorldName(this.world) + "' at (" + this.chunkX + "," ++ + this.chunkZ + ") type: " + this.regionDataController.type.name() + ", hash: " + this.hashCode(); ++ } ++ ++ private static final class InProgressRead { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(InProgressRead.class); ++ ++ private CompoundTag value; ++ private Throwable throwable; ++ private final MultiThreadedQueue<BiConsumer<CompoundTag, Throwable>> callbacks = new MultiThreadedQueue<>(); ++ ++ public boolean hasNoWaiters() { ++ return this.callbacks.isEmpty(); ++ } ++ ++ public boolean addToAsyncWaiters(final BiConsumer<CompoundTag, Throwable> callback) { ++ return this.callbacks.add(callback); ++ } ++ ++ public boolean cancel(final BiConsumer<CompoundTag, Throwable> callback) { ++ return this.callbacks.remove(callback); ++ } ++ ++ public void complete(final ChunkIOTask task, final CompoundTag value, final Throwable throwable) { ++ this.value = value; ++ this.throwable = throwable; ++ ++ BiConsumer<CompoundTag, Throwable> consumer; ++ while ((consumer = this.callbacks.pollOrBlockAdds()) != null) { ++ try { ++ consumer.accept(value == null ? null : value.copy(), throwable); ++ } catch (final Throwable thr) { ++ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data (read) for task " + task.toString(), thr); ++ } ++ } ++ } ++ } ++ ++ private static final class InProgressWrite { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(InProgressWrite.class); ++ ++ private CompoundTag value; ++ private Throwable throwable; ++ private volatile boolean complete; ++ private final MultiThreadedQueue<BiConsumer<CompoundTag, Throwable>> callbacks = new MultiThreadedQueue<>(); ++ ++ private final PrioritisedExecutor.PrioritisedTask writeTask; ++ ++ public InProgressWrite(final PrioritisedExecutor.PrioritisedTask writeTask) { ++ this.writeTask = writeTask; ++ } ++ ++ public boolean isComplete() { ++ return this.complete; ++ } ++ ++ public void schedule(final ChunkIOTask task, final Consumer<BiConsumer<CompoundTag, Throwable>> scheduler) { ++ scheduler.accept((final CompoundTag data, final Throwable throwable) -> { ++ InProgressWrite.this.complete(task, data, throwable); ++ }); ++ } ++ ++ public boolean addToAsyncWaiters(final BiConsumer<CompoundTag, Throwable> callback) { ++ return this.callbacks.add(callback); ++ } ++ ++ public void addToWaiters(final ChunkIOTask task, final BiConsumer<CompoundTag, Throwable> consumer) { ++ if (!this.callbacks.add(consumer)) { ++ this.syncAccept(task, consumer, this.value, this.throwable); ++ } ++ } ++ ++ private void syncAccept(final ChunkIOTask task, final BiConsumer<CompoundTag, Throwable> consumer, final CompoundTag value, final Throwable throwable) { ++ try { ++ consumer.accept(value == null ? null : value.copy(), throwable); ++ } catch (final Throwable thr) { ++ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data (write) for task " + task.toString(), thr); ++ } ++ } ++ ++ public void complete(final ChunkIOTask task, final CompoundTag value, final Throwable throwable) { ++ this.value = value; ++ this.throwable = throwable; ++ this.complete = true; ++ ++ task.pendingWriteComplete(this); ++ ++ BiConsumer<CompoundTag, Throwable> consumer; ++ while ((consumer = this.callbacks.pollOrBlockAdds()) != null) { ++ this.syncAccept(task, consumer, value, throwable); ++ } ++ } ++ ++ public boolean cancel(final BiConsumer<CompoundTag, Throwable> callback) { ++ return this.callbacks.remove(callback); ++ } ++ } ++ } ++ ++ public static abstract class RegionDataController { ++ ++ public final RegionFileType type; ++ private final PrioritisedExecutor compressionExecutor; ++ private final IOScheduler ioScheduler; ++ private final ConcurrentLong2ReferenceChainedHashTable<ChunkIOTask> chunkTasks = new ConcurrentLong2ReferenceChainedHashTable<>(); ++ ++ private final AtomicLong inProgressTasks = new AtomicLong(); ++ ++ public RegionDataController(final RegionFileType type, final PrioritisedExecutor ioExecutor, ++ final PrioritisedExecutor compressionExecutor) { ++ this.type = type; ++ this.compressionExecutor = compressionExecutor; ++ this.ioScheduler = new IOScheduler(ioExecutor); ++ } ++ ++ final void startTask(final ChunkIOTask task) { ++ this.inProgressTasks.getAndIncrement(); ++ } ++ ++ final void endTask(final ChunkIOTask task) { ++ this.inProgressTasks.getAndDecrement(); ++ } ++ ++ public boolean hasTasks() { ++ return this.inProgressTasks.get() != 0L; ++ } ++ ++ public long getTotalWorkingTasks() { ++ return this.inProgressTasks.get(); ++ } ++ ++ public abstract RegionFileStorage getCache(); ++ ++ public static record WriteData(CompoundTag input, WriteResult result, DataOutputStream output, IORunnable write) { ++ public static enum WriteResult { ++ WRITE, ++ DELETE; ++ } ++ } ++ ++ public abstract WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException; ++ ++ public abstract void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException; ++ ++ public static record ReadData(ReadResult result, DataInputStream input, CompoundTag syncRead) { ++ public static enum ReadResult { ++ NO_DATA, ++ HAS_DATA, ++ SYNC_READ; ++ } ++ } ++ ++ public abstract ReadData readData(final int chunkX, final int chunkZ) throws IOException; ++ ++ // if the return value is null, then the caller needs to re-try with a new call to readData() ++ public abstract CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException; ++ ++ public static interface IORunnable { ++ ++ public void run(final RegionFile regionFile) throws IOException; ++ ++ } ++ } ++ ++ private static final class IOScheduler { ++ ++ private final ConcurrentLong2ReferenceChainedHashTable<RegionIOTasks> regionTasks = new ConcurrentLong2ReferenceChainedHashTable<>(); ++ private final PrioritisedExecutor executor; ++ ++ public IOScheduler(final PrioritisedExecutor executor) { ++ this.executor = executor; ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, ++ final Runnable run, final Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask[] ret = new PrioritisedExecutor.PrioritisedTask[1]; ++ final long subOrder = this.executor.generateNextSubOrder(); ++ this.regionTasks.compute(CoordinateUtils.getChunkKey(chunkX >> REGION_FILE_SHIFT, chunkZ >> REGION_FILE_SHIFT), ++ (final long regionKey, final RegionIOTasks existing) -> { ++ final RegionIOTasks res; ++ if (existing != null) { ++ res = existing; ++ } else { ++ res = new RegionIOTasks(regionKey, IOScheduler.this); ++ } ++ ++ ret[0] = res.createTask(run, priority, subOrder); ++ ++ return res; ++ }); ++ ++ return ret[0]; ++ } ++ } ++ ++ private static final class RegionIOTasks implements Runnable { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(RegionIOTasks.class); ++ ++ private final PrioritisedTaskQueue queue = new PrioritisedTaskQueue(); ++ private final long regionKey; ++ private final IOScheduler ioScheduler; ++ private long createdTasks; ++ private long executedTasks; ++ ++ private PrioritisedExecutor.PrioritisedTask task; ++ ++ public RegionIOTasks(final long regionKey, final IOScheduler ioScheduler) { ++ this.regionKey = regionKey; ++ this.ioScheduler = ioScheduler; ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createTask(final Runnable run, final Priority priority, ++ final long subOrder) { ++ ++this.createdTasks; ++ return new WrappedTask(this.queue.createTask(run, priority, subOrder)); ++ } ++ ++ private void adjustTaskPriority() { ++ final PrioritisedTaskQueue.PrioritySubOrderPair priority = this.queue.getHighestPrioritySubOrder(); ++ if (this.task == null) { ++ if (priority == null) { ++ return; ++ } ++ this.task = this.ioScheduler.executor.createTask(this, priority.priority(), priority.subOrder()); ++ this.task.queue(); ++ } else { ++ if (priority == null) { ++ throw new IllegalStateException(); ++ } else { ++ this.task.setPriorityAndSubOrder(priority.priority(), priority.subOrder()); ++ } ++ } ++ } ++ ++ @Override ++ public void run() { ++ final Runnable run; ++ synchronized (this) { ++ run = this.queue.pollTask(); ++ } ++ ++ try { ++ run.run(); ++ } finally { ++ synchronized (this) { ++ this.task = null; ++ this.adjustTaskPriority(); ++ } ++ this.ioScheduler.regionTasks.compute(this.regionKey, (final long keyInMap, final RegionIOTasks tasks) -> { ++ if (tasks != RegionIOTasks.this) { ++ throw new IllegalStateException("Region task mismatch"); ++ } ++ ++tasks.executedTasks; ++ if (tasks.createdTasks != tasks.executedTasks) { ++ return tasks; ++ } ++ ++ if (tasks.task != null) { ++ throw new IllegalStateException("Task may not be null when created==executed"); ++ } ++ ++ return null; ++ }); ++ } ++ } ++ ++ private final class WrappedTask implements PrioritisedExecutor.PrioritisedTask { ++ ++ private final PrioritisedExecutor.PrioritisedTask wrapped; ++ ++ public WrappedTask(final PrioritisedExecutor.PrioritisedTask wrap) { ++ this.wrapped = wrap; ++ } ++ ++ @Override ++ public PrioritisedExecutor getExecutor() { ++ return RegionIOTasks.this.ioScheduler.executor; ++ } ++ ++ @Override ++ public boolean queue() { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.queue()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ ++ @Override ++ public boolean isQueued() { ++ return this.wrapped.isQueued(); ++ } ++ ++ @Override ++ public boolean cancel() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean execute() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public Priority getPriority() { ++ return this.wrapped.getPriority(); ++ } ++ ++ @Override ++ public boolean setPriority(final Priority priority) { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.setPriority(priority) && this.wrapped.isQueued()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ ++ @Override ++ public boolean raisePriority(final Priority priority) { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.raisePriority(priority) && this.wrapped.isQueued()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ ++ @Override ++ public boolean lowerPriority(final Priority priority) { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.lowerPriority(priority) && this.wrapped.isQueued()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ ++ @Override ++ public long getSubOrder() { ++ return this.wrapped.getSubOrder(); ++ } ++ ++ @Override ++ public boolean setSubOrder(final long subOrder) { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.setSubOrder(subOrder) && this.wrapped.isQueued()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ ++ @Override ++ public boolean raiseSubOrder(final long subOrder) { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.raiseSubOrder(subOrder) && this.wrapped.isQueued()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ ++ @Override ++ public boolean lowerSubOrder(final long subOrder) { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.lowerSubOrder(subOrder) && this.wrapped.isQueued()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ ++ @Override ++ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.setPriorityAndSubOrder(priority, subOrder) && this.wrapped.isQueued()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java +deleted file mode 100644 +index 3218cbf84f54daf06e84442d5eb1a36d8da6b215..0000000000000000000000000000000000000000 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java ++++ /dev/null +@@ -1,1240 +0,0 @@ +-package ca.spottedleaf.moonrise.patches.chunk_system.io; +- +-import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +-import ca.spottedleaf.concurrentutil.executor.Cancellable; +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedQueueExecutorThread; +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue; +-import ca.spottedleaf.concurrentutil.function.BiLong1Function; +-import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; +-import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +-import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +-import ca.spottedleaf.moonrise.common.util.TickThread; +-import ca.spottedleaf.moonrise.common.util.WorldUtil; +-import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +-import net.minecraft.nbt.CompoundTag; +-import net.minecraft.server.level.ServerLevel; +-import net.minecraft.world.level.ChunkPos; +-import net.minecraft.world.level.chunk.storage.RegionFile; +-import net.minecraft.world.level.chunk.storage.RegionFileStorage; +-import org.slf4j.Logger; +-import org.slf4j.LoggerFactory; +-import java.io.IOException; +-import java.lang.invoke.VarHandle; +-import java.util.concurrent.CompletableFuture; +-import java.util.concurrent.CompletionException; +-import java.util.concurrent.atomic.AtomicInteger; +-import java.util.function.BiConsumer; +-import java.util.function.Consumer; +-import java.util.function.Function; +- +-/** +- * Prioritised RegionFile I/O executor, responsible for all RegionFile access. +- * <p> +- * All functions provided are MT-Safe, however certain ordering constraints are recommended: +- * <li> +- * Chunk saves may not occur for unloaded chunks. +- * </li> +- * <li> +- * Tasks must be scheduled on the chunk scheduler thread. +- * </li> +- * By following these constraints, no chunk data loss should occur with the exception of underlying I/O problems. +- * </p> +- */ +-public final class RegionFileIOThread extends PrioritisedQueueExecutorThread { +- +- private static final Logger LOGGER = LoggerFactory.getLogger(RegionFileIOThread.class); +- +- /** +- * The kinds of region files controlled by the region file thread. Add more when needed, and ensure +- * getControllerFor is updated. +- */ +- public static enum RegionFileType { +- CHUNK_DATA, +- POI_DATA, +- ENTITY_DATA; +- } +- +- private static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values(); +- +- public static ChunkDataController getControllerFor(final ServerLevel world, final RegionFileType type) { +- switch (type) { +- case CHUNK_DATA: +- return ((ChunkSystemServerLevel)world).moonrise$getChunkDataController(); +- case POI_DATA: +- return ((ChunkSystemServerLevel)world).moonrise$getPoiChunkDataController(); +- case ENTITY_DATA: +- return ((ChunkSystemServerLevel)world).moonrise$getEntityChunkDataController(); +- default: +- throw new IllegalStateException("Unknown controller type " + type); +- } +- } +- +- /** +- * Collects regionfile data for a certain chunk. +- */ +- public static final class RegionFileData { +- +- private final boolean[] hasResult = new boolean[CACHED_REGIONFILE_TYPES.length]; +- private final CompoundTag[] data = new CompoundTag[CACHED_REGIONFILE_TYPES.length]; +- private final Throwable[] throwables = new Throwable[CACHED_REGIONFILE_TYPES.length]; +- +- /** +- * Sets the result associated with the specified regionfile type. Note that +- * results can only be set once per regionfile type. +- * +- * @param type The regionfile type. +- * @param data The result to set. +- */ +- public void setData(final RegionFileType type, final CompoundTag data) { +- final int index = type.ordinal(); +- +- if (this.hasResult[index]) { +- throw new IllegalArgumentException("Result already exists for type " + type); +- } +- this.hasResult[index] = true; +- this.data[index] = data; +- } +- +- /** +- * Sets the result associated with the specified regionfile type. Note that +- * results can only be set once per regionfile type. +- * +- * @param type The regionfile type. +- * @param throwable The result to set. +- */ +- public void setThrowable(final RegionFileType type, final Throwable throwable) { +- final int index = type.ordinal(); +- +- if (this.hasResult[index]) { +- throw new IllegalArgumentException("Result already exists for type " + type); +- } +- this.hasResult[index] = true; +- this.throwables[index] = throwable; +- } +- +- /** +- * Returns whether there is a result for the specified regionfile type. +- * +- * @param type Specified regionfile type. +- * +- * @return Whether a result exists for {@code type}. +- */ +- public boolean hasResult(final RegionFileType type) { +- return this.hasResult[type.ordinal()]; +- } +- +- /** +- * Returns the data result for the regionfile type. +- * +- * @param type Specified regionfile type. +- * +- * @throws IllegalArgumentException If the result has not been set for {@code type}. +- * @return The data result for the specified type. If the result is a {@code Throwable}, +- * then returns {@code null}. +- */ +- public CompoundTag getData(final RegionFileType type) { +- final int index = type.ordinal(); +- +- if (!this.hasResult[index]) { +- throw new IllegalArgumentException("Result does not exist for type " + type); +- } +- +- return this.data[index]; +- } +- +- /** +- * Returns the throwable result for the regionfile type. +- * +- * @param type Specified regionfile type. +- * +- * @throws IllegalArgumentException If the result has not been set for {@code type}. +- * @return The throwable result for the specified type. If the result is an {@code CompoundTag}, +- * then returns {@code null}. +- */ +- public Throwable getThrowable(final RegionFileType type) { +- final int index = type.ordinal(); +- +- if (!this.hasResult[index]) { +- throw new IllegalArgumentException("Result does not exist for type " + type); +- } +- +- return this.throwables[index]; +- } +- } +- +- private static final Object INIT_LOCK = new Object(); +- +- static RegionFileIOThread[] threads; +- +- /* needs to be consistent given a set of parameters */ +- static RegionFileIOThread selectThread(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { +- if (threads == null) { +- throw new IllegalStateException("Threads not initialised"); +- } +- +- final int regionX = chunkX >> 5; +- final int regionZ = chunkZ >> 5; +- final int typeOffset = type.ordinal(); +- +- return threads[(System.identityHashCode(world) + regionX + regionZ + typeOffset) % threads.length]; +- } +- +- /** +- * Shuts down the I/O executor(s). Watis for all tasks to complete if specified. +- * Tasks queued during this call might not be accepted, and tasks queued after will not be accepted. +- * +- * @param wait Whether to wait until all tasks have completed. +- */ +- public static void close(final boolean wait) { +- for (int i = 0, len = threads.length; i < len; ++i) { +- threads[i].close(false, true); +- } +- if (wait) { +- RegionFileIOThread.flush(); +- } +- } +- +- public static long[] getExecutedTasks() { +- final long[] ret = new long[threads.length]; +- for (int i = 0, len = threads.length; i < len; ++i) { +- ret[i] = threads[i].getTotalTasksExecuted(); +- } +- +- return ret; +- } +- +- public static long[] getTasksScheduled() { +- final long[] ret = new long[threads.length]; +- for (int i = 0, len = threads.length; i < len; ++i) { +- ret[i] = threads[i].getTotalTasksScheduled(); +- } +- return ret; +- } +- +- public static void flush() { +- for (int i = 0, len = threads.length; i < len; ++i) { +- threads[i].waitUntilAllExecuted(); +- } +- } +- +- public static void flushRegionStorages(final ServerLevel world) throws IOException { +- for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { +- getControllerFor(world, type).getCache().flush(); +- } +- } +- +- public static void partialFlush(final int totalTasksRemaining) { +- long failures = 1L; // start out at 0.25ms +- +- for (;;) { +- final long[] executed = getExecutedTasks(); +- final long[] scheduled = getTasksScheduled(); +- +- long sum = 0; +- for (int i = 0; i < executed.length; ++i) { +- sum += scheduled[i] - executed[i]; +- } +- +- if (sum <= totalTasksRemaining) { +- break; +- } +- +- failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms +- } +- } +- +- /** +- * Inits the executor with the specified number of threads. +- * +- * @param threads Specified number of threads. +- */ +- public static void init(final int threads) { +- synchronized (INIT_LOCK) { +- if (RegionFileIOThread.threads != null) { +- throw new IllegalStateException("Already initialised threads"); +- } +- +- RegionFileIOThread.threads = new RegionFileIOThread[threads]; +- +- for (int i = 0; i < threads; ++i) { +- RegionFileIOThread.threads[i] = new RegionFileIOThread(i); +- RegionFileIOThread.threads[i].start(); +- } +- } +- } +- +- public static void deinit() { +- if (true) { // Paper +- // TODO does this cause issues with mods? how to implement +- close(true); +- synchronized (INIT_LOCK) { +- RegionFileIOThread.threads = null; +- } +- } else { RegionFileIOThread.flush(); } +- } +- +- private RegionFileIOThread(final int threadNumber) { +- super(new PrioritisedThreadedTaskQueue(), (int)(1.0e6)); // 1.0ms spinwait time +- this.setName("RegionFile I/O Thread #" + threadNumber); +- this.setPriority(Thread.NORM_PRIORITY - 2); // we keep priority close to normal because threads can wait on us +- this.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> { +- LOGGER.error("Uncaught exception thrown from I/O thread, report this! Thread: " + thread.getName(), thr); +- }); +- } +- +- /** +- * Returns whether the current thread is a regionfile I/O executor. +- * @return Whether the current thread is a regionfile I/O executor. +- */ +- public static boolean isRegionFileThread() { +- return Thread.currentThread() instanceof RegionFileIOThread; +- } +- +- /** +- * Returns the priority associated with blocking I/O based on the current thread. The goal is to avoid +- * dumb plugins from taking away priority from threads we consider crucial. +- * @return The priroity to use with blocking I/O on the current thread. +- */ +- public static Priority getIOBlockingPriorityForCurrentThread() { +- if (TickThread.isTickThread()) { +- return Priority.BLOCKING; +- } +- return Priority.HIGHEST; +- } +- +- /** +- * Returns the current {@code CompoundTag} pending for write for the specified chunk & regionfile type. +- * Note that this does not copy the result, so do not modify the result returned. +- * +- * @param world Specified world. +- * @param chunkX Specified chunk x. +- * @param chunkZ Specified chunk z. +- * @param type Specified regionfile type. +- * +- * @return The compound tag associated for the specified chunk. {@code null} if no write was pending, or if {@code null} is the write pending. +- */ +- public static CompoundTag getPendingWrite(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { +- final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); +- return thread.getPendingWriteInternal(world, chunkX, chunkZ, type); +- } +- +- CompoundTag getPendingWriteInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { +- final ChunkDataController taskController = getControllerFor(world, type); +- final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); +- +- if (task == null) { +- return null; +- } +- +- final CompoundTag ret = task.inProgressWrite; +- +- return ret == ChunkDataTask.NOTHING_TO_WRITE ? null : ret; +- } +- +- /** +- * Returns the priority for the specified regionfile type for the specified chunk. +- * @param world Specified world. +- * @param chunkX Specified chunk x. +- * @param chunkZ Specified chunk z. +- * @param type Specified regionfile type. +- * @return The priority for the chunk +- */ +- public static Priority getPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { +- final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); +- return thread.getPriorityInternal(world, chunkX, chunkZ, type); +- } +- +- Priority getPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { +- final ChunkDataController taskController = getControllerFor(world, type); +- final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); +- +- if (task == null) { +- return Priority.COMPLETING; +- } +- +- return task.prioritisedTask.getPriority(); +- } +- +- /** +- * Sets the priority for all regionfile types for the specified chunk. Note that great care should +- * be taken using this method, as there can be multiple tasks tied to the same chunk that want different +- * priorities. +- * +- * @param world Specified world. +- * @param chunkX Specified chunk x. +- * @param chunkZ Specified chunk z. +- * @param priority New priority. +- * +- * @see #raisePriority(ServerLevel, int, int, Priority) +- * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) +- * @see #lowerPriority(ServerLevel, int, int, Priority) +- * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) +- */ +- public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, +- final Priority priority) { +- for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { +- RegionFileIOThread.setPriority(world, chunkX, chunkZ, type, priority); +- } +- } +- +- /** +- * Sets the priority for the specified regionfile type for the specified chunk. Note that great care should +- * be taken using this method, as there can be multiple tasks tied to the same chunk that want different +- * priorities. +- * +- * @param world Specified world. +- * @param chunkX Specified chunk x. +- * @param chunkZ Specified chunk z. +- * @param type Specified regionfile type. +- * @param priority New priority. +- * +- * @see #raisePriority(ServerLevel, int, int, Priority) +- * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) +- * @see #lowerPriority(ServerLevel, int, int, Priority) +- * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) +- */ +- public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, +- final Priority priority) { +- final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); +- thread.setPriorityInternal(world, chunkX, chunkZ, type, priority); +- } +- +- void setPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, +- final Priority priority) { +- final ChunkDataController taskController = getControllerFor(world, type); +- final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); +- +- if (task != null) { +- task.prioritisedTask.setPriority(priority); +- } +- } +- +- /** +- * Raises the priority for all regionfile types for the specified chunk. +- * +- * @param world Specified world. +- * @param chunkX Specified chunk x. +- * @param chunkZ Specified chunk z. +- * @param priority New priority. +- * +- * @see #setPriority(ServerLevel, int, int, Priority) +- * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) +- * @see #lowerPriority(ServerLevel, int, int, Priority) +- * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) +- */ +- public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, +- final Priority priority) { +- for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { +- RegionFileIOThread.raisePriority(world, chunkX, chunkZ, type, priority); +- } +- } +- +- /** +- * Raises the priority for the specified regionfile type for the specified chunk. +- * +- * @param world Specified world. +- * @param chunkX Specified chunk x. +- * @param chunkZ Specified chunk z. +- * @param type Specified regionfile type. +- * @param priority New priority. +- * +- * @see #setPriority(ServerLevel, int, int, Priority) +- * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) +- * @see #lowerPriority(ServerLevel, int, int, Priority) +- * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) +- */ +- public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, +- final Priority priority) { +- final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); +- thread.raisePriorityInternal(world, chunkX, chunkZ, type, priority); +- } +- +- void raisePriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, +- final Priority priority) { +- final ChunkDataController taskController = getControllerFor(world, type); +- final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); +- +- if (task != null) { +- task.prioritisedTask.raisePriority(priority); +- } +- } +- +- /** +- * Lowers the priority for all regionfile types for the specified chunk. +- * +- * @param world Specified world. +- * @param chunkX Specified chunk x. +- * @param chunkZ Specified chunk z. +- * @param priority New priority. +- * +- * @see #raisePriority(ServerLevel, int, int, Priority) +- * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) +- * @see #setPriority(ServerLevel, int, int, Priority) +- * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) +- */ +- public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, +- final Priority priority) { +- for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { +- RegionFileIOThread.lowerPriority(world, chunkX, chunkZ, type, priority); +- } +- } +- +- /** +- * Lowers the priority for the specified regionfile type for the specified chunk. +- * +- * @param world Specified world. +- * @param chunkX Specified chunk x. +- * @param chunkZ Specified chunk z. +- * @param type Specified regionfile type. +- * @param priority New priority. +- * +- * @see #raisePriority(ServerLevel, int, int, Priority) +- * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) +- * @see #setPriority(ServerLevel, int, int, Priority) +- * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) +- */ +- public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, +- final Priority priority) { +- final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); +- thread.lowerPriorityInternal(world, chunkX, chunkZ, type, priority); +- } +- +- void lowerPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, +- final Priority priority) { +- final ChunkDataController taskController = getControllerFor(world, type); +- final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); +- +- if (task != null) { +- task.prioritisedTask.lowerPriority(priority); +- } +- } +- +- /** +- * Schedules the chunk data to be written asynchronously. +- * <p> +- * Impl notes: +- * </p> +- * <li> +- * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means +- * saves must be scheduled before a chunk is unloaded. +- * </li> +- * <li> +- * Writes may be called concurrently, although only the "later" write will go through. +- * </li> +- * +- * @param world Chunk's world +- * @param chunkX Chunk's x coordinate +- * @param chunkZ Chunk's z coordinate +- * @param data Chunk's data +- * @param type The regionfile type to write to. +- * +- * @throws IllegalStateException If the file io thread has shutdown. +- */ +- public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, +- final RegionFileType type) { +- RegionFileIOThread.scheduleSave(world, chunkX, chunkZ, data, type, Priority.NORMAL); +- } +- +- /** +- * Schedules the chunk data to be written asynchronously. +- * <p> +- * Impl notes: +- * </p> +- * <li> +- * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means +- * saves must be scheduled before a chunk is unloaded. +- * </li> +- * <li> +- * Writes may be called concurrently, although only the "later" write will go through. +- * </li> +- * +- * @param world Chunk's world +- * @param chunkX Chunk's x coordinate +- * @param chunkZ Chunk's z coordinate +- * @param data Chunk's data +- * @param type The regionfile type to write to. +- * @param priority The minimum priority to schedule at. +- * +- * @throws IllegalStateException If the file io thread has shutdown. +- */ +- public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, +- final RegionFileType type, final Priority priority) { +- final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); +- thread.scheduleSaveInternal(world, chunkX, chunkZ, data, type, priority); +- } +- +- void scheduleSaveInternal(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, +- final RegionFileType type, final Priority priority) { +- final ChunkDataController taskController = getControllerFor(world, type); +- +- final boolean[] created = new boolean[1]; +- final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); +- final ChunkDataTask task = taskController.tasks.compute(key, (final long keyInMap, final ChunkDataTask taskRunning) -> { +- if (taskRunning == null || taskRunning.failedWrite) { +- // no task is scheduled or the previous write failed - meaning we need to overwrite it +- +- // create task +- final ChunkDataTask newTask = new ChunkDataTask(world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority); +- newTask.inProgressWrite = data; +- created[0] = true; +- +- return newTask; +- } +- +- taskRunning.inProgressWrite = data; +- +- return taskRunning; +- }); +- +- if (created[0]) { +- task.prioritisedTask.queue(); +- } else { +- task.prioritisedTask.raisePriority(priority); +- } +- } +- +- /** +- * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call +- * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} +- * for single load. +- * <p> +- * Impl notes: +- * </p> +- * <li> +- * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may +- * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of +- * data is undefined behaviour, and can cause deadlock. +- * </li> +- * +- * @param world Chunk's world +- * @param chunkX Chunk's x coordinate +- * @param chunkZ Chunk's z coordinate +- * @param onComplete Consumer to execute once this task has completed +- * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost +- * of this call. +- * +- * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. +- * +- * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) +- * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) +- * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) +- * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) +- */ +- public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, +- final Consumer<RegionFileData> onComplete, final boolean intendingToBlock) { +- return RegionFileIOThread.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL); +- } +- +- /** +- * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call +- * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} +- * for single load. +- * <p> +- * Impl notes: +- * </p> +- * <li> +- * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may +- * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of +- * data is undefined behaviour, and can cause deadlock. +- * </li> +- * +- * @param world Chunk's world +- * @param chunkX Chunk's x coordinate +- * @param chunkZ Chunk's z coordinate +- * @param onComplete Consumer to execute once this task has completed +- * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost +- * of this call. +- * @param priority The minimum priority to load the data at. +- * +- * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. +- * +- * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) +- * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) +- * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) +- * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) +- */ +- public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, +- final Consumer<RegionFileData> onComplete, final boolean intendingToBlock, +- final Priority priority) { +- return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES); +- } +- +- /** +- * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and +- * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} +- * for single load. +- * <p> +- * Impl notes: +- * </p> +- * <li> +- * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may +- * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of +- * data is undefined behaviour, and can cause deadlock. +- * </li> +- * +- * @param world Chunk's world +- * @param chunkX Chunk's x coordinate +- * @param chunkZ Chunk's z coordinate +- * @param onComplete Consumer to execute once this task has completed +- * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost +- * of this call. +- * @param types The regionfile type(s) to load. +- * +- * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. +- * +- * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) +- * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) +- * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) +- * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) +- */ +- public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, +- final Consumer<RegionFileData> onComplete, final boolean intendingToBlock, +- final RegionFileType... types) { +- return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL, types); +- } +- +- /** +- * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and +- * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} +- * for single load. +- * <p> +- * Impl notes: +- * </p> +- * <li> +- * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may +- * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of +- * data is undefined behaviour, and can cause deadlock. +- * </li> +- * +- * @param world Chunk's world +- * @param chunkX Chunk's x coordinate +- * @param chunkZ Chunk's z coordinate +- * @param onComplete Consumer to execute once this task has completed +- * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost +- * of this call. +- * @param types The regionfile type(s) to load. +- * @param priority The minimum priority to load the data at. +- * +- * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. +- * +- * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) +- * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) +- * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) +- * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) +- */ +- public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, +- final Consumer<RegionFileData> onComplete, final boolean intendingToBlock, +- final Priority priority, final RegionFileType... types) { +- if (types == null) { +- throw new NullPointerException("Types cannot be null"); +- } +- if (types.length == 0) { +- throw new IllegalArgumentException("Types cannot be empty"); +- } +- +- final RegionFileData ret = new RegionFileData(); +- +- final Cancellable[] reads = new CancellableRead[types.length]; +- final AtomicInteger completions = new AtomicInteger(); +- final int expectedCompletions = types.length; +- +- for (int i = 0; i < expectedCompletions; ++i) { +- final RegionFileType type = types[i]; +- reads[i] = RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, +- (final CompoundTag data, final Throwable throwable) -> { +- if (throwable != null) { +- ret.setThrowable(type, throwable); +- } else { +- ret.setData(type, data); +- } +- +- if (completions.incrementAndGet() == expectedCompletions) { +- onComplete.accept(ret); +- } +- }, intendingToBlock, priority); +- } +- +- return new CancellableReads(reads); +- } +- +- /** +- * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call +- * {@code onComplete}. +- * <p> +- * Impl notes: +- * </p> +- * <li> +- * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may +- * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of +- * data is undefined behaviour, and can cause deadlock. +- * </li> +- * +- * @param world Chunk's world +- * @param chunkX Chunk's x coordinate +- * @param chunkZ Chunk's z coordinate +- * @param onComplete Consumer to execute once this task has completed +- * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost +- * of this call. +- * +- * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. +- * +- * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) +- * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) +- * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) +- * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) +- */ +- public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, +- final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete, +- final boolean intendingToBlock) { +- return RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, Priority.NORMAL); +- } +- +- /** +- * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call +- * {@code onComplete}. +- * <p> +- * Impl notes: +- * </p> +- * <li> +- * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may +- * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of +- * data is undefined behaviour, and can cause deadlock. +- * </li> +- * +- * @param world Chunk's world +- * @param chunkX Chunk's x coordinate +- * @param chunkZ Chunk's z coordinate +- * @param onComplete Consumer to execute once this task has completed +- * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost +- * of this call. +- * @param priority Minimum priority to load the data at. +- * +- * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. +- * +- * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) +- * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) +- * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) +- * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) +- */ +- public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, +- final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete, +- final boolean intendingToBlock, final Priority priority) { +- final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); +- return thread.loadDataAsyncInternal(world, chunkX, chunkZ, type, onComplete, intendingToBlock, priority); +- } +- +- Cancellable loadDataAsyncInternal(final ServerLevel world, final int chunkX, final int chunkZ, +- final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete, +- final boolean intendingToBlock, final Priority priority) { +- final ChunkDataController taskController = getControllerFor(world, type); +- +- final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion(); +- +- final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); +- final BiLong1Function<ChunkDataTask, ChunkDataTask> compute = (final long keyInMap, final ChunkDataTask running) -> { +- if (running == null) { +- // not scheduled +- +- // set up task +- final ChunkDataTask newTask = new ChunkDataTask( +- world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority +- ); +- newTask.inProgressRead = new InProgressRead(); +- newTask.inProgressRead.addToAsyncWaiters(onComplete); +- +- callbackInfo.tasksNeedsScheduling = true; +- return newTask; +- } +- +- final CompoundTag pendingWrite = running.inProgressWrite; +- +- if (pendingWrite == ChunkDataTask.NOTHING_TO_WRITE) { +- // need to add to waiters here, because the regionfile thread will use compute() to lock and check for cancellations +- if (!running.inProgressRead.addToAsyncWaiters(onComplete)) { +- callbackInfo.data = running.inProgressRead.value; +- callbackInfo.throwable = running.inProgressRead.throwable; +- callbackInfo.completeNow = true; +- } +- return running; +- } +- +- // at this stage we have to use the in progress write's data to avoid an order issue +- callbackInfo.data = pendingWrite; +- callbackInfo.throwable = null; +- callbackInfo.completeNow = true; +- return running; +- }; +- +- final ChunkDataTask ret = taskController.tasks.compute(key, compute); +- +- // needs to be scheduled +- if (callbackInfo.tasksNeedsScheduling) { +- ret.prioritisedTask.queue(); +- } else if (callbackInfo.completeNow) { +- try { +- onComplete.accept(callbackInfo.data == null ? null : callbackInfo.data.copy(), callbackInfo.throwable); +- } catch (final Throwable thr) { +- LOGGER.error("Callback " + ConcurrentUtil.genericToString(onComplete) + " synchronously failed to handle chunk data for task " + ret.toString(), thr); +- } +- } else { +- // we're waiting on a task we didn't schedule, so raise its priority to what we want +- ret.prioritisedTask.raisePriority(priority); +- } +- +- return new CancellableRead(onComplete, ret); +- } +- +- /** +- * Schedules a load task to be executed asynchronously, and blocks on that task. +- * +- * @param world Chunk's world +- * @param chunkX Chunk's x coordinate +- * @param chunkZ Chunk's z coordinate +- * @param type Regionfile type +- * @param priority Minimum priority to load the data at. +- * +- * @return The chunk data for the chunk. Note that a {@code null} result means the chunk or regionfile does not exist on disk. +- * +- * @throws IOException If the load fails for any reason +- */ +- public static CompoundTag loadData(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, +- final Priority priority) throws IOException { +- final CompletableFuture<CompoundTag> ret = new CompletableFuture<>(); +- +- RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> { +- if (thr != null) { +- ret.completeExceptionally(thr); +- } else { +- ret.complete(compound); +- } +- }, true, priority); +- +- try { +- return ret.join(); +- } catch (final CompletionException ex) { +- throw new IOException(ex); +- } +- } +- +- private static final class ImmediateCallbackCompletion { +- +- public CompoundTag data; +- public Throwable throwable; +- public boolean completeNow; +- public boolean tasksNeedsScheduling; +- +- } +- +- private static final class CancellableRead implements Cancellable { +- +- private BiConsumer<CompoundTag, Throwable> callback; +- private ChunkDataTask task; +- +- CancellableRead(final BiConsumer<CompoundTag, Throwable> callback, final ChunkDataTask task) { +- this.callback = callback; +- this.task = task; +- } +- +- @Override +- public boolean cancel() { +- final BiConsumer<CompoundTag, Throwable> callback = this.callback; +- final ChunkDataTask task = this.task; +- +- if (callback == null || task == null) { +- return false; +- } +- +- this.callback = null; +- this.task = null; +- +- final InProgressRead read = task.inProgressRead; +- +- // read can be null if no read was scheduled (i.e no regionfile existed or chunk in regionfile didn't) +- return read != null && read.cancel(callback); +- } +- } +- +- private static final class CancellableReads implements Cancellable { +- +- private Cancellable[] reads; +- +- private static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class); +- +- CancellableReads(final Cancellable[] reads) { +- this.reads = reads; +- } +- +- @Override +- public boolean cancel() { +- final Cancellable[] reads = (Cancellable[])READS_HANDLE.getAndSet((CancellableReads)this, (Cancellable[])null); +- +- if (reads == null) { +- return false; +- } +- +- boolean ret = false; +- +- for (final Cancellable read : reads) { +- ret |= read.cancel(); +- } +- +- return ret; +- } +- } +- +- private static final class InProgressRead { +- +- private static final Logger LOGGER = LoggerFactory.getLogger(InProgressRead.class); +- +- private CompoundTag value; +- private Throwable throwable; +- private final MultiThreadedQueue<BiConsumer<CompoundTag, Throwable>> callbacks = new MultiThreadedQueue<>(); +- +- public boolean hasNoWaiters() { +- return this.callbacks.isEmpty(); +- } +- +- public boolean addToAsyncWaiters(final BiConsumer<CompoundTag, Throwable> callback) { +- return this.callbacks.add(callback); +- } +- +- public boolean cancel(final BiConsumer<CompoundTag, Throwable> callback) { +- return this.callbacks.remove(callback); +- } +- +- public void complete(final ChunkDataTask task, final CompoundTag value, final Throwable throwable) { +- this.value = value; +- this.throwable = throwable; +- +- BiConsumer<CompoundTag, Throwable> consumer; +- while ((consumer = this.callbacks.pollOrBlockAdds()) != null) { +- try { +- consumer.accept(value == null ? null : value.copy(), throwable); +- } catch (final Throwable thr) { +- LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data for task " + task.toString(), thr); +- } +- } +- } +- } +- +- public static abstract class ChunkDataController { +- +- // ConcurrentHashMap synchronizes per chain, so reduce the chance of task's hashes colliding. +- private final ConcurrentLong2ReferenceChainedHashTable<ChunkDataTask> tasks = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(8192, 0.5f); +- +- public final RegionFileType type; +- +- public ChunkDataController(final RegionFileType type) { +- this.type = type; +- } +- +- public abstract RegionFileStorage getCache(); +- +- public abstract void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException; +- +- public abstract CompoundTag readData(final int chunkX, final int chunkZ) throws IOException; +- +- public boolean hasTasks() { +- return !this.tasks.isEmpty(); +- } +- +- public boolean doesRegionFileNotExist(final int chunkX, final int chunkZ) { +- return ((ChunkSystemRegionFileStorage)(Object)this.getCache()).moonrise$doesRegionFileNotExistNoIO(chunkX, chunkZ); +- } +- +- public <T> T computeForRegionFile(final int chunkX, final int chunkZ, final boolean existingOnly, final Function<RegionFile, T> function) { +- final RegionFileStorage cache = this.getCache(); +- final RegionFile regionFile; +- synchronized (cache) { +- try { +- if (existingOnly) { +- regionFile = ((ChunkSystemRegionFileStorage)(Object)cache).moonrise$getRegionFileIfExists(chunkX, chunkZ); +- } else { +- regionFile = cache.getRegionFile(new ChunkPos(chunkX, chunkZ), existingOnly); +- } +- } catch (final IOException ex) { +- throw new RuntimeException(ex); +- } +- +- return function.apply(regionFile); +- } +- } +- +- public <T> T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function<RegionFile, T> function) { +- final RegionFileStorage cache = this.getCache(); +- final RegionFile regionFile; +- +- synchronized (cache) { +- regionFile = ((ChunkSystemRegionFileStorage)(Object)cache).moonrise$getRegionFileIfLoaded(chunkX, chunkZ); +- +- return function.apply(regionFile); +- } +- } +- } +- +- private static final class ChunkDataTask implements Runnable { +- +- private static final CompoundTag NOTHING_TO_WRITE = new CompoundTag(); +- +- private static final Logger LOGGER = LoggerFactory.getLogger(ChunkDataTask.class); +- +- private InProgressRead inProgressRead; +- private volatile CompoundTag inProgressWrite = NOTHING_TO_WRITE; // only needs to be acquire/release +- +- private boolean failedWrite; +- +- private final ServerLevel world; +- private final int chunkX; +- private final int chunkZ; +- private final ChunkDataController taskController; +- +- private final PrioritisedTask prioritisedTask; +- +- /* +- * IO thread will perform reads before writes for a given chunk x and z +- * +- * How reads/writes are scheduled: +- * +- * If read is scheduled while scheduling write, take no special action and just schedule write +- * If read is scheduled while scheduling read and no write is scheduled, chain the read task +- * +- * +- * If write is scheduled while scheduling read, use the pending write data and ret immediately (so no read is scheduled) +- * If write is scheduled while scheduling write (ignore read in progress), overwrite the write in progress data +- * +- * This allows the reads and writes to act as if they occur synchronously to the thread scheduling them, however +- * it fails to properly propagate write failures thanks to writes overwriting each other +- */ +- +- public ChunkDataTask(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkDataController taskController, +- final PrioritisedExecutor executor, final Priority priority) { +- this.world = world; +- this.chunkX = chunkX; +- this.chunkZ = chunkZ; +- this.taskController = taskController; +- this.prioritisedTask = executor.createTask(this, priority); +- } +- +- @Override +- public String toString() { +- return "Task for world: '" + WorldUtil.getWorldName(this.world) + "' at (" + this.chunkX + "," + this.chunkZ + +- ") type: " + this.taskController.type.name() + ", hash: " + this.hashCode(); +- } +- +- @Override +- public void run() { +- final InProgressRead read = this.inProgressRead; +- final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ); +- +- if (read != null) { +- final boolean[] canRead = new boolean[] { true }; +- +- if (read.hasNoWaiters()) { +- // cancelled read? go to task controller to confirm +- final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> { +- if (valueInMap == null) { +- throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); +- } +- if (valueInMap != ChunkDataTask.this) { +- throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); +- } +- +- if (!read.hasNoWaiters()) { +- return valueInMap; +- } else { +- canRead[0] = false; +- } +- +- return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap; +- }); +- +- if (inMap == null) { +- // read is cancelled - and no write pending, so we're done +- return; +- } +- // if there is a write in progress, we don't actually have to worry about waiters gaining new entries - +- // the readers will just use the in progress write, so the value in canRead is good to use without +- // further synchronisation. +- } +- +- if (canRead[0]) { +- CompoundTag compound = null; +- Throwable throwable = null; +- +- try { +- compound = this.taskController.readData(this.chunkX, this.chunkZ); +- } catch (final Throwable thr) { +- throwable = thr; +- LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr); +- } +- read.complete(this, compound, throwable); +- } +- } +- +- CompoundTag write = this.inProgressWrite; +- +- if (write == NOTHING_TO_WRITE) { +- final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> { +- if (valueInMap == null) { +- throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); +- } +- if (valueInMap != ChunkDataTask.this) { +- throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); +- } +- return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap; +- }); +- +- if (inMap == null) { +- return; // set the task value to null, indicating we're done +- } // else: inProgressWrite changed, so now we have something to write +- } +- +- for (;;) { +- write = this.inProgressWrite; +- final CompoundTag dataWritten = write; +- +- boolean failedWrite = false; +- +- try { +- this.taskController.writeData(this.chunkX, this.chunkZ, write); +- } catch (final Throwable thr) { +- if (thr instanceof RegionFileStorage.RegionFileSizeException) { +- final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024); +- LOGGER.error("Chunk at (" + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk."); +- } else { +- failedWrite = thr instanceof IOException; +- LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr); +- } +- } +- +- final boolean finalFailWrite = failedWrite; +- final boolean[] done = new boolean[] { false }; +- +- this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> { +- if (valueInMap == null) { +- throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); +- } +- if (valueInMap != ChunkDataTask.this) { +- throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); +- } +- if (valueInMap.inProgressWrite == dataWritten) { +- valueInMap.failedWrite = finalFailWrite; +- done[0] = true; +- // keep the data in map if we failed the write so we can try to prevent data loss +- return finalFailWrite ? valueInMap : null; +- } +- // different data than expected, means we need to retry write +- return valueInMap; +- }); +- +- if (done[0]) { +- return; +- } +- +- // fetch & write new data +- continue; +- } +- } +- } +-} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java +index c35e0c29700be48dda3e53e7d2db224766ef17b7..a36ab89f5c37f5f9ab0152f087bb4cf3560f8581 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java +@@ -1,22 +1,24 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller; + +-import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemChunkMap; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; + import ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkStorage; + import net.minecraft.nbt.CompoundTag; + import net.minecraft.server.level.ServerLevel; + import net.minecraft.world.level.ChunkPos; + import net.minecraft.world.level.chunk.storage.RegionFileStorage; + import java.io.IOException; +-import java.util.Optional; + import java.util.concurrent.CompletableFuture; + import java.util.concurrent.CompletionException; + +-public final class ChunkDataController extends RegionFileIOThread.ChunkDataController { ++public final class ChunkDataController extends MoonriseRegionFileIO.RegionDataController { + + private final ServerLevel world; + +- public ChunkDataController(final ServerLevel world) { +- super(RegionFileIOThread.RegionFileType.CHUNK_DATA); ++ public ChunkDataController(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { ++ super(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor); + this.world = world; + } + +@@ -26,31 +28,23 @@ public final class ChunkDataController extends RegionFileIOThread.ChunkDataContr + } + + @Override +- public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { +- final CompletableFuture<Void> future = this.world.getChunkSource().chunkMap.write(new ChunkPos(chunkX, chunkZ), compound); +- +- try { +- if (future != null) { +- // rets non-null when sync writing (i.e. future should be completed here) +- future.join(); +- } +- } catch (final CompletionException ex) { +- if (ex.getCause() instanceof IOException ioException) { +- throw ioException; +- } +- throw ex; +- } ++ public WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$startWrite(chunkX, chunkZ, compound); + } + + @Override +- public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException { +- try { +- return this.world.getChunkSource().chunkMap.read(new ChunkPos(chunkX, chunkZ)).join().orElse(null); +- } catch (final CompletionException ex) { +- if (ex.getCause() instanceof IOException ioException) { +- throw ioException; +- } +- throw ex; +- } ++ public void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException { ++ ((ChunkSystemChunkMap)this.world.getChunkSource().chunkMap).moonrise$writeFinishCallback(new ChunkPos(chunkX, chunkZ)); ++ ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishWrite(chunkX, chunkZ, writeData); ++ } ++ ++ @Override ++ public ReadData readData(final int chunkX, final int chunkZ) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$readData(chunkX, chunkZ); ++ } ++ ++ @Override ++ public CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishRead(chunkX, chunkZ, readData); + } + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java +index fdd189ef056187941d43809c5d61cab717aecf60..828c868f68c2a20bf90d0f7ec253fdeb591f15f6 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java +@@ -1,6 +1,8 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller; + +-import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; + import net.minecraft.nbt.CompoundTag; + import net.minecraft.world.level.ChunkPos; + import net.minecraft.world.level.chunk.storage.EntityStorage; +@@ -9,12 +11,12 @@ import net.minecraft.world.level.chunk.storage.RegionStorageInfo; + import java.io.IOException; + import java.nio.file.Path; + +-public final class EntityDataController extends RegionFileIOThread.ChunkDataController { ++public final class EntityDataController extends MoonriseRegionFileIO.RegionDataController { + + private final EntityRegionFileStorage storage; + +- public EntityDataController(final EntityRegionFileStorage storage) { +- super(RegionFileIOThread.RegionFileType.ENTITY_DATA); ++ public EntityDataController(final EntityRegionFileStorage storage, final ChunkTaskScheduler taskScheduler) { ++ super(MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor); + this.storage = storage; + } + +@@ -24,13 +26,35 @@ public final class EntityDataController extends RegionFileIOThread.ChunkDataCont + } + + @Override +- public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { +- this.storage.write(new ChunkPos(chunkX, chunkZ), compound); ++ public WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { ++ checkPosition(new ChunkPos(chunkX, chunkZ), compound); ++ ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$startWrite(chunkX, chunkZ, compound); ++ } ++ ++ @Override ++ public void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException { ++ ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishWrite(chunkX, chunkZ, writeData); ++ } ++ ++ @Override ++ public ReadData readData(final int chunkX, final int chunkZ) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$readData(chunkX, chunkZ); + } + + @Override +- public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException { +- return this.storage.read(new ChunkPos(chunkX, chunkZ)); ++ public CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishRead(chunkX, chunkZ, readData); ++ } ++ ++ private static void checkPosition(final ChunkPos pos, final CompoundTag nbt) { ++ final ChunkPos nbtPos = nbt == null ? null : EntityStorage.readChunkPos(nbt); ++ if (nbtPos != null && !pos.equals(nbtPos)) { ++ throw new IllegalArgumentException( ++ "Entity chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos.toString() ++ + " but compound says coordinate is " + nbtPos ++ ); ++ } + } + + public static final class EntityRegionFileStorage extends RegionFileStorage { +@@ -42,13 +66,7 @@ public final class EntityDataController extends RegionFileIOThread.ChunkDataCont + + @Override + public void write(final ChunkPos pos, final CompoundTag nbt) throws IOException { +- final ChunkPos nbtPos = nbt == null ? null : EntityStorage.readChunkPos(nbt); +- if (nbtPos != null && !pos.equals(nbtPos)) { +- throw new IllegalArgumentException( +- "Entity chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos.toString() +- + " but compound says coordinate is " + nbtPos + " for world: " + this +- ); +- } ++ checkPosition(pos, nbt); + super.write(pos, nbt); + } + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java +index af867f8fedd0bb8f675e94243aa1a3f17363483b..bd0d782852f9cfe5bc0b5339ecf4d82c10332ec9 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java +@@ -1,18 +1,20 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller; + +-import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; + import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; + import net.minecraft.nbt.CompoundTag; + import net.minecraft.server.level.ServerLevel; + import net.minecraft.world.level.chunk.storage.RegionFileStorage; + import java.io.IOException; + +-public final class PoiDataController extends RegionFileIOThread.ChunkDataController { ++public final class PoiDataController extends MoonriseRegionFileIO.RegionDataController { + + private final ServerLevel world; + +- public PoiDataController(final ServerLevel world) { +- super(RegionFileIOThread.RegionFileType.POI_DATA); ++ public PoiDataController(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { ++ super(MoonriseRegionFileIO.RegionFileType.POI_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor); + this.world = world; + } + +@@ -22,12 +24,22 @@ public final class PoiDataController extends RegionFileIOThread.ChunkDataControl + } + + @Override +- public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { +- ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$write(chunkX, chunkZ, compound); ++ public WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$startWrite(chunkX, chunkZ, compound); + } + + @Override +- public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException { +- return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$read(chunkX, chunkZ); ++ public void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException { ++ ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishWrite(chunkX, chunkZ, writeData); ++ } ++ ++ @Override ++ public ReadData readData(final int chunkX, final int chunkZ) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$readData(chunkX, chunkZ); ++ } ++ ++ @Override ++ public CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishRead(chunkX, chunkZ, readData); + } + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..47a4d3376d08dde94a39254bec21473ff27f53e6 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java +@@ -0,0 +1,10 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level; ++ ++import net.minecraft.world.level.ChunkPos; ++import java.io.IOException; ++ ++public interface ChunkSystemChunkMap { ++ ++ public void moonrise$writeFinishCallback(final ChunkPos pos) throws IOException; ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java +index efcd9057f008f0b9cf0d22b2b21d1851205841e5..5d4d650186b18eb00782429d53d861564d8e4ba9 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java +@@ -1,5 +1,6 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.level; + ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; + import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup; + import net.minecraft.world.level.chunk.ChunkAccess; + import net.minecraft.world.level.chunk.LevelChunk; +@@ -19,4 +20,14 @@ public interface ChunkSystemLevel { + + public void moonrise$midTickTasks(); + ++ public ChunkData moonrise$getChunkData(final long chunkKey); ++ ++ public ChunkData moonrise$getChunkData(final int chunkX, final int chunkZ); ++ ++ public ChunkData moonrise$requestChunkData(final long chunkKey); ++ ++ public ChunkData moonrise$releaseChunkData(final long chunkKey); ++ ++ public boolean moonrise$areChunksLoaded(final int fromX, final int fromZ, final int toX, final int toZ); ++ + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java +index b8a87b7e6505feb76ce1bd58c84615256cf6faa6..9d46482476f9ed9032a2b0f89afc20e03ed42dbb 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java +@@ -1,12 +1,13 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.level; + +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.Priority; + import ca.spottedleaf.moonrise.common.list.ReferenceList; + import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; +-import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; + import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader; + import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; + import net.minecraft.core.BlockPos; ++import net.minecraft.server.level.ChunkHolder; + import net.minecraft.server.level.ServerChunkCache; + import net.minecraft.world.level.chunk.ChunkAccess; + import net.minecraft.world.level.chunk.status.ChunkStatus; +@@ -17,32 +18,34 @@ public interface ChunkSystemServerLevel extends ChunkSystemLevel { + + public ChunkTaskScheduler moonrise$getChunkTaskScheduler(); + +- public RegionFileIOThread.ChunkDataController moonrise$getChunkDataController(); ++ public MoonriseRegionFileIO.RegionDataController moonrise$getChunkDataController(); + +- public RegionFileIOThread.ChunkDataController moonrise$getPoiChunkDataController(); ++ public MoonriseRegionFileIO.RegionDataController moonrise$getPoiChunkDataController(); + +- public RegionFileIOThread.ChunkDataController moonrise$getEntityChunkDataController(); ++ public MoonriseRegionFileIO.RegionDataController moonrise$getEntityChunkDataController(); + + public int moonrise$getRegionChunkShift(); + +- // Paper - marked closing not needed on CB ++ public boolean moonrise$isMarkedClosing(); ++ ++ public void moonrise$setMarkedClosing(final boolean value); + + public RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader(); + + public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, +- final PrioritisedExecutor.Priority priority, ++ final Priority priority, + final Consumer<List<ChunkAccess>> onLoad); + + public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, +- final ChunkStatus chunkStatus, final PrioritisedExecutor.Priority priority, ++ final ChunkStatus chunkStatus, final Priority priority, + final Consumer<List<ChunkAccess>> onLoad); + + public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, +- final PrioritisedExecutor.Priority priority, ++ final Priority priority, + final Consumer<List<ChunkAccess>> onLoad); + + public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, +- final ChunkStatus chunkStatus, final PrioritisedExecutor.Priority priority, ++ final ChunkStatus chunkStatus, final Priority priority, + final Consumer<List<ChunkAccess>> onLoad); + + public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder(); +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8b9dc582627b46843f4b5ea6f8c3df2d8cac46fa +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java +@@ -0,0 +1,21 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; ++ ++import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; ++ ++public final class ChunkData { ++ ++ private int referenceCount = 0; ++ public NearbyPlayers.TrackedChunk nearbyPlayers; // Moonrise - nearby players ++ ++ public ChunkData() { ++ ++ } ++ ++ public int increaseRef() { ++ return ++this.referenceCount; ++ } ++ ++ public int decreaseRef() { ++ return --this.referenceCount; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java +index 883fe6401f1b9711fa544d18a815b4d638f580df..aacd543f03b35908011d0c2891e978cc093ebcf5 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java +@@ -1,9 +1,12 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; + ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; + import net.minecraft.server.level.ChunkMap; + + public interface ChunkSystemDistanceManager { + + public ChunkMap moonrise$getChunkMap(); + ++ public ChunkHolderManager moonrise$getChunkHolderManager(); ++ + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java +index 5c7f2471a0b15ac2e714527296ad2aa7291999eb..40dc7569c32b100d4eebbde9024ded2acbb2fb20 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java +@@ -1,6 +1,8 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.level.entity; + ++import ca.spottedleaf.moonrise.common.PlatformHooks; + import ca.spottedleaf.moonrise.common.list.EntityList; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; + import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity; + import com.google.common.collect.ImmutableList; + import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; +@@ -13,6 +15,7 @@ import net.minecraft.server.level.FullChunkStatus; + import net.minecraft.server.level.ServerLevel; + import net.minecraft.util.Mth; + import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntitySpawnReason; + import net.minecraft.world.entity.EntityType; + import net.minecraft.world.entity.boss.EnderDragonPart; + import net.minecraft.world.entity.boss.enderdragon.EnderDragon; +@@ -26,7 +29,6 @@ import java.util.Arrays; + import java.util.Iterator; + import java.util.List; + import java.util.function.Predicate; +-import org.bukkit.event.entity.EntityRemoveEvent; + + public final class ChunkEntitySlices { + +@@ -43,6 +45,7 @@ public final class ChunkEntitySlices { + private final EntityList entities = new EntityList(); + + public FullChunkStatus status; ++ public final ChunkData chunkData; + + private boolean isTransient; + +@@ -55,7 +58,7 @@ public final class ChunkEntitySlices { + } + + public ChunkEntitySlices(final Level world, final int chunkX, final int chunkZ, final FullChunkStatus status, +- final int minSection, final int maxSection) { // inclusive, inclusive ++ final ChunkData chunkData, final int minSection, final int maxSection) { // inclusive, inclusive + this.minSection = minSection; + this.maxSection = maxSection; + this.chunkX = chunkX; +@@ -68,11 +71,12 @@ public final class ChunkEntitySlices { + this.entitiesByType = new Reference2ObjectOpenHashMap<>(); + + this.status = status; ++ this.chunkData = chunkData; + } + + public static List<Entity> readEntities(final ServerLevel world, final CompoundTag compoundTag) { + // TODO check this and below on update for format changes +- return EntityType.loadEntitiesRecursive(compoundTag.getList("Entities", 10), world).collect(ImmutableList.toImmutableList()); ++ return EntityType.loadEntitiesRecursive(compoundTag.getList("Entities", 10), world, EntitySpawnReason.LOAD).collect(ImmutableList.toImmutableList()); + } + + // Paper start - rewrite chunk system +@@ -100,18 +104,7 @@ public final class ChunkEntitySlices { + } + + final ListTag entitiesTag = new ListTag(); +- final java.util.Map<net.minecraft.world.entity.EntityType<?>, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk +- for (final Entity entity : entities) { +- // Paper start - Entity load/save limit per chunk +- final EntityType<?> entityType = entity.getType(); +- final int saveLimit = world.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); +- if (saveLimit > -1) { +- if (savedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { +- continue; +- } +- savedEntityCounts.merge(entityType, 1, Integer::sum); +- } +- // Paper end - Entity load/save limit per chunk ++ for (final Entity entity : PlatformHooks.get().modifySavedEntities(world, chunkPos.x, chunkPos.z, entities)) { + CompoundTag compoundTag = new CompoundTag(); + if (entity.save(compoundTag)) { + entitiesTag.add(compoundTag); +@@ -158,12 +151,12 @@ public final class ChunkEntitySlices { + continue; + } + if (entity.shouldBeSaved()) { +- entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD); ++ PlatformHooks.get().unloadEntity(entity); + if (entity.isVehicle()) { + // we cannot assume that these entities are contained within this chunk, because entities can + // desync - so we need to remove them all + for (final Entity passenger : entity.getIndirectPassengers()) { +- passenger.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD); ++ PlatformHooks.get().unloadEntity(passenger); + } + } + } +@@ -172,33 +165,6 @@ public final class ChunkEntitySlices { + return this.entities.size() != 0; + } + +- // Paper start +- public org.bukkit.entity.Entity[] getChunkEntities() { +- List<org.bukkit.entity.Entity> ret = new java.util.ArrayList<>(); +- final Entity[] entities = this.entities.getRawData(); +- for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { +- final Entity entity = entities[i]; +- if (entity == null) { +- continue; +- } +- final org.bukkit.entity.Entity bukkit = entity.getBukkitEntity(); +- if (bukkit != null && bukkit.isValid()) { +- ret.add(bukkit); +- } +- } +- +- return ret.toArray(new org.bukkit.entity.Entity[0]); +- } +- +- public void callEntitiesLoadEvent() { +- org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesLoadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities()); +- } +- +- public void callEntitiesUnloadEvent() { +- org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesUnloadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities()); +- } +- // Paper end +- + private List<Entity> getAllEntities() { + final int len = this.entities.size(); + if (len == 0) { +@@ -262,6 +228,7 @@ public final class ChunkEntitySlices { + return false; + } + ((ChunkSystemEntity)entity).moonrise$setChunkStatus(this.status); ++ ((ChunkSystemEntity)entity).moonrise$setChunkData(this.chunkData); + final int sectionIndex = chunkSection - this.minSection; + + this.allEntities.addEntity(entity, sectionIndex); +@@ -295,6 +262,7 @@ public final class ChunkEntitySlices { + return false; + } + ((ChunkSystemEntity)entity).moonrise$setChunkStatus(null); ++ ((ChunkSystemEntity)entity).moonrise$setChunkData(null); + final int sectionIndex = chunkSection - this.minSection; + + this.allEntities.removeEntity(entity, sectionIndex); +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java +index efc0c1acc8239dd7b00211a1d3bfd3fc3b2c810c..93335de8cf514dc8417e4b9b2d495663deda2904 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java +@@ -46,8 +46,6 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> { + + protected final SWMRLong2ObjectHashTable<ChunkSlicesRegion> regions = new SWMRLong2ObjectHashTable<>(128, 0.5f); + +- protected final int minSection; // inclusive +- protected final int maxSection; // inclusive + protected final LevelCallback<Entity> worldCallback; + + protected final ConcurrentLong2ReferenceChainedHashTable<Entity> entityById = new ConcurrentLong2ReferenceChainedHashTable<>(); +@@ -56,8 +54,6 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> { + + public EntityLookup(final Level world, final LevelCallback<Entity> worldCallback) { + this.world = world; +- this.minSection = WorldUtil.getMinSection(world); +- this.maxSection = WorldUtil.getMaxSection(world); + this.worldCallback = worldCallback; + } + +@@ -91,7 +87,7 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> { + + protected abstract void entityEndTicking(final Entity entity); + +- protected abstract boolean screenEntity(final Entity entity); ++ protected abstract boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event); + + private static Entity maskNonAccessible(final Entity entity) { + if (entity == null) { +@@ -347,7 +343,7 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> { + } + + protected void addRecursivelySafe(final Entity root, final boolean fromDisk) { +- if (!this.addEntity(root, fromDisk)) { ++ if (!this.addEntity(root, fromDisk, true)) { + // possible we are a passenger, and so should dismount from any valid entity in the world + root.stopRiding(); + return; +@@ -386,7 +382,11 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> { + } + + public boolean addNewEntity(final Entity entity) { +- return this.addEntity(entity, false); ++ return this.addNewEntity(entity, true); ++ } ++ ++ public boolean addNewEntity(final Entity entity, final boolean event) { ++ return this.addEntity(entity, false, event); + } + + public static Visibility getEntityStatus(final Entity entity) { +@@ -397,10 +397,10 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> { + return Visibility.fromFullChunkStatus(entityStatus == null ? FullChunkStatus.INACCESSIBLE : entityStatus); + } + +- protected boolean addEntity(final Entity entity, final boolean fromDisk) { ++ protected boolean addEntity(final Entity entity, final boolean fromDisk, final boolean event) { + final BlockPos pos = entity.blockPosition(); + final int sectionX = pos.getX() >> 4; +- final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection); ++ final int sectionY = Mth.clamp(pos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); + final int sectionZ = pos.getZ() >> 4; + this.checkThread(sectionX, sectionZ, "Cannot add entity off-main thread"); + +@@ -414,7 +414,7 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> { + return false; + } + +- if (!this.screenEntity(entity)) { ++ if (!this.screenEntity(entity, fromDisk, event)) { + return false; + } + +@@ -519,7 +519,7 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> { + final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ(); + final BlockPos newPos = entity.blockPosition(); + final int newSectionX = newPos.getX() >> 4; +- final int newSectionY = Mth.clamp(newPos.getY() >> 4, this.minSection, this.maxSection); ++ final int newSectionY = Mth.clamp(newPos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); + final int newSectionZ = newPos.getZ() >> 4; + + if (newSectionX == sectionX && newSectionY == sectionY && newSectionZ == sectionZ) { +@@ -959,7 +959,7 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> { + + public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) { + final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); +- ChunkEntitySlices ret; ++ final ChunkEntitySlices ret; + if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) { + return this.createEntityChunk(chunkX, chunkZ, true); + } +@@ -1057,7 +1057,7 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> { + @Override + public void onRemove(final Entity.RemovalReason reason) { + final Entity entity = this.entity; +- EntityLookup.this.checkThread(entity, "Cannot remove entity off-main"); // Paper - rewrite chunk system ++ EntityLookup.this.checkThread(entity, "Cannot remove entity off-main"); + final Visibility tickingState = EntityLookup.getEntityStatus(entity); + + EntityLookup.this.removeEntity(entity); +@@ -1080,4 +1080,4 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> { + @Override + public void onRemove(final Entity.RemovalReason reason) {} + } +-} +\ No newline at end of file ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java +index edcde00206d068bd79175fea33efa05b0e8c1562..a038215156a163b0b1cbc870ada5b4ac85ed1335 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java +@@ -1,5 +1,6 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.client; + ++import ca.spottedleaf.moonrise.common.PlatformHooks; + import ca.spottedleaf.moonrise.common.util.CoordinateUtils; + import ca.spottedleaf.moonrise.common.util.WorldUtil; + import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; +@@ -45,7 +46,8 @@ public final class ClientEntityLookup extends EntityLookup { + + final ChunkEntitySlices ret = new ChunkEntitySlices( + this.world, chunkX, chunkZ, +- ticking ? FullChunkStatus.ENTITY_TICKING : FullChunkStatus.FULL, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) ++ ticking ? FullChunkStatus.ENTITY_TICKING : FullChunkStatus.FULL, null, ++ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) + ); + + // note: not handled by superclass +@@ -63,7 +65,11 @@ public final class ClientEntityLookup extends EntityLookup { + protected void entitySectionChangeCallback(final Entity entity, + final int oldSectionX, final int oldSectionY, final int oldSectionZ, + final int newSectionX, final int newSectionY, final int newSectionZ) { +- ++ PlatformHooks.get().entityMove( ++ entity, ++ CoordinateUtils.getChunkSectionKey(oldSectionX, oldSectionY, oldSectionZ), ++ CoordinateUtils.getChunkSectionKey(newSectionX, newSectionY, newSectionZ) ++ ); + } + + @Override +@@ -97,7 +103,7 @@ public final class ClientEntityLookup extends EntityLookup { + } + + @Override +- protected boolean screenEntity(final Entity entity) { ++ protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) { + return true; + } + +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java +index 465469e44346c50f30f3abd6b44f4173ccfcf248..2ff58cf753c60913ee73aae015182e9c5560d529 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java +@@ -32,7 +32,7 @@ public final class DefaultEntityLookup extends EntityLookup { + protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { + final ChunkEntitySlices ret = new ChunkEntitySlices( + this.world, chunkX, chunkZ, FullChunkStatus.FULL, +- WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) ++ null, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) + ); + + // note: not handled by superclass +@@ -84,7 +84,7 @@ public final class DefaultEntityLookup extends EntityLookup { + } + + @Override +- protected boolean screenEntity(final Entity entity) { ++ protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) { + return true; + } + +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java +index dacf2b2988ce603879fe525a3418ac77f8a663f7..58d9187adc188b693b6becc400f766e069bf1bf5 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java +@@ -1,6 +1,8 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server; + ++import ca.spottedleaf.moonrise.common.PlatformHooks; + import ca.spottedleaf.moonrise.common.list.ReferenceList; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; + import ca.spottedleaf.moonrise.common.util.TickThread; + import ca.spottedleaf.moonrise.common.util.ChunkSystem; + import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +@@ -17,7 +19,6 @@ public final class ServerEntityLookup extends EntityLookup { + + private final ServerLevel serverWorld; + public final ReferenceList<Entity> trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker +- public final ReferenceList<Entity> trackerUnloadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker + + public ServerEntityLookup(final ServerLevel world, final LevelCallback<Entity> worldCallback) { + super(world, worldCallback); +@@ -63,6 +64,11 @@ public final class ServerEntityLookup extends EntityLookup { + if (entity instanceof ServerPlayer player) { + ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().tickPlayer(player); + } ++ PlatformHooks.get().entityMove( ++ entity, ++ CoordinateUtils.getChunkSectionKey(oldSectionX, oldSectionY, oldSectionZ), ++ CoordinateUtils.getChunkSectionKey(newSectionX, newSectionY, newSectionZ) ++ ); + } + + @Override +@@ -77,14 +83,12 @@ public final class ServerEntityLookup extends EntityLookup { + if (entity instanceof ServerPlayer player) { + ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().removePlayer(player); + } +- this.trackerUnloadedEntities.remove(entity); // Moonrise - entity tracker + } + + @Override + protected void entityStartLoaded(final Entity entity) { + // Moonrise start - entity tracker + this.trackerEntities.add(entity); +- this.trackerUnloadedEntities.remove(entity); + // Moonrise end - entity tracker + } + +@@ -92,7 +96,6 @@ public final class ServerEntityLookup extends EntityLookup { + protected void entityEndLoaded(final Entity entity) { + // Moonrise start - entity tracker + this.trackerEntities.remove(entity); +- this.trackerUnloadedEntities.add(entity); + // Moonrise end - entity tracker + } + +@@ -107,7 +110,7 @@ public final class ServerEntityLookup extends EntityLookup { + } + + @Override +- protected boolean screenEntity(final Entity entity) { +- return ChunkSystem.screenEntity(this.serverWorld, entity); ++ protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) { ++ return ChunkSystem.screenEntity(this.serverWorld, entity, fromDisk, event); + } + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java +index fd35e4db0c8fec8f86b8743bcc2b15ed2e7433f1..bbf9d6c1c9525d97160806819a57be03eca290f1 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java +@@ -3,7 +3,6 @@ package ca.spottedleaf.moonrise.patches.chunk_system.level.poi; + import ca.spottedleaf.moonrise.common.util.CoordinateUtils; + import ca.spottedleaf.moonrise.common.util.TickThread; + import ca.spottedleaf.moonrise.common.util.WorldUtil; +-import com.mojang.serialization.Codec; + import com.mojang.serialization.DataResult; + import net.minecraft.SharedConstants; + import net.minecraft.nbt.CompoundTag; +@@ -123,7 +122,6 @@ public final class PoiChunk { + ret.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion()); + + final ServerLevel world = this.world; +- final PoiManager poiManager = world.getPoiManager(); + final int chunkX = this.chunkX; + final int chunkZ = this.chunkZ; + +@@ -133,13 +131,8 @@ public final class PoiChunk { + continue; + } + +- final long key = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ); +- // codecs are honestly such a fucking disaster. What the fuck is this trash? +- final Codec<PoiSection> codec = PoiSection.codec(() -> { +- poiManager.setDirty(key); +- }); +- +- final DataResult<Tag> serializedResult = codec.encodeStart(registryOps, section); ++ // I do not believe asynchronously converting to CompoundTag is worth the scheduling. ++ final DataResult<Tag> serializedResult = PoiSection.Packed.CODEC.encodeStart(registryOps, section.pack()); + final int finalSectionY = sectionY; + final Tag serialized = serializedResult.resultOrPartial((final String description) -> { + LOGGER.error("Failed to serialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); +@@ -183,19 +176,18 @@ public final class PoiChunk { + continue; + } + +- final long coordinateKey = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ); +- // codecs are honestly such a fucking disaster. What the fuck is this trash? +- final Codec<PoiSection> codec = PoiSection.codec(() -> { +- poiManager.setDirty(coordinateKey); +- }); +- + final CompoundTag section = sections.getCompound(key); +- final DataResult<PoiSection> deserializeResult = codec.parse(registryOps, section); ++ final DataResult<PoiSection.Packed> deserializeResult = PoiSection.Packed.CODEC.parse(registryOps, section); + final int finalSectionY = sectionY; +- final PoiSection deserialized = deserializeResult.resultOrPartial((final String description) -> { ++ final PoiSection.Packed packed = deserializeResult.resultOrPartial((final String description) -> { + LOGGER.error("Failed to deserialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); + }).orElse(null); + ++ final long coordinateKey = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ); ++ final PoiSection deserialized = packed == null ? null : packed.unpack(() -> { ++ poiManager.setDirty(coordinateKey); ++ }); ++ + if (deserialized == null || ((ChunkSystemPoiSection)deserialized).moonrise$isEmpty()) { + // completely empty, no point in storing this + continue; +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java +index 3f5edb756beb9c31b6f591a24b778d6ac2b0bf51..23bd741dc0bfd48b8064a2b498d71cfa8f8145b2 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java +@@ -1,12 +1,8 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.level.storage; + +-import com.mojang.serialization.Dynamic; + import net.minecraft.nbt.CompoundTag; +-import net.minecraft.nbt.Tag; + import net.minecraft.world.level.chunk.storage.RegionFileStorage; + import java.io.IOException; +-import java.util.Optional; +-import java.util.concurrent.CompletableFuture; + + public interface ChunkSystemSectionStorage { + +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +index a608f57ebca98eda88ad749d0aad021678be54f9..b2fa9883aefb07f64bb5db7e0052218d2ad09aba 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +@@ -1,7 +1,8 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.player; + +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; + import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.PlatformHooks; + import ca.spottedleaf.moonrise.common.misc.AllocatingRateLimiter; + import ca.spottedleaf.moonrise.common.misc.SingleUserAreaMap; + import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +@@ -412,7 +413,11 @@ public final class RegionizedPlayerChunkLoader { + if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { + ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager + .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$addReceivedChunk(this.player); +- PlayerChunkSender.sendChunk(this.player.connection, this.world, ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ)); ++ ++ final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ); ++ ++ PlatformHooks.get().onChunkWatch(this.world, chunk, this.player); ++ PlayerChunkSender.sendChunk(this.player.connection, this.world, chunk); + return; + } + throw new IllegalStateException(); +@@ -426,17 +431,12 @@ public final class RegionizedPlayerChunkLoader { + } + + private void sendUnloadChunkRaw(final int chunkX, final int chunkZ) { ++ PlatformHooks.get().onChunkUnWatch(this.world, new ChunkPos(chunkX, chunkZ), this.player); + // Note: Check PlayerChunkSender#dropChunk for other logic + // Note: drop isAlive() check so that chunks properly unload client-side when the player dies + ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager + .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$removeReceivedChunk(this.player); +- final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); +- this.player.connection.send(new ClientboundForgetLevelChunkPacket(chunkPos)); +- // Paper start - PlayerChunkUnloadEvent +- if (io.papermc.paper.event.packet.PlayerChunkUnloadEvent.getHandlerList().getRegisteredListeners().length > 0) { +- new io.papermc.paper.event.packet.PlayerChunkUnloadEvent(this.world.getWorld().getChunkAt(chunkPos.longKey), this.player.getBukkitEntity()).callEvent(); +- } +- // Paper end - PlayerChunkUnloadEvent ++ this.player.connection.send(new ClientboundForgetLevelChunkPacket(new ChunkPos(chunkX, chunkZ))); + } + + private final SingleUserAreaMap<PlayerChunkLoaderData> broadcastMap = new SingleUserAreaMap<>(this) { +@@ -529,7 +529,7 @@ public final class RegionizedPlayerChunkLoader { + final int playerSendViewDistance, final int worldSendViewDistance) { + return Math.min( + loadViewDistance - 1, +- playerSendViewDistance < 0 ? (!io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance ++ playerSendViewDistance < 0 ? (!PlatformHooks.get().configAutoConfigSendDistance() || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance + ); + } + +@@ -554,26 +554,26 @@ public final class RegionizedPlayerChunkLoader { + } + + private double getMaxChunkLoadRate() { +- final double configRate = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate; ++ final double configRate = PlatformHooks.get().configPlayerMaxLoadRate(); + + return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); + } + + private double getMaxChunkGenRate() { +- final double configRate = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate; ++ final double configRate = PlatformHooks.get().configPlayerMaxGenRate(); + + return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); + } + + private double getMaxChunkSendRate() { +- final double configRate = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate; ++ final double configRate = PlatformHooks.get().configPlayerMaxSendRate(); + + return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); + } + + private long getMaxChunkLoads() { + final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L); +- long configLimit = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads; ++ long configLimit = (long)PlatformHooks.get().configPlayerMaxConcurrentLoads(); + if (configLimit == 0L) { + // by default, only allow 1/5th of the chunks in the view distance to be concurrently active + configLimit = Math.max(5L, radiusChunks / 5L); +@@ -587,7 +587,7 @@ public final class RegionizedPlayerChunkLoader { + + private long getMaxChunkGenerates() { + final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L); +- long configLimit = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates; ++ long configLimit = (long)PlatformHooks.get().configPlayerMaxConcurrentGens(); + if (configLimit == 0L) { + // by default, only allow 1/5th of the chunks in the view distance to be concurrently active + configLimit = Math.max(5L, radiusChunks / 5L); +@@ -709,7 +709,7 @@ public final class RegionizedPlayerChunkLoader { + final int queuedChunkX = CoordinateUtils.getChunkX(queuedLoadChunk); + final int queuedChunkZ = CoordinateUtils.getChunkZ(queuedLoadChunk); + ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkLoad( +- queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, PrioritisedExecutor.Priority.NORMAL, null ++ queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, Priority.NORMAL, null + ); + if (this.removed) { + return; +@@ -825,7 +825,7 @@ public final class RegionizedPlayerChunkLoader { + } + if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) { + // not yet post-processed, need to do this so that tile entities can properly be sent to clients +- chunk.postProcessGeneration(); ++ chunk.postProcessGeneration(this.world); + // check if there was any recursive action + if (this.removed || this.sendQueue.isEmpty() || this.sendQueue.firstLong() != pendingSend) { + return; +@@ -864,7 +864,6 @@ public final class RegionizedPlayerChunkLoader { + final int clientViewDistance = getClientViewDistance(this.player); + final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance); + +- // TODO check PlayerList diff in paper chunk system patch + // send view distances + this.player.connection.send(this.updateClientChunkRadius(sendViewDistance)); + this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance)); +@@ -1079,8 +1078,7 @@ public final class RegionizedPlayerChunkLoader { + // now all tickets should be removed, which is all of our external state + } + +- // For external checks +- public it.unimi.dsi.fastutil.longs.LongOpenHashSet getSentChunksRaw() { ++ public LongOpenHashSet getSentChunksRaw() { + return this.sentChunks; + } + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +index 58d3d1a47e9f2423c467bb329c2d5f4b58a8b5ef..f98df65eaed2abedc66f3a49790e0cfb65354ed9 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +@@ -1,14 +1,14 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; + +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; + import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; + import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.PlatformHooks; + import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +-import ca.spottedleaf.moonrise.common.util.MoonriseCommon; + import ca.spottedleaf.moonrise.common.util.TickThread; + import ca.spottedleaf.moonrise.common.util.WorldUtil; + import ca.spottedleaf.moonrise.common.util.ChunkSystem; +-import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; + import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; + import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; + import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; +@@ -20,7 +20,6 @@ import ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket; + import ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet; + import com.google.gson.JsonArray; + import com.google.gson.JsonObject; +-import com.mojang.logging.LogUtils; + import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap; + import it.unimi.dsi.fastutil.longs.Long2ByteMap; + import it.unimi.dsi.fastutil.longs.Long2IntMap; +@@ -40,7 +39,9 @@ import net.minecraft.server.level.TicketType; + import net.minecraft.util.SortedArraySet; + import net.minecraft.util.Unit; + import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.LevelChunk; + import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; + import java.io.IOException; + import java.text.DecimalFormat; + import java.util.ArrayDeque; +@@ -58,7 +59,7 @@ import java.util.function.Predicate; + + public final class ChunkHolderManager { + +- private static final Logger LOGGER = LogUtils.getClassLogger(); ++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkHolderManager.class); + + public static final int FULL_LOADED_TICKET_LEVEL = ChunkLevel.FULL_CHUNK_LEVEL; + public static final int BLOCK_TICKING_TICKET_LEVEL = ChunkLevel.BLOCK_TICKING_LEVEL; +@@ -189,7 +190,7 @@ public final class ChunkHolderManager { + if (halt) { + LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'"); + if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) { +- LOGGER.warn("Failed to halt world generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'"); ++ LOGGER.warn("Failed to halt generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'"); + } else { + LOGGER.info("Halted chunk system for world '" + WorldUtil.getWorldName(this.world) + "'"); + } +@@ -199,21 +200,21 @@ public final class ChunkHolderManager { + this.saveAllChunks(true, true, true); + } + +- boolean hasTasks = false; +- for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) { +- if (RegionFileIOThread.getControllerFor(this.world, type).hasTasks()) { +- hasTasks = true; +- break; ++ MoonriseRegionFileIO.flush(this.world); ++ ++ if (halt) { ++ LOGGER.info("Waiting 60s for chunk I/O to halt for world '" + WorldUtil.getWorldName(this.world) + "'"); ++ if (!this.taskScheduler.haltIO(true, TimeUnit.SECONDS.toNanos(60L))) { ++ LOGGER.warn("Failed to halt I/O tasks for world '" + WorldUtil.getWorldName(this.world) + "'"); ++ } else { ++ LOGGER.info("Halted I/O scheduler for world '" + WorldUtil.getWorldName(this.world) + "'"); + } + } +- if (hasTasks) { +- RegionFileIOThread.flush(); +- } + + // kill regionfile cache +- for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) { ++ for (final MoonriseRegionFileIO.RegionFileType type : MoonriseRegionFileIO.RegionFileType.values()) { + try { +- RegionFileIOThread.getControllerFor(this.world, type).getCache().close(); ++ MoonriseRegionFileIO.getControllerFor(this.world, type).getCache().close(); + } catch (final IOException ex) { + LOGGER.error("Failed to close '" + type.name() + "' regionfile cache for world '" + WorldUtil.getWorldName(this.world) + "'", ex); + } +@@ -230,8 +231,8 @@ public final class ChunkHolderManager { + public void autoSave() { + final List<NewChunkHolder> reschedule = new ArrayList<>(); + final long currentTick = this.currentTick; +- final long maxSaveTime = currentTick - Math.max(1L, this.world.paperConfig().chunks.autoSaveInterval.value()); +- final int maxToSave = this.world.paperConfig().chunks.maxAutoSaveChunksPerTick; ++ final long maxSaveTime = currentTick - Math.max(1L, PlatformHooks.get().configAutoSaveInterval()); ++ final int maxToSave = PlatformHooks.get().configMaxAutoSavePerTick(); + for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) { + final NewChunkHolder holder = this.autoSaveQueue.first(); + +@@ -271,55 +272,74 @@ public final class ChunkHolderManager { + + long start = System.nanoTime(); + long lastLog = start; +- boolean needsFlush = false; +- final int flushInterval = 50; ++ final int flushInterval = 200; ++ int lastFlush = 0; + + int savedChunk = 0; + int savedEntity = 0; + int savedPoi = 0; + ++ if (shutdown) { ++ // Normal unload process does not occur during shutdown: fire event manually ++ // for mods that expect ChunkEvent.Unload to fire on shutdown (before LevelEvent.Unload) ++ for (int i = 0, len = holders.size(); i < len; ++i) { ++ final NewChunkHolder holder = holders.get(i); ++ if (holder.getCurrentChunk() instanceof LevelChunk levelChunk) { ++ PlatformHooks.get().chunkUnloadFromWorld(levelChunk); ++ } ++ } ++ } + for (int i = 0, len = holders.size(); i < len; ++i) { + final NewChunkHolder holder = holders.get(i); + try { + final NewChunkHolder.SaveStat saveStat = holder.save(shutdown); + if (saveStat != null) { +- ++saved; +- needsFlush = flush; + if (saveStat.savedChunk()) { + ++savedChunk; ++ ++saved; + } + if (saveStat.savedEntityChunk()) { + ++savedEntity; ++ ++saved; + } + if (saveStat.savedPoiChunk()) { + ++savedPoi; ++ ++saved; + } + } + } catch (final Throwable thr) { + LOGGER.error("Failed to save chunk (" + holder.chunkX + "," + holder.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr); + } +- if (needsFlush && (saved % flushInterval) == 0) { +- needsFlush = false; +- RegionFileIOThread.partialFlush(flushInterval / 2); ++ if (flush && (saved - lastFlush) > (flushInterval / 2)) { ++ lastFlush = saved; ++ MoonriseRegionFileIO.partialFlush(this.world, flushInterval / 2); + } + if (logProgress) { + final long currTime = System.nanoTime(); + if ((currTime - lastLog) > TimeUnit.SECONDS.toNanos(10L)) { + lastLog = currTime; +- LOGGER.info("Saved " + saved + " chunks (" + format.format((double)(i+1)/(double)len * 100.0) + "%) in world '" + WorldUtil.getWorldName(this.world) + "'"); ++ LOGGER.info( ++ "Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi ++ + " poi chunks in world '" + WorldUtil.getWorldName(this.world) + "', progress: " ++ + format.format((double)(i+1)/(double)len * 100.0) ++ ); + } + } + } + if (flush) { +- RegionFileIOThread.flush(); ++ MoonriseRegionFileIO.flush(this.world); + try { +- RegionFileIOThread.flushRegionStorages(this.world); ++ MoonriseRegionFileIO.flushRegionStorages(this.world); + } catch (final IOException ex) { + LOGGER.error("Exception when flushing regions in world '" + WorldUtil.getWorldName(this.world) + "'", ex); + } + } + if (logProgress) { +- LOGGER.info("Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi + " poi chunks in world '" + WorldUtil.getWorldName(this.world) + "' in " + format.format(1.0E-9 * (System.nanoTime() - start)) + "s"); ++ LOGGER.info( ++ "Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi ++ + " poi chunks in world '" + WorldUtil.getWorldName(this.world) + "' in " ++ + format.format(1.0E-9 * (System.nanoTime() - start)) + "s" ++ ); + } + } + +@@ -798,21 +818,21 @@ public final class ChunkHolderManager { + return this.chunkHolders.get(position); + } + +- public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ public void raisePriority(final int x, final int z, final Priority priority) { + final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); + if (chunkHolder != null) { + chunkHolder.raisePriority(priority); + } + } + +- public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ public void setPriority(final int x, final int z, final Priority priority) { + final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); + if (chunkHolder != null) { + chunkHolder.setPriority(priority); + } + } + +- public void lowerPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ public void lowerPriority(final int x, final int z, final Priority priority) { + final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); + if (chunkHolder != null) { + chunkHolder.lowerPriority(priority); +@@ -895,7 +915,7 @@ public final class ChunkHolderManager { + final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask(); + + if (entityLoad != null) { +- entityLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING); ++ entityLoad.raisePriority(Priority.BLOCKING); + } + } + } +@@ -971,7 +991,7 @@ public final class ChunkHolderManager { + final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask(); + + if (poiLoad != null) { +- poiLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING); ++ poiLoad.raisePriority(Priority.BLOCKING); + } + } + } finally { +@@ -1018,7 +1038,7 @@ public final class ChunkHolderManager { + } + + ChunkHolderManager.this.processPendingFullUpdate(); +- }, PrioritisedExecutor.Priority.HIGHEST); ++ }, Priority.HIGHEST); + } else { + final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate; + for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { +@@ -1028,11 +1048,10 @@ public final class ChunkHolderManager { + } + + private void removeChunkHolder(final NewChunkHolder holder) { +- holder.markUnloaded(); ++ holder.onUnload(); + this.autoSaveQueue.remove(holder); + ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder); + this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ)); +- + } + + // note: never call while inside the chunk system, this will absolutely break everything +@@ -1313,6 +1332,9 @@ public final class ChunkHolderManager { + if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { + throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager"); + } ++ if (!PlatformHooks.get().allowAsyncTicketUpdates() && !TickThread.isTickThread()) { ++ TickThread.ensureTickThread("Cannot asynchronously process ticket updates"); ++ } + + List<NewChunkHolder> changedFullStatus = null; + +@@ -1328,10 +1350,15 @@ public final class ChunkHolderManager { + } + changedFullStatus = new ArrayList<>(); + +- ret |= this.ticketLevelPropagator.performUpdates( +- this.ticketLockArea, this.taskScheduler.schedulingLockArea, +- scheduledTasks, changedFullStatus +- ); ++ this.blockTicketUpdates(); ++ try { ++ ret |= this.ticketLevelPropagator.performUpdates( ++ this.ticketLockArea, this.taskScheduler.schedulingLockArea, ++ scheduledTasks, changedFullStatus ++ ); ++ } finally { ++ this.unblockTicketUpdates(Boolean.FALSE); ++ } + } + + if (changedFullStatus != null) { +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java +index 8671a90e969d16c7a57ddc38fedb7cf01815f64c..120ce31729dc8d4bba0901ca06d3212f3158d089 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java +@@ -1,16 +1,17 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; + +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool; +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.queue.PrioritisedTaskQueue; ++import ca.spottedleaf.concurrentutil.executor.thread.PrioritisedThreadPool; + import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; + import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.config.moonrise.MoonriseConfig; + import ca.spottedleaf.moonrise.common.util.CoordinateUtils; + import ca.spottedleaf.moonrise.common.util.JsonUtil; + import ca.spottedleaf.moonrise.common.util.MoonriseCommon; + import ca.spottedleaf.moonrise.common.util.TickThread; + import ca.spottedleaf.moonrise.common.util.WorldUtil; +-import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; + import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; + import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; + import ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer; +@@ -23,7 +24,6 @@ import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkUpgrade + import ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer; + import ca.spottedleaf.moonrise.patches.chunk_system.status.ChunkSystemChunkStep; + import ca.spottedleaf.moonrise.patches.chunk_system.util.ParallelSearchRadiusIteration; +-import com.mojang.logging.LogUtils; + import com.google.gson.JsonArray; + import com.google.gson.JsonObject; + import net.minecraft.CrashReport; +@@ -48,6 +48,7 @@ import net.minecraft.world.level.chunk.status.ChunkStatus; + import net.minecraft.world.level.chunk.status.ChunkStep; + import net.minecraft.world.phys.Vec3; + import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; + import java.io.File; + import java.time.LocalDateTime; + import java.time.format.DateTimeFormatter; +@@ -63,51 +64,14 @@ import java.util.function.Consumer; + + public final class ChunkTaskScheduler { + +- private static final Logger LOGGER = LogUtils.getClassLogger(); ++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkTaskScheduler.class); + +- static int newChunkSystemIOThreads; +- static int newChunkSystemGenParallelism; +- static int newChunkSystemGenPopulationParallelism; +- static int newChunkSystemLoadParallelism; +- +- private static boolean initialised = false; +- +- public static void init(io.papermc.paper.configuration.GlobalConfiguration.ChunkSystem chunkSystem) { +- if (initialised) { +- return; +- } +- initialised = true; +- MoonriseCommon.init(chunkSystem); // Paper +- newChunkSystemIOThreads = chunkSystem.ioThreads; +- if (newChunkSystemIOThreads <= 0) { +- newChunkSystemIOThreads = 1; +- } else { +- newChunkSystemIOThreads = Math.max(1, newChunkSystemIOThreads); +- } +- +- String newChunkSystemGenParallelism = chunkSystem.genParallelism; +- if (newChunkSystemGenParallelism.equalsIgnoreCase("default")) { +- newChunkSystemGenParallelism = "true"; +- } +- +- boolean useParallelGen; +- if (newChunkSystemGenParallelism.equalsIgnoreCase("on") || newChunkSystemGenParallelism.equalsIgnoreCase("enabled") +- || newChunkSystemGenParallelism.equalsIgnoreCase("true")) { +- useParallelGen = true; +- } else if (newChunkSystemGenParallelism.equalsIgnoreCase("off") || newChunkSystemGenParallelism.equalsIgnoreCase("disabled") +- || newChunkSystemGenParallelism.equalsIgnoreCase("false")) { +- useParallelGen = false; +- } else { +- throw new IllegalStateException("Invalid option for gen-parallelism: must be one of [on, off, enabled, disabled, true, false, default]"); ++ public static void init(final boolean useParallelGen) { ++ for (final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor executor : MoonriseCommon.RADIUS_AWARE_GROUP.getAllExecutors()) { ++ executor.setMaxParallelism(useParallelGen ? -1 : 1); + } + +- ChunkTaskScheduler.newChunkSystemGenParallelism = MoonriseCommon.WORKER_THREADS; +- ChunkTaskScheduler.newChunkSystemGenPopulationParallelism = useParallelGen ? MoonriseCommon.WORKER_THREADS : 1; +- ChunkTaskScheduler.newChunkSystemLoadParallelism = MoonriseCommon.WORKER_THREADS; +- +- RegionFileIOThread.init(newChunkSystemIOThreads); +- +- LOGGER.info("Chunk system is using " + newChunkSystemIOThreads + " I/O threads, " + MoonriseCommon.WORKER_THREADS + " worker threads, and population gen parallelism of " + ChunkTaskScheduler.newChunkSystemGenPopulationParallelism + " threads"); ++ LOGGER.info("Chunk system is using population gen parallelism: " + useParallelGen); + } + + public static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_system:chunk_load", Long::compareTo); +@@ -151,13 +115,15 @@ public final class ChunkTaskScheduler { + } + + public final ServerLevel world; +- public final PrioritisedThreadPool workers; + public final RadiusAwarePrioritisedExecutor radiusAwareScheduler; +- public final PrioritisedThreadPool.PrioritisedPoolExecutor parallelGenExecutor; +- private final PrioritisedThreadPool.PrioritisedPoolExecutor radiusAwareGenExecutor; +- public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor; ++ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor parallelGenExecutor; ++ private final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor radiusAwareGenExecutor; ++ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor loadExecutor; ++ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor ioExecutor; ++ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor compressionExecutor; ++ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor saveExecutor; + +- private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue(); ++ private final PrioritisedTaskQueue mainThreadExecutor = new PrioritisedTaskQueue(); + + public final ChunkHolderManager chunkHolderManager; + +@@ -306,9 +272,8 @@ public final class ChunkTaskScheduler { + return this.lockShift; + } + +- public ChunkTaskScheduler(final ServerLevel world, final PrioritisedThreadPool workers) { ++ public ChunkTaskScheduler(final ServerLevel world) { + this.world = world; +- this.workers = workers; + // must be >= region shift (in paper, doesn't exist) and must be >= ticket propagator section shift + // it must be >= region shift since the regioniser assumes ticket updates do not occur in parallel for the region sections + // it must be >= ticket propagator section shift so that the ticket propagator can assume that owning a position implies owning +@@ -317,11 +282,14 @@ public final class ChunkTaskScheduler { + this.lockShift = Math.max(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift(), ThreadedTicketLevelPropagator.SECTION_SHIFT); + this.schedulingLockArea = new ReentrantAreaLock(this.getChunkSystemLockShift()); + +- final String worldName = WorldUtil.getWorldName(world); +- this.parallelGenExecutor = workers.createExecutor("Chunk parallel generation executor for world '" + worldName + "'", 1, Math.max(1, newChunkSystemGenParallelism)); +- this.radiusAwareGenExecutor = workers.createExecutor("Chunk radius aware generator for world '" + worldName + "'", 1, Math.max(1, newChunkSystemGenPopulationParallelism)); +- this.loadExecutor = workers.createExecutor("Chunk load executor for world '" + worldName + "'", 1, newChunkSystemLoadParallelism); +- this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, Math.max(2, 1 + newChunkSystemGenPopulationParallelism)); ++ this.parallelGenExecutor = MoonriseCommon.PARALLEL_GEN_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0); ++ this.radiusAwareGenExecutor = MoonriseCommon.RADIUS_AWARE_GROUP.createExecutor(1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0); ++ this.loadExecutor = MoonriseCommon.LOAD_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0); ++ this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, 16); ++ this.ioExecutor = MoonriseCommon.SERVER_REGION_IO_GROUP.createExecutor(-1, MoonriseCommon.IO_QUEUE_HOLD_TIME, 0); ++ // we need a separate executor here so that on shutdown we can continue to process I/O tasks ++ this.compressionExecutor = MoonriseCommon.LOAD_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0); ++ this.saveExecutor = MoonriseCommon.LOAD_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0); + this.chunkHolderManager = new ChunkHolderManager(world, this); + } + +@@ -360,7 +328,7 @@ public final class ChunkTaskScheduler { + }; + + // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions +- this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING); ++ this.scheduleChunkTask(chunkX, chunkZ, crash, Priority.BLOCKING); + // so, make the main thread pick it up + ((ChunkSystemMinecraftServer)this.world.getServer()).moonrise$setChunkSystemCrash(new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException)); + } +@@ -370,20 +338,20 @@ public final class ChunkTaskScheduler { + return this.mainThreadExecutor.executeTask(); + } + +- public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ public void raisePriority(final int x, final int z, final Priority priority) { + this.chunkHolderManager.raisePriority(x, z, priority); + } + +- public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ public void setPriority(final int x, final int z, final Priority priority) { + this.chunkHolderManager.setPriority(x, z, priority); + } + +- public void lowerPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ public void lowerPriority(final int x, final int z, final Priority priority) { + this.chunkHolderManager.lowerPriority(x, z, priority); + } + + public void scheduleTickingState(final int chunkX, final int chunkZ, final FullChunkStatus toStatus, +- final boolean addTicket, final PrioritisedExecutor.Priority priority, ++ final boolean addTicket, final Priority priority, + final Consumer<LevelChunk> onComplete) { + final int radius = toStatus.ordinal() - 1; // 0 -> BORDER, 1 -> TICKING, 2 -> ENTITY_TICKING + +@@ -479,7 +447,7 @@ public final class ChunkTaskScheduler { + } + + public void scheduleChunkLoad(final int chunkX, final int chunkZ, final boolean gen, final ChunkStatus toStatus, final boolean addTicket, +- final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) { ++ final Priority priority, final Consumer<ChunkAccess> onComplete) { + if (gen) { + this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + return; +@@ -503,7 +471,7 @@ public final class ChunkTaskScheduler { + + // only appropriate to use with syncLoadNonFull + public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus, +- final PrioritisedExecutor.Priority priority) { ++ final Priority priority) { + final int accessRadius = getAccessRadius(toStatus); + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus); +@@ -548,6 +516,11 @@ public final class ChunkTaskScheduler { + if (status == null || status.isOrAfter(ChunkStatus.FULL)) { + throw new IllegalArgumentException("Status: " + status); + } ++ ++ if (!TickThread.isTickThread()) { ++ return this.world.getChunkSource().getChunk(chunkX, chunkZ, status, true); ++ } ++ + ChunkAccess loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status); + if (loaded != null) { + return loaded; +@@ -558,7 +531,7 @@ public final class ChunkTaskScheduler { + this.chunkHolderManager.addTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId); + this.chunkHolderManager.processTicketUpdates(); + +- this.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, PrioritisedExecutor.Priority.BLOCKING); ++ this.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, Priority.BLOCKING); + + // we could do a simple spinwait here, since we do not need to process tasks while performing this load + // but we process tasks only because it's a better use of the time spent +@@ -578,7 +551,7 @@ public final class ChunkTaskScheduler { + } + + public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket, +- final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) { ++ final Priority priority, final Consumer<ChunkAccess> onComplete) { + if (!TickThread.isTickThreadFor(this.world, chunkX, chunkZ)) { + this.scheduleChunkTask(chunkX, chunkZ, () -> { + ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); +@@ -670,7 +643,7 @@ public final class ChunkTaskScheduler { + + private ChunkProgressionTask createTask(final int chunkX, final int chunkZ, final ChunkAccess chunk, + final NewChunkHolder chunkHolder, final StaticCache2D<GenerationChunkHolder> neighbours, +- final ChunkStatus toStatus, final PrioritisedExecutor.Priority initialPriority) { ++ final ChunkStatus toStatus, final Priority initialPriority) { + if (toStatus == ChunkStatus.EMPTY) { + return new ChunkLoadTask(this, this.world, chunkX, chunkZ, chunkHolder, initialPriority); + } +@@ -686,7 +659,7 @@ public final class ChunkTaskScheduler { + + ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, final NewChunkHolder chunkHolder, + final List<ChunkProgressionTask> allTasks) { +- return this.schedule(chunkX, chunkZ, targetStatus, chunkHolder, allTasks, chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)); ++ return this.schedule(chunkX, chunkZ, targetStatus, chunkHolder, allTasks, chunkHolder.getEffectivePriority(Priority.NORMAL)); + } + + // rets new task scheduled for the _specified_ chunk +@@ -695,7 +668,7 @@ public final class ChunkTaskScheduler { + // schedule will ignore the generation target, so it should be checked by the caller to ensure the target is not regressed! + private ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, + final NewChunkHolder chunkHolder, final List<ChunkProgressionTask> allTasks, +- final PrioritisedExecutor.Priority minPriority) { ++ final Priority minPriority) { + if (!this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, getAccessRadius(targetStatus))) { + throw new IllegalStateException("Not holding scheduling lock"); + } +@@ -705,8 +678,8 @@ public final class ChunkTaskScheduler { + return null; + } + +- final PrioritisedExecutor.Priority requestedPriority = PrioritisedExecutor.Priority.max( +- minPriority, chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) ++ final Priority requestedPriority = Priority.max( ++ minPriority, chunkHolder.getEffectivePriority(Priority.NORMAL) + ); + final ChunkStatus currentGenStatus = chunkHolder.getCurrentGenStatus(); + final ChunkAccess chunk = chunkHolder.getCurrentChunk(); +@@ -743,7 +716,7 @@ public final class ChunkTaskScheduler { + + final int neighbourReadRadius = Math.max( + 0, +- chunkPyramid.getStepTo(toStatus).getAccumulatedRadiusOf(ChunkStatus.EMPTY) ++ chunkStep.getAccumulatedRadiusOf(ChunkStatus.EMPTY) + ); + + boolean unGeneratedNeighbours = false; +@@ -783,7 +756,7 @@ public final class ChunkTaskScheduler { + + final ChunkProgressionTask task = this.createTask( + chunkX, chunkZ, chunk, chunkHolder, neighbours, toStatus, +- chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) ++ chunkHolder.getEffectivePriority(Priority.NORMAL) + ); + allTasks.add(task); + +@@ -794,7 +767,7 @@ public final class ChunkTaskScheduler { + + // rets true if the neighbour is not at the required status, false otherwise + private boolean checkNeighbour(final int chunkX, final int chunkZ, final ChunkStatus requiredStatus, final NewChunkHolder center, +- final List<ChunkProgressionTask> tasks, final PrioritisedExecutor.Priority minPriority) { ++ final List<ChunkProgressionTask> tasks, final Priority minPriority) { + final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ); + + if (chunkHolder == null) { +@@ -830,41 +803,41 @@ public final class ChunkTaskScheduler { + */ + @Deprecated + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) { +- return this.scheduleChunkTask(run, PrioritisedExecutor.Priority.NORMAL); ++ return this.scheduleChunkTask(run, Priority.NORMAL); + } + + /** + * @deprecated Chunk tasks must be tied to coordinates in the future + */ + @Deprecated +- public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final PrioritisedExecutor.Priority priority) { +- return this.mainThreadExecutor.queueRunnable(run, priority); ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final Priority priority) { ++ return this.mainThreadExecutor.queueTask(run, priority); + } + + public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) { +- return this.createChunkTask(chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); ++ return this.createChunkTask(chunkX, chunkZ, run, Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run, +- final PrioritisedExecutor.Priority priority) { ++ final Priority priority) { + return this.mainThreadExecutor.createTask(run, priority); + } + + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) { +- return this.scheduleChunkTask(chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); ++ return this.scheduleChunkTask(chunkX, chunkZ, run, Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run, +- final PrioritisedExecutor.Priority priority) { +- return this.mainThreadExecutor.queueRunnable(run, priority); ++ final Priority priority) { ++ return this.mainThreadExecutor.queueTask(run, priority); + } + + public boolean halt(final boolean sync, final long maxWaitNS) { + this.radiusAwareGenExecutor.halt(); + this.parallelGenExecutor.halt(); + this.loadExecutor.halt(); +- final long time = System.nanoTime(); + if (sync) { ++ final long time = System.nanoTime(); + for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { + if ( + !this.radiusAwareGenExecutor.isActive() && +@@ -882,6 +855,29 @@ public final class ChunkTaskScheduler { + return true; + } + ++ public boolean haltIO(final boolean sync, final long maxWaitNS) { ++ this.ioExecutor.halt(); ++ this.saveExecutor.halt(); ++ this.compressionExecutor.halt(); ++ if (sync) { ++ final long time = System.nanoTime(); ++ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { ++ if ( ++ !this.ioExecutor.isActive() && ++ !this.saveExecutor.isActive() && ++ !this.compressionExecutor.isActive() ++ ) { ++ return true; ++ } ++ if ((System.nanoTime() - time) >= maxWaitNS) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ + public static final ArrayDeque<ChunkInfo> WAITING_CHUNKS = new ArrayDeque<>(); // stack + + public static final class ChunkInfo { +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java +index 45eda96fd8a1acb87dbb69ce5495fec7e451416f..381631e405895ba3eede1cd2e1011c64aadbd662 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java +@@ -1,18 +1,20 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; + +-import ca.spottedleaf.concurrentutil.completable.Completable; ++import ca.spottedleaf.concurrentutil.completable.CallbackCompletable; + import ca.spottedleaf.concurrentutil.executor.Cancellable; +-import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask; +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; + import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; + import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.PlatformHooks; ++import ca.spottedleaf.moonrise.common.misc.LazyRunnable; + import ca.spottedleaf.moonrise.common.util.CoordinateUtils; + import ca.spottedleaf.moonrise.common.util.TickThread; + import ca.spottedleaf.moonrise.common.util.WorldUtil; + import ca.spottedleaf.moonrise.common.util.ChunkSystem; +-import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemFeatures; +-import ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData; +-import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; + import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; + import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder; + import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; +@@ -36,13 +38,14 @@ import net.minecraft.server.level.ChunkHolder; + import net.minecraft.server.level.ChunkLevel; + import net.minecraft.server.level.FullChunkStatus; + import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.progress.ChunkProgressListener; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.level.ChunkPos; + import net.minecraft.world.level.chunk.ChunkAccess; + import net.minecraft.world.level.chunk.ImposterProtoChunk; + import net.minecraft.world.level.chunk.LevelChunk; + import net.minecraft.world.level.chunk.status.ChunkStatus; +-import net.minecraft.world.level.chunk.storage.ChunkSerializer; ++import net.minecraft.world.level.chunk.storage.SerializableChunkData; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + import java.lang.invoke.VarHandle; +@@ -58,6 +61,8 @@ public final class NewChunkHolder { + + private static final Logger LOGGER = LoggerFactory.getLogger(NewChunkHolder.class); + ++ public final ChunkData holderData; ++ + public final ServerLevel world; + public final int chunkX; + public final int chunkZ; +@@ -89,7 +94,7 @@ public final class NewChunkHolder { + if (this.entityChunk == null) { + ret = this.entityChunk = new ChunkEntitySlices( + this.world, this.chunkX, this.chunkZ, this.getChunkStatus(), +- WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) ++ this.holderData, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) + ); + + ret.setTransient(transientChunk); +@@ -173,7 +178,7 @@ public final class NewChunkHolder { + // no tasks to schedule _for_ + } else { + entityDataLoadTask = this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( +- this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL) + ); + entityDataLoadTask.addCallback(this::completeEntityLoad); + // need one schedule() per waiter +@@ -220,7 +225,7 @@ public final class NewChunkHolder { + + if (this.entityDataLoadTask == null) { + this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( +- this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL) + ); + this.entityDataLoadTask.addCallback(this::completeEntityLoad); + this.entityDataLoadTaskWaiters = new ArrayList<>(); +@@ -294,7 +299,7 @@ public final class NewChunkHolder { + // no tasks to schedule _for_ + } else { + poiDataLoadTask = this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( +- this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL) + ); + poiDataLoadTask.addCallback(this::completePoiLoad); + // need one schedule() per waiter +@@ -340,7 +345,7 @@ public final class NewChunkHolder { + + if (this.poiDataLoadTask == null) { + this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( +- this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL) + ); + this.poiDataLoadTask.addCallback(this::completePoiLoad); + this.poiDataLoadTaskWaiters = new ArrayList<>(); +@@ -519,15 +524,15 @@ public final class NewChunkHolder { + // priority state + + // the target priority for this chunk to generate at +- private PrioritisedExecutor.Priority priority = null; ++ private Priority priority = null; + private boolean priorityLocked; + + // the priority neighbouring chunks have requested this chunk generate at +- private PrioritisedExecutor.Priority neighbourRequestedPriority = null; ++ private Priority neighbourRequestedPriority = null; + +- public PrioritisedExecutor.Priority getEffectivePriority(final PrioritisedExecutor.Priority dfl) { +- final PrioritisedExecutor.Priority neighbour = this.neighbourRequestedPriority; +- final PrioritisedExecutor.Priority us = this.priority; ++ public Priority getEffectivePriority(final Priority dfl) { ++ final Priority neighbour = this.neighbourRequestedPriority; ++ final Priority us = this.priority; + + if (neighbour == null) { + return us == null ? dfl : us; +@@ -536,7 +541,7 @@ public final class NewChunkHolder { + return neighbour; + } + +- return PrioritisedExecutor.Priority.max(us, neighbour); ++ return Priority.max(us, neighbour); + } + + private void recalculateNeighbourRequestedPriority() { +@@ -545,18 +550,18 @@ public final class NewChunkHolder { + return; + } + +- PrioritisedExecutor.Priority max = null; ++ Priority max = null; + + for (final NewChunkHolder holder : this.neighboursWaitingForUs.keySet()) { +- final PrioritisedExecutor.Priority neighbourPriority = holder.getEffectivePriority(null); ++ final Priority neighbourPriority = holder.getEffectivePriority(null); + if (neighbourPriority != null && (max == null || neighbourPriority.isHigherPriority(max))) { + max = neighbourPriority; + } + } + +- final PrioritisedExecutor.Priority current = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL); ++ final Priority current = this.getEffectivePriority(Priority.NORMAL); + this.neighbourRequestedPriority = max; +- final PrioritisedExecutor.Priority next = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL); ++ final Priority next = this.getEffectivePriority(Priority.NORMAL); + + if (current == next) { + return; +@@ -578,7 +583,7 @@ public final class NewChunkHolder { + } + + // must hold scheduling lock +- public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ public void raisePriority(final Priority priority) { + if (this.priority != null && this.priority.isHigherOrEqualPriority(priority)) { + return; + } +@@ -591,13 +596,13 @@ public final class NewChunkHolder { + } + + // must hold scheduling lock +- public void setPriority(final PrioritisedExecutor.Priority priority) { ++ public void setPriority(final Priority priority) { + if (this.priorityLocked) { + return; + } +- final PrioritisedExecutor.Priority old = this.getEffectivePriority(null); ++ final Priority old = this.getEffectivePriority(null); + this.priority = priority; +- final PrioritisedExecutor.Priority newPriority = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL); ++ final Priority newPriority = this.getEffectivePriority(Priority.NORMAL); + + if (old != newPriority) { + if (this.generationTask != null) { +@@ -609,7 +614,7 @@ public final class NewChunkHolder { + } + + // must hold scheduling lock +- public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ public void lowerPriority(final Priority priority) { + if (this.priority != null && this.priority.isLowerOrEqualPriority(priority)) { + return; + } +@@ -632,7 +637,7 @@ public final class NewChunkHolder { + } + + // ticket level state +- public int oldTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1; ++ private int oldTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1; + private int currentTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1; + + public int getTicketLevel() { +@@ -651,6 +656,7 @@ public final class NewChunkHolder { + world.getLightEngine(), null, world.getChunkSource().chunkMap + ); + ((ChunkSystemChunkHolder)this.vanillaChunkHolder).moonrise$setRealChunkHolder(this); ++ this.holderData = ((ChunkSystemLevel)this.world).moonrise$requestChunkData(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } + + public ChunkAccess getCurrentChunk() { +@@ -750,8 +756,9 @@ public final class NewChunkHolder { + /** Unloaded from chunk map */ + private boolean unloaded; + +- void markUnloaded() { ++ void onUnload() { + this.unloaded = true; ++ ((ChunkSystemLevel)this.world).moonrise$releaseChunkData(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ)); + } + + private boolean inUnloadQueue = false; +@@ -788,9 +795,10 @@ public final class NewChunkHolder { + private UnloadTask entityDataUnload; + private UnloadTask poiDataUnload; + +- public static final record UnloadTask(Completable<CompoundTag> completable, DelayedPrioritisedTask task) {} ++ public static final record UnloadTask(CallbackCompletable<CompoundTag> completable, PrioritisedExecutor.PrioritisedTask task, ++ LazyRunnable toRun) {} + +- public UnloadTask getUnloadTask(final RegionFileIOThread.RegionFileType type) { ++ public UnloadTask getUnloadTask(final MoonriseRegionFileIO.RegionFileType type) { + switch (type) { + case CHUNK_DATA: + return this.chunkDataUnload; +@@ -803,7 +811,7 @@ public final class NewChunkHolder { + } + } + +- private void removeUnloadTask(final RegionFileIOThread.RegionFileType type) { ++ private void removeUnloadTask(final MoonriseRegionFileIO.RegionFileType type) { + switch (type) { + case CHUNK_DATA: { + this.chunkDataUnload = null; +@@ -836,10 +844,10 @@ public final class NewChunkHolder { + // chunk state + this.currentChunk = null; + this.currentGenStatus = null; +- this.lastChunkCompletion = null; + for (int i = 0; i < this.chunkCompletions.length; ++i) { +- CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, i, (ChunkCompletion)null); ++ CHUNK_COMPLETION_ARRAY_HANDLE.setRelease(this.chunkCompletions, i, (ChunkCompletion)null); + } ++ this.lastChunkCompletion = null; + // entity chunk state + this.entityChunk = null; + this.pendingEntityChunk = null; +@@ -851,22 +859,23 @@ public final class NewChunkHolder { + this.priorityLocked = false; + + if (chunk != null) { +- this.chunkDataUnload = new UnloadTask(new Completable<>(), new DelayedPrioritisedTask(PrioritisedExecutor.Priority.NORMAL)); ++ final LazyRunnable toRun = new LazyRunnable(); ++ this.chunkDataUnload = new UnloadTask(new CallbackCompletable<>(), this.scheduler.saveExecutor.createTask(toRun), toRun); + } + if (poiChunk != null) { +- this.poiDataUnload = new UnloadTask(new Completable<>(), null); ++ this.poiDataUnload = new UnloadTask(new CallbackCompletable<>(), null, null); + } + if (entityChunk != null) { +- this.entityDataUnload = new UnloadTask(new Completable<>(), null); ++ this.entityDataUnload = new UnloadTask(new CallbackCompletable<>(), null, null); + } + + return this.unloadState = (chunk != null || entityChunk != null || poiChunk != null) ? new UnloadState(this, chunk, entityChunk, poiChunk) : null; + } + + // data is null if failed or does not need to be saved +- void completeAsyncUnloadDataSave(final RegionFileIOThread.RegionFileType type, final CompoundTag data) { ++ void completeAsyncUnloadDataSave(final MoonriseRegionFileIO.RegionFileType type, final CompoundTag data) { + if (data != null) { +- RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, data, type); ++ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, data, type); + } + + this.getUnloadTask(type).completable().complete(data); +@@ -886,18 +895,19 @@ public final class NewChunkHolder { + final ChunkEntitySlices entityChunk = state.entityChunk(); + final PoiChunk poiChunk = state.poiChunk(); + +- final boolean shouldLevelChunkNotSave = ChunkSystemFeatures.forceNoSave(chunk); ++ final boolean shouldLevelChunkNotSave = PlatformHooks.get().forceNoSave(chunk); + + // unload chunk data + if (chunk != null) { + if (chunk instanceof LevelChunk levelChunk) { + levelChunk.setLoaded(false); ++ PlatformHooks.get().chunkUnloadFromWorld(levelChunk); + } + + if (!shouldLevelChunkNotSave) { + this.saveChunk(chunk, true); + } else { +- this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null); ++ this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, null); + } + + if (chunk instanceof LevelChunk levelChunk) { +@@ -1066,6 +1076,9 @@ public final class NewChunkHolder { + if (oldUnloaded != newUnloaded) { + this.checkUnload(); + } ++ ++ // Don't really have a choice but to place this hook here ++ PlatformHooks.get().onChunkHolderTicketChange(this.world, this, oldLevel, newLevel); + } + + static final int NEIGHBOUR_RADIUS = 2; +@@ -1111,24 +1124,6 @@ public final class NewChunkHolder { + private static final long CHUNK_LOADED_MASK_RAD1 = getLoadedMask(1); + private static final long CHUNK_LOADED_MASK_RAD2 = getLoadedMask(2); + +- public static boolean areNeighboursFullLoaded(final long bitset, final int radius) { +- switch (radius) { +- case 0: { +- return (bitset & CHUNK_LOADED_MASK_RAD0) == CHUNK_LOADED_MASK_RAD0; +- } +- case 1: { +- return (bitset & CHUNK_LOADED_MASK_RAD1) == CHUNK_LOADED_MASK_RAD1; +- } +- case 2: { +- return (bitset & CHUNK_LOADED_MASK_RAD2) == CHUNK_LOADED_MASK_RAD2; +- } +- +- default: { +- throw new IllegalArgumentException("Radius not recognized: " + radius); +- } +- } +- } +- + // only updated while holding scheduling lock + private FullChunkStatus pendingFullChunkStatus = FullChunkStatus.INACCESSIBLE; + // updated while holding no locks, but adds a ticket before to prevent pending status from dropping +@@ -1363,6 +1358,17 @@ public final class NewChunkHolder { + } + + private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) { ++ // Update progress listener for LevelLoadingScreen ++ if (chunk != null) { ++ final ChunkProgressListener progressListener = this.world.getChunkSource().chunkMap.progressListener; ++ if (progressListener != null) { ++ final ChunkStatus finalStatus = status; ++ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { ++ progressListener.onStatusChange(this.vanillaChunkHolder.getPos(), finalStatus); ++ }); ++ } ++ } ++ + // need to tell future statuses to complete if cancelled + do { + this.completeStatusConsumers0(status, chunk); +@@ -1386,7 +1392,7 @@ public final class NewChunkHolder { + LOGGER.error("Failed to process chunk status callback", thr); + } + } +- }, PrioritisedExecutor.Priority.HIGHEST); ++ }, Priority.HIGHEST); + } + + private final Reference2ObjectOpenHashMap<FullChunkStatus, List<Consumer<LevelChunk>>> fullStatusWaiters = new Reference2ObjectOpenHashMap<>(); +@@ -1414,7 +1420,7 @@ public final class NewChunkHolder { + LOGGER.error("Failed to process chunk status callback", thr); + } + } +- }, PrioritisedExecutor.Priority.HIGHEST); ++ }, Priority.HIGHEST); + } + + // note: must hold scheduling lock +@@ -1670,6 +1676,8 @@ public final class NewChunkHolder { + + public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {} + ++ private static final MoonriseRegionFileIO.RegionFileType[] REGION_FILE_TYPES = MoonriseRegionFileIO.RegionFileType.values(); ++ + public SaveStat save(final boolean shutdown) { + TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main"); + +@@ -1677,6 +1685,7 @@ public final class NewChunkHolder { + PoiChunk poi = this.getPoiChunk(); + ChunkEntitySlices entities = this.getEntityChunk(); + boolean executedUnloadTask = false; ++ final boolean[] executedUnloadTasks = new boolean[REGION_FILE_TYPES.length]; + + if (shutdown) { + // make sure that the async unloads complete +@@ -1686,17 +1695,22 @@ public final class NewChunkHolder { + poi = this.unloadState.poiChunk(); + entities = this.unloadState.entityChunk(); + } +- final UnloadTask chunkUnloadTask = this.chunkDataUnload; +- final DelayedPrioritisedTask chunkDataUnloadTask = chunkUnloadTask == null ? null : chunkUnloadTask.task(); +- if (chunkDataUnloadTask != null) { +- final PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnloadTask.getTask(); +- if (unloadTask != null) { +- executedUnloadTask = unloadTask.execute(); ++ for (final MoonriseRegionFileIO.RegionFileType regionFileType : REGION_FILE_TYPES) { ++ final UnloadTask unloadTask = this.getUnloadTask(regionFileType); ++ if (unloadTask == null) { ++ continue; ++ } ++ ++ final PrioritisedExecutor.PrioritisedTask task = unloadTask.task(); ++ if (task != null && task.isQueued()) { ++ final boolean executed = task.execute(); ++ executedUnloadTask |= executed; ++ executedUnloadTasks[regionFileType.ordinal()] = executed; + } + } + } + +- final boolean forceNoSaveChunk = ChunkSystemFeatures.forceNoSave(chunk); ++ final boolean forceNoSaveChunk = PlatformHooks.get().forceNoSave(chunk); + + // can only synchronously save worldgen chunks during shutdown + boolean canSaveChunk = !forceNoSaveChunk && (chunk != null && ((shutdown || chunk instanceof LevelChunk) && chunk.isUnsaved())); +@@ -1717,106 +1731,55 @@ public final class NewChunkHolder { + } + } + +- return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI ? new SaveStat(executedUnloadTask || canSaveChunk, canSaveEntities, canSavePOI): null; +- } +- +- static final class AsyncChunkSerializeTask implements Runnable { +- +- private final ServerLevel world; +- private final ChunkAccess chunk; +- private final AsyncChunkSaveData asyncSaveData; +- private final NewChunkHolder toComplete; +- +- public AsyncChunkSerializeTask(final ServerLevel world, final ChunkAccess chunk, final AsyncChunkSaveData asyncSaveData, +- final NewChunkHolder toComplete) { +- this.world = world; +- this.chunk = chunk; +- this.asyncSaveData = asyncSaveData; +- this.toComplete = toComplete; +- } +- +- @Override +- public void run() { +- final CompoundTag toSerialize; +- try { +- toSerialize = ChunkSystemFeatures.saveChunkAsync(this.world, this.chunk, this.asyncSaveData); +- } catch (final Throwable throwable) { +- LOGGER.error("Failed to asynchronously save chunk " + this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(this.world) + "', falling back to synchronous save", throwable); +- final ChunkPos pos = this.chunk.getPos(); +- ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkTask(pos.x, pos.z, () -> { +- final CompoundTag synchronousSave; +- try { +- synchronousSave = ChunkSystemFeatures.saveChunkAsync(AsyncChunkSerializeTask.this.world, AsyncChunkSerializeTask.this.chunk, AsyncChunkSerializeTask.this.asyncSaveData); +- } catch (final Throwable throwable2) { +- LOGGER.error("Failed to synchronously save chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(AsyncChunkSerializeTask.this.world) + "', chunk data will be lost", throwable2); +- AsyncChunkSerializeTask.this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null); +- return; +- } +- +- AsyncChunkSerializeTask.this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, synchronousSave); +- LOGGER.info("Successfully serialized chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(AsyncChunkSerializeTask.this.world) + "' synchronously"); +- +- }, PrioritisedExecutor.Priority.HIGHEST); +- return; +- } +- this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, toSerialize); +- } +- +- @Override +- public String toString() { +- return "AsyncChunkSerializeTask{" + +- "chunk={pos=" + this.chunk.getPos() + ",world=\"" + WorldUtil.getWorldName(this.world) + "\"}" + +- "}"; +- } ++ return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI ? ++ new SaveStat( ++ canSaveChunk | executedUnloadTasks[MoonriseRegionFileIO.RegionFileType.CHUNK_DATA.ordinal()], ++ canSaveEntities | executedUnloadTasks[MoonriseRegionFileIO.RegionFileType.ENTITY_DATA.ordinal()], ++ canSavePOI | executedUnloadTasks[MoonriseRegionFileIO.RegionFileType.POI_DATA.ordinal()] ++ ) ++ : null; + } + + private boolean saveChunk(final ChunkAccess chunk, final boolean unloading) { + if (!chunk.isUnsaved()) { + if (unloading) { +- this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null); ++ this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, null); + } + return false; + } +- boolean completing = false; +- boolean failedAsyncPrepare = false; + try { +- if (unloading && ChunkSystemFeatures.supportsAsyncChunkSave()) { +- try { +- final AsyncChunkSaveData asyncSaveData = ChunkSystemFeatures.getAsyncSaveData(this.world, chunk); ++ final SerializableChunkData chunkData = SerializableChunkData.copyOf(this.world, chunk); ++ PlatformHooks.get().chunkSyncSave(this.world, chunk, chunkData); + +- final PrioritisedExecutor.PrioritisedTask task = this.scheduler.loadExecutor.createTask(new AsyncChunkSerializeTask(this.world, chunk, asyncSaveData, this)); ++ chunk.tryMarkSaved(); + +- this.chunkDataUnload.task().setTask(task); ++ final CallbackCompletable<CompoundTag> completable = new CallbackCompletable<>(); + +- chunk.setUnsaved(false); ++ final Runnable run = () -> { ++ final CompoundTag data = chunkData.write(); + +- task.queue(); ++ completable.complete(data); + +- return true; +- } catch (final Throwable thr) { +- LOGGER.error("Failed to prepare async chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', falling back to synchronous save", thr); +- failedAsyncPrepare = true; +- // fall through to synchronous save ++ if (unloading) { ++ NewChunkHolder.this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, data); + } +- } +- +- final CompoundTag save = ChunkSerializer.write(this.world, chunk); ++ }; + ++ final PrioritisedExecutor.PrioritisedTask task; + if (unloading) { +- completing = true; +- this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, save); +- if (failedAsyncPrepare) { +- LOGGER.info("Successfully serialized chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "' synchronously"); +- } ++ this.chunkDataUnload.toRun().setRunnable(run); ++ task = this.chunkDataUnload.task(); + } else { +- RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.CHUNK_DATA); ++ task = this.scheduler.saveExecutor.createTask(run); + } +- chunk.setUnsaved(false); ++ ++ task.queue(); ++ ++ MoonriseRegionFileIO.scheduleSave( ++ this.world, this.chunkX, this.chunkZ, completable, task, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, Priority.NORMAL ++ ); + } catch (final Throwable thr) { + LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr); +- if (unloading && !completing) { +- this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null); +- } + } + + return true; +@@ -1834,7 +1797,7 @@ public final class NewChunkHolder { + return false; + } + try { +- mergeFrom = RegionFileIOThread.loadData(this.world, this.chunkX, this.chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, PrioritisedExecutor.Priority.BLOCKING); ++ mergeFrom = MoonriseRegionFileIO.loadData(this.world, this.chunkX, this.chunkZ, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, Priority.BLOCKING); + } catch (final Exception ex) { + LOGGER.error("Cannot merge transient entities for chunk (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', data on disk will be replaced", ex); + } +@@ -1853,7 +1816,7 @@ public final class NewChunkHolder { + return false; + } + +- RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.ENTITY_DATA); ++ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, save, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA); + this.lastEntitySaveNull = save == null; + if (unloading) { + this.lastEntityUnload = save; +@@ -1877,7 +1840,7 @@ public final class NewChunkHolder { + return false; + } + +- RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.POI_DATA); ++ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, save, MoonriseRegionFileIO.RegionFileType.POI_DATA); + this.lastPoiSaveNull = save == null; + if (unloading) { + this.poiDataUnload.completable().complete(save); +@@ -1924,7 +1887,7 @@ public final class NewChunkHolder { + return element == null ? JsonNull.INSTANCE : new JsonPrimitive(element.toString()); + } + +- private static JsonObject serializeCompletable(final Completable<?> completable) { ++ private static JsonObject serializeCompletable(final CallbackCompletable<?> completable) { + final JsonObject ret = new JsonObject(); + + if (completable == null) { +@@ -2019,13 +1982,13 @@ public final class NewChunkHolder { + ret.add("poi_unload_completable", serializeCompletable(poiDataUnload == null ? null : poiDataUnload.completable())); + ret.add("chunk_unload_completable", serializeCompletable(chunkDataUnload == null ? null : chunkDataUnload.completable())); + +- final DelayedPrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task(); ++ final PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task(); + if (unloadTask == null) { + ret.addProperty("unload_task_priority", "null"); +- ret.addProperty("unload_task_priority_raw", "null"); ++ ret.addProperty("unload_task_suborder", Long.valueOf(0L)); + } else { + ret.addProperty("unload_task_priority", Objects.toString(unloadTask.getPriority())); +- ret.addProperty("unload_task_priority_raw", Integer.valueOf(unloadTask.getPriorityInternal())); ++ ret.addProperty("unload_task_suborder", Long.valueOf(unloadTask.getSubOrder())); + } + + ret.addProperty("killed", Boolean.valueOf(this.unloaded)); +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java +index 261e09454f49d04eb159c984ec695d7c7aa6a3a8..6b468c621b74449a6218391f6477cf63cfc98c7c 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java +@@ -1,7 +1,7 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; + +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; + import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; + import java.lang.invoke.VarHandle; + + public abstract class PriorityHolder { +@@ -28,8 +28,8 @@ public abstract class PriorityHolder { + PRIORITY_HANDLE.set((PriorityHolder)this, (int)val); + } + +- protected PriorityHolder(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ protected PriorityHolder(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.setPriorityPlain(priority.priority); +@@ -69,7 +69,7 @@ public abstract class PriorityHolder { + return; + } + +- this.scheduleTask(PrioritisedExecutor.Priority.getPriority(priority)); ++ this.scheduleTask(Priority.getPriority(priority)); + + int failures = 0; + for (;;) { +@@ -86,7 +86,7 @@ public abstract class PriorityHolder { + return; + } + +- this.setPriorityScheduled(PrioritisedExecutor.Priority.getPriority(priority)); ++ this.setPriorityScheduled(Priority.getPriority(priority)); + + ++failures; + for (int i = 0; i < failures; ++i) { +@@ -95,19 +95,19 @@ public abstract class PriorityHolder { + } + } + +- public final PrioritisedExecutor.Priority getPriority() { ++ public final Priority getPriority() { + final int ret = this.getPriorityVolatile(); + if ((ret & PRIORITY_EXECUTED) != 0) { +- return PrioritisedExecutor.Priority.COMPLETING; ++ return Priority.COMPLETING; + } + if ((ret & PRIORITY_SCHEDULED) != 0) { + return this.getScheduledPriority(); + } +- return PrioritisedExecutor.Priority.getPriority(ret); ++ return Priority.getPriority(ret); + } + +- public final void lowerPriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ public final void lowerPriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + +@@ -139,8 +139,8 @@ public abstract class PriorityHolder { + } + } + +- public final void setPriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ public final void setPriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + +@@ -168,8 +168,8 @@ public abstract class PriorityHolder { + } + } + +- public final void raisePriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ public final void raisePriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + +@@ -203,13 +203,13 @@ public abstract class PriorityHolder { + + protected abstract void cancelScheduled(); + +- protected abstract PrioritisedExecutor.Priority getScheduledPriority(); ++ protected abstract Priority getScheduledPriority(); + +- protected abstract void scheduleTask(final PrioritisedExecutor.Priority priority); ++ protected abstract void scheduleTask(final Priority priority); + +- protected abstract void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority); ++ protected abstract void lowerPriorityScheduled(final Priority priority); + +- protected abstract void setPriorityScheduled(final PrioritisedExecutor.Priority priority); ++ protected abstract void setPriorityScheduled(final Priority priority); + +- protected abstract void raisePriorityScheduled(final PrioritisedExecutor.Priority priority); ++ protected abstract void raisePriorityScheduled(final Priority priority); + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java +index e0b26ccb63596748b80fc6a5e47e373ba811ba8b..5f4b99d8c5453f8ad2e600a57ea4e7dafa2d45f8 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java +@@ -1,10 +1,10 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor; + +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.Priority; + import ca.spottedleaf.moonrise.common.util.CoordinateUtils; + import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; + import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +- + import java.util.ArrayList; + import java.util.Comparator; + import java.util.List; +@@ -16,15 +16,36 @@ public class RadiusAwarePrioritisedExecutor { + return Long.compare(t1.id, t2.id); + }; + +- private final DependencyTree[] queues = new DependencyTree[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES]; ++ private final PrioritisedExecutor executor; ++ private final DependencyTree[] queues = new DependencyTree[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; + private static final int NO_TASKS_QUEUED = -1; + private int selectedQueue = NO_TASKS_QUEUED; + private boolean canQueueTasks = true; + + public RadiusAwarePrioritisedExecutor(final PrioritisedExecutor executor, final int maxToSchedule) { ++ this.executor = executor; ++ + for (int i = 0; i < this.queues.length; ++i) { +- this.queues[i] = new DependencyTree(this, executor, maxToSchedule, i); ++ this.queues[i] = new DependencyTree(this, executor, maxToSchedule); ++ } ++ } ++ ++ public void setMaxToSchedule(final int maxToSchedule) { ++ final List<PrioritisedExecutor.PrioritisedTask> tasks; ++ ++ synchronized (this) { ++ for (final DependencyTree dependencyTree : this.queues) { ++ dependencyTree.maxToSchedule = maxToSchedule; ++ } ++ ++ if (this.selectedQueue == NO_TASKS_QUEUED || !this.canQueueTasks) { ++ return; ++ } ++ ++ tasks = this.queues[this.selectedQueue].tryPushTasks(); + } ++ ++ scheduleTasks(tasks); + } + + private boolean canQueueTasks() { +@@ -56,7 +77,7 @@ public class RadiusAwarePrioritisedExecutor { + return null; + } + +- private List<PrioritisedExecutor.PrioritisedTask> queue(final Task task, final PrioritisedExecutor.Priority priority) { ++ private List<PrioritisedExecutor.PrioritisedTask> queue(final Task task, final Priority priority) { + final int priorityId = priority.priority; + final DependencyTree queue = this.queues[priorityId]; + +@@ -79,7 +100,7 @@ public class RadiusAwarePrioritisedExecutor { + return null; + } + +- if (PrioritisedExecutor.Priority.isHigherPriority(priorityId, this.selectedQueue)) { ++ if (Priority.isHigherPriority(priorityId, this.selectedQueue)) { + // prevent the lower priority tree from queueing more tasks + this.canQueueTasks = false; + return null; +@@ -90,7 +111,7 @@ public class RadiusAwarePrioritisedExecutor { + } + + public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius, +- final Runnable run, final PrioritisedExecutor.Priority priority) { ++ final Runnable run, final Priority priority) { + if (radius < 0) { + throw new IllegalArgumentException("Radius must be > 0: " + radius); + } +@@ -99,11 +120,11 @@ public class RadiusAwarePrioritisedExecutor { + + public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius, + final Runnable run) { +- return this.createTask(chunkX, chunkZ, radius, run, PrioritisedExecutor.Priority.NORMAL); ++ return this.createTask(chunkX, chunkZ, radius, run, Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius, +- final Runnable run, final PrioritisedExecutor.Priority priority) { ++ final Runnable run, final Priority priority) { + final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run, priority); + + ret.queue(); +@@ -120,15 +141,15 @@ public class RadiusAwarePrioritisedExecutor { + return ret; + } + +- public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run, final Priority priority) { + return new Task(this, 0, 0, -1, run, priority); + } + + public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run) { +- return this.createInfiniteRadiusTask(run, PrioritisedExecutor.Priority.NORMAL); ++ return this.createInfiniteRadiusTask(run, Priority.NORMAL); + } + +- public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final Priority priority) { + final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, priority); + + ret.queue(); +@@ -137,20 +158,27 @@ public class RadiusAwarePrioritisedExecutor { + } + + public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run) { +- final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, PrioritisedExecutor.Priority.NORMAL); ++ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, Priority.NORMAL); + + ret.queue(); + + return ret; + } + ++ private static void scheduleTasks(final List<PrioritisedExecutor.PrioritisedTask> toSchedule) { ++ if (toSchedule != null) { ++ for (int i = 0, len = toSchedule.size(); i < len; ++i) { ++ toSchedule.get(i).queue(); ++ } ++ } ++ } ++ + // all accesses must be synchronised by the radius aware object + private static final class DependencyTree { + + private final RadiusAwarePrioritisedExecutor scheduler; + private final PrioritisedExecutor executor; +- private final int maxToSchedule; +- private final int treeIndex; ++ private int maxToSchedule; + + private int currentlyExecuting; + private long idGenerator; +@@ -163,11 +191,10 @@ public class RadiusAwarePrioritisedExecutor { + private final Long2ReferenceOpenHashMap<DependencyNode> nodeByPosition = new Long2ReferenceOpenHashMap<>(); + + public DependencyTree(final RadiusAwarePrioritisedExecutor scheduler, final PrioritisedExecutor executor, +- final int maxToSchedule, final int treeIndex) { ++ final int maxToSchedule) { + this.scheduler = scheduler; + this.executor = executor; + this.maxToSchedule = maxToSchedule; +- this.treeIndex = treeIndex; + } + + public boolean hasWaitingTasks() { +@@ -412,13 +439,13 @@ public class RadiusAwarePrioritisedExecutor { + private final int chunkZ; + private final int radius; + private Runnable run; +- private PrioritisedExecutor.Priority priority; ++ private Priority priority; + + private DependencyNode dependencyNode; + private PrioritisedExecutor.PrioritisedTask queuedTask; + + private Task(final RadiusAwarePrioritisedExecutor scheduler, final int chunkX, final int chunkZ, final int radius, +- final Runnable run, final PrioritisedExecutor.Priority priority) { ++ final Runnable run, final Priority priority) { + this.scheduler = scheduler; + this.chunkX = chunkX; + this.chunkZ = chunkZ; +@@ -441,14 +468,6 @@ public class RadiusAwarePrioritisedExecutor { + run.run(); + } + +- private static void scheduleTasks(final List<PrioritisedExecutor.PrioritisedTask> toSchedule) { +- if (toSchedule != null) { +- for (int i = 0, len = toSchedule.size(); i < len; ++i) { +- toSchedule.get(i).queue(); +- } +- } +- } +- + private void returnNode() { + final List<PrioritisedExecutor.PrioritisedTask> toSchedule; + synchronized (this.scheduler) { +@@ -460,6 +479,11 @@ public class RadiusAwarePrioritisedExecutor { + scheduleTasks(toSchedule); + } + ++ @Override ++ public PrioritisedExecutor getExecutor() { ++ return this.scheduler.executor; ++ } ++ + @Override + public void run() { + final Runnable run = this.run; +@@ -475,7 +499,7 @@ public class RadiusAwarePrioritisedExecutor { + public boolean queue() { + final List<PrioritisedExecutor.PrioritisedTask> toSchedule; + synchronized (this.scheduler) { +- if (this.queuedTask != null || this.dependencyNode != null || this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ if (this.queuedTask != null || this.dependencyNode != null || this.priority == Priority.COMPLETING) { + return false; + } + +@@ -486,16 +510,23 @@ public class RadiusAwarePrioritisedExecutor { + return true; + } + ++ @Override ++ public boolean isQueued() { ++ synchronized (this.scheduler) { ++ return (this.queuedTask != null || this.dependencyNode != null) && this.priority != Priority.COMPLETING; ++ } ++ } ++ + @Override + public boolean cancel() { + final PrioritisedExecutor.PrioritisedTask task; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { +- if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ if (this.priority == Priority.COMPLETING) { + return false; + } + +- this.priority = PrioritisedExecutor.Priority.COMPLETING; ++ this.priority = Priority.COMPLETING; + if (this.dependencyNode != null) { + this.dependencyNode.purged = true; + this.dependencyNode = null; +@@ -519,11 +550,11 @@ public class RadiusAwarePrioritisedExecutor { + final PrioritisedExecutor.PrioritisedTask task; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { +- if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ if (this.priority == Priority.COMPLETING) { + return false; + } + +- this.priority = PrioritisedExecutor.Priority.COMPLETING; ++ this.priority = Priority.COMPLETING; + if (this.dependencyNode != null) { + this.dependencyNode.purged = true; + this.dependencyNode = null; +@@ -543,7 +574,7 @@ public class RadiusAwarePrioritisedExecutor { + } + + @Override +- public PrioritisedExecutor.Priority getPriority() { ++ public Priority getPriority() { + final PrioritisedExecutor.PrioritisedTask task; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { +@@ -555,8 +586,8 @@ public class RadiusAwarePrioritisedExecutor { + } + + @Override +- public boolean setPriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ public boolean setPriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + +@@ -564,7 +595,7 @@ public class RadiusAwarePrioritisedExecutor { + List<PrioritisedExecutor.PrioritisedTask> toSchedule = null; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { +- if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ if (this.priority == Priority.COMPLETING) { + return false; + } + +@@ -592,8 +623,8 @@ public class RadiusAwarePrioritisedExecutor { + } + + @Override +- public boolean raisePriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ public boolean raisePriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + +@@ -601,7 +632,7 @@ public class RadiusAwarePrioritisedExecutor { + List<PrioritisedExecutor.PrioritisedTask> toSchedule = null; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { +- if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ if (this.priority == Priority.COMPLETING) { + return false; + } + +@@ -629,8 +660,8 @@ public class RadiusAwarePrioritisedExecutor { + } + + @Override +- public boolean lowerPriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ public boolean lowerPriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + +@@ -638,7 +669,7 @@ public class RadiusAwarePrioritisedExecutor { + List<PrioritisedExecutor.PrioritisedTask> toSchedule = null; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { +- if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ if (this.priority == Priority.COMPLETING) { + return false; + } + +@@ -664,5 +695,35 @@ public class RadiusAwarePrioritisedExecutor { + + return true; + } ++ ++ @Override ++ public long getSubOrder() { ++ // TODO implement ++ return 0; ++ } ++ ++ @Override ++ public boolean setSubOrder(final long subOrder) { ++ // TODO implement ++ return false; ++ } ++ ++ @Override ++ public boolean raiseSubOrder(final long subOrder) { ++ // TODO implement ++ return false; ++ } ++ ++ @Override ++ public boolean lowerSubOrder(final long subOrder) { ++ // TODO implement ++ return false; ++ } ++ ++ @Override ++ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) { ++ // TODO implement ++ return this.setPriority(priority); ++ } + } +-} +\ No newline at end of file ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java +index fbdf721e8b4cfe6cef4ee60c53c680cbfc858d88..98382575eb6cd48aa163264e4935c812db1f1aff 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java +@@ -1,7 +1,9 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; + import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.PlatformHooks; + import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; + import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk; + import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager; +@@ -29,7 +31,7 @@ public final class ChunkFullTask extends ChunkProgressionTask implements Runnabl + private final PrioritisedExecutor.PrioritisedTask convertToFullTask; + + public ChunkFullTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, +- final NewChunkHolder chunkHolder, final ChunkAccess fromChunk, final PrioritisedExecutor.Priority priority) { ++ final NewChunkHolder chunkHolder, final ChunkAccess fromChunk, final Priority priority) { + super(scheduler, world, chunkX, chunkZ); + this.chunkHolder = chunkHolder; + this.fromChunk = fromChunk; +@@ -43,6 +45,8 @@ public final class ChunkFullTask extends ChunkProgressionTask implements Runnabl + + @Override + public void run() { ++ final PlatformHooks platformHooks = PlatformHooks.get(); ++ + // See Vanilla ChunkPyramid#LOADING_PYRAMID.FULL for what this function should be doing + final LevelChunk chunk; + try { +@@ -61,7 +65,7 @@ public final class ChunkFullTask extends ChunkProgressionTask implements Runnabl + final ServerLevel world = this.world; + final ProtoChunk protoChunk = (ProtoChunk)this.fromChunk; + chunk = new LevelChunk(this.world, protoChunk, (final LevelChunk unused) -> { +- ChunkStatusTasks.postLoadProtoChunk(world, protoChunk.getEntities(), protoChunk.getPos()); // Paper - pass chunk pos ++ ChunkStatusTasks.postLoadProtoChunk(world, protoChunk.getEntities()); + }); + this.chunkHolder.replaceProtoChunk(new ImposterProtoChunk(chunk, false)); + } +@@ -71,16 +75,21 @@ public final class ChunkFullTask extends ChunkProgressionTask implements Runnabl + final NewChunkHolder chunkHolder = this.chunkHolder; + + chunk.setFullStatus(chunkHolder::getChunkStatus); +- chunk.runPostLoad(); +- // Unlike Vanilla, we load the entity chunk here, as we load the NBT in empty status (unlike Vanilla) +- // This brings entity addition back in line with older versions of the game +- // Since we load the NBT in the empty status, this will never block for I/O +- ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getOrCreateEntityChunk(this.chunkX, this.chunkZ, false); +- +- // we don't need the entitiesInLevel, not sure why it's there +- chunk.setLoaded(true); +- chunk.registerAllBlockEntitiesAfterLevelLoad(); +- chunk.registerTickContainerInLevel(this.world); ++ try { ++ platformHooks.setCurrentlyLoading(this.chunkHolder.vanillaChunkHolder, chunk); ++ chunk.runPostLoad(); ++ // Unlike Vanilla, we load the entity chunk here, as we load the NBT in empty status (unlike Vanilla) ++ // This brings entity addition back in line with older versions of the game ++ // Since we load the NBT in the empty status, this will never block for I/O ++ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getOrCreateEntityChunk(this.chunkX, this.chunkZ, false); ++ chunk.setLoaded(true); ++ chunk.registerAllBlockEntitiesAfterLevelLoad(); ++ chunk.registerTickContainerInLevel(this.world); ++ chunk.setUnsavedListener(this.world.getChunkSource().chunkMap.worldGenContext.unsavedListener()); ++ platformHooks.chunkFullStatusComplete(chunk, (ProtoChunk)this.fromChunk); ++ } finally { ++ platformHooks.setCurrentlyLoading(this.chunkHolder.vanillaChunkHolder, null); ++ } + } catch (final Throwable throwable) { + this.complete(null, throwable); + return; +@@ -112,29 +121,29 @@ public final class ChunkFullTask extends ChunkProgressionTask implements Runnabl + } + + @Override +- public PrioritisedExecutor.Priority getPriority() { ++ public Priority getPriority() { + return this.convertToFullTask.getPriority(); + } + + @Override +- public void lowerPriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ public void lowerPriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.convertToFullTask.lowerPriority(priority); + } + + @Override +- public void setPriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ public void setPriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.convertToFullTask.setPriority(priority); + } + + @Override +- public void raisePriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ public void raisePriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.convertToFullTask.raisePriority(priority); +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java +index 7c2e6752228fac175c4aa97fa3d817b8a938922f..4538ccfaea83d217ed85eaf16e82393c7f286489 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java +@@ -1,6 +1,6 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.Priority; + import ca.spottedleaf.moonrise.common.util.WorldUtil; + import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; + import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.PriorityHolder; +@@ -25,9 +25,9 @@ public final class ChunkLightTask extends ChunkProgressionTask { + private final LightTaskPriorityHolder priorityHolder; + + public ChunkLightTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, +- final ChunkAccess chunk, final PrioritisedExecutor.Priority priority) { ++ final ChunkAccess chunk, final Priority priority) { + super(scheduler, world, chunkX, chunkZ); +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.priorityHolder = new LightTaskPriorityHolder(priority, this); +@@ -55,22 +55,22 @@ public final class ChunkLightTask extends ChunkProgressionTask { + } + + @Override +- public PrioritisedExecutor.Priority getPriority() { ++ public Priority getPriority() { + return this.priorityHolder.getPriority(); + } + + @Override +- public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ public void lowerPriority(final Priority priority) { + this.priorityHolder.raisePriority(priority); + } + + @Override +- public void setPriority(final PrioritisedExecutor.Priority priority) { ++ public void setPriority(final Priority priority) { + this.priorityHolder.setPriority(priority); + } + + @Override +- public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ public void raisePriority(final Priority priority) { + this.priorityHolder.raisePriority(priority); + } + +@@ -78,7 +78,7 @@ public final class ChunkLightTask extends ChunkProgressionTask { + + private final ChunkLightTask task; + +- private LightTaskPriorityHolder(final PrioritisedExecutor.Priority priority, final ChunkLightTask task) { ++ private LightTaskPriorityHolder(final Priority priority, final ChunkLightTask task) { + super(priority); + this.task = task; + } +@@ -90,13 +90,13 @@ public final class ChunkLightTask extends ChunkProgressionTask { + } + + @Override +- protected PrioritisedExecutor.Priority getScheduledPriority() { ++ protected Priority getScheduledPriority() { + final ChunkLightTask task = this.task; + return ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine().getServerLightQueue().getPriority(task.chunkX, task.chunkZ); + } + + @Override +- protected void scheduleTask(final PrioritisedExecutor.Priority priority) { ++ protected void scheduleTask(final Priority priority) { + final ChunkLightTask task = this.task; + final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); + final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); +@@ -105,7 +105,7 @@ public final class ChunkLightTask extends ChunkProgressionTask { + } + + @Override +- protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ protected void lowerPriorityScheduled(final Priority priority) { + final ChunkLightTask task = this.task; + final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); + final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); +@@ -113,7 +113,7 @@ public final class ChunkLightTask extends ChunkProgressionTask { + } + + @Override +- protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ protected void setPriorityScheduled(final Priority priority) { + final ChunkLightTask task = this.task; + final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); + final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); +@@ -121,7 +121,7 @@ public final class ChunkLightTask extends ChunkProgressionTask { + } + + @Override +- protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ protected void raisePriorityScheduled(final Priority priority) { + final ChunkLightTask task = this.task; + final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); + final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java +index 1ab93f219246d0b4dcdfd0f685f47c13091425f8..e0a88615a8b6d58191f29b1ff1a26427f0a4c1a6 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java +@@ -1,12 +1,13 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + + import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; + import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; + import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.PlatformHooks; + import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemConverters; +-import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemFeatures; +-import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; + import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; + import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; + import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +@@ -18,7 +19,7 @@ import net.minecraft.world.level.chunk.ChunkAccess; + import net.minecraft.world.level.chunk.ProtoChunk; + import net.minecraft.world.level.chunk.UpgradeData; + import net.minecraft.world.level.chunk.status.ChunkStatus; +-import net.minecraft.world.level.chunk.storage.ChunkSerializer; ++import net.minecraft.world.level.chunk.storage.SerializableChunkData; + import net.minecraft.world.level.levelgen.blending.BlendingData; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; +@@ -41,7 +42,7 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + private final AtomicInteger taskCountToComplete = new AtomicInteger(3); // one for poi, one for entity, and one for chunk data + + public ChunkLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, +- final NewChunkHolder chunkHolder, final PrioritisedExecutor.Priority priority) { ++ final NewChunkHolder chunkHolder, final Priority priority) { + super(scheduler, world, chunkX, chunkZ); + this.chunkHolder = chunkHolder; + this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority); +@@ -170,12 +171,12 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + } + + @Override +- public PrioritisedExecutor.Priority getPriority() { ++ public Priority getPriority() { + return this.loadTask.getPriority(); + } + + @Override +- public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ public void lowerPriority(final Priority priority) { + final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); + if (entityLoad != null) { + entityLoad.lowerPriority(priority); +@@ -191,7 +192,7 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + } + + @Override +- public void setPriority(final PrioritisedExecutor.Priority priority) { ++ public void setPriority(final Priority priority) { + final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); + if (entityLoad != null) { + entityLoad.setPriority(priority); +@@ -207,7 +208,7 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + } + + @Override +- public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ public void raisePriority(final Priority priority) { + final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); + if (entityLoad != null) { + entityLoad.raisePriority(priority); +@@ -231,8 +232,8 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(CallbackDataLoadTask.class, "completed", boolean.class); + + protected CallbackDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, +- final int chunkZ, final RegionFileIOThread.RegionFileType type, +- final PrioritisedExecutor.Priority priority) { ++ final int chunkZ, final MoonriseRegionFileIO.RegionFileType type, ++ final Priority priority) { + super(scheduler, world, chunkX, chunkZ, type, priority); + } + +@@ -272,10 +273,13 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + } + } + +- private static final class ChunkDataLoadTask extends CallbackDataLoadTask<CompoundTag, ChunkAccess> { ++ ++ private static record ReadChunk(ProtoChunk protoChunk, SerializableChunkData chunkData) {} ++ ++ private static final class ChunkDataLoadTask extends CallbackDataLoadTask<ReadChunk, ChunkAccess> { + private ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, +- final int chunkZ, final PrioritisedExecutor.Priority priority) { +- super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.CHUNK_DATA, priority); ++ final int chunkZ, final Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, priority); + } + + @Override +@@ -289,40 +293,42 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + } + + @Override +- protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority) { + return this.scheduler.loadExecutor.createTask(run, priority); + } + + @Override +- protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) { + return this.scheduler.createChunkTask(this.chunkX, this.chunkZ, run, priority); + } + + @Override +- protected TaskResult<ChunkAccess, Throwable> completeOnMainOffMain(final CompoundTag data, final Throwable throwable) { ++ protected TaskResult<ChunkAccess, Throwable> completeOnMainOffMain(final ReadChunk data, final Throwable throwable) { + if (throwable != null) { + return new TaskResult<>(null, throwable); + } +- if (data == null) { ++ ++ if (data == null || data.protoChunk() == null) { + return new TaskResult<>(this.getEmptyChunk(), null); + } + +- if (ChunkSystemFeatures.supportsAsyncChunkDeserialization()) { +- return this.deserialize(data); ++ if (!PlatformHooks.get().hasMainChunkLoadHook()) { ++ return new TaskResult<>(data.protoChunk(), null); + } +- // need to deserialize on main thread ++ ++ // need to invoke the callback for loading on the main thread + return null; + } + + private ProtoChunk getEmptyChunk() { + return new ProtoChunk( + new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, this.world, +- this.world.registryAccess().registryOrThrow(Registries.BIOME), (BlendingData)null ++ this.world.registryAccess().lookupOrThrow(Registries.BIOME), (BlendingData)null + ); + } + + @Override +- protected TaskResult<CompoundTag, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) { ++ protected TaskResult<ReadChunk, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) { + if (throwable != null) { + LOGGER.error("Failed to load chunk data for task: " + this.toString() + ", chunk data will be lost", throwable); + return new TaskResult<>(null, null); +@@ -334,42 +340,43 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + + try { + // run converters +- final CompoundTag converted = this.world.getChunkSource().chunkMap.upgradeChunkTag(data, new net.minecraft.world.level.ChunkPos(this.chunkX, this.chunkZ)); ++ final CompoundTag converted = this.world.getChunkSource().chunkMap.upgradeChunkTag(data); + +- return new TaskResult<>(converted, null); +- } catch (final Throwable thr2) { +- LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2); +- return new TaskResult<>(null, null); +- } +- } ++ // unpack the data ++ final SerializableChunkData chunkData = SerializableChunkData.parse( ++ this.world, this.world.registryAccess(), converted ++ ); + +- private TaskResult<ChunkAccess, Throwable> deserialize(final CompoundTag data) { +- try { +- final ChunkAccess deserialized = ChunkSerializer.read( +- this.world, this.world.getPoiManager(), this.world.getChunkSource().chunkMap.storageInfo(), new ChunkPos(this.chunkX, this.chunkZ), data ++ if (chunkData == null) { ++ LOGGER.error("Deserialized chunk for task: " + this.toString() + " produced null, chunk data will be lost?"); ++ } ++ ++ // read into ProtoChunk ++ final ProtoChunk chunk = chunkData == null ? null : chunkData.read( ++ this.world, this.world.getPoiManager(), this.world.getChunkSource().chunkMap.storageInfo(), ++ new ChunkPos(this.chunkX, this.chunkZ) + ); +- return new TaskResult<>(deserialized, null); ++ ++ return new TaskResult<>(new ReadChunk(chunk, chunkData), null); + } catch (final Throwable thr2) { + LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2); +- return new TaskResult<>(this.getEmptyChunk(), null); ++ return new TaskResult<>(null, null); + } + } + + @Override +- protected TaskResult<ChunkAccess, Throwable> runOnMain(final CompoundTag data, final Throwable throwable) { +- // data != null && throwable == null +- if (ChunkSystemFeatures.supportsAsyncChunkDeserialization()) { +- throw new UnsupportedOperationException(); +- } +- return this.deserialize(data); ++ protected TaskResult<ChunkAccess, Throwable> runOnMain(final ReadChunk data, final Throwable throwable) { ++ PlatformHooks.get().mainChunkLoad(data.protoChunk(), data.chunkData()); ++ ++ return new TaskResult<>(data.protoChunk(), null); + } + } + + public static final class PoiDataLoadTask extends CallbackDataLoadTask<PoiChunk, PoiChunk> { + + public PoiDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, +- final int chunkZ, final PrioritisedExecutor.Priority priority) { +- super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.POI_DATA, priority); ++ final int chunkZ, final Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.POI_DATA, priority); + } + + @Override +@@ -383,12 +390,12 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + } + + @Override +- protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority) { + return this.scheduler.loadExecutor.createTask(run, priority); + } + + @Override +- protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) { + throw new UnsupportedOperationException(); + } + +@@ -430,8 +437,8 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + public static final class EntityDataLoadTask extends CallbackDataLoadTask<CompoundTag, CompoundTag> { + + public EntityDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, +- final int chunkZ, final PrioritisedExecutor.Priority priority) { +- super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, priority); ++ final int chunkZ, final Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, priority); + } + + @Override +@@ -445,12 +452,12 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + } + + @Override +- protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority) { + return this.scheduler.loadExecutor.createTask(run, priority); + } + + @Override +- protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) { + throw new UnsupportedOperationException(); + } + +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java +index 70e900b0f9c131900bf8b3f3ecbfbd5df5361205..002ee365aa70d8e6a6e6bd5c95988bd17db4395a 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java +@@ -1,8 +1,8 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + + import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; + import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; + import ca.spottedleaf.moonrise.common.util.WorldUtil; + import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; + import net.minecraft.server.level.ServerLevel; +@@ -46,15 +46,15 @@ public abstract class ChunkProgressionTask { + /* May be called multiple times */ + public abstract void cancel(); + +- public abstract PrioritisedExecutor.Priority getPriority(); ++ public abstract Priority getPriority(); + + /* Schedule lock is always held for the priority update calls */ + +- public abstract void lowerPriority(final PrioritisedExecutor.Priority priority); ++ public abstract void lowerPriority(final Priority priority); + +- public abstract void setPriority(final PrioritisedExecutor.Priority priority); ++ public abstract void setPriority(final Priority priority); + +- public abstract void raisePriority(final PrioritisedExecutor.Priority priority); ++ public abstract void raisePriority(final Priority priority); + + public final void onComplete(final BiConsumer<ChunkAccess, Throwable> onComplete) { + if (!this.waiters.add(onComplete)) { +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java +index 2c17d5589f15f1155be08be670d29acbe954a8fa..25d8da4773dcee5096053e7e3788bfc224d705a7 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java +@@ -1,7 +1,8 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; + import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; + import ca.spottedleaf.moonrise.common.util.WorldUtil; + import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; + import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +@@ -36,9 +37,9 @@ public final class ChunkUpgradeGenericStatusTask extends ChunkProgressionTask im + + public ChunkUpgradeGenericStatusTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, + final int chunkZ, final ChunkAccess chunk, final StaticCache2D<GenerationChunkHolder> neighbours, +- final ChunkStatus toStatus, final PrioritisedExecutor.Priority priority) { ++ final ChunkStatus toStatus, final Priority priority) { + super(scheduler, world, chunkX, chunkZ); +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.fromChunk = chunk; +@@ -187,29 +188,29 @@ public final class ChunkUpgradeGenericStatusTask extends ChunkProgressionTask im + } + + @Override +- public PrioritisedExecutor.Priority getPriority() { ++ public Priority getPriority() { + return this.generateTask.getPriority(); + } + + @Override +- public void lowerPriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ public void lowerPriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.generateTask.lowerPriority(priority); + } + + @Override +- public void setPriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ public void setPriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.generateTask.setPriority(priority); + } + + @Override +- public void raisePriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ public void raisePriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.generateTask.raisePriority(priority); +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java +index 7a65d351b448873c6f2c145c975c92be314b876c..bdcd1879457bafcca4e76523aac0555968f37c0b 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java +@@ -1,12 +1,13 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + ++import ca.spottedleaf.concurrentutil.completable.CallbackCompletable; + import ca.spottedleaf.concurrentutil.completable.Completable; + import ca.spottedleaf.concurrentutil.executor.Cancellable; +-import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask; +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; + import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; + import ca.spottedleaf.moonrise.common.util.WorldUtil; +-import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; + import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; + import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; + import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +@@ -47,11 +48,11 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + protected final ServerLevel world; + protected final int chunkX; + protected final int chunkZ; +- protected final RegionFileIOThread.RegionFileType type; ++ protected final MoonriseRegionFileIO.RegionFileType type; + + public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, +- final int chunkZ, final RegionFileIOThread.RegionFileType type, +- final PrioritisedExecutor.Priority priority) { ++ final int chunkZ, final MoonriseRegionFileIO.RegionFileType type, ++ final Priority priority) { + this.scheduler = scheduler; + this.world = world; + this.chunkX = chunkX; +@@ -89,9 +90,9 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + + protected abstract boolean hasOnMain(); + +- protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority); ++ protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority); + +- protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority); ++ protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority); + + protected abstract TaskResult<OnMain, Throwable> runOffMain(final CompoundTag data, final Throwable throwable); + +@@ -108,7 +109,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + ", type: " + this.type.toString() + "}"; + } + +- public PrioritisedExecutor.Priority getPriority() { ++ public Priority getPriority() { + if (this.processOnMain != null) { + return this.processOnMain.getPriority(); + } else { +@@ -116,7 +117,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + } + } + +- public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ public void lowerPriority(final Priority priority) { + // can't lower I/O tasks, we don't know what they affect + if (this.processOffMain != null) { + this.processOffMain.lowerPriority(priority); +@@ -126,7 +127,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + } + } + +- public void setPriority(final PrioritisedExecutor.Priority priority) { ++ public void setPriority(final Priority priority) { + // can't lower I/O tasks, we don't know what they affect + this.loadDataFromDiskTask.raisePriority(priority); + if (this.processOffMain != null) { +@@ -137,7 +138,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + } + } + +- public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ public void raisePriority(final Priority priority) { + // can't lower I/O tasks, we don't know what they affect + this.loadDataFromDiskTask.raisePriority(priority); + if (this.processOffMain != null) { +@@ -382,10 +383,10 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + private final int chunkX; + private final int chunkZ; + +- private final RegionFileIOThread.RegionFileType type; ++ private final MoonriseRegionFileIO.RegionFileType type; + private Cancellable dataLoadTask; + private Cancellable dataUnloadCancellable; +- private DelayedPrioritisedTask dataUnloadTask; ++ private PrioritisedExecutor.PrioritisedTask dataUnloadTask; + + private final BiConsumer<CompoundTag, Throwable> onComplete; + private final AtomicBoolean scheduled = new AtomicBoolean(); +@@ -393,10 +394,10 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + // onComplete should be caller sensitive, it may complete synchronously with schedule() - which does + // hold a priority lock. + public LoadDataFromDiskTask(final ServerLevel world, final int chunkX, final int chunkZ, +- final RegionFileIOThread.RegionFileType type, ++ final MoonriseRegionFileIO.RegionFileType type, + final BiConsumer<CompoundTag, Throwable> onComplete, +- final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.world = world; +@@ -426,8 +427,8 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0; + } + +- public void lowerPriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ public void lowerPriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + +@@ -439,7 +440,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + } + + if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { +- RegionFileIOThread.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); ++ MoonriseRegionFileIO.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); + return; + } + +@@ -467,8 +468,8 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + } + } + +- public void setPriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ public void setPriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + +@@ -480,7 +481,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + } + + if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { +- RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); ++ MoonriseRegionFileIO.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); + return; + } + +@@ -504,8 +505,8 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + } + } + +- public void raisePriority(final PrioritisedExecutor.Priority priority) { +- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ public void raisePriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + +@@ -517,7 +518,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + } + + if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { +- RegionFileIOThread.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority); ++ MoonriseRegionFileIO.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority); + return; + } + +@@ -583,7 +584,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + } // else: cancelled + }; + +- final PrioritisedExecutor.Priority initialPriority = PrioritisedExecutor.Priority.getPriority(priority); ++ final Priority initialPriority = Priority.getPriority(priority); + boolean scheduledUnload = false; + + final NewChunkHolder holder = ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ); +@@ -593,13 +594,13 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + consumer.accept(data, null); + } else { + // need to schedule task +- LoadDataFromDiskTask.this.schedule(false, consumer, PrioritisedExecutor.Priority.getPriority(LoadDataFromDiskTask.this.getPriorityVolatile() & ~PRIORITY_FLAGS)); ++ LoadDataFromDiskTask.this.schedule(false, consumer, Priority.getPriority(LoadDataFromDiskTask.this.getPriorityVolatile() & ~PRIORITY_FLAGS)); + } + }; + Cancellable unloadCancellable = null; + CompoundTag syncComplete = null; + final NewChunkHolder.UnloadTask unloadTask = holder.getUnloadTask(this.type); // can be null if no task exists +- final Completable<CompoundTag> unloadCompletable = unloadTask == null ? null : unloadTask.completable(); ++ final CallbackCompletable<CompoundTag> unloadCompletable = unloadTask == null ? null : unloadTask.completable(); + if (unloadCompletable != null) { + unloadCancellable = unloadCompletable.addAsynchronousWaiter(unloadConsumer); + if (unloadCancellable == null) { +@@ -622,7 +623,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + this.schedule(scheduledUnload, consumer, initialPriority); + } + +- private void schedule(final boolean scheduledUnload, final BiConsumer<CompoundTag, Throwable> consumer, final PrioritisedExecutor.Priority initialPriority) { ++ private void schedule(final boolean scheduledUnload, final BiConsumer<CompoundTag, Throwable> consumer, final Priority initialPriority) { + int priority = this.getPriorityVolatile(); + + if ((priority & PRIORITY_EXECUTED) != 0) { +@@ -631,9 +632,9 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + } + + if (!scheduledUnload) { +- this.dataLoadTask = RegionFileIOThread.loadDataAsync( ++ this.dataLoadTask = MoonriseRegionFileIO.loadDataAsync( + this.world, this.chunkX, this.chunkZ, this.type, consumer, +- initialPriority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), initialPriority ++ initialPriority.isHigherPriority(Priority.NORMAL), initialPriority + ); + } + +@@ -657,10 +658,10 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { + + if (scheduledUnload) { + if (this.dataUnloadTask != null) { +- this.dataUnloadTask.setPriority(PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS)); ++ this.dataUnloadTask.setPriority(Priority.getPriority(priority & ~PRIORITY_FLAGS)); + } + } else { +- RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS)); ++ MoonriseRegionFileIO.setPriority(this.world, this.chunkX, this.chunkZ, this.type, Priority.getPriority(priority & ~PRIORITY_FLAGS)); + } + + ++failures; +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..51c126735ace8fdde89ad97b5cab62f244212db0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java +@@ -0,0 +1,12 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.storage; ++ ++import net.minecraft.world.level.chunk.storage.RegionFile; ++import java.io.IOException; ++ ++public interface ChunkSystemChunkBuffer { ++ public boolean moonrise$getWriteOnClose(); ++ ++ public void moonrise$setWriteOnClose(final boolean value); ++ ++ public void moonrise$write(final RegionFile regionFile) throws IOException; ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3bd1b59250dbab15097a64d515999b278636795a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java +@@ -0,0 +1,12 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.storage; ++ ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.world.level.ChunkPos; ++import java.io.IOException; ++ ++public interface ChunkSystemRegionFile { ++ ++ public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(final CompoundTag data, final ChunkPos pos) throws IOException; ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java +index 3a9a564edfdb99e006e4816cb8821bd1e9ecff43..93fd23027c00cef76562098306737272fda1350a 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java +@@ -1,6 +1,7 @@ + package ca.spottedleaf.moonrise.patches.chunk_system.util; + + import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.MoonriseConstants; + import it.unimi.dsi.fastutil.HashCommon; + import it.unimi.dsi.fastutil.longs.LongArrayList; + import it.unimi.dsi.fastutil.longs.LongIterator; +@@ -13,7 +14,7 @@ public final class ParallelSearchRadiusIteration { + + // expected that this list returns for a given radius, the set of chunks ordered + // by manhattan distance +- private static final long[][] SEARCH_RADIUS_ITERATION_LIST = new long[64+2+1][]; ++ private static final long[][] SEARCH_RADIUS_ITERATION_LIST = new long[MoonriseConstants.MAX_VIEW_DISTANCE+2+1][]; + static { + for (int i = 0; i < SEARCH_RADIUS_ITERATION_LIST.length; ++i) { + // a BFS around -x, -z, +x, +z will give increasing manhatten distance +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7ef3dcca89ed7578c6c0f5565131889110063056 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java +@@ -0,0 +1,37 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.util.stream; ++ ++import java.io.DataInputStream; ++import java.io.FilterInputStream; ++import java.io.InputStream; ++import java.lang.reflect.Field; ++ ++/** ++ * Used to mark chunk data streams that are on external files ++ */ ++public class ExternalChunkStreamMarker extends DataInputStream { ++ ++ private static final Field IN_FIELD; ++ static { ++ Field field; ++ try { ++ field = FilterInputStream.class.getDeclaredField("in"); ++ field.setAccessible(true); ++ } catch (final Throwable throwable) { ++ field = null; ++ } ++ ++ IN_FIELD = field; ++ } ++ ++ private static InputStream getWrapped(final FilterInputStream in) { ++ try { ++ return (InputStream)IN_FIELD.get(in); ++ } catch (final Throwable throwable) { ++ return in; ++ } ++ } ++ ++ public ExternalChunkStreamMarker(final DataInputStream in) { ++ super(getWrapped(in)); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +index 748ab4d637ce463272bae4fdbab6842a27385126..3abd4ad6379c383c3a31931255292b42d9435694 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +@@ -1,15 +1,58 @@ + package ca.spottedleaf.moonrise.patches.collisions; + ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter; ++import ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState; ++import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity; ++import ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData; ++import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape; ++import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; ++import ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection; ++import it.unimi.dsi.fastutil.doubles.DoubleArrayList; ++import it.unimi.dsi.fastutil.doubles.DoubleList; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Direction; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.border.WorldBorder; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ChunkSource; ++import net.minecraft.world.level.chunk.LevelChunkSection; ++import net.minecraft.world.level.chunk.PalettedContainer; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.material.FluidState; ++import net.minecraft.world.phys.AABB; ++import net.minecraft.world.phys.Vec3; ++import net.minecraft.world.phys.shapes.ArrayVoxelShape; ++import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape; ++import net.minecraft.world.phys.shapes.BooleanOp; ++import net.minecraft.world.phys.shapes.CollisionContext; ++import net.minecraft.world.phys.shapes.DiscreteVoxelShape; ++import net.minecraft.world.phys.shapes.EntityCollisionContext; ++import net.minecraft.world.phys.shapes.OffsetDoubleList; ++import net.minecraft.world.phys.shapes.Shapes; ++import net.minecraft.world.phys.shapes.SliceShape; ++import net.minecraft.world.phys.shapes.VoxelShape; ++import java.util.Arrays; ++import java.util.List; ++import java.util.Objects; ++import java.util.function.BiPredicate; ++import java.util.function.Predicate; ++ + public final class CollisionUtil { + + public static final double COLLISION_EPSILON = 1.0E-7; +- public static final it.unimi.dsi.fastutil.doubles.DoubleArrayList ZERO_ONE = it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(new double[] { 0.0, 1.0 }); ++ public static final DoubleArrayList ZERO_ONE = DoubleArrayList.wrap(new double[] { 0.0, 1.0 }); + + public static boolean isSpecialCollidingBlock(final net.minecraft.world.level.block.state.BlockBehaviour.BlockStateBase block) { +- return block.hasLargeCollisionShape() || block.getBlock() == net.minecraft.world.level.block.Blocks.MOVING_PISTON; ++ return block.hasLargeCollisionShape() || block.getBlock() == Blocks.MOVING_PISTON; + } + +- public static boolean isEmpty(final net.minecraft.world.phys.AABB aabb) { ++ public static boolean isEmpty(final AABB aabb) { + return (aabb.maxX - aabb.minX) < COLLISION_EPSILON || (aabb.maxY - aabb.minY) < COLLISION_EPSILON || (aabb.maxZ - aabb.minZ) < COLLISION_EPSILON; + } + +@@ -18,11 +61,11 @@ public final class CollisionUtil { + return (maxX - minX) < COLLISION_EPSILON || (maxY - minY) < COLLISION_EPSILON || (maxZ - minZ) < COLLISION_EPSILON; + } + +- public static net.minecraft.world.phys.AABB getBoxForChunk(final int chunkX, final int chunkZ) { ++ public static AABB getBoxForChunk(final int chunkX, final int chunkZ) { + double x = (double)(chunkX << 4); + double z = (double)(chunkZ << 4); + // use a bounding box bigger than the chunk to prevent entities from entering it on move +- return new net.minecraft.world.phys.AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON, ++ return new AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON, + x + (16.0 + 3*COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*COLLISION_EPSILON)); + } + +@@ -43,21 +86,21 @@ public final class CollisionUtil { + (minZ1 - maxZ2) < -COLLISION_EPSILON && (maxZ1 - minZ2) > COLLISION_EPSILON; + } + +- public static boolean voxelShapeIntersect(final net.minecraft.world.phys.AABB box, final double minX, final double minY, final double minZ, ++ public static boolean voxelShapeIntersect(final AABB box, final double minX, final double minY, final double minZ, + final double maxX, final double maxY, final double maxZ) { + return (box.minX - maxX) < -COLLISION_EPSILON && (box.maxX - minX) > COLLISION_EPSILON && + (box.minY - maxY) < -COLLISION_EPSILON && (box.maxY - minY) > COLLISION_EPSILON && + (box.minZ - maxZ) < -COLLISION_EPSILON && (box.maxZ - minZ) > COLLISION_EPSILON; + } + +- public static boolean voxelShapeIntersect(final net.minecraft.world.phys.AABB box1, final net.minecraft.world.phys.AABB box2) { ++ public static boolean voxelShapeIntersect(final AABB box1, final AABB box2) { + return (box1.minX - box2.maxX) < -COLLISION_EPSILON && (box1.maxX - box2.minX) > COLLISION_EPSILON && + (box1.minY - box2.maxY) < -COLLISION_EPSILON && (box1.maxY - box2.minY) > COLLISION_EPSILON && + (box1.minZ - box2.maxZ) < -COLLISION_EPSILON && (box1.maxZ - box2.minZ) > COLLISION_EPSILON; + } + + // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON +- public static double collideX(final net.minecraft.world.phys.AABB target, final net.minecraft.world.phys.AABB source, final double source_move) { ++ public static double collideX(final AABB target, final AABB source, final double source_move) { + if ((source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON && + (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { + if (source_move >= 0.0) { +@@ -78,7 +121,7 @@ public final class CollisionUtil { + } + + // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON +- public static double collideY(final net.minecraft.world.phys.AABB target, final net.minecraft.world.phys.AABB source, final double source_move) { ++ public static double collideY(final AABB target, final AABB source, final double source_move) { + if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && + (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { + if (source_move >= 0.0) { +@@ -99,7 +142,7 @@ public final class CollisionUtil { + } + + // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON +- public static double collideZ(final net.minecraft.world.phys.AABB target, final net.minecraft.world.phys.AABB source, final double source_move) { ++ public static double collideZ(final AABB target, final AABB source, final double source_move) { + if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && + (source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON) { + if (source_move >= 0.0) { +@@ -121,7 +164,8 @@ public final class CollisionUtil { + + // startIndex and endIndex inclusive + // assumes indices are in range of array +- private static int findFloor(final double[] values, final double value, int startIndex, int endIndex) { ++ public static int findFloor(final double[] values, final double value, int startIndex, int endIndex) { ++ Objects.checkFromToIndex(startIndex, endIndex + 1, values.length); + do { + final int middle = (startIndex + endIndex) >>> 1; + final double middleVal = values[middle]; +@@ -136,7 +180,217 @@ public final class CollisionUtil { + return startIndex - 1; + } + +- public static boolean voxelShapeIntersectNoEmpty(final net.minecraft.world.phys.shapes.VoxelShape voxel, final net.minecraft.world.phys.AABB aabb) { ++ private static VoxelShape sliceShapeVanilla(final VoxelShape src, final Direction.Axis axis, ++ final int index) { ++ return new SliceShape(src, axis, index); ++ } ++ ++ private static DoubleList offsetList(final double[] src, final double by) { ++ final DoubleArrayList wrap = DoubleArrayList.wrap(src); ++ if (by == 0.0) { ++ return wrap; ++ } ++ return new OffsetDoubleList(wrap, by); ++ } ++ ++ private static VoxelShape sliceShapeOptimised(final VoxelShape src, final Direction.Axis axis, ++ final int index) { ++ // assume index in range ++ final double off_x = ((CollisionVoxelShape)src).moonrise$offsetX(); ++ final double off_y = ((CollisionVoxelShape)src).moonrise$offsetY(); ++ final double off_z = ((CollisionVoxelShape)src).moonrise$offsetZ(); ++ ++ final double[] coords_x = ((CollisionVoxelShape)src).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((CollisionVoxelShape)src).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((CollisionVoxelShape)src).moonrise$rootCoordinatesZ(); ++ ++ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)src).moonrise$getCachedVoxelData(); ++ ++ // note: size = coords.length - 1 ++ final int size_x = cached_shape_data.sizeX(); ++ final int size_y = cached_shape_data.sizeY(); ++ final int size_z = cached_shape_data.sizeZ(); ++ ++ final long[] bitset = cached_shape_data.voxelSet(); ++ ++ final DoubleList list_x; ++ final DoubleList list_y; ++ final DoubleList list_z; ++ final int shape_sx; ++ final int shape_ex; ++ final int shape_sy; ++ final int shape_ey; ++ final int shape_sz; ++ final int shape_ez; ++ ++ switch (axis) { ++ case X: { ++ // validate index ++ if (index < 0 || index >= size_x) { ++ return Shapes.empty(); ++ } ++ ++ // test if input is already "sliced" ++ if (coords_x.length == 2 && (coords_x[0] + off_x) == 0.0 && (coords_x[1] + off_x) == 1.0) { ++ return src; ++ } ++ ++ // test if result would be full box ++ if (coords_y.length == 2 && coords_z.length == 2 && ++ (coords_y[0] + off_y) == 0.0 && (coords_y[1] + off_y) == 1.0 && ++ (coords_z[0] + off_z) == 0.0 && (coords_z[1] + off_z) == 1.0) { ++ // note: size_y == size_z == 1 ++ final int bitIdx = 0 + 0*size_z + index*(size_z*size_y); ++ return (bitset[bitIdx >>> 6] & (1L << bitIdx)) == 0L ? Shapes.empty() : Shapes.block(); ++ } ++ ++ list_x = ZERO_ONE; ++ list_y = offsetList(coords_y, off_y); ++ list_z = offsetList(coords_z, off_z); ++ shape_sx = index; ++ shape_ex = index + 1; ++ shape_sy = 0; ++ shape_ey = size_y; ++ shape_sz = 0; ++ shape_ez = size_z; ++ ++ break; ++ } ++ case Y: { ++ // validate index ++ if (index < 0 || index >= size_y) { ++ return Shapes.empty(); ++ } ++ ++ // test if input is already "sliced" ++ if (coords_y.length == 2 && (coords_y[0] + off_y) == 0.0 && (coords_y[1] + off_y) == 1.0) { ++ return src; ++ } ++ ++ // test if result would be full box ++ if (coords_x.length == 2 && coords_z.length == 2 && ++ (coords_x[0] + off_x) == 0.0 && (coords_x[1] + off_x) == 1.0 && ++ (coords_z[0] + off_z) == 0.0 && (coords_z[1] + off_z) == 1.0) { ++ // note: size_x == size_z == 1 ++ final int bitIdx = 0 + index*size_z + 0*(size_z*size_y); ++ return (bitset[bitIdx >>> 6] & (1L << bitIdx)) == 0L ? Shapes.empty() : Shapes.block(); ++ } ++ ++ list_x = offsetList(coords_x, off_x); ++ list_y = ZERO_ONE; ++ list_z = offsetList(coords_z, off_z); ++ shape_sx = 0; ++ shape_ex = size_x; ++ shape_sy = index; ++ shape_ey = index + 1; ++ shape_sz = 0; ++ shape_ez = size_z; ++ ++ break; ++ } ++ case Z: { ++ // validate index ++ if (index < 0 || index >= size_z) { ++ return Shapes.empty(); ++ } ++ ++ // test if input is already "sliced" ++ if (coords_z.length == 2 && (coords_z[0] + off_z) == 0.0 && (coords_z[1] + off_z) == 1.0) { ++ return src; ++ } ++ ++ // test if result would be full box ++ if (coords_x.length == 2 && coords_y.length == 2 && ++ (coords_x[0] + off_x) == 0.0 && (coords_x[1] + off_x) == 1.0 && ++ (coords_y[0] + off_y) == 0.0 && (coords_y[1] + off_y) == 1.0) { ++ // note: size_x == size_y == 1 ++ final int bitIdx = index + 0*size_z + 0*(size_z*size_y); ++ return (bitset[bitIdx >>> 6] & (1L << bitIdx)) == 0L ? Shapes.empty() : Shapes.block(); ++ } ++ ++ list_x = offsetList(coords_x, off_x); ++ list_y = offsetList(coords_y, off_y); ++ list_z = ZERO_ONE; ++ shape_sx = 0; ++ shape_ex = size_x; ++ shape_sy = 0; ++ shape_ey = size_y; ++ shape_sz = index; ++ shape_ez = index + 1; ++ ++ break; ++ } ++ default: { ++ throw new IllegalStateException("Unknown axis: " + axis); ++ } ++ } ++ ++ final int local_len_x = shape_ex - shape_sx; ++ final int local_len_y = shape_ey - shape_sy; ++ final int local_len_z = shape_ez - shape_sz; ++ ++ final BitSetDiscreteVoxelShape shape = new BitSetDiscreteVoxelShape(local_len_x, local_len_y, local_len_z); ++ ++ final int bitset_mul_x = size_z*size_y; ++ final int idx_off = shape_sz + shape_sy*size_z + shape_sx*bitset_mul_x; ++ final int shape_mul_x = local_len_y*local_len_z; ++ for (int x = 0; x < local_len_x; ++x) { ++ boolean setX = false; ++ for (int y = 0; y < local_len_y; ++y) { ++ boolean setY = false; ++ for (int z = 0; z < local_len_z; ++z) { ++ final int unslicedIdx = idx_off + z + y*size_z + x*bitset_mul_x; ++ if ((bitset[unslicedIdx >>> 6] & (1L << unslicedIdx)) == 0L) { ++ continue; ++ } ++ ++ setY = true; ++ setX = true; ++ shape.zMin = Math.min(shape.zMin, z); ++ shape.zMax = Math.max(shape.zMax, z + 1); ++ ++ shape.storage.set( ++ z + y*local_len_z + x*shape_mul_x ++ ); ++ } ++ ++ if (setY) { ++ shape.yMin = Math.min(shape.yMin, y); ++ shape.yMax = Math.max(shape.yMax, y + 1); ++ } ++ } ++ if (setX) { ++ shape.xMin = Math.min(shape.xMin, x); ++ shape.xMax = Math.max(shape.xMax, x + 1); ++ } ++ } ++ ++ return shape.isEmpty() ? Shapes.empty() : new ArrayVoxelShape( ++ shape, list_x, list_y, list_z ++ ); ++ } ++ ++ private static final boolean DEBUG_SLICE_SHAPE = false; ++ ++ public static VoxelShape sliceShape(final VoxelShape src, final Direction.Axis axis, ++ final int index) { ++ final VoxelShape ret = sliceShapeOptimised(src, axis, index); ++ if (DEBUG_SLICE_SHAPE) { ++ final VoxelShape vanilla = sliceShapeVanilla(src, axis, index); ++ if (!equals(ret, vanilla)) { ++ // special case: SliceShape is not empty when it should be! ++ if (areAnyFull(ret.shape) || areAnyFull(vanilla.shape)) { ++ equals(ret, vanilla); ++ sliceShapeOptimised(src, axis, index); ++ throw new IllegalStateException("Slice shape mismatch"); ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public static boolean voxelShapeIntersectNoEmpty(final VoxelShape voxel, final AABB aabb) { + if (voxel.isEmpty()) { + return false; + } +@@ -144,15 +398,15 @@ public final class CollisionUtil { + // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true + + // offsets that should be applied to coords +- final double off_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetX(); +- final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetY(); +- final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetZ(); ++ final double off_x = ((CollisionVoxelShape)voxel).moonrise$offsetX(); ++ final double off_y = ((CollisionVoxelShape)voxel).moonrise$offsetY(); ++ final double off_z = ((CollisionVoxelShape)voxel).moonrise$offsetZ(); + +- final double[] coords_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesX(); +- final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesY(); +- final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ(); ++ final double[] coords_x = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ(); + +- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getCachedVoxelData(); ++ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)voxel).moonrise$getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); +@@ -246,23 +500,23 @@ public final class CollisionUtil { + } + + // assume !target.isEmpty() && abs(source_move) >= COLLISION_EPSILON +- public static double collideX(final net.minecraft.world.phys.shapes.VoxelShape target, final net.minecraft.world.phys.AABB source, final double source_move) { +- final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation(); ++ public static double collideX(final VoxelShape target, final AABB source, final double source_move) { ++ final AABB single_aabb = ((CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation(); + if (single_aabb != null) { + return collideX(single_aabb, source, source_move); + } + // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true + + // offsets that should be applied to coords +- final double off_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetX(); +- final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetY(); +- final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetZ(); ++ final double off_x = ((CollisionVoxelShape)target).moonrise$offsetX(); ++ final double off_y = ((CollisionVoxelShape)target).moonrise$offsetY(); ++ final double off_z = ((CollisionVoxelShape)target).moonrise$offsetZ(); + +- final double[] coords_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesX(); +- final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesY(); +- final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesZ(); ++ final double[] coords_x = ((CollisionVoxelShape)target).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((CollisionVoxelShape)target).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((CollisionVoxelShape)target).moonrise$rootCoordinatesZ(); + +- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getCachedVoxelData(); ++ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).moonrise$getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); +@@ -404,23 +658,23 @@ public final class CollisionUtil { + } + } + +- public static double collideY(final net.minecraft.world.phys.shapes.VoxelShape target, final net.minecraft.world.phys.AABB source, final double source_move) { +- final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation(); ++ public static double collideY(final VoxelShape target, final AABB source, final double source_move) { ++ final AABB single_aabb = ((CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation(); + if (single_aabb != null) { + return collideY(single_aabb, source, source_move); + } + // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true + + // offsets that should be applied to coords +- final double off_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetX(); +- final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetY(); +- final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetZ(); ++ final double off_x = ((CollisionVoxelShape)target).moonrise$offsetX(); ++ final double off_y = ((CollisionVoxelShape)target).moonrise$offsetY(); ++ final double off_z = ((CollisionVoxelShape)target).moonrise$offsetZ(); + +- final double[] coords_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesX(); +- final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesY(); +- final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesZ(); ++ final double[] coords_x = ((CollisionVoxelShape)target).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((CollisionVoxelShape)target).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((CollisionVoxelShape)target).moonrise$rootCoordinatesZ(); + +- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getCachedVoxelData(); ++ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).moonrise$getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); +@@ -562,23 +816,23 @@ public final class CollisionUtil { + } + } + +- public static double collideZ(final net.minecraft.world.phys.shapes.VoxelShape target, final net.minecraft.world.phys.AABB source, final double source_move) { +- final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation(); ++ public static double collideZ(final VoxelShape target, final AABB source, final double source_move) { ++ final AABB single_aabb = ((CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation(); + if (single_aabb != null) { + return collideZ(single_aabb, source, source_move); + } + // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true + + // offsets that should be applied to coords +- final double off_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetX(); +- final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetY(); +- final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetZ(); ++ final double off_x = ((CollisionVoxelShape)target).moonrise$offsetX(); ++ final double off_y = ((CollisionVoxelShape)target).moonrise$offsetY(); ++ final double off_z = ((CollisionVoxelShape)target).moonrise$offsetZ(); + +- final double[] coords_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesX(); +- final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesY(); +- final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesZ(); ++ final double[] coords_x = ((CollisionVoxelShape)target).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((CollisionVoxelShape)target).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((CollisionVoxelShape)target).moonrise$rootCoordinatesZ(); + +- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getCachedVoxelData(); ++ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).moonrise$getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); +@@ -721,13 +975,13 @@ public final class CollisionUtil { + } + + // does not use epsilon +- public static boolean strictlyContains(final net.minecraft.world.phys.shapes.VoxelShape voxel, final net.minecraft.world.phys.Vec3 point) { ++ public static boolean strictlyContains(final VoxelShape voxel, final Vec3 point) { + return strictlyContains(voxel, point.x, point.y, point.z); + } + + // does not use epsilon +- public static boolean strictlyContains(final net.minecraft.world.phys.shapes.VoxelShape voxel, double x, double y, double z) { +- final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation(); ++ public static boolean strictlyContains(final VoxelShape voxel, double x, double y, double z) { ++ final AABB single_aabb = ((CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation(); + if (single_aabb != null) { + return single_aabb.contains(x, y, z); + } +@@ -738,15 +992,15 @@ public final class CollisionUtil { + } + + // offset input +- x -= ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetX(); +- y -= ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetY(); +- z -= ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetZ(); ++ x -= ((CollisionVoxelShape)voxel).moonrise$offsetX(); ++ y -= ((CollisionVoxelShape)voxel).moonrise$offsetY(); ++ z -= ((CollisionVoxelShape)voxel).moonrise$offsetZ(); + +- final double[] coords_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesX(); +- final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesY(); +- final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ(); ++ final double[] coords_x = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ(); + +- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getCachedVoxelData(); ++ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)voxel).moonrise$getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); +@@ -788,10 +1042,10 @@ public final class CollisionUtil { + return ((ft ? 1 : 0) << 1) | ((tf ? 1 : 0) << 2) | ((tt ? 1 : 0) << 3); + } + +- private static net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape merge(final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst, final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond, +- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX, final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY, +- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ, +- final int booleanOp) { ++ private static BitSetDiscreteVoxelShape merge(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond, ++ final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY, ++ final MergedVoxelCoordinateList mergedZ, ++ final int booleanOp) { + final int sizeX = mergedX.voxels; + final int sizeY = mergedY.voxels; + final int sizeZ = mergedZ.voxels; +@@ -806,7 +1060,7 @@ public final class CollisionUtil { + final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY(); + + // note: indices may contain -1, but nothing > size +- final net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape ret = new net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape(sizeX, sizeY, sizeZ); ++ final BitSetDiscreteVoxelShape ret = new BitSetDiscreteVoxelShape(sizeX, sizeY, sizeZ); + + boolean empty = true; + +@@ -823,10 +1077,11 @@ public final class CollisionUtil { + final int s1z = mergedZ.firstIndices[idxZ]; + final int s2z = mergedZ.secondIndices[idxZ]; + +- int idx; ++ int idx1; ++ int idx2; + +- final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L); +- final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L); ++ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx1 = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx1) & 1L); ++ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx2 = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx2) & 1L); + + // idx ff -> 0 + // idx ft -> 1 +@@ -861,9 +1116,9 @@ public final class CollisionUtil { + return empty ? null : ret; + } + +- private static boolean isMergeEmpty(final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst, final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond, +- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX, final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY, +- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ, ++ private static boolean isMergeEmpty(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond, ++ final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY, ++ final MergedVoxelCoordinateList mergedZ, + final int booleanOp) { + final int sizeX = mergedX.voxels; + final int sizeY = mergedY.voxels; +@@ -889,10 +1144,11 @@ public final class CollisionUtil { + final int s1z = mergedZ.firstIndices[idxZ]; + final int s2z = mergedZ.secondIndices[idxZ]; + +- int idx; ++ int idx1; ++ int idx2; + +- final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L); +- final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L); ++ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx1 = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx1) & 1L); ++ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx2 = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx2) & 1L); + + // idx ff -> 0 + // idx ft -> 1 +@@ -911,11 +1167,11 @@ public final class CollisionUtil { + return true; + } + +- public static net.minecraft.world.phys.shapes.VoxelShape joinOptimized(final net.minecraft.world.phys.shapes.VoxelShape first, final net.minecraft.world.phys.shapes.VoxelShape second, final net.minecraft.world.phys.shapes.BooleanOp operator) { ++ public static VoxelShape joinOptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { + return joinUnoptimized(first, second, operator).optimize(); + } + +- public static net.minecraft.world.phys.shapes.VoxelShape joinUnoptimized(final net.minecraft.world.phys.shapes.VoxelShape first, final net.minecraft.world.phys.shapes.VoxelShape second, final net.minecraft.world.phys.shapes.BooleanOp operator) { ++ public static VoxelShape joinUnoptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { + final boolean ff = operator.apply(false, false); + if (ff) { + // technically, should be an infinite box but that's clearly an error +@@ -925,23 +1181,23 @@ public final class CollisionUtil { + final boolean tt = operator.apply(true, true); + + if (first == second) { +- return tt ? first : net.minecraft.world.phys.shapes.Shapes.empty(); ++ return tt ? first : Shapes.empty(); + } + + final boolean ft = operator.apply(false, true); + final boolean tf = operator.apply(true, false); + + if (first.isEmpty()) { +- return ft ? second : net.minecraft.world.phys.shapes.Shapes.empty(); ++ return ft ? second : Shapes.empty(); + } + if (second.isEmpty()) { +- return tf ? first : net.minecraft.world.phys.shapes.Shapes.empty(); ++ return tf ? first : Shapes.empty(); + } + + if (!tt) { + // try to check for no intersection, since tt = false +- final net.minecraft.world.phys.AABB aabbF = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation(); +- final net.minecraft.world.phys.AABB aabbS = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation(); ++ final AABB aabbF = ((CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation(); ++ final AABB aabbS = ((CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation(); + + final boolean intersect; + +@@ -962,7 +1218,7 @@ public final class CollisionUtil { + + if (!intersect) { + if (!tf & !ft) { +- return net.minecraft.world.phys.shapes.Shapes.empty(); ++ return Shapes.empty(); + } + if (!tf | !ft) { + return tf ? first : second; +@@ -970,50 +1226,50 @@ public final class CollisionUtil { + } + } + +- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge( +- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetX(), +- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetX(), ++ final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge( ++ ((CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)first).moonrise$offsetX(), ++ ((CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)second).moonrise$offsetX(), + ft, tf + ); +- if (mergedX == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) { +- return net.minecraft.world.phys.shapes.Shapes.empty(); ++ if (mergedX == null) { ++ return Shapes.empty(); + } +- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge( +- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetY(), +- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetY(), ++ final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge( ++ ((CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)first).moonrise$offsetY(), ++ ((CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)second).moonrise$offsetY(), + ft, tf + ); +- if (mergedY == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) { +- return net.minecraft.world.phys.shapes.Shapes.empty(); ++ if (mergedY == null) { ++ return Shapes.empty(); + } +- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge( +- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetZ(), +- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetZ(), ++ final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge( ++ ((CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)first).moonrise$offsetZ(), ++ ((CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)second).moonrise$offsetZ(), + ft, tf + ); +- if (mergedZ == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) { +- return net.minecraft.world.phys.shapes.Shapes.empty(); ++ if (mergedZ == null) { ++ return Shapes.empty(); + } + +- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getCachedVoxelData(); +- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getCachedVoxelData(); ++ final CachedShapeData shapeDataFirst = ((CollisionVoxelShape)first).moonrise$getCachedVoxelData(); ++ final CachedShapeData shapeDataSecond = ((CollisionVoxelShape)second).moonrise$getCachedVoxelData(); + +- final net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape mergedShape = merge( ++ final BitSetDiscreteVoxelShape mergedShape = merge( + shapeDataFirst, shapeDataSecond, + mergedX, mergedY, mergedZ, + makeBitset(ft, tf, tt) + ); + + if (mergedShape == null) { +- return net.minecraft.world.phys.shapes.Shapes.empty(); ++ return Shapes.empty(); + } + +- return new net.minecraft.world.phys.shapes.ArrayVoxelShape( ++ return new ArrayVoxelShape( + mergedShape, mergedX.wrapCoords(), mergedY.wrapCoords(), mergedZ.wrapCoords() + ); + } + +- public static boolean isJoinNonEmpty(final net.minecraft.world.phys.shapes.VoxelShape first, final net.minecraft.world.phys.shapes.VoxelShape second, final net.minecraft.world.phys.shapes.BooleanOp operator) { ++ public static boolean isJoinNonEmpty(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { + final boolean ff = operator.apply(false, false); + if (ff) { + // technically, should be an infinite box but that's clearly an error +@@ -1035,8 +1291,8 @@ public final class CollisionUtil { + final boolean tf = operator.apply(true, false); + + // try to check intersection +- final net.minecraft.world.phys.AABB aabbF = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation(); +- final net.minecraft.world.phys.AABB aabbS = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation(); ++ final AABB aabbF = ((CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation(); ++ final AABB aabbS = ((CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation(); + + final boolean intersect; + +@@ -1068,33 +1324,33 @@ public final class CollisionUtil { + } + } + +- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge( +- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetX(), +- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetX(), ++ final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge( ++ ((CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)first).moonrise$offsetX(), ++ ((CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)second).moonrise$offsetX(), + ft, tf + ); +- if (mergedX == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) { ++ if (mergedX == null) { + return false; + } +- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge( +- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetY(), +- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetY(), ++ final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge( ++ ((CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)first).moonrise$offsetY(), ++ ((CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)second).moonrise$offsetY(), + ft, tf + ); +- if (mergedY == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) { ++ if (mergedY == null) { + return false; + } +- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge( +- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetZ(), +- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetZ(), ++ final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge( ++ ((CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)first).moonrise$offsetZ(), ++ ((CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)second).moonrise$offsetZ(), + ft, tf + ); +- if (mergedZ == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) { ++ if (mergedZ == null) { + return false; + } + +- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getCachedVoxelData(); +- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getCachedVoxelData(); ++ final CachedShapeData shapeDataFirst = ((CollisionVoxelShape)first).moonrise$getCachedVoxelData(); ++ final CachedShapeData shapeDataSecond = ((CollisionVoxelShape)second).moonrise$getCachedVoxelData(); + + return !isMergeEmpty( + shapeDataFirst, shapeDataSecond, +@@ -1112,10 +1368,6 @@ public final class CollisionUtil { + } + } + +- private static final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList EMPTY = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList( +- new double[] { 0.0 }, 0.0, new int[0], new int[0], 0 +- ); +- + private static int[] getIndices(final int length) { + final int[] ret = new int[length]; + +@@ -1142,25 +1394,25 @@ public final class CollisionUtil { + this.voxels = voxels; + } + +- public it.unimi.dsi.fastutil.doubles.DoubleList wrapCoords() { ++ public DoubleList wrapCoords() { + if (this.coordinateOffset == 0.0) { +- return it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(this.coordinates, this.voxels + 1); ++ return DoubleArrayList.wrap(this.coordinates, this.voxels + 1); + } +- return new net.minecraft.world.phys.shapes.OffsetDoubleList(it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(this.coordinates, this.voxels + 1), this.coordinateOffset); ++ return new OffsetDoubleList(DoubleArrayList.wrap(this.coordinates, this.voxels + 1), this.coordinateOffset); + } + + // assume coordinates.length > 1 +- public static ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList getForSingle(final double[] coordinates, final double offset) { ++ public static MergedVoxelCoordinateList getForSingle(final double[] coordinates, final double offset) { + final int voxels = coordinates.length - 1; + final int[] indices = voxels < SIMPLE_INDICES_CACHE.length ? SIMPLE_INDICES_CACHE[voxels] : getIndices(voxels); + +- return new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList(coordinates, offset, indices, indices, voxels); ++ return new MergedVoxelCoordinateList(coordinates, offset, indices, indices, voxels); + } + + // assume coordinates.length > 1 +- public static ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList merge(final double[] firstCoordinates, final double firstOffset, +- final double[] secondCoordinates, final double secondOffset, +- final boolean ft, final boolean tf) { ++ public static MergedVoxelCoordinateList merge(final double[] firstCoordinates, final double firstOffset, ++ final double[] secondCoordinates, final double secondOffset, ++ final boolean ft, final boolean tf) { + if (firstCoordinates == secondCoordinates && firstOffset == secondOffset) { + return getForSingle(firstCoordinates, firstOffset); + } +@@ -1250,13 +1502,13 @@ public final class CollisionUtil { + } + } + +- return resultSize <= 1 ? EMPTY : new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1); ++ return resultSize <= 1 ? null : new MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1); + } + } + +- public static boolean equals(final net.minecraft.world.phys.shapes.DiscreteVoxelShape shape1, final net.minecraft.world.phys.shapes.DiscreteVoxelShape shape2) { +- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cachedShapeData1 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape)shape1).moonrise$getOrCreateCachedShapeData(); +- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cachedShapeData2 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape)shape2).moonrise$getOrCreateCachedShapeData(); ++ public static boolean equals(final DiscreteVoxelShape shape1, final DiscreteVoxelShape shape2) { ++ final CachedShapeData cachedShapeData1 = ((CollisionDiscreteVoxelShape)shape1).moonrise$getOrCreateCachedShapeData(); ++ final CachedShapeData cachedShapeData2 = ((CollisionDiscreteVoxelShape)shape2).moonrise$getOrCreateCachedShapeData(); + + final boolean isEmpty1 = cachedShapeData1.isEmpty(); + final boolean isEmpty2 = cachedShapeData2.isEmpty(); +@@ -1265,7 +1517,7 @@ public final class CollisionUtil { + return true; + } else if (isEmpty1 ^ isEmpty2) { + return false; +- } ++ } // else: isEmpty1 = isEmpty2 = false + + if (cachedShapeData1.hasSingleAABB() != cachedShapeData2.hasSingleAABB()) { + return false; +@@ -1281,153 +1533,237 @@ public final class CollisionUtil { + return false; + } + +- return java.util.Arrays.equals(cachedShapeData1.voxelSet(), cachedShapeData2.voxelSet()); ++ return Arrays.equals(cachedShapeData1.voxelSet(), cachedShapeData2.voxelSet()); + } + + // useful only for testing +- public static boolean equals(final net.minecraft.world.phys.shapes.VoxelShape shape1, final net.minecraft.world.phys.shapes.VoxelShape shape2) { ++ public static boolean equals(final VoxelShape shape1, final VoxelShape shape2) { ++ if (shape1.isEmpty() & shape2.isEmpty()) { ++ return true; ++ } else if (shape1.isEmpty() ^ shape2.isEmpty()) { ++ return false; ++ } ++ + if (!equals(shape1.shape, shape2.shape)) { + return false; + } + +- return shape1.getCoords(net.minecraft.core.Direction.Axis.X).equals(shape2.getCoords(net.minecraft.core.Direction.Axis.X)) && +- shape1.getCoords(net.minecraft.core.Direction.Axis.Y).equals(shape2.getCoords(net.minecraft.core.Direction.Axis.Y)) && +- shape1.getCoords(net.minecraft.core.Direction.Axis.Z).equals(shape2.getCoords(net.minecraft.core.Direction.Axis.Z)); ++ return shape1.getCoords(Direction.Axis.X).equals(shape2.getCoords(Direction.Axis.X)) && ++ shape1.getCoords(Direction.Axis.Y).equals(shape2.getCoords(Direction.Axis.Y)) && ++ shape1.getCoords(Direction.Axis.Z).equals(shape2.getCoords(Direction.Axis.Z)); ++ } ++ ++ public static boolean areAnyFull(final DiscreteVoxelShape shape) { ++ if (shape.isEmpty()) { ++ return false; ++ } ++ ++ final int sizeX = shape.getXSize(); ++ final int sizeY = shape.getYSize(); ++ final int sizeZ = shape.getZSize(); ++ ++ for (int x = 0; x < sizeX; ++x) { ++ for (int y = 0; y < sizeY; ++y) { ++ for (int z = 0; z < sizeZ; ++z) { ++ if (shape.isFull(x, y, z)) { ++ return true; ++ } ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ public static String shapeMismatch(final DiscreteVoxelShape shape1, final DiscreteVoxelShape shape2) { ++ final CachedShapeData cachedShapeData1 = ((CollisionDiscreteVoxelShape)shape1).moonrise$getOrCreateCachedShapeData(); ++ final CachedShapeData cachedShapeData2 = ((CollisionDiscreteVoxelShape)shape2).moonrise$getOrCreateCachedShapeData(); ++ ++ final boolean isEmpty1 = cachedShapeData1.isEmpty(); ++ final boolean isEmpty2 = cachedShapeData2.isEmpty(); ++ ++ if (isEmpty1 & isEmpty2) { ++ return null; ++ } else if (isEmpty1 ^ isEmpty2) { ++ return null; ++ } // else: isEmpty1 = isEmpty2 = false ++ ++ if (cachedShapeData1.sizeX() != cachedShapeData2.sizeX()) { ++ return "size x: " + cachedShapeData1.sizeX() + " != " + cachedShapeData2.sizeX(); ++ } ++ if (cachedShapeData1.sizeY() != cachedShapeData2.sizeY()) { ++ return "size y: " + cachedShapeData1.sizeY() + " != " + cachedShapeData2.sizeY(); ++ } ++ if (cachedShapeData1.sizeZ() != cachedShapeData2.sizeZ()) { ++ return "size z: " + cachedShapeData1.sizeZ() + " != " + cachedShapeData2.sizeZ(); ++ } ++ ++ final StringBuilder ret = new StringBuilder(); ++ ++ final int sizeX = cachedShapeData1.sizeX();; ++ final int sizeY = cachedShapeData1.sizeY(); ++ final int sizeZ = cachedShapeData1.sizeZ(); ++ ++ boolean first = true; ++ ++ for (int x = 0; x < sizeX; ++x) { ++ for (int y = 0; y < sizeY; ++y) { ++ for (int z = 0; z < sizeZ; ++z) { ++ final boolean isFull1 = shape1.isFull(x, y, z); ++ final boolean isFull2 = shape2.isFull(x, y, z); ++ ++ if (isFull1 == isFull2) { ++ continue; ++ } ++ ++ if (first) { ++ first = false; ++ } else { ++ ret.append(", "); ++ } ++ ++ ret.append("(").append(x).append(",").append(y).append(",").append(z) ++ .append("): shape1: ").append(isFull1).append(", shape2: ").append(isFull2); ++ } ++ } ++ } ++ ++ return ret.isEmpty() ? null : ret.toString(); + } + +- public static net.minecraft.world.phys.AABB offsetX(final net.minecraft.world.phys.AABB box, final double dx) { +- return new net.minecraft.world.phys.AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); ++ public static AABB offsetX(final AABB box, final double dx) { ++ return new AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); + } + +- public static net.minecraft.world.phys.AABB offsetY(final net.minecraft.world.phys.AABB box, final double dy) { +- return new net.minecraft.world.phys.AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ); ++ public static AABB offsetY(final AABB box, final double dy) { ++ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ); + } + +- public static net.minecraft.world.phys.AABB offsetZ(final net.minecraft.world.phys.AABB box, final double dz) { +- return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz); ++ public static AABB offsetZ(final AABB box, final double dz) { ++ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz); + } + +- public static net.minecraft.world.phys.AABB expandRight(final net.minecraft.world.phys.AABB box, final double dx) { // dx > 0.0 +- return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); ++ public static AABB expandRight(final AABB box, final double dx) { // dx > 0.0 ++ return new AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); + } + +- public static net.minecraft.world.phys.AABB expandLeft(final net.minecraft.world.phys.AABB box, final double dx) { // dx < 0.0 +- return new net.minecraft.world.phys.AABB(box.minX - dx, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ); ++ public static AABB expandLeft(final AABB box, final double dx) { // dx < 0.0 ++ return new AABB(box.minX - dx, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ); + } + +- public static net.minecraft.world.phys.AABB expandUpwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy > 0.0 +- return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, box.maxZ); ++ public static AABB expandUpwards(final AABB box, final double dy) { // dy > 0.0 ++ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, box.maxZ); + } + +- public static net.minecraft.world.phys.AABB expandDownwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy < 0.0 +- return new net.minecraft.world.phys.AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, box.maxZ); ++ public static AABB expandDownwards(final AABB box, final double dy) { // dy < 0.0 ++ return new AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, box.maxZ); + } + +- public static net.minecraft.world.phys.AABB expandForwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz > 0.0 +- return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz); ++ public static AABB expandForwards(final AABB box, final double dz) { // dz > 0.0 ++ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz); + } + +- public static net.minecraft.world.phys.AABB expandBackwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz < 0.0 +- return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ); ++ public static AABB expandBackwards(final AABB box, final double dz) { // dz < 0.0 ++ return new AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ); + } + +- public static net.minecraft.world.phys.AABB cutRight(final net.minecraft.world.phys.AABB box, final double dx) { // dx > 0.0 +- return new net.minecraft.world.phys.AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); ++ public static AABB cutRight(final AABB box, final double dx) { // dx > 0.0 ++ return new AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); + } + +- public static net.minecraft.world.phys.AABB cutLeft(final net.minecraft.world.phys.AABB box, final double dx) { // dx < 0.0 +- return new net.minecraft.world.phys.AABB(box.minX + dx, box.minY, box.minZ, box.minX, box.maxY, box.maxZ); ++ public static AABB cutLeft(final AABB box, final double dx) { // dx < 0.0 ++ return new AABB(box.minX + dx, box.minY, box.minZ, box.minX, box.maxY, box.maxZ); + } + +- public static net.minecraft.world.phys.AABB cutUpwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy > 0.0 +- return new net.minecraft.world.phys.AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, box.maxZ); ++ public static AABB cutUpwards(final AABB box, final double dy) { // dy > 0.0 ++ return new AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, box.maxZ); + } + +- public static net.minecraft.world.phys.AABB cutDownwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy < 0.0 +- return new net.minecraft.world.phys.AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, box.maxZ); ++ public static AABB cutDownwards(final AABB box, final double dy) { // dy < 0.0 ++ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, box.maxZ); + } + +- public static net.minecraft.world.phys.AABB cutForwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz > 0.0 +- return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz); ++ public static AABB cutForwards(final AABB box, final double dz) { // dz > 0.0 ++ return new AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz); + } + +- public static net.minecraft.world.phys.AABB cutBackwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz < 0.0 +- return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ); ++ public static AABB cutBackwards(final AABB box, final double dz) { // dz < 0.0 ++ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ); + } + +- public static double performAABBCollisionsX(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) { ++ public static double performAABBCollisionsX(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + if (Math.abs(value) < COLLISION_EPSILON) { + return 0.0; + } +- final net.minecraft.world.phys.AABB target = potentialCollisions.get(i); ++ final AABB target = potentialCollisions.get(i); + value = collideX(target, currentBoundingBox, value); + } + +- return value; ++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value; + } + +- public static double performAABBCollisionsY(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) { ++ public static double performAABBCollisionsY(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + if (Math.abs(value) < COLLISION_EPSILON) { + return 0.0; + } +- final net.minecraft.world.phys.AABB target = potentialCollisions.get(i); ++ final AABB target = potentialCollisions.get(i); + value = collideY(target, currentBoundingBox, value); + } + +- return value; ++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value; + } + +- public static double performAABBCollisionsZ(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) { ++ public static double performAABBCollisionsZ(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + if (Math.abs(value) < COLLISION_EPSILON) { + return 0.0; + } +- final net.minecraft.world.phys.AABB target = potentialCollisions.get(i); ++ final AABB target = potentialCollisions.get(i); + value = collideZ(target, currentBoundingBox, value); + } + +- return value; ++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value; + } + +- public static double performVoxelCollisionsX(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) { ++ public static double performVoxelCollisionsX(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + if (Math.abs(value) < COLLISION_EPSILON) { + return 0.0; + } +- final net.minecraft.world.phys.shapes.VoxelShape target = potentialCollisions.get(i); ++ final VoxelShape target = potentialCollisions.get(i); + value = collideX(target, currentBoundingBox, value); + } + +- return value; ++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value; + } + +- public static double performVoxelCollisionsY(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) { ++ public static double performVoxelCollisionsY(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + if (Math.abs(value) < COLLISION_EPSILON) { + return 0.0; + } +- final net.minecraft.world.phys.shapes.VoxelShape target = potentialCollisions.get(i); ++ final VoxelShape target = potentialCollisions.get(i); + value = collideY(target, currentBoundingBox, value); + } + +- return value; ++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value; + } + +- public static double performVoxelCollisionsZ(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) { ++ public static double performVoxelCollisionsZ(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + if (Math.abs(value) < COLLISION_EPSILON) { + return 0.0; + } +- final net.minecraft.world.phys.shapes.VoxelShape target = potentialCollisions.get(i); ++ final VoxelShape target = potentialCollisions.get(i); + value = collideZ(target, currentBoundingBox, value); + } + +- return value; ++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value; + } + +- public static net.minecraft.world.phys.Vec3 performVoxelCollisions(final net.minecraft.world.phys.Vec3 moveVector, net.minecraft.world.phys.AABB axisalignedbb, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) { ++ public static Vec3 performVoxelCollisions(final Vec3 moveVector, AABB axisalignedbb, final List<VoxelShape> potentialCollisions) { + double x = moveVector.x; + double y = moveVector.y; + double z = moveVector.z; +@@ -1459,10 +1795,10 @@ public final class CollisionUtil { + z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions); + } + +- return new net.minecraft.world.phys.Vec3(x, y, z); ++ return new Vec3(x, y, z); + } + +- public static net.minecraft.world.phys.Vec3 performAABBCollisions(final net.minecraft.world.phys.Vec3 moveVector, net.minecraft.world.phys.AABB axisalignedbb, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) { ++ public static Vec3 performAABBCollisions(final Vec3 moveVector, AABB axisalignedbb, final List<AABB> potentialCollisions) { + double x = moveVector.x; + double y = moveVector.y; + double z = moveVector.z; +@@ -1494,12 +1830,12 @@ public final class CollisionUtil { + z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions); + } + +- return new net.minecraft.world.phys.Vec3(x, y, z); ++ return new Vec3(x, y, z); + } + +- public static net.minecraft.world.phys.Vec3 performCollisions(final net.minecraft.world.phys.Vec3 moveVector, net.minecraft.world.phys.AABB axisalignedbb, +- final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> voxels, +- final java.util.List<net.minecraft.world.phys.AABB> aabbs) { ++ public static Vec3 performCollisions(final Vec3 moveVector, AABB axisalignedbb, ++ final List<VoxelShape> voxels, ++ final List<AABB> aabbs) { + if (voxels.isEmpty()) { + // fast track only AABBs + return performAABBCollisions(moveVector, axisalignedbb, aabbs); +@@ -1540,14 +1876,14 @@ public final class CollisionUtil { + z = performVoxelCollisionsZ(axisalignedbb, z, voxels); + } + +- return new net.minecraft.world.phys.Vec3(x, y, z); ++ return new Vec3(x, y, z); + } + +- public static boolean isCollidingWithBorder(final net.minecraft.world.level.border.WorldBorder worldborder, final net.minecraft.world.phys.AABB boundingBox) { ++ public static boolean isCollidingWithBorder(final WorldBorder worldborder, final AABB boundingBox) { + return isCollidingWithBorder(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); + } + +- public static boolean isCollidingWithBorder(final net.minecraft.world.level.border.WorldBorder worldborder, ++ public static boolean isCollidingWithBorder(final WorldBorder worldborder, + final double boxMinX, final double boxMaxX, + final double boxMinZ, final double boxMaxZ) { + final double borderMinX = Math.floor(worldborder.getMinX()); // -X +@@ -1557,8 +1893,8 @@ public final class CollisionUtil { + final double borderMaxZ = Math.ceil(worldborder.getMaxZ()); // +Z + + // inverted check for world border enclosing the specified box expanded by -EPSILON +- return (borderMinX - boxMinX) > ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON || (borderMaxX - boxMaxX) < -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON || +- (borderMinZ - boxMinZ) > ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON || (borderMaxZ - boxMaxZ) < -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON; ++ return (borderMinX - boxMinX) > CollisionUtil.COLLISION_EPSILON || (borderMaxX - boxMaxX) < -CollisionUtil.COLLISION_EPSILON || ++ (borderMinZ - boxMinZ) > CollisionUtil.COLLISION_EPSILON || (borderMaxZ - boxMaxZ) < -CollisionUtil.COLLISION_EPSILON; + } + + /* Math.max/min specify that any NaN argument results in a NaN return, unlike these functions */ +@@ -1575,38 +1911,38 @@ public final class CollisionUtil { + public static final int COLLISION_FLAG_CHECK_BORDER = 1 << 2; + public static final int COLLISION_FLAG_CHECK_ONLY = 1 << 3; + +- public static boolean getCollisionsForBlocksOrWorldBorder(final net.minecraft.world.level.Level world, final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB aabb, +- final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> intoVoxel, final java.util.List<net.minecraft.world.phys.AABB> intoAABB, +- final int collisionFlags, final java.util.function.BiPredicate<net.minecraft.world.level.block.state.BlockState, net.minecraft.core.BlockPos> predicate) { ++ public static boolean getCollisionsForBlocksOrWorldBorder(final Level world, final Entity entity, final AABB aabb, ++ final List<VoxelShape> intoVoxel, final List<AABB> intoAABB, ++ final int collisionFlags, final BiPredicate<BlockState, BlockPos> predicate) { + final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; + boolean ret = false; + + if ((collisionFlags & COLLISION_FLAG_CHECK_BORDER) != 0) { +- final net.minecraft.world.level.border.WorldBorder worldBorder = world.getWorldBorder(); +- if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isCollidingWithBorder(worldBorder, aabb) && entity != null && worldBorder.isInsideCloseToBorder(entity, aabb)) { ++ final WorldBorder worldBorder = world.getWorldBorder(); ++ if (CollisionUtil.isCollidingWithBorder(worldBorder, aabb) && entity != null && worldBorder.isInsideCloseToBorder(entity, aabb)) { + if (checkOnly) { + return true; + } else { +- final net.minecraft.world.phys.shapes.VoxelShape borderShape = worldBorder.getCollisionShape(); ++ final VoxelShape borderShape = worldBorder.getCollisionShape(); + intoVoxel.add(borderShape); + ret = true; + } + } + } + +- final int minSection = ((ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel)world).moonrise$getMinSection(); ++ final int minSection = WorldUtil.getMinSection(world); + +- final int minBlockX = net.minecraft.util.Mth.floor(aabb.minX - COLLISION_EPSILON) - 1; +- final int maxBlockX = net.minecraft.util.Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1; ++ final int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1; ++ final int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1; + +- final int minBlockY = Math.max((minSection << 4) - 1, net.minecraft.util.Mth.floor(aabb.minY - COLLISION_EPSILON) - 1); +- final int maxBlockY = Math.min((((ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel)world).moonrise$getMaxSection() << 4) + 16, net.minecraft.util.Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1); ++ final int minBlockY = Math.max((minSection << 4) - 1, Mth.floor(aabb.minY - COLLISION_EPSILON) - 1); ++ final int maxBlockY = Math.min((WorldUtil.getMaxSection(world) << 4) + 16, Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1); + +- final int minBlockZ = net.minecraft.util.Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1; +- final int maxBlockZ = net.minecraft.util.Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1; ++ final int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1; ++ final int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1; + +- final net.minecraft.core.BlockPos.MutableBlockPos mutablePos = new net.minecraft.core.BlockPos.MutableBlockPos(); +- final net.minecraft.world.phys.shapes.CollisionContext collisionShape = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(entity); ++ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); ++ final CollisionContext collisionShape = new LazyEntityCollisionContext(entity); + + // special cases: + if (minBlockY > maxBlockY) { +@@ -1624,11 +1960,11 @@ public final class CollisionUtil { + final int maxChunkZ = maxBlockZ >> 4; + + final boolean loadChunks = (collisionFlags & COLLISION_FLAG_LOAD_CHUNKS) != 0; +- final net.minecraft.world.level.chunk.ChunkSource chunkSource = world.getChunkSource(); ++ final ChunkSource chunkSource = world.getChunkSource(); + + for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { + for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { +- final net.minecraft.world.level.chunk.ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, loadChunks); ++ final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks); + + if (chunk == null) { + if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) { +@@ -1642,7 +1978,7 @@ public final class CollisionUtil { + continue; + } + +- final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections(); ++ final LevelChunkSection[] sections = chunk.getSections(); + + // bound y + for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { +@@ -1650,16 +1986,16 @@ public final class CollisionUtil { + if (sectionIdx < 0 || sectionIdx >= sections.length) { + continue; + } +- final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx]; +- if (section == null || section.hasOnlyAir()) { ++ final LevelChunkSection section = sections[sectionIdx]; ++ if (section.hasOnlyAir()) { + // empty + continue; + } + +- final boolean hasSpecial = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getSpecialCollidingBlocks() != 0; ++ final boolean hasSpecial = ((BlockCountingChunkSection)section).moonrise$hasSpecialCollidingBlocks(); + final int sectionAdjust = !hasSpecial ? 1 : 0; + +- final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> blocks = section.states; ++ final PalettedContainer<BlockState> blocks = section.states; + + final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0; + final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15; +@@ -1683,21 +2019,21 @@ public final class CollisionUtil { + continue; + } + +- final net.minecraft.world.level.block.state.BlockState blockData = blocks.get(localBlockIndex); ++ final BlockState blockData = blocks.get(localBlockIndex); + +- if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$emptyCollisionShape()) { ++ if (((CollisionBlockState)blockData).moonrise$emptyContextCollisionShape()) { + continue; + } + +- net.minecraft.world.phys.shapes.VoxelShape blockCollision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$getConstantCollisionShape(); ++ VoxelShape blockCollision = ((CollisionBlockState)blockData).moonrise$getConstantContextCollisionShape(); + +- if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == net.minecraft.world.level.block.Blocks.MOVING_PISTON))) { ++ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) { + if (blockCollision == null) { + mutablePos.set(blockX, blockY, blockZ); + blockCollision = blockData.getCollisionShape(world, mutablePos, collisionShape); + } + +- net.minecraft.world.phys.AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation(); ++ AABB singleAABB = ((CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation(); + if (singleAABB != null) { + singleAABB = singleAABB.move((double)blockX, (double)blockY, (double)blockZ); + if (!voxelShapeIntersect(aabb, singleAABB)) { +@@ -1724,7 +2060,7 @@ public final class CollisionUtil { + continue; + } + +- final net.minecraft.world.phys.shapes.VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ); ++ final VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ); + + if (!voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) { + continue; +@@ -1755,8 +2091,8 @@ public final class CollisionUtil { + return ret; + } + +- public static boolean getEntityHardCollisions(final net.minecraft.world.level.Level world, final net.minecraft.world.entity.Entity entity, net.minecraft.world.phys.AABB aabb, +- final java.util.List<net.minecraft.world.phys.AABB> into, final int collisionFlags, final java.util.function.Predicate<net.minecraft.world.entity.Entity> predicate) { ++ public static boolean getEntityHardCollisions(final Level world, final Entity entity, AABB aabb, ++ final List<AABB> into, final int collisionFlags, final Predicate<Entity> predicate) { + final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; + + boolean ret = false; +@@ -1765,15 +2101,15 @@ public final class CollisionUtil { + // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems + // specifically with boat collisions. + aabb = aabb.inflate(-COLLISION_EPSILON, -COLLISION_EPSILON, -COLLISION_EPSILON); +- final java.util.List<net.minecraft.world.entity.Entity> entities; +- if (entity != null && ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$isHardColliding()) { ++ final List<Entity> entities; ++ if (entity != null && ((ChunkSystemEntity)entity).moonrise$isHardColliding()) { + entities = world.getEntities(entity, aabb, predicate); + } else { +- entities = ((ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter)world).moonrise$getHardCollidingEntities(entity, aabb, predicate); ++ entities = ((ChunkSystemEntityGetter)world).moonrise$getHardCollidingEntities(entity, aabb, predicate); + } + + for (int i = 0, len = entities.size(); i < len; ++i) { +- final net.minecraft.world.entity.Entity otherEntity = entities.get(i); ++ final Entity otherEntity = entities.get(i); + + if (otherEntity.isSpectator()) { + continue; +@@ -1792,10 +2128,10 @@ public final class CollisionUtil { + return ret; + } + +- public static boolean getCollisions(final net.minecraft.world.level.Level world, final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB aabb, +- final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> intoVoxel, final java.util.List<net.minecraft.world.phys.AABB> intoAABB, final int collisionFlags, +- final java.util.function.BiPredicate<net.minecraft.world.level.block.state.BlockState, net.minecraft.core.BlockPos> blockPredicate, +- final java.util.function.Predicate<net.minecraft.world.entity.Entity> entityPredicate) { ++ public static boolean getCollisions(final Level world, final Entity entity, final AABB aabb, ++ final List<VoxelShape> intoVoxel, final List<AABB> intoAABB, final int collisionFlags, ++ final BiPredicate<BlockState, BlockPos> blockPredicate, ++ final Predicate<Entity> entityPredicate) { + if ((collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0) { + return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate) + || getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate); +@@ -1805,12 +2141,12 @@ public final class CollisionUtil { + } + } + +- public static final class LazyEntityCollisionContext extends net.minecraft.world.phys.shapes.EntityCollisionContext { ++ public static final class LazyEntityCollisionContext extends EntityCollisionContext { + +- private net.minecraft.world.phys.shapes.CollisionContext delegate; ++ private CollisionContext delegate; + private boolean delegated; + +- public LazyEntityCollisionContext(final net.minecraft.world.entity.Entity entity) { ++ public LazyEntityCollisionContext(final Entity entity) { + super(false, 0.0, null, null, entity); + } + +@@ -1820,10 +2156,10 @@ public final class CollisionUtil { + return delegated; + } + +- public net.minecraft.world.phys.shapes.CollisionContext getDelegate() { ++ public CollisionContext getDelegate() { + this.delegated = true; +- final net.minecraft.world.entity.Entity entity = this.getEntity(); +- return this.delegate == null ? this.delegate = (entity == null ? net.minecraft.world.phys.shapes.CollisionContext.empty() : net.minecraft.world.phys.shapes.CollisionContext.of(entity)) : this.delegate; ++ final Entity entity = this.getEntity(); ++ return this.delegate == null ? this.delegate = (entity == null ? CollisionContext.empty() : CollisionContext.of(entity)) : this.delegate; + } + + @Override +@@ -1832,17 +2168,17 @@ public final class CollisionUtil { + } + + @Override +- public boolean isAbove(final net.minecraft.world.phys.shapes.VoxelShape shape, final net.minecraft.core.BlockPos pos, final boolean defaultValue) { ++ public boolean isAbove(final VoxelShape shape, final BlockPos pos, final boolean defaultValue) { + return this.getDelegate().isAbove(shape, pos, defaultValue); + } + + @Override +- public boolean isHoldingItem(final net.minecraft.world.item.Item item) { ++ public boolean isHoldingItem(final Item item) { + return this.getDelegate().isHoldingItem(item); + } + + @Override +- public boolean canStandOnFluid(final net.minecraft.world.level.material.FluidState state, final net.minecraft.world.level.material.FluidState fluidState) { ++ public boolean canStandOnFluid(final FluidState state, final FluidState fluidState) { + return this.getDelegate().canStandOnFluid(state, fluidState); + } + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java +index eb7200657d5c7ac37ee93868ba43be0aefecac6d..35c8aaf0bfa42717f45eed1d1072e1614874de91 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java +@@ -1,18 +1,23 @@ + package ca.spottedleaf.moonrise.patches.collisions; + ++import net.minecraft.core.BlockPos; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.material.FluidState; ++import net.minecraft.world.phys.shapes.VoxelShape; ++ + public final class ExplosionBlockCache { + + public final long key; +- public final net.minecraft.core.BlockPos immutablePos; +- public final net.minecraft.world.level.block.state.BlockState blockState; +- public final net.minecraft.world.level.material.FluidState fluidState; ++ public final BlockPos immutablePos; ++ public final BlockState blockState; ++ public final FluidState fluidState; + public final float resistance; + public final boolean outOfWorld; + public Boolean shouldExplode; // null -> not called yet +- public net.minecraft.world.phys.shapes.VoxelShape cachedCollisionShape; ++ public VoxelShape cachedCollisionShape; + +- public ExplosionBlockCache(final long key, final net.minecraft.core.BlockPos immutablePos, final net.minecraft.world.level.block.state.BlockState blockState, +- final net.minecraft.world.level.material.FluidState fluidState, final float resistance, final boolean outOfWorld) { ++ public ExplosionBlockCache(final long key, final BlockPos immutablePos, final BlockState blockState, ++ final FluidState fluidState, final float resistance, final boolean outOfWorld) { + this.key = key; + this.immutablePos = immutablePos; + this.blockState = blockState; +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java +index 02b29c563a298e06186de010de68a716bccba494..a38ab583200ebf68ca68fdddf2d12077720b72b7 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java +@@ -1,5 +1,7 @@ + package ca.spottedleaf.moonrise.patches.collisions.block; + ++import net.minecraft.world.phys.shapes.VoxelShape; ++ + public interface CollisionBlockState { + + // note: this does not consider canOcclude, it is only based on the cached collision shape (i.e hasCache()) +@@ -9,6 +11,9 @@ public interface CollisionBlockState { + // whether the cached collision shape exists and is empty + public boolean moonrise$emptyCollisionShape(); + ++ // whether the context-sensitive shape is constant and is empty ++ public boolean moonrise$emptyContextCollisionShape(); ++ + // indicates that occludesFullBlock is cached for the collision shape + public boolean moonrise$hasCache(); + +@@ -20,7 +25,5 @@ public interface CollisionBlockState { + // value is still unique + public int moonrise$uniqueId2(); + +- public net.minecraft.world.phys.shapes.VoxelShape moonrise$getConstantCollisionShape(); +- +- public net.minecraft.world.phys.AABB moonrise$getConstantCollisionAABB(); ++ public VoxelShape moonrise$getConstantContextCollisionShape(); + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java +index 5fe1dad9dad368911aedbe6ba7fcd8f9b0189d32..9d33ead3a97d86b371e4d9ad9fed80d789bed844 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java +@@ -1,27 +1,31 @@ + package ca.spottedleaf.moonrise.patches.collisions.shape; + ++import net.minecraft.world.phys.AABB; ++import java.util.ArrayList; ++import java.util.List; ++ + public record CachedToAABBs( +- java.util.List<net.minecraft.world.phys.AABB> aabbs, ++ List<AABB> aabbs, + boolean isOffset, + double offX, double offY, double offZ + ) { + +- public ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs removeOffset() { +- final java.util.List<net.minecraft.world.phys.AABB> toOffset = this.aabbs; ++ public CachedToAABBs removeOffset() { ++ final List<AABB> toOffset = this.aabbs; + final double offX = this.offX; + final double offY = this.offY; + final double offZ = this.offZ; + +- final java.util.List<net.minecraft.world.phys.AABB> ret = new java.util.ArrayList<>(toOffset.size()); ++ final List<AABB> ret = new ArrayList<>(toOffset.size()); + + for (int i = 0, len = toOffset.size(); i < len; ++i) { + ret.add(toOffset.get(i).move(offX, offY, offZ)); + } + +- return new ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs(ret, false, 0.0, 0.0, 0.0); ++ return new CachedToAABBs(ret, false, 0.0, 0.0, 0.0); + } + +- public static ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs offset(final ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cache, final double offX, final double offY, final double offZ) { ++ public static CachedToAABBs offset(final CachedToAABBs cache, final double offX, final double offY, final double offZ) { + if (offX == 0.0 && offY == 0.0 && offZ == 0.0) { + return cache; + } +@@ -30,6 +34,6 @@ public record CachedToAABBs( + final double resY = cache.offY + offY; + final double resZ = cache.offZ + offZ; + +- return new ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs(cache.aabbs, true, resX, resY, resZ); ++ return new CachedToAABBs(cache.aabbs, true, resX, resY, resZ); + } + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java +index a09efadea9b733840bbe69830dd8f2a303fe656f..07fe5e02c2d0a27d2fe37bb45761654dc2d02e5d 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java +@@ -2,6 +2,6 @@ package ca.spottedleaf.moonrise.patches.collisions.shape; + + public interface CollisionDiscreteVoxelShape { + +- public ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getOrCreateCachedShapeData(); ++ public CachedShapeData moonrise$getOrCreateCachedShapeData(); + + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java +index 70371eb87c11a106e8513cdbc8d938dda088f745..05d7b3f9d8659c259f3ed0537c57e6e43eb6e288 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java +@@ -1,5 +1,9 @@ + package ca.spottedleaf.moonrise.patches.collisions.shape; + ++import net.minecraft.core.Direction; ++import net.minecraft.world.phys.AABB; ++import net.minecraft.world.phys.shapes.VoxelShape; ++ + public interface CollisionVoxelShape { + + public double moonrise$offsetX(); +@@ -14,16 +18,16 @@ public interface CollisionVoxelShape { + + public double[] moonrise$rootCoordinatesZ(); + +- public ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getCachedVoxelData(); ++ public CachedShapeData moonrise$getCachedVoxelData(); + + // rets null if not possible to represent this shape as one AABB +- public net.minecraft.world.phys.AABB moonrise$getSingleAABBRepresentation(); ++ public AABB moonrise$getSingleAABBRepresentation(); + + // ONLY USE INTERNALLY, ONLY FOR INITIALISING IN CONSTRUCTOR: VOXELSHAPES ARE STATIC + public void moonrise$initCache(); + + // this returns empty if not clamped to 1.0 or 0.0 depending on direction +- public net.minecraft.world.phys.shapes.VoxelShape moonrise$getFaceShapeClamped(final net.minecraft.core.Direction direction); ++ public VoxelShape moonrise$getFaceShapeClamped(final Direction direction); + + public boolean moonrise$isFullBlock(); + +@@ -32,5 +36,5 @@ public interface CollisionVoxelShape { + public boolean moonrise$occludesFullBlockIfCached(); + + // uses a cache internally +- public net.minecraft.world.phys.shapes.VoxelShape moonrise$orUnoptimized(final net.minecraft.world.phys.shapes.VoxelShape other); ++ public VoxelShape moonrise$orUnoptimized(final VoxelShape other); + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java +index 4217426d3eca5e5cd2bc37e509f84da1d6fed0b2..44831fc18efb7534dc6e4822f3c9b5cdc4dcc33e 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java +@@ -1,8 +1,10 @@ + package ca.spottedleaf.moonrise.patches.collisions.shape; + ++import net.minecraft.world.phys.shapes.VoxelShape; ++ + public record MergedORCache( +- net.minecraft.world.phys.shapes.VoxelShape key, +- net.minecraft.world.phys.shapes.VoxelShape result ++ VoxelShape key, ++ VoxelShape result + ) { + + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java +deleted file mode 100644 +index 673103f160cbe577c6e05f998706af4e6850011b..0000000000000000000000000000000000000000 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java ++++ /dev/null +@@ -1,225 +0,0 @@ +-package ca.spottedleaf.moonrise.patches.collisions.util; +- +-import java.util.Iterator; +-import java.util.Optional; +-import java.util.Spliterator; +-import java.util.stream.Stream; +- +-public final class EmptyStreamForMoveCall<T> implements java.util.stream.Stream<T> { +- +- public static final ca.spottedleaf.moonrise.patches.collisions.util.EmptyStreamForMoveCall INSTANCE = new ca.spottedleaf.moonrise.patches.collisions.util.EmptyStreamForMoveCall(); +- +- @Override +- public boolean noneMatch(java.util.function.Predicate<? super T> predicate) { +- return false; // important: ret false so the branch is never taken by mojang code +- } +- +- @Override +- public java.util.stream.Stream<T> filter(java.util.function.Predicate<? super T> predicate) { +- return null; +- } +- +- @Override +- public <R> java.util.stream.Stream<R> map(java.util.function.Function<? super T, ? extends R> mapper) { +- return null; +- } +- +- @Override +- public java.util.stream.IntStream mapToInt(java.util.function.ToIntFunction<? super T> mapper) { +- return null; +- } +- +- @Override +- public java.util.stream.LongStream mapToLong(java.util.function.ToLongFunction<? super T> mapper) { +- return null; +- } +- +- @Override +- public java.util.stream.DoubleStream mapToDouble(java.util.function.ToDoubleFunction<? super T> mapper) { +- return null; +- } +- +- @Override +- public <R> java.util.stream.Stream<R> flatMap(java.util.function.Function<? super T, ? extends java.util.stream.Stream<? extends R>> mapper) { +- return null; +- } +- +- @Override +- public java.util.stream.IntStream flatMapToInt(java.util.function.Function<? super T, ? extends java.util.stream.IntStream> mapper) { +- return null; +- } +- +- @Override +- public java.util.stream.LongStream flatMapToLong(java.util.function.Function<? super T, ? extends java.util.stream.LongStream> mapper) { +- return null; +- } +- +- @Override +- public java.util.stream.DoubleStream flatMapToDouble(java.util.function.Function<? super T, ? extends java.util.stream.DoubleStream> mapper) { +- return null; +- } +- +- @Override +- public java.util.stream.Stream<T> distinct() { +- return null; +- } +- +- @Override +- public java.util.stream.Stream<T> sorted() { +- return null; +- } +- +- @Override +- public java.util.stream.Stream<T> sorted(java.util.Comparator<? super T> comparator) { +- return null; +- } +- +- @Override +- public java.util.stream.Stream<T> peek(java.util.function.Consumer<? super T> action) { +- return null; +- } +- +- @Override +- public java.util.stream.Stream<T> limit(long maxSize) { +- return null; +- } +- +- @Override +- public java.util.stream.Stream<T> skip(long n) { +- return null; +- } +- +- @Override +- public void forEach(java.util.function.Consumer<? super T> action) { +- +- } +- +- @Override +- public void forEachOrdered(java.util.function.Consumer<? super T> action) { +- +- } +- +- @org.jetbrains.annotations.NotNull +- @Override +- public Object[] toArray() { +- return new Object[0]; +- } +- +- @org.jetbrains.annotations.NotNull +- @Override +- public <A> A[] toArray(java.util.function.IntFunction<A[]> generator) { +- return null; +- } +- +- @Override +- public T reduce(T identity, java.util.function.BinaryOperator<T> accumulator) { +- return null; +- } +- +- @org.jetbrains.annotations.NotNull +- @Override +- public Optional<T> reduce(java.util.function.BinaryOperator<T> accumulator) { +- return java.util.Optional.empty(); +- } +- +- @Override +- public <U> U reduce(U identity, java.util.function.BiFunction<U, ? super T, U> accumulator, java.util.function.BinaryOperator<U> combiner) { +- return null; +- } +- +- @Override +- public <R> R collect(java.util.function.Supplier<R> supplier, java.util.function.BiConsumer<R, ? super T> accumulator, java.util.function.BiConsumer<R, R> combiner) { +- return null; +- } +- +- @Override +- public <R, A> R collect(java.util.stream.Collector<? super T, A, R> collector) { +- return null; +- } +- +- @org.jetbrains.annotations.NotNull +- @Override +- public Optional<T> min(java.util.Comparator<? super T> comparator) { +- return java.util.Optional.empty(); +- } +- +- @org.jetbrains.annotations.NotNull +- @Override +- public Optional<T> max(java.util.Comparator<? super T> comparator) { +- return java.util.Optional.empty(); +- } +- +- @Override +- public long count() { +- return 0; +- } +- +- @Override +- public boolean anyMatch(java.util.function.Predicate<? super T> predicate) { +- return false; +- } +- +- @Override +- public boolean allMatch(java.util.function.Predicate<? super T> predicate) { +- return false; +- } +- +- @org.jetbrains.annotations.NotNull +- @Override +- public Optional<T> findFirst() { +- return java.util.Optional.empty(); +- } +- +- @org.jetbrains.annotations.NotNull +- @Override +- public Optional<T> findAny() { +- return java.util.Optional.empty(); +- } +- +- +- @org.jetbrains.annotations.NotNull +- @Override +- public Iterator<T> iterator() { +- return null; +- } +- +- @org.jetbrains.annotations.NotNull +- @Override +- public Spliterator<T> spliterator() { +- return null; +- } +- +- @Override +- public boolean isParallel() { +- return false; +- } +- +- @org.jetbrains.annotations.NotNull +- @Override +- public Stream<T> sequential() { +- return null; +- } +- +- @org.jetbrains.annotations.NotNull +- @Override +- public Stream<T> parallel() { +- return null; +- } +- +- @org.jetbrains.annotations.NotNull +- @Override +- public Stream<T> unordered() { +- return null; +- } +- +- @org.jetbrains.annotations.NotNull +- @Override +- public Stream<T> onClose(Runnable closeHandler) { +- return null; +- } +- +- @Override +- public void close() { +- +- } +-} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java +index 128267ff40b38c7b3ea0feb5133825cc6aae075b..cf9ffdeff6bf0b62a45f7a44dbfe0dd7d17dc4f4 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java +@@ -1,4 +1,7 @@ + package ca.spottedleaf.moonrise.patches.collisions.util; + +-public record FluidOcclusionCacheKey(net.minecraft.world.level.block.state.BlockState first, net.minecraft.world.level.block.state.BlockState second, net.minecraft.core.Direction direction, boolean result) { ++import net.minecraft.core.Direction; ++import net.minecraft.world.level.block.state.BlockState; ++ ++public record FluidOcclusionCacheKey(BlockState first, BlockState second, Direction direction, boolean result) { + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java +deleted file mode 100644 +index e851e81e13edbad6316df63fcb7095d48f85c5b0..0000000000000000000000000000000000000000 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java ++++ /dev/null +@@ -1,9 +0,0 @@ +-package ca.spottedleaf.moonrise.patches.collisions.world; +- +-public interface CollisionLevel { +- +- public int moonrise$getMinSection(); +- +- public int moonrise$getMaxSection(); +- +-} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java +index 1fa07bef57d82c6d5242aaaf66011f0913515231..8e7472157a98de607c03769a91f64c8369fd3ea6 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java +@@ -10,4 +10,6 @@ public interface EntityTrackerTrackedEntity { + + public void moonrise$clearPlayers(); + ++ public boolean moonrise$hasPlayers(); ++ + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4a7abd239a9c59aa98947e7993962d75e9051902 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.fast_palette; ++ ++public interface FastPalette<T> { ++ ++ public default T[] moonrise$getRawPalette(final FastPaletteData<T> src) { ++ return null; ++ } ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4503f3495846a7d7ed082b9e24636044e4fbccd1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.fast_palette; ++ ++public interface FastPaletteData<T> { ++ ++ public T[] moonrise$getPalette(); ++ ++ public void moonrise$setPalette(final T[] palette); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java b/src/main/java/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java +new file mode 100644 +index 0000000000000000000000000000000000000000..107c97089354edd35f330582f5e0c8a18e792a6e +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java +@@ -0,0 +1,5 @@ ++package ca.spottedleaf.moonrise.patches.fluid; ++ ++public interface FluidFluidState { ++ public void moonrise$initCaches(); ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java +similarity index 75% +rename from src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java +rename to src/main/java/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java +index 08338917dc61c856eaba0b76e05c1497c458399d..540c14a6d2c216cd3ef2a9c4056e15712bf8cb8c 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java +@@ -1,4 +1,4 @@ +-package ca.spottedleaf.moonrise.patches.chunk_getblock; ++package ca.spottedleaf.moonrise.patches.getblock; + + import net.minecraft.world.level.block.state.BlockState; + +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java +index 2bfdf3721db9a45e36538d71cbefcb1d339e6c58..8e6d79b7c10ef25f5478b72c53c555423d615a2f 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java +@@ -4,6 +4,4 @@ public interface StarlightAbstractBlockState { + + public boolean starlight$isConditionallyFullOpaque(); + +- public int starlight$getOpacityIfCached(); +- + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java +index 154443ac1ee1d6d18b8ff0f40a307d638b213aeb..fa7b784a89626e8528c249d7889a598bd7ee3d49 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java +@@ -1,8 +1,10 @@ + package ca.spottedleaf.moonrise.patches.starlight.light; + ++import ca.spottedleaf.moonrise.common.PlatformHooks; + import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState; + import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; + import net.minecraft.core.BlockPos; ++import net.minecraft.world.level.BlockGetter; + import net.minecraft.world.level.Level; + import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.chunk.ChunkAccess; +@@ -91,7 +93,7 @@ public final class BlockStarLightEngine extends StarLightEngine { + + final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); + final BlockState blockState = this.getBlockState(worldX, worldY, worldZ); +- final int emittedLevel = blockState.getLightEmission() & emittedMask; ++ final int emittedLevel = (PlatformHooks.get().getLightEmission(blockState, lightAccess.getLevel(), this.lightEmissionPos.set(worldX, worldY, worldZ))) & emittedMask; + + this.setLightLevel(worldX, worldY, worldZ, emittedLevel); + // this accounts for change in emitted light that would cause an increase +@@ -119,37 +121,32 @@ public final class BlockStarLightEngine extends StarLightEngine { + } + + protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); +- protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); + + @Override + protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect) { ++ this.recalcCenterPos.set(worldX, worldY, worldZ); ++ + final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); +- int level = centerState.getLightEmission() & 0xF; ++ final BlockGetter world = lightAccess.getLevel(); ++ int level = (PlatformHooks.get().getLightEmission(centerState, world, this.recalcCenterPos)) & this.emittedLightMask; + + if (level >= (15 - 1) || level > expect) { + return level; + } + +- final int sectionOffset = this.chunkSectionIndexOffset; +- final BlockState conditionallyOpaqueState; +- int opacity = ((StarlightAbstractBlockState)centerState).starlight$getOpacityIfCached(); +- +- if (opacity == -1) { +- this.recalcCenterPos.set(worldX, worldY, worldZ); +- opacity = centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos); +- if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) { +- conditionallyOpaqueState = centerState; +- } else { +- conditionallyOpaqueState = null; +- } +- } else if (opacity >= 15) { ++ final int opacity = Math.max(1, centerState.getLightBlock()); ++ if (opacity >= 15) { + return level; ++ } ++ final BlockState conditionallyOpaqueState; ++ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) { ++ conditionallyOpaqueState = centerState; + } else { + conditionallyOpaqueState = null; + } +- opacity = Math.max(1, opacity); + ++ final int sectionOffset = this.chunkSectionIndexOffset; + for (final AxisDirection direction : AXIS_DIRECTIONS) { + final int offX = worldX + direction.x; + final int offY = worldY + direction.y; +@@ -169,9 +166,8 @@ public final class BlockStarLightEngine extends StarLightEngine { + // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that + // we don't read the blockstate because most of the time this is false, so using the faster + // known transparency lookup results in a net win +- this.recalcNeighbourPos.set(offX, offY, offZ); +- final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); +- final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); ++ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(direction.opposite.nms); ++ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(direction.nms); + if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { + // not allowed to propagate + continue; +@@ -205,30 +201,34 @@ public final class BlockStarLightEngine extends StarLightEngine { + final int offX = chunk.getPos().x << 4; + final int offZ = chunk.getPos().z << 4; + ++ final PlatformHooks platformHooks = PlatformHooks.get(); ++ ++ final BlockGetter world = lightAccess.getLevel(); + final LevelChunkSection[] sections = chunk.getSections(); + for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { + final LevelChunkSection section = sections[sectionY - this.minSection]; +- if (section == null || section.hasOnlyAir()) { ++ if (section.hasOnlyAir()) { + // no sources in empty sections + continue; + } +- if (!section.maybeHas((final BlockState state) -> { +- return state.getLightEmission() > 0; +- })) { ++ if (!section.maybeHas(platformHooks.maybeHasLightEmission())) { + // no light sources in palette + continue; + } + final PalettedContainer<BlockState> states = section.states; + final int offY = sectionY << 4; + ++ final BlockPos.MutableBlockPos mutablePos = this.lightEmissionPos; + for (int index = 0; index < (16 * 16 * 16); ++index) { + final BlockState state = states.get(index); +- if (state.getLightEmission() <= 0) { ++ mutablePos.set(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15)); ++ ++ if ((platformHooks.getLightEmission(state, world, mutablePos)) == 0) { + continue; + } + + // index = x | (z << 4) | (y << 8) +- sources.add(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15))); ++ sources.add(mutablePos.immutable()); + } + } + +@@ -238,12 +238,15 @@ public final class BlockStarLightEngine extends StarLightEngine { + @Override + public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { + // setup sources ++ final BlockGetter world = lightAccess.getLevel(); ++ final PlatformHooks platformHooks = PlatformHooks.get(); ++ + final int emittedMask = this.emittedLightMask; + final List<BlockPos> positions = this.getSources(lightAccess, chunk); + for (int i = 0, len = positions.size(); i < len; ++i) { + final BlockPos pos = positions.get(i); + final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ()); +- final int emittedLight = blockState.getLightEmission() & emittedMask; ++ final int emittedLight = platformHooks.getLightEmission(blockState, world, pos) & emittedMask; + + if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) { + // some other source is brighter +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java +index fdbc015f498164c9d2c578cd84a73def568142a4..f9aef289e9a2d6f63c98c72c56ef32b8793f57f4 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java +@@ -290,9 +290,6 @@ public final class SkyStarLightEngine extends StarLightEngine { + ); + } + +- protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); +- protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); +- + @Override + protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect) { +@@ -302,20 +299,13 @@ public final class SkyStarLightEngine extends StarLightEngine { + + final int sectionOffset = this.chunkSectionIndexOffset; + final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); +- int opacity = ((StarlightAbstractBlockState)centerState).starlight$getOpacityIfCached(); + + final BlockState conditionallyOpaqueState; +- if (opacity < 0) { +- this.recalcCenterPos.set(worldX, worldY, worldZ); +- opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos)); +- if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) { +- conditionallyOpaqueState = centerState; +- } else { +- conditionallyOpaqueState = null; +- } ++ final int opacity = Math.max(1, centerState.getLightBlock()); ++ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) { ++ conditionallyOpaqueState = centerState; + } else { + conditionallyOpaqueState = null; +- opacity = Math.max(1, opacity); + } + + int level = 0; +@@ -340,9 +330,8 @@ public final class SkyStarLightEngine extends StarLightEngine { + // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that + // we don't read the blockstate because most of the time this is false, so using the faster + // known transparency lookup results in a net win +- this.recalcNeighbourPos.set(offX, offY, offZ); +- final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); +- final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); ++ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(direction.opposite.nms); ++ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(direction.nms); + if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { + // not allowed to propagate + continue; +@@ -610,7 +599,6 @@ public final class SkyStarLightEngine extends StarLightEngine { + // clobbering the light values will result in broken propagation) + protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ, + final boolean extrudeInitialised, final boolean delayLightSet) { +- final BlockPos.MutableBlockPos mutablePos = this.mutablePos3; + final int encodeOffset = this.coordinateOffset; + final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards. + +@@ -632,8 +620,7 @@ public final class SkyStarLightEngine extends StarLightEngine { + + final VoxelShape fromShape; + if (((StarlightAbstractBlockState)above).starlight$isConditionallyFullOpaque()) { +- this.mutablePos2.set(worldX, startY + 1, worldZ); +- fromShape = above.getFaceOcclusionShape(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms); ++ fromShape = above.getFaceOcclusionShape(AxisDirection.NEGATIVE_Y.nms); + if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { + // above wont let us propagate + break; +@@ -642,49 +629,32 @@ public final class SkyStarLightEngine extends StarLightEngine { + fromShape = Shapes.empty(); + } + +- final int opacityIfCached = ((StarlightAbstractBlockState)current).starlight$getOpacityIfCached(); + // does light propagate from the top down? +- if (opacityIfCached != -1) { +- if (opacityIfCached != 0) { +- // we cannot propagate 15 through this +- break; +- } +- // most of the time it falls here. +- // add to propagate +- // light set delayed until we determine if this nibble section is null +- this.appendToIncreaseQueue( +- ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | (15L << (6 + 6 + 16)) // we know we're at full lit here +- | (propagateDirection << (6 + 6 + 16 + 4)) +- ); +- } else { +- mutablePos.set(worldX, startY, worldZ); +- long flags = 0L; +- if (((StarlightAbstractBlockState)current).starlight$isConditionallyFullOpaque()) { +- final VoxelShape cullingFace = current.getFaceOcclusionShape(world, mutablePos, AxisDirection.POSITIVE_Y.nms); ++ long flags = 0L; ++ if (((StarlightAbstractBlockState)current).starlight$isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = current.getFaceOcclusionShape(AxisDirection.POSITIVE_Y.nms); + +- if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { +- // can't propagate here, we're done on this column. +- break; +- } +- flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; +- } +- +- final int opacity = current.getLightBlock(world, mutablePos); +- if (opacity > 0) { +- // let the queued value (if any) handle it from here. ++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { ++ // can't propagate here, we're done on this column. + break; + } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } + +- // light set delayed until we determine if this nibble section is null +- this.appendToIncreaseQueue( +- ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | (15L << (6 + 6 + 16)) // we know we're at full lit here +- | (propagateDirection << (6 + 6 + 16 + 4)) +- | flags +- ); ++ final int opacity = current.getLightBlock(); ++ if (opacity > 0) { ++ // let the queued value (if any) handle it from here. ++ break; + } + ++ // light set delayed until we determine if this nibble section is null ++ this.appendToIncreaseQueue( ++ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | (15L << (6 + 6 + 16)) // we know we're at full lit here ++ | (propagateDirection << (6 + 6 + 16 + 4)) ++ | flags ++ ); ++ + above = current; + + if (this.getNibbleFromCache(worldX >> 4, startY >> 4, worldZ >> 4) == null) { +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java +index 382c9e445af0d6ad2428fc22d0f63017c58191e2..8aeb5fb87f94a35659347a09a638420699b52a6f 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java +@@ -1,6 +1,7 @@ + package ca.spottedleaf.moonrise.patches.starlight.light; + + import ca.spottedleaf.concurrentutil.util.IntegerUtil; ++import ca.spottedleaf.moonrise.common.PlatformHooks; + import ca.spottedleaf.moonrise.common.util.CoordinateUtils; + import ca.spottedleaf.moonrise.common.util.WorldUtil; + import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState; +@@ -43,9 +44,9 @@ public abstract class StarLightEngine { + protected static enum AxisDirection { + + // Declaration order is important and relied upon. Do not change without modifying propagation code. +- POSITIVE_X(1, 0, 0), NEGATIVE_X(-1, 0, 0), +- POSITIVE_Z(0, 0, 1), NEGATIVE_Z(0, 0, -1), +- POSITIVE_Y(0, 1, 0), NEGATIVE_Y(0, -1, 0); ++ POSITIVE_X(1, 0, 0, Direction.EAST) , NEGATIVE_X(-1, 0, 0, Direction.WEST), ++ POSITIVE_Z(0, 0, 1, Direction.SOUTH), NEGATIVE_Z(0, 0, -1, Direction.NORTH), ++ POSITIVE_Y(0, 1, 0, Direction.UP) , NEGATIVE_Y(0, -1, 0, Direction.DOWN); + + static { + POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X; +@@ -62,11 +63,11 @@ public abstract class StarLightEngine { + public final long everythingButThisDirection; + public final long everythingButTheOppositeDirection; + +- AxisDirection(final int x, final int y, final int z) { ++ AxisDirection(final int x, final int y, final int z, final Direction nms) { + this.x = x; + this.y = y; + this.z = z; +- this.nms = Direction.fromDelta(x, y, z); ++ this.nms = nms; + this.everythingButThisDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << this.ordinal())); + // positive is always even, negative is always odd. Flip the 1 bit to get the negative direction. + this.everythingButTheOppositeDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << (this.ordinal() ^ 1))); +@@ -106,9 +107,7 @@ public abstract class StarLightEngine { + // index = x + (z * 5) + protected final boolean[][] emptinessMapCache = new boolean[5 * 5][]; + +- protected final BlockPos.MutableBlockPos mutablePos1 = new BlockPos.MutableBlockPos(); +- protected final BlockPos.MutableBlockPos mutablePos2 = new BlockPos.MutableBlockPos(); +- protected final BlockPos.MutableBlockPos mutablePos3 = new BlockPos.MutableBlockPos(); ++ protected final BlockPos.MutableBlockPos lightEmissionPos = new BlockPos.MutableBlockPos(); + + protected int encodeOffsetX; + protected int encodeOffsetY; +@@ -1150,69 +1149,46 @@ public abstract class StarLightEngine { + if (blockState == null) { + continue; + } +- final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached(); +- if (opacityCached != -1) { +- final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached); +- if (targetLevel > currentLevel) { +- currentNibble.set(localIndex, targetLevel); +- this.postLightUpdate(offX, offY, offZ); +- +- if (targetLevel > 1) { +- if (queueLength >= queue.length) { +- queue = this.resizeIncreaseQueue(); +- } +- queue[queueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((targetLevel & 0xFL) << (6 + 6 + 16)) +- | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); +- continue; +- } +- } +- continue; +- } else { +- this.mutablePos1.set(offX, offY, offZ); +- long flags = 0; +- if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { +- final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); ++ long flags = 0; ++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); + +- if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { +- continue; +- } +- flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; +- } +- +- final int opacity = blockState.getLightBlock(world, this.mutablePos1); +- final int targetLevel = propagatedLightLevel - Math.max(1, opacity); +- if (targetLevel <= currentLevel) { ++ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { + continue; + } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } + +- currentNibble.set(localIndex, targetLevel); +- this.postLightUpdate(offX, offY, offZ); ++ final int opacity = blockState.getLightBlock(); ++ final int targetLevel = propagatedLightLevel - Math.max(1, opacity); ++ if (targetLevel <= currentLevel) { ++ continue; ++ } + +- if (targetLevel > 1) { +- if (queueLength >= queue.length) { +- queue = this.resizeIncreaseQueue(); +- } +- queue[queueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((targetLevel & 0xFL) << (6 + 6 + 16)) +- | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) +- | (flags); ++ currentNibble.set(localIndex, targetLevel); ++ this.postLightUpdate(offX, offY, offZ); ++ ++ if (targetLevel > 1) { ++ if (queueLength >= queue.length) { ++ queue = this.resizeIncreaseQueue(); + } +- continue; ++ queue[queueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((targetLevel & 0xFL) << (6 + 6 + 16)) ++ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) ++ | (flags); + } ++ continue; + } + } else { + // we actually need to worry about our state here + final BlockState fromBlock = this.getBlockState(posX, posY, posZ); +- this.mutablePos2.set(posX, posY, posZ); + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + +- final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); ++ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(propagate.nms) : Shapes.empty(); + + if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { + continue; +@@ -1232,58 +1208,36 @@ public abstract class StarLightEngine { + if (blockState == null) { + continue; + } +- final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached(); +- if (opacityCached != -1) { +- final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached); +- if (targetLevel > currentLevel) { +- currentNibble.set(localIndex, targetLevel); +- this.postLightUpdate(offX, offY, offZ); +- +- if (targetLevel > 1) { +- if (queueLength >= queue.length) { +- queue = this.resizeIncreaseQueue(); +- } +- queue[queueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((targetLevel & 0xFL) << (6 + 6 + 16)) +- | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); +- continue; +- } +- } +- continue; +- } else { +- this.mutablePos1.set(offX, offY, offZ); +- long flags = 0; +- if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { +- final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); +- +- if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { +- continue; +- } +- flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; +- } ++ long flags = 0; ++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); + +- final int opacity = blockState.getLightBlock(world, this.mutablePos1); +- final int targetLevel = propagatedLightLevel - Math.max(1, opacity); +- if (targetLevel <= currentLevel) { ++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { + continue; + } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } + +- currentNibble.set(localIndex, targetLevel); +- this.postLightUpdate(offX, offY, offZ); ++ final int opacity = blockState.getLightBlock(); ++ final int targetLevel = propagatedLightLevel - Math.max(1, opacity); ++ if (targetLevel <= currentLevel) { ++ continue; ++ } + +- if (targetLevel > 1) { +- if (queueLength >= queue.length) { +- queue = this.resizeIncreaseQueue(); +- } +- queue[queueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((targetLevel & 0xFL) << (6 + 6 + 16)) +- | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) +- | (flags); ++ currentNibble.set(localIndex, targetLevel); ++ this.postLightUpdate(offX, offY, offZ); ++ ++ if (targetLevel > 1) { ++ if (queueLength >= queue.length) { ++ queue = this.resizeIncreaseQueue(); + } +- continue; ++ queue[queueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((targetLevel & 0xFL) << (6 + 6 + 16)) ++ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) ++ | (flags); + } ++ continue; + } + } + } +@@ -1304,6 +1258,8 @@ public abstract class StarLightEngine { + final int sectionOffset = this.chunkSectionIndexOffset; + final int emittedMask = this.emittedLightMask; + ++ final PlatformHooks platformHooks = PlatformHooks.get(); ++ + while (queueReadIndex < queueLength) { + final long queueValue = queue[queueReadIndex++]; + +@@ -1335,109 +1291,63 @@ public abstract class StarLightEngine { + if (blockState == null) { + continue; + } +- final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached(); +- if (opacityCached != -1) { +- final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached)); +- if (lightLevel > targetLevel) { +- // it looks like another source propagated here, so re-propagate it +- if (increaseQueueLength >= increaseQueue.length) { +- increaseQueue = this.resizeIncreaseQueue(); +- } +- increaseQueue[increaseQueueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((lightLevel & 0xFL) << (6 + 6 + 16)) +- | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) +- | FLAG_RECHECK_LEVEL; +- continue; +- } +- final int emittedLight = blockState.getLightEmission() & emittedMask; +- if (emittedLight != 0) { +- // re-propagate source +- // note: do not set recheck level, or else the propagation will fail +- if (increaseQueueLength >= increaseQueue.length) { +- increaseQueue = this.resizeIncreaseQueue(); +- } +- increaseQueue[increaseQueueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((emittedLight & 0xFL) << (6 + 6 + 16)) +- | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) +- | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL); +- } +- +- currentNibble.set(localIndex, 0); +- this.postLightUpdate(offX, offY, offZ); ++ this.lightEmissionPos.set(offX, offY, offZ); ++ long flags = 0; ++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); + +- if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... +- if (queueLength >= queue.length) { +- queue = this.resizeDecreaseQueue(); +- } +- queue[queueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((targetLevel & 0xFL) << (6 + 6 + 16)) +- | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); ++ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { + continue; + } +- continue; +- } else { +- this.mutablePos1.set(offX, offY, offZ); +- long flags = 0; +- if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { +- final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); +- +- if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { +- continue; +- } +- flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; +- } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } + +- final int opacity = blockState.getLightBlock(world, this.mutablePos1); +- final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); +- if (lightLevel > targetLevel) { +- // it looks like another source propagated here, so re-propagate it +- if (increaseQueueLength >= increaseQueue.length) { +- increaseQueue = this.resizeIncreaseQueue(); +- } +- increaseQueue[increaseQueueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((lightLevel & 0xFL) << (6 + 6 + 16)) +- | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) +- | (FLAG_RECHECK_LEVEL | flags); +- continue; ++ final int opacity = blockState.getLightBlock(); ++ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); ++ if (lightLevel > targetLevel) { ++ // it looks like another source propagated here, so re-propagate it ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); + } +- final int emittedLight = blockState.getLightEmission() & emittedMask; +- if (emittedLight != 0) { +- // re-propagate source +- // note: do not set recheck level, or else the propagation will fail +- if (increaseQueueLength >= increaseQueue.length) { +- increaseQueue = this.resizeIncreaseQueue(); +- } +- increaseQueue[increaseQueueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((emittedLight & 0xFL) << (6 + 6 + 16)) +- | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) +- | (flags | FLAG_WRITE_LEVEL); ++ increaseQueue[increaseQueueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((lightLevel & 0xFL) << (6 + 6 + 16)) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | (FLAG_RECHECK_LEVEL | flags); ++ continue; ++ } ++ final int emittedLight = (platformHooks.getLightEmission(blockState, world, this.lightEmissionPos)) & emittedMask; ++ if (emittedLight != 0) { ++ // re-propagate source ++ // note: do not set recheck level, or else the propagation will fail ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); + } ++ increaseQueue[increaseQueueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((emittedLight & 0xFL) << (6 + 6 + 16)) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | (flags | FLAG_WRITE_LEVEL); ++ } + +- currentNibble.set(localIndex, 0); +- this.postLightUpdate(offX, offY, offZ); ++ currentNibble.set(localIndex, 0); ++ this.postLightUpdate(offX, offY, offZ); + +- if (targetLevel > 0) { +- if (queueLength >= queue.length) { +- queue = this.resizeDecreaseQueue(); +- } +- queue[queueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((targetLevel & 0xFL) << (6 + 6 + 16)) +- | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) +- | flags; ++ if (targetLevel > 0) { ++ if (queueLength >= queue.length) { ++ queue = this.resizeDecreaseQueue(); + } +- continue; ++ queue[queueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((targetLevel & 0xFL) << (6 + 6 + 16)) ++ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) ++ | flags; + } ++ continue; + } + } else { + // we actually need to worry about our state here + final BlockState fromBlock = this.getBlockState(posX, posY, posZ); +- this.mutablePos2.set(posX, posY, posZ); + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; +@@ -1446,7 +1356,7 @@ public abstract class StarLightEngine { + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + +- final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); ++ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(propagate.nms) : Shapes.empty(); + + if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { + continue; +@@ -1464,104 +1374,59 @@ public abstract class StarLightEngine { + if (blockState == null) { + continue; + } +- final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached(); +- if (opacityCached != -1) { +- final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached)); +- if (lightLevel > targetLevel) { +- // it looks like another source propagated here, so re-propagate it +- if (increaseQueueLength >= increaseQueue.length) { +- increaseQueue = this.resizeIncreaseQueue(); +- } +- increaseQueue[increaseQueueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((lightLevel & 0xFL) << (6 + 6 + 16)) +- | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) +- | FLAG_RECHECK_LEVEL; +- continue; +- } +- final int emittedLight = blockState.getLightEmission() & emittedMask; +- if (emittedLight != 0) { +- // re-propagate source +- // note: do not set recheck level, or else the propagation will fail +- if (increaseQueueLength >= increaseQueue.length) { +- increaseQueue = this.resizeIncreaseQueue(); +- } +- increaseQueue[increaseQueueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((emittedLight & 0xFL) << (6 + 6 + 16)) +- | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) +- | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL); +- } +- +- currentNibble.set(localIndex, 0); +- this.postLightUpdate(offX, offY, offZ); ++ this.lightEmissionPos.set(offX, offY, offZ); ++ long flags = 0; ++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); + +- if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... +- if (queueLength >= queue.length) { +- queue = this.resizeDecreaseQueue(); +- } +- queue[queueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((targetLevel & 0xFL) << (6 + 6 + 16)) +- | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); ++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { + continue; + } +- continue; +- } else { +- this.mutablePos1.set(offX, offY, offZ); +- long flags = 0; +- if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { +- final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); +- +- if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { +- continue; +- } +- flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; +- } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } + +- final int opacity = blockState.getLightBlock(world, this.mutablePos1); +- final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); +- if (lightLevel > targetLevel) { +- // it looks like another source propagated here, so re-propagate it +- if (increaseQueueLength >= increaseQueue.length) { +- increaseQueue = this.resizeIncreaseQueue(); +- } +- increaseQueue[increaseQueueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((lightLevel & 0xFL) << (6 + 6 + 16)) +- | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) +- | (FLAG_RECHECK_LEVEL | flags); +- continue; ++ final int opacity = blockState.getLightBlock(); ++ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); ++ if (lightLevel > targetLevel) { ++ // it looks like another source propagated here, so re-propagate it ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); + } +- final int emittedLight = blockState.getLightEmission() & emittedMask; +- if (emittedLight != 0) { +- // re-propagate source +- // note: do not set recheck level, or else the propagation will fail +- if (increaseQueueLength >= increaseQueue.length) { +- increaseQueue = this.resizeIncreaseQueue(); +- } +- increaseQueue[increaseQueueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((emittedLight & 0xFL) << (6 + 6 + 16)) +- | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) +- | (flags | FLAG_WRITE_LEVEL); ++ increaseQueue[increaseQueueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((lightLevel & 0xFL) << (6 + 6 + 16)) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | (FLAG_RECHECK_LEVEL | flags); ++ continue; ++ } ++ final int emittedLight = (platformHooks.getLightEmission(blockState, world, this.lightEmissionPos)) & emittedMask; ++ if (emittedLight != 0) { ++ // re-propagate source ++ // note: do not set recheck level, or else the propagation will fail ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); + } ++ increaseQueue[increaseQueueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((emittedLight & 0xFL) << (6 + 6 + 16)) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | (flags | FLAG_WRITE_LEVEL); ++ } + +- currentNibble.set(localIndex, 0); +- this.postLightUpdate(offX, offY, offZ); ++ currentNibble.set(localIndex, 0); ++ this.postLightUpdate(offX, offY, offZ); + +- if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... +- if (queueLength >= queue.length) { +- queue = this.resizeDecreaseQueue(); +- } +- queue[queueLength++] = +- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) +- | ((targetLevel & 0xFL) << (6 + 6 + 16)) +- | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) +- | flags; ++ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... ++ if (queueLength >= queue.length) { ++ queue = this.resizeDecreaseQueue(); + } +- continue; ++ queue[queueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((targetLevel & 0xFL) << (6 + 6 + 16)) ++ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) ++ | flags; + } ++ continue; + } + } + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java +index c64ab41198a5e0c7cbcbe6452af11f82f5938862..571db5f9bf94745a8afe2cd313e593fb15db5e37 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java +@@ -1,8 +1,9 @@ + package ca.spottedleaf.moonrise.patches.starlight.light; + + import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; + import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; ++import ca.spottedleaf.concurrentutil.util.Priority; + import ca.spottedleaf.moonrise.common.util.CoordinateUtils; + import ca.spottedleaf.moonrise.common.util.WorldUtil; + import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; +@@ -745,34 +746,34 @@ public final class StarLightInterface { + super(lightInterface); + } + +- public void lowerPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ public void lowerPriority(final int chunkX, final int chunkZ, final Priority priority) { + final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (task != null) { + task.lowerPriority(priority); + } + } + +- public void setPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ public void setPriority(final int chunkX, final int chunkZ, final Priority priority) { + final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (task != null) { + task.setPriority(priority); + } + } + +- public void raisePriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ public void raisePriority(final int chunkX, final int chunkZ, final Priority priority) { + final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (task != null) { + task.raisePriority(priority); + } + } + +- public PrioritisedExecutor.Priority getPriority(final int chunkX, final int chunkZ) { ++ public Priority getPriority(final int chunkX, final int chunkZ) { + final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (task != null) { + return task.getPriority(); + } + +- return PrioritisedExecutor.Priority.COMPLETING; ++ return Priority.COMPLETING; + } + + @Override +@@ -816,7 +817,7 @@ public final class StarLightInterface { + return ret; + } + +- public ServerChunkTasks queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final PrioritisedExecutor.Priority priority) { ++ public ServerChunkTasks queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final Priority priority) { + final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { + if (valueInMap == null) { + valueInMap = new ServerChunkTasks( +@@ -879,11 +880,11 @@ public final class StarLightInterface { + + public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, + final ServerLightQueue queue) { +- this(chunkCoordinate, lightEngine, queue, PrioritisedExecutor.Priority.NORMAL); ++ this(chunkCoordinate, lightEngine, queue, Priority.NORMAL); + } + + public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, +- final ServerLightQueue queue, final PrioritisedExecutor.Priority priority) { ++ final ServerLightQueue queue, final Priority priority) { + super(chunkCoordinate, lightEngine, queue); + this.task = ((ChunkSystemServerLevel)(ServerLevel)lightEngine.getWorld()).moonrise$getChunkTaskScheduler().radiusAwareScheduler.createTask( + CoordinateUtils.getChunkX(chunkCoordinate), CoordinateUtils.getChunkZ(chunkCoordinate), +@@ -903,19 +904,19 @@ public final class StarLightInterface { + return this.task.cancel(); + } + +- public PrioritisedExecutor.Priority getPriority() { ++ public Priority getPriority() { + return this.task.getPriority(); + } + +- public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ public void lowerPriority(final Priority priority) { + this.task.lowerPriority(priority); + } + +- public void setPriority(final PrioritisedExecutor.Priority priority) { ++ public void setPriority(final Priority priority) { + this.task.setPriority(priority); + } + +- public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ public void raisePriority(final Priority priority) { + this.task.raisePriority(priority); + } + +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..40d004afdc6449530f5bb2d7c7638b8ee3e3a577 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java +@@ -0,0 +1,13 @@ ++package ca.spottedleaf.moonrise.patches.starlight.storage; ++ ++public interface StarlightSectionData { ++ ++ public int starlight$getBlockLightState(); ++ ++ public void starlight$setBlockLightState(final int state); ++ ++ public int starlight$getSkyLightState(); ++ ++ public void starlight$setSkyLightState(final int state); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java +index 57692a503e147a00ac4e1586cd78e12b71a80d3f..689ce367164e79e0426eeecb81dbbc521d4bc742 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java +@@ -14,19 +14,20 @@ import net.minecraft.world.level.chunk.ChunkAccess; + import net.minecraft.world.level.chunk.status.ChunkStatus; + import org.slf4j.Logger; + ++// note: keep in-sync with SerializableChunkDataMixin + public final class SaveUtil { + + private static final Logger LOGGER = LogUtils.getLogger(); + +- private static final int STARLIGHT_LIGHT_VERSION = 9; ++ public static final int STARLIGHT_LIGHT_VERSION = 9; + + public static int getLightVersion() { + return STARLIGHT_LIGHT_VERSION; + } + +- private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state"; +- private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; +- private static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; ++ public static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state"; ++ public static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; ++ public static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; + + public static void saveLightHook(final Level world, final ChunkAccess chunk, final CompoundTag nbt) { + try { +diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +index a9dd0e5216e95afd98fd2200d110e2cc0b1b0dca..8371ce4e3df5ef8e39acd6e005209337cc76b451 100644 +--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java ++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +@@ -243,6 +243,7 @@ public class GlobalConfiguration extends ConfigurationPart { + + @PostProcess + private void postProcess() { ++ ca.spottedleaf.moonrise.common.util.MoonriseCommon.adjustWorkerThreads(this.workerThreads, this.ioThreads); + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.init(this); + } + } +diff --git a/src/main/java/net/minecraft/core/MappedRegistry.java b/src/main/java/net/minecraft/core/MappedRegistry.java +index f22d22ebcedcc9c20225677844c86a1ad27c4211..ee1d39b6196c0f87ba473a15d3377e858922696c 100644 +--- a/src/main/java/net/minecraft/core/MappedRegistry.java ++++ b/src/main/java/net/minecraft/core/MappedRegistry.java +@@ -78,6 +78,19 @@ public class MappedRegistry<T> implements WritableRegistry<T> { + }; + private final Object tagAdditionLock = new Object(); + ++ // Paper start - fluid method optimisations ++ private void injectFluidRegister( ++ final ResourceKey<?> resourceKey, ++ final T object ++ ) { ++ if (resourceKey.registryKey() == (Object)net.minecraft.core.registries.Registries.FLUID) { ++ for (final net.minecraft.world.level.material.FluidState possibleState : ((net.minecraft.world.level.material.Fluid)object).getStateDefinition().getPossibleStates()) { ++ ((ca.spottedleaf.moonrise.patches.fluid.FluidFluidState)(Object)possibleState).moonrise$initCaches(); ++ } ++ } ++ } ++ // Paper end - fluid method optimisations ++ + public MappedRegistry(ResourceKey<? extends Registry<T>> key, Lifecycle lifecycle) { + this(key, lifecycle, false); + } +@@ -145,6 +158,7 @@ public class MappedRegistry<T> implements WritableRegistry<T> { + this.toId.put(value, i); + this.registrationInfos.put(key, info); + this.registryLifecycle = this.registryLifecycle.add(info.lifecycle()); ++ this.injectFluidRegister(key, value); // Paper - fluid method optimisations + return reference; + } + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 696d075ca2883f3c37e35f983c4d020e5db89d16..25047b8536bc7e7a91ac0dfb82ad77099af6f125 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -1106,7 +1106,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + // Spigot end + + // Paper end - Improved watchdog support; move final shutdown items here +- ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.deinit(); // Paper - rewrite chunk system ++ // Paper start - rewrite chunk system ++ LOGGER.info("Waiting for I/O tasks to complete..."); ++ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.flush((MinecraftServer)(Object)this); ++ LOGGER.info("All I/O tasks to complete"); ++ if ((Object)this instanceof DedicatedServer) { ++ ca.spottedleaf.moonrise.common.util.MoonriseCommon.haltExecutors(); ++ } ++ // Paper end - rewrite chunk system + // Paper start - move final shutdown items here + Util.shutdownExecutors(); + try { +diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java +index 4db96543e2072e47040bb25a9d97ea6a69c4a43d..2b082a644332569f0bcc84011430366150ef504d 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java ++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java +@@ -245,7 +245,7 @@ public class ChunkHolder extends GenerationChunkHolder implements ca.spottedleaf + + if (ichunkaccess != null) { + ichunkaccess.setUnsaved(true); +- LevelChunk chunk = this.getChunkToSend(); // Paper - rewrite chunk system ++ LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system + + if (chunk != null) { + int j = this.lightEngine.getMinLightSection(); +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index af8cb316ac169aa8d98a88765b85bb013b9ba961..38de8d8ed2efa3476d4e906bdc4b5944430bd1b7 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -916,7 +916,6 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + // Paper start - optimise entity tracker + private void newTrackerTick() { +- final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers(); + final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup entityLookup = (ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup)((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getEntityLookup();; + + final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.world.entity.Entity> trackerEntities = entityLookup.trackerEntities; +@@ -927,21 +926,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + if (tracker == null) { + continue; + } +- ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$tick(nearbyPlayers.getChunk(entity.chunkPosition())); +- tracker.serverEntity.sendChanges(); +- } +- +- // process unloads +- final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.world.entity.Entity> unloadedEntities = entityLookup.trackerUnloadedEntities; +- final Entity[] unloadedEntitiesRaw = java.util.Arrays.copyOf(unloadedEntities.getRawDataUnchecked(), unloadedEntities.size()); +- unloadedEntities.clear(); +- +- for (final Entity entity : unloadedEntitiesRaw) { +- final ChunkMap.TrackedEntity tracker = ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity(); +- if (tracker == null) { +- continue; ++ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$tick(((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$getChunkData().nearbyPlayers); ++ if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$hasPlayers() ++ || ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING)) { ++ tracker.serverEntity.sendChanges(); + } +- ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$clearPlayers(); + } + } + // Paper end - optimise entity tracker +@@ -1178,6 +1167,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.removePlayer(player); + } + } ++ ++ @Override ++ public final boolean moonrise$hasPlayers() { ++ return !this.seenBy.isEmpty(); ++ } + // Paper end - optimise entity tracker + + public TrackedEntity(final Entity entity, final int i, final int j, final boolean flag) { +@@ -1283,20 +1277,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + private int getEffectiveRange() { +- int i = this.range; +- Iterator iterator = this.entity.getIndirectPassengers().iterator(); ++ // Paper start - optimise entity tracker ++ final Entity entity = this.entity; ++ int range = ca.spottedleaf.moonrise.common.PlatformHooks.get().modifyEntityTrackingRange(entity, this.range); + +- while (iterator.hasNext()) { +- Entity entity = (Entity) iterator.next(); +- int j = entity.getType().clientTrackingRange() * 16; +- j = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, j); // Paper ++ if (entity.getPassengers() == ImmutableList.<Entity>of()) { ++ return this.scaledRange(range); ++ } + +- if (j > i) { +- i = j; +- } ++ // note: we change to List ++ final List<Entity> passengers = (List<Entity>)entity.getIndirectPassengers(); ++ for (int i = 0, len = passengers.size(); i < len; ++i) { ++ final Entity passenger = passengers.get(i); ++ // note: max should be branchless ++ range = Math.max(range, ca.spottedleaf.moonrise.common.PlatformHooks.get().modifyEntityTrackingRange(passenger, passenger.getType().clientTrackingRange() << 4)); + } + +- return this.scaledRange(i); ++ return this.scaledRange(range); ++ // Paper end - optimise entity tracker + } + + public void updatePlayers(List<ServerPlayer> players) { +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index dcb5651d1d9b10b40430fb2f713beedf68336704..b380509cc942a054ea176216210ba69f5b517ae9 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -92,7 +92,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler(); + final CompletableFuture<ChunkAccess> completable = new CompletableFuture<>(); + chunkTaskScheduler.scheduleChunkLoad( +- chunkX, chunkZ, toStatus, true, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.BLOCKING, ++ chunkX, chunkZ, toStatus, true, ca.spottedleaf.concurrentutil.util.Priority.BLOCKING, + completable::complete + ); + +@@ -123,6 +123,15 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + return ifPresent; + } + ++ final ca.spottedleaf.moonrise.common.PlatformHooks platformHooks = ca.spottedleaf.moonrise.common.PlatformHooks.get(); ++ ++ if (platformHooks.hasCurrentlyLoadingChunk() && currentChunk != null) { ++ final ChunkAccess loading = platformHooks.getCurrentlyLoadingChunk(currentChunk.vanillaChunkHolder); ++ if (loading != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { ++ return loading; ++ } ++ } ++ + return load ? this.syncLoad(chunkX, chunkZ, toStatus) : null; + } + // Paper end - rewrite chunk system +@@ -242,7 +251,24 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + @Nullable + @Override + public LevelChunk getChunkNow(int chunkX, int chunkZ) { +- return this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); // Paper - rewrite chunk system ++ // Paper start - rewrite chunk system ++ final LevelChunk ret = this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ if (!ca.spottedleaf.moonrise.common.PlatformHooks.get().hasCurrentlyLoadingChunk()) { ++ return ret; ++ } ++ ++ if (ret != null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { ++ return ret; ++ } ++ ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler() ++ .chunkHolderManager.getChunkHolder(chunkX, chunkZ); ++ if (holder == null) { ++ return ret; ++ } ++ ++ return ca.spottedleaf.moonrise.common.PlatformHooks.get().getCurrentlyLoadingChunk(holder.vanillaChunkHolder); ++ // Paper end - rewrite chunk system + } + + private void clearCache() { +@@ -299,7 +325,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().scheduleChunkLoad( + chunkX, chunkZ, leastStatus, true, +- ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, ++ ca.spottedleaf.concurrentutil.util.Priority.HIGHER, + complete + ); + +@@ -556,7 +582,8 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + + private void getFullChunk(long pos, Consumer<LevelChunk> chunkConsumer) { + // Paper start - rewrite chunk system +- final LevelChunk fullChunk = this.getChunkNow(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos)); ++ // note: bypass currentlyLoaded from getChunkNow ++ final LevelChunk fullChunk = this.fullChunks.get(pos); + if (fullChunk != null) { + chunkConsumer.accept(fullChunk); + } +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index f9abf63e12ea930275121b470e4e4906cff0fc12..55e81892b928a02e240aaa31dd501b0aacef8dbe 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -799,11 +799,13 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. + } + + // Paper start - optimise random ticking ++ private final ca.spottedleaf.moonrise.common.util.SimpleRandom simpleRandom = new ca.spottedleaf.moonrise.common.util.SimpleRandom(0L); ++ + private void optimiseRandomTick(final LevelChunk chunk, final int tickSpeed) { + final LevelChunkSection[] sections = chunk.getSections(); + final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection((ServerLevel)(Object)this); +- final RandomSource random = this.random; +- final boolean tickFluids = false; // Paper - not configurable - MC-224294 ++ final ca.spottedleaf.moonrise.common.util.SimpleRandom simpleRandom = this.simpleRandom; ++ final boolean doubleTickFluids = !ca.spottedleaf.moonrise.common.PlatformHooks.get().configFixMC224294(); + + final ChunkPos cpos = chunk.getPos(); + final int offsetX = cpos.x << 4; +@@ -813,39 +815,32 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. + final int offsetY = (sectionIndex + minSection) << 4; + final LevelChunkSection section = sections[sectionIndex]; + final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> states = section.states; +- if (section == null || !section.isRandomlyTickingBlocks()) { ++ if (!section.isRandomlyTickingBlocks()) { + continue; + } + +- final ca.spottedleaf.moonrise.common.list.IBlockDataList tickList = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getTickingBlockList(); +- if (tickList.size() == 0) { +- continue; +- } ++ final ca.spottedleaf.moonrise.common.list.ShortList tickList = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getTickingBlockList(); + + for (int i = 0; i < tickSpeed; ++i) { + final int tickingBlocks = tickList.size(); +- final int index = random.nextInt() & ((16 * 16 * 16) - 1); ++ final int index = simpleRandom.nextInt() & ((16 * 16 * 16) - 1); + + if (index >= tickingBlocks) { + // most of the time we fall here + continue; + } + +- final long raw = tickList.getRaw(index); +- final int location = ca.spottedleaf.moonrise.common.list.IBlockDataList.getLocationFromRaw(raw); +- final int randomX = (location & 15); +- final int randomY = ((location >>> (4 + 4)) & 255); +- final int randomZ = ((location >>> 4) & 15); +- final BlockState state = states.get(randomX | (randomZ << 4) | (randomY << 8)); ++ final int location = (int)tickList.getRaw(index) & 0xFFFF; ++ final BlockState state = states.get(location); + + // do not use a mutable pos, as some random tick implementations store the input without calling immutable()! +- final BlockPos pos = new BlockPos(randomX | offsetX, randomY | offsetY, randomZ | offsetZ); ++ final BlockPos pos = new BlockPos((location & 15) | offsetX, ((location >>> (4 + 4)) & 15) | offsetY, ((location >>> 4) & 15) | offsetZ); + +- state.randomTick((ServerLevel)(Object)this, pos, random); +- if (tickFluids) { ++ state.randomTick((ServerLevel)(Object)this, pos, simpleRandom); ++ if (doubleTickFluids) { + final FluidState fluidState = state.getFluidState(); + if (fluidState.isRandomlyTicking()) { +- fluidState.randomTick((ServerLevel)(Object)this, pos, random); ++ fluidState.randomTick((ServerLevel)(Object)this, pos, simpleRandom); + } + } + } +@@ -856,6 +851,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. + // Paper end - optimise random ticking + + public void tickChunk(LevelChunk chunk, int randomTickSpeed) { ++ final ca.spottedleaf.moonrise.common.util.SimpleRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking + ChunkPos chunkcoordintpair = chunk.getPos(); + boolean flag = this.isRaining(); + int j = chunkcoordintpair.getMinBlockX(); +@@ -863,7 +859,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. + ProfilerFiller gameprofilerfiller = this.getProfiler(); + + gameprofilerfiller.push("thunder"); +- if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder ++ if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && simpleRandom.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder // Paper - optimise random ticking + BlockPos blockposition = this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15)); + + if (this.isRainingAt(blockposition)) { +@@ -895,7 +891,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. + + if (!this.paperConfig().environment.disableIceAndSnow) { // Paper - Option to disable ice and snow + for (int l = 0; l < randomTickSpeed; ++l) { +- if (this.random.nextInt(48) == 0) { ++ if (simpleRandom.nextInt(48) == 0) { // Paper - optimise random ticking + this.tickPrecipitation(this.getBlockRandomPos(j, 0, k, 15)); + } + } +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 5e2c4969e77c669acbb4a13c07033cb267c3d586..f1371f3b9ea0cda0cf146d876828a6823632463c 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -1475,7 +1475,7 @@ public abstract class PlayerList { + + public void setViewDistance(int viewDistance) { + this.viewDistance = viewDistance; +- this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); ++ //this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); // Paper - rewrite chunk system + Iterator iterator = this.server.getAllLevels().iterator(); + + while (iterator.hasNext()) { +diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/net/minecraft/util/BitStorage.java +index 19661e106612b8e4e152085fb398db7bd06acc23..e4e153cb8899e70273aa150b8ea26907cf68b15c 100644 +--- a/src/main/java/net/minecraft/util/BitStorage.java ++++ b/src/main/java/net/minecraft/util/BitStorage.java +@@ -24,15 +24,15 @@ public interface BitStorage extends ca.spottedleaf.moonrise.patches.block_counti + // Paper start - block counting + // provide default impl in case mods implement this... + @Override +- public default it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> moonrise$countEntries() { +- final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(); ++ public default it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> moonrise$countEntries() { ++ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(); + + final int size = this.getSize(); + for (int index = 0; index < size; ++index) { + final int paletteIdx = this.get(index); + ret.computeIfAbsent(paletteIdx, (final int key) -> { +- return new it.unimi.dsi.fastutil.ints.IntArrayList(); +- }).add(index); ++ return new it.unimi.dsi.fastutil.shorts.ShortArrayList(); ++ }).add((short)index); + } + + return ret; +diff --git a/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java b/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java +index 61dee55417bc802e25b9ba2f271d32d8c12844a9..a8a260a3caaa8e5004069b833ecc8b17b2fc8db5 100644 +--- a/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java ++++ b/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java +@@ -7,7 +7,7 @@ import java.util.Iterator; + import javax.annotation.Nullable; + import net.minecraft.core.IdMap; + +-public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K> { ++public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<K> { // Paper - optimise palette reads + private static final int NOT_FOUND = -1; + private static final Object EMPTY_SLOT = null; + private static final float LOADFACTOR = 0.8F; +@@ -17,6 +17,16 @@ public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K> { + private int nextId; + private int size; + ++ // Paper start - optimise palette reads ++ private ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<K> reference; ++ ++ @Override ++ public final K[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<K> src) { ++ this.reference = src; ++ return this.byId; ++ } ++ // Paper end - optimise palette reads ++ + private CrudeIncrementalIntIdentityHashBiMap(int size) { + this.keys = (K[])(new Object[size]); + this.values = new int[size]; +@@ -88,6 +98,12 @@ public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K> { + this.byId = crudeIncrementalIntIdentityHashBiMap.byId; + this.nextId = crudeIncrementalIntIdentityHashBiMap.nextId; + this.size = crudeIncrementalIntIdentityHashBiMap.size; ++ // Paper start - optimise palette reads ++ final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<K> ref = this.reference; ++ if (ref != null) { ++ ref.moonrise$setPalette(this.byId); ++ } ++ // Paper end - optimise palette reads + } + + public void addMapping(K value, int id) { +diff --git a/src/main/java/net/minecraft/util/SimpleBitStorage.java b/src/main/java/net/minecraft/util/SimpleBitStorage.java +index 8acf2f2491a8d9d13392c5e89b2bd5c9918285e1..d99ec470b4653beab630999a5b2c1a6428b20c38 100644 +--- a/src/main/java/net/minecraft/util/SimpleBitStorage.java ++++ b/src/main/java/net/minecraft/util/SimpleBitStorage.java +@@ -208,6 +208,20 @@ public class SimpleBitStorage implements BitStorage { + private final int divideAdd; private final long divideAddUnsigned; // Paper - Perf: Optimize SimpleBitStorage + private final int divideShift; + ++ // Paper start - optimise bitstorage read/write operations ++ private static final int[] BETTER_MAGIC = new int[33]; ++ static { ++ // 20 bits of precision ++ // since index is always [0, 4095] (i.e 12 bits), multiplication by a magic value here (20 bits) ++ // fits exactly in an int and allows us to use integer arithmetic ++ for (int bits = 1; bits < BETTER_MAGIC.length; ++bits) { ++ BETTER_MAGIC[bits] = (int)ca.spottedleaf.concurrentutil.util.IntegerUtil.getUnsignedDivisorMagic(64L / bits, 20); ++ } ++ } ++ private final int magic; ++ private final int mulBits; ++ // Paper end - optimise bitstorage read/write operations ++ + public SimpleBitStorage(int elementBits, int size, int[] data) { + this(elementBits, size); + int i = 0; +@@ -261,6 +275,13 @@ public class SimpleBitStorage implements BitStorage { + } else { + this.data = new long[j]; + } ++ // Paper start - optimise bitstorage read/write operations ++ this.magic = BETTER_MAGIC[this.bits]; ++ this.mulBits = (64 / this.bits) * this.bits; ++ if (this.size > 4096) { ++ throw new IllegalStateException("Size > 4096 not supported"); ++ } ++ // Paper end - optimise bitstorage read/write operations + } + + private int cellIndex(int index) { +@@ -273,31 +294,54 @@ public class SimpleBitStorage implements BitStorage { + public final int getAndSet(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage + //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage + //Validate.inclusiveBetween(0L, this.mask, (long)value); // Paper - Perf: Optimize SimpleBitStorage +- int i = this.cellIndex(index); +- long l = this.data[i]; +- int j = (index - i * this.valuesPerLong) * this.bits; +- int k = (int)(l >> j & this.mask); +- this.data[i] = l & ~(this.mask << j) | ((long)value & this.mask) << j; +- return k; ++ // Paper start - optimise bitstorage read/write operations ++ final int full = this.magic * index; // 20 bits of magic + 12 bits of index = barely int ++ final int divQ = full >>> 20; ++ final int divR = (full & 0xFFFFF) * this.mulBits >>> 20; ++ ++ final long[] dataArray = this.data; ++ ++ final long data = dataArray[divQ]; ++ final long mask = this.mask; ++ ++ final long write = data & ~(mask << divR) | ((long)value & mask) << divR; ++ ++ dataArray[divQ] = write; ++ ++ return (int)(data >>> divR & mask); ++ // Paper end - optimise bitstorage read/write operations + } + + @Override + public final void set(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage + //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage + //Validate.inclusiveBetween(0L, this.mask, (long)value); // Paper - Perf: Optimize SimpleBitStorage +- int i = this.cellIndex(index); +- long l = this.data[i]; +- int j = (index - i * this.valuesPerLong) * this.bits; +- this.data[i] = l & ~(this.mask << j) | ((long)value & this.mask) << j; ++ // Paper start - optimise bitstorage read/write operations ++ final int full = this.magic * index; // 20 bits of magic + 12 bits of index = barely int ++ final int divQ = full >>> 20; ++ final int divR = (full & 0xFFFFF) * this.mulBits >>> 20; ++ ++ final long[] dataArray = this.data; ++ ++ final long data = dataArray[divQ]; ++ final long mask = this.mask; ++ ++ final long write = data & ~(mask << divR) | ((long)value & mask) << divR; ++ ++ dataArray[divQ] = write; ++ // Paper end - optimise bitstorage read/write operations + } + + @Override + public final int get(int index) { // Paper - Perf: Optimize SimpleBitStorage + //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage +- int i = this.cellIndex(index); +- long l = this.data[i]; +- int j = (index - i * this.valuesPerLong) * this.bits; +- return (int)(l >> j & this.mask); ++ // Paper start - optimise bitstorage read/write operations ++ final int full = this.magic * index; // 20 bits of magic + 12 bits of index = barely int ++ final int divQ = full >>> 20; ++ final int divR = (full & 0xFFFFF) * this.mulBits >>> 20; ++ ++ return (int)(this.data[divQ] >>> divR & this.mask); ++ // Paper end - optimise bitstorage read/write operations + } + + @Override +@@ -364,35 +408,62 @@ public class SimpleBitStorage implements BitStorage { + + // Paper start - block counting + @Override +- public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> moonrise$countEntries() { ++ public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> moonrise$countEntries() { + final int valuesPerLong = this.valuesPerLong; + final int bits = this.bits; +- final long mask = this.mask; ++ final long mask = (1L << bits) - 1L; + final int size = this.size; + +- // we may be backed by global palette, so limit bits for init capacity +- final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>( +- 1 << Math.min(6, bits) +- ); +- +- int index = 0; +- +- for (long value : this.data) { +- int li = 0; +- do { +- final int paletteIdx = (int)(value & mask); +- value >>= bits; ++ if (bits <= 6) { ++ final it.unimi.dsi.fastutil.shorts.ShortArrayList[] byId = new it.unimi.dsi.fastutil.shorts.ShortArrayList[1 << bits]; ++ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1 << bits); ++ ++ int index = 0; ++ ++ for (long value : this.data) { ++ int li = 0; ++ do { ++ final int paletteIdx = (int)(value & mask); ++ value >>= bits; ++ ++li; ++ ++ final it.unimi.dsi.fastutil.shorts.ShortArrayList coords = byId[paletteIdx]; ++ if (coords != null) { ++ coords.add((short)index++); ++ continue; ++ } else { ++ final it.unimi.dsi.fastutil.shorts.ShortArrayList newCoords = new it.unimi.dsi.fastutil.shorts.ShortArrayList(64); ++ byId[paletteIdx] = newCoords; ++ newCoords.add((short)index++); ++ ret.put(paletteIdx, newCoords); ++ continue; ++ } ++ } while (li < valuesPerLong && index < size); ++ } + +- ret.computeIfAbsent(paletteIdx, (final int key) -> { +- return new it.unimi.dsi.fastutil.ints.IntArrayList(); +- }).add(index); ++ return ret; ++ } else { ++ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>( ++ 1 << 6 ++ ); ++ ++ int index = 0; ++ ++ for (long value : this.data) { ++ int li = 0; ++ do { ++ final int paletteIdx = (int)(value & mask); ++ value >>= bits; ++ ++li; ++ ++ ret.computeIfAbsent(paletteIdx, (final int key) -> { ++ return new it.unimi.dsi.fastutil.shorts.ShortArrayList(64); ++ }).add((short)index++); ++ } while (li < valuesPerLong && index < size); ++ } + +- ++li; +- ++index; +- } while (li < valuesPerLong && index < size); ++ return ret; + } +- +- return ret; + } + // Paper end - block counting + +diff --git a/src/main/java/net/minecraft/util/ZeroBitStorage.java b/src/main/java/net/minecraft/util/ZeroBitStorage.java +index 15c5164d0ef41a978c16ee317fa73e97f2480207..1f9c436a632e4f110be61cf76fcfc3b7eb80334e 100644 +--- a/src/main/java/net/minecraft/util/ZeroBitStorage.java ++++ b/src/main/java/net/minecraft/util/ZeroBitStorage.java +@@ -65,17 +65,17 @@ public class ZeroBitStorage implements BitStorage { + + // Paper start - block counting + @Override +- public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> moonrise$countEntries() { ++ public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> moonrise$countEntries() { + final int size = this.size; + +- final int[] raw = new int[size]; ++ final short[] raw = new short[size]; + for (int i = 0; i < size; ++i) { +- raw[i] = i; ++ raw[i] = (short)i; + } + +- final it.unimi.dsi.fastutil.ints.IntArrayList coordinates = it.unimi.dsi.fastutil.ints.IntArrayList.wrap(raw, size); ++ final it.unimi.dsi.fastutil.shorts.ShortArrayList coordinates = it.unimi.dsi.fastutil.shorts.ShortArrayList.wrap(raw, size); + +- final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1); ++ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1); + ret.put(0, coordinates); + return ret; + } +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 4b54d0ea31062972e68ee8fafe3cfaf68f65a5cd..379456dd5c18ca627b9cf6a473fd0355e8d839ef 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -461,6 +461,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + // Paper start - rewrite chunk system + private final boolean isHardColliding = this.moonrise$isHardCollidingUncached(); + private net.minecraft.server.level.FullChunkStatus chunkStatus; ++ private ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData; + private int sectionX = Integer.MIN_VALUE; + private int sectionY = Integer.MIN_VALUE; + private int sectionZ = Integer.MIN_VALUE; +@@ -481,6 +482,16 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.chunkStatus = status; + } + ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$getChunkData() { ++ return this.chunkData; ++ } ++ ++ @Override ++ public final void moonrise$setChunkData(final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData) { ++ this.chunkData = chunkData; ++ } ++ + @Override + public final int moonrise$getSectionX() { + return this.sectionX; +@@ -529,6 +540,54 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return this.getIndirectPassengersStream().anyMatch((entity) -> entity instanceof Player); + } + // Paper end - rewrite chunk system ++ // Paper start - optimise collisions ++ private static float[] calculateStepHeights(final AABB box, final List<VoxelShape> voxels, final List<AABB> aabbs, final float stepHeight, ++ final float collidedY) { ++ final FloatArraySet ret = new FloatArraySet(); ++ ++ for (int i = 0, len = voxels.size(); i < len; ++i) { ++ final VoxelShape shape = voxels.get(i); ++ ++ final double[] yCoords = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$rootCoordinatesY(); ++ final double yOffset = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$offsetY(); ++ ++ for (final double yUnoffset : yCoords) { ++ final double y = yUnoffset + yOffset; ++ ++ final float step = (float)(y - box.minY); ++ ++ if (step > stepHeight) { ++ break; ++ } ++ ++ if (step < 0.0f || !(step != collidedY)) { ++ continue; ++ } ++ ++ ret.add(step); ++ } ++ } ++ ++ for (int i = 0, len = aabbs.size(); i < len; ++i) { ++ final AABB shape = aabbs.get(i); ++ ++ final float step1 = (float)(shape.minY - box.minY); ++ final float step2 = (float)(shape.maxY - box.minY); ++ ++ if (!(step1 < 0.0f) && step1 != collidedY && !(step1 > stepHeight)) { ++ ret.add(step1); ++ } ++ ++ if (!(step2 < 0.0f) && step2 != collidedY && !(step2 > stepHeight)) { ++ ret.add(step2); ++ } ++ } ++ ++ final float[] steps = ret.toFloatArray(); ++ FloatArrays.unstableSort(steps); ++ return steps; ++ } ++ // Paper end - optimise collisions + // Paper start - optimise entity tracker + private net.minecraft.server.level.ChunkMap.TrackedEntity trackedEntity; + +@@ -1465,73 +1524,67 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return movement; + } + +- final Level world = this.level; +- final AABB currBoundingBox = this.getBoundingBox(); +- +- if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(currBoundingBox)) { +- return movement; +- } ++ final AABB currentBox = this.getBoundingBox(); + +- final List<AABB> potentialCollisionsBB = new ArrayList<>(); + final List<VoxelShape> potentialCollisionsVoxel = new ArrayList<>(); +- final double stepHeight = (double)this.maxUpStep(); +- final AABB collisionBox; +- final boolean onGround = this.onGround; ++ final List<AABB> potentialCollisionsBB = new ArrayList<>(); + ++ final AABB initialCollisionBox; + if (xZero & zZero) { +- if (movement.y > 0.0) { +- collisionBox = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutUpwards(currBoundingBox, movement.y); +- } else { +- collisionBox = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutDownwards(currBoundingBox, movement.y); +- } ++ // note: xZero & zZero -> collision on x/z == 0 -> no step height calculation ++ // this specifically optimises entities standing still ++ initialCollisionBox = movement.y < 0.0 ? ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutDownwards(currentBox, movement.y) : ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutUpwards(currentBox, movement.y); + } else { +- // note: xZero == false or zZero == false +- if (stepHeight > 0.0 && (onGround || (movement.y < 0.0))) { +- // don't bother getting the collisions if we don't need them. +- if (movement.y <= 0.0) { +- collisionBox = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.expandUpwards(currBoundingBox.expandTowards(movement.x, movement.y, movement.z), stepHeight); +- } else { +- collisionBox = currBoundingBox.expandTowards(movement.x, Math.max(stepHeight, movement.y), movement.z); +- } +- } else { +- collisionBox = currBoundingBox.expandTowards(movement.x, movement.y, movement.z); +- } ++ initialCollisionBox = currentBox.expandTowards(movement); + } + +- ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisions( +- world, (Entity)(Object)this, collisionBox, potentialCollisionsVoxel, potentialCollisionsBB, +- ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, +- null, null ++ final List<AABB> entityAABBs = new ArrayList<>(); ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getEntityHardCollisions( ++ this.level, (Entity)(Object)this, initialCollisionBox, entityAABBs, 0, null + ); + +- if (potentialCollisionsVoxel.isEmpty() && potentialCollisionsBB.isEmpty()) { +- return movement; +- } ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder( ++ this.level, (Entity)(Object)this, initialCollisionBox, potentialCollisionsVoxel, potentialCollisionsBB, ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, null ++ ); ++ potentialCollisionsBB.addAll(entityAABBs); ++ final Vec3 collided = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(movement, currentBox, potentialCollisionsVoxel, potentialCollisionsBB); + +- final Vec3 limitedMoveVector = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(movement, currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB); ++ final boolean collidedX = collided.x != movement.x; ++ final boolean collidedY = collided.y != movement.y; ++ final boolean collidedZ = collided.z != movement.z; + +- if (stepHeight > 0.0 +- && (onGround || (limitedMoveVector.y != movement.y && movement.y < 0.0)) +- && (limitedMoveVector.x != movement.x || limitedMoveVector.z != movement.z)) { +- Vec3 vec3d2 = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(movement.x, stepHeight, movement.z), currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB); +- final Vec3 vec3d3 = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(0.0, stepHeight, 0.0), currBoundingBox.expandTowards(movement.x, 0.0, movement.z), potentialCollisionsVoxel, potentialCollisionsBB); ++ final boolean collidedDownwards = collidedY && movement.y < 0.0; + +- if (vec3d3.y < stepHeight) { +- final Vec3 vec3d4 = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(movement.x, 0.0D, movement.z), currBoundingBox.move(vec3d3), potentialCollisionsVoxel, potentialCollisionsBB).add(vec3d3); ++ final double stepHeight; + +- if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) { +- vec3d2 = vec3d4; +- } +- } ++ if ((!collidedDownwards && !this.onGround) || (!collidedX && !collidedZ) || (stepHeight = (double)this.maxUpStep()) <= 0.0) { ++ return collided; ++ } + +- if (vec3d2.horizontalDistanceSqr() > limitedMoveVector.horizontalDistanceSqr()) { +- return vec3d2.add(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), currBoundingBox.move(vec3d2), potentialCollisionsVoxel, potentialCollisionsBB)); +- } ++ final AABB collidedYBox = collidedDownwards ? currentBox.move(0.0, collided.y, 0.0) : currentBox; ++ AABB stepRetrievalBox = collidedYBox.expandTowards(movement.x, stepHeight, movement.z); ++ if (!collidedDownwards) { ++ stepRetrievalBox = stepRetrievalBox.expandTowards(0.0, (double)-1.0E-5F, 0.0); ++ } + +- return limitedMoveVector; +- } else { +- return limitedMoveVector; ++ final List<VoxelShape> stepVoxels = new ArrayList<>(); ++ final List<AABB> stepAABBs = entityAABBs; ++ ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder( ++ this.level, (Entity)(Object)this, stepRetrievalBox, stepVoxels, stepAABBs, ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, null ++ ); ++ ++ for (final float step : calculateStepHeights(collidedYBox, stepVoxels, stepAABBs, (float)stepHeight, (float)collided.y)) { ++ final Vec3 stepResult = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(movement.x, (double)step, movement.z), collidedYBox, stepVoxels, stepAABBs); ++ if (stepResult.horizontalDistanceSqr() > collided.horizontalDistanceSqr()) { ++ return stepResult.add(0.0, collidedYBox.minY - currentBox.minY, 0.0); ++ } + } ++ ++ return collided; + // Paper end - optimise collisions + } + +@@ -2853,64 +2906,99 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return false; + } + +- final float reducedWith = this.dimensions.width() * 0.8F; +- final AABB box = AABB.ofSize(this.getEyePosition(), reducedWith, 1.0E-6D, reducedWith); ++ final double reducedWith = (double)(this.dimensions.width() * 0.8F); ++ final AABB boundingBox = AABB.ofSize(this.getEyePosition(), reducedWith, 1.0E-6D, reducedWith); ++ final Level world = this.level; + +- if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(box)) { ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(boundingBox)) { + return false; + } + +- final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos(); ++ final int minBlockX = Mth.floor(boundingBox.minX); ++ final int minBlockY = Mth.floor(boundingBox.minY); ++ final int minBlockZ = Mth.floor(boundingBox.minZ); ++ ++ final int maxBlockX = Mth.floor(boundingBox.maxX); ++ final int maxBlockY = Mth.floor(boundingBox.maxY); ++ final int maxBlockZ = Mth.floor(boundingBox.maxZ); + +- final int minX = Mth.floor(box.minX); +- final int minY = Mth.floor(box.minY); +- final int minZ = Mth.floor(box.minZ); +- final int maxX = Mth.floor(box.maxX); +- final int maxY = Mth.floor(box.maxY); +- final int maxZ = Mth.floor(box.maxZ); ++ final int minChunkX = minBlockX >> 4; ++ final int minChunkY = minBlockY >> 4; ++ final int minChunkZ = minBlockZ >> 4; + +- final net.minecraft.world.level.chunk.ChunkSource chunkProvider = this.level.getChunkSource(); ++ final int maxChunkX = maxBlockX >> 4; ++ final int maxChunkY = maxBlockY >> 4; ++ final int maxChunkZ = maxBlockZ >> 4; + +- long lastChunkKey = ChunkPos.INVALID_CHUNK_POS; +- net.minecraft.world.level.chunk.LevelChunk lastChunk = null; +- for (int fz = minZ; fz <= maxZ; ++fz) { +- tempPos.setZ(fz); +- for (int fx = minX; fx <= maxX; ++fx) { +- final int newChunkX = fx >> 4; +- final int newChunkZ = fz >> 4; +- final net.minecraft.world.level.chunk.LevelChunk chunk = lastChunkKey == (lastChunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(newChunkX, newChunkZ)) ? +- lastChunk : (lastChunk = (net.minecraft.world.level.chunk.LevelChunk)chunkProvider.getChunk(newChunkX, newChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true)); +- tempPos.setX(fx); +- for (int fy = minY; fy <= maxY; ++fy) { +- tempPos.setY(fy); ++ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(world); ++ final net.minecraft.world.level.chunk.ChunkSource chunkSource = world.getChunkSource(); ++ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); + +- final BlockState state = chunk.getBlockState(tempPos); ++ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { ++ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { ++ final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true).getSections(); + +- if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)state).moonrise$emptyCollisionShape() || !state.isSuffocating(this.level, tempPos)) { ++ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { ++ final int sectionIdx = currChunkY - minSection; ++ if (sectionIdx < 0 || sectionIdx >= sections.length) { + continue; + } +- +- // Yes, it does not use the Entity context stuff. +- final VoxelShape collisionShape = state.getCollisionShape(this.level, tempPos); +- +- if (collisionShape.isEmpty()) { ++ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx]; ++ if (section.hasOnlyAir()) { ++ // empty + continue; + } + +- final AABB toCollide = box.move(-(double)fx, -(double)fy, -(double)fz); ++ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> blocks = section.states; ++ ++ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) : 0; ++ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) : 15; ++ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) : 0; ++ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) : 15; ++ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) : 0; ++ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) : 15; ++ ++ for (int currY = minYIterate; currY <= maxYIterate; ++currY) { ++ final int blockY = currY | (currChunkY << 4); ++ mutablePos.setY(blockY); ++ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { ++ final int blockZ = currZ | (currChunkZ << 4); ++ mutablePos.setZ(blockZ); ++ for (int currX = minXIterate; currX <= maxXIterate; ++currX) { ++ final int blockX = currX | (currChunkX << 4); ++ mutablePos.setX(blockX); ++ ++ final BlockState blockState = blocks.get((currX) | (currZ << 4) | ((currY) << 8)); ++ ++ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$emptyCollisionShape() ++ || !blockState.isSuffocating(world, mutablePos)) { ++ continue; ++ } + +- final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)collisionShape).moonrise$getSingleAABBRepresentation(); +- if (singleAABB != null) { +- if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(singleAABB, toCollide)) { +- return true; +- } +- continue; +- } ++ // Yes, it does not use the Entity context stuff. ++ final VoxelShape collisionShape = blockState.getCollisionShape(world, mutablePos); ++ ++ if (collisionShape.isEmpty()) { ++ continue; ++ } + +- if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(collisionShape, toCollide)) { +- return true; ++ final AABB toCollide = boundingBox.move(-(double)blockX, -(double)blockY, -(double)blockZ); ++ ++ final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)collisionShape).moonrise$getSingleAABBRepresentation(); ++ if (singleAABB != null) { ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(singleAABB, toCollide)) { ++ return true; ++ } ++ continue; ++ } ++ ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(collisionShape, toCollide)) { ++ return true; ++ } ++ continue; ++ } ++ } + } +- continue; + } + } + } +@@ -4451,82 +4539,136 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return Mth.lerp(delta, this.yRotO, this.yRot); + } + +- public boolean updateFluidHeightAndDoFluidPushing(TagKey<Fluid> tag, double speed) { ++ // Paper start - optimise collisions ++ public boolean updateFluidHeightAndDoFluidPushing(final TagKey<Fluid> fluid, final double flowScale) { + if (this.touchingUnloadedChunk()) { + return false; +- } else { +- AABB axisalignedbb = this.getBoundingBox().deflate(0.001D); +- int i = Mth.floor(axisalignedbb.minX); +- int j = Mth.ceil(axisalignedbb.maxX); +- int k = Mth.floor(axisalignedbb.minY); +- int l = Mth.ceil(axisalignedbb.maxY); +- int i1 = Mth.floor(axisalignedbb.minZ); +- int j1 = Mth.ceil(axisalignedbb.maxZ); +- double d1 = 0.0D; +- boolean flag = this.isPushedByFluid(); +- boolean flag1 = false; +- Vec3 vec3d = Vec3.ZERO; +- int k1 = 0; +- BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos(); ++ } + +- for (int l1 = i; l1 < j; ++l1) { +- for (int i2 = k; i2 < l; ++i2) { +- for (int j2 = i1; j2 < j1; ++j2) { +- blockposition_mutableblockposition.set(l1, i2, j2); +- FluidState fluid = this.level().getFluidState(blockposition_mutableblockposition); ++ final AABB boundingBox = this.getBoundingBox().deflate(1.0E-3); + +- if (fluid.is(tag)) { +- double d2 = (double) ((float) i2 + fluid.getHeight(this.level(), blockposition_mutableblockposition)); ++ final Level world = this.level; ++ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(world); + +- if (d2 >= axisalignedbb.minY) { +- flag1 = true; +- d1 = Math.max(d2 - axisalignedbb.minY, d1); +- if (flag) { +- Vec3 vec3d1 = fluid.getFlow(this.level(), blockposition_mutableblockposition); ++ final int minBlockX = Mth.floor(boundingBox.minX); ++ final int minBlockY = Math.max((minSection << 4), Mth.floor(boundingBox.minY)); ++ final int minBlockZ = Mth.floor(boundingBox.minZ); + +- if (d1 < 0.4D) { +- vec3d1 = vec3d1.scale(d1); +- } ++ // note: bounds are exclusive in Vanilla, so we subtract 1 - our loop expects bounds to be inclusive ++ final int maxBlockX = Mth.ceil(boundingBox.maxX) - 1; ++ final int maxBlockY = Math.min((ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(world) << 4) | 15, Mth.ceil(boundingBox.maxY) - 1); ++ final int maxBlockZ = Mth.ceil(boundingBox.maxZ) - 1; ++ ++ final boolean isPushable = this.isPushedByFluid(); ++ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); + +- vec3d = vec3d.add(vec3d1); +- ++k1; ++ Vec3 pushVector = Vec3.ZERO; ++ double totalPushes = 0.0; ++ double maxHeightDiff = 0.0; ++ boolean inFluid = false; ++ ++ final int minChunkX = minBlockX >> 4; ++ final int maxChunkX = maxBlockX >> 4; ++ ++ final int minChunkY = minBlockY >> 4; ++ final int maxChunkY = maxBlockY >> 4; ++ ++ final int minChunkZ = minBlockZ >> 4; ++ final int maxChunkZ = maxBlockZ >> 4; ++ ++ final net.minecraft.world.level.chunk.ChunkSource chunkSource = world.getChunkSource(); ++ ++ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { ++ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { ++ final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, false).getSections(); ++ ++ // bound y ++ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { ++ final int sectionIdx = currChunkY - minSection; ++ if (sectionIdx < 0 || sectionIdx >= sections.length) { ++ continue; ++ } ++ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx]; ++ if (section.hasOnlyAir()) { ++ // empty ++ continue; ++ } ++ ++ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> blocks = section.states; ++ ++ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) : 0; ++ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) : 15; ++ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) : 0; ++ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) : 15; ++ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) : 0; ++ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) : 15; ++ ++ for (int currY = minYIterate; currY <= maxYIterate; ++currY) { ++ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { ++ for (int currX = minXIterate; currX <= maxXIterate; ++currX) { ++ final FluidState fluidState = blocks.get((currX) | (currZ << 4) | ((currY) << 8)).getFluidState(); ++ ++ if (fluidState.isEmpty() || !fluidState.is(fluid)) { ++ continue; + } +- // CraftBukkit start - store last lava contact location +- if (tag == FluidTags.LAVA) { +- this.lastLavaContact = blockposition_mutableblockposition.immutable(); ++ ++ mutablePos.set(currX | (currChunkX << 4), currY | (currChunkY << 4), currZ | (currChunkZ << 4)); ++ ++ final double height = (double)((float)mutablePos.getY() + fluidState.getHeight(world, mutablePos)); ++ final double diff = height - boundingBox.minY; ++ ++ if (diff < 0.0) { ++ continue; ++ } ++ ++ inFluid = true; ++ maxHeightDiff = Math.max(maxHeightDiff, diff); ++ ++ if (!isPushable) { ++ continue; ++ } ++ ++ ++totalPushes; ++ ++ final Vec3 flow = fluidState.getFlow(world, mutablePos); ++ ++ if (diff < 0.4) { ++ pushVector = pushVector.add(flow.scale(diff)); ++ } else { ++ pushVector = pushVector.add(flow); + } +- // CraftBukkit end + } + } + } + } + } ++ } + +- if (vec3d.length() > 0.0D) { +- if (k1 > 0) { +- vec3d = vec3d.scale(1.0D / (double) k1); +- } ++ this.fluidHeight.put(fluid, maxHeightDiff); + +- if (!(this instanceof Player)) { +- vec3d = vec3d.normalize(); +- } ++ if (pushVector.lengthSqr() == 0.0) { ++ return inFluid; ++ } + +- Vec3 vec3d2 = this.getDeltaMovement(); ++ // note: totalPushes != 0 as pushVector != 0 ++ pushVector = pushVector.scale(1.0 / totalPushes); ++ final Vec3 currMovement = this.getDeltaMovement(); + +- vec3d = vec3d.scale(speed); +- double d3 = 0.003D; ++ if (!((Entity)(Object)this instanceof Player)) { ++ pushVector = pushVector.normalize(); ++ } + +- if (Math.abs(vec3d2.x) < 0.003D && Math.abs(vec3d2.z) < 0.003D && vec3d.length() < 0.0045000000000000005D) { +- vec3d = vec3d.normalize().scale(0.0045000000000000005D); +- } ++ pushVector = pushVector.scale(flowScale); ++ if (Math.abs(currMovement.x) < 0.003 && Math.abs(currMovement.z) < 0.003 && pushVector.length() < 0.0045000000000000005) { ++ pushVector = pushVector.normalize().scale(0.0045000000000000005); ++ } + +- this.setDeltaMovement(this.getDeltaMovement().add(vec3d)); +- } ++ this.setDeltaMovement(currMovement.add(pushVector)); + +- this.fluidHeight.put(tag, d1); +- return flag1; +- } ++ // note: inFluid = true here as pushVector != 0 ++ return true; + } ++ // Paper end - optimise collisions + + public boolean touchingUnloadedChunk() { + AABB axisalignedbb = this.getBoundingBox().inflate(1.0D); +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +index a908bf1dc5e821dcf6981a8c21076fb0bdc6516d..42642b49a4ed6d06f52e4076dfb745b32a964a34 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +@@ -73,8 +73,7 @@ public class PoiManager extends SectionStorage<PoiSection> implements ca.spotted + + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); + +- final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager; +- final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); ++ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getPoiChunkIfLoaded(chunkX, chunkZ, true); + + return ret == null ? Optional.empty() : ret.getSectionForVanilla(chunkY); + } +@@ -128,9 +127,13 @@ public class PoiManager extends SectionStorage<PoiSection> implements ca.spotted + public final void moonrise$onUnload(final long coordinate) { // Paper - rewrite chunk system + final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(coordinate); + final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(coordinate); ++ ++ final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world); ++ final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world); ++ + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Unloading poi chunk off-main"); +- for (int section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) { +- final long sectionPos = SectionPos.asLong(chunkX, section, chunkZ); ++ for (int sectionY = minY; sectionY <= maxY; ++sectionY) { ++ final long sectionPos = SectionPos.asLong(chunkX, sectionY, chunkZ); + this.updateDistanceTracking(sectionPos); + } + } +@@ -139,8 +142,12 @@ public class PoiManager extends SectionStorage<PoiSection> implements ca.spotted + public final void moonrise$loadInPoiChunk(final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk poiChunk) { + final int chunkX = poiChunk.chunkX; + final int chunkZ = poiChunk.chunkZ; ++ ++ final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world); ++ final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world); ++ + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Loading poi chunk off-main"); +- for (int sectionY = this.levelHeightAccessor.getMinSection(); sectionY < this.levelHeightAccessor.getMaxSection(); ++sectionY) { ++ for (int sectionY = minY; sectionY <= maxY; ++sectionY) { + final PoiSection section = poiChunk.getSection(sectionY); + if (section != null && !((ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection)section).moonrise$isEmpty()) { + this.onSectionLoad(SectionPos.asLong(chunkX, sectionY, chunkZ)); +@@ -166,22 +173,15 @@ public class PoiManager extends SectionStorage<PoiSection> implements ca.spotted + + @Override + public final net.minecraft.nbt.CompoundTag moonrise$read(final int chunkX, final int chunkZ) throws java.io.IOException { +- if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) { +- return ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.loadData( +- this.world, chunkX, chunkZ, ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.POI_DATA, +- ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread() +- ); +- } +- return this.moonrise$getRegionStorage().read(new ChunkPos(chunkX, chunkZ)); ++ return ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.loadData( ++ this.world, chunkX, chunkZ, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionFileType.POI_DATA, ++ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.getIOBlockingPriorityForCurrentThread() ++ ); + } + + @Override + public final void moonrise$write(final int chunkX, final int chunkZ, final net.minecraft.nbt.CompoundTag data) throws java.io.IOException { +- if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) { +- ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.scheduleSave(this.world, chunkX, chunkZ, data, ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.POI_DATA); +- return; +- } +- this.moonrise$getRegionStorage().write(new ChunkPos(chunkX, chunkZ), data); ++ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.scheduleSave(this.world, chunkX, chunkZ, data, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionFileType.POI_DATA); + } + // Paper end - rewrite chunk system + +@@ -359,8 +359,10 @@ public class PoiManager extends SectionStorage<PoiSection> implements ca.spotted + } + + public int sectionsToVillage(SectionPos pos) { +- this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system +- return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(pos))); // Paper - rewrite chunk system ++ // Paper start - rewrite chunk system ++ this.villageDistanceTracker.propagateUpdates(); ++ return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(pos))); ++ // Paper end - rewrite chunk system + } + + boolean isVillageCenter(long pos) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java +index 11e895d837794d79a76303b912092096bd7d07a8..f1458b0153f6a93875f2e439759324985fa13556 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java +@@ -43,7 +43,7 @@ public class PoiSection implements ca.spottedleaf.moonrise.patches.chunk_system. + } + + // Paper start - rewrite chunk system +- private final Optional<PoiSection> noAllocOptional = Optional.of((PoiSection)(Object)this);; ++ private final Optional<PoiSection> noAllocOptional = Optional.of((PoiSection)(Object)this); + + @Override + public final boolean moonrise$isEmpty() { +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 507671476c3d2d92a2fdb05be24443af27d26dcf..41ecc30c1e38bf84655464d539ced43549b35d20 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -280,26 +280,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + } + // Paper end - rewrite chunk system + // Paper start - optimise collisions +- private final int minSection; +- private final int maxSection; +- +- @Override +- public final int moonrise$getMinSection() { +- return this.minSection; +- } +- +- @Override +- public final int moonrise$getMaxSection() { +- return this.maxSection; +- } +- + /** + * Route to faster lookup. + * See {@link EntityGetter#isUnobstructed(Entity, VoxelShape)} for expected behavior + * @author Spottedleaf + */ + @Override +- public final boolean isUnobstructed(final Entity entity) { ++ public boolean isUnobstructed(final Entity entity) { + final AABB boundingBox = entity.getBoundingBox(); + if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(boundingBox)) { + return false; +@@ -329,7 +316,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + final Vec3 to = clipContext.getTo(); + final Vec3 from = clipContext.getFrom(); + +- return net.minecraft.world.phys.BlockHitResult.miss(to, Direction.getNearest(from.x - to.x, from.y - to.y, from.z - to.z), BlockPos.containing(to.x, to.y, to.z)); ++ return net.minecraft.world.phys.BlockHitResult.miss(to, Direction.getApproximateNearest(from.x - to.x, from.y - to.y, from.z - to.z), BlockPos.containing(to.x, to.y, to.z)); + } + + private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState(); +@@ -383,7 +370,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + int lastChunkY = Integer.MIN_VALUE; + int lastChunkZ = Integer.MIN_VALUE; + +- final int minSection = ((ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel)level).moonrise$getMinSection(); ++ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(level); + + for (;;) { + currPos.set(currX, currY, currZ); +@@ -466,7 +453,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + * @author Spottedleaf + */ + @Override +- public final net.minecraft.world.phys.BlockHitResult clip(final ClipContext clipContext) { ++ public net.minecraft.world.phys.BlockHitResult clip(final ClipContext clipContext) { + // can only do this in this class, as not everything that implements BlockGetter can retrieve chunks + return fastClip(clipContext.getFrom(), clipContext.getTo(), (Level)(Object)this, clipContext); + } +@@ -476,7 +463,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + * @author Spottedleaf + */ + @Override +- public final boolean collidesWithSuffocatingBlock(final Entity entity, final AABB box) { ++ public boolean collidesWithSuffocatingBlock(final Entity entity, final AABB box) { + return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder((Level)(Object)this, entity, box, null, null, + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_ONLY, + (final BlockState state, final BlockPos pos) -> { +@@ -502,8 +489,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + * @author Spottedleaf + */ + @Override +- public final java.util.Optional<net.minecraft.world.phys.Vec3> findFreePosition(final Entity entity, final VoxelShape boundsShape, final Vec3 fromPosition, +- final double rangeX, final double rangeY, final double rangeZ) { ++ public java.util.Optional<net.minecraft.world.phys.Vec3> findFreePosition(final Entity entity, final VoxelShape boundsShape, final Vec3 fromPosition, ++ final double rangeX, final double rangeY, final double rangeZ) { + if (boundsShape.isEmpty()) { + return java.util.Optional.empty(); + } +@@ -562,103 +549,139 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + * @author Spottedleaf + */ + @Override +- public final java.util.Optional<net.minecraft.core.BlockPos> findSupportingBlock(final Entity entity, final AABB aabb) { ++ public java.util.Optional<net.minecraft.core.BlockPos> findSupportingBlock(final Entity entity, final AABB aabb) { ++ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection((Level)(Object)this); ++ + final int minBlockX = Mth.floor(aabb.minX - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1; + final int maxBlockX = Mth.floor(aabb.maxX + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1; + +- final int minBlockY = Mth.floor(aabb.minY - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1; +- final int maxBlockY = Mth.floor(aabb.maxY + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1; ++ final int minBlockY = Math.max((minSection << 4) - 1, Mth.floor(aabb.minY - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1); ++ final int maxBlockY = Math.min((ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection((Level)(Object)this) << 4) + 16, Mth.floor(aabb.maxY + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1); + + final int minBlockZ = Mth.floor(aabb.minZ - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1; + final int maxBlockZ = Mth.floor(aabb.maxZ + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1; + +- ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext collisionContext = null; +- +- final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); ++ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); ++ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext collisionShape = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(entity); + BlockPos selected = null; + double selectedDistance = Double.MAX_VALUE; +- + final Vec3 entityPos = entity.position(); + +- LevelChunk lastChunk = null; +- int lastChunkX = Integer.MIN_VALUE; +- int lastChunkZ = Integer.MIN_VALUE; ++ // special cases: ++ if (minBlockY > maxBlockY) { ++ // no point in checking ++ return java.util.Optional.empty(); ++ } + +- final ChunkSource chunkSource = this.getChunkSource(); ++ final int minChunkX = minBlockX >> 4; ++ final int maxChunkX = maxBlockX >> 4; + +- for (int currZ = minBlockZ; currZ <= maxBlockZ; ++currZ) { +- pos.setZ(currZ); +- for (int currX = minBlockX; currX <= maxBlockX; ++currX) { +- pos.setX(currX); ++ final int minChunkY = minBlockY >> 4; ++ final int maxChunkY = maxBlockY >> 4; + +- final int newChunkX = currX >> 4; +- final int newChunkZ = currZ >> 4; ++ final int minChunkZ = minBlockZ >> 4; ++ final int maxChunkZ = maxBlockZ >> 4; + +- if (((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)) != 0) { +- lastChunkX = newChunkX; +- lastChunkZ = newChunkZ; +- lastChunk = (LevelChunk)chunkSource.getChunk(newChunkX, newChunkZ, ChunkStatus.FULL, false); +- } ++ final ChunkSource chunkSource = this.getChunkSource(); ++ ++ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { ++ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { ++ final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, false); + +- if (lastChunk == null) { ++ if (chunk == null) { + continue; + } +- for (int currY = minBlockY; currY <= maxBlockY; ++currY) { +- int edgeCount = ((currX == minBlockX || currX == maxBlockX) ? 1 : 0) + +- ((currY == minBlockY || currY == maxBlockY) ? 1 : 0) + +- ((currZ == minBlockZ || currZ == maxBlockZ) ? 1 : 0); +- if (edgeCount == 3) { +- continue; +- } + +- pos.setY(currY); ++ final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections(); + +- final double distance = pos.distToCenterSqr(entityPos); +- if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(pos) >= 0)) { ++ // bound y ++ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { ++ final int sectionIdx = currChunkY - minSection; ++ if (sectionIdx < 0 || sectionIdx >= sections.length) { + continue; + } +- +- final BlockState state = ((ca.spottedleaf.moonrise.patches.chunk_getblock.GetBlockChunk)lastChunk).moonrise$getBlock(currX, currY, currZ); +- if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)state).moonrise$emptyCollisionShape()) { ++ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx]; ++ if (section.hasOnlyAir()) { ++ // empty + continue; + } + +- VoxelShape blockCollision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)state).moonrise$getConstantCollisionShape(); +- +- if ((edgeCount != 1 || state.hasLargeCollisionShape()) && (edgeCount != 2 || state.getBlock() == Blocks.MOVING_PISTON)) { +- if (collisionContext == null) { +- collisionContext = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(entity); +- } +- +- if (blockCollision == null) { +- blockCollision = state.getCollisionShape((Level)(Object)this, pos, collisionContext); +- } +- +- if (blockCollision.isEmpty()) { +- continue; +- } +- +- // avoid VoxelShape#move by shifting the entity collision shape instead +- final AABB shiftedAABB = aabb.move(-(double)currX, -(double)currY, -(double)currZ); +- +- final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation(); +- if (singleAABB != null) { +- if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(singleAABB, shiftedAABB)) { +- continue; ++ final boolean hasSpecial = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$hasSpecialCollidingBlocks(); ++ final int sectionAdjust = !hasSpecial ? 1 : 0; ++ ++ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> blocks = section.states; ++ ++ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0; ++ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15; ++ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) + sectionAdjust : 0; ++ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) - sectionAdjust : 15; ++ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) + sectionAdjust : 0; ++ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) - sectionAdjust : 15; ++ ++ for (int currY = minYIterate; currY <= maxYIterate; ++currY) { ++ final int blockY = currY | (currChunkY << 4); ++ mutablePos.setY(blockY); ++ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { ++ final int blockZ = currZ | (currChunkZ << 4); ++ mutablePos.setZ(blockZ); ++ for (int currX = minXIterate; currX <= maxXIterate; ++currX) { ++ final int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8); ++ final int blockX = currX | (currChunkX << 4); ++ mutablePos.setX(blockX); ++ ++ final int edgeCount = hasSpecial ? ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + ++ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + ++ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0) : 0; ++ if (edgeCount == 3) { ++ continue; ++ } ++ ++ final double distance = mutablePos.distToCenterSqr(entityPos); ++ if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(mutablePos) >= 0)) { ++ continue; ++ } ++ ++ final BlockState blockData = blocks.get(localBlockIndex); ++ ++ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$emptyContextCollisionShape()) { ++ continue; ++ } ++ ++ VoxelShape blockCollision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$getConstantContextCollisionShape(); ++ ++ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) { ++ if (blockCollision == null) { ++ blockCollision = blockData.getCollisionShape((Level)(Object)this, mutablePos, collisionShape); ++ ++ if (blockCollision.isEmpty()) { ++ continue; ++ } ++ } ++ ++ // avoid VoxelShape#move by shifting the entity collision shape instead ++ final AABB shiftedAABB = aabb.move(-(double)blockX, -(double)blockY, -(double)blockZ); ++ ++ final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation(); ++ if (singleAABB != null) { ++ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(singleAABB, shiftedAABB)) { ++ continue; ++ } ++ ++ selected = mutablePos.immutable(); ++ selectedDistance = distance; ++ continue; ++ } ++ ++ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(blockCollision, shiftedAABB)) { ++ continue; ++ } ++ ++ selected = mutablePos.immutable(); ++ selectedDistance = distance; ++ continue; ++ } + } +- +- selected = pos.immutable(); +- selectedDistance = distance; +- continue; + } +- +- if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(blockCollision, shiftedAABB)) { +- continue; +- } +- +- selected = pos.immutable(); +- selectedDistance = distance; +- continue; + } + } + } +@@ -667,6 +690,74 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + return java.util.Optional.ofNullable(selected); + } + // Paper end - optimise collisions ++ // Paper start - getblock optimisations - cache world height/sections ++ private final int minY; ++ private final int height; ++ private final int maxY; ++ private final int minSectionY; ++ private final int maxSectionY; ++ private final int sectionsCount; ++ ++ @Override ++ public int getMinY() { ++ return this.minY; ++ } ++ ++ @Override ++ public int getHeight() { ++ return this.height; ++ } ++ ++ @Override ++ public int getMaxY() { ++ return this.maxY; ++ } ++ ++ @Override ++ public int getSectionsCount() { ++ return this.sectionsCount; ++ } ++ ++ @Override ++ public int getMinSectionY() { ++ return this.minSectionY; ++ } ++ ++ @Override ++ public int getMaxSectionY() { ++ return this.maxSectionY; ++ } ++ ++ @Override ++ public boolean isInsideBuildHeight(final int blockY) { ++ return blockY >= this.minY && blockY <= this.maxY; ++ } ++ ++ @Override ++ public boolean isOutsideBuildHeight(final BlockPos pos) { ++ return this.isOutsideBuildHeight(pos.getY()); ++ } ++ ++ @Override ++ public boolean isOutsideBuildHeight(final int blockY) { ++ return blockY < this.minY || blockY > this.maxY; ++ } ++ ++ @Override ++ public int getSectionIndex(final int blockY) { ++ return (blockY >> 4) - this.minSectionY; ++ } ++ ++ @Override ++ public int getSectionIndexFromSectionY(final int sectionY) { ++ return sectionY - this.minSectionY; ++ } ++ ++ @Override ++ public int getSectionYFromSectionIndex(final int sectionIdx) { ++ return sectionIdx + this.minSectionY; ++ } ++ // Paper end - getblock optimisations - cache world height/sections + // Paper start - optimise random ticking + @Override + public abstract Holder<Biome> getUncachedNoiseBiome(final int x, final int y, final int z); +@@ -685,6 +776,15 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + // Paper end - optimise random ticking + + protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, RegistryAccess iregistrycustom, Holder<DimensionType> holder, Supplier<ProfilerFiller> supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - create paper world config & Anti-Xray ++ // Paper start - getblock optimisations - cache world height/sections ++ final DimensionType dimType = holder.value(); ++ this.minY = dimType.minY(); ++ this.height = dimType.height(); ++ this.maxY = this.minY + this.height - 1; ++ this.minSectionY = this.minY >> 4; ++ this.maxSectionY = this.maxY >> 4; ++ this.sectionsCount = this.maxSectionY - this.minSectionY + 1; ++ // Paper end - getblock optimisations - cache world height/sections + this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot + this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config + this.generator = gen; +diff --git a/src/main/java/net/minecraft/world/level/biome/BiomeManager.java b/src/main/java/net/minecraft/world/level/biome/BiomeManager.java +index 01352cc83b25eb0e30b7e0ff521fc7c1b3d5155b..90f8360f547ce709fd13ee34f8e67d8bfa94b498 100644 +--- a/src/main/java/net/minecraft/world/level/biome/BiomeManager.java ++++ b/src/main/java/net/minecraft/world/level/biome/BiomeManager.java +@@ -98,8 +98,7 @@ public class BiomeManager { + } + + private static double getFiddle(long l) { +- double d = (double)Math.floorMod(l >> 24, 1024) / 1024.0; +- return (d - 0.5) * 0.9; ++ return (double)(((l >> 24) & (1024 - 1)) - (1024/2)) * (0.9 / 1024.0); // Paper - avoid floorMod, fp division, and fp subtraction + } + + public interface NoiseBiomeSource { +diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +index d0109633e8bdf109cfc9178963d7b6cf92f8b189..ad9c85c19146970371106050ec009a96075769c1 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java ++++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +@@ -796,18 +796,12 @@ public abstract class BlockBehaviour implements FeatureElement { + private boolean isRandomlyTicking; + + // Paper start - rewrite chunk system +- private int opacityIfCached; + private boolean isConditionallyFullOpaque; + + @Override + public final boolean starlight$isConditionallyFullOpaque() { + return this.isConditionallyFullOpaque; + } +- +- @Override +- public final int starlight$getOpacityIfCached() { +- return this.opacityIfCached; +- } + // Paper end - rewrite chunk system + // Paper start - optimise collisions + private static final int RANDOM_OFFSET = 704237939; +@@ -817,16 +811,22 @@ public abstract class BlockBehaviour implements FeatureElement { + private final int id2 = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement() + RANDOM_OFFSET) + RANDOM_OFFSET); + private boolean occludesFullBlock; + private boolean emptyCollisionShape; ++ private boolean emptyConstantCollisionShape; + private VoxelShape constantCollisionShape; +- private AABB constantAABBCollision; + +- private static void initCaches(final VoxelShape shape) { ++ private static void initCaches(final VoxelShape shape, final boolean neighbours) { + ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$isFullBlock(); + ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$occludesFullBlock(); + shape.toAabbs(); + if (!shape.isEmpty()) { + shape.bounds(); + } ++ if (neighbours) { ++ for (final Direction direction : DIRECTIONS_CACHED) { ++ initCaches(((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$getFaceShapeClamped(direction), false); ++ initCaches(shape.getFaceShape(direction), false); ++ } ++ } + } + + @Override +@@ -844,6 +844,11 @@ public abstract class BlockBehaviour implements FeatureElement { + return this.emptyCollisionShape; + } + ++ @Override ++ public final boolean moonrise$emptyContextCollisionShape() { ++ return this.emptyConstantCollisionShape; ++ } ++ + @Override + public final int moonrise$uniqueId1() { + return this.id1; +@@ -855,14 +860,9 @@ public abstract class BlockBehaviour implements FeatureElement { + } + + @Override +- public final VoxelShape moonrise$getConstantCollisionShape() { ++ public final VoxelShape moonrise$getConstantContextCollisionShape() { + return this.constantCollisionShape; + } +- +- @Override +- public final AABB moonrise$getConstantCollisionAABB() { +- return this.constantAABBCollision; +- } + // Paper end - optimise collisions + + protected BlockStateBase(Block block, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<BlockState> codec) { +@@ -931,39 +931,37 @@ public abstract class BlockBehaviour implements FeatureElement { + this.legacySolid = this.calculateSolid(); + // Paper start - rewrite chunk system + this.isConditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion; +- this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque ? -1 : this.cache.lightBlock; + // Paper end - rewrite chunk system + // Paper start - optimise collisions + if (this.cache != null) { + final VoxelShape collisionShape = this.cache.collisionShape; + try { + this.constantCollisionShape = this.getCollisionShape(null, null, null); +- this.constantAABBCollision = this.constantCollisionShape == null ? null : ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this.constantCollisionShape).moonrise$getSingleAABBRepresentation(); + } catch (final Throwable throwable) { + this.constantCollisionShape = null; +- this.constantAABBCollision = null; + } + this.occludesFullBlock = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)collisionShape).moonrise$occludesFullBlock(); + this.emptyCollisionShape = collisionShape.isEmpty(); ++ this.emptyConstantCollisionShape = this.constantCollisionShape != null && this.constantCollisionShape.isEmpty(); + // init caches +- initCaches(collisionShape); +- if (collisionShape != Shapes.empty() && collisionShape != Shapes.block()) { +- for (final Direction direction : DIRECTIONS_CACHED) { +- // initialise the directional face shape cache as well +- final VoxelShape shape = Shapes.getFaceShape(collisionShape, direction); +- initCaches(shape); +- } +- } +- if (this.cache.occlusionShapes != null) { +- for (final VoxelShape shape : this.cache.occlusionShapes) { +- initCaches(shape); +- } ++ initCaches(collisionShape, true); ++ if (this.constantCollisionShape != null) { ++ initCaches(this.constantCollisionShape, true); + } + } else { + this.occludesFullBlock = false; + this.emptyCollisionShape = false; ++ this.emptyConstantCollisionShape = false; + this.constantCollisionShape = null; +- this.constantAABBCollision = null; ++ } ++ ++ if (this.occlusionShape != null) { ++ initCaches(this.occlusionShape, true); ++ } ++ if (this.occlusionShapesByFace != null) { ++ for (final VoxelShape shape : this.occlusionShapesByFace) { ++ initCaches(shape, true); ++ } + } + // Paper end - optimise collisions + } +diff --git a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java +index daedcfd867ed6171fb61bdcbded417a11c8a5b0f..97a7860b66be418399912307f8e68db9b4edf121 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java ++++ b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java +@@ -17,7 +17,7 @@ import java.util.stream.Collectors; + import javax.annotation.Nullable; + import net.minecraft.world.level.block.state.properties.Property; + +-public abstract class StateHolder<O, S> { ++public abstract class StateHolder<O, S> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccessStateHolder { // Paper - optimise blockstate property access + public static final String NAME_TAG = "Name"; + public static final String PROPERTIES_TAG = "Properties"; + public static final Function<Entry<Property<?>, Comparable<?>>, String> PROPERTY_ENTRY_TO_STRING_FUNCTION = new Function<Entry<Property<?>, Comparable<?>>, String>() { +@@ -36,14 +36,28 @@ public abstract class StateHolder<O, S> { + } + }; + protected final O owner; +- private final Reference2ObjectArrayMap<Property<?>, Comparable<?>> values; ++ private Reference2ObjectArrayMap<Property<?>, Comparable<?>> values; // Paper - optimise blockstate property access - remove final + private Table<Property<?>, Comparable<?>, S> neighbours; + protected final MapCodec<S> propertiesCodec; + ++ // Paper start - optimise blockstate property access ++ protected ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<O, S> optimisedTable; ++ protected final long tableIndex; ++ ++ @Override ++ public final long moonrise$getTableIndex() { ++ return this.tableIndex; ++ } ++ // Paper end - optimise blockstate property access ++ + protected StateHolder(O owner, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<S> codec) { + this.owner = owner; + this.values = propertyMap; + this.propertiesCodec = codec; ++ // Paper start - optimise blockstate property access ++ this.optimisedTable = new ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<>(this.values.keySet()); ++ this.tableIndex = this.optimisedTable.getIndex((StateHolder<O, S>)(Object)this); ++ // Paper end - optimise blockstate property access + } + + public <T extends Comparable<T>> S cycle(Property<T> property) { +@@ -80,20 +94,21 @@ public abstract class StateHolder<O, S> { + } + + public Collection<Property<?>> getProperties() { +- return Collections.unmodifiableCollection(this.values.keySet()); ++ return this.optimisedTable.getProperties(); // Paper - optimise blockstate property access + } + + public <T extends Comparable<T>> boolean hasProperty(Property<T> property) { +- return this.values.containsKey(property); ++ return property != null && this.optimisedTable.hasProperty(property); // Paper - optimise blockstate property access + } + + public <T extends Comparable<T>> T getValue(Property<T> property) { +- Comparable<?> comparable = this.values.get(property); +- if (comparable == null) { +- throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner); +- } else { +- return property.getValueClass().cast(comparable); ++ // Paper start - optimise blockstate property access ++ final T ret = this.optimisedTable.get(this.tableIndex, property); ++ if (ret != null) { ++ return ret; + } ++ throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner); ++ // Paper end - optimise blockstate property access + } + + public <T extends Comparable<T>> Optional<T> getOptionalValue(Property<T> property) { +@@ -102,36 +117,52 @@ public abstract class StateHolder<O, S> { + } + + public <T extends Comparable<T>, V extends T> S setValue(Property<T> property, V value) { +- Comparable<?> comparable = this.values.get(property); +- if (comparable == null) { +- throw new IllegalArgumentException("Cannot set property " + property + " as it does not exist in " + this.owner); +- } else if (comparable.equals(value)) { +- return (S)this; +- } else { +- S object = this.neighbours.get(property, value); +- if (object == null) { +- throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); +- } else { +- return object; +- } ++ // Paper start - optimise blockstate property access ++ final S ret = this.optimisedTable.set(this.tableIndex, property, value); ++ if (ret != null) { ++ return ret; + } ++ throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner); ++ // Paper end - optimise blockstate property access + } + + public <T extends Comparable<T>, V extends T> S trySetValue(Property<T> property, V value) { +- Comparable<?> comparable = this.values.get(property); +- if (comparable != null && !comparable.equals(value)) { +- S object = this.neighbours.get(property, value); +- if (object == null) { +- throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); +- } else { +- return object; +- } +- } else { +- return (S)this; ++ // Paper start - optimise blockstate property access ++ if (property == null) { ++ return (S)(StateHolder<O, S>)(Object)this; + } ++ final S ret = this.optimisedTable.trySet(this.tableIndex, property, value, (S)(StateHolder<O, S>)(Object)this); ++ if (ret != null) { ++ return ret; ++ } ++ throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner); ++ // Paper end - optimise blockstate property access + } + + public void populateNeighbours(Map<Map<Property<?>, Comparable<?>>, S> states) { ++ // Paper start - optimise blockstate property access ++ final Map<Map<Property<?>, Comparable<?>>, S> map = states; ++ if (true) { ++ if (this.optimisedTable.isLoaded()) { ++ return; ++ } ++ this.optimisedTable.loadInTable(map); ++ ++ // de-duplicate the tables ++ for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : map.entrySet()) { ++ final S value = entry.getValue(); ++ ((StateHolder<O, S>)value).optimisedTable = this.optimisedTable; ++ } ++ ++ // remove values arrays ++ for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : map.entrySet()) { ++ final S value = entry.getValue(); ++ ((StateHolder<O, S>)value).values = null; ++ } ++ ++ return; ++ } ++ // Paper end - optimise blockstate property access + if (this.neighbours != null) { + throw new IllegalStateException(); + } else { +@@ -158,7 +189,11 @@ public abstract class StateHolder<O, S> { + } + + public Map<Property<?>, Comparable<?>> getValues() { +- return this.values; ++ // Paper start - optimise blockstate property access ++ ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<O, S> table = this.optimisedTable; ++ // We have to use this.values until the table is loaded ++ return table.isLoaded() ? table.getMapView(this.tableIndex) : this.values; ++ // Paper end - optimise blockstate property access + } + + protected static <O, S extends StateHolder<O, S>> Codec<S> codec(Codec<O> codec, Function<O, S> ownerToStateFunction) { +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java +index b63116b333b6e06494091a82588acfb639bddb71..054a2569b5b103835facef1e34867c60884e5c29 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java +@@ -4,11 +4,21 @@ import com.google.common.collect.ImmutableSet; + import java.util.Collection; + import java.util.Optional; + +-public class BooleanProperty extends Property<Boolean> { ++public class BooleanProperty extends Property<Boolean> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<Boolean> { // Paper - optimise blockstate property access + private final ImmutableSet<Boolean> values = ImmutableSet.of(true, false); + ++ // Paper start - optimise blockstate property access ++ private static final Boolean[] BY_ID = new Boolean[]{ Boolean.FALSE, Boolean.TRUE }; ++ ++ @Override ++ public final int moonrise$getIdFor(final Boolean value) { ++ return value.booleanValue() ? 1 : 0; ++ } ++ // Paper end - optimise blockstate property access ++ + protected BooleanProperty(String name) { + super(name, Boolean.class); ++ this.moonrise$setById(BY_ID); // Paper - optimise blockstate property access + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java +index 3097298fe356df98967cf4bdeaaede69dfe8a441..c23cb4ac167799e61ab9a4e1f43a5722d596332e 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java +@@ -11,10 +11,38 @@ import java.util.function.Predicate; + import java.util.stream.Collectors; + import net.minecraft.util.StringRepresentable; + +-public class EnumProperty<T extends Enum<T> & StringRepresentable> extends Property<T> { ++public class EnumProperty<T extends Enum<T> & StringRepresentable> extends Property<T> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<T> { // Paper - optimise blockstate property access + private final ImmutableSet<T> values; + private final Map<String, T> names = Maps.newHashMap(); + ++ // Paper start - optimise blockstate property access ++ private int[] idLookupTable; ++ ++ @Override ++ public final int moonrise$getIdFor(final T value) { ++ final Class<T> target = this.getValueClass(); ++ return ((value.getClass() != target && value.getDeclaringClass() != target)) ? -1 : this.idLookupTable[value.ordinal()]; ++ } ++ ++ private void init() { ++ final Collection<T> values = this.getPossibleValues(); ++ final Class<T> clazz = this.getValueClass(); ++ ++ int id = 0; ++ this.idLookupTable = new int[clazz.getEnumConstants().length]; ++ Arrays.fill(this.idLookupTable, -1); ++ final T[] byId = (T[])java.lang.reflect.Array.newInstance(clazz, values.size()); ++ ++ for (final T value : values) { ++ final int valueId = id++; ++ this.idLookupTable[value.ordinal()] = valueId; ++ byId[valueId] = value; ++ } ++ ++ this.moonrise$setById(byId); ++ } ++ // Paper end - optimise blockstate property access ++ + protected EnumProperty(String name, Class<T> type, Collection<T> values) { + super(name, type); + this.values = ImmutableSet.copyOf(values); +@@ -27,6 +55,7 @@ public class EnumProperty<T extends Enum<T> & StringRepresentable> extends Prope + + this.names.put(string, enum_); + } ++ this.init(); // Paper - optimise blockstate property access + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java +index 3a850321a4bcc68058483b5fd53e829c425a68af..4a21ec538d7410159bb26b9bf3701605b5ef317f 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java +@@ -6,11 +6,33 @@ import java.util.Collection; + import java.util.Optional; + import java.util.Set; + +-public class IntegerProperty extends Property<Integer> { ++public class IntegerProperty extends Property<Integer> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<Integer> { // Paper - optimise blockstate property access + private final ImmutableSet<Integer> values; + public final int min; + public final int max; + ++ // Paper start - optimise blockstate property access ++ @Override ++ public final int moonrise$getIdFor(final Integer value) { ++ final int val = value.intValue(); ++ final int ret = val - this.min; ++ ++ return ret | ((this.max - ret) >> 31); ++ } ++ ++ private void init() { ++ final int min = this.min; ++ final int max = this.max; ++ ++ final Integer[] byId = new Integer[max - min + 1]; ++ for (int i = min; i <= max; ++i) { ++ byId[i - min] = Integer.valueOf(i); ++ } ++ ++ this.moonrise$setById(byId); ++ } ++ // Paper end - optimise blockstate property access ++ + protected IntegerProperty(String name, int min, int max) { + super(name, Integer.class); + if (min < 0) { +@@ -28,6 +50,7 @@ public class IntegerProperty extends Property<Integer> { + + this.values = ImmutableSet.copyOf(set); + } ++ this.init(); // Paper - optimise blockstate property access + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java +index 9055f15af0cae55effa6942913a9d7edf3857e07..e63a7541f43e5c8f92f348c4e49756b33698c668 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java +@@ -10,7 +10,7 @@ import java.util.stream.Stream; + import javax.annotation.Nullable; + import net.minecraft.world.level.block.state.StateHolder; + +-public abstract class Property<T extends Comparable<T>> { ++public abstract class Property<T extends Comparable<T>> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<T> { // Paper - optimise blockstate property access + private final Class<T> clazz; + private final String name; + @Nullable +@@ -24,9 +24,38 @@ public abstract class Property<T extends Comparable<T>> { + ); + private final Codec<Property.Value<T>> valueCodec = this.codec.xmap(this::value, Property.Value::value); + ++ // Paper start - optimise blockstate property access ++ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger(); ++ private final int id; ++ private T[] byId; ++ ++ @Override ++ public final int moonrise$getId() { ++ return this.id; ++ } ++ ++ @Override ++ public final T moonrise$getById(final int id) { ++ final T[] byId = this.byId; ++ return id < 0 || id >= byId.length ? null : this.byId[id]; ++ } ++ ++ @Override ++ public final void moonrise$setById(final T[] byId) { ++ if (this.byId != null) { ++ throw new IllegalStateException(); ++ } ++ this.byId = byId; ++ } ++ ++ @Override ++ public abstract int moonrise$getIdFor(final T value); ++ // Paper end - optimise blockstate property access ++ + protected Property(String name, Class<T> type) { + this.clazz = type; + this.name = name; ++ this.id = ID_GENERATOR.getAndIncrement(); // Paper - optimise blockstate property access + } + + public Property.Value<T> value(T value) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java b/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java +index c5e1040c239874dcf20b79472bf690ee7f0a9e5f..0f403e0c257b1304be2ede89b8529676dbe8c32b 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java ++++ b/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java +@@ -8,12 +8,19 @@ import net.minecraft.network.FriendlyByteBuf; + import net.minecraft.network.VarInt; + import net.minecraft.util.CrudeIncrementalIntIdentityHashBiMap; + +-public class HashMapPalette<T> implements Palette<T> { ++public class HashMapPalette<T> implements Palette<T>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads + private final IdMap<T> registry; + private final CrudeIncrementalIntIdentityHashBiMap<T> values; + private final PaletteResize<T> resizeHandler; + private final int bits; + ++ // Paper start - optimise palette reads ++ @Override ++ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> container) { ++ return ((ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T>)this.values).moonrise$getRawPalette(container); ++ } ++ // Paper end - optimise palette reads ++ + public HashMapPalette(IdMap<T> idList, int bits, PaletteResize<T> listener, List<T> entries) { + this(idList, bits, listener); + entries.forEach(this.values::add); +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index 7c11853c5090fbc4fa5b3e73a69acf166158fdec..170df85f42410f4766b04e73fc3a95e6dc4a3b8a 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -53,7 +53,7 @@ import net.minecraft.world.ticks.LevelChunkTicks; + import net.minecraft.world.ticks.TickContainerAccess; + import org.slf4j.Logger; + +-public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk, ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk, ca.spottedleaf.moonrise.patches.chunk_getblock.GetBlockChunk { // Paper - rewrite chunk system // Paper - get block chunk optimisation ++public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk, ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk, ca.spottedleaf.moonrise.patches.getblock.GetBlockChunk { // Paper - rewrite chunk system // Paper - get block chunk optimisation + + static final Logger LOGGER = LogUtils.getLogger(); + private static final TickingBlockEntity NULL_TICKER = new TickingBlockEntity() { +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +index c3b1caa352b988ec44fa2b2eb0536517711f5460..df1b3f3ae48f66137484e0eb3f4c7323269e1a74 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +@@ -27,15 +27,17 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + private PalettedContainer<Holder<Biome>> biomes; + + // Paper start - block counting +- private static final it.unimi.dsi.fastutil.ints.IntArrayList FULL_LIST = new it.unimi.dsi.fastutil.ints.IntArrayList(16*16*16); ++ private static final it.unimi.dsi.fastutil.shorts.ShortArrayList FULL_LIST = new it.unimi.dsi.fastutil.shorts.ShortArrayList(16*16*16); + static { +- for (int i = 0; i < (16*16*16); ++i) { ++ for (short i = 0; i < (16*16*16); ++i) { + FULL_LIST.add(i); + } + } + +- private int specialCollidingBlocks; +- private final ca.spottedleaf.moonrise.common.list.IBlockDataList tickingBlocks = new ca.spottedleaf.moonrise.common.list.IBlockDataList(); ++ private boolean isClient; ++ private static final short CLIENT_FORCED_SPECIAL_COLLIDING_BLOCKS = (short)9999; ++ private short specialCollidingBlocks; ++ private final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = new ca.spottedleaf.moonrise.common.list.ShortList(); + + @Override + public final int moonrise$getSpecialCollidingBlocks() { +@@ -43,7 +45,7 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + } + + @Override +- public final ca.spottedleaf.moonrise.common.list.IBlockDataList moonrise$getTickingBlockList() { ++ public final ca.spottedleaf.moonrise.common.list.ShortList moonrise$getTickingBlockList() { + return this.tickingBlocks; + } + // Paper end - block counting +@@ -83,6 +85,45 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + return this.setBlockState(x, y, z, state, true); + } + ++ // Paper start - block counting ++ private void updateBlockCallback(final int x, final int y, final int z, final BlockState newState, ++ final BlockState oldState) { ++ if (oldState == newState) { ++ return; ++ } ++ ++ if (this.isClient) { ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(newState)) { ++ this.specialCollidingBlocks = CLIENT_FORCED_SPECIAL_COLLIDING_BLOCKS; ++ } ++ return; ++ } ++ ++ final boolean isSpecialOld = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(oldState); ++ final boolean isSpecialNew = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(newState); ++ if (isSpecialOld != isSpecialNew) { ++ if (isSpecialOld) { ++ --this.specialCollidingBlocks; ++ } else { ++ ++this.specialCollidingBlocks; ++ } ++ } ++ ++ final boolean oldTicking = oldState.isRandomlyTicking(); ++ final boolean newTicking = newState.isRandomlyTicking(); ++ if (oldTicking != newTicking) { ++ final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = this.tickingBlocks; ++ final short position = (short)(x | (z << 4) | (y << (4+4))); ++ ++ if (oldTicking) { ++ tickingBlocks.remove(position); ++ } else { ++ tickingBlocks.add(position); ++ } ++ } ++ } ++ // Paper end - block counting ++ + public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) { + BlockState iblockdata1; + +@@ -102,7 +143,7 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + } + } + +- if (!fluid.isEmpty()) { ++ if (!!fluid.isRandomlyTicking()) { // Paper - block counting + --this.tickingFluidCount; + } + +@@ -113,25 +154,11 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + } + } + +- if (!fluid1.isEmpty()) { ++ if (!!fluid1.isRandomlyTicking()) { // Paper - block counting + ++this.tickingFluidCount; + } + +- // Paper start - block counting +- if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(iblockdata1)) { +- --this.specialCollidingBlocks; +- } +- if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(state)) { +- ++this.specialCollidingBlocks; +- } +- +- if (iblockdata1.isRandomlyTicking()) { +- this.tickingBlocks.remove(x, y, z); +- } +- if (state.isRandomlyTicking()) { +- this.tickingBlocks.add(x, y, z, state); +- } +- // Paper end - block counting ++ this.updateBlockCallback(x, y, z, state, iblockdata1); // Paper - block counting + + return iblockdata1; + } +@@ -167,7 +194,7 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + final int paletteSize = palette.getSize(); + final net.minecraft.util.BitStorage storage = data.storage(); + +- final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> counts; ++ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> counts; + if (paletteSize == 1) { + counts = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1); + counts.put(0, FULL_LIST); +@@ -175,10 +202,10 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + counts = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingBitStorage)storage).moonrise$countEntries(); + } + +- for (final java.util.Iterator<it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<it.unimi.dsi.fastutil.ints.IntArrayList>> iterator = counts.int2ObjectEntrySet().fastIterator(); iterator.hasNext();) { +- final it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<it.unimi.dsi.fastutil.ints.IntArrayList> entry = iterator.next(); ++ for (final java.util.Iterator<it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<it.unimi.dsi.fastutil.shorts.ShortArrayList>> iterator = counts.int2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<it.unimi.dsi.fastutil.shorts.ShortArrayList> entry = iterator.next(); + final int paletteIdx = entry.getIntKey(); +- final it.unimi.dsi.fastutil.ints.IntArrayList coordinates = entry.getValue(); ++ final it.unimi.dsi.fastutil.shorts.ShortArrayList coordinates = entry.getValue(); + final int paletteCount = coordinates.size(); + + final BlockState state = palette.valueFor(paletteIdx); +@@ -188,25 +215,30 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + } + + if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(state)) { +- this.specialCollidingBlocks += paletteCount; ++ this.specialCollidingBlocks += (short)paletteCount; + } +- this.nonEmptyBlockCount += paletteCount; ++ this.nonEmptyBlockCount += (short)paletteCount; + if (state.isRandomlyTicking()) { +- this.tickingBlockCount += paletteCount; +- final int[] raw = coordinates.elements(); ++ this.tickingBlockCount += (short)paletteCount; ++ final short[] raw = coordinates.elements(); ++ final int rawLen = raw.length; ++ ++ final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = this.tickingBlocks; ++ ++ tickingBlocks.setMinCapacity(Math.min((rawLen + tickingBlocks.size()) * 3 / 2, 16*16*16)); + + java.util.Objects.checkFromToIndex(0, paletteCount, raw.length); + for (int i = 0; i < paletteCount; ++i) { +- this.tickingBlocks.add(raw[i], state); ++ tickingBlocks.add(raw[i]); + } + } + + final FluidState fluid = state.getFluidState(); + + if (!fluid.isEmpty()) { +- //this.nonEmptyBlockCount += count; // fix vanilla bug: make non empty block count correct ++ //this.nonEmptyBlockCount += count; // fix vanilla bug: make non-empty block count correct + if (fluid.isRandomlyTicking()) { +- this.tickingFluidCount += paletteCount; ++ this.tickingFluidCount += (short)paletteCount; + } + } + } +@@ -229,7 +261,11 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + + datapaletteblock.read(buf); + this.biomes = datapaletteblock; +- this.recalcBlockCounts(); // Paper - block counting ++ // Paper start - block counting ++ this.isClient = true; ++ // force has special colliding blocks to be true ++ this.specialCollidingBlocks = this.nonEmptyBlockCount != (short)0 && this.maybeHas(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil::isSpecialCollidingBlock) ? CLIENT_FORCED_SPECIAL_COLLIDING_BLOCKS : (short)0; ++ // Paper end - block counting + } + + public void readBiomes(FriendlyByteBuf buf) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java b/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java +index f4c3f2a49b8d023b8ef67529eba30cf31467d8bf..716eb6f406db4b81b8854de0ea693a72f9ca9d13 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java +@@ -7,13 +7,20 @@ import net.minecraft.network.FriendlyByteBuf; + import net.minecraft.network.VarInt; + import org.apache.commons.lang3.Validate; + +-public class LinearPalette<T> implements Palette<T> { ++public class LinearPalette<T> implements Palette<T>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads + private final IdMap<T> registry; + private final T[] values; + private final PaletteResize<T> resizeHandler; + private final int bits; + private int size; + ++ // Paper start - optimise palette reads ++ @Override ++ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> container) { ++ return this.values; ++ } ++ // Paper end - optimise palette reads ++ + private LinearPalette(IdMap<T> idList, int bits, PaletteResize<T> listener, List<T> list) { + this.registry = idList; + this.values = (T[])(new Object[1 << bits]); +diff --git a/src/main/java/net/minecraft/world/level/chunk/Palette.java b/src/main/java/net/minecraft/world/level/chunk/Palette.java +index e379f39cc6e03723a5323d8392b4c10bfde65115..882284fe7beeb56a8b35ac6153ff41e84ebad27e 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/Palette.java ++++ b/src/main/java/net/minecraft/world/level/chunk/Palette.java +@@ -5,7 +5,7 @@ import java.util.function.Predicate; + import net.minecraft.core.IdMap; + import net.minecraft.network.FriendlyByteBuf; + +-public interface Palette<T> { ++public interface Palette<T> extends ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads + int idFor(T object); + + boolean maybeHas(Predicate<T> predicate); +diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +index 13d3c877b006a4975e7370713e3919c661e7890f..955862559b8c751b82082a5c0e02031324bf4805 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +@@ -41,6 +41,33 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + // this.threadingDetector.checkAndUnlock(); // Paper - disable this + } + ++ // Paper start - optimise palette reads ++ private void updateData(final PalettedContainer.Data<T> data) { ++ if (data != null) { ++ ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T>)(Object)data).moonrise$setPalette( ++ ((ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T>)data.palette).moonrise$getRawPalette((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T>)(Object)data) ++ ); ++ } ++ } ++ ++ private T readPaletteSlow(final PalettedContainer.Data<T> data, final int paletteIdx) { ++ return data.palette.valueFor(paletteIdx); ++ } ++ ++ private T readPalette(final PalettedContainer.Data<T> data, final int paletteIdx) { ++ final T[] palette = ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T>)(Object)data).moonrise$getPalette(); ++ if (palette == null) { ++ return this.readPaletteSlow(data, paletteIdx); ++ } ++ ++ final T ret = palette[paletteIdx]; ++ if (ret == null) { ++ throw new IllegalArgumentException("Palette index out of bounds"); ++ } ++ return ret; ++ } ++ // Paper end - optimise palette reads ++ + // Paper start - Anti-Xray - Add preset values + @Deprecated @io.papermc.paper.annotation.DoNotUse public static <T> Codec<PalettedContainer<T>> codecRW(IdMap<T> idList, Codec<T> entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue) { return PalettedContainer.codecRW(idList, entryCodec, paletteProvider, defaultValue, null); } + public static <T> Codec<PalettedContainer<T>> codecRW(IdMap<T> idList, Codec<T> entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues) { +@@ -113,6 +140,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + } + } + // Paper end ++ this.updateData(this.data); // Paper - optimise palette reads + } + + // Paper start - Anti-Xray - Add preset values +@@ -122,6 +150,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + this.registry = idList; + this.strategy = paletteProvider; + this.data = data; ++ this.updateData(this.data); // Paper - optimise palette reads + } + + // Paper start - Anti-Xray - Add preset values +@@ -133,6 +162,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + this.registry = idList; + this.data = this.createOrReuseData(null, 0); + this.data.palette.idFor(object); ++ this.updateData(this.data); // Paper - optimise palette reads + } + + private PalettedContainer.Data<T> createOrReuseData(@Nullable PalettedContainer.Data<T> previousData, int bits) { +@@ -158,6 +188,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + PalettedContainer.Data<T> data2 = this.createOrReuseData(data, newBits); + data2.copyFrom(data.palette, data.storage); + this.data = data2; ++ this.updateData(this.data); // Paper - optimise palette reads + this.addPresetValues(); + return object == null ? -1 : data2.palette.idFor(object); + // Paper end +@@ -191,9 +222,12 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + } + + private synchronized T getAndSet(int index, T value) { // Paper - synchronize +- int i = this.data.palette.idFor(value); +- int j = this.data.storage.getAndSet(index, i); +- return this.data.palette.valueFor(j); ++ // Paper start - optimise palette reads ++ final int paletteIdx = this.data.palette.idFor(value); ++ final PalettedContainer.Data<T> data = this.data; ++ final int prev = data.storage.getAndSet(index, paletteIdx); ++ return this.readPalette(data, prev); ++ // Paper end - optimise palette reads + } + + public void set(int x, int y, int z, T value) { +@@ -217,8 +251,10 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + } + + public T get(int index) { // Paper - public +- PalettedContainer.Data<T> data = this.data; +- return data.palette.valueFor(data.storage.get(index)); ++ // Paper start - optimise palette reads ++ final PalettedContainer.Data<T> data = this.data; ++ return this.readPalette(data, data.storage.get(index)); ++ // Paper end - optimise palette reads + } + + @Override +@@ -238,6 +274,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + data.palette.read(buf); + buf.readLongArray(data.storage.getRaw()); + this.data = data; ++ this.updateData(this.data); // Paper - optimise palette reads + this.addPresetValues(); // Paper - Anti-Xray - Add preset values (inefficient, but this isn't used by the server) + } finally { + this.release(); +@@ -386,7 +423,44 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + void accept(T object, int count); + } + +- static record Data<T>(PalettedContainer.Configuration<T> configuration, BitStorage storage, Palette<T> palette) { ++ // Paper start - optimise palette reads ++ public static final class Data<T> implements ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> { ++ ++ private final PalettedContainer.Configuration<T> configuration; ++ private final BitStorage storage; ++ private final Palette<T> palette; ++ ++ private T[] moonrise$palette; ++ ++ public Data(final PalettedContainer.Configuration<T> configuration, final BitStorage storage, final Palette<T> palette) { ++ this.configuration = configuration; ++ this.storage = storage; ++ this.palette = palette; ++ } ++ ++ public PalettedContainer.Configuration<T> configuration() { ++ return this.configuration; ++ } ++ ++ public BitStorage storage() { ++ return this.storage; ++ } ++ ++ public Palette<T> palette() { ++ return this.palette; ++ } ++ ++ @Override ++ public final T[] moonrise$getPalette() { ++ return this.moonrise$palette; ++ } ++ ++ @Override ++ public final void moonrise$setPalette(final T[] palette) { ++ this.moonrise$palette = palette; ++ } ++ // Paper end - optimise palette reads ++ + public void copyFrom(Palette<T> palette, BitStorage storage) { + for (int i = 0; i < storage.getSize(); i++) { + T object = palette.valueFor(storage.get(i)); +diff --git a/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java b/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java +index 24b608cfcd6f39db02e682e5d8162dc4ad9fd6d6..3a06392a327f26d78c28fdcce39f74b130c4d906 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java ++++ b/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java +@@ -8,12 +8,24 @@ import net.minecraft.network.FriendlyByteBuf; + import net.minecraft.network.VarInt; + import org.apache.commons.lang3.Validate; + +-public class SingleValuePalette<T> implements Palette<T> { ++public class SingleValuePalette<T> implements Palette<T>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads + private final IdMap<T> registry; + @Nullable + private T value; + private final PaletteResize<T> resizeHandler; + ++ // Paper start - optimise palette reads ++ private T[] rawPalette; ++ ++ @Override ++ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> container) { ++ if (this.rawPalette != null) { ++ return this.rawPalette; ++ } ++ return this.rawPalette = (T[])new Object[] { this.value }; ++ } ++ // Paper end - optimise palette reads ++ + public SingleValuePalette(IdMap<T> idList, PaletteResize<T> listener, List<T> entries) { + this.registry = idList; + this.resizeHandler = listener; +@@ -33,6 +45,11 @@ public class SingleValuePalette<T> implements Palette<T> { + return this.resizeHandler.onResize(1, object); + } else { + this.value = object; ++ // Paper start - optimise palette reads ++ if (this.rawPalette != null) { ++ this.rawPalette[0] = object; ++ } ++ // Paper end - optimise palette reads + return 0; + } + } +@@ -58,6 +75,11 @@ public class SingleValuePalette<T> implements Palette<T> { + @Override + public void read(FriendlyByteBuf buf) { + this.value = this.registry.byIdOrThrow(buf.readVarInt()); ++ // Paper start - optimise palette reads ++ if (this.rawPalette != null) { ++ this.rawPalette[0] = this.value; ++ } ++ // Paper end - optimise palette reads + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +index 1e0439cf3f4008fa430acb90b45f5bc4cdd6d7f2..e46bdcbf3514eaa6f4990a797a63c5041a142807 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +@@ -28,7 +28,7 @@ import net.minecraft.nbt.NbtIo; // Paper + import net.minecraft.world.level.ChunkPos; + import org.slf4j.Logger; + +-public class RegionFile implements AutoCloseable { ++public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile { // Paper - rewrite chunk system + + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int SECTOR_BYTES = 4096; +@@ -51,6 +51,20 @@ public class RegionFile implements AutoCloseable { + private final IntBuffer timestamps; + @VisibleForTesting + protected final RegionBitmap usedSectors; ++ // Paper start - rewrite chunk system ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(final CompoundTag data, final ChunkPos pos) throws IOException { ++ final RegionFile.ChunkBuffer buffer = ((RegionFile)(Object)this).new ChunkBuffer(pos); ++ ((ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkBuffer)buffer).moonrise$setWriteOnClose(false); ++ ++ final DataOutputStream out = new DataOutputStream(this.version.wrap(buffer)); ++ ++ return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData( ++ data, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.WRITE, ++ out, ((ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkBuffer)buffer)::moonrise$write ++ ); ++ } ++ // Paper end - rewrite chunk system + // Paper start - Attempt to recalculate regionfile header if it is corrupt + private static long roundToSectors(long bytes) { + long sectors = bytes >>> 12; // 4096 = 2^12 +@@ -682,6 +696,16 @@ public class RegionFile implements AutoCloseable { + + @Nullable + private DataInputStream createExternalChunkInputStream(ChunkPos pos, byte flags) throws IOException { ++ // Paper start - rewrite chunk system ++ final DataInputStream is = this.createExternalChunkInputStream0(pos, flags); ++ if (is == null) { ++ return is; ++ } ++ return new ca.spottedleaf.moonrise.patches.chunk_system.util.stream.ExternalChunkStreamMarker(is); ++ } ++ @Nullable ++ private DataInputStream createExternalChunkInputStream0(ChunkPos pos, byte flags) throws IOException { ++ // Paper end - rewrite chunk system + Path path = this.getExternalChunkPath(pos); + + if (!Files.isRegularFile(path, new LinkOption[0])) { +@@ -978,10 +1002,29 @@ public class RegionFile implements AutoCloseable { + + } + // Paper end +- private class ChunkBuffer extends ByteArrayOutputStream { ++ private class ChunkBuffer extends ByteArrayOutputStream implements ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkBuffer { // Paper - rewrite chunk system + + private final ChunkPos pos; + ++ // Paper start - rewrite chunk system ++ private boolean writeOnClose = true; ++ ++ @Override ++ public final boolean moonrise$getWriteOnClose() { ++ return this.writeOnClose; ++ } ++ ++ @Override ++ public final void moonrise$setWriteOnClose(final boolean value) { ++ this.writeOnClose = value; ++ } ++ ++ @Override ++ public final void moonrise$write(final RegionFile regionFile) throws IOException { ++ regionFile.write(this.pos, ByteBuffer.wrap(this.buf, 0, this.count)); ++ } ++ // Paper end - rewrite chunk system ++ + public ChunkBuffer(final ChunkPos chunkcoordintpair) { + super(8096); + super.write(0); +@@ -1015,7 +1058,7 @@ public class RegionFile implements AutoCloseable { + + JvmProfiler.INSTANCE.onRegionFileWrite(RegionFile.this.info, this.pos, RegionFile.this.version, i); + bytebuffer.putInt(0, i); +- RegionFile.this.write(this.pos, bytebuffer); ++ if (this.writeOnClose) { RegionFile.this.write(this.pos, bytebuffer); } // Paper - rewrite chunk system + } + } + +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +index 40689256711cc94a806ca1da346f4f62eda31526..b0ace4c7f25425a58c0707e7a7992446164bef88 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +@@ -28,8 +28,8 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + + // Paper start - rewrite chunk system + private static final int REGION_SHIFT = 5; +- private static final int MAX_NON_EXISTING_CACHE = 1024 * 64; +- private final it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet nonExistingRegionFiles = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet(MAX_NON_EXISTING_CACHE+1); ++ private static final int MAX_NON_EXISTING_CACHE = 1024 * 4; ++ private final it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet nonExistingRegionFiles = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet(); + private static String getRegionFileName(final int chunkX, final int chunkZ) { + return "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT) + ".mca"; + } +@@ -104,6 +104,97 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + + return ret; + } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite( ++ final int chunkX, final int chunkZ, final CompoundTag compound ++ ) throws IOException { ++ if (compound == null) { ++ return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData( ++ compound, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.DELETE, ++ null, null ++ ); ++ } ++ ++ final ChunkPos pos = new ChunkPos(chunkX, chunkZ); ++ final RegionFile regionFile = this.getRegionFile(pos); ++ ++ // note: not required to keep regionfile loaded after this call, as the write param takes a regionfile as input ++ // (and, the regionfile parameter is unused for writing until the write call) ++ final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData writeData = ((ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile)regionFile).moonrise$startWrite(compound, pos); ++ ++ try { ++ NbtIo.write(compound, writeData.output()); ++ } finally { ++ writeData.output().close(); ++ } ++ ++ return writeData; ++ } ++ ++ @Override ++ public final void moonrise$finishWrite( ++ final int chunkX, final int chunkZ, final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData writeData ++ ) throws IOException { ++ final ChunkPos pos = new ChunkPos(chunkX, chunkZ); ++ if (writeData.result() == ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.DELETE) { ++ final RegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); ++ if (regionFile != null) { ++ regionFile.clear(pos); ++ } // else: didn't exist ++ ++ return; ++ } ++ ++ writeData.write().run(this.getRegionFile(pos)); ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData moonrise$readData( ++ final int chunkX, final int chunkZ ++ ) throws IOException { ++ final RegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); ++ ++ final DataInputStream input = regionFile == null ? null : regionFile.getChunkDataInputStream(new ChunkPos(chunkX, chunkZ)); ++ ++ if (input == null) { ++ return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData( ++ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData.ReadResult.NO_DATA, null, null ++ ); ++ } ++ ++ final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData ret = new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData( ++ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData.ReadResult.HAS_DATA, input, null ++ ); ++ ++ if (!(input instanceof ca.spottedleaf.moonrise.patches.chunk_system.util.stream.ExternalChunkStreamMarker)) { ++ // internal stream, which is fully read ++ return ret; ++ } ++ ++ final CompoundTag syncRead = this.moonrise$finishRead(chunkX, chunkZ, ret); ++ ++ if (syncRead == null) { ++ // need to try again ++ return this.moonrise$readData(chunkX, chunkZ); ++ } ++ ++ return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData( ++ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData.ReadResult.SYNC_READ, null, syncRead ++ ); ++ } ++ ++ // if the return value is null, then the caller needs to re-try with a new call to readData() ++ @Override ++ public final CompoundTag moonrise$finishRead( ++ final int chunkX, final int chunkZ, final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData readData ++ ) throws IOException { ++ try { ++ return NbtIo.read(readData.input()); ++ } finally { ++ readData.input().close(); ++ } ++ } + // Paper end - rewrite chunk system + // Paper start - recalculate region file headers + private final boolean isChunkData; +@@ -143,6 +234,12 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + this.isChunkData = isChunkDataFolder(this.folder); // Paper - recalculate region file headers + } + ++ // Paper start - rewrite chunk system ++ public RegionFile getRegionFile(ChunkPos chunkcoordintpair) throws IOException { ++ return this.getRegionFile(chunkcoordintpair, false); ++ } ++ // Paper end - rewrite chunk system ++ + public RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit // Paper - public + // Paper start - rewrite chunk system + if (existingOnly) { +diff --git a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +index 1c0712295695727ee9c4d430d4157b8e17cbd71f..7b8e5be1648ef8741aadabd6cbdcf991012c3ce2 100644 +--- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java ++++ b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +@@ -55,6 +55,48 @@ public abstract class FlowingFluid extends Fluid { + }); + private final Map<FluidState, VoxelShape> shapes = Maps.newIdentityHashMap(); + ++ // Paper start - fluid method optimisations ++ private FluidState sourceFalling; ++ private FluidState sourceNotFalling; ++ ++ private static final int TOTAL_FLOWING_STATES = FALLING.getPossibleValues().size() * LEVEL.getPossibleValues().size(); ++ private static final int MIN_LEVEL = LEVEL.getPossibleValues().stream().sorted().findFirst().get().intValue(); ++ ++ // index = (falling ? 1 : 0) + level*2 ++ private FluidState[] flowingLookUp; ++ private volatile boolean init; ++ ++ private static final int COLLISION_OCCLUSION_CACHE_SIZE = 2048; ++ private static final ThreadLocal<ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey[]> COLLISION_OCCLUSION_CACHE = ThreadLocal.withInitial(() -> new ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey[COLLISION_OCCLUSION_CACHE_SIZE]); ++ ++ ++ /** ++ * Due to init order, we need to use callbacks to initialise our state ++ */ ++ private void init() { ++ synchronized (this) { ++ if (this.init) { ++ return; ++ } ++ this.flowingLookUp = new FluidState[TOTAL_FLOWING_STATES]; ++ final FluidState defaultFlowState = this.getFlowing().defaultFluidState(); ++ for (int i = 0; i < TOTAL_FLOWING_STATES; ++i) { ++ final int falling = i & 1; ++ final int level = (i >>> 1) + MIN_LEVEL; ++ ++ this.flowingLookUp[i] = defaultFlowState.setValue(FALLING, falling == 1 ? Boolean.TRUE : Boolean.FALSE) ++ .setValue(LEVEL, Integer.valueOf(level)); ++ } ++ ++ final FluidState defaultFallState = this.getSource().defaultFluidState(); ++ this.sourceFalling = defaultFallState.setValue(FALLING, Boolean.TRUE); ++ this.sourceNotFalling = defaultFallState.setValue(FALLING, Boolean.FALSE); ++ ++ this.init = true; ++ } ++ } ++ // Paper end - fluid method optimisations ++ + public FlowingFluid() {} + + @Override +@@ -239,53 +281,70 @@ public abstract class FlowingFluid extends Fluid { + } + } + +- private boolean canPassThroughWall(Direction face, BlockGetter world, BlockPos pos, BlockState state, BlockPos fromPos, BlockState fromState) { +- Object2ByteLinkedOpenHashMap object2bytelinkedopenhashmap; ++ // Paper start - fluid method optimisations ++ private static boolean canPassThroughWall(final Direction direction, final BlockGetter level, ++ final BlockPos fromPos, final BlockState fromState, ++ final BlockPos toPos, final BlockState toState) { ++ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$emptyCollisionShape() & ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$emptyCollisionShape()) { ++ // don't even try to cache simple cases ++ return true; ++ } + +- if (!state.getBlock().hasDynamicShape() && !fromState.getBlock().hasDynamicShape()) { +- object2bytelinkedopenhashmap = (Object2ByteLinkedOpenHashMap) FlowingFluid.OCCLUSION_CACHE.get(); +- } else { +- object2bytelinkedopenhashmap = null; ++ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$occludesFullBlock() | ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$occludesFullBlock()) { ++ // don't even try to cache simple cases ++ return false; + } + +- Block.BlockStatePairKey block_a; ++ final ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey[] cache = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$hasCache() & ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$hasCache() ? ++ COLLISION_OCCLUSION_CACHE.get() : null; + +- if (object2bytelinkedopenhashmap != null) { +- block_a = new Block.BlockStatePairKey(state, fromState, face); +- byte b0 = object2bytelinkedopenhashmap.getAndMoveToFirst(block_a); ++ final int keyIndex ++ = (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$uniqueId1() ^ ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$uniqueId2() ^ ((ca.spottedleaf.moonrise.patches.collisions.util.CollisionDirection)(Object)direction).moonrise$uniqueId()) ++ & (COLLISION_OCCLUSION_CACHE_SIZE - 1); + +- if (b0 != 127) { +- return b0 != 0; ++ if (cache != null) { ++ final ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey cached = cache[keyIndex]; ++ if (cached != null && cached.first() == fromState && cached.second() == toState && cached.direction() == direction) { ++ return cached.result(); + } +- } else { +- block_a = null; + } + +- VoxelShape voxelshape = state.getCollisionShape(world, pos); +- VoxelShape voxelshape1 = fromState.getCollisionShape(world, fromPos); +- boolean flag = !Shapes.mergedFaceOccludes(voxelshape, voxelshape1, face); ++ final VoxelShape shape1 = fromState.getCollisionShape(level, fromPos); ++ final VoxelShape shape2 = toState.getCollisionShape(level, toPos); + +- if (object2bytelinkedopenhashmap != null) { +- if (object2bytelinkedopenhashmap.size() == 200) { +- object2bytelinkedopenhashmap.removeLastByte(); +- } ++ final boolean result = !Shapes.mergedFaceOccludes(shape1, shape2, direction); + +- object2bytelinkedopenhashmap.putAndMoveToFirst(block_a, (byte) (flag ? 1 : 0)); ++ if (cache != null) { ++ // we can afford to replace in-use keys more often due to the excessive caching the collision patch does in mergedFaceOccludes ++ cache[keyIndex] = new ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey(fromState, toState, direction, result); + } + +- return flag; ++ return result; + } ++ // Paper end - fluid method optimisations + + public abstract Fluid getFlowing(); + + public FluidState getFlowing(int level, boolean falling) { +- return (FluidState) ((FluidState) this.getFlowing().defaultFluidState().setValue(FlowingFluid.LEVEL, level)).setValue(FlowingFluid.FALLING, falling); ++ // Paper start - fluid method optimisations ++ final int amount = level; ++ if (!this.init) { ++ this.init(); ++ } ++ final int index = (falling ? 1 : 0) | ((amount - MIN_LEVEL) << 1); ++ return this.flowingLookUp[index]; ++ // Paper end - fluid method optimisations + } + + public abstract Fluid getSource(); + + public FluidState getSource(boolean falling) { +- return (FluidState) this.getSource().defaultFluidState().setValue(FlowingFluid.FALLING, falling); ++ // Paper start - fluid method optimisations ++ if (!this.init) { ++ this.init(); ++ } ++ return falling ? this.sourceFalling : this.sourceNotFalling; ++ // Paper end - fluid method optimisations + } + + protected abstract boolean canConvertToSource(Level world); +diff --git a/src/main/java/net/minecraft/world/level/material/FluidState.java b/src/main/java/net/minecraft/world/level/material/FluidState.java +index 14bb12d2a0066e8b020f2e0e670a7a5c74633623..8ceeb053a391d1a53083eb0680cf34c453afcba2 100644 +--- a/src/main/java/net/minecraft/world/level/material/FluidState.java ++++ b/src/main/java/net/minecraft/world/level/material/FluidState.java +@@ -21,12 +21,30 @@ import net.minecraft.world.level.block.state.properties.Property; + import net.minecraft.world.phys.Vec3; + import net.minecraft.world.phys.shapes.VoxelShape; + +-public final class FluidState extends StateHolder<Fluid, FluidState> { ++public final class FluidState extends StateHolder<Fluid, FluidState> implements ca.spottedleaf.moonrise.patches.fluid.FluidFluidState { // Paper - fluid method optimisations + public static final Codec<FluidState> CODEC = codec(BuiltInRegistries.FLUID.byNameCodec(), Fluid::defaultFluidState).stable(); + public static final int AMOUNT_MAX = 9; + public static final int AMOUNT_FULL = 8; + protected final boolean isEmpty; // Paper - Perf: moved from isEmpty() + ++ // Paper start - fluid method optimisations ++ private int amount; ++ //private boolean isEmpty; ++ private boolean isSource; ++ private float ownHeight; ++ private boolean isRandomlyTicking; ++ private BlockState legacyBlock; ++ ++ @Override ++ public final void moonrise$initCaches() { ++ this.amount = this.getType().getAmount((FluidState)(Object)this); ++ //this.isEmpty = this.getType().isEmpty(); ++ this.isSource = this.getType().isSource((FluidState)(Object)this); ++ this.ownHeight = this.getType().getOwnHeight((FluidState)(Object)this); ++ this.isRandomlyTicking = this.getType().isRandomlyTicking(); ++ } ++ // Paper end - fluid method optimisations ++ + public FluidState(Fluid fluid, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<FluidState> codec) { + super(fluid, propertyMap, codec); + this.isEmpty = fluid.isEmpty(); // Paper - Perf: moved from isEmpty() +@@ -37,11 +55,11 @@ public final class FluidState extends StateHolder<Fluid, FluidState> { + } + + public boolean isSource() { +- return this.getType().isSource(this); ++ return this.isSource; // Paper - fluid method optimisations + } + + public boolean isSourceOfType(Fluid fluid) { +- return this.owner == fluid && this.owner.isSource(this); ++ return this.isSource && this.owner == fluid; // Paper - fluid method optimisations + } + + public boolean isEmpty() { +@@ -53,11 +71,11 @@ public final class FluidState extends StateHolder<Fluid, FluidState> { + } + + public float getOwnHeight() { +- return this.getType().getOwnHeight(this); ++ return this.ownHeight; // Paper - fluid method optimisations + } + + public int getAmount() { +- return this.getType().getAmount(this); ++ return this.amount; // Paper - fluid method optimisations + } + + public boolean shouldRenderBackwardUpFace(BlockGetter world, BlockPos pos) { +@@ -83,7 +101,7 @@ public final class FluidState extends StateHolder<Fluid, FluidState> { + } + + public boolean isRandomlyTicking() { +- return this.getType().isRandomlyTicking(); ++ return this.isRandomlyTicking; // Paper - fluid method optimisations + } + + public void randomTick(Level world, BlockPos pos, RandomSource random) { +@@ -95,7 +113,12 @@ public final class FluidState extends StateHolder<Fluid, FluidState> { + } + + public BlockState createLegacyBlock() { +- return this.getType().createLegacyBlock(this); ++ // Paper start - fluid method optimisations ++ if (this.legacyBlock != null) { ++ return this.legacyBlock; ++ } ++ return this.legacyBlock = this.getType().createLegacyBlock((FluidState)(Object)this); ++ // Paper end - fluid method optimisations + } + + @Nullable +diff --git a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java +index 1d36f8dcffd22cf844448d3d8351fb8718cf5227..fbe0c4b0fdbb992b7002f6afe1e74d63cbb420f2 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java +@@ -57,7 +57,7 @@ public abstract class DiscreteVoxelShape implements ca.spottedleaf.moonrise.patc + } + } + +- final boolean hasSingleAABB = sizeX == 1 && sizeY == 1 && sizeZ == 1 && !isEmpty && discreteVoxelShape.isFull(0, 0, 0); ++ final boolean hasSingleAABB = sizeX == 1 && sizeY == 1 && sizeZ == 1 && !isEmpty && (voxelSet[0] & 1L) != 0L; + + final int minFullX = discreteVoxelShape.firstFull(Direction.Axis.X); + final int minFullY = discreteVoxelShape.firstFull(Direction.Axis.Y); +diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java +index c348171c150bf69d24303d0862e45ab78baddcab..4c8185fbc560a5c5304729e5827ae7762933ec36 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java +@@ -180,13 +180,13 @@ public final class Shapes { + final VoxelShape first = tmp[i]; + final VoxelShape second = tmp[next]; + +- tmp[newSize++] = Shapes.or(first, second); ++ tmp[newSize++] = Shapes.joinUnoptimized(first, second, BooleanOp.OR); + } + } + size = newSize; + } + +- return tmp[0]; ++ return tmp[0].optimize(); + // Paper end - optimise collisions + } + +@@ -255,7 +255,22 @@ public final class Shapes { + } + + public static VoxelShape getFaceShape(VoxelShape shape, Direction direction) { +- return ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$getFaceShapeClamped(direction); // Paper - optimise collisions ++ if (shape == block()) { ++ return block(); ++ } else { ++ Direction.Axis axis = direction.getAxis(); ++ boolean bl; ++ int i; ++ if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) { ++ bl = DoubleMath.fuzzyEquals(shape.max(axis), 1.0, 1.0E-7); ++ i = shape.shape.getSize(axis) - 1; ++ } else { ++ bl = DoubleMath.fuzzyEquals(shape.min(axis), 0.0, 1.0E-7); ++ i = 0; ++ } ++ ++ return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i)); ++ } + } + + // Paper start - optimise collisions +diff --git a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java +index 11824d39e72fa003b3a56aa9b8d679fe8e23a1a4..c207e54dc52e67ba91dac5c6ce1391a77f597bcd 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java +@@ -162,13 +162,13 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll + + if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) { + if (DoubleMath.fuzzyEquals(this.max(axis), 1.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) { +- ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, this.shape.getSize(axis) - 1)); ++ ret = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.sliceShape((VoxelShape)(Object)this, axis, this.shape.getSize(axis) - 1); + } else { + ret = Shapes.empty(); + } + } else { + if (DoubleMath.fuzzyEquals(this.min(axis), 0.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) { +- ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, 0)); ++ ret = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.sliceShape((VoxelShape)(Object)this, axis, 0); + } else { + ret = Shapes.empty(); + } +@@ -179,23 +179,6 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll + return ret; + } + +- private static VoxelShape tryForceBlock(final VoxelShape other) { +- if (other == Shapes.block()) { +- return other; +- } +- +- final AABB otherAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)other).moonrise$getSingleAABBRepresentation(); +- if (otherAABB == null) { +- return other; +- } +- +- if (((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)Shapes.block()).moonrise$getSingleAABBRepresentation().equals(otherAABB)) { +- return Shapes.block(); +- } +- +- return other; +- } +- + private boolean computeOccludesFullBlock() { + if (this.isEmpty) { + this.occludesFullBlock = Boolean.FALSE; +@@ -293,18 +276,21 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll + return result; + } + +- private static DoubleList offsetList(final DoubleList src, final double by) { +- if (src instanceof OffsetDoubleList offsetDoubleList) { +- return new OffsetDoubleList(offsetDoubleList.delegate, by + offsetDoubleList.offset); ++ private static DoubleList offsetList(final double[] src, final double by) { ++ final it.unimi.dsi.fastutil.doubles.DoubleArrayList wrap = it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(src); ++ if (by == 0.0) { ++ return wrap; + } +- return new OffsetDoubleList(src, by); ++ return new OffsetDoubleList(wrap, by); + } + + private List<AABB> toAabbsUncached() { +- final List<AABB> ret = new java.util.ArrayList<>(); ++ final List<AABB> ret; + if (this.singleAABBRepresentation != null) { ++ ret = new java.util.ArrayList<>(1); + ret.add(this.singleAABBRepresentation); + } else { ++ ret = new java.util.ArrayList<>(); + final double[] coordsX = this.rootCoordinatesX; + final double[] coordsY = this.rootCoordinatesY; + final double[] coordsZ = this.rootCoordinatesZ; +@@ -426,6 +412,26 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll + final double minDistance = minDistanceArr[0]; + return new BlockHitResult(from.add(minDistance * diffX, minDistance * diffY, minDistance * diffZ), direction, offset, false); + } ++ ++ private VoxelShape calculateFaceDirect(final Direction direction, final Direction.Axis axis, final double[] coords, final double offset) { ++ if (coords.length == 2 && ++ DoubleMath.fuzzyEquals(coords[0] + offset, 0.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) && ++ DoubleMath.fuzzyEquals(coords[1] + offset, 1.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) { ++ return (VoxelShape)(Object)this; ++ } ++ ++ final boolean positiveDir = direction.getAxisDirection() == Direction.AxisDirection.POSITIVE; ++ ++ // see findIndex ++ final int index = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor( ++ coords, (positiveDir ? (1.0 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) : (0.0 + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) - offset, ++ 0, coords.length - 1 ++ ); ++ ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.sliceShape( ++ (VoxelShape)(Object)this, axis, index ++ ); ++ } + // Paper end - optimise collisions + + protected VoxelShape(DiscreteVoxelShape voxels) { +@@ -517,20 +523,32 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll + } + + public VoxelShape singleEncompassing() { +- return this.isEmpty() +- ? Shapes.empty() +- : Shapes.box( +- this.min(Direction.Axis.X), +- this.min(Direction.Axis.Y), +- this.min(Direction.Axis.Z), +- this.max(Direction.Axis.X), +- this.max(Direction.Axis.Y), +- this.max(Direction.Axis.Z) +- ); ++ // Paper start - optimise collisions ++ if (this.isEmpty) { ++ return Shapes.empty(); ++ } ++ return Shapes.create(this.bounds()); ++ // Paper end - optimise collisions + } + + protected double get(Direction.Axis axis, int index) { +- return this.getCoords(axis).getDouble(index); ++ // Paper start - optimise collisions ++ final int idx = index; ++ switch (axis) { ++ case X: { ++ return this.rootCoordinatesX[idx] + this.offsetX; ++ } ++ case Y: { ++ return this.rootCoordinatesY[idx] + this.offsetY; ++ } ++ case Z: { ++ return this.rootCoordinatesZ[idx] + this.offsetZ; ++ } ++ default: { ++ throw new IllegalStateException("Unknown axis: " + axis); ++ } ++ } ++ // Paper end - optimise collisions + } + + public abstract DoubleList getCoords(Direction.Axis axis); +@@ -547,9 +565,9 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll + + final ArrayVoxelShape ret = new ArrayVoxelShape( + this.shape, +- offsetList(this.getCoords(Direction.Axis.X), x), +- offsetList(this.getCoords(Direction.Axis.Y), y), +- offsetList(this.getCoords(Direction.Axis.Z), z) ++ offsetList(this.rootCoordinatesX, this.offsetX + x), ++ offsetList(this.rootCoordinatesY, this.offsetY + y), ++ offsetList(this.rootCoordinatesZ, this.offsetZ + z) + ); + + final ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cachedToAABBs = this.cachedToAABBs; +@@ -574,6 +592,11 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll + + final List<AABB> aabbs = this.toAabbs(); + ++ if (aabbs.isEmpty()) { ++ // We are a SliceShape, which does not properly fill isEmpty for every case ++ return Shapes.empty(); ++ } ++ + if (aabbs.size() == 1) { + final AABB singleAABB = aabbs.get(0); + final VoxelShape ret = Shapes.create(singleAABB); +@@ -700,7 +723,32 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll + } + + protected int findIndex(Direction.Axis axis, double coord) { +- return Mth.binarySearch(0, this.shape.getSize(axis) + 1, i -> coord < this.get(axis, i)) - 1; ++ // Paper start - optimise collisions ++ final double value = coord; ++ switch (axis) { ++ case X: { ++ final double[] values = this.rootCoordinatesX; ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor( ++ values, value - this.offsetX, 0, values.length - 1 ++ ); ++ } ++ case Y: { ++ final double[] values = this.rootCoordinatesY; ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor( ++ values, value - this.offsetY, 0, values.length - 1 ++ ); ++ } ++ case Z: { ++ final double[] values = this.rootCoordinatesZ; ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor( ++ values, value - this.offsetZ, 0, values.length - 1 ++ ); ++ } ++ default: { ++ throw new IllegalStateException("Unknown axis: " + axis); ++ } ++ } ++ // Paper end - optimise collisions + } + + @Nullable +@@ -723,13 +771,13 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll + final AABB singleAABB = this.singleAABBRepresentation; + if (singleAABB != null) { + if (singleAABB.contains(fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) { +- return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true); ++ return new BlockHitResult(fromBehind, Direction.getApproximateNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true); + } + return clip(singleAABB, from, to, offset); + } + + if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.strictlyContains((VoxelShape)(Object)this, fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) { +- return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true); ++ return new BlockHitResult(fromBehind, Direction.getApproximateNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true); + } + + return AABB.clip(((VoxelShape)(Object)this).toAabbs(), from, to, offset); +@@ -783,17 +831,23 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll + } + + private VoxelShape calculateFace(Direction direction) { +- Direction.Axis axis = direction.getAxis(); +- DoubleList doubleList = this.getCoords(axis); +- if (doubleList.size() == 2 +- && DoubleMath.fuzzyEquals(doubleList.getDouble(0), 0.0, 1.0E-7) +- && DoubleMath.fuzzyEquals(doubleList.getDouble(1), 1.0, 1.0E-7)) { +- return this; +- } else { +- Direction.AxisDirection axisDirection = direction.getAxisDirection(); +- int i = this.findIndex(axis, axisDirection == Direction.AxisDirection.POSITIVE ? 0.9999999 : 1.0E-7); +- return new SliceShape(this, axis, i); ++ // Paper start - optimise collisions ++ final Direction.Axis axis = direction.getAxis(); ++ switch (axis) { ++ case X: { ++ return this.calculateFaceDirect(direction, axis, this.rootCoordinatesX, this.offsetX); ++ } ++ case Y: { ++ return this.calculateFaceDirect(direction, axis, this.rootCoordinatesY, this.offsetY); ++ } ++ case Z: { ++ return this.calculateFaceDirect(direction, axis, this.rootCoordinatesZ, this.offsetZ); ++ } ++ default: { ++ throw new IllegalStateException("Unknown axis: " + axis); ++ } + } ++ // Paper end - optimise collisions + } + + // Paper start - optimise collisions |