PaperMC/patches/server/0988-Moonrise-optimisation-patches.patch
Spottedleaf 3234b20ee4 Do not allow chunk unloading outside of the regular tick loop
Allowing chunk loading to occur at any point via purgeUnload()
introduces possible undesirable behavior to occur recursively
inside the chunk system.
2024-07-28 16:18:25 -07:00

33026 lines
1.5 MiB

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Fri, 14 Jun 2024 11:57:26 -0700
Subject: [PATCH] Moonrise optimisation patches
Currently includes:
- Starlight + Chunk System
- Entity tracker optimisations
- Collision optimisations
- Random block ticking optimisations
- Chunk tick iteration optimisations
See https://github.com/Tuinity/Moonrise
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 da323a1105347d5cf4b946df10ded78a953236f2..0abba00741b39b69a7f167e5d2670f2565c9a752 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,10 @@
package ca.spottedleaf.moonrise.common.util;
import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+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 com.mojang.logging.LogUtils;
import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.FullChunkStatus;
@@ -17,203 +21,46 @@ import java.util.function.Consumer;
public final class ChunkSystem {
private static final Logger LOGGER = LogUtils.getLogger();
- private static final net.minecraft.world.level.chunk.status.ChunkStep FULL_CHUNK_STEP = net.minecraft.world.level.chunk.status.ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
-
- private static int getDistance(final ChunkStatus status) {
- return FULL_CHUNK_STEP.getAccumulatedRadiusOf(status);
- }
public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
}
public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) {
- level.chunkSource.mainThreadProcessor.execute(run);
+ ((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 Consumer<ChunkAccess> onComplete) {
- if (gen) {
- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
- return;
- }
- scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
- if (chunk == null) {
- if (onComplete != null) {
- onComplete.accept(null);
- }
- } else {
- if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
- } else {
- if (onComplete != null) {
- onComplete.accept(null);
- }
- }
- }
- });
+ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete);
}
- static final net.minecraft.server.level.TicketType<Long> CHUNK_LOAD = net.minecraft.server.level.TicketType.create("chunk_load", Long::compareTo);
-
- private static long chunkLoadCounter = 0L;
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) {
- if (!org.bukkit.Bukkit.isPrimaryThread()) {
- scheduleChunkTask(level, chunkX, chunkZ, () -> {
- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
- }, priority);
- return;
- }
-
- final int minLevel = 33 + getDistance(toStatus);
- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
- final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
-
- if (addTicket) {
- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
- }
- level.chunkSource.runDistanceManagerUpdates();
-
- final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> {
- try {
- if (onComplete != null) {
- onComplete.accept(chunk);
- }
- } catch (final Throwable thr) {
- LOGGER.error("Exception handling chunk load callback", thr);
- com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
- } finally {
- if (addTicket) {
- level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
- }
- }
- };
-
- final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-
- if (holder == null || holder.getTicketLevel() > minLevel) {
- loadCallback.accept(null);
- return;
- }
-
- final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap);
-
- if (loadFuture.isDone()) {
- loadCallback.accept(loadFuture.join().orElse(null));
- return;
- }
-
- loadFuture.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess> result, final Throwable thr) -> {
- if (thr != null) {
- loadCallback.accept(null);
- return;
- }
- loadCallback.accept(result.orElse(null));
- }, (final Runnable r) -> {
- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
- });
+ ((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) {
- // This method goes unused until the chunk system rewrite
- if (toStatus == FullChunkStatus.INACCESSIBLE) {
- throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
- }
-
- if (!org.bukkit.Bukkit.isPrimaryThread()) {
- scheduleChunkTask(level, chunkX, chunkZ, () -> {
- scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
- }, priority);
- return;
- }
-
- final int minLevel = 33 - (toStatus.ordinal() - 1);
- final int radius = toStatus.ordinal() - 1;
- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
- final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
-
- if (addTicket) {
- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
- }
- level.chunkSource.runDistanceManagerUpdates();
-
- final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> {
- try {
- if (onComplete != null) {
- onComplete.accept(chunk);
- }
- } catch (final Throwable thr) {
- LOGGER.error("Exception handling chunk load callback", thr);
- com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
- } finally {
- if (addTicket) {
- level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
- }
- }
- };
-
- final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-
- if (holder == null || holder.getTicketLevel() > minLevel) {
- loadCallback.accept(null);
- return;
- }
-
- final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk>> tickingState;
- switch (toStatus) {
- case FULL: {
- tickingState = holder.getFullChunkFuture();
- break;
- }
- case BLOCK_TICKING: {
- tickingState = holder.getTickingChunkFuture();
- break;
- }
- case ENTITY_TICKING: {
- tickingState = holder.getEntityTickingChunkFuture();
- break;
- }
- default: {
- throw new IllegalStateException("Cannot reach here");
- }
- }
-
- if (tickingState.isDone()) {
- loadCallback.accept(tickingState.join().orElse(null));
- return;
- }
-
- tickingState.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk> result, final Throwable thr) -> {
- if (thr != null) {
- loadCallback.accept(null);
- return;
- }
- loadCallback.accept(result.orElse(null));
- }, (final Runnable r) -> {
- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
- });
+ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
}
public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
- return new java.util.ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
+ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
}
public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
- return new java.util.ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
+ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
}
public static int getVisibleChunkHolderCount(final ServerLevel level) {
- return level.chunkSource.chunkMap.visibleChunkMap.size();
+ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
}
public static int getUpdatingChunkHolderCount(final ServerLevel level) {
- return level.chunkSource.chunkMap.updatingChunkMap.size();
+ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
}
public static boolean hasAnyChunkHolders(final ServerLevel level) {
@@ -232,52 +79,85 @@ public final class ChunkSystem {
}
- public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
+ public static void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) {
+ ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource())
+ .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, chunk);
+ }
+ public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
+ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().add(
+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
+ );
+ chunk.loadCallback();
}
public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
+ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().remove(
+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
+ );
+ chunk.unloadCallback();
+ }
+ public static void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
+ ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource())
+ .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, null);
}
public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
-
+ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().add(
+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
+ );
+ if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) {
+ chunk.postProcessGeneration();
+ }
+ ((ServerLevel)chunk.getLevel()).startTickingChunk(chunk);
+ ((ServerLevel)chunk.getLevel()).getChunkSource().chunkMap.tickingGenerated.incrementAndGet();
}
public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
-
+ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove(
+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
+ );
}
public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
-
+ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().add(
+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
+ );
}
public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
-
+ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().remove(
+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
+ );
}
public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
- return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ);
+ return null;
}
public static int getSendViewDistance(final ServerPlayer player) {
- return getLoadViewDistance(player);
+ return RegionizedPlayerChunkLoader.getAPISendViewDistance(player);
}
public static int getLoadViewDistance(final ServerPlayer player) {
- final ServerLevel level = player.serverLevel();
- if (level == null) {
- return org.bukkit.Bukkit.getViewDistance();
- }
- return level.chunkSource.chunkMap.getPlayerViewDistance(player);
+ return RegionizedPlayerChunkLoader.getLoadViewDistance(player);
}
public static int getTickViewDistance(final ServerPlayer player) {
- final ServerLevel level = player.serverLevel();
- if (level == null) {
- return org.bukkit.Bukkit.getSimulationDistance();
- }
- return level.chunkSource.chunkMap.distanceManager.simulationDistance;
+ return RegionizedPlayerChunkLoader.getAPITickViewDistance(player);
+ }
+
+ public static void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player) {
+ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().addPlayer(player);
+ }
+
+ public static void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player) {
+ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().removePlayer(player);
+ }
+
+ public static void updateMaps(final ServerLevel world, final ServerPlayer player) {
+ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().updatePlayer(player);
}
private ChunkSystem() {}
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 7deb341b7e2b4592ae3f88733d6cacf6e58764e4..11b7f15755dde766140c29bedca456c80d53293f 100644
--- a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
@@ -1,9 +1,9 @@
package ca.spottedleaf.moonrise.common.util;
import net.minecraft.core.BlockPos;
-import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import org.slf4j.Logger;
@@ -26,21 +26,21 @@ public class TickThread extends Thread {
}
}
- public static void ensureTickThread(final ServerLevel world, final BlockPos pos, final String reason) {
+ public static void ensureTickThread(final Level world, final BlockPos pos, final String reason) {
if (!isTickThreadFor(world, pos)) {
LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
throw new IllegalStateException(reason);
}
}
- public static void ensureTickThread(final ServerLevel world, final ChunkPos pos, final String reason) {
+ public static void ensureTickThread(final Level world, final ChunkPos pos, final String reason) {
if (!isTickThreadFor(world, pos)) {
LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
throw new IllegalStateException(reason);
}
}
- public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) {
+ public static void ensureTickThread(final Level world, final int chunkX, final int chunkZ, final String reason) {
if (!isTickThreadFor(world, chunkX, chunkZ)) {
LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
throw new IllegalStateException(reason);
@@ -54,14 +54,14 @@ public class TickThread extends Thread {
}
}
- public static void ensureTickThread(final ServerLevel world, final AABB aabb, final String reason) {
+ public static void ensureTickThread(final Level world, final AABB aabb, final String reason) {
if (!isTickThreadFor(world, aabb)) {
LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
throw new IllegalStateException(reason);
}
}
- public static void ensureTickThread(final ServerLevel world, final double blockX, final double blockZ, final String reason) {
+ public static void ensureTickThread(final Level world, final double blockX, final double blockZ, final String reason) {
if (!isTickThreadFor(world, blockX, blockZ)) {
LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
throw new IllegalStateException(reason);
@@ -90,46 +90,46 @@ public class TickThread extends Thread {
}
public static boolean isTickThread() {
- return org.bukkit.Bukkit.isPrimaryThread(); // Paper
+ return Thread.currentThread() instanceof TickThread;
}
public static boolean isShutdownThread() {
return false;
}
- public static boolean isTickThreadFor(final ServerLevel world, final BlockPos pos) {
+ public static boolean isTickThreadFor(final Level world, final BlockPos pos) {
return isTickThread();
}
- public static boolean isTickThreadFor(final ServerLevel world, final ChunkPos pos) {
+ public static boolean isTickThreadFor(final Level world, final ChunkPos pos) {
return isTickThread();
}
- public static boolean isTickThreadFor(final ServerLevel world, final Vec3 pos) {
+ public static boolean isTickThreadFor(final Level world, final Vec3 pos) {
return isTickThread();
}
- public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) {
+ public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ) {
return isTickThread();
}
- public static boolean isTickThreadFor(final ServerLevel world, final AABB aabb) {
+ public static boolean isTickThreadFor(final Level world, final AABB aabb) {
return isTickThread();
}
- public static boolean isTickThreadFor(final ServerLevel world, final double blockX, final double blockZ) {
+ public static boolean isTickThreadFor(final Level world, final double blockX, final double blockZ) {
return isTickThread();
}
- public static boolean isTickThreadFor(final ServerLevel world, final Vec3 position, final Vec3 deltaMovement, final int buffer) {
+ public static boolean isTickThreadFor(final Level world, final Vec3 position, final Vec3 deltaMovement, final int buffer) {
return isTickThread();
}
- public static boolean isTickThreadFor(final ServerLevel world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) {
+ public static boolean isTickThreadFor(final Level world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) {
return isTickThread();
}
- public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) {
+ public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ, final int radius) {
return isTickThread();
}
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
new file mode 100644
index 0000000000000000000000000000000000000000..aef4fc0d3c272febe675d1ac846b88e58b4e7533
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java
@@ -0,0 +1,10 @@
+package ca.spottedleaf.moonrise.patches.block_counting;
+
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+
+public interface BlockCountingBitStorage {
+
+ public Int2ObjectOpenHashMap<IntArrayList> 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
new file mode 100644
index 0000000000000000000000000000000000000000..a08ddb0598d44368af5b6bace971ee31edf9919e
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java
@@ -0,0 +1,11 @@
+package ca.spottedleaf.moonrise.patches.block_counting;
+
+import ca.spottedleaf.moonrise.common.list.IBlockDataList;
+
+public interface BlockCountingChunkSection {
+
+ public int moonrise$getSpecialCollidingBlocks();
+
+ public IBlockDataList moonrise$getTickingBlockList();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java
new file mode 100644
index 0000000000000000000000000000000000000000..08338917dc61c856eaba0b76e05c1497c458399d
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.chunk_getblock;
+
+import net.minecraft.world.level.block.state.BlockState;
+
+public interface GetBlockChunk {
+
+ public BlockState moonrise$getBlock(final int x, final int y, final int z);
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..49160a30b8e19e5c5ada811fbcae2a05959524f3
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
@@ -0,0 +1,38 @@
+package ca.spottedleaf.moonrise.patches.chunk_system;
+
+import net.minecraft.SharedConstants;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.Tag;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.util.datafix.DataFixTypes;
+
+public final class ChunkSystemConverters {
+
+ // See SectionStorage#getVersion
+ private static final int DEFAULT_POI_DATA_VERSION = 1945;
+
+ private static final int DEFAULT_ENTITY_CHUNK_DATA_VERSION = -1;
+
+ private static int getCurrentVersion() {
+ return SharedConstants.getCurrentVersion().getDataVersion().getVersion();
+ }
+
+ private static int getDataVersion(final CompoundTag data, final int dfl) {
+ return !data.contains(SharedConstants.DATA_VERSION_TAG, Tag.TAG_ANY_NUMERIC)
+ ? dfl : data.getInt(SharedConstants.DATA_VERSION_TAG);
+ }
+
+ public static CompoundTag convertPoiCompoundTag(final CompoundTag data, final ServerLevel world) {
+ final int dataVersion = getDataVersion(data, DEFAULT_POI_DATA_VERSION);
+
+ return DataFixTypes.POI_CHUNK.update(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);
+
+ return DataFixTypes.ENTITY_CHUNK.update(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
new file mode 100644
index 0000000000000000000000000000000000000000..67f6dd9a4855611cfe242c2e37e90f6d27d4c823
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java
@@ -0,0 +1,36 @@
+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
new file mode 100644
index 0000000000000000000000000000000000000000..becd1c6d54ed6c912aee3a9178a970e2751d3694
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java
@@ -0,0 +1,11 @@
+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
new file mode 100644
index 0000000000000000000000000000000000000000..2c279854bdf214538380fa354e4298ec4bd9ac4e
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
@@ -0,0 +1,39 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.entity;
+
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.monster.Shulker;
+import net.minecraft.world.entity.vehicle.AbstractMinecart;
+import net.minecraft.world.entity.vehicle.Boat;
+
+public interface ChunkSystemEntity {
+
+ public boolean moonrise$isHardColliding();
+
+ // for mods to override
+ public default boolean moonrise$isHardCollidingUncached() {
+ return this instanceof Boat || this instanceof AbstractMinecart || this instanceof Shulker || ((Entity)this).canBeCollidedWith();
+ }
+
+ public FullChunkStatus moonrise$getChunkStatus();
+
+ public void moonrise$setChunkStatus(final FullChunkStatus status);
+
+ public int moonrise$getSectionX();
+
+ public void moonrise$setSectionX(final int x);
+
+ public int moonrise$getSectionY();
+
+ public void moonrise$setSectionY(final int y);
+
+ public int moonrise$getSectionZ();
+
+ public void moonrise$setSectionZ(final int z);
+
+ public boolean moonrise$isUpdatingSectionStatus();
+
+ public void moonrise$setUpdatingSectionStatus(final boolean to);
+
+ public boolean moonrise$hasAnyPlayerPassengers();
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..73df26b27146bbad2106d57b22dd3c792ed3dd1d
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
@@ -0,0 +1,14 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.io;
+
+import net.minecraft.world.level.chunk.storage.RegionFile;
+import java.io.IOException;
+
+public interface ChunkSystemRegionFileStorage {
+
+ public boolean moonrise$doesRegionFileNotExistNoIO(final int chunkX, final int chunkZ);
+
+ public RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ);
+
+ public RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException;
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d8da6b215
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java
@@ -0,0 +1,1240 @@
+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
new file mode 100644
index 0000000000000000000000000000000000000000..c35e0c29700be48dda3e53e7d2db224766ef17b7
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
@@ -0,0 +1,56 @@
+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.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 {
+
+ private final ServerLevel world;
+
+ public ChunkDataController(final ServerLevel world) {
+ super(RegionFileIOThread.RegionFileType.CHUNK_DATA);
+ this.world = world;
+ }
+
+ @Override
+ public RegionFileStorage getCache() {
+ return ((ChunkSystemChunkStorage)this.world.getChunkSource().chunkMap).moonrise$getRegionStorage();
+ }
+
+ @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;
+ }
+ }
+
+ @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;
+ }
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..fdd189ef056187941d43809c5d61cab717aecf60
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
@@ -0,0 +1,55 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
+
+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.storage.EntityStorage;
+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
+import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
+import java.io.IOException;
+import java.nio.file.Path;
+
+public final class EntityDataController extends RegionFileIOThread.ChunkDataController {
+
+ private final EntityRegionFileStorage storage;
+
+ public EntityDataController(final EntityRegionFileStorage storage) {
+ super(RegionFileIOThread.RegionFileType.ENTITY_DATA);
+ this.storage = storage;
+ }
+
+ @Override
+ public RegionFileStorage getCache() {
+ return this.storage;
+ }
+
+ @Override
+ public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
+ this.storage.write(new ChunkPos(chunkX, chunkZ), compound);
+ }
+
+ @Override
+ public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException {
+ return this.storage.read(new ChunkPos(chunkX, chunkZ));
+ }
+
+ public static final class EntityRegionFileStorage extends RegionFileStorage {
+
+ public EntityRegionFileStorage(final RegionStorageInfo regionStorageInfo, final Path directory,
+ final boolean dsync) {
+ super(regionStorageInfo, directory, dsync);
+ }
+
+ @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
+ );
+ }
+ 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
new file mode 100644
index 0000000000000000000000000000000000000000..af867f8fedd0bb8f675e94243aa1a3f17363483b
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
@@ -0,0 +1,33 @@
+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.level.storage.ChunkSystemSectionStorage;
+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 {
+
+ private final ServerLevel world;
+
+ public PoiDataController(final ServerLevel world) {
+ super(RegionFileIOThread.RegionFileType.POI_DATA);
+ this.world = world;
+ }
+
+ @Override
+ public RegionFileStorage getCache() {
+ return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$getRegionStorage();
+ }
+
+ @Override
+ public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
+ ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$write(chunkX, chunkZ, compound);
+ }
+
+ @Override
+ public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException {
+ return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$read(chunkX, chunkZ);
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..efcd9057f008f0b9cf0d22b2b21d1851205841e5
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
@@ -0,0 +1,22 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level;
+
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+
+public interface ChunkSystemLevel {
+
+ public EntityLookup moonrise$getEntityLookup();
+
+ public void moonrise$setEntityLookup(final EntityLookup entityLookup);
+
+ public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ);
+
+ public ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ);
+
+ public ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus);
+
+ public void moonrise$midTickTasks();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..0b58701342d573fa43cdd06681534854a0e51d77
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java
@@ -0,0 +1,10 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level;
+
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+
+public interface ChunkSystemLevelReader {
+
+ public ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status);
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..b8a87b7e6505feb76ce1bd58c84615256cf6faa6
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
@@ -0,0 +1,61 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+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.player.RegionizedPlayerChunkLoader;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.level.ServerChunkCache;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import java.util.List;
+import java.util.function.Consumer;
+
+public interface ChunkSystemServerLevel extends ChunkSystemLevel {
+
+ public ChunkTaskScheduler moonrise$getChunkTaskScheduler();
+
+ public RegionFileIOThread.ChunkDataController moonrise$getChunkDataController();
+
+ public RegionFileIOThread.ChunkDataController moonrise$getPoiChunkDataController();
+
+ public RegionFileIOThread.ChunkDataController moonrise$getEntityChunkDataController();
+
+ public int moonrise$getRegionChunkShift();
+
+ // Paper - marked closing not needed on CB
+
+ public RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader();
+
+ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
+ final PrioritisedExecutor.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 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 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 Consumer<List<ChunkAccess>> onLoad);
+
+ public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder();
+
+ public long moonrise$getLastMidTickFailure();
+
+ public void moonrise$setLastMidTickFailure(final long time);
+
+ public NearbyPlayers moonrise$getNearbyPlayers();
+
+ public ReferenceList<ServerChunkCache.ChunkAndHolder> moonrise$getLoadedChunks();
+
+ public ReferenceList<ServerChunkCache.ChunkAndHolder> moonrise$getTickingChunks();
+
+ public ReferenceList<ServerChunkCache.ChunkAndHolder> moonrise$getEntityTickingChunks();
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..7d049d750df88762566f13a9c4fc7574a2df4825
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java
@@ -0,0 +1,26 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
+
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.level.chunk.LevelChunk;
+import java.util.List;
+
+public interface ChunkSystemChunkHolder {
+
+ public NewChunkHolder moonrise$getRealChunkHolder();
+
+ public void moonrise$setRealChunkHolder(final NewChunkHolder newChunkHolder);
+
+ public void moonrise$addReceivedChunk(final ServerPlayer player);
+
+ public void moonrise$removeReceivedChunk(final ServerPlayer player);
+
+ public boolean moonrise$hasChunkBeenSent();
+
+ public boolean moonrise$hasChunkBeenSent(final ServerPlayer to);
+
+ public List<ServerPlayer> moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge);
+
+ public LevelChunk moonrise$getFullChunk();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java
new file mode 100644
index 0000000000000000000000000000000000000000..f4bc44bb266763345c4e6f859c89352c769a104d
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java
@@ -0,0 +1,26 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
+
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public interface ChunkSystemChunkStatus {
+
+ public boolean moonrise$isParallelCapable();
+
+ public void moonrise$setParallelCapable(final boolean value);
+
+ public int moonrise$getWriteRadius();
+
+ public void moonrise$setWriteRadius(final int value);
+
+ public ChunkStatus moonrise$getNextStatus();
+
+ public boolean moonrise$isEmptyLoadStatus();
+
+ public void moonrise$setEmptyLoadStatus(final boolean value);
+
+ public boolean moonrise$isEmptyGenStatus();
+
+ public AtomicBoolean moonrise$getWarnedAboutNoImmediateComplete();
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..883fe6401f1b9711fa544d18a815b4d638f580df
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
+
+import net.minecraft.server.level.ChunkMap;
+
+public interface ChunkSystemDistanceManager {
+
+ public ChunkMap moonrise$getChunkMap();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java
new file mode 100644
index 0000000000000000000000000000000000000000..5b092bca7027e37aeee8f4b852ad896dd0d5febc
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java
@@ -0,0 +1,13 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
+
+import net.minecraft.server.level.ServerChunkCache;
+
+public interface ChunkSystemLevelChunk {
+
+ public boolean moonrise$isPostProcessingDone();
+
+ public ServerChunkCache.ChunkAndHolder moonrise$getChunkAndHolder();
+
+ public void moonrise$setChunkAndHolder(final ServerChunkCache.ChunkAndHolder holder);
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..997b05167c19472acb98edac32d4548cc65efa8e
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
@@ -0,0 +1,819 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.entity;
+
+import ca.spottedleaf.moonrise.common.list.EntityList;
+import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity;
+import com.google.common.collect.ImmutableList;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.ListTag;
+import net.minecraft.nbt.NbtUtils;
+import net.minecraft.nbt.Tag;
+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.EntityType;
+import net.minecraft.world.entity.boss.EnderDragonPart;
+import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.chunk.storage.EntityStorage;
+import net.minecraft.world.level.entity.Visibility;
+import net.minecraft.world.phys.AABB;
+import java.util.ArrayList;
+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 {
+
+ public final int minSection;
+ public final int maxSection;
+ public final int chunkX;
+ public final int chunkZ;
+ public final Level world;
+
+ private final EntityCollectionBySection allEntities;
+ private final EntityCollectionBySection hardCollidingEntities;
+ private final Reference2ObjectOpenHashMap<Class<? extends Entity>, EntityCollectionBySection> entitiesByClass;
+ private final Reference2ObjectOpenHashMap<EntityType<?>, EntityCollectionBySection> entitiesByType;
+ private final EntityList entities = new EntityList();
+
+ public FullChunkStatus status;
+
+ private boolean isTransient;
+
+ public boolean isTransient() {
+ return this.isTransient;
+ }
+
+ public void setTransient(final boolean value) {
+ this.isTransient = value;
+ }
+
+ public ChunkEntitySlices(final Level world, final int chunkX, final int chunkZ, final FullChunkStatus status,
+ final int minSection, final int maxSection) { // inclusive, inclusive
+ this.minSection = minSection;
+ this.maxSection = maxSection;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.world = world;
+
+ this.allEntities = new EntityCollectionBySection(this);
+ this.hardCollidingEntities = new EntityCollectionBySection(this);
+ this.entitiesByClass = new Reference2ObjectOpenHashMap<>();
+ this.entitiesByType = new Reference2ObjectOpenHashMap<>();
+
+ this.status = status;
+ }
+
+ 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());
+ }
+
+ // Paper start - rewrite chunk system
+ public static void copyEntities(final CompoundTag from, final CompoundTag into) {
+ if (from == null) {
+ return;
+ }
+ final ListTag entitiesFrom = from.getList("Entities", Tag.TAG_COMPOUND);
+ if (entitiesFrom == null || entitiesFrom.isEmpty()) {
+ return;
+ }
+
+ final ListTag entitiesInto = into.getList("Entities", Tag.TAG_COMPOUND);
+ into.put("Entities", entitiesInto); // this is in case into doesn't have any entities
+ entitiesInto.addAll(0, entitiesFrom);
+ }
+
+ public static CompoundTag saveEntityChunk(final List<Entity> entities, final ChunkPos chunkPos, final ServerLevel world) {
+ return saveEntityChunk0(entities, chunkPos, world, false);
+ }
+
+ public static CompoundTag saveEntityChunk0(final List<Entity> entities, final ChunkPos chunkPos, final ServerLevel world, final boolean force) {
+ if (!force && entities.isEmpty()) {
+ return null;
+ }
+
+ final ListTag entitiesTag = new ListTag();
+ for (final Entity entity : entities) {
+ CompoundTag compoundTag = new CompoundTag();
+ if (entity.save(compoundTag)) {
+ entitiesTag.add(compoundTag);
+ }
+ }
+ final CompoundTag ret = NbtUtils.addCurrentDataVersion(new CompoundTag());
+ ret.put("Entities", entitiesTag);
+ EntityStorage.writeChunkPos(ret, chunkPos);
+
+ return !force && entitiesTag.isEmpty() ? null : ret;
+ }
+
+ public CompoundTag save() {
+ final int len = this.entities.size();
+ if (len == 0) {
+ return null;
+ }
+
+ final Entity[] rawData = this.entities.getRawData();
+ final List<Entity> collectedEntities = new ArrayList<>(len);
+ for (int i = 0; i < len; ++i) {
+ final Entity entity = rawData[i];
+ if (entity.shouldBeSaved()) {
+ collectedEntities.add(entity);
+ }
+ }
+
+ if (collectedEntities.isEmpty()) {
+ return null;
+ }
+
+ return saveEntityChunk(collectedEntities, new ChunkPos(this.chunkX, this.chunkZ), (ServerLevel)this.world);
+ }
+
+ // returns true if this chunk has transient entities remaining
+ public boolean unload() {
+ final int len = this.entities.size();
+ final Entity[] collectedEntities = Arrays.copyOf(this.entities.getRawData(), len);
+
+ for (int i = 0; i < len; ++i) {
+ final Entity entity = collectedEntities[i];
+ if (entity.isRemoved()) {
+ // removed by us below
+ continue;
+ }
+ if (entity.shouldBeSaved()) {
+ entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD);
+ 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);
+ }
+ }
+ }
+ }
+
+ 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) {
+ return new ArrayList<>();
+ }
+
+ final Entity[] rawData = this.entities.getRawData();
+ final List<Entity> collectedEntities = new ArrayList<>(len);
+ for (int i = 0; i < len; ++i) {
+ collectedEntities.add(rawData[i]);
+ }
+
+ return collectedEntities;
+ }
+
+ public boolean isEmpty() {
+ return this.entities.size() == 0;
+ }
+
+ public void mergeInto(final ChunkEntitySlices slices) {
+ 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];
+ slices.addEntity(entity, ((ChunkSystemEntity)entity).moonrise$getSectionY());
+ }
+ }
+
+ private boolean preventStatusUpdates;
+ public boolean startPreventingStatusUpdates() {
+ final boolean ret = this.preventStatusUpdates;
+ this.preventStatusUpdates = true;
+ return ret;
+ }
+
+ public boolean isPreventingStatusUpdates() {
+ return this.preventStatusUpdates;
+ }
+
+ public void stopPreventingStatusUpdates(final boolean prev) {
+ this.preventStatusUpdates = prev;
+ }
+
+ public void updateStatus(final FullChunkStatus status, final EntityLookup lookup) {
+ this.status = status;
+
+ final Entity[] entities = this.entities.getRawData();
+
+ for (int i = 0, size = this.entities.size(); i < size; ++i) {
+ final Entity entity = entities[i];
+
+ final Visibility oldVisibility = EntityLookup.getEntityStatus(entity);
+ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(status);
+ final Visibility newVisibility = EntityLookup.getEntityStatus(entity);
+
+ lookup.entityStatusChange(entity, this, oldVisibility, newVisibility, false, false, false);
+ }
+ }
+
+ public boolean addEntity(final Entity entity, final int chunkSection) {
+ if (!this.entities.add(entity)) {
+ return false;
+ }
+ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(this.status);
+ final int sectionIndex = chunkSection - this.minSection;
+
+ this.allEntities.addEntity(entity, sectionIndex);
+
+ if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) {
+ this.hardCollidingEntities.addEntity(entity, sectionIndex);
+ }
+
+ for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator =
+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
+ final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next();
+
+ if (entry.getKey().isInstance(entity)) {
+ entry.getValue().addEntity(entity, sectionIndex);
+ }
+ }
+
+ EntityCollectionBySection byType = this.entitiesByType.get(entity.getType());
+ if (byType != null) {
+ byType.addEntity(entity, sectionIndex);
+ } else {
+ this.entitiesByType.put(entity.getType(), byType = new EntityCollectionBySection(this));
+ byType.addEntity(entity, sectionIndex);
+ }
+
+ return true;
+ }
+
+ public boolean removeEntity(final Entity entity, final int chunkSection) {
+ if (!this.entities.remove(entity)) {
+ return false;
+ }
+ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(null);
+ final int sectionIndex = chunkSection - this.minSection;
+
+ this.allEntities.removeEntity(entity, sectionIndex);
+
+ if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) {
+ this.hardCollidingEntities.removeEntity(entity, sectionIndex);
+ }
+
+ for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator =
+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
+ final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next();
+
+ if (entry.getKey().isInstance(entity)) {
+ entry.getValue().removeEntity(entity, sectionIndex);
+ }
+ }
+
+ final EntityCollectionBySection byType = this.entitiesByType.get(entity.getType());
+ byType.removeEntity(entity, sectionIndex);
+
+ return true;
+ }
+
+ public void getHardCollidingEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ this.hardCollidingEntities.getEntities(except, box, into, predicate);
+ }
+
+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate);
+ }
+
+ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ this.allEntities.getEntities(except, box, into, predicate);
+ }
+
+
+ public boolean getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
+ final int maxCount) {
+ return this.allEntities.getEntitiesWithEnderDragonPartsLimited(except, box, into, predicate, maxCount);
+ }
+
+ public boolean getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
+ final int maxCount) {
+ return this.allEntities.getEntitiesLimited(except, box, into, predicate, maxCount);
+ }
+
+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate) {
+ final EntityCollectionBySection byType = this.entitiesByType.get(type);
+
+ if (byType != null) {
+ byType.getEntities((Entity)null, box, (List)into, (Predicate) predicate);
+ }
+ }
+
+ public <T extends Entity> boolean getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate, final int maxCount) {
+ final EntityCollectionBySection byType = this.entitiesByType.get(type);
+
+ if (byType != null) {
+ return byType.getEntitiesLimited((Entity)null, box, (List)into, (Predicate)predicate, maxCount);
+ }
+
+ return false;
+ }
+
+ protected EntityCollectionBySection initClass(final Class<? extends Entity> clazz) {
+ final EntityCollectionBySection ret = new EntityCollectionBySection(this);
+
+ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) {
+ final BasicEntityList<Entity> sectionEntities = this.allEntities.entitiesBySection[sectionIndex];
+ if (sectionEntities == null) {
+ continue;
+ }
+
+ final Entity[] storage = sectionEntities.storage;
+
+ for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) {
+ final Entity entity = storage[i];
+
+ if (clazz.isInstance(entity)) {
+ ret.addEntity(entity, sectionIndex);
+ }
+ }
+ }
+
+ return ret;
+ }
+
+ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate) {
+ EntityCollectionBySection collection = this.entitiesByClass.get(clazz);
+ if (collection != null) {
+ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate);
+ } else {
+ this.entitiesByClass.put(clazz, collection = this.initClass(clazz));
+ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate);
+ }
+ }
+
+ public <T extends Entity> boolean getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate, final int maxCount) {
+ EntityCollectionBySection collection = this.entitiesByClass.get(clazz);
+ if (collection != null) {
+ return collection.getEntitiesWithEnderDragonPartsLimited(except, clazz, box, (List)into, (Predicate)predicate, maxCount);
+ } else {
+ this.entitiesByClass.put(clazz, collection = this.initClass(clazz));
+ return collection.getEntitiesWithEnderDragonPartsLimited(except, clazz, box, (List)into, (Predicate)predicate, maxCount);
+ }
+ }
+
+ private static final class BasicEntityList<E extends Entity> {
+
+ private static final Entity[] EMPTY = new Entity[0];
+ private static final int DEFAULT_CAPACITY = 4;
+
+ private E[] storage;
+ private int size;
+
+ public BasicEntityList() {
+ this(0);
+ }
+
+ public BasicEntityList(final int cap) {
+ this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]);
+ }
+
+ public boolean isEmpty() {
+ return this.size == 0;
+ }
+
+ public int size() {
+ return this.size;
+ }
+
+ private void resize() {
+ if (this.storage == EMPTY) {
+ this.storage = (E[])new Entity[DEFAULT_CAPACITY];
+ } else {
+ this.storage = Arrays.copyOf(this.storage, this.storage.length * 2);
+ }
+ }
+
+ public void add(final E entity) {
+ final int idx = this.size++;
+ if (idx >= this.storage.length) {
+ this.resize();
+ this.storage[idx] = entity;
+ } else {
+ this.storage[idx] = entity;
+ }
+ }
+
+ public int indexOf(final E entity) {
+ final E[] storage = this.storage;
+
+ for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) {
+ if (storage[i] == entity) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ public boolean remove(final E entity) {
+ final int idx = this.indexOf(entity);
+ if (idx == -1) {
+ return false;
+ }
+
+ final int size = --this.size;
+ final E[] storage = this.storage;
+ if (idx != size) {
+ System.arraycopy(storage, idx + 1, storage, idx, size - idx);
+ }
+
+ storage[size] = null;
+
+ return true;
+ }
+
+ public boolean has(final E entity) {
+ return this.indexOf(entity) != -1;
+ }
+ }
+
+ private static final class EntityCollectionBySection {
+
+ private final ChunkEntitySlices slices;
+ private final BasicEntityList<Entity>[] entitiesBySection;
+ private int count;
+
+ public EntityCollectionBySection(final ChunkEntitySlices slices) {
+ this.slices = slices;
+
+ final int sectionCount = slices.maxSection - slices.minSection + 1;
+
+ this.entitiesBySection = new BasicEntityList[sectionCount];
+ }
+
+ public void addEntity(final Entity entity, final int sectionIndex) {
+ BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex];
+
+ if (list != null && list.has(entity)) {
+ return;
+ }
+
+ if (list == null) {
+ this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>();
+ }
+
+ list.add(entity);
+ ++this.count;
+ }
+
+ public void removeEntity(final Entity entity, final int sectionIndex) {
+ final BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex];
+
+ if (list == null || !list.remove(entity)) {
+ return;
+ }
+
+ --this.count;
+
+ if (list.isEmpty()) {
+ this.entitiesBySection[sectionIndex] = null;
+ }
+ }
+
+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ if (this.count == 0) {
+ return;
+ }
+
+ final int minSection = this.slices.minSection;
+ final int maxSection = this.slices.maxSection;
+
+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
+
+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
+
+ for (int section = min; section <= max; ++section) {
+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
+
+ if (list == null) {
+ continue;
+ }
+
+ final Entity[] storage = list.storage;
+
+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
+ final Entity entity = storage[i];
+
+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate != null && !predicate.test(entity)) {
+ continue;
+ }
+
+ into.add(entity);
+ }
+ }
+ }
+
+ public boolean getEntitiesLimited(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
+ final int maxCount) {
+ if (this.count == 0) {
+ return false;
+ }
+
+ final int minSection = this.slices.minSection;
+ final int maxSection = this.slices.maxSection;
+
+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
+
+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
+
+ for (int section = min; section <= max; ++section) {
+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
+
+ if (list == null) {
+ continue;
+ }
+
+ final Entity[] storage = list.storage;
+
+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
+ final Entity entity = storage[i];
+
+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate != null && !predicate.test(entity)) {
+ continue;
+ }
+
+ into.add(entity);
+ if (into.size() >= maxCount) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public void getEntitiesWithEnderDragonParts(final Entity except, final AABB box, final List<Entity> into,
+ final Predicate<? super Entity> predicate) {
+ if (this.count == 0) {
+ return;
+ }
+
+ final int minSection = this.slices.minSection;
+ final int maxSection = this.slices.maxSection;
+
+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
+
+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
+
+ for (int section = min; section <= max; ++section) {
+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
+
+ if (list == null) {
+ continue;
+ }
+
+ final Entity[] storage = list.storage;
+
+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
+ final Entity entity = storage[i];
+
+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate == null || predicate.test(entity)) {
+ into.add(entity);
+ } // else: continue to test the ender dragon parts
+
+ if (entity instanceof EnderDragon) {
+ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) {
+ if (part == except || !part.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate != null && !predicate.test(part)) {
+ continue;
+ }
+
+ into.add(part);
+ }
+ }
+ }
+ }
+ }
+
+ public boolean getEntitiesWithEnderDragonPartsLimited(final Entity except, final AABB box, final List<Entity> into,
+ final Predicate<? super Entity> predicate, final int maxCount) {
+ if (this.count == 0) {
+ return false;
+ }
+
+ final int minSection = this.slices.minSection;
+ final int maxSection = this.slices.maxSection;
+
+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
+
+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
+
+ for (int section = min; section <= max; ++section) {
+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
+
+ if (list == null) {
+ continue;
+ }
+
+ final Entity[] storage = list.storage;
+
+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
+ final Entity entity = storage[i];
+
+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate == null || predicate.test(entity)) {
+ into.add(entity);
+ if (into.size() >= maxCount) {
+ return true;
+ }
+ } // else: continue to test the ender dragon parts
+
+ if (entity instanceof EnderDragon) {
+ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) {
+ if (part == except || !part.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate != null && !predicate.test(part)) {
+ continue;
+ }
+
+ into.add(part);
+ if (into.size() >= maxCount) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public void getEntitiesWithEnderDragonParts(final Entity except, final Class<?> clazz, final AABB box, final List<Entity> into,
+ final Predicate<? super Entity> predicate) {
+ if (this.count == 0) {
+ return;
+ }
+
+ final int minSection = this.slices.minSection;
+ final int maxSection = this.slices.maxSection;
+
+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
+
+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
+
+ for (int section = min; section <= max; ++section) {
+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
+
+ if (list == null) {
+ continue;
+ }
+
+ final Entity[] storage = list.storage;
+
+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
+ final Entity entity = storage[i];
+
+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate == null || predicate.test(entity)) {
+ into.add(entity);
+ } // else: continue to test the ender dragon parts
+
+ if (entity instanceof EnderDragon) {
+ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) {
+ if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) {
+ continue;
+ }
+
+ if (predicate != null && !predicate.test(part)) {
+ continue;
+ }
+
+ into.add(part);
+ }
+ }
+ }
+ }
+ }
+
+ public boolean getEntitiesWithEnderDragonPartsLimited(final Entity except, final Class<?> clazz, final AABB box, final List<Entity> into,
+ final Predicate<? super Entity> predicate, final int maxCount) {
+ if (this.count == 0) {
+ return false;
+ }
+
+ final int minSection = this.slices.minSection;
+ final int maxSection = this.slices.maxSection;
+
+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
+
+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
+
+ for (int section = min; section <= max; ++section) {
+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
+
+ if (list == null) {
+ continue;
+ }
+
+ final Entity[] storage = list.storage;
+
+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
+ final Entity entity = storage[i];
+
+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate == null || predicate.test(entity)) {
+ into.add(entity);
+ if (into.size() >= maxCount) {
+ return true;
+ }
+ } // else: continue to test the ender dragon parts
+
+ if (entity instanceof EnderDragon) {
+ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) {
+ if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) {
+ continue;
+ }
+
+ if (predicate != null && !predicate.test(part)) {
+ continue;
+ }
+
+ into.add(part);
+ if (into.size() >= maxCount) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..efc0c1acc8239dd7b00211a1d3bfd3fc3b2c810c
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
@@ -0,0 +1,1083 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.entity;
+
+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
+import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable;
+import ca.spottedleaf.moonrise.common.list.EntityList;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity;
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.util.AbortableIterationConsumer;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.entity.EntityInLevelCallback;
+import net.minecraft.world.level.entity.EntityTypeTest;
+import net.minecraft.world.level.entity.LevelCallback;
+import net.minecraft.world.level.entity.LevelEntityGetter;
+import net.minecraft.world.level.entity.Visibility;
+import net.minecraft.world.phys.AABB;
+import net.minecraft.world.phys.Vec3;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+public abstract class EntityLookup implements LevelEntityGetter<Entity> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(EntityLookup.class);
+
+ protected static final int REGION_SHIFT = 5;
+ protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1;
+ protected static final int REGION_SIZE = 1 << REGION_SHIFT;
+
+ public final Level world;
+
+ 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<>();
+ protected final ConcurrentHashMap<UUID, Entity> entityByUUID = new ConcurrentHashMap<>();
+ protected final EntityList accessibleEntities = new EntityList();
+
+ 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;
+ }
+
+ protected abstract Boolean blockTicketUpdates();
+
+ protected abstract void setBlockTicketUpdates(final Boolean value);
+
+ protected abstract void checkThread(final int chunkX, final int chunkZ, final String reason);
+
+ protected abstract void checkThread(final Entity entity, final String reason);
+
+ protected abstract ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk);
+
+ protected abstract void onEmptySlices(final int chunkX, final int chunkZ);
+
+ protected abstract void entitySectionChangeCallback(
+ final Entity entity,
+ final int oldSectionX, final int oldSectionY, final int oldSectionZ,
+ final int newSectionX, final int newSectionY, final int newSectionZ
+ );
+
+ protected abstract void addEntityCallback(final Entity entity);
+
+ protected abstract void removeEntityCallback(final Entity entity);
+
+ protected abstract void entityStartLoaded(final Entity entity);
+
+ protected abstract void entityEndLoaded(final Entity entity);
+
+ protected abstract void entityStartTicking(final Entity entity);
+
+ protected abstract void entityEndTicking(final Entity entity);
+
+ protected abstract boolean screenEntity(final Entity entity);
+
+ private static Entity maskNonAccessible(final Entity entity) {
+ if (entity == null) {
+ return null;
+ }
+ final Visibility visibility = EntityLookup.getEntityStatus(entity);
+ return visibility.isAccessible() ? entity : null;
+ }
+
+ @Override
+ public Entity get(final int id) {
+ return maskNonAccessible(this.entityById.get((long)id));
+ }
+
+ @Override
+ public Entity get(final UUID id) {
+ return maskNonAccessible(id == null ? null : this.entityByUUID.get(id));
+ }
+
+ public boolean hasEntity(final UUID uuid) {
+ return this.get(uuid) != null;
+ }
+
+ public String getDebugInfo() {
+ return "count_id:" + this.entityById.size() + ",count_uuid:" + this.entityByUUID.size() + ",count_accessible:" + this.getEntityCount() + ",region_count:" + this.regions.size();
+ }
+
+ protected static final class ArrayIterable<T> implements Iterable<T> {
+
+ private final T[] array;
+ private final int off;
+ private final int length;
+
+ public ArrayIterable(final T[] array, final int off, final int length) {
+ this.array = array;
+ this.off = off;
+ this.length = length;
+ if (length > array.length) {
+ throw new IllegalArgumentException("Length must be no greater-than the array length");
+ }
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ return new ArrayIterator<>(this.array, this.off, this.length);
+ }
+
+ protected static final class ArrayIterator<T> implements Iterator<T> {
+
+ private final T[] array;
+ private int off;
+ private final int length;
+
+ public ArrayIterator(final T[] array, final int off, final int length) {
+ this.array = array;
+ this.off = off;
+ this.length = length;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return this.off < this.length;
+ }
+
+ @Override
+ public T next() {
+ if (this.off >= this.length) {
+ throw new NoSuchElementException();
+ }
+ return this.array[this.off++];
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ @Override
+ public Iterable<Entity> getAll() {
+ synchronized (this.accessibleEntities) {
+ final int len = this.accessibleEntities.size();
+ final Entity[] cpy = Arrays.copyOf(this.accessibleEntities.getRawData(), len, Entity[].class);
+
+ Objects.checkFromToIndex(0, len, cpy.length);
+
+ return new ArrayIterable<>(cpy, 0, len);
+ }
+ }
+
+ public int getEntityCount() {
+ synchronized (this.accessibleEntities) {
+ return this.accessibleEntities.size();
+ }
+ }
+
+ public Entity[] getAllCopy() {
+ synchronized (this.accessibleEntities) {
+ return Arrays.copyOf(this.accessibleEntities.getRawData(), this.accessibleEntities.size(), Entity[].class);
+ }
+ }
+
+ @Override
+ public <U extends Entity> void get(final EntityTypeTest<Entity, U> filter, final AbortableIterationConsumer<U> action) {
+ for (final Iterator<Entity> iterator = this.entityById.valueIterator(); iterator.hasNext();) {
+ final Entity entity = iterator.next();
+ final Visibility visibility = EntityLookup.getEntityStatus(entity);
+ if (!visibility.isAccessible()) {
+ continue;
+ }
+ final U casted = filter.tryCast(entity);
+ if (casted != null && action.accept(casted).shouldAbort()) {
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void get(final AABB box, final Consumer<Entity> action) {
+ List<Entity> entities = new ArrayList<>();
+ this.getEntitiesWithoutDragonParts(null, box, entities, null);
+ for (int i = 0, len = entities.size(); i < len; ++i) {
+ action.accept(entities.get(i));
+ }
+ }
+
+ @Override
+ public <U extends Entity> void get(final EntityTypeTest<Entity, U> filter, final AABB box, final AbortableIterationConsumer<U> action) {
+ List<Entity> entities = new ArrayList<>();
+ this.getEntitiesWithoutDragonParts(null, box, entities, null);
+ for (int i = 0, len = entities.size(); i < len; ++i) {
+ final U casted = filter.tryCast(entities.get(i));
+ if (casted != null && action.accept(casted).shouldAbort()) {
+ break;
+ }
+ }
+ }
+
+ public void entityStatusChange(final Entity entity, final ChunkEntitySlices slices, final Visibility oldVisibility, final Visibility newVisibility, final boolean moved,
+ final boolean created, final boolean destroyed) {
+ this.checkThread(entity, "Entity status change must only happen on the main thread");
+
+ if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) {
+ // recursive status update
+ LOGGER.error("Cannot recursively update entity chunk status for entity " + entity, new Throwable());
+ return;
+ }
+
+ final boolean entityStatusUpdateBefore = slices == null ? false : slices.startPreventingStatusUpdates();
+
+ if (entityStatusUpdateBefore) {
+ LOGGER.error("Cannot update chunk status for entity " + entity + " since entity chunk (" + slices.chunkX + "," + slices.chunkZ + ") is receiving update", new Throwable());
+ return;
+ }
+
+ try {
+ final Boolean ticketBlockBefore = this.blockTicketUpdates();
+ try {
+ ((ChunkSystemEntity)entity).moonrise$setUpdatingSectionStatus(true);
+ try {
+ if (created) {
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onCreated(entity);
+ }
+ }
+
+ if (oldVisibility == newVisibility) {
+ if (moved && newVisibility.isAccessible()) {
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onSectionChange(entity);
+ }
+ }
+ return;
+ }
+
+ if (newVisibility.ordinal() > oldVisibility.ordinal()) {
+ // status upgrade
+ if (!oldVisibility.isAccessible() && newVisibility.isAccessible()) {
+ EntityLookup.this.entityStartLoaded(entity);
+ synchronized (this.accessibleEntities) {
+ this.accessibleEntities.add(entity);
+ }
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onTrackingStart(entity);
+ }
+ }
+
+ if (!oldVisibility.isTicking() && newVisibility.isTicking()) {
+ EntityLookup.this.entityStartTicking(entity);
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onTickingStart(entity);
+ }
+ }
+ } else {
+ // status downgrade
+ if (oldVisibility.isTicking() && !newVisibility.isTicking()) {
+ EntityLookup.this.entityEndTicking(entity);
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onTickingEnd(entity);
+ }
+ }
+
+ if (oldVisibility.isAccessible() && !newVisibility.isAccessible()) {
+ EntityLookup.this.entityEndLoaded(entity);
+ synchronized (this.accessibleEntities) {
+ this.accessibleEntities.remove(entity);
+ }
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onTrackingEnd(entity);
+ }
+ }
+ }
+
+ if (moved && newVisibility.isAccessible()) {
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onSectionChange(entity);
+ }
+ }
+
+ if (destroyed) {
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onDestroyed(entity);
+ }
+ }
+ } finally {
+ ((ChunkSystemEntity)entity).moonrise$setUpdatingSectionStatus(false);
+ }
+ } finally {
+ this.setBlockTicketUpdates(ticketBlockBefore);
+ }
+ } finally {
+ if (slices != null) {
+ slices.stopPreventingStatusUpdates(false);
+ }
+ }
+ }
+
+ public void chunkStatusChange(final int x, final int z, final FullChunkStatus newStatus) {
+ this.getChunk(x, z).updateStatus(newStatus, this);
+ }
+
+ public void addLegacyChunkEntities(final List<Entity> entities, final ChunkPos forChunk) {
+ this.addEntityChunk(entities, forChunk, true);
+ }
+
+ public void addEntityChunkEntities(final List<Entity> entities, final ChunkPos forChunk) {
+ this.addEntityChunk(entities, forChunk, true);
+ }
+
+ public void addWorldGenChunkEntities(final List<Entity> entities, final ChunkPos forChunk) {
+ this.addEntityChunk(entities, forChunk, false);
+ }
+
+ protected void addRecursivelySafe(final Entity root, final boolean fromDisk) {
+ if (!this.addEntity(root, fromDisk)) {
+ // possible we are a passenger, and so should dismount from any valid entity in the world
+ root.stopRiding();
+ return;
+ }
+ for (final Entity passenger : root.getPassengers()) {
+ this.addRecursivelySafe(passenger, fromDisk);
+ }
+ }
+
+ protected void addEntityChunk(final List<Entity> entities, final ChunkPos forChunk, final boolean fromDisk) {
+ for (int i = 0, len = entities.size(); i < len; ++i) {
+ final Entity entity = entities.get(i);
+ if (entity.isPassenger()) {
+ continue;
+ }
+
+ if (forChunk != null && !entity.chunkPosition().equals(forChunk)) {
+ LOGGER.warn("Root entity " + entity + " is outside of serialized chunk " + forChunk);
+ // can't set removed here, as we may not own the chunk position
+ // skip the entity
+ continue;
+ }
+
+ final Vec3 rootPosition = entity.position();
+
+ // always adjust positions before adding passengers in case plugins access the entity, and so that
+ // they are added to the right entity chunk
+ for (final Entity passenger : entity.getIndirectPassengers()) {
+ if (forChunk != null && !passenger.chunkPosition().equals(forChunk)) {
+ passenger.setPosRaw(rootPosition.x, rootPosition.y, rootPosition.z);
+ }
+ }
+
+ this.addRecursivelySafe(entity, fromDisk);
+ }
+ }
+
+ public boolean addNewEntity(final Entity entity) {
+ return this.addEntity(entity, false);
+ }
+
+ public static Visibility getEntityStatus(final Entity entity) {
+ if (entity.isAlwaysTicking()) {
+ return Visibility.TICKING;
+ }
+ final FullChunkStatus entityStatus = ((ChunkSystemEntity)entity).moonrise$getChunkStatus();
+ return Visibility.fromFullChunkStatus(entityStatus == null ? FullChunkStatus.INACCESSIBLE : entityStatus);
+ }
+
+ protected boolean addEntity(final Entity entity, final boolean fromDisk) {
+ 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 sectionZ = pos.getZ() >> 4;
+ this.checkThread(sectionX, sectionZ, "Cannot add entity off-main thread");
+
+ if (entity.isRemoved()) {
+ LOGGER.warn("Refusing to add removed entity: " + entity);
+ return false;
+ }
+
+ if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) {
+ LOGGER.warn("Entity " + entity + " is currently prevented from being added/removed to world since it is processing section status updates", new Throwable());
+ return false;
+ }
+
+ if (!this.screenEntity(entity)) {
+ return false;
+ }
+
+ Entity currentlyMapped = this.entityById.putIfAbsent((long)entity.getId(), entity);
+ if (currentlyMapped != null) {
+ LOGGER.warn("Entity id already exists: " + entity.getId() + ", mapped to " + currentlyMapped + ", can't add " + entity);
+ return false;
+ }
+
+ currentlyMapped = this.entityByUUID.putIfAbsent(entity.getUUID(), entity);
+ if (currentlyMapped != null) {
+ // need to remove mapping for id
+ this.entityById.remove((long)entity.getId(), entity);
+ LOGGER.warn("Entity uuid already exists: " + entity.getUUID() + ", mapped to " + currentlyMapped + ", can't add " + entity);
+ return false;
+ }
+
+ ((ChunkSystemEntity)entity).moonrise$setSectionX(sectionX);
+ ((ChunkSystemEntity)entity).moonrise$setSectionY(sectionY);
+ ((ChunkSystemEntity)entity).moonrise$setSectionZ(sectionZ);
+ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ);
+ if (!slices.addEntity(entity, sectionY)) {
+ LOGGER.warn("Entity " + entity + " added to world '" + WorldUtil.getWorldName(this.world) + "', but was already contained in entity chunk (" + sectionX + "," + sectionZ + ")");
+ }
+
+ entity.setLevelCallback(new EntityCallback(entity));
+
+ this.addEntityCallback(entity);
+
+ this.entityStatusChange(entity, slices, Visibility.HIDDEN, getEntityStatus(entity), false, !fromDisk, false);
+
+ return true;
+ }
+
+ public boolean canRemoveEntity(final Entity entity) {
+ if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) {
+ return false;
+ }
+
+ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX();
+ final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ();
+ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ);
+ return slices == null || !slices.isPreventingStatusUpdates();
+ }
+
+ protected void removeEntity(final Entity entity) {
+ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX();
+ final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY();
+ final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ();
+ this.checkThread(sectionX, sectionZ, "Cannot remove entity off-main");
+ if (!entity.isRemoved()) {
+ throw new IllegalStateException("Only call Entity#setRemoved to remove an entity");
+ }
+ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ);
+ // all entities should be in a chunk
+ if (slices == null) {
+ LOGGER.warn("Cannot remove entity " + entity + " from null entity slices (" + sectionX + "," + sectionZ + ")");
+ } else {
+ if (slices.isPreventingStatusUpdates()) {
+ throw new IllegalStateException("Attempting to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ") that is receiving status updates");
+ }
+ if (!slices.removeEntity(entity, sectionY)) {
+ LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")");
+ }
+ }
+ ((ChunkSystemEntity)entity).moonrise$setSectionX(Integer.MIN_VALUE);
+ ((ChunkSystemEntity)entity).moonrise$setSectionY(Integer.MIN_VALUE);
+ ((ChunkSystemEntity)entity).moonrise$setSectionZ(Integer.MIN_VALUE);
+
+
+ Entity currentlyMapped;
+ if ((currentlyMapped = this.entityById.remove(entity.getId(), entity)) != entity) {
+ LOGGER.warn("Failed to remove entity " + entity + " by id, current entity mapped: " + currentlyMapped);
+ }
+
+ Entity[] currentlyMappedArr = new Entity[1];
+
+ // need reference equality
+ this.entityByUUID.compute(entity.getUUID(), (final UUID keyInMap, final Entity valueInMap) -> {
+ currentlyMappedArr[0] = valueInMap;
+ if (valueInMap != entity) {
+ return valueInMap;
+ }
+ return null;
+ });
+
+ if (currentlyMappedArr[0] != entity) {
+ LOGGER.warn("Failed to remove entity " + entity + " by uuid, current entity mapped: " + currentlyMappedArr[0]);
+ }
+
+ if (slices != null && slices.isEmpty()) {
+ this.onEmptySlices(sectionX, sectionZ);
+ }
+ }
+
+ protected ChunkEntitySlices moveEntity(final Entity entity) {
+ // ensure we own the entity
+ this.checkThread(entity, "Cannot move entity off-main");
+
+ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX();
+ final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY();
+ 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 newSectionZ = newPos.getZ() >> 4;
+
+ if (newSectionX == sectionX && newSectionY == sectionY && newSectionZ == sectionZ) {
+ return null;
+ }
+
+ // ensure the new section is owned by this tick thread
+ this.checkThread(newSectionX, newSectionZ, "Cannot move entity off-main");
+
+ // ensure the old section is owned by this tick thread
+ this.checkThread(sectionX, sectionZ, "Cannot move entity off-main");
+
+ final ChunkEntitySlices old = this.getChunk(sectionX, sectionZ);
+ final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ);
+
+ if (!old.removeEntity(entity, sectionY)) {
+ LOGGER.warn("Could not remove entity " + entity + " from its old chunk section (" + sectionX + "," + sectionY + "," + sectionZ + ") since it was not contained in the section");
+ }
+
+ if (!slices.addEntity(entity, newSectionY)) {
+ LOGGER.warn("Could not add entity " + entity + " to its new chunk section (" + newSectionX + "," + newSectionY + "," + newSectionZ + ") as it is already contained in the section");
+ }
+
+ ((ChunkSystemEntity)entity).moonrise$setSectionX(newSectionX);
+ ((ChunkSystemEntity)entity).moonrise$setSectionY(newSectionY);
+ ((ChunkSystemEntity)entity).moonrise$setSectionZ(newSectionZ);
+
+ if (old.isEmpty()) {
+ this.onEmptySlices(sectionX, sectionZ);
+ }
+
+ this.entitySectionChangeCallback(
+ entity,
+ sectionX, sectionY, sectionZ,
+ newSectionX, newSectionY, newSectionZ
+ );
+
+ return slices;
+ }
+
+ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ chunk.getEntitiesWithoutDragonParts(except, box, into, predicate);
+ }
+ }
+ }
+ }
+ }
+
+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ chunk.getEntities(except, box, into, predicate);
+ }
+ }
+ }
+ }
+ }
+
+ public void getHardCollidingEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ chunk.getHardCollidingEntities(except, box, into, predicate);
+ }
+ }
+ }
+ }
+ }
+
+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ chunk.getEntities(type, box, (List)into, (Predicate)predicate);
+ }
+ }
+ }
+ }
+ }
+
+ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ chunk.getEntities(clazz, except, box, into, predicate);
+ }
+ }
+ }
+ }
+ }
+
+ //////// Limited ////////
+
+ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
+ final int maxCount) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ if (chunk.getEntitiesWithoutDragonParts(except, box, into, predicate, maxCount)) {
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
+ final int maxCount) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ if (chunk.getEntities(except, box, into, predicate, maxCount)) {
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate, final int maxCount) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ if (chunk.getEntities(type, box, (List)into, (Predicate)predicate, maxCount)) {
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate, final int maxCount) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ if (chunk.getEntities(clazz, except, box, into, predicate, maxCount)) {
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public void entitySectionLoad(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) {
+ this.checkThread(chunkX, chunkZ, "Cannot load in entity section off-main");
+ synchronized (this) {
+ final ChunkEntitySlices curr = this.getChunk(chunkX, chunkZ);
+ if (curr != null) {
+ this.removeChunk(chunkX, chunkZ);
+
+ curr.mergeInto(slices);
+
+ this.addChunk(chunkX, chunkZ, slices);
+ } else {
+ this.addChunk(chunkX, chunkZ, slices);
+ }
+ }
+ }
+
+ public void entitySectionUnload(final int chunkX, final int chunkZ) {
+ this.checkThread(chunkX, chunkZ, "Cannot unload entity section off-main");
+ this.removeChunk(chunkX, chunkZ);
+ }
+
+ public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) {
+ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
+ if (region == null) {
+ return null;
+ }
+
+ return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT));
+ }
+
+ public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) {
+ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
+ ChunkEntitySlices ret;
+ if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) {
+ return this.createEntityChunk(chunkX, chunkZ, true);
+ }
+
+ return ret;
+ }
+
+ public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) {
+ final long key = CoordinateUtils.getChunkKey(regionX, regionZ);
+
+ return this.regions.get(key);
+ }
+
+ protected synchronized void removeChunk(final int chunkX, final int chunkZ) {
+ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
+ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT);
+
+ final ChunkSlicesRegion region = this.regions.get(key);
+ final int remaining = region.remove(relIndex);
+
+ if (remaining == 0) {
+ this.regions.remove(key);
+ }
+ }
+
+ public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) {
+ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
+ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT);
+
+ ChunkSlicesRegion region = this.regions.get(key);
+ if (region != null) {
+ region.add(relIndex, slices);
+ } else {
+ region = new ChunkSlicesRegion();
+ region.add(relIndex, slices);
+ this.regions.put(key, region);
+ }
+ }
+
+ public static final class ChunkSlicesRegion {
+
+ private final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE];
+ private int sliceCount;
+
+ public ChunkEntitySlices get(final int index) {
+ return this.slices[index];
+ }
+
+ public int remove(final int index) {
+ final ChunkEntitySlices slices = this.slices[index];
+ if (slices == null) {
+ throw new IllegalStateException();
+ }
+
+ this.slices[index] = null;
+
+ return --this.sliceCount;
+ }
+
+ public void add(final int index, final ChunkEntitySlices slices) {
+ final ChunkEntitySlices curr = this.slices[index];
+ if (curr != null) {
+ throw new IllegalStateException();
+ }
+
+ this.slices[index] = slices;
+
+ ++this.sliceCount;
+ }
+ }
+
+ protected final class EntityCallback implements EntityInLevelCallback {
+
+ public final Entity entity;
+
+ public EntityCallback(final Entity entity) {
+ this.entity = entity;
+ }
+
+ @Override
+ public void onMove() {
+ final Entity entity = this.entity;
+ final Visibility oldVisibility = getEntityStatus(entity);
+ final ChunkEntitySlices newSlices = EntityLookup.this.moveEntity(this.entity);
+ if (newSlices == null) {
+ // no new section, so didn't change sections
+ return;
+ }
+
+ final Visibility newVisibility = getEntityStatus(entity);
+
+ EntityLookup.this.entityStatusChange(entity, newSlices, oldVisibility, newVisibility, true, false, false);
+ }
+
+ @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
+ final Visibility tickingState = EntityLookup.getEntityStatus(entity);
+
+ EntityLookup.this.removeEntity(entity);
+
+ EntityLookup.this.entityStatusChange(entity, null, tickingState, Visibility.HIDDEN, false, false, reason.shouldDestroy());
+
+ EntityLookup.this.removeEntityCallback(entity);
+
+ this.entity.setLevelCallback(NoOpCallback.INSTANCE);
+ }
+ }
+
+ protected static final class NoOpCallback implements EntityInLevelCallback {
+
+ public static final NoOpCallback INSTANCE = new NoOpCallback();
+
+ @Override
+ public void onMove() {}
+
+ @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
new file mode 100644
index 0000000000000000000000000000000000000000..edcde00206d068bd79175fea33efa05b0e8c1562
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
@@ -0,0 +1,123 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.client;
+
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.entity.LevelCallback;
+
+public final class ClientEntityLookup extends EntityLookup {
+
+ private final LongOpenHashSet tickingChunks = new LongOpenHashSet();
+
+ public ClientEntityLookup(final Level world, final LevelCallback<Entity> worldCallback) {
+ super(world, worldCallback);
+ }
+
+ @Override
+ protected Boolean blockTicketUpdates() {
+ // not present on client
+ return null;
+ }
+
+ @Override
+ protected void setBlockTicketUpdates(Boolean value) {
+ // not present on client
+ }
+
+ @Override
+ protected void checkThread(final int chunkX, final int chunkZ, final String reason) {
+ // TODO implement?
+ }
+
+ @Override
+ protected void checkThread(final Entity entity, final String reason) {
+ // TODO implement?
+ }
+
+ @Override
+ protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
+ final boolean ticking = this.tickingChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ final ChunkEntitySlices ret = new ChunkEntitySlices(
+ this.world, chunkX, chunkZ,
+ ticking ? FullChunkStatus.ENTITY_TICKING : FullChunkStatus.FULL, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
+ );
+
+ // note: not handled by superclass
+ this.addChunk(chunkX, chunkZ, ret);
+
+ return ret;
+ }
+
+ @Override
+ protected void onEmptySlices(final int chunkX, final int chunkZ) {
+ this.removeChunk(chunkX, chunkZ);
+ }
+
+ @Override
+ protected void entitySectionChangeCallback(final Entity entity,
+ final int oldSectionX, final int oldSectionY, final int oldSectionZ,
+ final int newSectionX, final int newSectionY, final int newSectionZ) {
+
+ }
+
+ @Override
+ protected void addEntityCallback(final Entity entity) {
+
+ }
+
+ @Override
+ protected void removeEntityCallback(final Entity entity) {
+
+ }
+
+ @Override
+ protected void entityStartLoaded(final Entity entity) {
+
+ }
+
+ @Override
+ protected void entityEndLoaded(final Entity entity) {
+
+ }
+
+ @Override
+ protected void entityStartTicking(final Entity entity) {
+
+ }
+
+ @Override
+ protected void entityEndTicking(final Entity entity) {
+
+ }
+
+ @Override
+ protected boolean screenEntity(final Entity entity) {
+ return true;
+ }
+
+ public void markTicking(final long pos) {
+ if (this.tickingChunks.add(pos)) {
+ final int chunkX = CoordinateUtils.getChunkX(pos);
+ final int chunkZ = CoordinateUtils.getChunkZ(pos);
+ if (this.getChunk(chunkX, chunkZ) != null) {
+ this.chunkStatusChange(chunkX, chunkZ, FullChunkStatus.ENTITY_TICKING);
+ }
+ }
+ }
+
+ public void markNonTicking(final long pos) {
+ if (this.tickingChunks.remove(pos)) {
+ final int chunkX = CoordinateUtils.getChunkX(pos);
+ final int chunkZ = CoordinateUtils.getChunkZ(pos);
+ if (this.getChunk(chunkX, chunkZ) != null) {
+ this.chunkStatusChange(chunkX, chunkZ, FullChunkStatus.FULL);
+ }
+ }
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..465469e44346c50f30f3abd6b44f4173ccfcf248
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
@@ -0,0 +1,114 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl;
+
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.entity.LevelCallback;
+
+public final class DefaultEntityLookup extends EntityLookup {
+ public DefaultEntityLookup(final Level world) {
+ super(world, new DefaultLevelCallback());
+ }
+
+ @Override
+ protected Boolean blockTicketUpdates() {
+ return null;
+ }
+
+ @Override
+ protected void setBlockTicketUpdates(final Boolean value) {}
+
+ @Override
+ protected void checkThread(final int chunkX, final int chunkZ, final String reason) {}
+
+ @Override
+ protected void checkThread(final Entity entity, final String reason) {}
+
+ @Override
+ 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)
+ );
+
+ // note: not handled by superclass
+ this.addChunk(chunkX, chunkZ, ret);
+
+ return ret;
+ }
+
+ @Override
+ protected void onEmptySlices(final int chunkX, final int chunkZ) {
+ this.removeChunk(chunkX, chunkZ);
+ }
+
+ @Override
+ protected void entitySectionChangeCallback(final Entity entity,
+ final int oldSectionX, final int oldSectionY, final int oldSectionZ,
+ final int newSectionX, final int newSectionY, final int newSectionZ) {
+
+ }
+
+ @Override
+ protected void addEntityCallback(final Entity entity) {
+
+ }
+
+ @Override
+ protected void removeEntityCallback(final Entity entity) {
+
+ }
+
+ @Override
+ protected void entityStartLoaded(final Entity entity) {
+
+ }
+
+ @Override
+ protected void entityEndLoaded(final Entity entity) {
+
+ }
+
+ @Override
+ protected void entityStartTicking(final Entity entity) {
+
+ }
+
+ @Override
+ protected void entityEndTicking(final Entity entity) {
+
+ }
+
+ @Override
+ protected boolean screenEntity(final Entity entity) {
+ return true;
+ }
+
+ protected static final class DefaultLevelCallback implements LevelCallback<Entity> {
+
+ @Override
+ public void onCreated(final Entity entity) {}
+
+ @Override
+ public void onDestroyed(final Entity entity) {}
+
+ @Override
+ public void onTickingStart(final Entity entity) {}
+
+ @Override
+ public void onTickingEnd(final Entity entity) {}
+
+ @Override
+ public void onTrackingStart(final Entity entity) {}
+
+ @Override
+ public void onTrackingEnd(final Entity entity) {}
+
+ @Override
+ public void onSectionChange(final Entity entity) {}
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..dacf2b2988ce603879fe525a3418ac77f8a663f7
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
@@ -0,0 +1,113 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server;
+
+import ca.spottedleaf.moonrise.common.list.ReferenceList;
+import ca.spottedleaf.moonrise.common.util.TickThread;
+import ca.spottedleaf.moonrise.common.util.ChunkSystem;
+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.entity.EntityLookup;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.entity.LevelCallback;
+
+public final class ServerEntityLookup extends EntityLookup {
+
+ private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0];
+
+ 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);
+ this.serverWorld = world;
+ }
+
+ @Override
+ protected Boolean blockTicketUpdates() {
+ return ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager.blockTicketUpdates();
+ }
+
+ @Override
+ protected void setBlockTicketUpdates(final Boolean value) {
+ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager.unblockTicketUpdates(value);
+ }
+
+ @Override
+ protected void checkThread(final int chunkX, final int chunkZ, final String reason) {
+ TickThread.ensureTickThread(this.serverWorld, chunkX, chunkZ, reason);
+ }
+
+ @Override
+ protected void checkThread(final Entity entity, final String reason) {
+ TickThread.ensureTickThread(entity, reason);
+ }
+
+ @Override
+ protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
+ // loadInEntityChunk will call addChunk for us
+ return ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager
+ .getOrCreateEntityChunk(chunkX, chunkZ, transientChunk);
+ }
+
+ @Override
+ protected void onEmptySlices(final int chunkX, final int chunkZ) {
+ // entity slices unloading is managed by ticket levels in chunk system
+ }
+
+ @Override
+ protected void entitySectionChangeCallback(final Entity entity,
+ final int oldSectionX, final int oldSectionY, final int oldSectionZ,
+ final int newSectionX, final int newSectionY, final int newSectionZ) {
+ if (entity instanceof ServerPlayer player) {
+ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().tickPlayer(player);
+ }
+ }
+
+ @Override
+ protected void addEntityCallback(final Entity entity) {
+ if (entity instanceof ServerPlayer player) {
+ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().addPlayer(player);
+ }
+ }
+
+ @Override
+ protected void removeEntityCallback(final Entity entity) {
+ 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
+ }
+
+ @Override
+ protected void entityEndLoaded(final Entity entity) {
+ // Moonrise start - entity tracker
+ this.trackerEntities.remove(entity);
+ this.trackerUnloadedEntities.add(entity);
+ // Moonrise end - entity tracker
+ }
+
+ @Override
+ protected void entityStartTicking(final Entity entity) {
+
+ }
+
+ @Override
+ protected void entityEndTicking(final Entity entity) {
+
+ }
+
+ @Override
+ protected boolean screenEntity(final Entity entity) {
+ return ChunkSystem.screenEntity(this.serverWorld, entity);
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..458d1fc5e1222912512e6c59b56f6fca347d9ee9
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java
@@ -0,0 +1,17 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.poi;
+
+import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.chunk.ChunkAccess;
+
+public interface ChunkSystemPoiManager extends ChunkSystemSectionStorage {
+
+ public ServerLevel moonrise$getWorld();
+
+ public void moonrise$onUnload(final long coordinate);
+
+ public void moonrise$loadInPoiChunk(final PoiChunk poiChunk);
+
+ public void moonrise$checkConsistency(final ChunkAccess chunk);
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java
new file mode 100644
index 0000000000000000000000000000000000000000..89b956b8fdf1a0d862a843104511005e2990a897
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java
@@ -0,0 +1,12 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.poi;
+
+import net.minecraft.world.entity.ai.village.poi.PoiSection;
+import java.util.Optional;
+
+public interface ChunkSystemPoiSection {
+
+ public boolean moonrise$isEmpty();
+
+ public Optional<PoiSection> moonrise$asOptional();
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..fd35e4db0c8fec8f86b8743bcc2b15ed2e7433f1
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
@@ -0,0 +1,212 @@
+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;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.nbt.Tag;
+import net.minecraft.resources.RegistryOps;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.entity.ai.village.poi.PoiManager;
+import net.minecraft.world.entity.ai.village.poi.PoiSection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.Optional;
+
+public final class PoiChunk {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PoiChunk.class);
+
+ public final ServerLevel world;
+ public final int chunkX;
+ public final int chunkZ;
+ public final int minSection;
+ public final int maxSection;
+
+ private final PoiSection[] sections;
+
+ private boolean isDirty;
+ private boolean loaded;
+
+ public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection) {
+ this(world, chunkX, chunkZ, minSection, maxSection, new PoiSection[maxSection - minSection + 1]);
+ }
+
+ public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection, final PoiSection[] sections) {
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.minSection = minSection;
+ this.maxSection = maxSection;
+ this.sections = sections;
+ if (this.sections.length != (maxSection - minSection + 1)) {
+ throw new IllegalStateException("Incorrect length used, expected " + (maxSection - minSection + 1) + ", got " + this.sections.length);
+ }
+ }
+
+ public void load() {
+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Loading in poi chunk off-main");
+ if (this.loaded) {
+ return;
+ }
+ this.loaded = true;
+ ((ChunkSystemPoiManager)this.world.getChunkSource().getPoiManager()).moonrise$loadInPoiChunk(this);
+ }
+
+ public boolean isLoaded() {
+ return this.loaded;
+ }
+
+ public boolean isEmpty() {
+ for (final PoiSection section : this.sections) {
+ if (section != null && !((ChunkSystemPoiSection)section).moonrise$isEmpty()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public PoiSection getOrCreateSection(final int chunkY) {
+ if (chunkY >= this.minSection && chunkY <= this.maxSection) {
+ final int idx = chunkY - this.minSection;
+ final PoiSection ret = this.sections[idx];
+ if (ret != null) {
+ return ret;
+ }
+
+ final PoiManager poiManager = this.world.getPoiManager();
+ final long key = CoordinateUtils.getChunkSectionKey(this.chunkX, chunkY, this.chunkZ);
+
+ return this.sections[idx] = new PoiSection(() -> {
+ poiManager.setDirty(key);
+ });
+ }
+ throw new IllegalArgumentException("chunkY is out of bounds, chunkY: " + chunkY + " outside [" + this.minSection + "," + this.maxSection + "]");
+ }
+
+ public PoiSection getSection(final int chunkY) {
+ if (chunkY >= this.minSection && chunkY <= this.maxSection) {
+ return this.sections[chunkY - this.minSection];
+ }
+ return null;
+ }
+
+ public Optional<PoiSection> getSectionForVanilla(final int chunkY) {
+ if (chunkY >= this.minSection && chunkY <= this.maxSection) {
+ final PoiSection ret = this.sections[chunkY - this.minSection];
+ return ret == null ? Optional.empty() : ((ChunkSystemPoiSection)ret).moonrise$asOptional();
+ }
+ return Optional.empty();
+ }
+
+ public boolean isDirty() {
+ return this.isDirty;
+ }
+
+ public void setDirty(final boolean dirty) {
+ this.isDirty = dirty;
+ }
+
+ // returns null if empty
+ public CompoundTag save() {
+ final RegistryOps<Tag> registryOps = RegistryOps.create(NbtOps.INSTANCE, this.world.registryAccess());
+
+ final CompoundTag ret = new CompoundTag();
+ final CompoundTag sections = new CompoundTag();
+ ret.put("Sections", sections);
+
+ 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;
+
+ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) {
+ final PoiSection section = this.sections[sectionY - this.minSection];
+ if (section == null || ((ChunkSystemPoiSection)section).moonrise$isEmpty()) {
+ 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);
+ 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);
+ }).orElse(null);
+ if (serialized == null) {
+ // failed, should be logged from the resultOrPartial
+ continue;
+ }
+
+ sections.put(Integer.toString(sectionY), serialized);
+ }
+
+ return sections.isEmpty() ? null : ret;
+ }
+
+ public static PoiChunk empty(final ServerLevel world, final int chunkX, final int chunkZ) {
+ final PoiChunk ret = new PoiChunk(world, chunkX, chunkZ, WorldUtil.getMinSection(world), WorldUtil.getMaxSection(world));
+ ret.loaded = true;
+ return ret;
+ }
+
+ public static PoiChunk parse(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data) {
+ final PoiChunk ret = empty(world, chunkX, chunkZ);
+
+ final RegistryOps<Tag> registryOps = RegistryOps.create(NbtOps.INSTANCE, world.registryAccess());
+
+ final CompoundTag sections = data.getCompound("Sections");
+
+ if (sections.isEmpty()) {
+ // nothing to parse
+ return ret;
+ }
+
+ final PoiManager poiManager = world.getPoiManager();
+
+ boolean readAnything = false;
+
+ for (int sectionY = ret.minSection; sectionY <= ret.maxSection; ++sectionY) {
+ final String key = Integer.toString(sectionY);
+ if (!sections.contains(key)) {
+ 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 int finalSectionY = sectionY;
+ final PoiSection deserialized = 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);
+
+ if (deserialized == null || ((ChunkSystemPoiSection)deserialized).moonrise$isEmpty()) {
+ // completely empty, no point in storing this
+ continue;
+ }
+
+ readAnything = true;
+ ret.sections[sectionY - ret.minSection] = deserialized;
+ }
+
+ ret.loaded = !readAnything; // Set loaded to false if we read anything to ensure proper callbacks to PoiManager are made on #load
+
+ return ret;
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..3f5edb756beb9c31b6f591a24b778d6ac2b0bf51
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
@@ -0,0 +1,21 @@
+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 {
+
+ public CompoundTag moonrise$read(final int chunkX, final int chunkZ) throws IOException;
+
+ public void moonrise$write(final int chunkX, final int chunkZ, final CompoundTag data) throws IOException;
+
+ public RegionFileStorage moonrise$getRegionStorage();
+
+ public void moonrise$close() throws IOException;
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java
new file mode 100644
index 0000000000000000000000000000000000000000..003a857e70ead858e8437e3c1bfaf22f4daba0df
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java
@@ -0,0 +1,15 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.player;
+
+public interface ChunkSystemServerPlayer {
+
+ public boolean moonrise$isRealPlayer();
+
+ public void moonrise$setRealPlayer(final boolean real);
+
+ public RegionizedPlayerChunkLoader.PlayerChunkLoaderData moonrise$getChunkLoader();
+
+ public void moonrise$setChunkLoader(final RegionizedPlayerChunkLoader.PlayerChunkLoaderData loader);
+
+ public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder();
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..7249b78e21b12117480a18f2d07d0c978fe42e56
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
@@ -0,0 +1,1082 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.player;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.moonrise.common.misc.AllocatingRateLimiter;
+import ca.spottedleaf.moonrise.common.misc.SingleUserAreaMap;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.TickThread;
+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.ChunkSystemLevelChunk;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
+import ca.spottedleaf.moonrise.patches.chunk_system.util.ParallelSearchRadiusIteration;
+import com.google.gson.JsonObject;
+import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.longs.LongComparator;
+import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue;
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket;
+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket;
+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket;
+import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket;
+import net.minecraft.server.level.ChunkTrackingView;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.level.TicketType;
+import net.minecraft.server.network.PlayerChunkSender;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.GameRules;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.level.levelgen.BelowZeroRetrogen;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayDeque;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Function;
+
+public final class RegionizedPlayerChunkLoader {
+
+ public static final TicketType<Long> PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo);
+ public static final TicketType<Long> PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 5 * 20);
+
+ public static final int MIN_VIEW_DISTANCE = 2;
+ public static final int MAX_VIEW_DISTANCE = 32;
+
+ public static final int GENERATED_TICKET_LEVEL = ChunkHolderManager.FULL_LOADED_TICKET_LEVEL;
+ public static final int LOADED_TICKET_LEVEL = ChunkTaskScheduler.getTicketLevel(ChunkStatus.EMPTY);
+ public static final int TICK_TICKET_LEVEL = ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL;
+
+ public static final class ViewDistanceHolder {
+
+ private volatile ViewDistances viewDistances;
+ private static final VarHandle VIEW_DISTANCES_HANDLE = ConcurrentUtil.getVarHandle(ViewDistanceHolder.class, "viewDistances", ViewDistances.class);
+
+ public ViewDistanceHolder() {
+ VIEW_DISTANCES_HANDLE.setVolatile(this, new ViewDistances(-1, -1, -1));
+ }
+
+ public ViewDistances getViewDistances() {
+ return (ViewDistances)VIEW_DISTANCES_HANDLE.getVolatile(this);
+ }
+
+ public ViewDistances compareAndExchangeViewDistance(final ViewDistances expect, final ViewDistances update) {
+ return (ViewDistances)VIEW_DISTANCES_HANDLE.compareAndExchange(this, expect, update);
+ }
+
+ public void updateViewDistance(final Function<ViewDistances, ViewDistances> update) {
+ int failures = 0;
+ for (ViewDistances curr = this.getViewDistances();;) {
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (curr == (curr = this.compareAndExchangeViewDistance(curr, update.apply(curr)))) {
+ return;
+ }
+ ++failures;
+ }
+ }
+
+ public void setTickViewDistance(final int distance) {
+ this.updateViewDistance((final ViewDistances param) -> {
+ return param.setTickViewDistance(distance);
+ });
+ }
+
+ public void setLoadViewDistance(final int distance) {
+ this.updateViewDistance((final ViewDistances param) -> {
+ return param.setLoadViewDistance(distance);
+ });
+ }
+
+ public void setSendViewDistance(final int distance) {
+ this.updateViewDistance((final ViewDistances param) -> {
+ return param.setTickViewDistance(distance);
+ });
+ }
+
+ public JsonObject toJson() {
+ return this.getViewDistances().toJson();
+ }
+ }
+
+ public static final record ViewDistances(
+ int tickViewDistance,
+ int loadViewDistance,
+ int sendViewDistance
+ ) {
+ public ViewDistances setTickViewDistance(final int distance) {
+ return new ViewDistances(distance, this.loadViewDistance, this.sendViewDistance);
+ }
+
+ public ViewDistances setLoadViewDistance(final int distance) {
+ return new ViewDistances(this.tickViewDistance, distance, this.sendViewDistance);
+ }
+
+ public ViewDistances setSendViewDistance(final int distance) {
+ return new ViewDistances(this.tickViewDistance, this.loadViewDistance, distance);
+ }
+
+ public JsonObject toJson() {
+ final JsonObject ret = new JsonObject();
+
+ ret.addProperty("tick-view-distance", this.tickViewDistance);
+ ret.addProperty("load-view-distance", this.loadViewDistance);
+ ret.addProperty("send-view-distance", this.sendViewDistance);
+
+ return ret;
+ }
+ }
+
+ public static int getAPITickViewDistance(final ServerPlayer player) {
+ final ServerLevel level = player.serverLevel();
+ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (data == null) {
+ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPITickDistance();
+ }
+ return data.lastTickDistance;
+ }
+
+ public static int getAPIViewDistance(final ServerPlayer player) {
+ final ServerLevel level = player.serverLevel();
+ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (data == null) {
+ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPIViewDistance();
+ }
+ // view distance = load distance + 1
+ return data.lastLoadDistance - 1;
+ }
+
+ public static int getLoadViewDistance(final ServerPlayer player) {
+ final ServerLevel level = player.serverLevel();
+ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (data == null) {
+ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPIViewDistance();
+ }
+ // view distance = load distance + 1
+ return data.lastLoadDistance - 1;
+ }
+
+ public static int getAPISendViewDistance(final ServerPlayer player) {
+ final ServerLevel level = player.serverLevel();
+ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (data == null) {
+ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPISendViewDistance();
+ }
+ return data.lastSendDistance;
+ }
+
+ private final ServerLevel world;
+
+ public RegionizedPlayerChunkLoader(final ServerLevel world) {
+ this.world = world;
+ }
+
+ public void addPlayer(final ServerPlayer player) {
+ TickThread.ensureTickThread(player, "Cannot add player to player chunk loader async");
+ if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) {
+ return;
+ }
+
+ if (((ChunkSystemServerPlayer)player).moonrise$getChunkLoader() != null) {
+ throw new IllegalStateException("Player is already added to player chunk loader");
+ }
+
+ final PlayerChunkLoaderData loader = new PlayerChunkLoaderData(this.world, player);
+
+ ((ChunkSystemServerPlayer)player).moonrise$setChunkLoader(loader);
+ loader.add();
+ }
+
+ public void updatePlayer(final ServerPlayer player) {
+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (loader != null) {
+ loader.update();
+ // update view distances for nearby players
+ ((ChunkSystemServerLevel)loader.world).moonrise$getNearbyPlayers().tickPlayer(player);
+ }
+ }
+
+ public void removePlayer(final ServerPlayer player) {
+ TickThread.ensureTickThread(player, "Cannot remove player from player chunk loader async");
+ if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) {
+ return;
+ }
+
+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+
+ if (loader == null) {
+ return;
+ }
+
+ loader.remove();
+ ((ChunkSystemServerPlayer)player).moonrise$setChunkLoader(null);
+ }
+
+ public void setSendDistance(final int distance) {
+ ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setSendViewDistance(distance);
+ }
+
+ public void setLoadDistance(final int distance) {
+ ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setLoadViewDistance(distance);
+ }
+
+ public void setTickDistance(final int distance) {
+ ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setTickViewDistance(distance);
+ }
+
+ // Note: follow the player chunk loader so everything stays consistent...
+ public int getAPITickDistance() {
+ final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(
+ -1, distances.tickViewDistance,
+ -1, distances.loadViewDistance
+ );
+ return tickViewDistance;
+ }
+
+ public int getAPIViewDistance() {
+ final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(
+ -1, distances.tickViewDistance,
+ -1, distances.loadViewDistance
+ );
+ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance);
+
+ // loadDistance = api view distance + 1
+ return loadDistance - 1;
+ }
+
+ public int getAPISendViewDistance() {
+ final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(
+ -1, distances.tickViewDistance,
+ -1, distances.loadViewDistance
+ );
+ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance);
+ final int sendViewDistance = PlayerChunkLoaderData.getSendViewDistance(
+ loadDistance, -1, -1, distances.sendViewDistance
+ );
+
+ return sendViewDistance;
+ }
+
+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ, final boolean borderOnly) {
+ return borderOnly ? this.isChunkSentBorderOnly(player, chunkX, chunkZ) : this.isChunkSent(player, chunkX, chunkZ);
+ }
+
+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) {
+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (loader == null) {
+ return false;
+ }
+
+ return loader.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+
+ public boolean isChunkSentBorderOnly(final ServerPlayer player, final int chunkX, final int chunkZ) {
+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (loader == null) {
+ return false;
+ }
+
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ if (!loader.sentChunks.contains(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ))) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public void tick() {
+ TickThread.ensureTickThread("Cannot tick player chunk loader async");
+ long currTime = System.nanoTime();
+ for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) {
+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (loader == null || loader.removed || loader.world != this.world) {
+ // not our problem anymore
+ continue;
+ }
+ loader.update(); // can't invoke plugin logic
+ loader.updateQueues(currTime);
+ }
+ }
+
+ public static final class PlayerChunkLoaderData {
+
+ private static final AtomicLong ID_GENERATOR = new AtomicLong();
+ private final long id = ID_GENERATOR.incrementAndGet();
+ private final Long idBoxed = Long.valueOf(this.id);
+
+ private static final long MAX_RATE = 10_000L;
+
+ private final ServerPlayer player;
+ private final ServerLevel world;
+
+ private int lastChunkX = Integer.MIN_VALUE;
+ private int lastChunkZ = Integer.MIN_VALUE;
+
+ private int lastSendDistance = Integer.MIN_VALUE;
+ private int lastLoadDistance = Integer.MIN_VALUE;
+ private int lastTickDistance = Integer.MIN_VALUE;
+
+ private int lastSentChunkCenterX = Integer.MIN_VALUE;
+ private int lastSentChunkCenterZ = Integer.MIN_VALUE;
+
+ private int lastSentChunkRadius = Integer.MIN_VALUE;
+ private int lastSentSimulationDistance = Integer.MIN_VALUE;
+
+ private boolean canGenerateChunks = true;
+
+ private final ArrayDeque<ChunkHolderManager.TicketOperation<?, ?>> delayedTicketOps = new ArrayDeque<>();
+ private final LongOpenHashSet sentChunks = new LongOpenHashSet();
+
+ private static final byte CHUNK_TICKET_STAGE_NONE = 0;
+ private static final byte CHUNK_TICKET_STAGE_LOADING = 1;
+ private static final byte CHUNK_TICKET_STAGE_LOADED = 2;
+ private static final byte CHUNK_TICKET_STAGE_GENERATING = 3;
+ private static final byte CHUNK_TICKET_STAGE_GENERATED = 4;
+ private static final byte CHUNK_TICKET_STAGE_TICK = 5;
+ private static final int[] TICKET_STAGE_TO_LEVEL = new int[] {
+ ChunkHolderManager.MAX_TICKET_LEVEL + 1,
+ LOADED_TICKET_LEVEL,
+ LOADED_TICKET_LEVEL,
+ GENERATED_TICKET_LEVEL,
+ GENERATED_TICKET_LEVEL,
+ TICK_TICKET_LEVEL
+ };
+ private final Long2ByteOpenHashMap chunkTicketStage = new Long2ByteOpenHashMap();
+ {
+ this.chunkTicketStage.defaultReturnValue(CHUNK_TICKET_STAGE_NONE);
+ }
+
+ // rate limiting
+ private static final long ALLOCATION_GRANULARITY = TimeUnit.SECONDS.toNanos(1L);
+ private final AllocatingRateLimiter chunkSendLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY);
+ private final AllocatingRateLimiter chunkLoadTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY);
+ private final AllocatingRateLimiter chunkGenerateTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY);
+
+ // queues
+ private final LongComparator CLOSEST_MANHATTAN_DIST = (final long c1, final long c2) -> {
+ final int c1x = CoordinateUtils.getChunkX(c1);
+ final int c1z = CoordinateUtils.getChunkZ(c1);
+
+ final int c2x = CoordinateUtils.getChunkX(c2);
+ final int c2z = CoordinateUtils.getChunkZ(c2);
+
+ final int centerX = PlayerChunkLoaderData.this.lastChunkX;
+ final int centerZ = PlayerChunkLoaderData.this.lastChunkZ;
+
+ return Integer.compare(
+ Math.abs(c1x - centerX) + Math.abs(c1z - centerZ),
+ Math.abs(c2x - centerX) + Math.abs(c2z - centerZ)
+ );
+ };
+ private final LongHeapPriorityQueue sendQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+ private final LongHeapPriorityQueue tickingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+ private final LongHeapPriorityQueue generatingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+ private final LongHeapPriorityQueue genQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+ private final LongHeapPriorityQueue loadingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+ private final LongHeapPriorityQueue loadQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+
+ private volatile boolean removed;
+
+ public PlayerChunkLoaderData(final ServerLevel world, final ServerPlayer player) {
+ this.world = world;
+ this.player = player;
+ }
+
+ private void flushDelayedTicketOps() {
+ if (this.delayedTicketOps.isEmpty()) {
+ return;
+ }
+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.performTicketUpdates(this.delayedTicketOps);
+ this.delayedTicketOps.clear();
+ }
+
+ private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation<?, ?> op) {
+ this.delayedTicketOps.addLast(op);
+ }
+
+ private void sendChunk(final int chunkX, final int chunkZ) {
+ 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));
+ return;
+ }
+ throw new IllegalStateException();
+ }
+
+ private void sendUnloadChunk(final int chunkX, final int chunkZ) {
+ if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
+ return;
+ }
+ this.sendUnloadChunkRaw(chunkX, chunkZ);
+ }
+
+ private void sendUnloadChunkRaw(final int chunkX, final int chunkZ) {
+ // 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
+ }
+
+ private final SingleUserAreaMap<PlayerChunkLoaderData> broadcastMap = new SingleUserAreaMap<>(this) {
+ @Override
+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ // do nothing, we only care about remove
+ }
+
+ @Override
+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ parameter.sendUnloadChunk(chunkX, chunkZ);
+ }
+ };
+ private final SingleUserAreaMap<PlayerChunkLoaderData> loadTicketCleanup = new SingleUserAreaMap<>(this) {
+ @Override
+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ // do nothing, we only care about remove
+ }
+
+ @Override
+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ final byte ticketStage = parameter.chunkTicketStage.remove(chunk);
+ final int level = TICKET_STAGE_TO_LEVEL[ticketStage];
+ if (level > ChunkHolderManager.MAX_TICKET_LEVEL) {
+ return;
+ }
+
+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove(
+ chunk,
+ PLAYER_TICKET_DELAYED, level, parameter.idBoxed,
+ PLAYER_TICKET, level, parameter.idBoxed
+ ));
+ }
+ };
+ private final SingleUserAreaMap<PlayerChunkLoaderData> tickMap = new SingleUserAreaMap<>(this) {
+ @Override
+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ // do nothing, we will detect ticking chunks when we try to load them
+ }
+
+ @Override
+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ // note: by the time this is called, the tick cleanup should have ran - so, if the chunk is at
+ // the tick stage it was deemed in range for loading. Thus, we need to move it to generated
+ if (!parameter.chunkTicketStage.replace(chunk, CHUNK_TICKET_STAGE_TICK, CHUNK_TICKET_STAGE_GENERATED)) {
+ return;
+ }
+
+ // Since we are possibly downgrading the ticket level, we add the delayed unload ticket so that
+ // the level is kept for a short period of time
+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove(
+ chunk,
+ PLAYER_TICKET_DELAYED, TICK_TICKET_LEVEL, parameter.idBoxed,
+ PLAYER_TICKET, TICK_TICKET_LEVEL, parameter.idBoxed
+ ));
+ // keep chunk at new generated level
+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addOp(
+ chunk, PLAYER_TICKET, GENERATED_TICKET_LEVEL, parameter.idBoxed
+ ));
+ }
+ };
+
+ private static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ,
+ final int sendRadius) {
+ // expect sendRadius to be = 1 + target viewable radius
+ return ChunkTrackingView.isWithinDistance(centerX, centerZ, sendRadius, chunkX, chunkZ, true);
+ }
+
+ private static int getClientViewDistance(final ServerPlayer player) {
+ final Integer vd = player.requestedViewDistance();
+ return vd == null ? -1 : Math.max(0, vd.intValue());
+ }
+
+ private static int getTickDistance(final int playerTickViewDistance, final int worldTickViewDistance,
+ final int playerLoadViewDistance, final int worldLoadViewDistance) {
+ return Math.min(
+ playerTickViewDistance < 0 ? worldTickViewDistance : playerTickViewDistance,
+ playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance
+ );
+ }
+
+ private static int getLoadViewDistance(final int tickViewDistance, final int playerLoadViewDistance,
+ final int worldLoadViewDistance) {
+ return Math.max(tickViewDistance + 1, playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance);
+ }
+
+ private static int getSendViewDistance(final int loadViewDistance, final int clientViewDistance,
+ 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
+ );
+ }
+
+ private Packet<?> updateClientChunkRadius(final int radius) {
+ this.lastSentChunkRadius = radius;
+ return new ClientboundSetChunkCacheRadiusPacket(radius);
+ }
+
+ private Packet<?> updateClientSimulationDistance(final int distance) {
+ this.lastSentSimulationDistance = distance;
+ return new ClientboundSetSimulationDistancePacket(distance);
+ }
+
+ private Packet<?> updateClientChunkCenter(final int chunkX, final int chunkZ) {
+ this.lastSentChunkCenterX = chunkX;
+ this.lastSentChunkCenterZ = chunkZ;
+ return new ClientboundSetChunkCacheCenterPacket(chunkX, chunkZ);
+ }
+
+ private boolean canPlayerGenerateChunks() {
+ return !this.player.isSpectator() || this.world.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS);
+ }
+
+ private double getMaxChunkLoadRate() {
+ final double configRate = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate;
+
+ 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;
+
+ 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;
+
+ 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;
+ 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);
+ } else if (configLimit < 0L) {
+ configLimit = Integer.MAX_VALUE;
+ } // else: use the value configured
+ configLimit = configLimit - this.loadingQueue.size();
+
+ return configLimit;
+ }
+
+ private long getMaxChunkGenerates() {
+ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L);
+ long configLimit = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates;
+ 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);
+ } else if (configLimit < 0L) {
+ configLimit = Integer.MAX_VALUE;
+ } // else: use the value configured
+ configLimit = configLimit - this.generatingQueue.size();
+
+ return configLimit;
+ }
+
+ private boolean wantChunkSent(final int chunkX, final int chunkZ) {
+ final int dx = this.lastChunkX - chunkX;
+ final int dz = this.lastChunkZ - chunkZ;
+ return (Math.max(Math.abs(dx), Math.abs(dz)) <= (this.lastSendDistance + 1)) && wantChunkLoaded(
+ this.lastChunkX, this.lastChunkZ, chunkX, chunkZ, this.lastSendDistance
+ );
+ }
+
+ private boolean wantChunkTicked(final int chunkX, final int chunkZ) {
+ final int dx = this.lastChunkX - chunkX;
+ final int dz = this.lastChunkZ - chunkZ;
+ return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance;
+ }
+
+ private boolean areNeighboursGenerated(final int chunkX, final int chunkZ, final int radius) {
+ for (int dz = -radius; dz <= radius; ++dz) {
+ for (int dx = -radius; dx <= radius; ++dx) {
+ if ((dx | dz) == 0) {
+ continue;
+ }
+
+ final long neighbour = CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ);
+ final byte stage = this.chunkTicketStage.get(neighbour);
+
+ if (stage != CHUNK_TICKET_STAGE_GENERATED && stage != CHUNK_TICKET_STAGE_TICK) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ void updateQueues(final long time) {
+ TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async");
+ if (this.removed) {
+ throw new IllegalStateException("Ticking removed player chunk loader");
+ }
+ // update rate limits
+ final double loadRate = this.getMaxChunkLoadRate();
+ final double genRate = this.getMaxChunkGenRate();
+ final double sendRate = this.getMaxChunkSendRate();
+
+ this.chunkLoadTicketLimiter.tickAllocation(time, loadRate, loadRate);
+ this.chunkGenerateTicketLimiter.tickAllocation(time, genRate, genRate);
+ this.chunkSendLimiter.tickAllocation(time, sendRate, sendRate);
+
+ // try to progress chunk loads
+ while (!this.loadingQueue.isEmpty()) {
+ final long pendingLoadChunk = this.loadingQueue.firstLong();
+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingLoadChunk);
+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingLoadChunk);
+ final ChunkAccess pending = ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(pendingChunkX, pendingChunkZ);
+ if (pending == null) {
+ // nothing to do here
+ break;
+ }
+ // chunk has loaded, so we can take it out of the queue
+ this.loadingQueue.dequeueLong();
+
+ // try to move to generate queue
+ final byte prev = this.chunkTicketStage.put(pendingLoadChunk, CHUNK_TICKET_STAGE_LOADED);
+ if (prev != CHUNK_TICKET_STAGE_LOADING) {
+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADING + ", not " + prev);
+ }
+
+ if (this.canGenerateChunks || this.isLoadedChunkGeneratable(pending)) {
+ this.genQueue.enqueue(pendingLoadChunk);
+ } // else: don't want to generate, so just leave it loaded
+ }
+
+ // try to push more chunk loads
+ final long maxLoads = Math.max(0L, Math.min(MAX_RATE, Math.min(this.loadQueue.size(), this.getMaxChunkLoads())));
+ final int maxLoadsThisTick = (int)this.chunkLoadTicketLimiter.takeAllocation(time, loadRate, maxLoads);
+ if (maxLoadsThisTick > 0) {
+ final LongArrayList chunks = new LongArrayList(maxLoadsThisTick);
+ for (int i = 0; i < maxLoadsThisTick; ++i) {
+ final long chunk = this.loadQueue.dequeueLong();
+ final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_LOADING);
+ if (prev != CHUNK_TICKET_STAGE_NONE) {
+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_NONE + ", not " + prev);
+ }
+ this.pushDelayedTicketOp(
+ ChunkHolderManager.TicketOperation.addOp(
+ chunk,
+ PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed
+ )
+ );
+ chunks.add(chunk);
+ this.loadingQueue.enqueue(chunk);
+ }
+
+ // here we need to flush tickets, as scheduleChunkLoad requires tickets to be propagated with addTicket = false
+ this.flushDelayedTicketOps();
+ // we only need to call scheduleChunkLoad because the loaded ticket level is not enough to start the chunk
+ // load - only generate ticket levels start anything, but they start generation...
+ // propagate levels
+ // Note: this CAN call plugin logic, so it is VITAL that our bookkeeping logic is completely done by the time this is invoked
+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates();
+
+ if (this.removed) {
+ // process ticket updates may invoke plugin logic, which may remove this player
+ return;
+ }
+
+ for (int i = 0; i < maxLoadsThisTick; ++i) {
+ final long queuedLoadChunk = chunks.getLong(i);
+ 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
+ );
+ if (this.removed) {
+ return;
+ }
+ }
+ }
+
+ // try to progress chunk generations
+ while (!this.generatingQueue.isEmpty()) {
+ final long pendingGenChunk = this.generatingQueue.firstLong();
+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingGenChunk);
+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingGenChunk);
+ final LevelChunk pending = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(pendingChunkX, pendingChunkZ);
+ if (pending == null) {
+ // nothing to do here
+ break;
+ }
+
+ // chunk has generated, so we can take it out of queue
+ this.generatingQueue.dequeueLong();
+
+ final byte prev = this.chunkTicketStage.put(pendingGenChunk, CHUNK_TICKET_STAGE_GENERATED);
+ if (prev != CHUNK_TICKET_STAGE_GENERATING) {
+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATING + ", not " + prev);
+ }
+
+ // try to move to send queue
+ if (this.wantChunkSent(pendingChunkX, pendingChunkZ)) {
+ this.sendQueue.enqueue(pendingGenChunk);
+ }
+ // try to move to tick queue
+ if (this.wantChunkTicked(pendingChunkX, pendingChunkZ)) {
+ this.tickingQueue.enqueue(pendingGenChunk);
+ }
+ }
+
+ // try to push more chunk generations
+ final long maxGens = Math.max(0L, Math.min(MAX_RATE, Math.min(this.genQueue.size(), this.getMaxChunkGenerates())));
+ // preview the allocations, as we may not actually utilise all of them
+ final long maxGensThisTick = this.chunkGenerateTicketLimiter.previewAllocation(time, genRate, maxGens);
+ long ratedGensThisTick = 0L;
+ while (!this.genQueue.isEmpty()) {
+ final long chunkKey = this.genQueue.firstLong();
+ final int chunkX = CoordinateUtils.getChunkX(chunkKey);
+ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey);
+ final ChunkAccess chunk = ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ);
+ if (chunk.getPersistedStatus() != ChunkStatus.FULL) {
+ // only rate limit actual generations
+ if ((ratedGensThisTick + 1L) > maxGensThisTick) {
+ break;
+ }
+ ++ratedGensThisTick;
+ }
+
+ this.genQueue.dequeueLong();
+
+ final byte prev = this.chunkTicketStage.put(chunkKey, CHUNK_TICKET_STAGE_GENERATING);
+ if (prev != CHUNK_TICKET_STAGE_LOADED) {
+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADED + ", not " + prev);
+ }
+ this.pushDelayedTicketOp(
+ ChunkHolderManager.TicketOperation.addAndRemove(
+ chunkKey,
+ PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed,
+ PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed
+ )
+ );
+ this.generatingQueue.enqueue(chunkKey);
+ }
+ // take the allocations we actually used
+ this.chunkGenerateTicketLimiter.takeAllocation(time, genRate, ratedGensThisTick);
+
+ // try to pull ticking chunks
+ while (!this.tickingQueue.isEmpty()) {
+ final long pendingTicking = this.tickingQueue.firstLong();
+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingTicking);
+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingTicking);
+
+ if (!this.areNeighboursGenerated(pendingChunkX, pendingChunkZ,
+ ChunkHolderManager.FULL_LOADED_TICKET_LEVEL - ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL)) {
+ break;
+ }
+
+ // only gets here if all neighbours were marked as generated or ticking themselves
+ this.tickingQueue.dequeueLong();
+ this.pushDelayedTicketOp(
+ ChunkHolderManager.TicketOperation.addAndRemove(
+ pendingTicking,
+ PLAYER_TICKET, TICK_TICKET_LEVEL, this.idBoxed,
+ PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed
+ )
+ );
+ // note: there is no queue to add after ticking
+ final byte prev = this.chunkTicketStage.put(pendingTicking, CHUNK_TICKET_STAGE_TICK);
+ if (prev != CHUNK_TICKET_STAGE_GENERATED) {
+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATED + ", not " + prev);
+ }
+ }
+
+ // try to pull sending chunks
+ final long maxSends = Math.max(0L, Math.min(MAX_RATE, Integer.MAX_VALUE)); // note: no logic to track concurrent sends
+ final int maxSendsThisTick = Math.min((int)this.chunkSendLimiter.takeAllocation(time, sendRate, maxSends), this.sendQueue.size());
+ // we do not return sends that we took from the allocation back because we want to limit the max send rate, not target it
+ for (int i = 0; i < maxSendsThisTick; ++i) {
+ final long pendingSend = this.sendQueue.firstLong();
+ final int pendingSendX = CoordinateUtils.getChunkX(pendingSend);
+ final int pendingSendZ = CoordinateUtils.getChunkZ(pendingSend);
+ final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(pendingSendX, pendingSendZ);
+ if (!this.areNeighboursGenerated(pendingSendX, pendingSendZ, 1) || !TickThread.isTickThreadFor(this.world, pendingSendX, pendingSendZ)) {
+ // nothing to do
+ // the target chunk may not be owned by this region, but this should be resolved in the future
+ break;
+ }
+ 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();
+ // check if there was any recursive action
+ if (this.removed || this.sendQueue.isEmpty() || this.sendQueue.firstLong() != pendingSend) {
+ return;
+ } // else: good to dequeue and send, fall through
+ }
+ this.sendQueue.dequeueLong();
+
+ this.sendChunk(pendingSendX, pendingSendZ);
+
+ if (this.removed) {
+ // sendChunk may invoke plugin logic
+ return;
+ }
+ }
+
+ this.flushDelayedTicketOps();
+ }
+
+ void add() {
+ TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
+ if (this.removed) {
+ throw new IllegalStateException("Adding removed player chunk loader");
+ }
+ final ViewDistances playerDistances = ((ChunkSystemServerPlayer)this.player).moonrise$getViewDistanceHolder().getViewDistances();
+ final ViewDistances worldDistances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
+ final int chunkX = this.player.chunkPosition().x;
+ final int chunkZ = this.player.chunkPosition().z;
+
+ final int tickViewDistance = getTickDistance(
+ playerDistances.tickViewDistance, worldDistances.tickViewDistance,
+ playerDistances.loadViewDistance, worldDistances.loadViewDistance
+ );
+ // load view cannot be less-than tick view + 1
+ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance);
+ // send view cannot be greater-than load view
+ 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));
+
+ // add to distance maps
+ this.broadcastMap.add(chunkX, chunkZ, sendViewDistance + 1);
+ this.loadTicketCleanup.add(chunkX, chunkZ, loadViewDistance + 1);
+ this.tickMap.add(chunkX, chunkZ, tickViewDistance);
+
+ // update chunk center
+ this.player.connection.send(this.updateClientChunkCenter(chunkX, chunkZ));
+
+ // reset limiters, they will start at a zero allocation
+ final long time = System.nanoTime();
+ this.chunkLoadTicketLimiter.reset(time);
+ this.chunkGenerateTicketLimiter.reset(time);
+ this.chunkSendLimiter.reset(time);
+
+ // now we can update
+ this.update();
+ }
+
+ private boolean isLoadedChunkGeneratable(final int chunkX, final int chunkZ) {
+ return this.isLoadedChunkGeneratable(((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ));
+ }
+
+ private boolean isLoadedChunkGeneratable(final ChunkAccess chunkAccess) {
+ final BelowZeroRetrogen belowZeroRetrogen;
+ // see PortalForcer#findPortalAround
+ return chunkAccess != null && (
+ chunkAccess.getPersistedStatus() == ChunkStatus.FULL ||
+ ((belowZeroRetrogen = chunkAccess.getBelowZeroRetrogen()) != null && belowZeroRetrogen.targetStatus().isOrAfter(ChunkStatus.SPAWN))
+ );
+ }
+
+ void update() {
+ TickThread.ensureTickThread(this.player, "Cannot update player asynchronously");
+ if (this.removed) {
+ throw new IllegalStateException("Updating removed player chunk loader");
+ }
+ final ViewDistances playerDistances = ((ChunkSystemServerPlayer)this.player).moonrise$getViewDistanceHolder().getViewDistances();
+ final ViewDistances worldDistances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
+
+ final int tickViewDistance = getTickDistance(
+ playerDistances.tickViewDistance, worldDistances.tickViewDistance,
+ playerDistances.loadViewDistance, worldDistances.loadViewDistance
+ );
+ // load view cannot be less-than tick view + 1
+ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance);
+ // send view cannot be greater-than load view
+ final int clientViewDistance = getClientViewDistance(this.player);
+ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance);
+
+ final ChunkPos playerPos = this.player.chunkPosition();
+ final boolean canGenerateChunks = this.canPlayerGenerateChunks();
+ final int currentChunkX = playerPos.x;
+ final int currentChunkZ = playerPos.z;
+
+ final int prevChunkX = this.lastChunkX;
+ final int prevChunkZ = this.lastChunkZ;
+
+ if (
+ // has view distance stayed the same?
+ sendViewDistance == this.lastSendDistance
+ && loadViewDistance == this.lastLoadDistance
+ && tickViewDistance == this.lastTickDistance
+
+ // has our chunk stayed the same?
+ && prevChunkX == currentChunkX
+ && prevChunkZ == currentChunkZ
+
+ // can we still generate chunks?
+ && this.canGenerateChunks == canGenerateChunks
+ ) {
+ // nothing we care about changed, so we're not re-calculating
+ return;
+ }
+
+ // update distance maps
+ this.broadcastMap.update(currentChunkX, currentChunkZ, sendViewDistance + 1);
+ this.loadTicketCleanup.update(currentChunkX, currentChunkZ, loadViewDistance + 1);
+ this.tickMap.update(currentChunkX, currentChunkZ, tickViewDistance);
+ if (sendViewDistance > loadViewDistance || tickViewDistance > loadViewDistance) {
+ throw new IllegalStateException();
+ }
+
+ // update VDs for client
+ // this should be after the distance map updates, as they will send unload packets
+ if (this.lastSentChunkRadius != sendViewDistance) {
+ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance));
+ }
+ if (this.lastSentSimulationDistance != tickViewDistance) {
+ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance));
+ }
+
+ this.sendQueue.clear();
+ this.tickingQueue.clear();
+ this.generatingQueue.clear();
+ this.genQueue.clear();
+ this.loadingQueue.clear();
+ this.loadQueue.clear();
+
+ this.lastChunkX = currentChunkX;
+ this.lastChunkZ = currentChunkZ;
+ this.lastSendDistance = sendViewDistance;
+ this.lastLoadDistance = loadViewDistance;
+ this.lastTickDistance = tickViewDistance;
+ this.canGenerateChunks = canGenerateChunks;
+
+ // +1 since we need to load chunks +1 around the load view distance...
+ final long[] toIterate = ParallelSearchRadiusIteration.getSearchIteration(loadViewDistance + 1);
+ // the iteration order is by increasing manhattan distance - so, we do NOT need to
+ // sort anything in the queue!
+ for (final long deltaChunk : toIterate) {
+ final int dx = CoordinateUtils.getChunkX(deltaChunk);
+ final int dz = CoordinateUtils.getChunkZ(deltaChunk);
+ final int chunkX = dx + currentChunkX;
+ final int chunkZ = dz + currentChunkZ;
+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz));
+ final int manhattanDistance = Math.abs(dx) + Math.abs(dz);
+
+ // since chunk sending is not by radius alone, we need an extra check here to account for
+ // everything <= sendDistance
+ // Note: Vanilla may want to send chunks outside the send view distance, so we do need
+ // the dist <= view check
+ final boolean sendChunk = (squareDistance <= (sendViewDistance + 1))
+ && wantChunkLoaded(currentChunkX, currentChunkZ, chunkX, chunkZ, sendViewDistance);
+ final boolean sentChunk = sendChunk ? this.sentChunks.contains(chunk) : this.sentChunks.remove(chunk);
+
+ if (!sendChunk && sentChunk) {
+ // have sent the chunk, but don't want it anymore
+ // unload it now
+ this.sendUnloadChunkRaw(chunkX, chunkZ);
+ }
+
+ final byte stage = this.chunkTicketStage.get(chunk);
+ switch (stage) {
+ case CHUNK_TICKET_STAGE_NONE: {
+ // we want the chunk to be at least loaded
+ this.loadQueue.enqueue(chunk);
+ break;
+ }
+ case CHUNK_TICKET_STAGE_LOADING: {
+ this.loadingQueue.enqueue(chunk);
+ break;
+ }
+ case CHUNK_TICKET_STAGE_LOADED: {
+ if (canGenerateChunks || this.isLoadedChunkGeneratable(chunkX, chunkZ)) {
+ this.genQueue.enqueue(chunk);
+ }
+ break;
+ }
+ case CHUNK_TICKET_STAGE_GENERATING: {
+ this.generatingQueue.enqueue(chunk);
+ break;
+ }
+ case CHUNK_TICKET_STAGE_GENERATED: {
+ if (sendChunk && !sentChunk) {
+ this.sendQueue.enqueue(chunk);
+ }
+ if (squareDistance <= tickViewDistance) {
+ this.tickingQueue.enqueue(chunk);
+ }
+ break;
+ }
+ case CHUNK_TICKET_STAGE_TICK: {
+ if (sendChunk && !sentChunk) {
+ this.sendQueue.enqueue(chunk);
+ }
+ break;
+ }
+ default: {
+ throw new IllegalStateException("Unknown stage: " + stage);
+ }
+ }
+ }
+
+ // update the chunk center
+ // this must be done last so that the client does not ignore any of our unload chunk packets above
+ if (this.lastSentChunkCenterX != currentChunkX || this.lastSentChunkCenterZ != currentChunkZ) {
+ this.player.connection.send(this.updateClientChunkCenter(currentChunkX, currentChunkZ));
+ }
+
+ this.flushDelayedTicketOps();
+ }
+
+ void remove() {
+ TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
+ if (this.removed) {
+ throw new IllegalStateException("Removing removed player chunk loader");
+ }
+ this.removed = true;
+ // sends the chunk unload packets
+ this.broadcastMap.remove();
+ // cleans up loading/generating tickets
+ this.loadTicketCleanup.remove();
+ // cleans up ticking tickets
+ this.tickMap.remove();
+
+ // purge queues
+ this.sendQueue.clear();
+ this.tickingQueue.clear();
+ this.generatingQueue.clear();
+ this.genQueue.clear();
+ this.loadingQueue.clear();
+ this.loadQueue.clear();
+
+ // flush ticket changes
+ this.flushDelayedTicketOps();
+
+ // now all tickets should be removed, which is all of our external state
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..7eafc5b7cba23d8dec92ecc1050afe3fd8c9e309
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
@@ -0,0 +1,144 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.queue;
+
+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import it.unimi.dsi.fastutil.longs.LongIterator;
+import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+public final class ChunkUnloadQueue {
+
+ public final int coordinateShift;
+ private final AtomicLong orderGenerator = new AtomicLong();
+ private final ConcurrentLong2ReferenceChainedHashTable<UnloadSection> unloadSections = new ConcurrentLong2ReferenceChainedHashTable<>();
+
+ /*
+ * Note: write operations do not occur in parallel for any given section.
+ * Note: coordinateShift <= region shift in order for retrieveForCurrentRegion() to function correctly
+ */
+
+ public ChunkUnloadQueue(final int coordinateShift) {
+ this.coordinateShift = coordinateShift;
+ }
+
+ public static record SectionToUnload(int sectionX, int sectionZ, long order, int count) {}
+
+ public List<SectionToUnload> retrieveForAllRegions() {
+ final List<SectionToUnload> ret = new ArrayList<>();
+
+ for (final Iterator<ConcurrentLong2ReferenceChainedHashTable.TableEntry<UnloadSection>> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) {
+ final ConcurrentLong2ReferenceChainedHashTable.TableEntry<UnloadSection> entry = iterator.next();
+ final long key = entry.getKey();
+ final UnloadSection section = entry.getValue();
+ final int sectionX = CoordinateUtils.getChunkX(key);
+ final int sectionZ = CoordinateUtils.getChunkZ(key);
+
+ ret.add(new SectionToUnload(sectionX, sectionZ, section.order, section.chunks.size()));
+ }
+
+ ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> {
+ return Long.compare(s1.order, s2.order);
+ });
+
+ return ret;
+ }
+
+ public UnloadSection getSectionUnsynchronized(final int sectionX, final int sectionZ) {
+ return this.unloadSections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ));
+ }
+
+ public UnloadSection removeSection(final int sectionX, final int sectionZ) {
+ return this.unloadSections.remove(CoordinateUtils.getChunkKey(sectionX, sectionZ));
+ }
+
+ // write operation
+ public boolean addChunk(final int chunkX, final int chunkZ) {
+ // write operations do not occur in parallel for a given section
+ final int shift = this.coordinateShift;
+ final int sectionX = chunkX >> shift;
+ final int sectionZ = chunkZ >> shift;
+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ UnloadSection section = this.unloadSections.get(sectionKey);
+ if (section == null) {
+ section = new UnloadSection(this.orderGenerator.getAndIncrement());
+ this.unloadSections.put(sectionKey, section);
+ }
+
+ return section.chunks.add(chunkKey);
+ }
+
+ // write operation
+ public boolean removeChunk(final int chunkX, final int chunkZ) {
+ // write operations do not occur in parallel for a given section
+ final int shift = this.coordinateShift;
+ final int sectionX = chunkX >> shift;
+ final int sectionZ = chunkZ >> shift;
+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ final UnloadSection section = this.unloadSections.get(sectionKey);
+
+ if (section == null) {
+ return false;
+ }
+
+ if (!section.chunks.remove(chunkKey)) {
+ return false;
+ }
+
+ if (section.chunks.isEmpty()) {
+ this.unloadSections.remove(sectionKey);
+ }
+
+ return true;
+ }
+
+ public JsonElement toDebugJson() {
+ final JsonArray ret = new JsonArray();
+
+ for (final SectionToUnload section : this.retrieveForAllRegions()) {
+ final JsonObject sectionJson = new JsonObject();
+ ret.add(sectionJson);
+
+ sectionJson.addProperty("sectionX", section.sectionX());
+ sectionJson.addProperty("sectionZ", section.sectionX());
+ sectionJson.addProperty("order", section.order());
+
+ final JsonArray coordinates = new JsonArray();
+ sectionJson.add("coordinates", coordinates);
+
+ final UnloadSection actualSection = this.getSectionUnsynchronized(section.sectionX(), section.sectionZ());
+ if (actualSection != null) {
+ for (final LongIterator iterator = actualSection.chunks.clone().iterator(); iterator.hasNext(); ) {
+ final long coordinate = iterator.nextLong();
+
+ final JsonObject coordinateJson = new JsonObject();
+ coordinates.add(coordinateJson);
+
+ coordinateJson.addProperty("chunkX", Integer.valueOf(CoordinateUtils.getChunkX(coordinate)));
+ coordinateJson.addProperty("chunkZ", Integer.valueOf(CoordinateUtils.getChunkZ(coordinate)));
+ }
+ }
+ }
+
+ return ret;
+ }
+
+ public static final class UnloadSection {
+
+ public final long order;
+ public final LongLinkedOpenHashSet chunks = new LongLinkedOpenHashSet();
+
+ public UnloadSection(final long order) {
+ this.order = order;
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b58a8b5ef
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
@@ -0,0 +1,1428 @@
+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.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.level.ChunkSystemServerLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk;
+import ca.spottedleaf.moonrise.patches.chunk_system.queue.ChunkUnloadQueue;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.GenericDataLoadTask;
+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;
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.longs.LongIterator;
+import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet;
+import net.minecraft.nbt.CompoundTag;
+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.Ticket;
+import net.minecraft.server.level.TicketType;
+import net.minecraft.util.SortedArraySet;
+import net.minecraft.util.Unit;
+import net.minecraft.world.level.ChunkPos;
+import org.slf4j.Logger;
+import java.io.IOException;
+import java.text.DecimalFormat;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.PrimitiveIterator;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.LockSupport;
+import java.util.function.Predicate;
+
+public final class ChunkHolderManager {
+
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+
+ public static final int FULL_LOADED_TICKET_LEVEL = ChunkLevel.FULL_CHUNK_LEVEL;
+ public static final int BLOCK_TICKING_TICKET_LEVEL = ChunkLevel.BLOCK_TICKING_LEVEL;
+ public static final int ENTITY_TICKING_TICKET_LEVEL = ChunkLevel.ENTITY_TICKING_LEVEL;
+ public static final int MAX_TICKET_LEVEL = ChunkLevel.MAX_LEVEL; // inclusive
+
+ public static final TicketType<Unit> UNLOAD_COOLDOWN = TicketType.create("unload_cooldown", (u1, u2) -> 0, 5 * 20);
+
+ private static final long NO_TIMEOUT_MARKER = Long.MIN_VALUE;
+ private static final long PROBE_MARKER = Long.MIN_VALUE + 1;
+ public final ReentrantAreaLock ticketLockArea;
+
+ private final ConcurrentLong2ReferenceChainedHashTable<SortedArraySet<Ticket<?>>> tickets = new ConcurrentLong2ReferenceChainedHashTable<>();
+ private final ConcurrentLong2ReferenceChainedHashTable<Long2IntOpenHashMap> sectionToChunkToExpireCount = new ConcurrentLong2ReferenceChainedHashTable<>();
+ final ChunkUnloadQueue unloadQueue;
+
+ private final ConcurrentLong2ReferenceChainedHashTable<NewChunkHolder> chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f);
+ private final ServerLevel world;
+ private final ChunkTaskScheduler taskScheduler;
+ private long currentTick;
+
+ private final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = new ArrayDeque<>();
+ private final ObjectRBTreeSet<NewChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
+ if (c1 == c2) {
+ return 0;
+ }
+
+ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
+
+ if (saveTickCompare != 0) {
+ return saveTickCompare;
+ }
+
+ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
+ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
+
+ if (coord1 == coord2) {
+ throw new IllegalStateException("Duplicate chunkholder in auto save queue");
+ }
+
+ return Long.compare(coord1, coord2);
+ });
+
+ public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
+ this.world = world;
+ this.taskScheduler = taskScheduler;
+ this.ticketLockArea = new ReentrantAreaLock(taskScheduler.getChunkSystemLockShift());
+ this.unloadQueue = new ChunkUnloadQueue(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift());
+ }
+
+ public boolean processTicketUpdates(final int posX, final int posZ) {
+ final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT;
+ final int ticketMask = (1 << ticketShift) - 1;
+ final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>();
+ final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
+ final boolean ret;
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
+ ((posX >> ticketShift) - 1) << ticketShift,
+ ((posZ >> ticketShift) - 1) << ticketShift,
+ (((posX >> ticketShift) + 1) << ticketShift) | ticketMask,
+ (((posZ >> ticketShift) + 1) << ticketShift) | ticketMask
+ );
+ try {
+ ret = this.processTicketUpdatesNoLock(posX >> ticketShift, posZ >> ticketShift, scheduledTasks, changedFullStatus);
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+
+ this.addChangedStatuses(changedFullStatus);
+
+ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) {
+ scheduledTasks.get(i).schedule();
+ }
+
+ return ret;
+ }
+
+ private boolean processTicketUpdatesNoLock(final int sectionX, final int sectionZ, final List<ChunkProgressionTask> scheduledTasks,
+ final List<NewChunkHolder> changedFullStatus) {
+ return this.ticketLevelPropagator.performUpdate(
+ sectionX, sectionZ, this.taskScheduler.schedulingLockArea, scheduledTasks, changedFullStatus
+ );
+ }
+
+ public List<ChunkHolder> getOldChunkHolders() {
+ final List<ChunkHolder> ret = new ArrayList<>(this.chunkHolders.size() + 1);
+ for (final Iterator<NewChunkHolder> iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) {
+ ret.add(iterator.next().vanillaChunkHolder);
+ }
+ return ret;
+ }
+
+ public List<NewChunkHolder> getChunkHolders() {
+ final List<NewChunkHolder> ret = new ArrayList<>(this.chunkHolders.size() + 1);
+ for (final Iterator<NewChunkHolder> iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) {
+ ret.add(iterator.next());
+ }
+ return ret;
+ }
+
+ public int size() {
+ return this.chunkHolders.size();
+ }
+
+ // TODO replace the need for this, specifically: optimise ServerChunkCache#tickChunks
+ public Iterable<ChunkHolder> getOldChunkHoldersIterable() {
+ return new Iterable<ChunkHolder>() {
+ @Override
+ public Iterator<ChunkHolder> iterator() {
+ final Iterator<NewChunkHolder> iterator = ChunkHolderManager.this.chunkHolders.valueIterator();
+ return new Iterator<ChunkHolder>() {
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public ChunkHolder next() {
+ return iterator.next().vanillaChunkHolder;
+ }
+ };
+ }
+ };
+ }
+
+ public void close(final boolean save, final boolean halt) {
+ TickThread.ensureTickThread("Closing world off-main");
+ 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) + "'");
+ } else {
+ LOGGER.info("Halted chunk system for world '" + WorldUtil.getWorldName(this.world) + "'");
+ }
+ }
+
+ if (save) {
+ 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;
+ }
+ }
+ if (hasTasks) {
+ RegionFileIOThread.flush();
+ }
+
+ // kill regionfile cache
+ for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) {
+ try {
+ RegionFileIOThread.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);
+ }
+ }
+ }
+
+ void ensureInAutosave(final NewChunkHolder holder) {
+ if (!this.autoSaveQueue.contains(holder)) {
+ holder.lastAutoSave = this.currentTick;
+ this.autoSaveQueue.add(holder);
+ }
+ }
+
+ 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;
+ for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) {
+ final NewChunkHolder holder = this.autoSaveQueue.first();
+
+ if (holder.lastAutoSave > maxSaveTime) {
+ break;
+ }
+
+ this.autoSaveQueue.remove(holder);
+
+ holder.lastAutoSave = currentTick;
+ if (holder.save(false) != null) {
+ ++autoSaved;
+ }
+
+ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) {
+ reschedule.add(holder);
+ }
+ }
+
+ for (final NewChunkHolder holder : reschedule) {
+ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) {
+ this.autoSaveQueue.add(holder);
+ }
+ }
+ }
+
+ public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) {
+ final List<NewChunkHolder> holders = this.getChunkHolders();
+
+ if (logProgress) {
+ LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'");
+ }
+
+ final DecimalFormat format = new DecimalFormat("#0.00");
+
+ int saved = 0;
+
+ long start = System.nanoTime();
+ long lastLog = start;
+ boolean needsFlush = false;
+ final int flushInterval = 50;
+
+ int savedChunk = 0;
+ int savedEntity = 0;
+ int savedPoi = 0;
+
+ 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;
+ }
+ if (saveStat.savedEntityChunk()) {
+ ++savedEntity;
+ }
+ if (saveStat.savedPoiChunk()) {
+ ++savedPoi;
+ }
+ }
+ } 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 (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) + "'");
+ }
+ }
+ }
+ if (flush) {
+ RegionFileIOThread.flush();
+ try {
+ RegionFileIOThread.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");
+ }
+ }
+
+ private final ThreadedTicketLevelPropagator ticketLevelPropagator = new ThreadedTicketLevelPropagator() {
+ @Override
+ protected void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates) {
+ // first the necessary chunkholders must be created, so just update the ticket levels
+ for (final Iterator<Long2ByteMap.Entry> iterator = updates.long2ByteEntrySet().fastIterator(); iterator.hasNext();) {
+ final Long2ByteMap.Entry entry = iterator.next();
+ final long key = entry.getLongKey();
+ final int newLevel = convertBetweenTicketLevels((int)entry.getByteValue());
+
+ NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key);
+ if (current == null && newLevel > MAX_TICKET_LEVEL) {
+ // not loaded and it shouldn't be loaded!
+ iterator.remove();
+ continue;
+ }
+
+ final int currentLevel = current == null ? MAX_TICKET_LEVEL + 1 : current.getCurrentTicketLevel();
+ if (currentLevel == newLevel) {
+ // nothing to do
+ iterator.remove();
+ continue;
+ }
+
+ if (current == null) {
+ // must create
+ current = ChunkHolderManager.this.createChunkHolder(key);
+ ChunkHolderManager.this.chunkHolders.put(key, current);
+ current.updateTicketLevel(newLevel);
+ } else {
+ current.updateTicketLevel(newLevel);
+ }
+ }
+ }
+
+ @Override
+ protected void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List<ChunkProgressionTask> scheduledTasks,
+ final List<NewChunkHolder> changedFullStatus) {
+ final List<ChunkProgressionTask> prev = CURRENT_TICKET_UPDATE_SCHEDULING.get();
+ CURRENT_TICKET_UPDATE_SCHEDULING.set(scheduledTasks);
+ try {
+ for (final LongIterator iterator = updates.keySet().iterator(); iterator.hasNext();) {
+ final long key = iterator.nextLong();
+ final NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key);
+
+ if (current == null) {
+ throw new IllegalStateException("Expected chunk holder to be created");
+ }
+
+ current.processTicketLevelUpdate(scheduledTasks, changedFullStatus);
+ }
+ } finally {
+ CURRENT_TICKET_UPDATE_SCHEDULING.set(prev);
+ }
+ }
+ };
+ // function for converting between ticket levels and propagator levels and vice versa
+ // the problem is the ticket level propagator will propagate from a set source down to zero, whereas mojang expects
+ // levels to propagate from a set value up to a maximum value. so we need to convert the levels we put into the propagator
+ // and the levels we get out of the propagator
+
+ public static int convertBetweenTicketLevels(final int level) {
+ return ChunkLevel.MAX_LEVEL - level + 1;
+ }
+
+ public String getTicketDebugString(final long coordinate) {
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate));
+ try {
+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coordinate);
+
+ return tickets != null ? tickets.first().toString() : "no_ticket";
+ } finally {
+ if (ticketLock != null) {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+ }
+
+ public Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> getTicketsCopy() {
+ final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> ret = new Long2ObjectOpenHashMap<>();
+ final Long2ObjectOpenHashMap<LongArrayList> sections = new Long2ObjectOpenHashMap<>();
+ final int sectionShift = this.taskScheduler.getChunkSystemLockShift();
+ for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) {
+ final long coord = iterator.nextLong();
+ sections.computeIfAbsent(
+ CoordinateUtils.getChunkKey(
+ CoordinateUtils.getChunkX(coord) >> sectionShift,
+ CoordinateUtils.getChunkZ(coord) >> sectionShift
+ ),
+ (final long keyInMap) -> {
+ return new LongArrayList();
+ }
+ ).add(coord);
+ }
+
+ for (final Iterator<Long2ObjectMap.Entry<LongArrayList>> iterator = sections.long2ObjectEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Long2ObjectMap.Entry<LongArrayList> entry = iterator.next();
+ final long sectionKey = entry.getLongKey();
+ final LongArrayList coordinates = entry.getValue();
+
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
+ CoordinateUtils.getChunkX(sectionKey) << sectionShift,
+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift
+ );
+ try {
+ for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) {
+ final long coord = iterator2.nextLong();
+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coord);
+ if (tickets == null) {
+ // removed before we acquired lock
+ continue;
+ }
+ ret.put(coord, ((ChunkSystemSortedArraySet<Ticket<?>>)tickets).moonrise$copy());
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+
+ return ret;
+ }
+
+ // Paper start
+ public Collection<org.bukkit.plugin.Plugin> getPluginChunkTickets(int x, int z) {
+ com.google.common.collect.ImmutableList.Builder<org.bukkit.plugin.Plugin> ret;
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(x, z);
+ try {
+ final long coordinate = CoordinateUtils.getChunkKey(x, z);
+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coordinate);
+
+ if (tickets == null) {
+ return java.util.Collections.emptyList();
+ }
+
+ ret = com.google.common.collect.ImmutableList.builder();
+ for (Ticket<?> ticket : tickets) {
+ if (ticket.getType() == TicketType.PLUGIN_TICKET) {
+ ret.add((org.bukkit.plugin.Plugin)ticket.key);
+ }
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+
+ return ret.build();
+ }
+ // Paper end
+
+ protected final void updateTicketLevel(final long coordinate, final int ticketLevel) {
+ if (ticketLevel > ChunkLevel.MAX_LEVEL) {
+ this.ticketLevelPropagator.removeSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate));
+ } else {
+ this.ticketLevelPropagator.setSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate), convertBetweenTicketLevels(ticketLevel));
+ }
+ }
+
+ private static int getTicketLevelAt(SortedArraySet<Ticket<?>> tickets) {
+ return !tickets.isEmpty() ? tickets.first().getTicketLevel() : MAX_TICKET_LEVEL + 1;
+ }
+
+ public <T> boolean addTicketAtLevel(final TicketType<T> type, final ChunkPos chunkPos, final int level,
+ final T identifier) {
+ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier);
+ }
+
+ public <T> boolean addTicketAtLevel(final TicketType<T> type, final int chunkX, final int chunkZ, final int level,
+ final T identifier) {
+ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier);
+ }
+
+ private void addExpireCount(final int chunkX, final int chunkZ) {
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
+ final long sectionKey = CoordinateUtils.getChunkKey(
+ chunkX >> sectionShift,
+ chunkZ >> sectionShift
+ );
+
+ this.sectionToChunkToExpireCount.computeIfAbsent(sectionKey, (final long keyInMap) -> {
+ return new Long2IntOpenHashMap();
+ }).addTo(chunkKey, 1);
+ }
+
+ private void removeExpireCount(final int chunkX, final int chunkZ) {
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
+ final long sectionKey = CoordinateUtils.getChunkKey(
+ chunkX >> sectionShift,
+ chunkZ >> sectionShift
+ );
+
+ final Long2IntOpenHashMap removeCounts = this.sectionToChunkToExpireCount.get(sectionKey);
+ final int prevCount = removeCounts.addTo(chunkKey, -1);
+
+ if (prevCount == 1) {
+ removeCounts.remove(chunkKey);
+ if (removeCounts.isEmpty()) {
+ this.sectionToChunkToExpireCount.remove(sectionKey);
+ }
+ }
+ }
+
+ // supposed to return true if the ticket was added and did not replace another
+ // but, we always return false if the ticket cannot be added
+ public <T> boolean addTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier) {
+ return this.addTicketAtLevel(type, chunk, level, identifier, true);
+ }
+
+ <T> boolean addTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier, final boolean lock) {
+ final long removeDelay = type.timeout <= 0 ? NO_TIMEOUT_MARKER : type.timeout;
+ if (level > MAX_TICKET_LEVEL) {
+ return false;
+ }
+
+ final int chunkX = CoordinateUtils.getChunkX(chunk);
+ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
+ final Ticket<T> ticket = new Ticket<>(type, level, identifier);
+ ((ChunkSystemTicket<T>)(Object)ticket).moonrise$setRemoveDelay(removeDelay);
+
+ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null;
+ try {
+ final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> {
+ return SortedArraySet.create(4);
+ });
+
+ final int levelBefore = getTicketLevelAt(ticketsAtChunk);
+ final Ticket<T> current = (Ticket<T>)((ChunkSystemSortedArraySet<Ticket<?>>)ticketsAtChunk).moonrise$replace(ticket);
+ final int levelAfter = getTicketLevelAt(ticketsAtChunk);
+
+ if (current != ticket) {
+ final long oldRemoveDelay = ((ChunkSystemTicket<T>)(Object)current).moonrise$getRemoveDelay();
+ if (removeDelay != oldRemoveDelay) {
+ if (oldRemoveDelay != NO_TIMEOUT_MARKER && removeDelay == NO_TIMEOUT_MARKER) {
+ this.removeExpireCount(chunkX, chunkZ);
+ } else if (oldRemoveDelay == NO_TIMEOUT_MARKER) {
+ // since old != new, we have that NO_TIMEOUT_MARKER != new
+ this.addExpireCount(chunkX, chunkZ);
+ }
+ }
+ } else {
+ if (removeDelay != NO_TIMEOUT_MARKER) {
+ this.addExpireCount(chunkX, chunkZ);
+ }
+ }
+
+ if (levelBefore != levelAfter) {
+ this.updateTicketLevel(chunk, levelAfter);
+ }
+
+ return current == ticket;
+ } finally {
+ if (ticketLock != null) {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+ }
+
+ public <T> boolean removeTicketAtLevel(final TicketType<T> type, final ChunkPos chunkPos, final int level, final T identifier) {
+ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier);
+ }
+
+ public <T> boolean removeTicketAtLevel(final TicketType<T> type, final int chunkX, final int chunkZ, final int level, final T identifier) {
+ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier);
+ }
+
+ public <T> boolean removeTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier) {
+ return this.removeTicketAtLevel(type, chunk, level, identifier, true);
+ }
+
+ <T> boolean removeTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier, final boolean lock) {
+ if (level > MAX_TICKET_LEVEL) {
+ return false;
+ }
+
+ final int chunkX = CoordinateUtils.getChunkX(chunk);
+ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
+ final Ticket<T> probe = new Ticket<>(type, level, identifier);
+
+ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null;
+ try {
+ final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.get(chunk);
+ if (ticketsAtChunk == null) {
+ return false;
+ }
+
+ final int oldLevel = getTicketLevelAt(ticketsAtChunk);
+ final Ticket<T> ticket = (Ticket<T>)((ChunkSystemSortedArraySet<Ticket<?>>)ticketsAtChunk).moonrise$removeAndGet(probe);
+
+ if (ticket == null) {
+ return false;
+ }
+
+ final int newLevel = getTicketLevelAt(ticketsAtChunk);
+ // we should not change the ticket levels while the target region may be ticking
+ if (oldLevel != newLevel) {
+ final Ticket<ChunkPos> unknownTicket = new Ticket<>(TicketType.UNKNOWN, level, new ChunkPos(chunk));
+ ((ChunkSystemTicket<ChunkPos>)(Object)unknownTicket).moonrise$setRemoveDelay(Math.max(1, TicketType.UNKNOWN.timeout));
+ if (ticketsAtChunk.add(unknownTicket)) {
+ this.addExpireCount(chunkX, chunkZ);
+ } else {
+ throw new IllegalStateException("Should have been able to add " + unknownTicket + " to " + ticketsAtChunk);
+ }
+ }
+
+ final long removeDelay = ((ChunkSystemTicket<T>)(Object)ticket).moonrise$getRemoveDelay();
+ if (removeDelay != NO_TIMEOUT_MARKER) {
+ this.removeExpireCount(chunkX, chunkZ);
+ }
+
+ return true;
+ } finally {
+ if (ticketLock != null) {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+ }
+
+ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk
+ public <T, V> void addAndRemoveTickets(final long chunk, final TicketType<T> addType, final int addLevel, final T addIdentifier,
+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) {
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk));
+ try {
+ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false);
+ this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false);
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+
+ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk
+ public <T, V> boolean addIfRemovedTicket(final long chunk, final TicketType<T> addType, final int addLevel, final T addIdentifier,
+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) {
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk));
+ try {
+ if (this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false)) {
+ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false);
+ return true;
+ }
+ return false;
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+
+ public <T> void removeAllTicketsFor(final TicketType<T> ticketType, final int ticketLevel, final T ticketIdentifier) {
+ if (ticketLevel > MAX_TICKET_LEVEL) {
+ return;
+ }
+
+ final Long2ObjectOpenHashMap<LongArrayList> sections = new Long2ObjectOpenHashMap<>();
+ final int sectionShift = this.taskScheduler.getChunkSystemLockShift();
+ for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) {
+ final long coord = iterator.nextLong();
+ sections.computeIfAbsent(
+ CoordinateUtils.getChunkKey(
+ CoordinateUtils.getChunkX(coord) >> sectionShift,
+ CoordinateUtils.getChunkZ(coord) >> sectionShift
+ ),
+ (final long keyInMap) -> {
+ return new LongArrayList();
+ }
+ ).add(coord);
+ }
+
+ for (final Iterator<Long2ObjectMap.Entry<LongArrayList>> iterator = sections.long2ObjectEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Long2ObjectMap.Entry<LongArrayList> entry = iterator.next();
+ final long sectionKey = entry.getLongKey();
+ final LongArrayList coordinates = entry.getValue();
+
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
+ CoordinateUtils.getChunkX(sectionKey) << sectionShift,
+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift
+ );
+ try {
+ for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) {
+ final long coord = iterator2.nextLong();
+ this.removeTicketAtLevel(ticketType, coord, ticketLevel, ticketIdentifier, false);
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+ }
+
+ public void tick() {
+ ++this.currentTick;
+
+ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
+
+ final Predicate<Ticket<?>> expireNow = (final Ticket<?> ticket) -> {
+ long removeDelay = ((ChunkSystemTicket<?>)(Object)ticket).moonrise$getRemoveDelay();
+ if (removeDelay == NO_TIMEOUT_MARKER) {
+ return false;
+ }
+ --removeDelay;
+ ((ChunkSystemTicket<?>)(Object)ticket).moonrise$setRemoveDelay(removeDelay);
+ return removeDelay <= 0L;
+ };
+
+ for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) {
+ final long sectionKey = iterator.nextLong();
+
+ if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) {
+ // removed concurrently
+ continue;
+ }
+
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
+ CoordinateUtils.getChunkX(sectionKey) << sectionShift,
+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift
+ );
+
+ try {
+ final Long2IntOpenHashMap chunkToExpireCount = this.sectionToChunkToExpireCount.get(sectionKey);
+ if (chunkToExpireCount == null) {
+ // lost to some race
+ continue;
+ }
+
+ for (final Iterator<Long2IntMap.Entry> iterator1 = chunkToExpireCount.long2IntEntrySet().fastIterator(); iterator1.hasNext();) {
+ final Long2IntMap.Entry entry = iterator1.next();
+
+ final long chunkKey = entry.getLongKey();
+ final int expireCount = entry.getIntValue();
+
+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(chunkKey);
+ final int levelBefore = getTicketLevelAt(tickets);
+
+ final int sizeBefore = tickets.size();
+ tickets.removeIf(expireNow);
+ final int sizeAfter = tickets.size();
+ final int levelAfter = getTicketLevelAt(tickets);
+
+ if (tickets.isEmpty()) {
+ this.tickets.remove(chunkKey);
+ }
+ if (levelBefore != levelAfter) {
+ this.updateTicketLevel(chunkKey, levelAfter);
+ }
+
+ final int newExpireCount = expireCount - (sizeBefore - sizeAfter);
+
+ if (newExpireCount == expireCount) {
+ continue;
+ }
+
+ if (newExpireCount != 0) {
+ entry.setValue(newExpireCount);
+ } else {
+ iterator1.remove();
+ }
+ }
+
+ if (chunkToExpireCount.isEmpty()) {
+ this.sectionToChunkToExpireCount.remove(sectionKey);
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+
+ this.processTicketUpdates();
+ }
+
+ public NewChunkHolder getChunkHolder(final int chunkX, final int chunkZ) {
+ return this.chunkHolders.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+
+ public NewChunkHolder getChunkHolder(final long position) {
+ return this.chunkHolders.get(position);
+ }
+
+ public void raisePriority(final int x, final int z, final PrioritisedExecutor.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) {
+ 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) {
+ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z);
+ if (chunkHolder != null) {
+ chunkHolder.lowerPriority(priority);
+ }
+ }
+
+ private NewChunkHolder createChunkHolder(final long position) {
+ final NewChunkHolder ret = new NewChunkHolder(this.world, CoordinateUtils.getChunkX(position), CoordinateUtils.getChunkZ(position), this.taskScheduler);
+
+ ChunkSystem.onChunkHolderCreate(this.world, ret.vanillaChunkHolder);
+
+ return ret;
+ }
+
+ // because this function creates the chunk holder without a ticket, it is the caller's responsibility to ensure
+ // the chunk holder eventually unloads. this should only be used to avoid using processTicketUpdates to create chunkholders,
+ // as processTicketUpdates may call plugin logic; in every other case a ticket is appropriate
+ private NewChunkHolder getOrCreateChunkHolder(final int chunkX, final int chunkZ) {
+ return this.getOrCreateChunkHolder(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+
+ private NewChunkHolder getOrCreateChunkHolder(final long position) {
+ final int chunkX = CoordinateUtils.getChunkX(position);
+ final int chunkZ = CoordinateUtils.getChunkZ(position);
+
+ if (!this.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ)) {
+ throw new IllegalStateException("Must hold ticket level update lock!");
+ }
+ if (!this.taskScheduler.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ)) {
+ throw new IllegalStateException("Must hold scheduler lock!!");
+ }
+
+ // we could just acquire these locks, but...
+ // must own the locks because the caller needs to ensure that no unload can occur AFTER this function returns
+
+ NewChunkHolder current = this.chunkHolders.get(position);
+ if (current != null) {
+ return current;
+ }
+
+ current = this.createChunkHolder(position);
+ this.chunkHolders.put(position, current);
+
+
+ return current;
+ }
+
+ public ChunkEntitySlices getOrCreateEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
+ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create entity chunk off-main");
+ ChunkEntitySlices ret;
+
+ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ);
+ if (current != null && (ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) {
+ return ret;
+ }
+
+ final AtomicBoolean isCompleted = new AtomicBoolean();
+ final Thread waiter = Thread.currentThread();
+ final Long entityLoadId = ChunkTaskScheduler.getNextEntityLoadId();
+ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null;
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ);
+ try {
+ this.addTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
+ final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ);
+ try {
+ current = this.getOrCreateChunkHolder(chunkX, chunkZ);
+ if ((ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) {
+ this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
+ return ret;
+ }
+
+ if (!transientChunk) {
+ if (current.isEntityChunkNBTLoaded()) {
+ isCompleted.setPlain(true);
+ } else {
+ loadTask = current.getOrLoadEntityData((final GenericDataLoadTask.TaskResult<CompoundTag, Throwable> result) -> {
+ isCompleted.set(true);
+ LockSupport.unpark(waiter);
+ });
+ final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask();
+
+ if (entityLoad != null) {
+ entityLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
+ }
+ }
+ }
+ } finally {
+ this.taskScheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+
+ if (loadTask != null) {
+ loadTask.schedule();
+ }
+
+ if (!transientChunk) {
+ // Note: no need to busy wait on the chunk queue, entity load will complete off-main
+ boolean interrupted = false;
+ while (!isCompleted.get()) {
+ interrupted |= Thread.interrupted();
+ LockSupport.park();
+ }
+
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ // now that the entity data is loaded, we can load it into the world
+
+ ret = current.loadInEntityChunk(transientChunk);
+
+ this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
+
+ return ret;
+ }
+
+ public PoiChunk getPoiChunkIfLoaded(final int chunkX, final int chunkZ, final boolean checkLoadInCallback) {
+ final NewChunkHolder holder = this.getChunkHolder(chunkX, chunkZ);
+ if (holder != null) {
+ final PoiChunk ret = holder.getPoiChunk();
+ return ret == null || (checkLoadInCallback && !ret.isLoaded()) ? null : ret;
+ }
+ return null;
+ }
+
+ public PoiChunk loadPoiChunk(final int chunkX, final int chunkZ) {
+ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create poi chunk off-main");
+ PoiChunk ret;
+
+ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ);
+ if (current != null && (ret = current.getPoiChunk()) != null) {
+ ret.load();
+ return ret;
+ }
+
+ final AtomicReference<PoiChunk> completed = new AtomicReference<>();
+ final AtomicBoolean isCompleted = new AtomicBoolean();
+ final Thread waiter = Thread.currentThread();
+ final Long poiLoadId = ChunkTaskScheduler.getNextPoiLoadId();
+ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null;
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ);
+ try {
+ this.addTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId);
+ final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ);
+ try {
+ current = this.getOrCreateChunkHolder(chunkX, chunkZ);
+ if (null == (ret = current.getPoiChunk())) {
+ loadTask = current.getOrLoadPoiData((final GenericDataLoadTask.TaskResult<PoiChunk, Throwable> result) -> {
+ completed.setPlain(result.left());
+ isCompleted.set(true);
+ LockSupport.unpark(waiter);
+ });
+ final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask();
+
+ if (poiLoad != null) {
+ poiLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
+ }
+ }
+ } finally {
+ this.taskScheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+
+ if (loadTask != null) {
+ loadTask.schedule();
+
+ // Note: no need to busy wait on the chunk queue, poi load will complete off-main
+
+ boolean interrupted = false;
+ while (!isCompleted.get()) {
+ interrupted |= Thread.interrupted();
+ LockSupport.park();
+ }
+
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+
+ ret = completed.getPlain();
+ } // else: became loaded during the scheduling attempt, need to ensure load() is invoked
+
+ ret.load();
+
+ this.removeTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId);
+
+ return ret;
+ }
+
+ void addChangedStatuses(final List<NewChunkHolder> changedFullStatus) {
+ if (changedFullStatus.isEmpty()) {
+ return;
+ }
+ if (!TickThread.isTickThread()) {
+ this.taskScheduler.scheduleChunkTask(() -> {
+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate;
+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
+ pendingFullLoadUpdate.add(changedFullStatus.get(i));
+ }
+
+ ChunkHolderManager.this.processPendingFullUpdate();
+ }, PrioritisedExecutor.Priority.HIGHEST);
+ } else {
+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
+ pendingFullLoadUpdate.add(changedFullStatus.get(i));
+ }
+ }
+ }
+
+ private void removeChunkHolder(final NewChunkHolder holder) {
+ holder.markUnloaded();
+ 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
+ public void processUnloads() {
+ TickThread.ensureTickThread("Cannot unload chunks off-main");
+
+ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) {
+ throw new IllegalStateException("Cannot unload chunks recursively");
+ }
+ final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift
+ final List<ChunkUnloadQueue.SectionToUnload> unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions();
+ int unloadCountTentative = 0;
+ for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
+ final ChunkUnloadQueue.UnloadSection section
+ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ());
+
+ if (section == null) {
+ // removed concurrently
+ continue;
+ }
+
+ // technically reading the size field is unsafe, and it may be incorrect.
+ // We assume that the error here cumulatively goes away over many ticks. If it did not, then it is possible
+ // for chunks to never unload or not unload fast enough.
+ unloadCountTentative += section.chunks.size();
+ }
+
+ if (unloadCountTentative <= 0) {
+ // no work to do
+ return;
+ }
+
+ // We do need to process updates here so that any addTicket that is synchronised before this call does not go missed.
+ this.processTicketUpdates();
+
+ final int toUnloadCount = Math.max(50, (int)(unloadCountTentative * 0.05));
+ int processedCount = 0;
+
+ for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
+ final List<NewChunkHolder> stage1 = new ArrayList<>();
+ final List<NewChunkHolder.UnloadState> stage2 = new ArrayList<>();
+
+ final int sectionLowerX = sectionRef.sectionX() << sectionShift;
+ final int sectionLowerZ = sectionRef.sectionZ() << sectionShift;
+
+ // stage 1: set up for stage 2 while holding critical locks
+ ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ);
+ try {
+ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ);
+ try {
+ final ChunkUnloadQueue.UnloadSection section
+ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ());
+
+ if (section == null) {
+ // removed concurrently
+ continue;
+ }
+
+ // collect the holders to run stage 1 on
+ final int sectionCount = section.chunks.size();
+
+ if ((sectionCount + processedCount) <= toUnloadCount) {
+ // we can just drain the entire section
+
+ for (final LongIterator iterator = section.chunks.iterator(); iterator.hasNext();) {
+ final NewChunkHolder holder = this.chunkHolders.get(iterator.nextLong());
+ if (holder == null) {
+ throw new IllegalStateException();
+ }
+ stage1.add(holder);
+ }
+
+ // remove section
+ this.unloadQueue.removeSection(sectionRef.sectionX(), sectionRef.sectionZ());
+ } else {
+ // processedCount + len = toUnloadCount
+ // we cannot drain the entire section
+ for (int i = 0, len = toUnloadCount - processedCount; i < len; ++i) {
+ final NewChunkHolder holder = this.chunkHolders.get(section.chunks.removeFirstLong());
+ if (holder == null) {
+ throw new IllegalStateException();
+ }
+ stage1.add(holder);
+ }
+ }
+
+ // run stage 1
+ for (int i = 0, len = stage1.size(); i < len; ++i) {
+ final NewChunkHolder chunkHolder = stage1.get(i);
+ chunkHolder.removeFromUnloadQueue();
+ if (chunkHolder.isSafeToUnload() != null) {
+ LOGGER.error("Chunkholder " + chunkHolder + " is not safe to unload but is inside the unload queue?");
+ continue;
+ }
+ final NewChunkHolder.UnloadState state = chunkHolder.unloadStage1();
+ if (state == null) {
+ // can unload immediately
+ this.removeChunkHolder(chunkHolder);
+ continue;
+ }
+ stage2.add(state);
+ }
+ } finally {
+ this.taskScheduler.schedulingLockArea.unlock(scheduleLock);
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+
+ // stage 2: invoke expensive unload logic, designed to run without locks thanks to stage 1
+ final List<NewChunkHolder> stage3 = new ArrayList<>(stage2.size());
+
+ final Boolean before = this.blockTicketUpdates();
+ try {
+ for (int i = 0, len = stage2.size(); i < len; ++i) {
+ final NewChunkHolder.UnloadState state = stage2.get(i);
+ final NewChunkHolder holder = state.holder();
+
+ holder.unloadStage2(state);
+ stage3.add(holder);
+ }
+ } finally {
+ this.unblockTicketUpdates(before);
+ }
+
+ // stage 3: actually attempt to remove the chunk holders
+ ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ);
+ try {
+ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ);
+ try {
+ for (int i = 0, len = stage3.size(); i < len; ++i) {
+ final NewChunkHolder holder = stage3.get(i);
+
+ if (holder.unloadStage3()) {
+ this.removeChunkHolder(holder);
+ } else {
+ // add cooldown so the next unload check is not immediately next tick
+ this.addTicketAtLevel(UNLOAD_COOLDOWN, CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ), MAX_TICKET_LEVEL, Unit.INSTANCE, false);
+ }
+ }
+ } finally {
+ this.taskScheduler.schedulingLockArea.unlock(scheduleLock);
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+
+ processedCount += stage1.size();
+
+ if (processedCount >= toUnloadCount) {
+ break;
+ }
+ }
+ }
+
+ public enum TicketOperationType {
+ ADD, REMOVE, ADD_IF_REMOVED, ADD_AND_REMOVE
+ }
+
+ public static record TicketOperation<T, V> (
+ TicketOperationType op, long chunkCoord,
+ TicketType<T> ticketType, int ticketLevel, T identifier,
+ TicketType<V> ticketType2, int ticketLevel2, V identifier2
+ ) {
+
+ private TicketOperation(TicketOperationType op, long chunkCoord,
+ TicketType<T> ticketType, int ticketLevel, T identifier) {
+ this(op, chunkCoord, ticketType, ticketLevel, identifier, null, 0, null);
+ }
+
+ public static <T> TicketOperation<T, T> addOp(final ChunkPos chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return addOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier);
+ }
+
+ public static <T> TicketOperation<T, T> addOp(final int chunkX, final int chunkZ, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return addOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier);
+ }
+
+ public static <T> TicketOperation<T, T> addOp(final long chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return new TicketOperation<>(TicketOperationType.ADD, chunk, type, ticketLevel, identifier);
+ }
+
+ public static <T> TicketOperation<T, T> removeOp(final ChunkPos chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return removeOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier);
+ }
+
+ public static <T> TicketOperation<T, T> removeOp(final int chunkX, final int chunkZ, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return removeOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier);
+ }
+
+ public static <T> TicketOperation<T, T> removeOp(final long chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return new TicketOperation<>(TicketOperationType.REMOVE, chunk, type, ticketLevel, identifier);
+ }
+
+ public static <T, V> TicketOperation<T, V> addIfRemovedOp(final long chunk,
+ final TicketType<T> addType, final int addLevel, final T addIdentifier,
+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) {
+ return new TicketOperation<>(
+ TicketOperationType.ADD_IF_REMOVED, chunk, addType, addLevel, addIdentifier,
+ removeType, removeLevel, removeIdentifier
+ );
+ }
+
+ public static <T, V> TicketOperation<T, V> addAndRemove(final long chunk,
+ final TicketType<T> addType, final int addLevel, final T addIdentifier,
+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) {
+ return new TicketOperation<>(
+ TicketOperationType.ADD_AND_REMOVE, chunk, addType, addLevel, addIdentifier,
+ removeType, removeLevel, removeIdentifier
+ );
+ }
+ }
+
+ private <T, V> boolean processTicketOp(TicketOperation<T, V> operation) {
+ boolean ret = false;
+ switch (operation.op) {
+ case ADD: {
+ ret |= this.addTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier);
+ break;
+ }
+ case REMOVE: {
+ ret |= this.removeTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier);
+ break;
+ }
+ case ADD_IF_REMOVED: {
+ ret |= this.addIfRemovedTicket(
+ operation.chunkCoord,
+ operation.ticketType, operation.ticketLevel, operation.identifier,
+ operation.ticketType2, operation.ticketLevel2, operation.identifier2
+ );
+ break;
+ }
+ case ADD_AND_REMOVE: {
+ ret = true;
+ this.addAndRemoveTickets(
+ operation.chunkCoord,
+ operation.ticketType, operation.ticketLevel, operation.identifier,
+ operation.ticketType2, operation.ticketLevel2, operation.identifier2
+ );
+ break;
+ }
+ }
+
+ return ret;
+ }
+
+ public void performTicketUpdates(final Collection<TicketOperation<?, ?>> operations) {
+ for (final TicketOperation<?, ?> operation : operations) {
+ this.processTicketOp(operation);
+ }
+ }
+
+ private final ThreadLocal<Boolean> BLOCK_TICKET_UPDATES = ThreadLocal.withInitial(() -> {
+ return Boolean.FALSE;
+ });
+
+ public Boolean blockTicketUpdates() {
+ final Boolean ret = BLOCK_TICKET_UPDATES.get();
+ BLOCK_TICKET_UPDATES.set(Boolean.TRUE);
+ return ret;
+ }
+
+ public void unblockTicketUpdates(final Boolean before) {
+ BLOCK_TICKET_UPDATES.set(before);
+ }
+
+ public boolean processTicketUpdates() {
+ return this.processTicketUpdates(true, null);
+ }
+
+ private static final ThreadLocal<List<ChunkProgressionTask>> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>();
+
+ static List<ChunkProgressionTask> getCurrentTicketUpdateScheduling() {
+ return CURRENT_TICKET_UPDATE_SCHEDULING.get();
+ }
+
+ private boolean processTicketUpdates(final boolean processFullUpdates, List<ChunkProgressionTask> scheduledTasks) {
+ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) {
+ throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager");
+ }
+
+ List<NewChunkHolder> changedFullStatus = null;
+
+ final boolean isTickThread = TickThread.isTickThread();
+
+ boolean ret = false;
+ final boolean canProcessFullUpdates = processFullUpdates & isTickThread;
+ final boolean canProcessScheduling = scheduledTasks == null;
+
+ if (this.ticketLevelPropagator.hasPendingUpdates()) {
+ if (scheduledTasks == null) {
+ scheduledTasks = new ArrayList<>();
+ }
+ changedFullStatus = new ArrayList<>();
+
+ ret |= this.ticketLevelPropagator.performUpdates(
+ this.ticketLockArea, this.taskScheduler.schedulingLockArea,
+ scheduledTasks, changedFullStatus
+ );
+ }
+
+ if (changedFullStatus != null) {
+ this.addChangedStatuses(changedFullStatus);
+ }
+
+ if (canProcessScheduling && scheduledTasks != null) {
+ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) {
+ scheduledTasks.get(i).schedule();
+ }
+ }
+
+ if (canProcessFullUpdates) {
+ ret |= this.processPendingFullUpdate();
+ }
+
+ return ret;
+ }
+
+ // only call on tick thread
+ private boolean processPendingFullUpdate() {
+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
+
+ boolean ret = false;
+
+ final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
+
+ NewChunkHolder holder;
+ while ((holder = pendingFullLoadUpdate.poll()) != null) {
+ ret |= holder.handleFullStatusChange(changedFullStatus);
+
+ if (!changedFullStatus.isEmpty()) {
+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
+ pendingFullLoadUpdate.add(changedFullStatus.get(i));
+ }
+ changedFullStatus.clear();
+ }
+ }
+
+ return ret;
+ }
+
+ public JsonObject getDebugJson() {
+ final JsonObject ret = new JsonObject();
+
+ ret.add("unload_queue", this.unloadQueue.toDebugJson());
+
+ final JsonArray holders = new JsonArray();
+ ret.add("chunkholders", holders);
+
+ for (final NewChunkHolder holder : this.getChunkHolders()) {
+ holders.add(holder.getDebugJson());
+ }
+
+ final JsonArray allTicketsJson = new JsonArray();
+ ret.add("tickets", allTicketsJson);
+
+ for (final Iterator<ConcurrentLong2ReferenceChainedHashTable.TableEntry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.entryIterator();
+ iterator.hasNext();) {
+ final ConcurrentLong2ReferenceChainedHashTable.TableEntry<SortedArraySet<Ticket<?>>> coordinateTickets = iterator.next();
+ final long coordinate = coordinateTickets.getKey();
+ final SortedArraySet<Ticket<?>> tickets = coordinateTickets.getValue();
+
+ final JsonObject coordinateJson = new JsonObject();
+ allTicketsJson.add(coordinateJson);
+
+ coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate)));
+ coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate)));
+
+ final JsonArray ticketsSerialized = new JsonArray();
+ coordinateJson.add("tickets", ticketsSerialized);
+
+ // note: by using a copy of the backing array, we can avoid explicit exceptions we may trip when iterating
+ // directly over the set using the iterator
+ // however, it also means we need to null-check the values, and there is a possibility that we _miss_ an
+ // entry OR iterate over an entry multiple times
+ for (final Object ticketUncasted : ((ChunkSystemSortedArraySet<Ticket<?>>)tickets).moonrise$copyBackingArray()) {
+ if (ticketUncasted == null) {
+ continue;
+ }
+ final Ticket<?> ticket = (Ticket<?>)ticketUncasted;
+ final JsonObject ticketSerialized = new JsonObject();
+ ticketsSerialized.add(ticketSerialized);
+
+ ticketSerialized.addProperty("type", ticket.getType().toString());
+ ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel()));
+ ticketSerialized.addProperty("identifier", Objects.toString(ticket.key));
+ ticketSerialized.addProperty("remove_tick", Long.valueOf(((ChunkSystemTicket<?>)(Object)ticket).moonrise$getRemoveDelay()));
+ }
+ }
+
+ return ret;
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf01815f64c
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
@@ -0,0 +1,1041 @@
+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.lock.ReentrantAreaLock;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+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;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor.RadiusAwarePrioritisedExecutor;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkFullTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLightTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkUpgradeGenericStatusTask;
+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;
+import net.minecraft.CrashReportCategory;
+import net.minecraft.ReportedException;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ChunkLevel;
+import net.minecraft.server.level.ChunkMap;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.server.level.GenerationChunkHolder;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.level.TicketType;
+import net.minecraft.util.StaticCache2D;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.status.ChunkPyramid;
+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 java.io.File;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+
+public final class ChunkTaskScheduler {
+
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+
+ 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]");
+ }
+
+ 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");
+ }
+
+ public static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_system:chunk_load", Long::compareTo);
+ private static final AtomicLong CHUNK_LOAD_IDS = new AtomicLong();
+
+ public static Long getNextChunkLoadId() {
+ return Long.valueOf(CHUNK_LOAD_IDS.getAndIncrement());
+ }
+
+ public static final TicketType<Long> NON_FULL_CHUNK_LOAD = TicketType.create("chunk_system:non_full_load", Long::compareTo);
+ private static final AtomicLong NON_FULL_CHUNK_LOAD_IDS = new AtomicLong();
+
+ public static Long getNextNonFullLoadId() {
+ return Long.valueOf(NON_FULL_CHUNK_LOAD_IDS.getAndIncrement());
+ }
+
+ public static final TicketType<Long> ENTITY_LOAD = TicketType.create("chunk_system:entity_load", Long::compareTo);
+ private static final AtomicLong ENTITY_LOAD_IDS = new AtomicLong();
+
+ public static Long getNextEntityLoadId() {
+ return Long.valueOf(ENTITY_LOAD_IDS.getAndIncrement());
+ }
+
+ public static final TicketType<Long> POI_LOAD = TicketType.create("chunk_system:poi_load", Long::compareTo);
+ private static final AtomicLong POI_LOAD_IDS = new AtomicLong();
+
+ public static Long getNextPoiLoadId() {
+ return Long.valueOf(POI_LOAD_IDS.getAndIncrement());
+ }
+
+ public static final TicketType<Long> CHUNK_RELIGHT = TicketType.create("starlight:chunk_relight", Long::compareTo);
+ private static final AtomicLong CHUNK_RELIGHT_IDS = new AtomicLong();
+
+ public static Long getNextChunkRelightId() {
+ return Long.valueOf(CHUNK_RELIGHT_IDS.getAndIncrement());
+ }
+
+
+ public static int getTicketLevel(final ChunkStatus status) {
+ return ChunkLevel.byStatus(status);
+ }
+
+ 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;
+
+ private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue();
+
+ public final ChunkHolderManager chunkHolderManager;
+
+ static {
+ ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_STARTS).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setWriteRadius(1);
+ ((ChunkSystemChunkStatus)ChunkStatus.INITIALIZE_LIGHT).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$setWriteRadius(2);
+ ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.FULL).moonrise$setWriteRadius(0);
+
+ ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setEmptyLoadStatus(true);
+ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setEmptyLoadStatus(true);
+ ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setEmptyLoadStatus(true);
+ ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setEmptyLoadStatus(true);
+ ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setEmptyLoadStatus(true);
+ ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setEmptyLoadStatus(true);
+ ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setEmptyLoadStatus(true);
+ ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setEmptyLoadStatus(true);
+
+ /*
+ It's important that the neighbour read radius is taken into account. If _any_ later status is using some chunk as
+ a neighbour, it must be also safe if that neighbour is being generated. i.e for any status later than FEATURES,
+ for a status to be parallel safe it must not read the block data from its neighbours.
+ */
+ final List<ChunkStatus> parallelCapableStatus = Arrays.asList(
+ // No-op executor.
+ ChunkStatus.EMPTY,
+
+ // This is parallel capable, as CB has fixed the concurrency issue with stronghold generations.
+ // Does not touch neighbour chunks.
+ ChunkStatus.STRUCTURE_STARTS,
+
+ // Surprisingly this is parallel capable. It is simply reading the already-created structure starts
+ // into the structure references for the chunk. So while it reads from it neighbours, its neighbours
+ // will not change, even if executed in parallel.
+ ChunkStatus.STRUCTURE_REFERENCES,
+
+ // Safe. Mojang runs it in parallel as well.
+ ChunkStatus.BIOMES,
+
+ // Safe. Mojang runs it in parallel as well.
+ ChunkStatus.NOISE,
+
+ // Parallel safe. Only touches the target chunk. Biome retrieval is now noise based, which is
+ // completely thread-safe.
+ ChunkStatus.SURFACE,
+
+ // No global state is modified in the carvers. It only touches the specified chunk. So it is parallel safe.
+ ChunkStatus.CARVERS,
+
+ // FEATURES is not parallel safe. It writes to neighbours.
+
+ // no-op executor
+ ChunkStatus.INITIALIZE_LIGHT
+
+ // LIGHT is not parallel safe. It also doesn't run on the generation executor, so no point.
+
+ // Only writes to the specified chunk. State is not read by later statuses. Parallel safe.
+ // Note: it may look unsafe because it writes to a worldgenregion, but the region size is always 0 -
+ // see the task margin.
+ // However, if the neighbouring FEATURES chunk is unloaded, but then fails to load in again (for whatever
+ // reason), then it would write to this chunk - and since this status reads blocks from itself, it's not
+ // safe to execute this in parallel.
+ // SPAWN
+
+ // FULL is executed on main.
+ );
+
+ for (final ChunkStatus status : parallelCapableStatus) {
+ ((ChunkSystemChunkStatus)status).moonrise$setParallelCapable(true);
+ }
+ }
+
+ private static final int[] ACCESS_RADIUS_TABLE_LOAD = new int[ChunkStatus.getStatusList().size()];
+ private static final int[] ACCESS_RADIUS_TABLE_GEN = new int[ChunkStatus.getStatusList().size()];
+ private static final int[] ACCESS_RADIUS_TABLE = new int[ChunkStatus.getStatusList().size()];
+ static {
+ Arrays.fill(ACCESS_RADIUS_TABLE_LOAD, -1);
+ Arrays.fill(ACCESS_RADIUS_TABLE_GEN, -1);
+ Arrays.fill(ACCESS_RADIUS_TABLE, -1);
+ }
+
+ private static int getAccessRadius0(final ChunkStatus toStatus, final ChunkPyramid pyramid) {
+ if (toStatus == ChunkStatus.EMPTY) {
+ return 0;
+ }
+
+ final ChunkStep chunkStep = pyramid.getStepTo(toStatus);
+
+ final int radius = chunkStep.getAccumulatedRadiusOf(ChunkStatus.EMPTY);
+ int maxRange = radius;
+
+ for (int dist = 0; dist <= radius; ++dist) {
+ final ChunkStatus requiredNeighbourStatus = ((ChunkSystemChunkStep)(Object)chunkStep).moonrise$getRequiredStatusAtRadius(dist);
+ final int rad = ACCESS_RADIUS_TABLE[requiredNeighbourStatus.getIndex()];
+ if (rad == -1) {
+ throw new IllegalStateException();
+ }
+
+ maxRange = Math.max(maxRange, dist + rad);
+ }
+
+ return maxRange;
+ }
+
+ private static final int MAX_ACCESS_RADIUS;
+
+ static {
+ final List<ChunkStatus> statuses = ChunkStatus.getStatusList();
+ for (int i = 0, len = statuses.size(); i < len; ++i) {
+ final ChunkStatus status = statuses.get(i);
+ ACCESS_RADIUS_TABLE_LOAD[i] = getAccessRadius0(status, ChunkPyramid.LOADING_PYRAMID);
+ ACCESS_RADIUS_TABLE_GEN[i] = getAccessRadius0(status, ChunkPyramid.GENERATION_PYRAMID);
+ ACCESS_RADIUS_TABLE[i] = Math.max(
+ ACCESS_RADIUS_TABLE_LOAD[i],
+ ACCESS_RADIUS_TABLE_GEN[i]
+ );
+ }
+ MAX_ACCESS_RADIUS = ACCESS_RADIUS_TABLE[ACCESS_RADIUS_TABLE.length - 1];
+ }
+
+ public static int getMaxAccessRadius() {
+ return MAX_ACCESS_RADIUS;
+ }
+
+ public static int getAccessRadius(final ChunkStatus genStatus) {
+ return ACCESS_RADIUS_TABLE[genStatus.getIndex()];
+ }
+
+ public static int getAccessRadius(final FullChunkStatus status) {
+ return (status.ordinal() - 1) + getAccessRadius(ChunkStatus.FULL);
+ }
+
+
+ public final ReentrantAreaLock schedulingLockArea;
+ private final int lockShift;
+
+ public final int getChunkSystemLockShift() {
+ return this.lockShift;
+ }
+
+ public ChunkTaskScheduler(final ServerLevel world, final PrioritisedThreadPool workers) {
+ 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
+ // the entire section
+ // we just take the max, as we want the smallest shift that satisfies these properties
+ 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.chunkHolderManager = new ChunkHolderManager(world, this);
+ }
+
+ private final AtomicBoolean failedChunkSystem = new AtomicBoolean();
+
+ public static Object stringIfNull(final Object obj) {
+ return obj == null ? "null" : obj;
+ }
+
+ public void unrecoverableChunkSystemFailure(final int chunkX, final int chunkZ, final Map<String, Object> objectsOfInterest, final Throwable thr) {
+ final NewChunkHolder holder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ);
+ LOGGER.error("Chunk system error at chunk (" + chunkX + "," + chunkZ + "), holder: " + holder + ", exception:", new Throwable(thr));
+
+ if (this.failedChunkSystem.getAndSet(true)) {
+ return;
+ }
+
+ final ReportedException reportedException = thr instanceof ReportedException ? (ReportedException)thr : new ReportedException(new CrashReport("Chunk system error", thr));
+
+ CrashReportCategory crashReportCategory = reportedException.getReport().addCategory("Chunk system details");
+ crashReportCategory.setDetail("Chunk coordinate", new ChunkPos(chunkX, chunkZ).toString());
+ crashReportCategory.setDetail("ChunkHolder", Objects.toString(holder));
+ crashReportCategory.setDetail("unrecoverableChunkSystemFailure caller thread", Thread.currentThread().getName());
+
+ crashReportCategory = reportedException.getReport().addCategory("Chunk System Objects of Interest");
+ for (final Map.Entry<String, Object> entry : objectsOfInterest.entrySet()) {
+ if (entry.getValue() instanceof Throwable thrObject) {
+ crashReportCategory.setDetailError(Objects.toString(entry.getKey()), thrObject);
+ } else {
+ crashReportCategory.setDetail(Objects.toString(entry.getKey()), Objects.toString(entry.getValue()));
+ }
+ }
+
+ final Runnable crash = () -> {
+ throw new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException);
+ };
+
+ // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions
+ this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.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));
+ }
+
+ public boolean executeMainThreadTask() {
+ TickThread.ensureTickThread("Cannot execute main thread task off-main");
+ return this.mainThreadExecutor.executeTask();
+ }
+
+ public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
+ this.chunkHolderManager.raisePriority(x, z, priority);
+ }
+
+ public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
+ this.chunkHolderManager.setPriority(x, z, priority);
+ }
+
+ public void lowerPriority(final int x, final int z, final PrioritisedExecutor.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 Consumer<LevelChunk> onComplete) {
+ final int radius = toStatus.ordinal() - 1; // 0 -> BORDER, 1 -> TICKING, 2 -> ENTITY_TICKING
+
+ if (!TickThread.isTickThreadFor(this.world, chunkX, chunkZ, Math.max(0, radius))) {
+ this.scheduleChunkTask(chunkX, chunkZ, () -> {
+ ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ }, priority);
+ return;
+ }
+ final int accessRadius = getAccessRadius(toStatus);
+ if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) {
+ throw new IllegalStateException("Cannot schedule chunk load during ticket level update");
+ }
+ if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) {
+ throw new IllegalStateException("Cannot schedule chunk loading recursively");
+ }
+
+ if (toStatus == FullChunkStatus.INACCESSIBLE) {
+ throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
+ }
+
+ final int minLevel = 33 - (toStatus.ordinal() - 1);
+ final Long chunkReference = addTicket ? getNextChunkLoadId() : null;
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ if (addTicket) {
+ this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
+ this.chunkHolderManager.processTicketUpdates();
+ }
+
+ final Consumer<LevelChunk> loadCallback = onComplete == null && !addTicket ? null : (final LevelChunk chunk) -> {
+ try {
+ if (onComplete != null) {
+ onComplete.accept(chunk);
+ }
+ } finally {
+ if (addTicket) {
+ ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
+ }
+ }
+ };
+
+ final boolean scheduled;
+ final LevelChunk chunk;
+ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius);
+ try {
+ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius);
+ try {
+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey);
+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) {
+ scheduled = false;
+ chunk = null;
+ } else {
+ final FullChunkStatus currStatus = chunkHolder.getChunkStatus();
+ if (currStatus.isOrAfter(toStatus)) {
+ scheduled = false;
+ chunk = (LevelChunk)chunkHolder.getCurrentChunk();
+ } else {
+ scheduled = true;
+ chunk = null;
+
+ for (int dz = -radius; dz <= radius; ++dz) {
+ for (int dx = -radius; dx <= radius; ++dx) {
+ final NewChunkHolder neighbour =
+ (dx | dz) == 0 ? chunkHolder : this.chunkHolderManager.getChunkHolder(dx + chunkX, dz + chunkZ);
+ if (neighbour != null) {
+ neighbour.raisePriority(priority);
+ }
+ }
+ }
+
+ // ticket level should schedule for us
+ if (loadCallback != null) {
+ chunkHolder.addFullStatusConsumer(toStatus, loadCallback);
+ }
+ }
+ }
+ } finally {
+ this.schedulingLockArea.unlock(schedulingLock);
+ }
+ } finally {
+ this.chunkHolderManager.ticketLockArea.unlock(ticketLock);
+ }
+
+ if (loadCallback != null && !scheduled) {
+ // couldn't schedule
+ try {
+ loadCallback.accept(chunk);
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to process chunk full status callback", thr);
+ }
+ }
+ }
+
+ 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) {
+ if (gen) {
+ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ return;
+ }
+ this.scheduleChunkLoad(chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
+ if (chunk == null) {
+ if (onComplete != null) {
+ onComplete.accept(null);
+ }
+ } else {
+ if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
+ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ } else {
+ if (onComplete != null) {
+ onComplete.accept(null);
+ }
+ }
+ }
+ });
+ }
+
+ // only appropriate to use with syncLoadNonFull
+ public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus,
+ final PrioritisedExecutor.Priority priority) {
+ final int accessRadius = getAccessRadius(toStatus);
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus);
+ final List<ChunkProgressionTask> tasks = new ArrayList<>();
+ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention
+ try {
+ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention
+ try {
+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey);
+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) {
+ return false;
+ } else {
+ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus();
+ if (genStatus != null && genStatus.isOrAfter(toStatus)) {
+ return true;
+ } else {
+ chunkHolder.raisePriority(priority);
+
+ if (!chunkHolder.upgradeGenTarget(toStatus)) {
+ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks);
+ }
+ }
+ }
+ } finally {
+ this.schedulingLockArea.unlock(schedulingLock);
+ }
+ } finally {
+ this.chunkHolderManager.ticketLockArea.unlock(ticketLock);
+ }
+
+ for (int i = 0, len = tasks.size(); i < len; ++i) {
+ tasks.get(i).schedule();
+ }
+
+ return true;
+ }
+
+ // Note: on Moonrise the non-full sync load requires blocking on managedBlock, but this is fine since there is only
+ // one main thread. On Folia, it is required that the non-full load can occur completely asynchronously to avoid deadlock
+ // between regions
+ public ChunkAccess syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status) {
+ if (status == null || status.isOrAfter(ChunkStatus.FULL)) {
+ throw new IllegalArgumentException("Status: " + status);
+ }
+ ChunkAccess loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status);
+ if (loaded != null) {
+ return loaded;
+ }
+
+ final Long ticketId = getNextNonFullLoadId();
+ final int ticketLevel = getTicketLevel(status);
+ this.chunkHolderManager.addTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId);
+ this.chunkHolderManager.processTicketUpdates();
+
+ this.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, PrioritisedExecutor.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
+ this.world.getChunkSource().mainThreadProcessor.managedBlock(() -> {
+ return ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status) != null;
+ });
+
+ loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status);
+
+ this.chunkHolderManager.removeTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId);
+
+ if (loaded == null) {
+ throw new IllegalStateException("Expected chunk to be loaded for status " + status);
+ }
+
+ return loaded;
+ }
+
+ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket,
+ final PrioritisedExecutor.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);
+ }, priority);
+ return;
+ }
+ final int accessRadius = getAccessRadius(toStatus);
+ if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) {
+ throw new IllegalStateException("Cannot schedule chunk load during ticket level update");
+ }
+ if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) {
+ throw new IllegalStateException("Cannot schedule chunk loading recursively");
+ }
+
+ if (toStatus == ChunkStatus.FULL) {
+ this.scheduleTickingState(chunkX, chunkZ, FullChunkStatus.FULL, addTicket, priority, (Consumer)onComplete);
+ return;
+ }
+
+ final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus);
+ final Long chunkReference = addTicket ? getNextChunkLoadId() : null;
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ if (addTicket) {
+ this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
+ this.chunkHolderManager.processTicketUpdates();
+ }
+
+ final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> {
+ try {
+ if (onComplete != null) {
+ onComplete.accept(chunk);
+ }
+ } finally {
+ if (addTicket) {
+ ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
+ }
+ }
+ };
+
+ final List<ChunkProgressionTask> tasks = new ArrayList<>();
+
+ final boolean scheduled;
+ final ChunkAccess chunk;
+ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius);
+ try {
+ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius);
+ try {
+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey);
+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) {
+ scheduled = false;
+ chunk = null;
+ } else {
+ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus();
+ if (genStatus != null && genStatus.isOrAfter(toStatus)) {
+ scheduled = false;
+ chunk = chunkHolder.getCurrentChunk();
+ } else {
+ scheduled = true;
+ chunk = null;
+ chunkHolder.raisePriority(priority);
+
+ if (!chunkHolder.upgradeGenTarget(toStatus)) {
+ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks);
+ }
+ chunkHolder.addStatusConsumer(toStatus, loadCallback);
+ }
+ }
+ } finally {
+ this.schedulingLockArea.unlock(schedulingLock);
+ }
+ } finally {
+ this.chunkHolderManager.ticketLockArea.unlock(ticketLock);
+ }
+
+ for (int i = 0, len = tasks.size(); i < len; ++i) {
+ tasks.get(i).schedule();
+ }
+
+ if (!scheduled) {
+ // couldn't schedule
+ try {
+ loadCallback.accept(chunk);
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to process chunk status callback", thr);
+ }
+ }
+ }
+
+ 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) {
+ if (toStatus == ChunkStatus.EMPTY) {
+ return new ChunkLoadTask(this, this.world, chunkX, chunkZ, chunkHolder, initialPriority);
+ }
+ if (toStatus == ChunkStatus.LIGHT) {
+ return new ChunkLightTask(this, this.world, chunkX, chunkZ, chunk, initialPriority);
+ }
+ if (toStatus == ChunkStatus.FULL) {
+ return new ChunkFullTask(this, this.world, chunkX, chunkZ, chunkHolder, chunk, initialPriority);
+ }
+
+ return new ChunkUpgradeGenericStatusTask(this, this.world, chunkX, chunkZ, chunk, neighbours, toStatus, initialPriority);
+ }
+
+ 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));
+ }
+
+ // rets new task scheduled for the _specified_ chunk
+ // note: this must hold the scheduling lock
+ // minPriority is only used to pass the priority through to neighbours, as priority calculation has not yet been done
+ // 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) {
+ if (!this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, getAccessRadius(targetStatus))) {
+ throw new IllegalStateException("Not holding scheduling lock");
+ }
+
+ if (chunkHolder.hasGenerationTask()) {
+ chunkHolder.upgradeGenTarget(targetStatus);
+ return null;
+ }
+
+ final PrioritisedExecutor.Priority requestedPriority = PrioritisedExecutor.Priority.max(
+ minPriority, chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
+ );
+ final ChunkStatus currentGenStatus = chunkHolder.getCurrentGenStatus();
+ final ChunkAccess chunk = chunkHolder.getCurrentChunk();
+
+ if (currentGenStatus == null) {
+ // not yet loaded
+ final ChunkProgressionTask task = this.createTask(
+ chunkX, chunkZ, chunk, chunkHolder, null, ChunkStatus.EMPTY, requestedPriority
+ );
+
+ allTasks.add(task);
+
+ final List<NewChunkHolder> chunkHolderNeighbours = new ArrayList<>(1);
+ chunkHolderNeighbours.add(chunkHolder);
+
+ chunkHolder.setGenerationTarget(targetStatus);
+ chunkHolder.setGenerationTask(task, ChunkStatus.EMPTY, chunkHolderNeighbours);
+
+ return task;
+ }
+
+ if (currentGenStatus.isOrAfter(targetStatus)) {
+ // nothing to do
+ return null;
+ }
+
+ // we know for sure now that we want to schedule _something_, so set the target
+ chunkHolder.setGenerationTarget(targetStatus);
+
+ final ChunkStatus chunkRealStatus = chunk.getPersistedStatus();
+ final ChunkStatus toStatus = ((ChunkSystemChunkStatus)currentGenStatus).moonrise$getNextStatus();
+ final ChunkPyramid chunkPyramid = chunkRealStatus.isOrAfter(toStatus) ? ChunkPyramid.LOADING_PYRAMID : ChunkPyramid.GENERATION_PYRAMID;
+ final ChunkStep chunkStep = chunkPyramid.getStepTo(toStatus);
+
+ final int neighbourReadRadius = Math.max(
+ 0,
+ chunkPyramid.getStepTo(toStatus).getAccumulatedRadiusOf(ChunkStatus.EMPTY)
+ );
+
+ boolean unGeneratedNeighbours = false;
+
+ if (neighbourReadRadius > 0) {
+ final ChunkMap chunkMap = this.world.getChunkSource().chunkMap;
+ for (final long pos : ParallelSearchRadiusIteration.getSearchIteration(neighbourReadRadius)) {
+ final int x = CoordinateUtils.getChunkX(pos);
+ final int z = CoordinateUtils.getChunkZ(pos);
+ final int radius = Math.max(Math.abs(x), Math.abs(z));
+ final ChunkStatus requiredNeighbourStatus = ((ChunkSystemChunkStep)(Object)chunkStep).moonrise$getRequiredStatusAtRadius(radius);
+
+ unGeneratedNeighbours |= this.checkNeighbour(
+ chunkX + x, chunkZ + z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority
+ );
+ }
+ }
+
+ if (unGeneratedNeighbours) {
+ // can't schedule, but neighbour completion will schedule for us when they're ALL done
+
+ // propagate our priority to neighbours
+ chunkHolder.recalculateNeighbourPriorities();
+ return null;
+ }
+
+ // need to gather neighbours
+
+ final List<NewChunkHolder> chunkHolderNeighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1));
+ final StaticCache2D<GenerationChunkHolder> neighbours = StaticCache2D
+ .create(chunkX, chunkZ, neighbourReadRadius, (final int nx, final int nz) -> {
+ final NewChunkHolder holder = nx == chunkX && nz == chunkZ ? chunkHolder : this.chunkHolderManager.getChunkHolder(nx, nz);
+ chunkHolderNeighbours.add(holder);
+
+ return holder.vanillaChunkHolder;
+ });
+
+ final ChunkProgressionTask task = this.createTask(
+ chunkX, chunkZ, chunk, chunkHolder, neighbours, toStatus,
+ chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
+ );
+ allTasks.add(task);
+
+ chunkHolder.setGenerationTask(task, toStatus, chunkHolderNeighbours);
+
+ return task;
+ }
+
+ // 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 NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ);
+
+ if (chunkHolder == null) {
+ throw new IllegalStateException("Missing chunkholder when required");
+ }
+
+ final ChunkStatus holderStatus = chunkHolder.getCurrentGenStatus();
+ if (holderStatus != null && holderStatus.isOrAfter(requiredStatus)) {
+ return false;
+ }
+
+ if (chunkHolder.hasFailedGeneration()) {
+ return true;
+ }
+
+ center.addGenerationBlockingNeighbour(chunkHolder);
+ chunkHolder.addWaitingNeighbour(center, requiredStatus);
+
+ if (chunkHolder.upgradeGenTarget(requiredStatus)) {
+ return true;
+ }
+
+ // not at status required, so we need to schedule its generation
+ this.schedule(
+ chunkX, chunkZ, requiredStatus, chunkHolder, tasks, minPriority
+ );
+
+ return true;
+ }
+
+ /**
+ * @deprecated Chunk tasks must be tied to coordinates in the future
+ */
+ @Deprecated
+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) {
+ return this.scheduleChunkTask(run, PrioritisedExecutor.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 createChunkTask(final int chunkX, final int chunkZ, final Runnable run) {
+ return this.createChunkTask(chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run,
+ final PrioritisedExecutor.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);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run,
+ final PrioritisedExecutor.Priority priority) {
+ return this.mainThreadExecutor.queueRunnable(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) {
+ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) {
+ if (
+ !this.radiusAwareGenExecutor.isActive() &&
+ !this.parallelGenExecutor.isActive() &&
+ !this.loadExecutor.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 {
+
+ public final int chunkX;
+ public final int chunkZ;
+ public final ServerLevel world;
+
+ public ChunkInfo(final int chunkX, final int chunkZ, final ServerLevel world) {
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.world = world;
+ }
+
+ public JsonObject toJson() {
+ final JsonObject ret = new JsonObject();
+
+ ret.addProperty("chunk-x", Integer.valueOf(this.chunkX));
+ ret.addProperty("chunk-z", Integer.valueOf(this.chunkZ));
+ ret.addProperty("world-name", WorldUtil.getWorldName(this.world));
+
+ return ret;
+ }
+
+ @Override
+ public String toString() {
+ return "[( " + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "']";
+ }
+ }
+
+ public static void pushChunkWait(final ServerLevel world, final int chunkX, final int chunkZ) {
+ synchronized (WAITING_CHUNKS) {
+ WAITING_CHUNKS.push(new ChunkInfo(chunkX, chunkZ, world));
+ }
+ }
+
+ public static void popChunkWait() {
+ synchronized (WAITING_CHUNKS) {
+ WAITING_CHUNKS.pop();
+ }
+ }
+
+ public static ChunkInfo[] getChunkInfos() {
+ synchronized (WAITING_CHUNKS) {
+ return WAITING_CHUNKS.toArray(new ChunkInfo[0]);
+ }
+ }
+
+ private static JsonObject debugPlayer(final ServerPlayer player) {
+ final Level world = player.level();
+
+ final JsonObject ret = new JsonObject();
+
+ ret.addProperty("name", player.getScoreboardName());
+ ret.addProperty("uuid", player.getUUID().toString());
+ ret.addProperty("real", ((ChunkSystemServerPlayer)player).moonrise$isRealPlayer());
+
+ ret.addProperty("world-name", WorldUtil.getWorldName(world));
+
+ final Vec3 pos = player.position();
+
+ ret.addProperty("x", pos.x);
+ ret.addProperty("y", pos.y);
+ ret.addProperty("z", pos.z);
+
+ final Entity.RemovalReason removalReason = player.getRemovalReason();
+
+ ret.addProperty("removal-reason", removalReason == null ? "null" : removalReason.name());
+
+ ret.add("view-distances", ((ChunkSystemServerPlayer)player).moonrise$getViewDistanceHolder().toJson());
+
+ return ret;
+ }
+
+ public JsonObject getDebugJson() {
+ final JsonObject ret = new JsonObject();
+
+ ret.addProperty("lock_shift", Integer.valueOf(this.getChunkSystemLockShift()));
+ ret.addProperty("ticket_shift", Integer.valueOf(ThreadedTicketLevelPropagator.SECTION_SHIFT));
+ ret.addProperty("region_shift", Integer.valueOf(((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift()));
+
+ ret.addProperty("name", WorldUtil.getWorldName(this.world));
+ ret.addProperty("view-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPIViewDistance());
+ ret.addProperty("tick-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPITickDistance());
+ ret.addProperty("send-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPISendViewDistance());
+
+ final JsonArray players = new JsonArray();
+ ret.add("players", players);
+
+ for (final ServerPlayer player : this.world.players()) {
+ players.add(debugPlayer(player));
+ }
+
+ ret.add("chunk-holder-manager", this.chunkHolderManager.getDebugJson());
+
+ return ret;
+ }
+
+ public static JsonObject debugAllWorlds(final MinecraftServer server) {
+ final JsonObject ret = new JsonObject();
+
+ ret.addProperty("data-version", 2);
+
+ final JsonArray allPlayers = new JsonArray();
+ ret.add("all-players", allPlayers);
+
+ for (final ServerPlayer player : server.getPlayerList().getPlayers()) {
+ allPlayers.add(debugPlayer(player));
+ }
+
+ final JsonArray chunkWaitInfos = new JsonArray();
+ ret.add("chunk-wait-infos", chunkWaitInfos);
+
+ for (final ChunkTaskScheduler.ChunkInfo info : getChunkInfos()) {
+ chunkWaitInfos.add(info.toJson());
+ }
+
+ final JsonArray worlds = new JsonArray();
+ ret.add("worlds", worlds);
+
+ for (final ServerLevel world : server.getAllLevels()) {
+ worlds.add(((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().getDebugJson());
+ }
+
+ return ret;
+ }
+
+ public static File getChunkDebugFile() {
+ return new File(
+ new File(new File("."), "debug"),
+ "chunks-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt"
+ );
+ }
+
+ public static void dumpAllChunkLoadInfo(final MinecraftServer server, final boolean writeDebugInfo) {
+ final ChunkInfo[] chunkInfos = getChunkInfos();
+ if (chunkInfos.length > 0) {
+ LOGGER.error("Chunk wait task info below: ");
+ for (final ChunkInfo chunkInfo : chunkInfos) {
+ final NewChunkHolder holder = ((ChunkSystemServerLevel)chunkInfo.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkInfo.chunkX, chunkInfo.chunkZ);
+ LOGGER.error("Chunk wait: " + chunkInfo);
+ LOGGER.error("Chunk holder: " + holder);
+ }
+
+ if (writeDebugInfo) {
+ final File file = getChunkDebugFile();
+ LOGGER.error("Writing chunk information dump to " + file);
+ try {
+ JsonUtil.writeJson(ChunkTaskScheduler.debugAllWorlds(server), file);
+ LOGGER.error("Successfully written chunk information!");
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to dump chunk information to file " + file.toString(), thr);
+ }
+ }
+ }
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7e451416f
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
@@ -0,0 +1,2035 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
+
+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.lock.ReentrantAreaLock;
+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.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.ChunkSystemServerLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.GenericDataLoadTask;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
+import net.minecraft.nbt.CompoundTag;
+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.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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+public final class NewChunkHolder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(NewChunkHolder.class);
+
+ public final ServerLevel world;
+ public final int chunkX;
+ public final int chunkZ;
+
+ public final ChunkTaskScheduler scheduler;
+
+ // load/unload state
+
+ // chunk data state
+
+ private ChunkEntitySlices entityChunk;
+ // entity chunk that is loaded, but not yet deserialized
+ private CompoundTag pendingEntityChunk;
+
+ ChunkEntitySlices loadInEntityChunk(final boolean transientChunk) {
+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot sync load entity data off-main");
+ final CompoundTag entityChunk;
+ final ChunkEntitySlices ret;
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ if (this.entityChunk != null && (transientChunk || !this.entityChunk.isTransient())) {
+ return this.entityChunk;
+ }
+ final CompoundTag pendingEntityChunk = this.pendingEntityChunk;
+ if (!transientChunk && pendingEntityChunk == null) {
+ throw new IllegalStateException("Must load entity data from disk before loading in the entity chunk!");
+ }
+
+ 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)
+ );
+
+ ret.setTransient(transientChunk);
+
+ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().entitySectionLoad(this.chunkX, this.chunkZ, ret);
+ } else {
+ // transientChunk = false here
+ ret = this.entityChunk;
+ this.entityChunk.setTransient(false);
+ }
+
+ if (!transientChunk) {
+ this.pendingEntityChunk = null;
+ entityChunk = pendingEntityChunk == EMPTY_ENTITY_CHUNK ? null : pendingEntityChunk;
+ } else {
+ entityChunk = null;
+ }
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+
+ if (!transientChunk) {
+ if (entityChunk != null) {
+ final List<Entity> entities = ChunkEntitySlices.readEntities(this.world, entityChunk);
+
+ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().addEntityChunkEntities(entities, new ChunkPos(this.chunkX, this.chunkZ));
+ }
+ }
+
+ return ret;
+ }
+
+ // needed to distinguish whether the entity chunk has been read from disk but is empty or whether it has _not_
+ // been read from disk
+ private static final CompoundTag EMPTY_ENTITY_CHUNK = new CompoundTag();
+
+ private ChunkLoadTask.EntityDataLoadTask entityDataLoadTask;
+ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0,
+ // then the task is rescheduled
+ private List<GenericDataLoadTaskCallback> entityDataLoadTaskWaiters;
+
+ public ChunkLoadTask.EntityDataLoadTask getEntityDataLoadTask() {
+ return this.entityDataLoadTask;
+ }
+
+ // must hold schedule lock for the two below functions
+
+ // returns only if the data has been loaded from disk, DOES NOT relate to whether it has been deserialized
+ // or added into the world (or even into entityChunk)
+ public boolean isEntityChunkNBTLoaded() {
+ return (this.entityChunk != null && !this.entityChunk.isTransient()) || this.pendingEntityChunk != null;
+ }
+
+ private void completeEntityLoad(final GenericDataLoadTask.TaskResult<CompoundTag, Throwable> result) {
+ final List<GenericDataLoadTaskCallback> completeWaiters;
+ ChunkLoadTask.EntityDataLoadTask entityDataLoadTask = null;
+ boolean scheduleEntityTask = false;
+ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ final List<GenericDataLoadTaskCallback> waiters = this.entityDataLoadTaskWaiters;
+ this.entityDataLoadTask = null;
+ if (result != null) {
+ this.entityDataLoadTaskWaiters = null;
+ this.pendingEntityChunk = result.left() == null ? EMPTY_ENTITY_CHUNK : result.left();
+ if (result.right() != null) {
+ LOGGER.error("Unhandled entity data load exception, data data will be lost: ", result.right());
+ }
+
+ for (final GenericDataLoadTaskCallback callback : waiters) {
+ callback.markCompleted();
+ }
+
+ completeWaiters = waiters;
+ } else {
+ // cancelled
+ completeWaiters = null;
+
+ // need to re-schedule?
+ if (waiters.isEmpty()) {
+ this.entityDataLoadTaskWaiters = null;
+ // 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)
+ );
+ entityDataLoadTask.addCallback(this::completeEntityLoad);
+ // need one schedule() per waiter
+ for (final GenericDataLoadTaskCallback callback : waiters) {
+ scheduleEntityTask |= entityDataLoadTask.schedule(true);
+ }
+ }
+ }
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+
+ if (scheduleEntityTask) {
+ entityDataLoadTask.scheduleNow();
+ }
+
+ // avoid holding the scheduling lock while completing
+ if (completeWaiters != null) {
+ for (final GenericDataLoadTaskCallback callback : completeWaiters) {
+ callback.acceptCompleted(result);
+ }
+ }
+
+ schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ this.checkUnload();
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ }
+
+ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held
+ // however, when the consumer is invoked, it will hold the schedule lock
+ public GenericDataLoadTaskCallback getOrLoadEntityData(final Consumer<GenericDataLoadTask.TaskResult<CompoundTag, Throwable>> consumer) {
+ if (this.isEntityChunkNBTLoaded()) {
+ throw new IllegalStateException("Cannot load entity data, it is already loaded");
+ }
+ // why not just acquire the lock? because the caller NEEDS to call isEntityChunkNBTLoaded before this!
+ if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) {
+ throw new IllegalStateException("Must hold scheduling lock");
+ }
+
+ final GenericDataLoadTaskCallback ret = new EntityDataLoadTaskCallback((Consumer)consumer, this);
+
+ if (this.entityDataLoadTask == null) {
+ this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask(
+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
+ );
+ this.entityDataLoadTask.addCallback(this::completeEntityLoad);
+ this.entityDataLoadTaskWaiters = new ArrayList<>();
+ }
+ this.entityDataLoadTaskWaiters.add(ret);
+ if (this.entityDataLoadTask.schedule(true)) {
+ ret.schedule = this.entityDataLoadTask;
+ }
+ this.checkUnload();
+
+ return ret;
+ }
+
+ private static final class EntityDataLoadTaskCallback extends GenericDataLoadTaskCallback {
+
+ public EntityDataLoadTaskCallback(final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer, final NewChunkHolder chunkHolder) {
+ super(consumer, chunkHolder);
+ }
+
+ @Override
+ void internalCancel() {
+ this.chunkHolder.entityDataLoadTaskWaiters.remove(this);
+ this.chunkHolder.entityDataLoadTask.cancel();
+ }
+ }
+
+ private PoiChunk poiChunk;
+
+ private ChunkLoadTask.PoiDataLoadTask poiDataLoadTask;
+ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0,
+ // then the task is rescheduled
+ private List<GenericDataLoadTaskCallback> poiDataLoadTaskWaiters;
+
+ public ChunkLoadTask.PoiDataLoadTask getPoiDataLoadTask() {
+ return this.poiDataLoadTask;
+ }
+
+ // must hold schedule lock for the two below functions
+
+ public boolean isPoiChunkLoaded() {
+ return this.poiChunk != null;
+ }
+
+ private void completePoiLoad(final GenericDataLoadTask.TaskResult<PoiChunk, Throwable> result) {
+ final List<GenericDataLoadTaskCallback> completeWaiters;
+ ChunkLoadTask.PoiDataLoadTask poiDataLoadTask = null;
+ boolean schedulePoiTask = false;
+ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ final List<GenericDataLoadTaskCallback> waiters = this.poiDataLoadTaskWaiters;
+ this.poiDataLoadTask = null;
+ if (result != null) {
+ this.poiDataLoadTaskWaiters = null;
+ this.poiChunk = result.left();
+ if (result.right() != null) {
+ LOGGER.error("Unhandled poi load exception, poi data will be lost: ", result.right());
+ }
+
+ for (final GenericDataLoadTaskCallback callback : waiters) {
+ callback.markCompleted();
+ }
+
+ completeWaiters = waiters;
+ } else {
+ // cancelled
+ completeWaiters = null;
+
+ // need to re-schedule?
+ if (waiters.isEmpty()) {
+ this.poiDataLoadTaskWaiters = null;
+ // 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)
+ );
+ poiDataLoadTask.addCallback(this::completePoiLoad);
+ // need one schedule() per waiter
+ for (final GenericDataLoadTaskCallback callback : waiters) {
+ schedulePoiTask |= poiDataLoadTask.schedule(true);
+ }
+ }
+ }
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+
+ if (schedulePoiTask) {
+ poiDataLoadTask.scheduleNow();
+ }
+
+ // avoid holding the scheduling lock while completing
+ if (completeWaiters != null) {
+ for (final GenericDataLoadTaskCallback callback : completeWaiters) {
+ callback.acceptCompleted(result);
+ }
+ }
+ schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ this.checkUnload();
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ }
+
+ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held
+ // however, when the consumer is invoked, it will hold the schedule lock
+ public GenericDataLoadTaskCallback getOrLoadPoiData(final Consumer<GenericDataLoadTask.TaskResult<PoiChunk, Throwable>> consumer) {
+ if (this.isPoiChunkLoaded()) {
+ throw new IllegalStateException("Cannot load poi data, it is already loaded");
+ }
+ // why not just acquire the lock? because the caller NEEDS to call isPoiChunkLoaded before this!
+ if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) {
+ throw new IllegalStateException("Must hold scheduling lock");
+ }
+
+ final GenericDataLoadTaskCallback ret = new PoiDataLoadTaskCallback((Consumer)consumer, this);
+
+ if (this.poiDataLoadTask == null) {
+ this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask(
+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
+ );
+ this.poiDataLoadTask.addCallback(this::completePoiLoad);
+ this.poiDataLoadTaskWaiters = new ArrayList<>();
+ }
+ this.poiDataLoadTaskWaiters.add(ret);
+ if (this.poiDataLoadTask.schedule(true)) {
+ ret.schedule = this.poiDataLoadTask;
+ }
+ this.checkUnload();
+
+ return ret;
+ }
+
+ private static final class PoiDataLoadTaskCallback extends GenericDataLoadTaskCallback {
+
+ public PoiDataLoadTaskCallback(final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer, final NewChunkHolder chunkHolder) {
+ super(consumer, chunkHolder);
+ }
+
+ @Override
+ void internalCancel() {
+ this.chunkHolder.poiDataLoadTaskWaiters.remove(this);
+ this.chunkHolder.poiDataLoadTask.cancel();
+ }
+ }
+
+ public static abstract class GenericDataLoadTaskCallback implements Cancellable {
+
+ protected final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer;
+ protected final NewChunkHolder chunkHolder;
+ protected boolean completed;
+ protected GenericDataLoadTask<?, ?> schedule;
+ protected final AtomicBoolean scheduled = new AtomicBoolean();
+
+ public GenericDataLoadTaskCallback(final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer,
+ final NewChunkHolder chunkHolder) {
+ this.consumer = consumer;
+ this.chunkHolder = chunkHolder;
+ }
+
+ public void schedule() {
+ if (this.scheduled.getAndSet(true)) {
+ throw new IllegalStateException("Double calling schedule()");
+ }
+ if (this.schedule != null) {
+ this.schedule.scheduleNow();
+ this.schedule = null;
+ }
+ }
+
+ boolean isCompleted() {
+ return this.completed;
+ }
+
+ // must hold scheduling lock
+ private boolean setCompleted() {
+ if (this.completed) {
+ return false;
+ }
+ return this.completed = true;
+ }
+
+ // must hold scheduling lock
+ void markCompleted() {
+ if (this.completed) {
+ throw new IllegalStateException("May not be completed here");
+ }
+ this.completed = true;
+ }
+
+ void acceptCompleted(final GenericDataLoadTask.TaskResult<?, Throwable> result) {
+ if (result != null) {
+ if (this.completed) {
+ this.consumer.accept(result);
+ } else {
+ throw new IllegalStateException("Cannot be uncompleted at this point");
+ }
+ } else {
+ throw new NullPointerException("Result cannot be null (cancelled)");
+ }
+ }
+
+ // holds scheduling lock
+ abstract void internalCancel();
+
+ @Override
+ public boolean cancel() {
+ final NewChunkHolder holder = this.chunkHolder;
+ final ReentrantAreaLock.Node schedulingLock = holder.scheduler.schedulingLockArea.lock(holder.chunkX, holder.chunkZ);
+ try {
+ if (!this.completed) {
+ this.completed = true;
+ this.internalCancel();
+ return true;
+ }
+ return false;
+ } finally {
+ holder.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ }
+ }
+
+ private ChunkAccess currentChunk;
+
+ // generation status state
+
+ /**
+ * Current status the chunk has been brought up to by the chunk system. null indicates no work at all
+ */
+ private ChunkStatus currentGenStatus;
+
+ // This allows lockless access to the chunk and last gen status
+ private static final ChunkStatus[] ALL_STATUSES = ChunkStatus.getStatusList().toArray(new ChunkStatus[0]);
+
+ public static final record ChunkCompletion(ChunkAccess chunk, ChunkStatus genStatus) {};
+ private static final VarHandle CHUNK_COMPLETION_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(ChunkCompletion[].class);
+ private final ChunkCompletion[] chunkCompletions = new ChunkCompletion[ALL_STATUSES.length];
+
+ private volatile ChunkCompletion lastChunkCompletion;
+
+ public ChunkCompletion getLastChunkCompletion() {
+ return this.lastChunkCompletion;
+ }
+
+ public ChunkAccess getChunkIfPresentUnchecked(final ChunkStatus status) {
+ final ChunkCompletion completion = (ChunkCompletion)CHUNK_COMPLETION_ARRAY_HANDLE.getVolatile(this.chunkCompletions, status.getIndex());
+ return completion == null ? null : completion.chunk;
+ }
+
+ public ChunkAccess getChunkIfPresent(final ChunkStatus status) {
+ final ChunkStatus maxStatus = ChunkLevel.generationStatus(this.getTicketLevel());
+
+ if (maxStatus == null || status.isAfter(maxStatus)) {
+ return null;
+ }
+
+ return this.getChunkIfPresentUnchecked(status);
+ }
+
+ public void replaceProtoChunk(final ImposterProtoChunk imposterProtoChunk) {
+ for (int i = 0, max = ChunkStatus.FULL.getIndex(); i < max; ++i) {
+ CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, i, new ChunkCompletion(imposterProtoChunk, ALL_STATUSES[i]));
+ }
+ }
+
+ /**
+ * The target final chunk status the chunk system will bring the chunk to.
+ */
+ private ChunkStatus requestedGenStatus;
+
+ private ChunkProgressionTask generationTask;
+ private ChunkStatus generationTaskStatus;
+
+ /**
+ * contains the neighbours that this chunk generation is blocking on
+ */
+ private final ReferenceLinkedOpenHashSet<NewChunkHolder> neighboursBlockingGenTask = new ReferenceLinkedOpenHashSet<>(4);
+
+ /**
+ * map of ChunkHolder -> Required Status for this chunk
+ */
+ private final Reference2ObjectLinkedOpenHashMap<NewChunkHolder, ChunkStatus> neighboursWaitingForUs = new Reference2ObjectLinkedOpenHashMap<>();
+
+ public void addGenerationBlockingNeighbour(final NewChunkHolder neighbour) {
+ this.neighboursBlockingGenTask.add(neighbour);
+ }
+
+ public void addWaitingNeighbour(final NewChunkHolder neighbour, final ChunkStatus requiredStatus) {
+ final boolean wasEmpty = this.neighboursWaitingForUs.isEmpty();
+ this.neighboursWaitingForUs.put(neighbour, requiredStatus);
+ if (wasEmpty) {
+ this.checkUnload();
+ }
+ }
+
+ // priority state
+
+ // the target priority for this chunk to generate at
+ private PrioritisedExecutor.Priority priority = null;
+ private boolean priorityLocked;
+
+ // the priority neighbouring chunks have requested this chunk generate at
+ private PrioritisedExecutor.Priority neighbourRequestedPriority = null;
+
+ public PrioritisedExecutor.Priority getEffectivePriority(final PrioritisedExecutor.Priority dfl) {
+ final PrioritisedExecutor.Priority neighbour = this.neighbourRequestedPriority;
+ final PrioritisedExecutor.Priority us = this.priority;
+
+ if (neighbour == null) {
+ return us == null ? dfl : us;
+ }
+ if (us == null) {
+ return neighbour;
+ }
+
+ return PrioritisedExecutor.Priority.max(us, neighbour);
+ }
+
+ private void recalculateNeighbourRequestedPriority() {
+ if (this.neighboursWaitingForUs.isEmpty()) {
+ this.neighbourRequestedPriority = null;
+ return;
+ }
+
+ PrioritisedExecutor.Priority max = null;
+
+ for (final NewChunkHolder holder : this.neighboursWaitingForUs.keySet()) {
+ final PrioritisedExecutor.Priority neighbourPriority = holder.getEffectivePriority(null);
+ if (neighbourPriority != null && (max == null || neighbourPriority.isHigherPriority(max))) {
+ max = neighbourPriority;
+ }
+ }
+
+ final PrioritisedExecutor.Priority current = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL);
+ this.neighbourRequestedPriority = max;
+ final PrioritisedExecutor.Priority next = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL);
+
+ if (current == next) {
+ return;
+ }
+
+ // our effective priority has changed, so change our task
+ if (this.generationTask != null) {
+ this.generationTask.setPriority(next);
+ }
+
+ // now propagate this to our neighbours
+ this.recalculateNeighbourPriorities();
+ }
+
+ public void recalculateNeighbourPriorities() {
+ for (final NewChunkHolder holder : this.neighboursBlockingGenTask) {
+ holder.recalculateNeighbourRequestedPriority();
+ }
+ }
+
+ // must hold scheduling lock
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
+ if (this.priority != null && this.priority.isHigherOrEqualPriority(priority)) {
+ return;
+ }
+ this.setPriority(priority);
+ }
+
+ private void lockPriority() {
+ this.priority = null;
+ this.priorityLocked = true;
+ }
+
+ // must hold scheduling lock
+ public void setPriority(final PrioritisedExecutor.Priority priority) {
+ if (this.priorityLocked) {
+ return;
+ }
+ final PrioritisedExecutor.Priority old = this.getEffectivePriority(null);
+ this.priority = priority;
+ final PrioritisedExecutor.Priority newPriority = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL);
+
+ if (old != newPriority) {
+ if (this.generationTask != null) {
+ this.generationTask.setPriority(newPriority);
+ }
+ }
+
+ this.recalculateNeighbourPriorities();
+ }
+
+ // must hold scheduling lock
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ if (this.priority != null && this.priority.isLowerOrEqualPriority(priority)) {
+ return;
+ }
+ this.setPriority(priority);
+ }
+
+ // error handling state
+ private ChunkStatus failedGenStatus;
+ private Throwable genTaskException;
+ private Thread genTaskFailedThread;
+
+ private boolean failedLightUpdate;
+
+ public void failedLightUpdate() {
+ this.failedLightUpdate = true;
+ }
+
+ public boolean hasFailedGeneration() {
+ return this.genTaskException != null;
+ }
+
+ // ticket level state
+ public int oldTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1;
+ private int currentTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1;
+
+ public int getTicketLevel() {
+ return this.currentTicketLevel;
+ }
+
+ public final ChunkHolder vanillaChunkHolder;
+
+ public NewChunkHolder(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkTaskScheduler scheduler) {
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.scheduler = scheduler;
+ this.vanillaChunkHolder = new ChunkHolder(
+ new ChunkPos(chunkX, chunkZ), ChunkHolderManager.MAX_TICKET_LEVEL, world,
+ world.getLightEngine(), null, world.getChunkSource().chunkMap
+ );
+ ((ChunkSystemChunkHolder)this.vanillaChunkHolder).moonrise$setRealChunkHolder(this);
+ }
+
+ public ChunkAccess getCurrentChunk() {
+ return this.currentChunk;
+ }
+
+ int getCurrentTicketLevel() {
+ return this.currentTicketLevel;
+ }
+
+ void updateTicketLevel(final int toLevel) {
+ this.currentTicketLevel = toLevel;
+ }
+
+ private int totalNeighboursUsingThisChunk = 0;
+
+ // holds schedule lock
+ public void addNeighbourUsingChunk() {
+ final int now = ++this.totalNeighboursUsingThisChunk;
+
+ if (now == 1) {
+ this.checkUnload();
+ }
+ }
+
+ // holds schedule lock
+ public void removeNeighbourUsingChunk() {
+ final int now = --this.totalNeighboursUsingThisChunk;
+
+ if (now == 0) {
+ this.checkUnload();
+ }
+
+ if (now < 0) {
+ throw new IllegalStateException("Neighbours using this chunk cannot be negative");
+ }
+ }
+
+ // must hold scheduling lock
+ // returns string reason for why chunk should remain loaded, null otherwise
+ public final String isSafeToUnload() {
+ // is ticket level below threshold?
+ if (this.oldTicketLevel <= ChunkHolderManager.MAX_TICKET_LEVEL) {
+ return "ticket_level";
+ }
+
+ // are we being used by another chunk for generation?
+ if (this.totalNeighboursUsingThisChunk != 0) {
+ return "neighbours_generating";
+ }
+
+ // are we going to be used by another chunk for generation?
+ if (!this.neighboursWaitingForUs.isEmpty()) {
+ return "neighbours_waiting";
+ }
+
+ // chunk must be marked inaccessible (i.e. unloaded to plugins)
+ if (this.getChunkStatus() != FullChunkStatus.INACCESSIBLE) {
+ return "fullchunkstatus";
+ }
+
+ // are we currently generating anything, or have requested generation?
+ if (this.generationTask != null) {
+ return "generating";
+ }
+ if (this.requestedGenStatus != null) {
+ return "requested_generation";
+ }
+
+ // entity data requested?
+ if (this.entityDataLoadTask != null) {
+ return "entity_data_requested";
+ }
+
+ // poi data requested?
+ if (this.poiDataLoadTask != null) {
+ return "poi_data_requested";
+ }
+
+ // are we pending serialization?
+ if (this.entityDataUnload != null) {
+ return "entity_serialization";
+ }
+ if (this.poiDataUnload != null) {
+ return "poi_serialization";
+ }
+ if (this.chunkDataUnload != null) {
+ return "chunk_serialization";
+ }
+
+ // Note: light tasks do not need a check, as they add a ticket.
+
+ // nothing is using this chunk, so it should be unloaded
+ return null;
+ }
+
+ /** Unloaded from chunk map */
+ private boolean unloaded;
+
+ void markUnloaded() {
+ this.unloaded = true;
+ }
+
+ private boolean inUnloadQueue = false;
+
+ void removeFromUnloadQueue() {
+ this.inUnloadQueue = false;
+ }
+
+ // must hold scheduling lock
+ private void checkUnload() {
+ if (this.unloaded) {
+ return;
+ }
+ if (this.isSafeToUnload() == null) {
+ // ensure in unload queue
+ if (!this.inUnloadQueue) {
+ this.inUnloadQueue = true;
+ this.scheduler.chunkHolderManager.unloadQueue.addChunk(this.chunkX, this.chunkZ);
+ }
+ } else {
+ // ensure not in unload queue
+ if (this.inUnloadQueue) {
+ this.inUnloadQueue = false;
+ this.scheduler.chunkHolderManager.unloadQueue.removeChunk(this.chunkX, this.chunkZ);
+ }
+ }
+ }
+
+ static final record UnloadState(NewChunkHolder holder, ChunkAccess chunk, ChunkEntitySlices entityChunk, PoiChunk poiChunk) {};
+
+ // note: these are completed with null to indicate that no write occurred
+ // they are also completed with null to indicate a null write occurred
+ private UnloadTask chunkDataUnload;
+ private UnloadTask entityDataUnload;
+ private UnloadTask poiDataUnload;
+
+ public static final record UnloadTask(Completable<CompoundTag> completable, DelayedPrioritisedTask task) {}
+
+ public UnloadTask getUnloadTask(final RegionFileIOThread.RegionFileType type) {
+ switch (type) {
+ case CHUNK_DATA:
+ return this.chunkDataUnload;
+ case ENTITY_DATA:
+ return this.entityDataUnload;
+ case POI_DATA:
+ return this.poiDataUnload;
+ default:
+ throw new IllegalStateException("Unknown regionfile type " + type);
+ }
+ }
+
+ private void removeUnloadTask(final RegionFileIOThread.RegionFileType type) {
+ switch (type) {
+ case CHUNK_DATA: {
+ this.chunkDataUnload = null;
+ return;
+ }
+ case ENTITY_DATA: {
+ this.entityDataUnload = null;
+ return;
+ }
+ case POI_DATA: {
+ this.poiDataUnload = null;
+ return;
+ }
+ default:
+ throw new IllegalStateException("Unknown regionfile type " + type);
+ }
+ }
+
+ private UnloadState unloadState;
+
+ // holds schedule lock
+ UnloadState unloadStage1() {
+ // because we hold the scheduling lock, we cannot actually unload anything
+ // so, what we do here instead is to null this chunk's state and setup the unload tasks
+ // the unload tasks will ensure that any loads that take place after stage1 (i.e during stage2, in which
+ // we do not hold the lock) c
+ final ChunkAccess chunk = this.currentChunk;
+ final ChunkEntitySlices entityChunk = this.entityChunk;
+ final PoiChunk poiChunk = this.poiChunk;
+ // 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);
+ }
+ // entity chunk state
+ this.entityChunk = null;
+ this.pendingEntityChunk = null;
+
+ // poi chunk state
+ this.poiChunk = null;
+
+ // priority state
+ this.priorityLocked = false;
+
+ if (chunk != null) {
+ this.chunkDataUnload = new UnloadTask(new Completable<>(), new DelayedPrioritisedTask(PrioritisedExecutor.Priority.NORMAL));
+ }
+ if (poiChunk != null) {
+ this.poiDataUnload = new UnloadTask(new Completable<>(), null);
+ }
+ if (entityChunk != null) {
+ this.entityDataUnload = new UnloadTask(new Completable<>(), 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) {
+ if (data != null) {
+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, data, type);
+ }
+
+ this.getUnloadTask(type).completable().complete(data);
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ // can only write to these fields while holding the schedule lock
+ this.removeUnloadTask(type);
+ this.checkUnload();
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ }
+
+ void unloadStage2(final UnloadState state) {
+ this.unloadState = null;
+ final ChunkAccess chunk = state.chunk();
+ final ChunkEntitySlices entityChunk = state.entityChunk();
+ final PoiChunk poiChunk = state.poiChunk();
+
+ final boolean shouldLevelChunkNotSave = ChunkSystemFeatures.forceNoSave(chunk);
+
+ // unload chunk data
+ if (chunk != null) {
+ if (chunk instanceof LevelChunk levelChunk) {
+ levelChunk.setLoaded(false);
+ }
+
+ if (!shouldLevelChunkNotSave) {
+ this.saveChunk(chunk, true);
+ } else {
+ this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
+ }
+
+ if (chunk instanceof LevelChunk levelChunk) {
+ this.world.unload(levelChunk);
+ }
+ }
+
+ // unload entity data
+ if (entityChunk != null) {
+ this.saveEntities(entityChunk, true);
+ // yes this is a hack to pass the compound tag through...
+ final CompoundTag lastEntityUnload = this.lastEntityUnload;
+ this.lastEntityUnload = null;
+
+ if (entityChunk.unload()) {
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ entityChunk.setTransient(true);
+ this.entityChunk = entityChunk;
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ } else {
+ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().entitySectionUnload(this.chunkX, this.chunkZ);
+ }
+ // we need to delay the callback until after determining transience, otherwise a potential loader could
+ // set entityChunk before we do
+ this.entityDataUnload.completable().complete(lastEntityUnload);
+ }
+
+ // unload poi data
+ if (poiChunk != null) {
+ if (poiChunk.isDirty() && !shouldLevelChunkNotSave) {
+ this.savePOI(poiChunk, true);
+ } else {
+ this.poiDataUnload.completable().complete(null);
+ }
+
+ if (poiChunk.isLoaded()) {
+ ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$onUnload(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ));
+ }
+ }
+ }
+
+ boolean unloadStage3() {
+ // can only write to these while holding the schedule lock, and we instantly complete them in stage2
+ this.poiDataUnload = null;
+ this.entityDataUnload = null;
+
+ // we need to check if anything has been loaded in the meantime (or if we have transient entities)
+ if (this.entityChunk != null || this.poiChunk != null || this.currentChunk != null) {
+ return false;
+ }
+
+ return this.isSafeToUnload() == null;
+ }
+
+ private void cancelGenTask() {
+ if (this.generationTask != null) {
+ this.generationTask.cancel();
+ } else {
+ // otherwise, we are blocking on neighbours, so remove them
+ if (!this.neighboursBlockingGenTask.isEmpty()) {
+ for (final NewChunkHolder neighbour : this.neighboursBlockingGenTask) {
+ if (neighbour.neighboursWaitingForUs.remove(this) == null) {
+ throw new IllegalStateException("Corrupt state");
+ }
+ if (neighbour.neighboursWaitingForUs.isEmpty()) {
+ neighbour.checkUnload();
+ }
+ }
+ this.neighboursBlockingGenTask.clear();
+ this.checkUnload();
+ }
+ }
+ }
+
+ // holds: ticket level update lock
+ // holds: schedule lock
+ public void processTicketLevelUpdate(final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedLoadStatus) {
+ final int oldLevel = this.oldTicketLevel;
+ final int newLevel = this.currentTicketLevel;
+
+ if (oldLevel == newLevel) {
+ return;
+ }
+
+ this.oldTicketLevel = newLevel;
+
+ final FullChunkStatus oldState = ChunkLevel.fullStatus(oldLevel);
+ final FullChunkStatus newState = ChunkLevel.fullStatus(newLevel);
+ final boolean oldUnloaded = oldLevel > ChunkHolderManager.MAX_TICKET_LEVEL;
+ final boolean newUnloaded = newLevel > ChunkHolderManager.MAX_TICKET_LEVEL;
+
+ final ChunkStatus maxGenerationStatusOld = ChunkLevel.generationStatus(oldLevel);
+ final ChunkStatus maxGenerationStatusNew = ChunkLevel.generationStatus(newLevel);
+
+ // check for cancellations from downgrading ticket level
+ if (this.requestedGenStatus != null && !newState.isOrAfter(FullChunkStatus.FULL) && newLevel > oldLevel) {
+ // note: cancel() may invoke onChunkGenComplete synchronously here
+ if (newUnloaded) {
+ // need to cancel all tasks
+ // note: requested status must be set to null here before cancellation, to indicate to the
+ // completion logic that we do not want rescheduling to occur
+ this.requestedGenStatus = null;
+ this.cancelGenTask();
+ } else {
+ final ChunkStatus toCancel = ((ChunkSystemChunkStatus)maxGenerationStatusNew).moonrise$getNextStatus();
+ final ChunkStatus currentRequestedStatus = this.requestedGenStatus;
+
+ if (currentRequestedStatus.isOrAfter(toCancel)) {
+ // we do have to cancel something here
+ // clamp requested status to the maximum
+ if (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(maxGenerationStatusNew)) {
+ // already generated to status, so we must cancel
+ this.requestedGenStatus = null;
+ this.cancelGenTask();
+ } else {
+ // not generated to status, so we may have to cancel
+ // note: gen task is always 1 status above current gen status if not null
+ this.requestedGenStatus = maxGenerationStatusNew;
+ if (this.generationTaskStatus != null && this.generationTaskStatus.isOrAfter(toCancel)) {
+ // TOOD is this even possible? i don't think so
+ throw new IllegalStateException("?????");
+ }
+ }
+ }
+ }
+ }
+
+ if (oldState != newState) {
+ if (newState.isOrAfter(oldState)) {
+ // status upgrade
+ if (!oldState.isOrAfter(FullChunkStatus.FULL) && newState.isOrAfter(FullChunkStatus.FULL)) {
+ // may need to schedule full load
+ if (this.currentGenStatus != ChunkStatus.FULL) {
+ if (this.requestedGenStatus != null) {
+ this.requestedGenStatus = ChunkStatus.FULL;
+ } else {
+ this.scheduler.schedule(
+ this.chunkX, this.chunkZ, ChunkStatus.FULL, this, scheduledTasks
+ );
+ }
+ }
+ }
+ } else {
+ // status downgrade
+ if (!newState.isOrAfter(FullChunkStatus.ENTITY_TICKING) && oldState.isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
+ this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, null);
+ }
+
+ if (!newState.isOrAfter(FullChunkStatus.BLOCK_TICKING) && oldState.isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
+ this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, null);
+ }
+
+ if (!newState.isOrAfter(FullChunkStatus.FULL) && oldState.isOrAfter(FullChunkStatus.FULL)) {
+ this.completeFullStatusConsumers(FullChunkStatus.FULL, null);
+ }
+ }
+
+ if (this.updatePendingStatus()) {
+ changedLoadStatus.add(this);
+ }
+ }
+
+ if (oldUnloaded != newUnloaded) {
+ this.checkUnload();
+ }
+ }
+
+ static final int NEIGHBOUR_RADIUS = 2;
+ private long fullNeighbourChunksLoadedBitset;
+
+ private static int getFullNeighbourIndex(final int relativeX, final int relativeZ) {
+ // index = (relativeX + NEIGHBOUR_CACHE_RADIUS) + (relativeZ + NEIGHBOUR_CACHE_RADIUS) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)
+ // optimised variant of the above by moving some of the ops to compile time
+ return relativeX + (relativeZ * (NEIGHBOUR_RADIUS * 2 + 1)) + (NEIGHBOUR_RADIUS + NEIGHBOUR_RADIUS * ((NEIGHBOUR_RADIUS * 2 + 1)));
+ }
+ public final boolean isNeighbourFullLoaded(final int relativeX, final int relativeZ) {
+ return (this.fullNeighbourChunksLoadedBitset & (1L << getFullNeighbourIndex(relativeX, relativeZ))) != 0;
+ }
+
+ // returns true if this chunk changed pending full status
+ // must hold scheduling lock
+ public final boolean setNeighbourFullLoaded(final int relativeX, final int relativeZ) {
+ final int index = getFullNeighbourIndex(relativeX, relativeZ);
+ this.fullNeighbourChunksLoadedBitset |= (1L << index);
+ return this.updatePendingStatus();
+ }
+
+ // returns true if this chunk changed pending full status
+ // must hold scheduling lock
+ public final boolean setNeighbourFullUnloaded(final int relativeX, final int relativeZ) {
+ final int index = getFullNeighbourIndex(relativeX, relativeZ);
+ this.fullNeighbourChunksLoadedBitset &= ~(1L << index);
+ return this.updatePendingStatus();
+ }
+
+ private static long getLoadedMask(final int radius) {
+ long mask = 0L;
+ for (int dx = -radius; dx <= radius; ++dx) {
+ for (int dz = -radius; dz <= radius; ++dz) {
+ mask |= (1L << getFullNeighbourIndex(dx, dz));
+ }
+ }
+
+ return mask;
+ }
+
+ private static final long CHUNK_LOADED_MASK_RAD0 = getLoadedMask(0);
+ 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
+ // so, current will never update to a value higher than pending
+ private FullChunkStatus currentFullChunkStatus = FullChunkStatus.INACCESSIBLE;
+
+ public FullChunkStatus getChunkStatus() {
+ // no volatile access, access off-main is considered racey anyways
+ return this.currentFullChunkStatus;
+ }
+
+ public boolean isEntityTickingReady() {
+ return this.getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING);
+ }
+
+ public boolean isTickingReady() {
+ return this.getChunkStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING);
+ }
+
+ public boolean isFullChunkReady() {
+ return this.getChunkStatus().isOrAfter(FullChunkStatus.FULL);
+ }
+
+ private static FullChunkStatus getStatusForBitset(final long bitset) {
+ if ((bitset & CHUNK_LOADED_MASK_RAD2) == CHUNK_LOADED_MASK_RAD2) {
+ return FullChunkStatus.ENTITY_TICKING;
+ } else if ((bitset & CHUNK_LOADED_MASK_RAD1) == CHUNK_LOADED_MASK_RAD1) {
+ return FullChunkStatus.BLOCK_TICKING;
+ } else if ((bitset & CHUNK_LOADED_MASK_RAD0) == CHUNK_LOADED_MASK_RAD0) {
+ return FullChunkStatus.FULL;
+ } else {
+ return FullChunkStatus.INACCESSIBLE;
+ }
+ }
+
+ // must hold scheduling lock
+ // returns whether the pending status was changed
+ private boolean updatePendingStatus() {
+ final FullChunkStatus byTicketLevel = ChunkLevel.fullStatus(this.oldTicketLevel); // oldTicketLevel is controlled by scheduling lock
+
+ FullChunkStatus pending = getStatusForBitset(this.fullNeighbourChunksLoadedBitset);
+ if (pending == FullChunkStatus.INACCESSIBLE && byTicketLevel.isOrAfter(FullChunkStatus.FULL) && this.currentGenStatus == ChunkStatus.FULL) {
+ // the bitset is only for chunks that have gone through the status updater
+ // but here we are ready to go to FULL
+ pending = FullChunkStatus.FULL;
+ }
+
+ if (pending.isOrAfter(byTicketLevel)) { // pending >= byTicketLevel
+ // cannot set above ticket level
+ pending = byTicketLevel;
+ }
+
+ if (this.pendingFullChunkStatus == pending) {
+ return false;
+ }
+
+ this.pendingFullChunkStatus = pending;
+
+ return true;
+ }
+
+ private void onFullChunkLoadChange(final boolean loaded, final List<NewChunkHolder> changedFullStatus) {
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, NEIGHBOUR_RADIUS);
+ try {
+ for (int dz = -NEIGHBOUR_RADIUS; dz <= NEIGHBOUR_RADIUS; ++dz) {
+ for (int dx = -NEIGHBOUR_RADIUS; dx <= NEIGHBOUR_RADIUS; ++dx) {
+ final NewChunkHolder holder = (dx | dz) == 0 ? this : this.scheduler.chunkHolderManager.getChunkHolder(dx + this.chunkX, dz + this.chunkZ);
+ if (loaded) {
+ if (holder.setNeighbourFullLoaded(-dx, -dz)) {
+ changedFullStatus.add(holder);
+ }
+ } else {
+ if (holder != null && holder.setNeighbourFullUnloaded(-dx, -dz)) {
+ changedFullStatus.add(holder);
+ }
+ }
+ }
+ }
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ }
+
+ private void changeEntityChunkStatus(final FullChunkStatus toStatus) {
+ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().chunkStatusChange(this.chunkX, this.chunkZ, toStatus);
+ }
+
+ private boolean processingFullStatus = false;
+
+ private void updateCurrentState(final FullChunkStatus to) {
+ this.currentFullChunkStatus = to;
+ }
+
+ // only to be called on the main thread, no locks need to be held
+ public boolean handleFullStatusChange(final List<NewChunkHolder> changedFullStatus) {
+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot update full status thread off-main");
+
+ boolean ret = false;
+
+ if (this.processingFullStatus) {
+ // we cannot process updates recursively, as we may be in the middle of logic to upgrade/downgrade status
+ return ret;
+ }
+
+ this.processingFullStatus = true;
+ try {
+ for (;;) {
+ // check if we have any remaining work to do
+
+ // we do not need to hold the scheduling lock to read pending, as changes to pending
+ // will queue a status update
+
+ final FullChunkStatus pending = this.pendingFullChunkStatus;
+ FullChunkStatus current = this.currentFullChunkStatus;
+
+ if (pending == current) {
+ if (pending == FullChunkStatus.INACCESSIBLE) {
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ this.checkUnload();
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ }
+ return ret;
+ }
+
+ ret = true;
+
+ // note: because the chunk system delays any ticket downgrade to the chunk holder manager tick, we
+ // do not need to consider cases where the ticket level may decrease during this call by asynchronous
+ // ticket changes
+
+ // chunks cannot downgrade state while status is pending a change
+ // note: currentChunk must be LevelChunk, as current != pending which means that at least one is not ACCESSIBLE
+ final LevelChunk chunk = (LevelChunk)this.currentChunk;
+
+ // Note: we assume that only load/unload contain plugin logic
+ // plugin logic is anything stupid enough to possibly change the chunk status while it is already
+ // being changed (i.e during load it is possible it will try to set to full ticking)
+ // in order to allow this change, we also need this plugin logic to be contained strictly after all
+ // of the chunk system load callbacks are invoked
+ if (pending.isOrAfter(current)) {
+ // state upgrade
+ if (!current.isOrAfter(FullChunkStatus.FULL) && pending.isOrAfter(FullChunkStatus.FULL)) {
+ this.updateCurrentState(FullChunkStatus.FULL);
+ ChunkSystem.onChunkPreBorder(chunk, this.vanillaChunkHolder);
+ this.scheduler.chunkHolderManager.ensureInAutosave(this);
+ this.changeEntityChunkStatus(FullChunkStatus.FULL);
+ ChunkSystem.onChunkBorder(chunk, this.vanillaChunkHolder);
+ this.onFullChunkLoadChange(true, changedFullStatus);
+ this.completeFullStatusConsumers(FullChunkStatus.FULL, chunk);
+ }
+
+ if (!current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
+ this.updateCurrentState(FullChunkStatus.BLOCK_TICKING);
+ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING);
+ ChunkSystem.onChunkTicking(chunk, this.vanillaChunkHolder);
+ this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, chunk);
+ }
+
+ if (!current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
+ this.updateCurrentState(FullChunkStatus.ENTITY_TICKING);
+ this.changeEntityChunkStatus(FullChunkStatus.ENTITY_TICKING);
+ ChunkSystem.onChunkEntityTicking(chunk, this.vanillaChunkHolder);
+ this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, chunk);
+ }
+ } else {
+ if (current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && !pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
+ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING);
+ ChunkSystem.onChunkNotEntityTicking(chunk, this.vanillaChunkHolder);
+ this.updateCurrentState(FullChunkStatus.BLOCK_TICKING);
+ }
+
+ if (current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && !pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
+ this.changeEntityChunkStatus(FullChunkStatus.FULL);
+ ChunkSystem.onChunkNotTicking(chunk, this.vanillaChunkHolder);
+ this.updateCurrentState(FullChunkStatus.FULL);
+ }
+
+ if (current.isOrAfter(FullChunkStatus.FULL) && !pending.isOrAfter(FullChunkStatus.FULL)) {
+ this.onFullChunkLoadChange(false, changedFullStatus);
+ this.changeEntityChunkStatus(FullChunkStatus.INACCESSIBLE);
+ ChunkSystem.onChunkNotBorder(chunk, this.vanillaChunkHolder);
+ ChunkSystem.onChunkPostNotBorder(chunk, this.vanillaChunkHolder);
+ this.updateCurrentState(FullChunkStatus.INACCESSIBLE);
+ }
+ }
+ }
+ } finally {
+ this.processingFullStatus = false;
+ }
+ }
+
+ // note: must hold scheduling lock
+ // rets true if the current requested gen status is not null (effectively, whether further scheduling is not needed)
+ boolean upgradeGenTarget(final ChunkStatus toStatus) {
+ if (toStatus == null) {
+ throw new NullPointerException("toStatus cannot be null");
+ }
+ if (this.requestedGenStatus == null && this.generationTask == null) {
+ return false;
+ }
+ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(toStatus)) {
+ this.requestedGenStatus = toStatus;
+ }
+ return true;
+ }
+
+ public void setGenerationTarget(final ChunkStatus toStatus) {
+ this.requestedGenStatus = toStatus;
+ }
+
+ public boolean hasGenerationTask() {
+ return this.generationTask != null;
+ }
+
+ public ChunkStatus getCurrentGenStatus() {
+ return this.currentGenStatus;
+ }
+
+ public ChunkStatus getRequestedGenStatus() {
+ return this.requestedGenStatus;
+ }
+
+ private final Reference2ObjectOpenHashMap<ChunkStatus, List<Consumer<ChunkAccess>>> statusWaiters = new Reference2ObjectOpenHashMap<>();
+
+ void addStatusConsumer(final ChunkStatus status, final Consumer<ChunkAccess> consumer) {
+ this.statusWaiters.computeIfAbsent(status, (final ChunkStatus keyInMap) -> {
+ return new ArrayList<>(4);
+ }).add(consumer);
+ }
+
+ private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) {
+ // need to tell future statuses to complete if cancelled
+ do {
+ this.completeStatusConsumers0(status, chunk);
+ } while (chunk == null && status != (status = ((ChunkSystemChunkStatus)status).moonrise$getNextStatus()));
+ }
+
+ private void completeStatusConsumers0(final ChunkStatus status, final ChunkAccess chunk) {
+ final List<Consumer<ChunkAccess>> consumers;
+ consumers = this.statusWaiters.remove(status);
+
+ if (consumers == null) {
+ return;
+ }
+
+ // must be scheduled to main, we do not trust the callback to not do anything stupid
+ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
+ for (final Consumer<ChunkAccess> consumer : consumers) {
+ try {
+ consumer.accept(chunk);
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to process chunk status callback", thr);
+ }
+ }
+ }, PrioritisedExecutor.Priority.HIGHEST);
+ }
+
+ private final Reference2ObjectOpenHashMap<FullChunkStatus, List<Consumer<LevelChunk>>> fullStatusWaiters = new Reference2ObjectOpenHashMap<>();
+
+ void addFullStatusConsumer(final FullChunkStatus status, final Consumer<LevelChunk> consumer) {
+ this.fullStatusWaiters.computeIfAbsent(status, (final FullChunkStatus keyInMap) -> {
+ return new ArrayList<>(4);
+ }).add(consumer);
+ }
+
+ private void completeFullStatusConsumers(FullChunkStatus status, final LevelChunk chunk) {
+ final List<Consumer<LevelChunk>> consumers;
+ consumers = this.fullStatusWaiters.remove(status);
+
+ if (consumers == null) {
+ return;
+ }
+
+ // must be scheduled to main, we do not trust the callback to not do anything stupid
+ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
+ for (final Consumer<LevelChunk> consumer : consumers) {
+ try {
+ consumer.accept(chunk);
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to process chunk status callback", thr);
+ }
+ }
+ }, PrioritisedExecutor.Priority.HIGHEST);
+ }
+
+ // note: must hold scheduling lock
+ private void onChunkGenComplete(final ChunkAccess newChunk, final ChunkStatus newStatus,
+ final List<ChunkProgressionTask> scheduleList, final List<NewChunkHolder> changedLoadStatus) {
+ if (!this.neighboursBlockingGenTask.isEmpty()) {
+ throw new IllegalStateException("Cannot have neighbours blocking this gen task");
+ }
+ if (newChunk != null || (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(newStatus))) {
+ this.completeStatusConsumers(newStatus, newChunk);
+ }
+ // done now, clear state (must be done before scheduling new tasks)
+ this.generationTask = null;
+ this.generationTaskStatus = null;
+ if (newChunk == null) {
+ // task was cancelled
+ // should be careful as this could be called while holding the schedule lock and/or inside the
+ // ticket level update
+ // while a task may be cancelled, it is possible for it to be later re-scheduled
+ // however, because generationTask is only set to null on _completion_, the scheduler leaves
+ // the rescheduling logic to us here
+ final ChunkStatus requestedGenStatus = this.requestedGenStatus;
+ this.requestedGenStatus = null;
+ if (requestedGenStatus != null) {
+ // it looks like it has been requested, so we must reschedule
+ if (!this.neighboursWaitingForUs.isEmpty()) {
+ for (final Iterator<Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus>> iterator = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
+ final Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus> entry = iterator.next();
+
+ final NewChunkHolder chunkHolder = entry.getKey();
+ final ChunkStatus toStatus = entry.getValue();
+
+ if (!requestedGenStatus.isOrAfter(toStatus)) {
+ // if we were cancelled, we are responsible for removing the waiter
+ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) {
+ throw new IllegalStateException("Corrupt state");
+ }
+ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) {
+ chunkHolder.checkUnload();
+ }
+ iterator.remove();
+ continue;
+ }
+ }
+ }
+
+ // note: only after generationTask -> null, generationTaskStatus -> null, and requestedGenStatus -> null
+ this.scheduler.schedule(
+ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList
+ );
+
+ // return, can't do anything further
+ return;
+ }
+
+ if (!this.neighboursWaitingForUs.isEmpty()) {
+ for (final NewChunkHolder chunkHolder : this.neighboursWaitingForUs.keySet()) {
+ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) {
+ throw new IllegalStateException("Corrupt state");
+ }
+ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) {
+ chunkHolder.checkUnload();
+ }
+ }
+ this.neighboursWaitingForUs.clear();
+ }
+ // reset priority, we have nothing left to generate to
+ this.setPriority(null);
+ this.checkUnload();
+ return;
+ }
+
+ this.currentChunk = newChunk;
+ this.currentGenStatus = newStatus;
+ final ChunkCompletion completion = new ChunkCompletion(newChunk, newStatus);
+ CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, newStatus.getIndex(), completion);
+ this.lastChunkCompletion = completion;
+
+ final ChunkStatus requestedGenStatus = this.requestedGenStatus;
+
+ List<NewChunkHolder> needsScheduling = null;
+ boolean recalculatePriority = false;
+ for (final Iterator<Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus>> iterator
+ = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
+ final Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus> entry = iterator.next();
+ final NewChunkHolder neighbour = entry.getKey();
+ final ChunkStatus requiredStatus = entry.getValue();
+
+ if (!newStatus.isOrAfter(requiredStatus)) {
+ if (requestedGenStatus == null || !requestedGenStatus.isOrAfter(requiredStatus)) {
+ // if we're cancelled, still need to clear this map
+ if (!neighbour.neighboursBlockingGenTask.remove(this)) {
+ throw new IllegalStateException("Neighbour is not waiting for us?");
+ }
+ if (neighbour.neighboursBlockingGenTask.isEmpty()) {
+ neighbour.checkUnload();
+ }
+
+ iterator.remove();
+ }
+ continue;
+ }
+
+ // doesn't matter what isCancelled is here, we need to schedule if we can
+
+ recalculatePriority = true;
+ if (!neighbour.neighboursBlockingGenTask.remove(this)) {
+ throw new IllegalStateException("Neighbour is not waiting for us?");
+ }
+
+ if (neighbour.neighboursBlockingGenTask.isEmpty()) {
+ if (neighbour.requestedGenStatus != null) {
+ if (needsScheduling == null) {
+ needsScheduling = new ArrayList<>();
+ }
+ needsScheduling.add(neighbour);
+ } else {
+ neighbour.checkUnload();
+ }
+ }
+
+ // remove last; access to entry will throw if removed
+ iterator.remove();
+ }
+
+ if (newStatus == ChunkStatus.FULL) {
+ this.lockPriority();
+ // try to push pending to FULL
+ if (this.updatePendingStatus()) {
+ changedLoadStatus.add(this);
+ }
+ }
+
+ if (recalculatePriority) {
+ this.recalculateNeighbourRequestedPriority();
+ }
+
+ if (requestedGenStatus != null && !newStatus.isOrAfter(requestedGenStatus)) {
+ this.scheduleNeighbours(needsScheduling, scheduleList);
+
+ // we need to schedule more tasks now
+ this.scheduler.schedule(
+ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList
+ );
+ } else {
+ // we're done now
+ if (requestedGenStatus != null) {
+ this.requestedGenStatus = null;
+ }
+ // reached final stage, so stop scheduling now
+ this.setPriority(null);
+ this.checkUnload();
+
+ this.scheduleNeighbours(needsScheduling, scheduleList);
+ }
+ }
+
+ private void scheduleNeighbours(final List<NewChunkHolder> needsScheduling, final List<ChunkProgressionTask> scheduleList) {
+ if (needsScheduling != null) {
+ for (int i = 0, len = needsScheduling.size(); i < len; ++i) {
+ final NewChunkHolder neighbour = needsScheduling.get(i);
+
+ this.scheduler.schedule(
+ neighbour.chunkX, neighbour.chunkZ, neighbour.requestedGenStatus, neighbour, scheduleList
+ );
+ }
+ }
+ }
+
+ public void setGenerationTask(final ChunkProgressionTask generationTask, final ChunkStatus taskStatus,
+ final List<NewChunkHolder> neighbours) {
+ if (this.generationTask != null || (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(taskStatus))) {
+ throw new IllegalStateException("Currently generating or provided task is trying to generate to a level we are already at!");
+ }
+ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(taskStatus)) {
+ throw new IllegalStateException("Cannot schedule generation task when not requested");
+ }
+ this.generationTask = generationTask;
+ this.generationTaskStatus = taskStatus;
+
+ for (int i = 0, len = neighbours.size(); i < len; ++i) {
+ neighbours.get(i).addNeighbourUsingChunk();
+ }
+
+ this.checkUnload();
+
+ generationTask.onComplete((final ChunkAccess access, final Throwable thr) -> {
+ if (generationTask != this.generationTask) {
+ throw new IllegalStateException(
+ "Cannot complete generation task '" + generationTask + "' because we are waiting on '" + this.generationTask + "' instead!"
+ );
+ }
+ if (thr != null) {
+ if (this.genTaskException != null) {
+ LOGGER.warn("Ignoring exception for " + this.toString(), thr);
+ return;
+ }
+ // don't set generation task to null, so that scheduling will not attempt to create another task and it
+ // will automatically block any further scheduling usage of this chunk as it will wait forever for a failed
+ // task to complete
+ this.genTaskException = thr;
+ this.failedGenStatus = taskStatus;
+ this.genTaskFailedThread = Thread.currentThread();
+
+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
+ "Generation task", ChunkTaskScheduler.stringIfNull(generationTask),
+ "Task to status", ChunkTaskScheduler.stringIfNull(taskStatus)
+ ), thr);
+ return;
+ }
+
+ final boolean scheduleTasks;
+ List<ChunkProgressionTask> tasks = ChunkHolderManager.getCurrentTicketUpdateScheduling();
+ if (tasks == null) {
+ scheduleTasks = true;
+ tasks = new ArrayList<>();
+ } else {
+ scheduleTasks = false;
+ // we are currently updating ticket levels, so we already hold the schedule lock
+ // this means we have to leave the ticket level update to handle the scheduling
+ }
+ final List<NewChunkHolder> changedLoadStatus = new ArrayList<>();
+ // theoretically, we could schedule a chunk at the max radius which performs another max radius access. So we need to double the radius.
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, 2 * ChunkTaskScheduler.getMaxAccessRadius());
+ try {
+ for (int i = 0, len = neighbours.size(); i < len; ++i) {
+ neighbours.get(i).removeNeighbourUsingChunk();
+ }
+ this.onChunkGenComplete(access, taskStatus, tasks, changedLoadStatus);
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ this.scheduler.chunkHolderManager.addChangedStatuses(changedLoadStatus);
+
+ if (scheduleTasks) {
+ // can't hold the lock while scheduling, so we have to build the tasks and then schedule after
+ for (int i = 0, len = tasks.size(); i < len; ++i) {
+ tasks.get(i).schedule();
+ }
+ }
+ });
+ }
+
+ public PoiChunk getPoiChunk() {
+ return this.poiChunk;
+ }
+
+ public ChunkEntitySlices getEntityChunk() {
+ return this.entityChunk;
+ }
+
+ public long lastAutoSave;
+
+ public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {}
+
+ public SaveStat save(final boolean shutdown) {
+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main");
+
+ ChunkAccess chunk = this.getCurrentChunk();
+ PoiChunk poi = this.getPoiChunk();
+ ChunkEntitySlices entities = this.getEntityChunk();
+ boolean executedUnloadTask = false;
+
+ if (shutdown) {
+ // make sure that the async unloads complete
+ if (this.unloadState != null) {
+ // must have errored during unload
+ chunk = this.unloadState.chunk();
+ 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();
+ }
+ }
+ }
+
+ final boolean forceNoSaveChunk = ChunkSystemFeatures.forceNoSave(chunk);
+
+ // can only synchronously save worldgen chunks during shutdown
+ boolean canSaveChunk = !forceNoSaveChunk && (chunk != null && ((shutdown || chunk instanceof LevelChunk) && chunk.isUnsaved()));
+ boolean canSavePOI = !forceNoSaveChunk && (poi != null && poi.isDirty());
+ boolean canSaveEntities = entities != null;
+
+ if (canSaveChunk) {
+ canSaveChunk = this.saveChunk(chunk, false);
+ }
+ if (canSavePOI) {
+ canSavePOI = this.savePOI(poi, false);
+ }
+ if (canSaveEntities) {
+ // on shutdown, we need to force transient entity chunks to save
+ canSaveEntities = this.saveEntities(entities, shutdown);
+ if (shutdown) {
+ this.lastEntityUnload = null;
+ }
+ }
+
+ 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) + "\"}" +
+ "}";
+ }
+ }
+
+ private boolean saveChunk(final ChunkAccess chunk, final boolean unloading) {
+ if (!chunk.isUnsaved()) {
+ if (unloading) {
+ this.completeAsyncUnloadDataSave(RegionFileIOThread.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 PrioritisedExecutor.PrioritisedTask task = this.scheduler.loadExecutor.createTask(new AsyncChunkSerializeTask(this.world, chunk, asyncSaveData, this));
+
+ this.chunkDataUnload.task().setTask(task);
+
+ chunk.setUnsaved(false);
+
+ task.queue();
+
+ 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
+ }
+ }
+
+ final CompoundTag save = ChunkSerializer.write(this.world, chunk);
+
+ 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");
+ }
+ } else {
+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.CHUNK_DATA);
+ }
+ chunk.setUnsaved(false);
+ } 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;
+ }
+
+ private boolean lastEntitySaveNull;
+ private CompoundTag lastEntityUnload;
+ private boolean saveEntities(final ChunkEntitySlices entities, final boolean unloading) {
+ try {
+ CompoundTag mergeFrom = null;
+ if (entities.isTransient()) {
+ if (!unloading) {
+ // if we're a transient chunk, we cannot save until unloading because otherwise a double save will
+ // result in double adding the entities
+ return false;
+ }
+ try {
+ mergeFrom = RegionFileIOThread.loadData(this.world, this.chunkX, this.chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, PrioritisedExecutor.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);
+ }
+ }
+
+ final CompoundTag save = entities.save();
+ if (mergeFrom != null) {
+ if (save == null) {
+ // don't override the data on disk with nothing
+ return false;
+ } else {
+ ChunkEntitySlices.copyEntities(mergeFrom, save);
+ }
+ }
+ if (save == null && this.lastEntitySaveNull) {
+ return false;
+ }
+
+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.ENTITY_DATA);
+ this.lastEntitySaveNull = save == null;
+ if (unloading) {
+ this.lastEntityUnload = save;
+ }
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr);
+ }
+
+ return true;
+ }
+
+ private boolean lastPoiSaveNull;
+ private boolean savePOI(final PoiChunk poi, final boolean unloading) {
+ try {
+ final CompoundTag save = poi.save();
+ poi.setDirty(false);
+ if (save == null && this.lastPoiSaveNull) {
+ if (unloading) {
+ this.poiDataUnload.completable().complete(null);
+ }
+ return false;
+ }
+
+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.POI_DATA);
+ this.lastPoiSaveNull = save == null;
+ if (unloading) {
+ this.poiDataUnload.completable().complete(save);
+ }
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr);
+ }
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ final ChunkCompletion lastCompletion = this.lastChunkCompletion;
+ final ChunkEntitySlices entityChunk = this.entityChunk;
+ final FullChunkStatus pendingFullStatus = this.pendingFullChunkStatus;
+ final FullChunkStatus currentFullStatus = this.currentFullChunkStatus;
+ return "NewChunkHolder{" +
+ "world=" + WorldUtil.getWorldName(this.world) +
+ ", chunkX=" + this.chunkX +
+ ", chunkZ=" + this.chunkZ +
+ ", entityChunkFromDisk=" + (entityChunk != null && !entityChunk.isTransient()) +
+ ", lastChunkCompletion={chunk_class=" + (lastCompletion == null || lastCompletion.chunk() == null ? "null" : lastCompletion.chunk().getClass().getName()) + ",status=" + (lastCompletion == null ? "null" : lastCompletion.genStatus()) + "}" +
+ ", currentGenStatus=" + this.currentGenStatus +
+ ", requestedGenStatus=" + this.requestedGenStatus +
+ ", generationTask=" + this.generationTask +
+ ", generationTaskStatus=" + this.generationTaskStatus +
+ ", priority=" + this.priority +
+ ", priorityLocked=" + this.priorityLocked +
+ ", neighbourRequestedPriority=" + this.neighbourRequestedPriority +
+ ", effective_priority=" + this.getEffectivePriority(null) +
+ ", oldTicketLevel=" + this.oldTicketLevel +
+ ", currentTicketLevel=" + this.currentTicketLevel +
+ ", totalNeighboursUsingThisChunk=" + this.totalNeighboursUsingThisChunk +
+ ", fullNeighbourChunksLoadedBitset=" + this.fullNeighbourChunksLoadedBitset +
+ ", currentChunkStatus=" + currentFullStatus +
+ ", pendingChunkStatus=" + pendingFullStatus +
+ ", is_unload_safe=" + this.isSafeToUnload() +
+ ", killed=" + this.unloaded +
+ '}';
+ }
+
+ private static JsonElement serializeStacktraceElement(final StackTraceElement element) {
+ return element == null ? JsonNull.INSTANCE : new JsonPrimitive(element.toString());
+ }
+
+ private static JsonObject serializeCompletable(final Completable<?> completable) {
+ final JsonObject ret = new JsonObject();
+
+ if (completable == null) {
+ return ret;
+ }
+
+ ret.addProperty("valid", Boolean.TRUE);
+
+ final boolean isCompleted = completable.isCompleted();
+ ret.addProperty("completed", Boolean.valueOf(isCompleted));
+
+ if (isCompleted) {
+ final Throwable throwable = completable.getThrowable();
+ if (throwable != null) {
+ final JsonArray throwableJson = new JsonArray();
+ ret.add("throwable", throwableJson);
+
+ for (final StackTraceElement element : throwable.getStackTrace()) {
+ throwableJson.add(serializeStacktraceElement(element));
+ }
+ } else {
+ final Object result = completable.getResult();
+ ret.add("result_class", result == null ? JsonNull.INSTANCE : new JsonPrimitive(result.getClass().getName()));
+ }
+ }
+
+ return ret;
+ }
+
+ // (probably) holds ticket and scheduling lock
+ public JsonObject getDebugJson() {
+ final JsonObject ret = new JsonObject();
+
+ final ChunkCompletion lastCompletion = this.lastChunkCompletion;
+ final ChunkEntitySlices slices = this.entityChunk;
+ final PoiChunk poiChunk = this.poiChunk;
+
+ ret.addProperty("chunkX", Integer.valueOf(this.chunkX));
+ ret.addProperty("chunkZ", Integer.valueOf(this.chunkZ));
+ ret.addProperty("entity_chunk", slices == null ? "null" : "transient=" + slices.isTransient());
+ ret.addProperty("poi_chunk", "null=" + (poiChunk == null));
+ ret.addProperty("completed_chunk_class", lastCompletion == null ? "null" : lastCompletion.chunk().getClass().getName());
+ ret.addProperty("completed_gen_status", lastCompletion == null ? "null" : lastCompletion.genStatus().toString());
+ ret.addProperty("priority", Objects.toString(this.priority));
+ ret.addProperty("neighbour_requested_priority", Objects.toString(this.neighbourRequestedPriority));
+ ret.addProperty("generation_task", Objects.toString(this.generationTask));
+ ret.addProperty("is_safe_unload", Objects.toString(this.isSafeToUnload()));
+ ret.addProperty("old_ticket_level", Integer.valueOf(this.oldTicketLevel));
+ ret.addProperty("current_ticket_level", Integer.valueOf(this.currentTicketLevel));
+ ret.addProperty("neighbours_using_chunk", Integer.valueOf(this.totalNeighboursUsingThisChunk));
+
+ final JsonObject neighbourWaitState = new JsonObject();
+ ret.add("neighbour_state", neighbourWaitState);
+
+ final JsonArray blockingGenNeighbours = new JsonArray();
+ neighbourWaitState.add("blocking_gen_task", blockingGenNeighbours);
+ for (final NewChunkHolder blockingGenNeighbour : this.neighboursBlockingGenTask) {
+ final JsonObject neighbour = new JsonObject();
+ blockingGenNeighbours.add(neighbour);
+
+ neighbour.addProperty("chunkX", Integer.valueOf(blockingGenNeighbour.chunkX));
+ neighbour.addProperty("chunkZ", Integer.valueOf(blockingGenNeighbour.chunkZ));
+ }
+
+ final JsonArray neighboursWaitingForUs = new JsonArray();
+ neighbourWaitState.add("neighbours_waiting_on_us", neighboursWaitingForUs);
+ for (final Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus> entry : this.neighboursWaitingForUs.reference2ObjectEntrySet()) {
+ final NewChunkHolder holder = entry.getKey();
+ final ChunkStatus status = entry.getValue();
+
+ final JsonObject neighbour = new JsonObject();
+ neighboursWaitingForUs.add(neighbour);
+
+
+ neighbour.addProperty("chunkX", Integer.valueOf(holder.chunkX));
+ neighbour.addProperty("chunkZ", Integer.valueOf(holder.chunkZ));
+ neighbour.addProperty("waiting_for", Objects.toString(status));
+ }
+
+ ret.addProperty("pending_chunk_full_status", Objects.toString(this.pendingFullChunkStatus));
+ ret.addProperty("current_chunk_full_status", Objects.toString(this.currentFullChunkStatus));
+ ret.addProperty("generation_task", Objects.toString(this.generationTask));
+ ret.addProperty("requested_generation", Objects.toString(this.requestedGenStatus));
+ ret.addProperty("has_entity_load_task", Boolean.valueOf(this.entityDataLoadTask != null));
+ ret.addProperty("has_poi_load_task", Boolean.valueOf(this.poiDataLoadTask != null));
+
+ final UnloadTask entityDataUnload = this.entityDataUnload;
+ final UnloadTask poiDataUnload = this.poiDataUnload;
+ final UnloadTask chunkDataUnload = this.chunkDataUnload;
+
+ ret.add("entity_unload_completable", serializeCompletable(entityDataUnload == null ? null : entityDataUnload.completable()));
+ 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();
+ if (unloadTask == null) {
+ ret.addProperty("unload_task_priority", "null");
+ ret.addProperty("unload_task_priority_raw", "null");
+ } else {
+ ret.addProperty("unload_task_priority", Objects.toString(unloadTask.getPriority()));
+ ret.addProperty("unload_task_priority_raw", Integer.valueOf(unloadTask.getPriorityInternal()));
+ }
+
+ ret.addProperty("killed", Boolean.valueOf(this.unloaded));
+
+ return ret;
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..261e09454f49d04eb159c984ec695d7c7aa6a3a8
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java
@@ -0,0 +1,215 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import java.lang.invoke.VarHandle;
+
+public abstract class PriorityHolder {
+
+ protected volatile int priority;
+ protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(PriorityHolder.class, "priority", int.class);
+
+ protected static final int PRIORITY_SCHEDULED = Integer.MIN_VALUE >>> 0;
+ protected static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 1;
+
+ protected final int getPriorityVolatile() {
+ return (int)PRIORITY_HANDLE.getVolatile((PriorityHolder)this);
+ }
+
+ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) {
+ return (int)PRIORITY_HANDLE.compareAndExchange((PriorityHolder)this, (int)expect, (int)update);
+ }
+
+ protected final int getAndOrPriorityVolatile(final int val) {
+ return (int)PRIORITY_HANDLE.getAndBitwiseOr((PriorityHolder)this, (int)val);
+ }
+
+ protected final void setPriorityPlain(final int val) {
+ PRIORITY_HANDLE.set((PriorityHolder)this, (int)val);
+ }
+
+ protected PriorityHolder(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.setPriorityPlain(priority.priority);
+ }
+
+ // used only for debug json
+ public boolean isScheduled() {
+ return (this.getPriorityVolatile() & PRIORITY_SCHEDULED) != 0;
+ }
+
+ // returns false if cancelled
+ public boolean markExecuting() {
+ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0;
+ }
+
+ public boolean isMarkedExecuted() {
+ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0;
+ }
+
+ public void cancel() {
+ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) {
+ // cancelled already
+ return;
+ }
+ this.cancelScheduled();
+ }
+
+ public void schedule() {
+ int priority = this.getPriorityVolatile();
+
+ if ((priority & PRIORITY_SCHEDULED) != 0) {
+ throw new IllegalStateException("schedule() called twice");
+ }
+
+ if ((priority & PRIORITY_EXECUTED) != 0) {
+ // cancelled
+ return;
+ }
+
+ this.scheduleTask(PrioritisedExecutor.Priority.getPriority(priority));
+
+ int failures = 0;
+ for (;;) {
+ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SCHEDULED))) {
+ return;
+ }
+
+ if ((priority & PRIORITY_SCHEDULED) != 0) {
+ throw new IllegalStateException("schedule() called twice");
+ }
+
+ if ((priority & PRIORITY_EXECUTED) != 0) {
+ // cancelled or executed
+ return;
+ }
+
+ this.setPriorityScheduled(PrioritisedExecutor.Priority.getPriority(priority));
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ public final PrioritisedExecutor.Priority getPriority() {
+ final int ret = this.getPriorityVolatile();
+ if ((ret & PRIORITY_EXECUTED) != 0) {
+ return PrioritisedExecutor.Priority.COMPLETING;
+ }
+ if ((ret & PRIORITY_SCHEDULED) != 0) {
+ return this.getScheduledPriority();
+ }
+ return PrioritisedExecutor.Priority.getPriority(ret);
+ }
+
+ public final 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_EXECUTED) != 0) {
+ return;
+ }
+
+ if ((curr & PRIORITY_SCHEDULED) != 0) {
+ this.lowerPriorityScheduled(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 final 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_EXECUTED) != 0) {
+ return;
+ }
+
+ if ((curr & PRIORITY_SCHEDULED) != 0) {
+ this.setPriorityScheduled(priority);
+ return;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
+ return;
+ }
+
+ // failed, retry
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ public final 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_EXECUTED) != 0) {
+ return;
+ }
+
+ if ((curr & PRIORITY_SCHEDULED) != 0) {
+ this.raisePriorityScheduled(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();
+ }
+ }
+ }
+
+ protected abstract void cancelScheduled();
+
+ protected abstract PrioritisedExecutor.Priority getScheduledPriority();
+
+ protected abstract void scheduleTask(final PrioritisedExecutor.Priority priority);
+
+ protected abstract void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority);
+
+ protected abstract void setPriorityScheduled(final PrioritisedExecutor.Priority priority);
+
+ protected abstract void raisePriorityScheduled(final PrioritisedExecutor.Priority priority);
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java
new file mode 100644
index 0000000000000000000000000000000000000000..310a8f80debadd64c2d962ebf83b7d0505ce6e42
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java
@@ -0,0 +1,1457 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
+import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.shorts.Short2ByteLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.shorts.Short2ByteMap;
+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.locks.LockSupport;
+
+public abstract class ThreadedTicketLevelPropagator {
+
+ // sections are 64 in length
+ public static final int SECTION_SHIFT = 6;
+ public static final int SECTION_SIZE = 1 << SECTION_SHIFT;
+ private static final int LEVEL_BITS = SECTION_SHIFT;
+ private static final int LEVEL_COUNT = 1 << LEVEL_BITS;
+ private static final int MIN_SOURCE_LEVEL = 1;
+ // we limit the max source to 62 because the de-propagation code _must_ attempt to de-propagate
+ // a 1 level to 0; and if a source was 63 then it may cross more than 2 sections in de-propagation
+ private static final int MAX_SOURCE_LEVEL = 62;
+
+ private static int getMaxSchedulingRadius() {
+ return 2 * ChunkTaskScheduler.getMaxAccessRadius();
+ }
+
+ private final UpdateQueue updateQueue;
+ private final ConcurrentLong2ReferenceChainedHashTable<Section> sections;
+
+ public ThreadedTicketLevelPropagator() {
+ this.updateQueue = new UpdateQueue();
+ this.sections = new ConcurrentLong2ReferenceChainedHashTable<>();
+ }
+
+ // must hold ticket lock for:
+ // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1))
+ public void setSource(final int posX, final int posZ, final int to) {
+ if (to < 1 || to > MAX_SOURCE_LEVEL) {
+ throw new IllegalArgumentException("Source: " + to);
+ }
+
+ final int sectionX = posX >> SECTION_SHIFT;
+ final int sectionZ = posZ >> SECTION_SHIFT;
+
+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ Section section = this.sections.get(coordinate);
+ if (section == null) {
+ if (null != this.sections.putIfAbsent(coordinate, section = new Section(sectionX, sectionZ))) {
+ throw new IllegalStateException("Race condition while creating new section");
+ }
+ }
+
+ final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
+ final short sLocalIdx = (short)localIdx;
+
+ final short sourceAndLevel = section.levels[localIdx];
+ final int currentSource = (sourceAndLevel >>> 8) & 0xFF;
+
+ if (currentSource == to) {
+ // nothing to do
+ // make sure to kill the current update, if any
+ section.queuedSources.replace(sLocalIdx, (byte)to);
+ return;
+ }
+
+ if (section.queuedSources.put(sLocalIdx, (byte)to) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) {
+ this.queueSectionUpdate(section);
+ }
+ }
+
+ // must hold ticket lock for:
+ // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1))
+ public void removeSource(final int posX, final int posZ) {
+ final int sectionX = posX >> SECTION_SHIFT;
+ final int sectionZ = posZ >> SECTION_SHIFT;
+
+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ final Section section = this.sections.get(coordinate);
+
+ if (section == null) {
+ return;
+ }
+
+ final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
+ final short sLocalIdx = (short)localIdx;
+
+ final int currentSource = (section.levels[localIdx] >>> 8) & 0xFF;
+
+ if (currentSource == 0) {
+ // we use replace here so that we do not possibly multi-queue a section for an update
+ section.queuedSources.replace(sLocalIdx, (byte)0);
+ return;
+ }
+
+ if (section.queuedSources.put(sLocalIdx, (byte)0) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) {
+ this.queueSectionUpdate(section);
+ }
+ }
+
+ private void queueSectionUpdate(final Section section) {
+ this.updateQueue.append(new UpdateQueue.UpdateQueueNode(section, null));
+ }
+
+ public boolean hasPendingUpdates() {
+ return !this.updateQueue.isEmpty();
+ }
+
+ // holds ticket lock for every chunk section represented by any position in the key set
+ // updates is modifiable and passed to processSchedulingUpdates after this call
+ protected abstract void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates);
+
+ // holds ticket lock for every chunk section represented by any position in the key set
+ // holds scheduling lock in max access radius for every position held by the ticket lock
+ // updates is cleared after this call
+ protected abstract void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List<ChunkProgressionTask> scheduledTasks,
+ final List<NewChunkHolder> changedFullStatus);
+
+ // must hold ticket lock for every position in the sections in one radius around sectionX,sectionZ
+ public boolean performUpdate(final int sectionX, final int sectionZ, final ReentrantAreaLock schedulingLock,
+ final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedFullStatus) {
+ if (!this.hasPendingUpdates()) {
+ return false;
+ }
+
+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ final Section section = this.sections.get(coordinate);
+
+ if (section == null || section.queuedSources.isEmpty()) {
+ // no section or no updates
+ return false;
+ }
+
+ final Propagator propagator = Propagator.acquirePropagator();
+ final boolean ret = this.performUpdate(section, null, propagator,
+ null, schedulingLock, scheduledTasks, changedFullStatus
+ );
+ Propagator.returnPropagator(propagator);
+ return ret;
+ }
+
+ private boolean performUpdate(final Section section, final UpdateQueue.UpdateQueueNode node, final Propagator propagator,
+ final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock,
+ final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedFullStatus) {
+ final int sectionX = section.sectionX;
+ final int sectionZ = section.sectionZ;
+
+ final int rad1MinX = (sectionX - 1) << SECTION_SHIFT;
+ final int rad1MinZ = (sectionZ - 1) << SECTION_SHIFT;
+ final int rad1MaxX = ((sectionX + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1);
+ final int rad1MaxZ = ((sectionZ + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1);
+
+ // set up encode offset first as we need to queue level changes _before_
+ propagator.setupEncodeOffset(sectionX, sectionZ);
+
+ final int coordinateOffset = propagator.coordinateOffset;
+
+ final ReentrantAreaLock.Node ticketNode = ticketLock == null ? null : ticketLock.lock(rad1MinX, rad1MinZ, rad1MaxX, rad1MaxZ);
+ final boolean ret;
+ try {
+ // first, check if this update was stolen
+ if (section != this.sections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ))) {
+ // occurs when a stolen update deletes this section
+ // it is possible that another update is scheduled, but that one will have the correct section
+ if (node != null) {
+ this.updateQueue.remove(node);
+ }
+ return false;
+ }
+
+ final int oldSourceSize = section.sources.size();
+
+ // process pending sources
+ for (final Iterator<Short2ByteMap.Entry> iterator = section.queuedSources.short2ByteEntrySet().fastIterator(); iterator.hasNext();) {
+ final Short2ByteMap.Entry entry = iterator.next();
+ final int pos = (int)entry.getShortKey();
+ final int posX = (pos & (SECTION_SIZE - 1)) | (sectionX << SECTION_SHIFT);
+ final int posZ = ((pos >> SECTION_SHIFT) & (SECTION_SIZE - 1)) | (sectionZ << SECTION_SHIFT);
+ final int newSource = (int)entry.getByteValue();
+
+ final short currentEncoded = section.levels[pos];
+ final int currLevel = currentEncoded & 0xFF;
+ final int prevSource = (currentEncoded >>> 8) & 0xFF;
+
+ if (prevSource == newSource) {
+ // nothing changed
+ continue;
+ }
+
+ if ((prevSource < currLevel && newSource <= currLevel) || newSource == currLevel) {
+ // just update the source, don't need to propagate change
+ section.levels[pos] = (short)(currLevel | (newSource << 8));
+ // level is unchanged, don't add to changed positions
+ } else {
+ // set current level and current source to new source
+ section.levels[pos] = (short)(newSource | (newSource << 8));
+ // must add to updated positions in case this is final
+ propagator.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)newSource);
+ if (newSource != 0) {
+ // queue increase with new source level
+ propagator.appendToIncreaseQueue(
+ ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) |
+ ((newSource & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) |
+ (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS))
+ );
+ }
+ // queue decrease with previous level
+ if (newSource < currLevel) {
+ propagator.appendToDecreaseQueue(
+ ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) |
+ ((currLevel & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) |
+ (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS))
+ );
+ }
+ }
+
+ if (newSource == 0) {
+ // prevSource != newSource, so we are removing this source
+ section.sources.remove((short)pos);
+ } else if (prevSource == 0) {
+ // prevSource != newSource, so we are adding this source
+ section.sources.add((short)pos);
+ }
+ }
+
+ section.queuedSources.clear();
+
+ final int newSourceSize = section.sources.size();
+
+ if (oldSourceSize == 0 && newSourceSize != 0) {
+ // need to make sure the sections in 1 radius are initialised
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ if ((dx | dz) == 0) {
+ continue;
+ }
+ final int offX = dx + sectionX;
+ final int offZ = dz + sectionZ;
+ final long coordinate = CoordinateUtils.getChunkKey(offX, offZ);
+ final Section neighbour = this.sections.computeIfAbsent(coordinate, (final long keyInMap) -> {
+ return new Section(CoordinateUtils.getChunkX(keyInMap), CoordinateUtils.getChunkZ(keyInMap));
+ });
+
+ // increase ref count
+ ++neighbour.oneRadNeighboursWithSources;
+ if (neighbour.oneRadNeighboursWithSources <= 0 || neighbour.oneRadNeighboursWithSources > 8) {
+ throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources));
+ }
+ }
+ }
+ }
+
+ if (propagator.hasUpdates()) {
+ propagator.setupCaches(this, sectionX, sectionZ, 1);
+ propagator.performDecrease();
+ // don't need try-finally, as any exception will cause the propagator to not be returned
+ propagator.destroyCaches();
+ }
+
+ if (newSourceSize == 0) {
+ final boolean decrementRef = oldSourceSize != 0;
+ // check for section de-init
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ final int offX = dx + sectionX;
+ final int offZ = dz + sectionZ;
+ final long coordinate = CoordinateUtils.getChunkKey(offX, offZ);
+ final Section neighbour = this.sections.get(coordinate);
+
+ if (neighbour == null) {
+ if (oldSourceSize == 0 && (dx | dz) != 0) {
+ // since we don't have sources, this section is allowed to be null
+ continue;
+ }
+ throw new IllegalStateException("??");
+ }
+
+ if (decrementRef && (dx | dz) != 0) {
+ // decrease ref count, but only for neighbours
+ --neighbour.oneRadNeighboursWithSources;
+ }
+
+ // we need to check the current section for de-init as well
+ if (neighbour.oneRadNeighboursWithSources == 0) {
+ if (neighbour.queuedSources.isEmpty() && neighbour.sources.isEmpty()) {
+ // need to de-init
+ this.sections.remove(coordinate);
+ } // else: neighbour is queued for an update, and it will de-init itself
+ } else if (neighbour.oneRadNeighboursWithSources < 0 || neighbour.oneRadNeighboursWithSources > 8) {
+ throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources));
+ }
+ }
+ }
+ }
+
+
+ ret = !propagator.updatedPositions.isEmpty();
+
+ if (ret) {
+ this.processLevelUpdates(propagator.updatedPositions);
+
+ if (!propagator.updatedPositions.isEmpty()) {
+ // now we can actually update the ticket levels in the chunk holders
+ final int maxScheduleRadius = getMaxSchedulingRadius();
+
+ // allow the chunkholders to process ticket level updates without needing to acquire the schedule lock every time
+ final ReentrantAreaLock.Node schedulingNode = schedulingLock.lock(
+ rad1MinX - maxScheduleRadius, rad1MinZ - maxScheduleRadius,
+ rad1MaxX + maxScheduleRadius, rad1MaxZ + maxScheduleRadius
+ );
+ try {
+ this.processSchedulingUpdates(propagator.updatedPositions, scheduledTasks, changedFullStatus);
+ } finally {
+ schedulingLock.unlock(schedulingNode);
+ }
+ }
+
+ propagator.updatedPositions.clear();
+ }
+ } finally {
+ if (ticketLock != null) {
+ ticketLock.unlock(ticketNode);
+ }
+ }
+
+ // finished
+ if (node != null) {
+ this.updateQueue.remove(node);
+ }
+
+ return ret;
+ }
+
+ public boolean performUpdates(final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock,
+ final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedFullStatus) {
+ if (this.updateQueue.isEmpty()) {
+ return false;
+ }
+
+ final long maxOrder = this.updateQueue.getLastOrder();
+
+ boolean updated = false;
+ Propagator propagator = null;
+
+ for (;;) {
+ final UpdateQueue.UpdateQueueNode toUpdate = this.updateQueue.acquireNextOrWait(maxOrder);
+ if (toUpdate == null) {
+ if (!this.updateQueue.hasRemainingUpdates(maxOrder)) {
+ if (propagator != null) {
+ Propagator.returnPropagator(propagator);
+ }
+ return updated;
+ }
+
+ continue;
+ }
+
+ if (propagator == null) {
+ propagator = Propagator.acquirePropagator();
+ }
+
+ updated |= this.performUpdate(toUpdate.section, toUpdate, propagator, ticketLock, schedulingLock, scheduledTasks, changedFullStatus);
+ }
+ }
+
+ // Similar implementation of concurrent FIFO queue (See MTQ in ConcurrentUtil) which has an additional node pointer
+ // for the last update node being handled
+ private static final class UpdateQueue {
+
+ private volatile UpdateQueueNode head;
+ private volatile UpdateQueueNode tail;
+
+ private static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "head", UpdateQueueNode.class);
+ private static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "tail", UpdateQueueNode.class);
+
+ /* head */
+
+ private final void setHeadPlain(final UpdateQueueNode newHead) {
+ HEAD_HANDLE.set(this, newHead);
+ }
+
+ private final void setHeadOpaque(final UpdateQueueNode newHead) {
+ HEAD_HANDLE.setOpaque(this, newHead);
+ }
+
+ private final UpdateQueueNode getHeadPlain() {
+ return (UpdateQueueNode)HEAD_HANDLE.get(this);
+ }
+
+ private final UpdateQueueNode getHeadOpaque() {
+ return (UpdateQueueNode)HEAD_HANDLE.getOpaque(this);
+ }
+
+ private final UpdateQueueNode getHeadAcquire() {
+ return (UpdateQueueNode)HEAD_HANDLE.getAcquire(this);
+ }
+
+ /* tail */
+
+ private final void setTailPlain(final UpdateQueueNode newTail) {
+ TAIL_HANDLE.set(this, newTail);
+ }
+
+ private final void setTailOpaque(final UpdateQueueNode newTail) {
+ TAIL_HANDLE.setOpaque(this, newTail);
+ }
+
+ private final UpdateQueueNode getTailPlain() {
+ return (UpdateQueueNode)TAIL_HANDLE.get(this);
+ }
+
+ private final UpdateQueueNode getTailOpaque() {
+ return (UpdateQueueNode)TAIL_HANDLE.getOpaque(this);
+ }
+
+ public UpdateQueue() {
+ final UpdateQueueNode dummy = new UpdateQueueNode(null, null);
+ dummy.order = -1L;
+ dummy.preventAdds();
+
+ this.setHeadPlain(dummy);
+ this.setTailPlain(dummy);
+ }
+
+ public boolean isEmpty() {
+ return this.peek() == null;
+ }
+
+ public boolean hasRemainingUpdates(final long maxUpdate) {
+ final UpdateQueueNode node = this.peek();
+ return node != null && node.order <= maxUpdate;
+ }
+
+ public long getLastOrder() {
+ for (UpdateQueueNode tail = this.getTailOpaque(), curr = tail;;) {
+ final UpdateQueueNode next = curr.getNextVolatile();
+ if (next == null) {
+ // try to update stale tail
+ if (this.getTailOpaque() == tail && curr != tail) {
+ this.setTailOpaque(curr);
+ }
+ return curr.order;
+ }
+ curr = next;
+ }
+ }
+
+ private static void await(final UpdateQueueNode node) {
+ final Thread currThread = Thread.currentThread();
+ // we do not use add-blocking because we use the nullability of the section to block
+ // remove() does not begin to poll from the wait queue until the section is null'd,
+ // and so provided we check the nullability before parking there is no ordering of these operations
+ // such that remove() finishes polling from the wait queue while section is not null
+ node.add(currThread);
+
+ // wait until completed
+ while (node.getSectionVolatile() != null) {
+ LockSupport.park();
+ }
+ }
+
+ public UpdateQueueNode acquireNextOrWait(final long maxOrder) {
+ final List<UpdateQueueNode> blocking = new ArrayList<>();
+
+ node_search:
+ for (UpdateQueueNode curr = this.peek(); curr != null && curr.order <= maxOrder; curr = curr.getNextVolatile()) {
+ if (curr.getSectionVolatile() == null) {
+ continue;
+ }
+
+ if (curr.getUpdatingVolatile()) {
+ blocking.add(curr);
+ continue;
+ }
+
+ for (int i = 0, len = blocking.size(); i < len; ++i) {
+ final UpdateQueueNode node = blocking.get(i);
+
+ if (node.intersects(curr)) {
+ continue node_search;
+ }
+ }
+
+ if (curr.getAndSetUpdatingVolatile(true)) {
+ blocking.add(curr);
+ continue;
+ }
+
+ return curr;
+ }
+
+ if (!blocking.isEmpty()) {
+ await(blocking.get(0));
+ }
+
+ return null;
+ }
+
+ public UpdateQueueNode peek() {
+ for (UpdateQueueNode head = this.getHeadOpaque(), curr = head;;) {
+ final UpdateQueueNode next = curr.getNextVolatile();
+ final Section element = curr.getSectionVolatile(); /* Likely in sync */
+
+ if (element != null) {
+ if (this.getHeadOpaque() == head && curr != head) {
+ this.setHeadOpaque(curr);
+ }
+ return curr;
+ }
+
+ if (next == null) {
+ if (this.getHeadOpaque() == head && curr != head) {
+ this.setHeadOpaque(curr);
+ }
+ return null;
+ }
+ curr = next;
+ }
+ }
+
+ public void remove(final UpdateQueueNode node) {
+ // mark as removed
+ node.setSectionVolatile(null);
+
+ // use peek to advance head
+ this.peek();
+
+ // unpark any waiters / block the wait queue
+ Thread unpark;
+ while ((unpark = node.poll()) != null) {
+ LockSupport.unpark(unpark);
+ }
+ }
+
+ public void append(final UpdateQueueNode node) {
+ int failures = 0;
+
+ for (UpdateQueueNode currTail = this.getTailOpaque(), curr = currTail;;) {
+ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */
+ /* It is likely due to a cache miss caused by another write to the next field */
+ final UpdateQueueNode next = curr.getNextVolatile();
+
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (next == null) {
+ node.order = curr.order + 1L;
+ final UpdateQueueNode compared = curr.compareExchangeNextVolatile(null, node);
+
+ if (compared == null) {
+ /* Added */
+ /* Avoid CASing on tail more than we need to */
+ /* CAS to avoid setting an out-of-date tail */
+ if (this.getTailOpaque() == currTail) {
+ this.setTailOpaque(node);
+ }
+ return;
+ }
+
+ ++failures;
+ curr = compared;
+ continue;
+ }
+
+ if (curr == currTail) {
+ /* Tail is likely not up-to-date */
+ curr = next;
+ } else {
+ /* Try to update to tail */
+ if (currTail == (currTail = this.getTailOpaque())) {
+ curr = next;
+ } else {
+ curr = currTail;
+ }
+ }
+ }
+ }
+
+ // each node also represents a set of waiters, represented by the MTQ
+ // if the queue is add-blocked, then the update is complete
+ private static final class UpdateQueueNode extends MultiThreadedQueue<Thread> {
+ private final int sectionX;
+ private final int sectionZ;
+
+ private long order;
+ private volatile Section section;
+ private volatile UpdateQueueNode next;
+ private volatile boolean updating;
+
+ private static final VarHandle SECTION_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "section", Section.class);
+ private static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "next", UpdateQueueNode.class);
+ private static final VarHandle UPDATING_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "updating", boolean.class);
+
+ public UpdateQueueNode(final Section section, final UpdateQueueNode next) {
+ if (section == null) {
+ this.sectionX = this.sectionZ = 0;
+ } else {
+ this.sectionX = section.sectionX;
+ this.sectionZ = section.sectionZ;
+ }
+
+ SECTION_HANDLE.set(this, section);
+ NEXT_HANDLE.set(this, next);
+ }
+
+ public boolean intersects(final UpdateQueueNode other) {
+ final int dist = Math.max(Math.abs(this.sectionX - other.sectionX), Math.abs(this.sectionZ - other.sectionZ));
+
+ // intersection radius is ticket update radius (1) + scheduling radius
+ return dist <= (1 + ((getMaxSchedulingRadius() + (SECTION_SIZE - 1)) >> SECTION_SHIFT));
+ }
+
+ /* section */
+
+ private final Section getSectionPlain() {
+ return (Section)SECTION_HANDLE.get(this);
+ }
+
+ private final Section getSectionVolatile() {
+ return (Section)SECTION_HANDLE.getVolatile(this);
+ }
+
+ private final void setSectionPlain(final Section update) {
+ SECTION_HANDLE.set(this, update);
+ }
+
+ private final void setSectionOpaque(final Section update) {
+ SECTION_HANDLE.setOpaque(this, update);
+ }
+
+ private final void setSectionVolatile(final Section update) {
+ SECTION_HANDLE.setVolatile(this, update);
+ }
+
+ private final Section getAndSetSectionVolatile(final Section update) {
+ return (Section)SECTION_HANDLE.getAndSet(this, update);
+ }
+
+ private final Section compareExchangeSectionVolatile(final Section expect, final Section update) {
+ return (Section)SECTION_HANDLE.compareAndExchange(this, expect, update);
+ }
+
+ /* next */
+
+ private final UpdateQueueNode getNextPlain() {
+ return (UpdateQueueNode)NEXT_HANDLE.get(this);
+ }
+
+ private final UpdateQueueNode getNextOpaque() {
+ return (UpdateQueueNode)NEXT_HANDLE.getOpaque(this);
+ }
+
+ private final UpdateQueueNode getNextAcquire() {
+ return (UpdateQueueNode)NEXT_HANDLE.getAcquire(this);
+ }
+
+ private final UpdateQueueNode getNextVolatile() {
+ return (UpdateQueueNode)NEXT_HANDLE.getVolatile(this);
+ }
+
+ private final void setNextPlain(final UpdateQueueNode next) {
+ NEXT_HANDLE.set(this, next);
+ }
+
+ private final void setNextVolatile(final UpdateQueueNode next) {
+ NEXT_HANDLE.setVolatile(this, next);
+ }
+
+ private final UpdateQueueNode compareExchangeNextVolatile(final UpdateQueueNode expect, final UpdateQueueNode set) {
+ return (UpdateQueueNode)NEXT_HANDLE.compareAndExchange(this, expect, set);
+ }
+
+ /* updating */
+
+ private final boolean getUpdatingVolatile() {
+ return (boolean)UPDATING_HANDLE.getVolatile(this);
+ }
+
+ private final boolean getAndSetUpdatingVolatile(final boolean value) {
+ return (boolean)UPDATING_HANDLE.getAndSet(this, value);
+ }
+ }
+ }
+
+ private static final class Section {
+
+ // upper 8 bits: sources, lower 8 bits: level
+ // if we REALLY wanted to get crazy, we could make the increase propagator use MethodHandles#byteArrayViewVarHandle
+ // to read and write the lower 8 bits of this array directly rather than reading, updating the bits, then writing back.
+ private final short[] levels = new short[SECTION_SIZE * SECTION_SIZE];
+ // set of local positions that represent sources
+ private final ShortOpenHashSet sources = new ShortOpenHashSet();
+ // map of local index to new source level
+ // the source level _cannot_ be updated in the backing storage immediately since the update
+ private static final byte NO_QUEUED_UPDATE = (byte)-1;
+ private final Short2ByteLinkedOpenHashMap queuedSources = new Short2ByteLinkedOpenHashMap();
+ {
+ this.queuedSources.defaultReturnValue(NO_QUEUED_UPDATE);
+ }
+ private int oneRadNeighboursWithSources = 0;
+
+ public final int sectionX;
+ public final int sectionZ;
+
+ public Section(final int sectionX, final int sectionZ) {
+ this.sectionX = sectionX;
+ this.sectionZ = sectionZ;
+ }
+
+ public boolean isZero() {
+ for (final short val : this.levels) {
+ if (val != 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder ret = new StringBuilder();
+
+ for (int x = 0; x < SECTION_SIZE; ++x) {
+ ret.append("levels x=").append(x).append("\n");
+ for (int z = 0; z < SECTION_SIZE; ++z) {
+ final short v = this.levels[x | (z << SECTION_SHIFT)];
+ ret.append(v & 0xFF).append(".");
+ }
+ ret.append("\n");
+ ret.append("sources x=").append(x).append("\n");
+ for (int z = 0; z < SECTION_SIZE; ++z) {
+ final short v = this.levels[x | (z << SECTION_SHIFT)];
+ ret.append((v >>> 8) & 0xFF).append(".");
+ }
+ ret.append("\n\n");
+ }
+
+ return ret.toString();
+ }
+ }
+
+
+ private static final class Propagator {
+
+ private static final ArrayDeque<Propagator> CACHED_PROPAGATORS = new ArrayDeque<>();
+ private static final int MAX_PROPAGATORS = Runtime.getRuntime().availableProcessors() * 2;
+
+ private static Propagator acquirePropagator() {
+ synchronized (CACHED_PROPAGATORS) {
+ final Propagator ret = CACHED_PROPAGATORS.pollFirst();
+ if (ret != null) {
+ return ret;
+ }
+ }
+ return new Propagator();
+ }
+
+ private static void returnPropagator(final Propagator propagator) {
+ synchronized (CACHED_PROPAGATORS) {
+ if (CACHED_PROPAGATORS.size() < MAX_PROPAGATORS) {
+ CACHED_PROPAGATORS.add(propagator);
+ }
+ }
+ }
+
+ private static final int SECTION_RADIUS = 2;
+ private static final int SECTION_CACHE_WIDTH = 2 * SECTION_RADIUS + 1;
+ // minimum number of bits to represent [0, SECTION_SIZE * SECTION_CACHE_WIDTH)
+ private static final int COORDINATE_BITS = 9;
+ private static final int COORDINATE_SIZE = 1 << COORDINATE_BITS;
+ static {
+ if ((SECTION_SIZE * SECTION_CACHE_WIDTH) > (1 << COORDINATE_BITS)) {
+ throw new IllegalStateException("Adjust COORDINATE_BITS");
+ }
+ }
+ // index = x + (z * SECTION_CACHE_WIDTH)
+ // (this requires x >= 0 and z >= 0)
+ private final Section[] sections = new Section[SECTION_CACHE_WIDTH * SECTION_CACHE_WIDTH];
+
+ private int encodeOffsetX;
+ private int encodeOffsetZ;
+
+ private int coordinateOffset;
+
+ private int encodeSectionOffsetX;
+ private int encodeSectionOffsetZ;
+
+ private int sectionIndexOffset;
+
+ public final boolean hasUpdates() {
+ return this.decreaseQueueInitialLength != 0 || this.increaseQueueInitialLength != 0;
+ }
+
+ private final void setupEncodeOffset(final int centerSectionX, final int centerSectionZ) {
+ final int maxCoordinate = (SECTION_RADIUS * SECTION_SIZE - 1);
+ // must have that encoded >= 0
+ // coordinates can range from [-maxCoordinate + centerSection*SECTION_SIZE, maxCoordinate + centerSection*SECTION_SIZE]
+ // we want a range of [0, maxCoordinate*2]
+ // so, 0 = -maxCoordinate + centerSection*SECTION_SIZE + offset
+ this.encodeOffsetX = maxCoordinate - (centerSectionX << SECTION_SHIFT);
+ this.encodeOffsetZ = maxCoordinate - (centerSectionZ << SECTION_SHIFT);
+
+ // encoded coordinates range from [0, SECTION_SIZE * SECTION_CACHE_WIDTH)
+ // coordinate index = (x + encodeOffsetX) + ((z + encodeOffsetZ) << COORDINATE_BITS)
+ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << COORDINATE_BITS);
+
+ // need encoded values to be >= 0
+ // so, 0 = (-SECTION_RADIUS + centerSectionX) + encodeOffset
+ this.encodeSectionOffsetX = SECTION_RADIUS - centerSectionX;
+ this.encodeSectionOffsetZ = SECTION_RADIUS - centerSectionZ;
+
+ // section index = (secX + encodeSectionOffsetX) + ((secZ + encodeSectionOffsetZ) * SECTION_CACHE_WIDTH)
+ this.sectionIndexOffset = this.encodeSectionOffsetX + (this.encodeSectionOffsetZ * SECTION_CACHE_WIDTH);
+ }
+
+ // must hold ticket lock for (centerSectionX,centerSectionZ) in radius rad
+ // must call setupEncodeOffset
+ private final void setupCaches(final ThreadedTicketLevelPropagator propagator,
+ final int centerSectionX, final int centerSectionZ,
+ final int rad) {
+ for (int dz = -rad; dz <= rad; ++dz) {
+ for (int dx = -rad; dx <= rad; ++dx) {
+ final int sectionX = centerSectionX + dx;
+ final int sectionZ = centerSectionZ + dz;
+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ final Section section = propagator.sections.get(coordinate);
+
+ if (section == null) {
+ throw new IllegalStateException("Section at " + coordinate + " should not be null");
+ }
+
+ this.setSectionInCache(sectionX, sectionZ, section);
+ }
+ }
+ }
+
+ private final void setSectionInCache(final int sectionX, final int sectionZ, final Section section) {
+ this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset] = section;
+ }
+
+ private final Section getSection(final int sectionX, final int sectionZ) {
+ return this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset];
+ }
+
+ private final int getLevel(final int posX, final int posZ) {
+ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset];
+ if (section != null) {
+ return (int)section.levels[(posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT)] & 0xFF;
+ }
+
+ return 0;
+ }
+
+ private final void setLevel(final int posX, final int posZ, final int to) {
+ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset];
+ if (section != null) {
+ final int index = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
+ final short level = section.levels[index];
+ section.levels[index] = (short)((level & ~0xFF) | (to & 0xFF));
+ this.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)to);
+ }
+ }
+
+ private final void destroyCaches() {
+ Arrays.fill(this.sections, null);
+ }
+
+ // contains:
+ // lower (COORDINATE_BITS(9) + COORDINATE_BITS(9) = 18) bits encoded position: (x | (z << COORDINATE_BITS))
+ // next LEVEL_BITS (6) bits: propagated level [0, 63]
+ // propagation directions bitset (16 bits):
+ private static final long ALL_DIRECTIONS_BITSET = (
+ // z = -1
+ (1L << ((1 - 1) | ((1 - 1) << 2))) |
+ (1L << ((1 + 0) | ((1 - 1) << 2))) |
+ (1L << ((1 + 1) | ((1 - 1) << 2))) |
+
+ // z = 0
+ (1L << ((1 - 1) | ((1 + 0) << 2))) |
+ //(1L << ((1 + 0) | ((1 + 0) << 2))) | // exclude (0,0)
+ (1L << ((1 + 1) | ((1 + 0) << 2))) |
+
+ // z = 1
+ (1L << ((1 - 1) | ((1 + 1) << 2))) |
+ (1L << ((1 + 0) | ((1 + 1) << 2))) |
+ (1L << ((1 + 1) | ((1 + 1) << 2)))
+ );
+
+ private void ex(int bitset) {
+ for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) {
+ final int set = Integer.numberOfTrailingZeros(bitset);
+ final int tailingBit = (-bitset) & bitset;
+ // XOR to remove the trailing bit
+ bitset ^= tailingBit;
+
+ // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits
+ // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the
+ // index of the set bit is the encoded value
+ // the encoded coordinate has 3 valid states:
+ // 0b00 (0) -> -1
+ // 0b01 (1) -> 0
+ // 0b10 (2) -> 1
+ // the decode operation then is val - 1, and the encode operation is val + 1
+ final int xOff = (set & 3) - 1;
+ final int zOff = ((set >>> 2) & 3) - 1;
+ System.out.println("Encoded: (" + xOff + "," + zOff + ")");
+ }
+ }
+
+ private void ch(long bs, int shift) {
+ int bitset = (int)(bs >>> shift);
+ for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) {
+ final int set = Integer.numberOfTrailingZeros(bitset);
+ final int tailingBit = (-bitset) & bitset;
+ // XOR to remove the trailing bit
+ bitset ^= tailingBit;
+
+ // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits
+ // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the
+ // index of the set bit is the encoded value
+ // the encoded coordinate has 3 valid states:
+ // 0b00 (0) -> -1
+ // 0b01 (1) -> 0
+ // 0b10 (2) -> 1
+ // the decode operation then is val - 1, and the encode operation is val + 1
+ final int xOff = (set & 3) - 1;
+ final int zOff = ((set >>> 2) & 3) - 1;
+ if (Math.abs(xOff) > 1 || Math.abs(zOff) > 1 || (xOff | zOff) == 0) {
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading
+ // updates for sources
+ private static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 1;
+ // whether the propagation needs to check if its current level is equal to the expected level
+ // used only in increase propagation
+ private static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 0;
+
+ private long[] increaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2];
+ private int increaseQueueInitialLength;
+ private long[] decreaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2];
+ private int decreaseQueueInitialLength;
+
+ private final Long2ByteLinkedOpenHashMap updatedPositions = new Long2ByteLinkedOpenHashMap();
+
+ private final long[] resizeIncreaseQueue() {
+ return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2);
+ }
+
+ private final long[] resizeDecreaseQueue() {
+ return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2);
+ }
+
+ private final void appendToIncreaseQueue(final long value) {
+ final int idx = this.increaseQueueInitialLength++;
+ long[] queue = this.increaseQueue;
+ if (idx >= queue.length) {
+ queue = this.resizeIncreaseQueue();
+ queue[idx] = value;
+ return;
+ } else {
+ queue[idx] = value;
+ return;
+ }
+ }
+
+ private final void appendToDecreaseQueue(final long value) {
+ final int idx = this.decreaseQueueInitialLength++;
+ long[] queue = this.decreaseQueue;
+ if (idx >= queue.length) {
+ queue = this.resizeDecreaseQueue();
+ queue[idx] = value;
+ return;
+ } else {
+ queue[idx] = value;
+ return;
+ }
+ }
+
+ private final void performIncrease() {
+ long[] queue = this.increaseQueue;
+ int queueReadIndex = 0;
+ int queueLength = this.increaseQueueInitialLength;
+ this.increaseQueueInitialLength = 0;
+ final int decodeOffsetX = -this.encodeOffsetX;
+ final int decodeOffsetZ = -this.encodeOffsetZ;
+ final int encodeOffset = this.coordinateOffset;
+ final int sectionOffset = this.sectionIndexOffset;
+
+ final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions;
+
+ while (queueReadIndex < queueLength) {
+ final long queueValue = queue[queueReadIndex++];
+
+ final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX;
+ final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ;
+ final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1);
+ // note: the above code requires coordinate bits * 2 < 32
+ // bitset is 16 bits
+ int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1);
+
+ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) {
+ if (this.getLevel(posX, posZ) != propagatedLevel) {
+ // not at the level we expect, so something changed.
+ continue;
+ }
+ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) {
+ // these are used to restore sources after a propagation decrease
+ this.setLevel(posX, posZ, propagatedLevel);
+ }
+
+ // this bitset represents the values that we have not propagated to
+ // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases
+ // significantly reducing the total number of ops
+ // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need
+ // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead
+ // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits)
+ // to make things easy, we use positions [0, 4] in the bitset, with current position being 2
+ // index = x | (z << 3)
+
+ // to start, we eliminate everything 1 radius from the current position as the previous propagator
+ // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius
+ // but the rest not propagated are already handled
+ long currentPropagation = ~(
+ // z = -1
+ (1L << ((2 - 1) | ((2 - 1) << 3))) |
+ (1L << ((2 + 0) | ((2 - 1) << 3))) |
+ (1L << ((2 + 1) | ((2 - 1) << 3))) |
+
+ // z = 0
+ (1L << ((2 - 1) | ((2 + 0) << 3))) |
+ (1L << ((2 + 0) | ((2 + 0) << 3))) |
+ (1L << ((2 + 1) | ((2 + 0) << 3))) |
+
+ // z = 1
+ (1L << ((2 - 1) | ((2 + 1) << 3))) |
+ (1L << ((2 + 0) | ((2 + 1) << 3))) |
+ (1L << ((2 + 1) | ((2 + 1) << 3)))
+ );
+
+ final int toPropagate = propagatedLevel - 1;
+
+ // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting
+ // the bits, the cpu loop predictor should perfectly predict the loop.
+ for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) {
+ final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset);
+ final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset;
+ propagateDirectionBitset ^= tailingBit;
+
+ // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset
+ // it has been split to save some cycles via parallelism
+ final int pDecodeX = (set & 3);
+ final int pDecodeZ = ((set >>> 2) & 3);
+
+ // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX
+ final int offX = (posX - 1) + pDecodeX;
+ final int offZ = (posZ - 1) + pDecodeZ;
+
+ final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset;
+ final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
+
+ // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset
+ // bitset idx = x | (z << 3)
+
+ // read three bits, so we need 7L
+ // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1
+ // nstartidx1 = x rel -1 for z rel -1
+ // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3)
+ // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3)
+ // = pDecodeX | (pDecodeZ << 3) = start
+ final int start = pDecodeX | (pDecodeZ << 3);
+ final long bitsetLine1 = currentPropagation & (7L << (start));
+
+ // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset)
+ final long bitsetLine2 = currentPropagation & (7L << (start + 8));
+
+ // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset)
+ final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8)));
+
+ // remove ("take") lines from bitset
+ currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3);
+
+ // now try to propagate
+ final Section section = this.sections[sectionIndex];
+
+ // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag
+ final short currentStoredLevel = section.levels[localIndex];
+ final int currentLevel = currentStoredLevel & 0xFF;
+
+ if (currentLevel >= toPropagate) {
+ continue; // already at the level we want
+ }
+
+ // update level
+ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF) | (toPropagate & 0xFF));
+ updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)toPropagate);
+
+ // queue next
+ if (toPropagate > 1) {
+ // now combine into one bitset to pass to child
+ // the child bitset is 4x4, so we just shift each line by 4
+ // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value
+ final long childPropagation =
+ ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1
+ ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0
+ ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1
+
+ // don't queue update if toPropagate cannot propagate anything to neighbours
+ // (for increase, propagating 0 to neighbours is useless)
+ if (queueLength >= queue.length) {
+ queue = this.resizeIncreaseQueue();
+ }
+ queue[queueLength++] =
+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) |
+ ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) |
+ childPropagation; //(ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS));
+ continue;
+ }
+ continue;
+ }
+ }
+ }
+
+ private final void performDecrease() {
+ long[] queue = this.decreaseQueue;
+ long[] increaseQueue = this.increaseQueue;
+ int queueReadIndex = 0;
+ int queueLength = this.decreaseQueueInitialLength;
+ this.decreaseQueueInitialLength = 0;
+ int increaseQueueLength = this.increaseQueueInitialLength;
+ final int decodeOffsetX = -this.encodeOffsetX;
+ final int decodeOffsetZ = -this.encodeOffsetZ;
+ final int encodeOffset = this.coordinateOffset;
+ final int sectionOffset = this.sectionIndexOffset;
+
+ final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions;
+
+ while (queueReadIndex < queueLength) {
+ final long queueValue = queue[queueReadIndex++];
+
+ final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX;
+ final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ;
+ final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1);
+ // note: the above code requires coordinate bits * 2 < 32
+ // bitset is 16 bits
+ int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1);
+
+ // this bitset represents the values that we have not propagated to
+ // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases
+ // significantly reducing the total number of ops
+ // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need
+ // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead
+ // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits)
+ // to make things easy, we use positions [0, 4] in the bitset, with current position being 2
+ // index = x | (z << 3)
+
+ // to start, we eliminate everything 1 radius from the current position as the previous propagator
+ // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius
+ // but the rest not propagated are already handled
+ long currentPropagation = ~(
+ // z = -1
+ (1L << ((2 - 1) | ((2 - 1) << 3))) |
+ (1L << ((2 + 0) | ((2 - 1) << 3))) |
+ (1L << ((2 + 1) | ((2 - 1) << 3))) |
+
+ // z = 0
+ (1L << ((2 - 1) | ((2 + 0) << 3))) |
+ (1L << ((2 + 0) | ((2 + 0) << 3))) |
+ (1L << ((2 + 1) | ((2 + 0) << 3))) |
+
+ // z = 1
+ (1L << ((2 - 1) | ((2 + 1) << 3))) |
+ (1L << ((2 + 0) | ((2 + 1) << 3))) |
+ (1L << ((2 + 1) | ((2 + 1) << 3)))
+ );
+
+ final int toPropagate = propagatedLevel - 1;
+
+ // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting
+ // the bits, the cpu loop predictor should perfectly predict the loop.
+ for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) {
+ final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset);
+ final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset;
+ propagateDirectionBitset ^= tailingBit;
+
+
+ // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset
+ // it has been split to save some cycles via parallelism
+ final int pDecodeX = (set & 3);
+ final int pDecodeZ = ((set >>> 2) & 3);
+
+ // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX
+ final int offX = (posX - 1) + pDecodeX;
+ final int offZ = (posZ - 1) + pDecodeZ;
+
+ final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset;
+ final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
+
+ // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset
+ // bitset idx = x | (z << 3)
+
+ // read three bits, so we need 7L
+ // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1
+ // nstartidx1 = x rel -1 for z rel -1
+ // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3)
+ // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3)
+ // = pDecodeX | (pDecodeZ << 3) = start
+ final int start = pDecodeX | (pDecodeZ << 3);
+ final long bitsetLine1 = currentPropagation & (7L << (start));
+
+ // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset)
+ final long bitsetLine2 = currentPropagation & (7L << (start + 8));
+
+ // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset)
+ final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8)));
+
+ // now try to propagate
+ final Section section = this.sections[sectionIndex];
+
+ // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag
+ final short currentStoredLevel = section.levels[localIndex];
+ final int currentLevel = currentStoredLevel & 0xFF;
+ final int sourceLevel = (currentStoredLevel >>> 8) & 0xFF;
+
+ if (currentLevel == 0) {
+ continue; // already at the level we want
+ }
+
+ if (currentLevel > toPropagate) {
+ // it looks like another source propagated here, so re-propagate it
+ if (increaseQueueLength >= increaseQueue.length) {
+ increaseQueue = this.resizeIncreaseQueue();
+ }
+ increaseQueue[increaseQueueLength++] =
+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) |
+ ((currentLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) |
+ (FLAG_RECHECK_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)));
+ continue;
+ }
+
+ // remove ("take") lines from bitset
+ // can't do this during decrease, TODO WHY?
+ //currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3);
+
+ // update level
+ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF));
+ updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)0);
+
+ if (sourceLevel != 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++] =
+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) |
+ ((sourceLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) |
+ (FLAG_WRITE_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)));
+ }
+
+ // queue next
+ // note: targetLevel > 0 here, since toPropagate >= currentLevel and currentLevel > 0
+ // now combine into one bitset to pass to child
+ // the child bitset is 4x4, so we just shift each line by 4
+ // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value
+ final long childPropagation =
+ ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1
+ ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0
+ ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1
+
+ // don't queue update if toPropagate cannot propagate anything to neighbours
+ // (for increase, propagating 0 to neighbours is useless)
+ if (queueLength >= queue.length) {
+ queue = this.resizeDecreaseQueue();
+ }
+ queue[queueLength++] =
+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) |
+ ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) |
+ (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); //childPropagation;
+ continue;
+ }
+ }
+
+ // propagate sources we clobbered
+ this.increaseQueueInitialLength = increaseQueueLength;
+ this.performIncrease();
+ }
+ }
+
+ /*
+ private static final java.util.Random random = new java.util.Random(4L);
+ private static final List<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void>> walkers =
+ new java.util.ArrayList<>();
+ static final int PLAYERS = 0;
+ static final int RAD_BLOCKS = 10000;
+ static final int RAD = RAD_BLOCKS >> 4;
+ static final int RAD_BIG_BLOCKS = 100_000;
+ static final int RAD_BIG = RAD_BIG_BLOCKS >> 4;
+ static final int VD = 4;
+ static final int BIG_PLAYERS = 50;
+ static final double WALK_CHANCE = 0.10;
+ static final double TP_CHANCE = 0.01;
+ static final int TP_BACK_PLAYERS = 200;
+ static final double TP_BACK_CHANCE = 0.25;
+ static final double TP_STEAL_CHANCE = 0.25;
+ private static final List<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void>> tpBack =
+ new java.util.ArrayList<>();
+
+ public static void main(final String[] args) {
+ final ReentrantAreaLock ticketLock = new ReentrantAreaLock(SECTION_SHIFT);
+ final ReentrantAreaLock schedulingLock = new ReentrantAreaLock(SECTION_SHIFT);
+ final Long2ByteLinkedOpenHashMap levelMap = new Long2ByteLinkedOpenHashMap();
+ final Long2ByteLinkedOpenHashMap refMap = new Long2ByteLinkedOpenHashMap();
+ final io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D ref = new io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D((final long coordinate, final byte oldLevel, final byte newLevel) -> {
+ if (newLevel == 0) {
+ refMap.remove(coordinate);
+ } else {
+ refMap.put(coordinate, newLevel);
+ }
+ });
+ final ThreadedTicketLevelPropagator propagator = new ThreadedTicketLevelPropagator() {
+ @Override
+ protected void processLevelUpdates(Long2ByteLinkedOpenHashMap updates) {
+ for (final long key : updates.keySet()) {
+ final byte val = updates.get(key);
+ if (val == 0) {
+ levelMap.remove(key);
+ } else {
+ levelMap.put(key, val);
+ }
+ }
+ }
+
+ @Override
+ protected void processSchedulingUpdates(Long2ByteLinkedOpenHashMap updates, List<ChunkProgressionTask> scheduledTasks, List<NewChunkHolder> changedFullStatus) {}
+ };
+
+ for (;;) {
+ if (walkers.isEmpty() && tpBack.isEmpty()) {
+ for (int i = 0; i < PLAYERS; ++i) {
+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD;
+ int posX = random.nextInt(-rad, rad + 1);
+ int posZ = random.nextInt(-rad, rad + 1);
+
+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) {
+ @Override
+ protected void addCallback(Void parameter, int chunkX, int chunkZ) {
+ int src = 45 - 31 + 1;
+ ref.setSource(chunkX, chunkZ, src);
+ propagator.setSource(chunkX, chunkZ, src);
+ }
+
+ @Override
+ protected void removeCallback(Void parameter, int chunkX, int chunkZ) {
+ ref.removeSource(chunkX, chunkZ);
+ propagator.removeSource(chunkX, chunkZ);
+ }
+ };
+
+ map.add(posX, posZ, VD);
+
+ walkers.add(map);
+ }
+ for (int i = 0; i < TP_BACK_PLAYERS; ++i) {
+ int rad = RAD_BIG;
+ int posX = random.nextInt(-rad, rad + 1);
+ int posZ = random.nextInt(-rad, rad + 1);
+
+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) {
+ @Override
+ protected void addCallback(Void parameter, int chunkX, int chunkZ) {
+ int src = 45 - 31 + 1;
+ ref.setSource(chunkX, chunkZ, src);
+ propagator.setSource(chunkX, chunkZ, src);
+ }
+
+ @Override
+ protected void removeCallback(Void parameter, int chunkX, int chunkZ) {
+ ref.removeSource(chunkX, chunkZ);
+ propagator.removeSource(chunkX, chunkZ);
+ }
+ };
+
+ map.add(posX, posZ, random.nextInt(1, 63));
+
+ tpBack.add(map);
+ }
+ } else {
+ for (int i = 0; i < PLAYERS; ++i) {
+ if (random.nextDouble() > WALK_CHANCE) {
+ continue;
+ }
+
+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = walkers.get(i);
+
+ int updateX = random.nextInt(-1, 2);
+ int updateZ = random.nextInt(-1, 2);
+
+ map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD);
+ }
+
+ for (int i = 0; i < PLAYERS; ++i) {
+ if (random.nextDouble() > TP_CHANCE) {
+ continue;
+ }
+
+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD;
+ int posX = random.nextInt(-rad, rad + 1);
+ int posZ = random.nextInt(-rad, rad + 1);
+
+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = walkers.get(i);
+
+ map.update(posX, posZ, VD);
+ }
+
+ for (int i = 0; i < TP_BACK_PLAYERS; ++i) {
+ if (random.nextDouble() > TP_BACK_CHANCE) {
+ continue;
+ }
+
+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = tpBack.get(i);
+
+ map.update(-map.lastChunkX, -map.lastChunkZ, random.nextInt(1, 63));
+
+ if (random.nextDouble() > TP_STEAL_CHANCE) {
+ propagator.performUpdate(
+ map.lastChunkX >> SECTION_SHIFT, map.lastChunkZ >> SECTION_SHIFT, schedulingLock, null, null
+ );
+ propagator.performUpdate(
+ (-map.lastChunkX >> SECTION_SHIFT), (-map.lastChunkZ >> SECTION_SHIFT), schedulingLock, null, null
+ );
+ }
+ }
+ }
+
+ ref.propagateUpdates();
+ propagator.performUpdates(ticketLock, schedulingLock, null, null);
+
+ if (!refMap.equals(levelMap)) {
+ throw new IllegalStateException("Error!");
+ }
+ }
+ }
+ */
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373ba811ba8b
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
@@ -0,0 +1,668 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+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;
+import java.util.PriorityQueue;
+
+public class RadiusAwarePrioritisedExecutor {
+
+ private static final Comparator<DependencyNode> DEPENDENCY_NODE_COMPARATOR = (final DependencyNode t1, final DependencyNode t2) -> {
+ return Long.compare(t1.id, t2.id);
+ };
+
+ private final DependencyTree[] queues = new DependencyTree[PrioritisedExecutor.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) {
+ for (int i = 0; i < this.queues.length; ++i) {
+ this.queues[i] = new DependencyTree(this, executor, maxToSchedule, i);
+ }
+ }
+
+ private boolean canQueueTasks() {
+ return this.canQueueTasks;
+ }
+
+ private List<PrioritisedExecutor.PrioritisedTask> treeFinished() {
+ this.canQueueTasks = true;
+ for (int priority = 0; priority < this.queues.length; ++priority) {
+ final DependencyTree queue = this.queues[priority];
+ if (queue.hasWaitingTasks()) {
+ final List<PrioritisedExecutor.PrioritisedTask> ret = queue.tryPushTasks();
+
+ if (ret == null || ret.isEmpty()) {
+ // this happens when the tasks in the wait queue were purged
+ // in this case, the queue was actually empty, we just had to purge it
+ // if we set the selected queue without scheduling any tasks, the queue will never be unselected
+ // as that requires a scheduled task completing...
+ continue;
+ }
+
+ this.selectedQueue = priority;
+ return ret;
+ }
+ }
+
+ this.selectedQueue = NO_TASKS_QUEUED;
+
+ return null;
+ }
+
+ private List<PrioritisedExecutor.PrioritisedTask> queue(final Task task, final PrioritisedExecutor.Priority priority) {
+ final int priorityId = priority.priority;
+ final DependencyTree queue = this.queues[priorityId];
+
+ final DependencyNode node = new DependencyNode(task, queue);
+
+ if (task.dependencyNode != null) {
+ throw new IllegalStateException();
+ }
+ task.dependencyNode = node;
+
+ queue.pushNode(node);
+
+ if (this.selectedQueue == NO_TASKS_QUEUED) {
+ this.canQueueTasks = true;
+ this.selectedQueue = priorityId;
+ return queue.tryPushTasks();
+ }
+
+ if (!this.canQueueTasks) {
+ return null;
+ }
+
+ if (PrioritisedExecutor.Priority.isHigherPriority(priorityId, this.selectedQueue)) {
+ // prevent the lower priority tree from queueing more tasks
+ this.canQueueTasks = false;
+ return null;
+ }
+
+ // priorityId != selectedQueue: lower priority, don't care - treeFinished will pick it up
+ return priorityId == this.selectedQueue ? queue.tryPushTasks() : null;
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius,
+ final Runnable run, final PrioritisedExecutor.Priority priority) {
+ if (radius < 0) {
+ throw new IllegalArgumentException("Radius must be > 0: " + radius);
+ }
+ return new Task(this, chunkX, chunkZ, radius, run, priority);
+ }
+
+ 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);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius,
+ final Runnable run, final PrioritisedExecutor.Priority priority) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run, priority);
+
+ ret.queue();
+
+ return ret;
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius,
+ final Runnable run) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run);
+
+ ret.queue();
+
+ return ret;
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.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);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, priority);
+
+ ret.queue();
+
+ return ret;
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, PrioritisedExecutor.Priority.NORMAL);
+
+ ret.queue();
+
+ return ret;
+ }
+
+ // 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 currentlyExecuting;
+ private long idGenerator;
+
+ private final PriorityQueue<DependencyNode> awaiting = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR);
+
+ private final PriorityQueue<DependencyNode> infiniteRadius = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR);
+ private boolean isInfiniteRadiusScheduled;
+
+ private final Long2ReferenceOpenHashMap<DependencyNode> nodeByPosition = new Long2ReferenceOpenHashMap<>();
+
+ public DependencyTree(final RadiusAwarePrioritisedExecutor scheduler, final PrioritisedExecutor executor,
+ final int maxToSchedule, final int treeIndex) {
+ this.scheduler = scheduler;
+ this.executor = executor;
+ this.maxToSchedule = maxToSchedule;
+ this.treeIndex = treeIndex;
+ }
+
+ public boolean hasWaitingTasks() {
+ return !this.awaiting.isEmpty() || !this.infiniteRadius.isEmpty();
+ }
+
+ private long nextId() {
+ return this.idGenerator++;
+ }
+
+ private boolean isExecutingAnyTasks() {
+ return this.currentlyExecuting != 0;
+ }
+
+ private void pushNode(final DependencyNode node) {
+ if (!node.task.isFiniteRadius()) {
+ this.infiniteRadius.add(node);
+ return;
+ }
+
+ // set up dependency for node
+ final Task task = node.task;
+
+ final int centerX = task.chunkX;
+ final int centerZ = task.chunkZ;
+ final int radius = task.radius;
+
+ final int minX = centerX - radius;
+ final int maxX = centerX + radius;
+
+ final int minZ = centerZ - radius;
+ final int maxZ = centerZ + radius;
+
+ ReferenceOpenHashSet<DependencyNode> parents = null;
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final DependencyNode dependency = this.nodeByPosition.put(CoordinateUtils.getChunkKey(currX, currZ), node);
+ if (dependency != null) {
+ if (parents == null) {
+ parents = new ReferenceOpenHashSet<>();
+ }
+ if (parents.add(dependency)) {
+ // added a dependency, so we need to add as a child to the dependency
+ if (dependency.children == null) {
+ dependency.children = new ArrayList<>();
+ }
+ dependency.children.add(node);
+ }
+ }
+ }
+ }
+
+ if (parents == null) {
+ // no dependencies, add straight to awaiting
+ this.awaiting.add(node);
+ } else {
+ node.parents = parents.size();
+ // we will be added to awaiting once we have no parents
+ }
+ }
+
+ // called only when a node is returned after being executed
+ private List<PrioritisedExecutor.PrioritisedTask> returnNode(final DependencyNode node) {
+ final Task task = node.task;
+
+ // now that the task is completed, we can push its children to the awaiting queue
+ this.pushChildren(node);
+
+ if (task.isFiniteRadius()) {
+ // remove from dependency map
+ this.removeNodeFromMap(node);
+ } else {
+ // mark as no longer executing infinite radius
+ if (!this.isInfiniteRadiusScheduled) {
+ throw new IllegalStateException();
+ }
+ this.isInfiniteRadiusScheduled = false;
+ }
+
+ // decrement executing count, we are done executing this task
+ --this.currentlyExecuting;
+
+ if (this.currentlyExecuting == 0) {
+ return this.scheduler.treeFinished();
+ }
+
+ return this.scheduler.canQueueTasks() ? this.tryPushTasks() : null;
+ }
+
+ private List<PrioritisedExecutor.PrioritisedTask> tryPushTasks() {
+ // tasks are not queued, but only created here - we do hold the lock for the map
+ List<PrioritisedExecutor.PrioritisedTask> ret = null;
+ PrioritisedExecutor.PrioritisedTask pushedTask;
+ while ((pushedTask = this.tryPushTask()) != null) {
+ if (ret == null) {
+ ret = new ArrayList<>();
+ }
+ ret.add(pushedTask);
+ }
+
+ return ret;
+ }
+
+ private void removeNodeFromMap(final DependencyNode node) {
+ final Task task = node.task;
+
+ final int centerX = task.chunkX;
+ final int centerZ = task.chunkZ;
+ final int radius = task.radius;
+
+ final int minX = centerX - radius;
+ final int maxX = centerX + radius;
+
+ final int minZ = centerZ - radius;
+ final int maxZ = centerZ + radius;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ this.nodeByPosition.remove(CoordinateUtils.getChunkKey(currX, currZ), node);
+ }
+ }
+ }
+
+ private void pushChildren(final DependencyNode node) {
+ // add all the children that we can into awaiting
+ final List<DependencyNode> children = node.children;
+ if (children != null) {
+ for (int i = 0, len = children.size(); i < len; ++i) {
+ final DependencyNode child = children.get(i);
+ int newParents = --child.parents;
+ if (newParents == 0) {
+ // no more dependents, we can push to awaiting
+ // even if the child is purged, we need to push it so that its children will be pushed
+ this.awaiting.add(child);
+ } else if (newParents < 0) {
+ throw new IllegalStateException();
+ }
+ }
+ }
+ }
+
+ private DependencyNode pollAwaiting() {
+ final DependencyNode ret = this.awaiting.poll();
+ if (ret == null) {
+ return ret;
+ }
+
+ if (ret.parents != 0) {
+ throw new IllegalStateException();
+ }
+
+ if (ret.purged) {
+ // need to manually remove from state here
+ this.pushChildren(ret);
+ this.removeNodeFromMap(ret);
+ } // else: delay children push until the task has finished
+
+ return ret;
+ }
+
+ private DependencyNode pollInfinite() {
+ return this.infiniteRadius.poll();
+ }
+
+ public PrioritisedExecutor.PrioritisedTask tryPushTask() {
+ if (this.currentlyExecuting >= this.maxToSchedule || this.isInfiniteRadiusScheduled) {
+ return null;
+ }
+
+ DependencyNode firstInfinite;
+ while ((firstInfinite = this.infiniteRadius.peek()) != null && firstInfinite.purged) {
+ this.pollInfinite();
+ }
+
+ DependencyNode firstAwaiting;
+ while ((firstAwaiting = this.awaiting.peek()) != null && firstAwaiting.purged) {
+ this.pollAwaiting();
+ }
+
+ if (firstInfinite == null && firstAwaiting == null) {
+ return null;
+ }
+
+ // firstAwaiting compared to firstInfinite
+ final int compare;
+
+ if (firstAwaiting == null) {
+ // we choose first infinite, or infinite < awaiting
+ compare = 1;
+ } else if (firstInfinite == null) {
+ // we choose first awaiting, or awaiting < infinite
+ compare = -1;
+ } else {
+ compare = DEPENDENCY_NODE_COMPARATOR.compare(firstAwaiting, firstInfinite);
+ }
+
+ if (compare >= 0) {
+ if (this.currentlyExecuting != 0) {
+ // don't queue infinite task while other tasks are executing in parallel
+ return null;
+ }
+ ++this.currentlyExecuting;
+ this.pollInfinite();
+ this.isInfiniteRadiusScheduled = true;
+ return firstInfinite.task.pushTask(this.executor);
+ } else {
+ ++this.currentlyExecuting;
+ this.pollAwaiting();
+ return firstAwaiting.task.pushTask(this.executor);
+ }
+ }
+ }
+
+ private static final class DependencyNode {
+
+ private final Task task;
+ private final DependencyTree tree;
+
+ // dependency tree fields
+ // (must hold lock on the scheduler to use)
+ // null is the same as empty, we just use it so that we don't allocate the set unless we need to
+ private List<DependencyNode> children;
+ // 0 indicates that this task is considered "awaiting"
+ private int parents;
+ // false -> scheduled and not cancelled
+ // true -> scheduled but cancelled
+ private boolean purged;
+ private final long id;
+
+ public DependencyNode(final Task task, final DependencyTree tree) {
+ this.task = task;
+ this.id = tree.nextId();
+ this.tree = tree;
+ }
+ }
+
+ private static final class Task implements PrioritisedExecutor.PrioritisedTask, Runnable {
+
+ // task specific fields
+ private final RadiusAwarePrioritisedExecutor scheduler;
+ private final int chunkX;
+ private final int chunkZ;
+ private final int radius;
+ private Runnable run;
+ private PrioritisedExecutor.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) {
+ this.scheduler = scheduler;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.radius = radius;
+ this.run = run;
+ this.priority = priority;
+ }
+
+ private boolean isFiniteRadius() {
+ return this.radius >= 0;
+ }
+
+ private PrioritisedExecutor.PrioritisedTask pushTask(final PrioritisedExecutor executor) {
+ return this.queuedTask = executor.createTask(this, this.priority);
+ }
+
+ private void executeTask() {
+ final Runnable run = this.run;
+ this.run = null;
+ 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) {
+ final DependencyNode node = this.dependencyNode;
+ this.dependencyNode = null;
+ toSchedule = node.tree.returnNode(node);
+ }
+
+ scheduleTasks(toSchedule);
+ }
+
+ @Override
+ public void run() {
+ final Runnable run = this.run;
+ this.run = null;
+ try {
+ run.run();
+ } finally {
+ this.returnNode();
+ }
+ }
+
+ @Override
+ public boolean queue() {
+ final List<PrioritisedExecutor.PrioritisedTask> toSchedule;
+ synchronized (this.scheduler) {
+ if (this.queuedTask != null || this.dependencyNode != null || this.priority == PrioritisedExecutor.Priority.COMPLETING) {
+ return false;
+ }
+
+ toSchedule = this.scheduler.queue(this, this.priority);
+ }
+
+ scheduleTasks(toSchedule);
+ return true;
+ }
+
+ @Override
+ public boolean cancel() {
+ final PrioritisedExecutor.PrioritisedTask task;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
+ return false;
+ }
+
+ this.priority = PrioritisedExecutor.Priority.COMPLETING;
+ if (this.dependencyNode != null) {
+ this.dependencyNode.purged = true;
+ this.dependencyNode = null;
+ }
+
+ return true;
+ }
+ }
+
+ if (task.cancel()) {
+ // must manually return the node
+ this.run = null;
+ this.returnNode();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean execute() {
+ final PrioritisedExecutor.PrioritisedTask task;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
+ return false;
+ }
+
+ this.priority = PrioritisedExecutor.Priority.COMPLETING;
+ if (this.dependencyNode != null) {
+ this.dependencyNode.purged = true;
+ this.dependencyNode = null;
+ }
+ // fall through to execution logic
+ }
+ }
+
+ if (task != null) {
+ // will run the return node logic automatically
+ return task.execute();
+ } else {
+ // don't run node removal/insertion logic, we aren't actually removed from the dependency tree
+ this.executeTask();
+ return true;
+ }
+ }
+
+ @Override
+ public PrioritisedExecutor.Priority getPriority() {
+ final PrioritisedExecutor.PrioritisedTask task;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
+ return this.priority;
+ }
+ }
+
+ return task.getPriority();
+ }
+
+ @Override
+ public boolean setPriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
+ final PrioritisedExecutor.PrioritisedTask task;
+ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
+ return false;
+ }
+
+ if (this.priority == priority) {
+ return true;
+ }
+
+ this.priority = priority;
+ if (this.dependencyNode != null) {
+ // need to re-insert node
+ this.dependencyNode.purged = true;
+ this.dependencyNode = null;
+ toSchedule = this.scheduler.queue(this, priority);
+ }
+ }
+ }
+
+ if (task != null) {
+ return task.setPriority(priority);
+ }
+
+ scheduleTasks(toSchedule);
+
+ return true;
+ }
+
+ @Override
+ public boolean raisePriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
+ final PrioritisedExecutor.PrioritisedTask task;
+ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
+ return false;
+ }
+
+ if (this.priority.isHigherOrEqualPriority(priority)) {
+ return true;
+ }
+
+ this.priority = priority;
+ if (this.dependencyNode != null) {
+ // need to re-insert node
+ this.dependencyNode.purged = true;
+ this.dependencyNode = null;
+ toSchedule = this.scheduler.queue(this, priority);
+ }
+ }
+ }
+
+ if (task != null) {
+ return task.raisePriority(priority);
+ }
+
+ scheduleTasks(toSchedule);
+
+ return true;
+ }
+
+ @Override
+ public boolean lowerPriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
+ final PrioritisedExecutor.PrioritisedTask task;
+ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
+ return false;
+ }
+
+ if (this.priority.isLowerOrEqualPriority(priority)) {
+ return true;
+ }
+
+ this.priority = priority;
+ if (this.dependencyNode != null) {
+ // need to re-insert node
+ this.dependencyNode.purged = true;
+ this.dependencyNode = null;
+ toSchedule = this.scheduler.queue(this, priority);
+ }
+ }
+ }
+
+ if (task != null) {
+ return task.lowerPriority(priority);
+ }
+
+ scheduleTasks(toSchedule);
+
+ return true;
+ }
+ }
+}
\ 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
new file mode 100644
index 0000000000000000000000000000000000000000..fbdf721e8b4cfe6cef4ee60c53c680cbfc858d88
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
@@ -0,0 +1,142 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+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;
+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;
+import net.minecraft.server.level.ServerChunkCache;
+import net.minecraft.server.level.ServerLevel;
+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.ProtoChunk;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.level.chunk.status.ChunkStatusTasks;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.lang.invoke.VarHandle;
+
+public final class ChunkFullTask extends ChunkProgressionTask implements Runnable {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkFullTask.class);
+
+ private final NewChunkHolder chunkHolder;
+ private final ChunkAccess fromChunk;
+ 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) {
+ super(scheduler, world, chunkX, chunkZ);
+ this.chunkHolder = chunkHolder;
+ this.fromChunk = fromChunk;
+ this.convertToFullTask = scheduler.createChunkTask(chunkX, chunkZ, this, priority);
+ }
+
+ @Override
+ public ChunkStatus getTargetStatus() {
+ return ChunkStatus.FULL;
+ }
+
+ @Override
+ public void run() {
+ // See Vanilla ChunkPyramid#LOADING_PYRAMID.FULL for what this function should be doing
+ final LevelChunk chunk;
+ try {
+ // moved from the load from nbt stage into here
+ final PoiChunk poiChunk = this.chunkHolder.getPoiChunk();
+ if (poiChunk == null) {
+ LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString());
+ } else {
+ poiChunk.load();
+ ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$checkConsistency(this.fromChunk);
+ }
+
+ if (this.fromChunk instanceof ImposterProtoChunk wrappedFull) {
+ chunk = wrappedFull.getWrapped();
+ } else {
+ 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
+ });
+ this.chunkHolder.replaceProtoChunk(new ImposterProtoChunk(chunk, false));
+ }
+
+ ((ChunkSystemLevelChunk)chunk).moonrise$setChunkAndHolder(new ServerChunkCache.ChunkAndHolder(chunk, this.chunkHolder.vanillaChunkHolder));
+
+ 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);
+ } catch (final Throwable throwable) {
+ this.complete(null, throwable);
+ return;
+ }
+ this.complete(chunk, null);
+ }
+
+ protected volatile boolean scheduled;
+ protected static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkFullTask.class, "scheduled", boolean.class);
+
+ @Override
+ public boolean isScheduled() {
+ return this.scheduled;
+ }
+
+ @Override
+ public void schedule() {
+ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkFullTask)this, true)) {
+ throw new IllegalStateException("Cannot double call schedule()");
+ }
+ this.convertToFullTask.queue();
+ }
+
+ @Override
+ public void cancel() {
+ if (this.convertToFullTask.cancel()) {
+ this.complete(null, null);
+ }
+ }
+
+ @Override
+ public PrioritisedExecutor.Priority getPriority() {
+ return this.convertToFullTask.getPriority();
+ }
+
+ @Override
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.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)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.convertToFullTask.setPriority(priority);
+ }
+
+ @Override
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.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
new file mode 100644
index 0000000000000000000000000000000000000000..7c2e6752228fac175c4aa97fa3d817b8a938922f
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java
@@ -0,0 +1,181 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+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;
+import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine;
+import ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface;
+import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.ProtoChunk;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import java.util.function.BooleanSupplier;
+
+public final class ChunkLightTask extends ChunkProgressionTask {
+
+ private static final Logger LOGGER = LogManager.getLogger();
+
+ private final ChunkAccess fromChunk;
+
+ 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) {
+ super(scheduler, world, chunkX, chunkZ);
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.priorityHolder = new LightTaskPriorityHolder(priority, this);
+ this.fromChunk = chunk;
+ }
+
+ @Override
+ public boolean isScheduled() {
+ return this.priorityHolder.isScheduled();
+ }
+
+ @Override
+ public ChunkStatus getTargetStatus() {
+ return ChunkStatus.LIGHT;
+ }
+
+ @Override
+ public void schedule() {
+ this.priorityHolder.schedule();
+ }
+
+ @Override
+ public void cancel() {
+ this.priorityHolder.cancel();
+ }
+
+ @Override
+ public PrioritisedExecutor.Priority getPriority() {
+ return this.priorityHolder.getPriority();
+ }
+
+ @Override
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ this.priorityHolder.raisePriority(priority);
+ }
+
+ @Override
+ public void setPriority(final PrioritisedExecutor.Priority priority) {
+ this.priorityHolder.setPriority(priority);
+ }
+
+ @Override
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
+ this.priorityHolder.raisePriority(priority);
+ }
+
+ private static final class LightTaskPriorityHolder extends PriorityHolder {
+
+ private final ChunkLightTask task;
+
+ private LightTaskPriorityHolder(final PrioritisedExecutor.Priority priority, final ChunkLightTask task) {
+ super(priority);
+ this.task = task;
+ }
+
+ @Override
+ protected void cancelScheduled() {
+ final ChunkLightTask task = this.task;
+ task.complete(null, null);
+ }
+
+ @Override
+ protected PrioritisedExecutor.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) {
+ final ChunkLightTask task = this.task;
+ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
+ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
+ lightQueue.queueChunkLightTask(new ChunkPos(task.chunkX, task.chunkZ), new LightTask(starLightInterface, task), priority);
+ lightQueue.setPriority(task.chunkX, task.chunkZ, priority);
+ }
+
+ @Override
+ protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) {
+ final ChunkLightTask task = this.task;
+ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
+ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
+ lightQueue.lowerPriority(task.chunkX, task.chunkZ, priority);
+ }
+
+ @Override
+ protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) {
+ final ChunkLightTask task = this.task;
+ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
+ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
+ lightQueue.setPriority(task.chunkX, task.chunkZ, priority);
+ }
+
+ @Override
+ protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) {
+ final ChunkLightTask task = this.task;
+ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
+ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
+ lightQueue.raisePriority(task.chunkX, task.chunkZ, priority);
+ }
+ }
+
+ private static final class LightTask implements BooleanSupplier {
+
+ private final StarLightInterface lightEngine;
+ private final ChunkLightTask task;
+
+ public LightTask(final StarLightInterface lightEngine, final ChunkLightTask task) {
+ this.lightEngine = lightEngine;
+ this.task = task;
+ }
+
+ @Override
+ public boolean getAsBoolean() {
+ final ChunkLightTask task = this.task;
+ // executed on light thread
+ if (!task.priorityHolder.markExecuting()) {
+ // cancelled
+ return false;
+ }
+
+ try {
+ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(task.fromChunk);
+
+ if (task.fromChunk.isLightCorrect() && task.fromChunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) {
+ this.lightEngine.forceLoadInChunk(task.fromChunk, emptySections);
+ this.lightEngine.checkChunkEdges(task.chunkX, task.chunkZ);
+ } else {
+ task.fromChunk.setLightCorrect(false);
+ this.lightEngine.lightChunk(task.fromChunk, emptySections);
+ task.fromChunk.setLightCorrect(true);
+ }
+ // we need to advance status
+ if (task.fromChunk instanceof ProtoChunk chunk && chunk.getPersistedStatus() == ChunkStatus.LIGHT.getParent()) {
+ chunk.setPersistedStatus(ChunkStatus.LIGHT);
+ }
+ } catch (final Throwable thr) {
+ LOGGER.fatal(
+ "Failed to light chunk " + task.fromChunk.getPos().toString()
+ + " in world '" + WorldUtil.getWorldName(this.lightEngine.getWorld()) + "'", thr
+ );
+
+ task.complete(null, thr);
+
+ return true;
+ }
+
+ task.complete(task.fromChunk, null);
+ return true;
+ }
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13091425f8
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
@@ -0,0 +1,487 @@
+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.lock.ReentrantAreaLock;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+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.level.poi.PoiChunk;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.ChunkPos;
+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.levelgen.blending.BlendingData;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.lang.invoke.VarHandle;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+public final class ChunkLoadTask extends ChunkProgressionTask {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkLoadTask.class);
+
+ private final NewChunkHolder chunkHolder;
+ private final ChunkDataLoadTask loadTask;
+
+ private volatile boolean cancelled;
+ private NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask;
+ private NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask;
+ private GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> loadResult;
+ 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) {
+ super(scheduler, world, chunkX, chunkZ);
+ this.chunkHolder = chunkHolder;
+ this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority);
+ this.loadTask.addCallback((final GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> result) -> {
+ ChunkLoadTask.this.loadResult = result; // must be before getAndDecrement
+ ChunkLoadTask.this.tryCompleteLoad();
+ });
+ }
+
+ private void tryCompleteLoad() {
+ final int count = this.taskCountToComplete.decrementAndGet();
+ if (count == 0) {
+ final GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> result = this.cancelled ? null : this.loadResult; // only after the getAndDecrement
+ ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right());
+ } else if (count < 0) {
+ throw new IllegalStateException("Called tryCompleteLoad() too many times");
+ }
+ }
+
+ @Override
+ public ChunkStatus getTargetStatus() {
+ return ChunkStatus.EMPTY;
+ }
+
+ private boolean scheduled;
+
+ @Override
+ public boolean isScheduled() {
+ return this.scheduled;
+ }
+
+ @Override
+ public void schedule() {
+ final NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask;
+ final NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask;
+
+ final Consumer<GenericDataLoadTask.TaskResult<?, ?>> scheduleLoadTask = (final GenericDataLoadTask.TaskResult<?, ?> result) -> {
+ ChunkLoadTask.this.tryCompleteLoad();
+ };
+
+ // NOTE: it is IMPOSSIBLE for getOrLoadEntityData/getOrLoadPoiData to complete synchronously, because
+ // they must schedule a task to off main or to on main to complete
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ if (this.scheduled) {
+ throw new IllegalStateException("schedule() called twice");
+ }
+ this.scheduled = true;
+ if (this.cancelled) {
+ return;
+ }
+ if (!this.chunkHolder.isEntityChunkNBTLoaded()) {
+ entityLoadTask = this.chunkHolder.getOrLoadEntityData((Consumer)scheduleLoadTask);
+ } else {
+ entityLoadTask = null;
+ this.tryCompleteLoad();
+ }
+
+ if (!this.chunkHolder.isPoiChunkLoaded()) {
+ poiLoadTask = this.chunkHolder.getOrLoadPoiData((Consumer)scheduleLoadTask);
+ } else {
+ poiLoadTask = null;
+ this.tryCompleteLoad();
+ }
+
+ this.entityLoadTask = entityLoadTask;
+ this.poiLoadTask = poiLoadTask;
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+
+ if (entityLoadTask != null) {
+ entityLoadTask.schedule();
+ }
+
+ if (poiLoadTask != null) {
+ poiLoadTask.schedule();
+ }
+
+ this.loadTask.schedule(false);
+ }
+
+ @Override
+ public void cancel() {
+ // must be before load task access, so we can synchronise with the writes to the fields
+ final boolean scheduled;
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ // must read field here, as it may be written later conucrrently -
+ // we need to know if we scheduled _before_ cancellation
+ scheduled = this.scheduled;
+ this.cancelled = true;
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+
+ /*
+ Note: The entityLoadTask/poiLoadTask do not complete when cancelled,
+ so we need to manually try to complete in those cases
+ It is also important to note that we set the cancelled field first, just in case
+ the chunk load task attempts to complete with a non-null value
+ */
+
+ if (scheduled) {
+ // since we scheduled, we need to cancel the tasks
+ if (this.entityLoadTask != null) {
+ if (this.entityLoadTask.cancel()) {
+ this.tryCompleteLoad();
+ }
+ }
+ if (this.poiLoadTask != null) {
+ if (this.poiLoadTask.cancel()) {
+ this.tryCompleteLoad();
+ }
+ }
+ } else {
+ // since nothing was scheduled, we need to decrement the task count here ourselves
+
+ // for entity load task
+ this.tryCompleteLoad();
+
+ // for poi load task
+ this.tryCompleteLoad();
+ }
+ this.loadTask.cancel();
+ }
+
+ @Override
+ public PrioritisedExecutor.Priority getPriority() {
+ return this.loadTask.getPriority();
+ }
+
+ @Override
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
+ if (entityLoad != null) {
+ entityLoad.lowerPriority(priority);
+ }
+
+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask();
+
+ if (poiLoad != null) {
+ poiLoad.lowerPriority(priority);
+ }
+
+ this.loadTask.lowerPriority(priority);
+ }
+
+ @Override
+ public void setPriority(final PrioritisedExecutor.Priority priority) {
+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
+ if (entityLoad != null) {
+ entityLoad.setPriority(priority);
+ }
+
+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask();
+
+ if (poiLoad != null) {
+ poiLoad.setPriority(priority);
+ }
+
+ this.loadTask.setPriority(priority);
+ }
+
+ @Override
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
+ if (entityLoad != null) {
+ entityLoad.raisePriority(priority);
+ }
+
+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask();
+
+ if (poiLoad != null) {
+ poiLoad.raisePriority(priority);
+ }
+
+ this.loadTask.raisePriority(priority);
+ }
+
+ protected static abstract class CallbackDataLoadTask<OnMain,FinalCompletion> extends GenericDataLoadTask<OnMain,FinalCompletion> {
+
+ private TaskResult<FinalCompletion, Throwable> result;
+ private final MultiThreadedQueue<Consumer<TaskResult<FinalCompletion, Throwable>>> waiters = new MultiThreadedQueue<>();
+
+ protected volatile boolean completed;
+ 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) {
+ super(scheduler, world, chunkX, chunkZ, type, priority);
+ }
+
+ public void addCallback(final Consumer<TaskResult<FinalCompletion, Throwable>> consumer) {
+ if (!this.waiters.add(consumer)) {
+ try {
+ consumer.accept(this.result);
+ } catch (final Throwable throwable) {
+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
+ "Consumer", ChunkTaskScheduler.stringIfNull(consumer),
+ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.result.right()),
+ "CallbackDataLoadTask impl", this.getClass().getName()
+ ), throwable);
+ }
+ }
+ }
+
+ @Override
+ protected void onComplete(final TaskResult<FinalCompletion, Throwable> result) {
+ if ((boolean)COMPLETED_HANDLE.getAndSet((CallbackDataLoadTask)this, (boolean)true)) {
+ throw new IllegalStateException("Already completed");
+ }
+ this.result = result;
+ Consumer<TaskResult<FinalCompletion, Throwable>> consumer;
+ while ((consumer = this.waiters.pollOrBlockAdds()) != null) {
+ try {
+ consumer.accept(result);
+ } catch (final Throwable throwable) {
+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
+ "Consumer", ChunkTaskScheduler.stringIfNull(consumer),
+ "Completed throwable", ChunkTaskScheduler.stringIfNull(result.right()),
+ "CallbackDataLoadTask impl", this.getClass().getName()
+ ), throwable);
+ return;
+ }
+ }
+ }
+ }
+
+ private static final class ChunkDataLoadTask extends CallbackDataLoadTask<CompoundTag, 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);
+ }
+
+ @Override
+ protected boolean hasOffMain() {
+ return true;
+ }
+
+ @Override
+ protected boolean hasOnMain() {
+ return true;
+ }
+
+ @Override
+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ return this.scheduler.loadExecutor.createTask(run, priority);
+ }
+
+ @Override
+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ return this.scheduler.createChunkTask(this.chunkX, this.chunkZ, run, priority);
+ }
+
+ @Override
+ protected TaskResult<ChunkAccess, Throwable> completeOnMainOffMain(final CompoundTag data, final Throwable throwable) {
+ if (throwable != null) {
+ return new TaskResult<>(null, throwable);
+ }
+ if (data == null) {
+ return new TaskResult<>(this.getEmptyChunk(), null);
+ }
+
+ if (ChunkSystemFeatures.supportsAsyncChunkDeserialization()) {
+ return this.deserialize(data);
+ }
+ // need to deserialize on 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
+ );
+ }
+
+ @Override
+ protected TaskResult<CompoundTag, 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);
+ }
+
+ if (data == null) {
+ return new TaskResult<>(null, null);
+ }
+
+ try {
+ // run converters
+ final CompoundTag converted = this.world.getChunkSource().chunkMap.upgradeChunkTag(data, new net.minecraft.world.level.ChunkPos(this.chunkX, this.chunkZ));
+
+ 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);
+ }
+ }
+
+ 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
+ );
+ return new TaskResult<>(deserialized, 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);
+ }
+ }
+
+ @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);
+ }
+ }
+
+ 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);
+ }
+
+ @Override
+ protected boolean hasOffMain() {
+ return true;
+ }
+
+ @Override
+ protected boolean hasOnMain() {
+ return false;
+ }
+
+ @Override
+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ return this.scheduler.loadExecutor.createTask(run, priority);
+ }
+
+ @Override
+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected TaskResult<PoiChunk, Throwable> completeOnMainOffMain(final PoiChunk data, final Throwable throwable) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected TaskResult<PoiChunk, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) {
+ if (throwable != null) {
+ LOGGER.error("Failed to load poi data for task: " + this.toString() + ", poi data will be lost", throwable);
+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null);
+ }
+
+ if (data == null || data.isEmpty()) {
+ // nothing to do
+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null);
+ }
+
+ try {
+ // run converters
+ final CompoundTag converted = ChunkSystemConverters.convertPoiCompoundTag(data, this.world);
+
+ // now we need to parse it
+ return new TaskResult<>(PoiChunk.parse(this.world, this.chunkX, this.chunkZ, converted), null);
+ } catch (final Throwable thr2) {
+ LOGGER.error("Failed to run parse poi data for task: " + this.toString() + ", poi data will be lost", thr2);
+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null);
+ }
+ }
+
+ @Override
+ protected TaskResult<PoiChunk, Throwable> runOnMain(final PoiChunk data, final Throwable throwable) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ 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);
+ }
+
+ @Override
+ protected boolean hasOffMain() {
+ return true;
+ }
+
+ @Override
+ protected boolean hasOnMain() {
+ return false;
+ }
+
+ @Override
+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ return this.scheduler.loadExecutor.createTask(run, priority);
+ }
+
+ @Override
+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected TaskResult<CompoundTag, Throwable> completeOnMainOffMain(final CompoundTag data, final Throwable throwable) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected TaskResult<CompoundTag, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) {
+ if (throwable != null) {
+ LOGGER.error("Failed to load entity data for task: " + this.toString() + ", entity data will be lost", throwable);
+ return new TaskResult<>(null, null);
+ }
+
+ if (data == null || data.isEmpty()) {
+ // nothing to do
+ return new TaskResult<>(null, null);
+ }
+
+ try {
+ return new TaskResult<>(ChunkSystemConverters.convertEntityChunkCompoundTag(data, this.world), null);
+ } catch (final Throwable thr2) {
+ LOGGER.error("Failed to run converters for entity data for task: " + this.toString() + ", entity data will be lost", thr2);
+ return new TaskResult<>(null, thr2);
+ }
+ }
+
+ @Override
+ protected TaskResult<CompoundTag, Throwable> runOnMain(final CompoundTag data, final Throwable throwable) {
+ 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
new file mode 100644
index 0000000000000000000000000000000000000000..70e900b0f9c131900bf8b3f3ecbfbd5df5361205
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java
@@ -0,0 +1,101 @@
+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.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import java.lang.invoke.VarHandle;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+public abstract class ChunkProgressionTask {
+
+ private final MultiThreadedQueue<BiConsumer<ChunkAccess, Throwable>> waiters = new MultiThreadedQueue<>();
+ private ChunkAccess completedChunk;
+ private Throwable completedThrowable;
+
+ protected final ChunkTaskScheduler scheduler;
+ protected final ServerLevel world;
+ protected final int chunkX;
+ protected final int chunkZ;
+
+ protected volatile boolean completed;
+ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(ChunkProgressionTask.class, "completed", boolean.class);
+
+ protected ChunkProgressionTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ) {
+ this.scheduler = scheduler;
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ }
+
+ // Used only for debug json
+ public abstract boolean isScheduled();
+
+ // Note: It is the responsibility of the task to set the chunk's status once it has completed
+ public abstract ChunkStatus getTargetStatus();
+
+ /* Only executed once */
+ /* Implementations must be prepared to handle cases where cancel() is called before schedule() */
+ public abstract void schedule();
+
+ /* May be called multiple times */
+ public abstract void cancel();
+
+ public abstract PrioritisedExecutor.Priority getPriority();
+
+ /* Schedule lock is always held for the priority update calls */
+
+ public abstract void lowerPriority(final PrioritisedExecutor.Priority priority);
+
+ public abstract void setPriority(final PrioritisedExecutor.Priority priority);
+
+ public abstract void raisePriority(final PrioritisedExecutor.Priority priority);
+
+ public final void onComplete(final BiConsumer<ChunkAccess, Throwable> onComplete) {
+ if (!this.waiters.add(onComplete)) {
+ try {
+ onComplete.accept(this.completedChunk, this.completedThrowable);
+ } catch (final Throwable throwable) {
+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
+ "Consumer", ChunkTaskScheduler.stringIfNull(onComplete),
+ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.completedThrowable)
+ ), throwable);
+ }
+ }
+ }
+
+ protected final void complete(final ChunkAccess chunk, final Throwable throwable) {
+ try {
+ this.complete0(chunk, throwable);
+ } catch (final Throwable thr2) {
+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
+ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable)
+ ), thr2);
+ }
+ }
+
+ private void complete0(final ChunkAccess chunk, final Throwable throwable) {
+ if ((boolean)COMPLETED_HANDLE.getAndSet((ChunkProgressionTask)this, (boolean)true)) {
+ throw new IllegalStateException("Already completed");
+ }
+ this.completedChunk = chunk;
+ this.completedThrowable = throwable;
+
+ BiConsumer<ChunkAccess, Throwable> consumer;
+ while ((consumer = this.waiters.pollOrBlockAdds()) != null) {
+ consumer.accept(chunk, throwable);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ChunkProgressionTask{class: " + this.getClass().getName() + ", for world: " + WorldUtil.getWorldName(this.world) +
+ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() +
+ ", status: " + this.getTargetStatus().toString() + ", scheduled: " + this.isScheduled() + "}";
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000000000000000000000000000000000..2c17d5589f15f1155be08be670d29acbe954a8fa
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
@@ -0,0 +1,217 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+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;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.ChunkMap;
+import net.minecraft.server.level.GenerationChunkHolder;
+import net.minecraft.server.level.ServerChunkCache;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.util.StaticCache2D;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.ProtoChunk;
+import net.minecraft.world.level.chunk.status.ChunkPyramid;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.level.chunk.status.WorldGenContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.lang.invoke.VarHandle;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+public final class ChunkUpgradeGenericStatusTask extends ChunkProgressionTask implements Runnable {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkUpgradeGenericStatusTask.class);
+
+ private final ChunkAccess fromChunk;
+ private final ChunkStatus fromStatus;
+ private final ChunkStatus toStatus;
+ private final StaticCache2D<GenerationChunkHolder> neighbours;
+
+ private final PrioritisedExecutor.PrioritisedTask generateTask;
+
+ 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) {
+ super(scheduler, world, chunkX, chunkZ);
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.fromChunk = chunk;
+ this.fromStatus = chunk.getPersistedStatus();
+ this.toStatus = toStatus;
+ this.neighbours = neighbours;
+ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isParallelCapable()) {
+ this.generateTask = this.scheduler.parallelGenExecutor.createTask(this, priority);
+ } else {
+ final int writeRadius = ((ChunkSystemChunkStatus)this.toStatus).moonrise$getWriteRadius();
+ if (writeRadius < 0) {
+ this.generateTask = this.scheduler.radiusAwareScheduler.createInfiniteRadiusTask(this, priority);
+ } else {
+ this.generateTask = this.scheduler.radiusAwareScheduler.createTask(chunkX, chunkZ, writeRadius, this, priority);
+ }
+ }
+ }
+
+ @Override
+ public ChunkStatus getTargetStatus() {
+ return this.toStatus;
+ }
+
+ private boolean isEmptyTask() {
+ // must use fromStatus here to avoid any race condition with run() overwriting the status
+ final boolean generation = !this.fromStatus.isOrAfter(this.toStatus);
+ return (generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) || (!generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus());
+ }
+
+ @Override
+ public void run() {
+ final ChunkAccess chunk = this.fromChunk;
+
+ final ServerChunkCache serverChunkCache = this.world.getChunkSource();
+ final ChunkMap chunkMap = serverChunkCache.chunkMap;
+
+ final CompletableFuture<ChunkAccess> completeFuture;
+
+ final boolean generation;
+ boolean completing = false;
+
+ // note: should optimise the case where the chunk does not need to execute the status, because
+ // schedule() calls this synchronously if it will run through that path
+
+ final WorldGenContext ctx = chunkMap.worldGenContext;
+ try {
+ generation = !chunk.getPersistedStatus().isOrAfter(this.toStatus);
+ if (generation) {
+ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) {
+ if (chunk instanceof ProtoChunk) {
+ ((ProtoChunk)chunk).setPersistedStatus(this.toStatus);
+ }
+ completing = true;
+ this.complete(chunk, null);
+ return;
+ }
+ completeFuture = ChunkPyramid.GENERATION_PYRAMID.getStepTo(this.toStatus).apply(ctx, this.neighbours, this.fromChunk)
+ .whenComplete((final ChunkAccess either, final Throwable throwable) -> {
+ if (either instanceof ProtoChunk proto) {
+ proto.setPersistedStatus(ChunkUpgradeGenericStatusTask.this.toStatus);
+ }
+ }
+ );
+ } else {
+ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus()) {
+ completing = true;
+ this.complete(chunk, null);
+ return;
+ }
+ completeFuture = ChunkPyramid.LOADING_PYRAMID.getStepTo(this.toStatus).apply(ctx, this.neighbours, this.fromChunk);
+ }
+ } catch (final Throwable throwable) {
+ if (!completing) {
+ this.complete(null, throwable);
+ return;
+ }
+
+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
+ "Target status", ChunkTaskScheduler.stringIfNull(this.toStatus),
+ "From status", ChunkTaskScheduler.stringIfNull(this.fromStatus),
+ "Generation task", this
+ ), throwable);
+
+ LOGGER.error(
+ "Failed to complete status for chunk: status:" + this.toStatus + ", chunk: (" + this.chunkX +
+ "," + this.chunkZ + "), world: " + WorldUtil.getWorldName(this.world),
+ throwable
+ );
+
+ return;
+ }
+
+ if (!completeFuture.isDone() && !((ChunkSystemChunkStatus)this.toStatus).moonrise$getWarnedAboutNoImmediateComplete().getAndSet(true)) {
+ LOGGER.warn("Future status not complete after scheduling: " + this.toStatus.toString() + ", generate: " + generation);
+ }
+
+ final ChunkAccess newChunk;
+
+ try {
+ newChunk = completeFuture.join();
+ } catch (final Throwable throwable) {
+ this.complete(null, throwable);
+ return;
+ }
+
+ if (newChunk == null) {
+ this.complete(null,
+ new IllegalStateException(
+ "Chunk for status: " + ChunkUpgradeGenericStatusTask.this.toStatus.toString()
+ + ", generation: " + generation + " should not be null! Future: " + completeFuture
+ ).fillInStackTrace()
+ );
+ return;
+ }
+
+ this.complete(newChunk, null);
+ }
+
+ private volatile boolean scheduled;
+ private static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkUpgradeGenericStatusTask.class, "scheduled", boolean.class);
+
+ @Override
+ public boolean isScheduled() {
+ return this.scheduled;
+ }
+
+ @Override
+ public void schedule() {
+ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkUpgradeGenericStatusTask)this, true)) {
+ throw new IllegalStateException("Cannot double call schedule()");
+ }
+ if (this.isEmptyTask()) {
+ if (this.generateTask.cancel()) {
+ this.run();
+ }
+ } else {
+ this.generateTask.queue();
+ }
+ }
+
+ @Override
+ public void cancel() {
+ if (this.generateTask.cancel()) {
+ this.complete(null, null);
+ }
+ }
+
+ @Override
+ public PrioritisedExecutor.Priority getPriority() {
+ return this.generateTask.getPriority();
+ }
+
+ @Override
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.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)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.generateTask.setPriority(priority);
+ }
+
+ @Override
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.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
new file mode 100644
index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be314b876c
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
@@ -0,0 +1,673 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
+
+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.util.ConcurrentUtil;
+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.scheduling.ChunkTaskScheduler;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerLevel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.lang.invoke.VarHandle;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BiConsumer;
+
+public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(GenericDataLoadTask.class);
+
+ protected static final CompoundTag CANCELLED_DATA = new CompoundTag();
+
+ // reference count is the upper 32 bits
+ protected final AtomicLong stageAndReferenceCount = new AtomicLong(STAGE_NOT_STARTED);
+
+ protected static final long STAGE_MASK = 0xFFFFFFFFL;
+ protected static final long STAGE_CANCELLED = 0xFFFFFFFFL;
+ protected static final long STAGE_NOT_STARTED = 0L;
+ protected static final long STAGE_LOADING = 1L;
+ protected static final long STAGE_PROCESSING = 2L;
+ protected static final long STAGE_COMPLETED = 3L;
+
+ // for loading data off disk
+ protected final LoadDataFromDiskTask loadDataFromDiskTask;
+ // processing off-main
+ protected final PrioritisedExecutor.PrioritisedTask processOffMain;
+ // processing on-main
+ protected final PrioritisedExecutor.PrioritisedTask processOnMain;
+
+ protected final ChunkTaskScheduler scheduler;
+ protected final ServerLevel world;
+ protected final int chunkX;
+ protected final int chunkZ;
+ protected final RegionFileIOThread.RegionFileType type;
+
+ public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
+ final int chunkZ, final RegionFileIOThread.RegionFileType type,
+ final PrioritisedExecutor.Priority priority) {
+ this.scheduler = scheduler;
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.type = type;
+
+ final ProcessOnMainTask mainTask;
+ if (this.hasOnMain()) {
+ mainTask = new ProcessOnMainTask();
+ this.processOnMain = this.createOnMain(mainTask, priority);
+ } else {
+ mainTask = null;
+ this.processOnMain = null;
+ }
+
+ final ProcessOffMainTask offMainTask;
+ if (this.hasOffMain()) {
+ offMainTask = new ProcessOffMainTask(mainTask);
+ this.processOffMain = this.createOffMain(offMainTask, priority);
+ } else {
+ offMainTask = null;
+ this.processOffMain = null;
+ }
+
+ if (this.processOffMain == null && this.processOnMain == null) {
+ throw new IllegalStateException("Illegal class implementation: " + this.getClass().getName() + ", should be able to schedule at least one task!");
+ }
+
+ this.loadDataFromDiskTask = new LoadDataFromDiskTask(world, chunkX, chunkZ, type, new DataLoadCallback(offMainTask, mainTask), priority);
+ }
+
+ public static final record TaskResult<L, R>(L left, R right) {}
+
+ protected abstract boolean hasOffMain();
+
+ protected abstract boolean hasOnMain();
+
+ protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority);
+
+ protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority);
+
+ protected abstract TaskResult<OnMain, Throwable> runOffMain(final CompoundTag data, final Throwable throwable);
+
+ protected abstract TaskResult<FinalCompletion, Throwable> runOnMain(final OnMain data, final Throwable throwable);
+
+ protected abstract void onComplete(final TaskResult<FinalCompletion,Throwable> result);
+
+ protected abstract TaskResult<FinalCompletion, Throwable> completeOnMainOffMain(final OnMain data, final Throwable throwable);
+
+ @Override
+ public String toString() {
+ return "GenericDataLoadTask{class: " + this.getClass().getName() + ", world: " + WorldUtil.getWorldName(this.world) +
+ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() +
+ ", type: " + this.type.toString() + "}";
+ }
+
+ public PrioritisedExecutor.Priority getPriority() {
+ if (this.processOnMain != null) {
+ return this.processOnMain.getPriority();
+ } else {
+ return this.processOffMain.getPriority();
+ }
+ }
+
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ // can't lower I/O tasks, we don't know what they affect
+ if (this.processOffMain != null) {
+ this.processOffMain.lowerPriority(priority);
+ }
+ if (this.processOnMain != null) {
+ this.processOnMain.lowerPriority(priority);
+ }
+ }
+
+ public void setPriority(final PrioritisedExecutor.Priority priority) {
+ // can't lower I/O tasks, we don't know what they affect
+ this.loadDataFromDiskTask.raisePriority(priority);
+ if (this.processOffMain != null) {
+ this.processOffMain.setPriority(priority);
+ }
+ if (this.processOnMain != null) {
+ this.processOnMain.setPriority(priority);
+ }
+ }
+
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
+ // can't lower I/O tasks, we don't know what they affect
+ this.loadDataFromDiskTask.raisePriority(priority);
+ if (this.processOffMain != null) {
+ this.processOffMain.raisePriority(priority);
+ }
+ if (this.processOnMain != null) {
+ this.processOnMain.raisePriority(priority);
+ }
+ }
+
+ // returns whether scheduleNow() needs to be called
+ public boolean schedule(final boolean delay) {
+ if (this.stageAndReferenceCount.get() != STAGE_NOT_STARTED ||
+ !this.stageAndReferenceCount.compareAndSet(STAGE_NOT_STARTED, (1L << 32) | STAGE_LOADING)) {
+ // try and increment reference count
+ int failures = 0;
+ for (long curr = this.stageAndReferenceCount.get();;) {
+ if ((curr & STAGE_MASK) == STAGE_CANCELLED || (curr & STAGE_MASK) == STAGE_COMPLETED) {
+ // cancelled or completed, nothing to do here
+ return false;
+ }
+
+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, curr + (1L << 32)))) {
+ // successful
+ return false;
+ }
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ if (!delay) {
+ this.scheduleNow();
+ return false;
+ }
+ return true;
+ }
+
+ public void scheduleNow() {
+ this.loadDataFromDiskTask.schedule(); // will schedule the rest
+ }
+
+ // assumes the current stage cannot be completed
+ // returns false if cancelled, returns true if can proceed
+ private boolean advanceStage(final long expect, final long to) {
+ int failures = 0;
+ for (long curr = this.stageAndReferenceCount.get();;) {
+ if ((curr & STAGE_MASK) != expect) {
+ // must be cancelled
+ return false;
+ }
+
+ final long newVal = (curr & ~STAGE_MASK) | to;
+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) {
+ return true;
+ }
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ public boolean cancel() {
+ int failures = 0;
+ for (long curr = this.stageAndReferenceCount.get();;) {
+ if ((curr & STAGE_MASK) == STAGE_COMPLETED || (curr & STAGE_MASK) == STAGE_CANCELLED) {
+ return false;
+ }
+
+ if ((curr & STAGE_MASK) == STAGE_NOT_STARTED || (curr & ~STAGE_MASK) == (1L << 32)) {
+ // no other references, so we can cancel
+ final long newVal = STAGE_CANCELLED;
+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) {
+ this.loadDataFromDiskTask.cancel();
+ if (this.processOffMain != null) {
+ this.processOffMain.cancel();
+ }
+ if (this.processOnMain != null) {
+ this.processOnMain.cancel();
+ }
+ this.onComplete(null);
+ return true;
+ }
+ } else {
+ if ((curr & ~STAGE_MASK) == (0L << 32)) {
+ throw new IllegalStateException("Reference count cannot be zero here");
+ }
+ // just decrease the reference count
+ final long newVal = curr - (1L << 32);
+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) {
+ return false;
+ }
+ }
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ private final class DataLoadCallback implements BiConsumer<CompoundTag, Throwable> {
+
+ private final ProcessOffMainTask offMainTask;
+ private final ProcessOnMainTask onMainTask;
+
+ public DataLoadCallback(final ProcessOffMainTask offMainTask, final ProcessOnMainTask onMainTask) {
+ this.offMainTask = offMainTask;
+ this.onMainTask = onMainTask;
+ }
+
+ @Override
+ public void accept(final CompoundTag compoundTag, final Throwable throwable) {
+ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) {
+ // don't try to schedule further
+ return;
+ }
+
+ try {
+ if (compoundTag == CANCELLED_DATA) {
+ // cancelled, except this isn't possible
+ LOGGER.error("Data callback says cancelled, but stage does not?");
+ return;
+ }
+
+ // get off of the regionfile callback ASAP, no clue what locks are held right now...
+ if (GenericDataLoadTask.this.processOffMain != null) {
+ this.offMainTask.data = compoundTag;
+ this.offMainTask.throwable = throwable;
+ GenericDataLoadTask.this.processOffMain.queue();
+ return;
+ } else {
+ // no off-main task, so go straight to main
+ this.onMainTask.data = (OnMain)compoundTag;
+ this.onMainTask.throwable = throwable;
+ GenericDataLoadTask.this.processOnMain.queue();
+ }
+ } catch (final Throwable thr2) {
+ LOGGER.error("Failed I/O callback for task: " + GenericDataLoadTask.this.toString(), thr2);
+ GenericDataLoadTask.this.scheduler.unrecoverableChunkSystemFailure(
+ GenericDataLoadTask.this.chunkX, GenericDataLoadTask.this.chunkZ, Map.of(
+ "Callback throwable", ChunkTaskScheduler.stringIfNull(throwable)
+ ), thr2
+ );
+ }
+ }
+ }
+
+ private final class ProcessOffMainTask implements Runnable {
+
+ private CompoundTag data;
+ private Throwable throwable;
+ private final ProcessOnMainTask schedule;
+
+ public ProcessOffMainTask(final ProcessOnMainTask schedule) {
+ this.schedule = schedule;
+ }
+
+ @Override
+ public void run() {
+ if (!GenericDataLoadTask.this.advanceStage(STAGE_LOADING, this.schedule == null ? STAGE_COMPLETED : STAGE_PROCESSING)) {
+ // cancelled
+ return;
+ }
+ final TaskResult<OnMain, Throwable> newData = GenericDataLoadTask.this.runOffMain(this.data, this.throwable);
+
+ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) {
+ // don't try to schedule further
+ return;
+ }
+
+ if (this.schedule != null) {
+ final TaskResult<FinalCompletion, Throwable> syncComplete = GenericDataLoadTask.this.completeOnMainOffMain(newData.left, newData.right);
+
+ if (syncComplete != null) {
+ if (GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) {
+ GenericDataLoadTask.this.onComplete(syncComplete);
+ } // else: cancelled
+ return;
+ }
+
+ this.schedule.data = newData.left;
+ this.schedule.throwable = newData.right;
+
+ GenericDataLoadTask.this.processOnMain.queue();
+ } else {
+ GenericDataLoadTask.this.onComplete((TaskResult<FinalCompletion, Throwable>)newData);
+ }
+ }
+ }
+
+ private final class ProcessOnMainTask implements Runnable {
+
+ private OnMain data;
+ private Throwable throwable;
+
+ @Override
+ public void run() {
+ if (!GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) {
+ // cancelled
+ return;
+ }
+ final TaskResult<FinalCompletion, Throwable> result = GenericDataLoadTask.this.runOnMain(this.data, this.throwable);
+
+ GenericDataLoadTask.this.onComplete(result);
+ }
+ }
+
+ protected static final class LoadDataFromDiskTask {
+
+ private volatile int priority;
+ private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(LoadDataFromDiskTask.class, "priority", int.class);
+
+ private static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 0;
+ private static final int PRIORITY_LOAD_SCHEDULED = Integer.MIN_VALUE >>> 1;
+ private static final int PRIORITY_UNLOAD_SCHEDULED = Integer.MIN_VALUE >>> 2;
+
+ private static final int PRIORITY_FLAGS = ~Character.MAX_VALUE;
+
+ private final int getPriorityVolatile() {
+ return (int)PRIORITY_HANDLE.getVolatile((LoadDataFromDiskTask)this);
+ }
+
+ private final int compareAndExchangePriorityVolatile(final int expect, final int update) {
+ return (int)PRIORITY_HANDLE.compareAndExchange((LoadDataFromDiskTask)this, (int)expect, (int)update);
+ }
+
+ private final int getAndOrPriorityVolatile(final int val) {
+ return (int)PRIORITY_HANDLE.getAndBitwiseOr((LoadDataFromDiskTask)this, (int)val);
+ }
+
+ private final void setPriorityPlain(final int val) {
+ PRIORITY_HANDLE.set((LoadDataFromDiskTask)this, (int)val);
+ }
+
+ private final ServerLevel world;
+ private final int chunkX;
+ private final int chunkZ;
+
+ private final RegionFileIOThread.RegionFileType type;
+ private Cancellable dataLoadTask;
+ private Cancellable dataUnloadCancellable;
+ private DelayedPrioritisedTask dataUnloadTask;
+
+ private final BiConsumer<CompoundTag, Throwable> onComplete;
+ private final AtomicBoolean scheduled = new AtomicBoolean();
+
+ // 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 BiConsumer<CompoundTag, Throwable> onComplete,
+ final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.type = type;
+ this.onComplete = onComplete;
+ this.setPriorityPlain(priority.priority);
+ }
+
+ private void complete(final CompoundTag data, final Throwable throwable) {
+ try {
+ this.onComplete.accept(data, throwable);
+ } catch (final Throwable thr2) {
+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
+ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable),
+ "Regionfile type", ChunkTaskScheduler.stringIfNull(this.type)
+ ), thr2);
+ }
+ }
+
+ private boolean markExecuting() {
+ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0;
+ }
+
+ private boolean isMarkedExecuted() {
+ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0;
+ }
+
+ 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_EXECUTED) != 0) {
+ // cancelled or executed
+ return;
+ }
+
+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
+ RegionFileIOThread.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
+ return;
+ }
+
+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) {
+ if (this.dataUnloadTask != null) {
+ this.dataUnloadTask.lowerPriority(priority);
+ }
+ // no return - we need to propagate priority
+ }
+
+ if (!priority.isHigherPriority(curr & ~PRIORITY_FLAGS)) {
+ return;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) {
+ 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_EXECUTED) != 0) {
+ // cancelled or executed
+ return;
+ }
+
+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
+ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
+ return;
+ }
+
+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) {
+ if (this.dataUnloadTask != null) {
+ this.dataUnloadTask.setPriority(priority);
+ }
+ // no return - we need to propagate priority
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) {
+ return;
+ }
+
+ // failed, retry
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ 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_EXECUTED) != 0) {
+ // cancelled or executed
+ return;
+ }
+
+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
+ RegionFileIOThread.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
+ return;
+ }
+
+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) {
+ if (this.dataUnloadTask != null) {
+ this.dataUnloadTask.raisePriority(priority);
+ }
+ // no return - we need to propagate priority
+ }
+
+ if (!priority.isLowerPriority(curr & ~PRIORITY_FLAGS)) {
+ return;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) {
+ return;
+ }
+
+ // failed, retry
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ public void cancel() {
+ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) {
+ // cancelled or executed already
+ return;
+ }
+
+ // OK if we miss the field read, the task cannot complete if the cancelled bit is set and
+ // the write to dataLoadTask will check for the cancelled bit
+ if (this.dataUnloadCancellable != null) {
+ this.dataUnloadCancellable.cancel();
+ }
+
+ if (this.dataLoadTask != null) {
+ this.dataLoadTask.cancel();
+ }
+
+ this.complete(CANCELLED_DATA, null);
+ }
+
+ public void schedule() {
+ if (this.scheduled.getAndSet(true)) {
+ throw new IllegalStateException("schedule() called twice");
+ }
+ int priority = this.getPriorityVolatile();
+
+ if ((priority & PRIORITY_EXECUTED) != 0) {
+ // cancelled
+ return;
+ }
+
+ final BiConsumer<CompoundTag, Throwable> consumer = (final CompoundTag data, final Throwable thr) -> {
+ // because cancelScheduled() cannot actually stop this task from executing in every case, we need
+ // to mark complete here to ensure we do not double complete
+ if (LoadDataFromDiskTask.this.markExecuting()) {
+ LoadDataFromDiskTask.this.complete(data, thr);
+ } // else: cancelled
+ };
+
+ final PrioritisedExecutor.Priority initialPriority = PrioritisedExecutor.Priority.getPriority(priority);
+ boolean scheduledUnload = false;
+
+ final NewChunkHolder holder = ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ);
+ if (holder != null) {
+ final BiConsumer<CompoundTag, Throwable> unloadConsumer = (final CompoundTag data, final Throwable thr) -> {
+ if (data != null) {
+ consumer.accept(data, null);
+ } else {
+ // need to schedule task
+ LoadDataFromDiskTask.this.schedule(false, consumer, PrioritisedExecutor.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();
+ if (unloadCompletable != null) {
+ unloadCancellable = unloadCompletable.addAsynchronousWaiter(unloadConsumer);
+ if (unloadCancellable == null) {
+ syncComplete = unloadCompletable.getResult();
+ }
+ }
+
+ if (syncComplete != null) {
+ consumer.accept(syncComplete, null);
+ return;
+ }
+
+ if (unloadCancellable != null) {
+ scheduledUnload = true;
+ this.dataUnloadCancellable = unloadCancellable;
+ this.dataUnloadTask = unloadTask.task();
+ }
+ }
+
+ this.schedule(scheduledUnload, consumer, initialPriority);
+ }
+
+ private void schedule(final boolean scheduledUnload, final BiConsumer<CompoundTag, Throwable> consumer, final PrioritisedExecutor.Priority initialPriority) {
+ int priority = this.getPriorityVolatile();
+
+ if ((priority & PRIORITY_EXECUTED) != 0) {
+ // cancelled
+ return;
+ }
+
+ if (!scheduledUnload) {
+ this.dataLoadTask = RegionFileIOThread.loadDataAsync(
+ this.world, this.chunkX, this.chunkZ, this.type, consumer,
+ initialPriority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), initialPriority
+ );
+ }
+
+ int failures = 0;
+ for (;;) {
+ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | (scheduledUnload ? PRIORITY_UNLOAD_SCHEDULED : PRIORITY_LOAD_SCHEDULED)))) {
+ return;
+ }
+
+ if ((priority & PRIORITY_EXECUTED) != 0) {
+ // cancelled or executed
+ if (this.dataUnloadCancellable != null) {
+ this.dataUnloadCancellable.cancel();
+ }
+
+ if (this.dataLoadTask != null) {
+ this.dataLoadTask.cancel();
+ }
+ return;
+ }
+
+ if (scheduledUnload) {
+ if (this.dataUnloadTask != null) {
+ this.dataUnloadTask.setPriority(PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS));
+ }
+ } else {
+ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS));
+ }
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java
new file mode 100644
index 0000000000000000000000000000000000000000..cb6af3712bf9f6f6b8f7a459c309c75dabe83a50
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.server;
+
+public interface ChunkSystemMinecraftServer {
+
+ public void moonrise$setChunkSystemCrash(final Throwable throwable);
+
+ public void moonrise$executeMidTickTasks();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java
new file mode 100644
index 0000000000000000000000000000000000000000..ea759ce6f10f2a5a4e107ab7528030fe931ba223
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.status;
+
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+
+public interface ChunkSystemChunkStep {
+
+ public ChunkStatus moonrise$getRequiredStatusAtRadius(final int radius);
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..129a35ff2db5b3bb6736810fc180796ce55e1875
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.storage;
+
+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
+
+public interface ChunkSystemChunkStorage {
+
+ public RegionFileStorage moonrise$getRegionStorage();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java
new file mode 100644
index 0000000000000000000000000000000000000000..786e6ad17cd6216ef0aadaa7cf10044a0c19c933
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.ticket;
+
+public interface ChunkSystemTicket<T> {
+
+ public long moonrise$getRemoveDelay();
+
+ public void moonrise$setRemoveDelay(final long removeDelay);
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java
new file mode 100644
index 0000000000000000000000000000000000000000..2add7fd15a2210286aeb9af5024263333340d34c
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.ticks;
+
+public interface ChunkSystemLevelChunkTicks {
+
+ public boolean moonrise$isDirty(final long tick);
+
+ public void moonrise$clearDirty();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java
new file mode 100644
index 0000000000000000000000000000000000000000..ce3bb903c9ccb7efa0f004cf79b291dcb1cb7a23
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java
@@ -0,0 +1,15 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.util;
+
+import net.minecraft.util.SortedArraySet;
+
+public interface ChunkSystemSortedArraySet<T> {
+
+ public SortedArraySet<T> moonrise$copy();
+
+ public Object[] moonrise$copyBackingArray();
+
+ public T moonrise$replace(final T object);
+
+ public T moonrise$removeAndGet(final T object);
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..3a9a564edfdb99e006e4816cb8821bd1e9ecff43
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
@@ -0,0 +1,320 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.util;
+
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import it.unimi.dsi.fastutil.HashCommon;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.longs.LongIterator;
+import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+import java.util.Arrays;
+import java.util.Objects;
+
+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][];
+ static {
+ for (int i = 0; i < SEARCH_RADIUS_ITERATION_LIST.length; ++i) {
+ // a BFS around -x, -z, +x, +z will give increasing manhatten distance
+ SEARCH_RADIUS_ITERATION_LIST[i] = generateBFSOrder(i);
+ }
+ }
+
+ public static long[] getSearchIteration(final int radius) {
+ return SEARCH_RADIUS_ITERATION_LIST[radius];
+ }
+
+ private static class CustomLongArray extends LongArrayList {
+
+ public CustomLongArray() {
+ super();
+ }
+
+ public CustomLongArray(final int expected) {
+ super(expected);
+ }
+
+ public boolean addAll(final CustomLongArray list) {
+ this.addElements(this.size, list.a, 0, list.size);
+ return list.size != 0;
+ }
+
+ public void addUnchecked(final long value) {
+ this.a[this.size++] = value;
+ }
+
+ public void forceSize(final int to) {
+ this.size = to;
+ }
+
+ @Override
+ public int hashCode() {
+ long h = 1L;
+
+ Objects.checkFromToIndex(0, this.size, this.a.length);
+
+ for (int i = 0; i < this.size; ++i) {
+ h = HashCommon.mix(h + this.a[i]);
+ }
+
+ return (int)h;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (o == this) {
+ return true;
+ }
+
+ if (!(o instanceof CustomLongArray other)) {
+ return false;
+ }
+
+ return this.size == other.size && Arrays.equals(this.a, 0, this.size, other.a, 0, this.size);
+ }
+ }
+
+ private static int getDistanceSize(final int radius, final int max) {
+ if (radius == 0) {
+ return 1;
+ }
+ final int diff = radius - max;
+ if (diff <= 0) {
+ return 4*radius;
+ }
+ return 4*(max - Math.max(0, diff - 1));
+ }
+
+ private static int getQ1DistanceSize(final int radius, final int max) {
+ if (radius == 0) {
+ return 1;
+ }
+ final int diff = radius - max;
+ if (diff <= 0) {
+ return radius+1;
+ }
+ return max - diff + 1;
+ }
+
+ private static final class BasicFIFOLQueue {
+
+ private final long[] values;
+ private int head, tail;
+
+ public BasicFIFOLQueue(final int cap) {
+ if (cap <= 1) {
+ throw new IllegalArgumentException();
+ }
+ this.values = new long[cap];
+ }
+
+ public boolean isEmpty() {
+ return this.head == this.tail;
+ }
+
+ public long removeFirst() {
+ final long ret = this.values[this.head];
+
+ if (this.head == this.tail) {
+ throw new IllegalStateException();
+ }
+
+ ++this.head;
+ if (this.head == this.values.length) {
+ this.head = 0;
+ }
+
+ return ret;
+ }
+
+ public void addLast(final long value) {
+ this.values[this.tail++] = value;
+
+ if (this.tail == this.head) {
+ throw new IllegalStateException();
+ }
+
+ if (this.tail == this.values.length) {
+ this.tail = 0;
+ }
+ }
+ }
+
+ private static CustomLongArray[] makeQ1BFS(final int radius) {
+ final CustomLongArray[] ret = new CustomLongArray[2 * radius + 1];
+ final BasicFIFOLQueue queue = new BasicFIFOLQueue(Math.max(1, 4 * radius) + 1);
+ final LongOpenHashSet seen = new LongOpenHashSet((radius + 1) * (radius + 1));
+
+ seen.add(CoordinateUtils.getChunkKey(0, 0));
+ queue.addLast(CoordinateUtils.getChunkKey(0, 0));
+ while (!queue.isEmpty()) {
+ final long chunk = queue.removeFirst();
+ final int chunkX = CoordinateUtils.getChunkX(chunk);
+ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
+
+ final int index = Math.abs(chunkX) + Math.abs(chunkZ);
+ final CustomLongArray list = ret[index];
+ if (list != null) {
+ list.addUnchecked(chunk);
+ } else {
+ (ret[index] = new CustomLongArray(getQ1DistanceSize(index, radius))).addUnchecked(chunk);
+ }
+
+ for (int i = 0; i < 4; ++i) {
+ // 0 -> -1, 0
+ // 1 -> 0, -1
+ // 2 -> 1, 0
+ // 3 -> 0, 1
+
+ final int signInv = -(i >>> 1); // 2/3 -> -(1), 0/1 -> -(0)
+ // note: -n = (~n) + 1
+ // (n ^ signInv) - signInv = signInv == 0 ? ((n ^ 0) - 0 = n) : ((n ^ -1) - (-1) = ~n + 1)
+
+ final int axis = i & 1; // 0/2 -> 0, 1/3 -> 1
+ final int dx = ((axis - 1) ^ signInv) - signInv; // 0 -> -1, 1 -> 0
+ final int dz = (-axis ^ signInv) - signInv; // 0 -> 0, 1 -> -1
+
+ final int neighbourX = chunkX + dx;
+ final int neighbourZ = chunkZ + dz;
+ final long neighbour = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
+
+ if ((neighbourX | neighbourZ) < 0 || Math.max(Math.abs(neighbourX), Math.abs(neighbourZ)) > radius) {
+ // don't enqueue out of range
+ continue;
+ }
+
+ if (!seen.add(neighbour)) {
+ continue;
+ }
+
+ queue.addLast(neighbour);
+ }
+ }
+
+ return ret;
+ }
+
+ // doesn't appear worth optimising this function now, even though it's 70% of the call
+ private static CustomLongArray spread(final CustomLongArray input, final int size) {
+ final LongLinkedOpenHashSet notAdded = new LongLinkedOpenHashSet(input);
+ final CustomLongArray added = new CustomLongArray(size);
+
+ while (!notAdded.isEmpty()) {
+ if (added.isEmpty()) {
+ added.addUnchecked(notAdded.removeLastLong());
+ continue;
+ }
+
+ long maxChunk = -1L;
+ int maxDist = 0;
+
+ // select the chunk from the not yet added set that has the largest minimum distance from
+ // the current set of added chunks
+
+ for (final LongIterator iterator = notAdded.iterator(); iterator.hasNext();) {
+ final long chunkKey = iterator.nextLong();
+ final int chunkX = CoordinateUtils.getChunkX(chunkKey);
+ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey);
+
+ int minDist = Integer.MAX_VALUE;
+
+ final int len = added.size();
+ final long[] addedArr = added.elements();
+ Objects.checkFromToIndex(0, len, addedArr.length);
+ for (int i = 0; i < len; ++i) {
+ final long addedKey = addedArr[i];
+ final int addedX = CoordinateUtils.getChunkX(addedKey);
+ final int addedZ = CoordinateUtils.getChunkZ(addedKey);
+
+ // here we use square distance because chunk generation uses neighbours in a square radius
+ final int dist = Math.max(Math.abs(addedX - chunkX), Math.abs(addedZ - chunkZ));
+
+ minDist = Math.min(dist, minDist);
+ }
+
+ if (minDist > maxDist) {
+ maxDist = minDist;
+ maxChunk = chunkKey;
+ }
+ }
+
+ // move the selected chunk from the not added set to the added set
+
+ if (!notAdded.remove(maxChunk)) {
+ throw new IllegalStateException();
+ }
+
+ added.addUnchecked(maxChunk);
+ }
+
+ return added;
+ }
+
+ private static void expandQuadrants(final CustomLongArray input, final int size) {
+ final int len = input.size();
+ final long[] array = input.elements();
+
+ int writeIndex = size - 1;
+ for (int i = len - 1; i >= 0; --i) {
+ final long key = array[i];
+ final int chunkX = CoordinateUtils.getChunkX(key);
+ final int chunkZ = CoordinateUtils.getChunkZ(key);
+
+ if ((chunkX | chunkZ) < 0 || (i != 0 && chunkX == 0 && chunkZ == 0)) {
+ throw new IllegalStateException();
+ }
+
+ // Q4
+ if (chunkZ != 0) {
+ array[writeIndex--] = CoordinateUtils.getChunkKey(chunkX, -chunkZ);
+ }
+ // Q3
+ if (chunkX != 0 && chunkZ != 0) {
+ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, -chunkZ);
+ }
+ // Q2
+ if (chunkX != 0) {
+ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, chunkZ);
+ }
+
+ array[writeIndex--] = key;
+ }
+
+ input.forceSize(size);
+
+ if (writeIndex != -1) {
+ throw new IllegalStateException();
+ }
+ }
+
+ private static long[] generateBFSOrder(final int radius) {
+ // by using only the first quadrant, we can reduce the total element size by 4 when spreading
+ final CustomLongArray[] byDistance = makeQ1BFS(radius);
+
+ // to increase generation parallelism, we want to space the chunks out so that they are not nearby when generating
+ // this also means we are minimising locality
+ // but, we need to maintain sorted order by manhatten distance
+
+ // per manhatten distance we transform the chunk list so that each element is maximally spaced out from each other
+ for (int i = 0, len = byDistance.length; i < len; ++i) {
+ final CustomLongArray points = byDistance[i];
+ final int expectedSize = getDistanceSize(i, radius);
+
+ final CustomLongArray spread = spread(points, expectedSize);
+ // add in Q2, Q3, Q4
+ expandQuadrants(spread, expectedSize);
+
+ byDistance[i] = spread;
+ }
+
+ // now, rebuild the list so that it still maintains manhatten distance order
+ final CustomLongArray ret = new CustomLongArray((2 * radius + 1) * (2 * radius + 1));
+
+ for (final CustomLongArray dist : byDistance) {
+ ret.addAll(dist);
+ }
+
+ return ret.elements();
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java
new file mode 100644
index 0000000000000000000000000000000000000000..ea6b6ed27b212719feb31610faac974899688839
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java
@@ -0,0 +1,12 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.world;
+
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.phys.AABB;
+import java.util.List;
+import java.util.function.Predicate;
+
+public interface ChunkSystemEntityGetter {
+
+ public List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate);
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java
new file mode 100644
index 0000000000000000000000000000000000000000..4b9e2fa963c14f65f15407c1814c543c2999ea32
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java
@@ -0,0 +1,11 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.world;
+
+import net.minecraft.world.level.chunk.LevelChunk;
+
+public interface ChunkSystemServerChunkCache {
+
+ public void moonrise$setFullChunk(final int chunkX, final int chunkZ, final LevelChunk chunk);
+
+ public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ);
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java
new file mode 100644
index 0000000000000000000000000000000000000000..e97e7d276faf055c89207385d3820debffb06463
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java
@@ -0,0 +1,7 @@
+package ca.spottedleaf.moonrise.patches.chunk_tick_iteration;
+
+public final class ChunkTickConstants {
+
+ public static final int PLAYER_SPAWN_TRACK_RANGE = 8;
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..f28fd0e01e2bdda0daf9d775e514a7253d32d8d0
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java
@@ -0,0 +1,16 @@
+package ca.spottedleaf.moonrise.patches.chunk_tick_iteration;
+
+import net.minecraft.core.SectionPos;
+import net.minecraft.server.level.ServerPlayer;
+
+public interface ChunkTickDistanceManager {
+
+ public void moonrise$addPlayer(final ServerPlayer player, final SectionPos pos);
+
+ public void moonrise$removePlayer(final ServerPlayer player, final SectionPos pos);
+
+ public void moonrise$updatePlayer(final ServerPlayer player,
+ final SectionPos oldPos, final SectionPos newPos,
+ final boolean oldIgnore, final boolean newIgnore);
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a27385126
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
@@ -0,0 +1,1853 @@
+package ca.spottedleaf.moonrise.patches.collisions;
+
+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 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;
+ }
+
+ public static boolean isEmpty(final net.minecraft.world.phys.AABB aabb) {
+ return (aabb.maxX - aabb.minX) < COLLISION_EPSILON || (aabb.maxY - aabb.minY) < COLLISION_EPSILON || (aabb.maxZ - aabb.minZ) < COLLISION_EPSILON;
+ }
+
+ public static boolean isEmpty(final double minX, final double minY, final double minZ,
+ final double maxX, final double maxY, final double maxZ) {
+ 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) {
+ 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,
+ x + (16.0 + 3*COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*COLLISION_EPSILON));
+ }
+
+ /*
+ A couple of rules for VoxelShape collisions:
+ Two shapes only intersect if they are actually more than EPSILON units into each other. This also applies to movement
+ checks.
+ If the two shapes strictly collide, then the return value of a collide call will return a value in the opposite
+ direction of the source move. However, this value will not be greater in magnitude than EPSILON. Collision code
+ will automatically round it to 0.
+ */
+
+ public static boolean voxelShapeIntersect(final double minX1, final double minY1, final double minZ1, final double maxX1,
+ final double maxY1, final double maxZ1, final double minX2, final double minY2,
+ final double minZ2, final double maxX2, final double maxY2, final double maxZ2) {
+ return (minX1 - maxX2) < -COLLISION_EPSILON && (maxX1 - minX2) > COLLISION_EPSILON &&
+ (minY1 - maxY2) < -COLLISION_EPSILON && (maxY1 - minY2) > COLLISION_EPSILON &&
+ (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,
+ 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) {
+ 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) {
+ 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) {
+ final double max_move = target.minX - source.maxX; // < 0.0 if no strict collision
+ if (max_move < -COLLISION_EPSILON) {
+ return source_move;
+ }
+ return Math.min(max_move, source_move);
+ } else {
+ final double max_move = target.maxX - source.minX; // > 0.0 if no strict collision
+ if (max_move > COLLISION_EPSILON) {
+ return source_move;
+ }
+ return Math.max(max_move, source_move);
+ }
+ }
+ return source_move;
+ }
+
+ // 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) {
+ 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) {
+ final double max_move = target.minY - source.maxY; // < 0.0 if no strict collision
+ if (max_move < -COLLISION_EPSILON) {
+ return source_move;
+ }
+ return Math.min(max_move, source_move);
+ } else {
+ final double max_move = target.maxY - source.minY; // > 0.0 if no strict collision
+ if (max_move > COLLISION_EPSILON) {
+ return source_move;
+ }
+ return Math.max(max_move, source_move);
+ }
+ }
+ return source_move;
+ }
+
+ // 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) {
+ 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) {
+ final double max_move = target.minZ - source.maxZ; // < 0.0 if no strict collision
+ if (max_move < -COLLISION_EPSILON) {
+ return source_move;
+ }
+ return Math.min(max_move, source_move);
+ } else {
+ final double max_move = target.maxZ - source.minZ; // > 0.0 if no strict collision
+ if (max_move > COLLISION_EPSILON) {
+ return source_move;
+ }
+ return Math.max(max_move, source_move);
+ }
+ }
+ return source_move;
+ }
+
+ // 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) {
+ do {
+ final int middle = (startIndex + endIndex) >>> 1;
+ final double middleVal = values[middle];
+
+ if (value < middleVal) {
+ endIndex = middle - 1;
+ } else {
+ startIndex = middle + 1;
+ }
+ } while (startIndex <= endIndex);
+
+ return startIndex - 1;
+ }
+
+ public static boolean voxelShapeIntersectNoEmpty(final net.minecraft.world.phys.shapes.VoxelShape voxel, final net.minecraft.world.phys.AABB aabb) {
+ if (voxel.isEmpty()) {
+ return false;
+ }
+
+ // 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[] 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).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();
+
+ // note: voxel bitset with set index (x, y, z) indicates that
+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
+
+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is
+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that)
+ // note: for intersection, one we find the floor of the min we can use that as the start index
+ // for the next check as source max >= source min
+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
+ // as this implies that coords[coords.length - 1] < source min
+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
+
+ final int floor_min_x = Math.max(
+ 0,
+ findFloor(coords_x, (aabb.minX - off_x) + COLLISION_EPSILON, 0, size_x)
+ );
+ if (floor_min_x >= size_x) {
+ // cannot intersect
+ return false;
+ }
+
+ final int ceil_max_x = Math.min(
+ size_x,
+ findFloor(coords_x, (aabb.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1
+ );
+ if (floor_min_x >= ceil_max_x) {
+ // cannot intersect
+ return false;
+ }
+
+ final int floor_min_y = Math.max(
+ 0,
+ findFloor(coords_y, (aabb.minY - off_y) + COLLISION_EPSILON, 0, size_y)
+ );
+ if (floor_min_y >= size_y) {
+ // cannot intersect
+ return false;
+ }
+
+ final int ceil_max_y = Math.min(
+ size_y,
+ findFloor(coords_y, (aabb.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1
+ );
+ if (floor_min_y >= ceil_max_y) {
+ // cannot intersect
+ return false;
+ }
+
+ final int floor_min_z = Math.max(
+ 0,
+ findFloor(coords_z, (aabb.minZ - off_z) + COLLISION_EPSILON, 0, size_z)
+ );
+ if (floor_min_z >= size_z) {
+ // cannot intersect
+ return false;
+ }
+
+ final int ceil_max_z = Math.min(
+ size_z,
+ findFloor(coords_z, (aabb.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1
+ );
+ if (floor_min_z >= ceil_max_z) {
+ // cannot intersect
+ return false;
+ }
+
+ final long[] bitset = cached_shape_data.voxelSet();
+
+ // check bitset to check if any shapes in range are full
+
+ final int mul_x = size_y*size_z;
+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
+ // note: JLS states long shift operators ANDS shift by 63
+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ // 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();
+ 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[] 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).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();
+
+ // note: voxel bitset with set index (x, y, z) indicates that
+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
+
+
+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is
+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that)
+ // note: for intersection, one we find the floor of the min we can use that as the start index
+ // for the next check as source max >= source min
+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
+ // as this implies that coords[coords.length - 1] < source min
+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
+
+ final int floor_min_y = Math.max(
+ 0,
+ findFloor(coords_y, (source.minY - off_y) + COLLISION_EPSILON, 0, size_y)
+ );
+ if (floor_min_y >= size_y) {
+ // cannot intersect
+ return source_move;
+ }
+
+ final int ceil_max_y = Math.min(
+ size_y,
+ findFloor(coords_y, (source.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1
+ );
+ if (floor_min_y >= ceil_max_y) {
+ // cannot intersect
+ return source_move;
+ }
+
+ final int floor_min_z = Math.max(
+ 0,
+ findFloor(coords_z, (source.minZ - off_z) + COLLISION_EPSILON, 0, size_z)
+ );
+ if (floor_min_z >= size_z) {
+ // cannot intersect
+ return source_move;
+ }
+
+ final int ceil_max_z = Math.min(
+ size_z,
+ findFloor(coords_z, (source.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1
+ );
+ if (floor_min_z >= ceil_max_z) {
+ // cannot intersect
+ return source_move;
+ }
+
+ // index = z + y*size_z + x*(size_z*size_y)
+
+ final long[] bitset = cached_shape_data.voxelSet();
+
+ if (source_move > 0.0) {
+ final double source_max = source.maxX - off_x;
+ final int ceil_max_x = findFloor(
+ coords_x, source_max - COLLISION_EPSILON, 0, size_x
+ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max
+
+ // note: only the order of the first loop matters
+
+ // note: we cannot collide with the face at index size on the collision axis for forward movement
+
+ final int mul_x = size_y*size_z;
+ for (int curr_x = ceil_max_x; curr_x < size_x; ++curr_x) {
+ double max_dist = coords_x[curr_x] - source_max;
+ if (max_dist >= source_move) {
+ // if we reach here, then we will never have a case where
+ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1]
+ // thus, we can return immediately
+
+ // this optimization is important since this loop is bounded by size, and _not_ by
+ // a calculated max index based off of source_move - so it would be possible to check
+ // the whole intersected shape for collisions when we didn't need to!
+ return source_move;
+ }
+ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
+ max_dist = Math.min(max_dist, source_move);
+ }
+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
+ // note: JLS states long shift operators ANDS shift by 63
+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
+ return max_dist;
+ }
+ }
+ }
+ }
+
+ return source_move;
+ } else {
+ final double source_min = source.minX - off_x;
+ final int floor_min_x = findFloor(
+ coords_x, source_min + COLLISION_EPSILON, 0, size_x
+ );
+
+ // note: only the order of the first loop matters
+
+ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement
+
+ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the
+ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1]
+ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid
+ final int mul_x = size_y*size_z;
+ for (int curr_x = floor_min_x - 1; curr_x >= 0; --curr_x) {
+ double max_dist = coords_x[curr_x + 1] - source_min;
+ if (max_dist <= source_move) {
+ // if we reach here, then we will never have a case where
+ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1]
+ // thus, we can return immediately
+
+ // this optimization is important since this loop is possibly bounded by size, and _not_ by
+ // a calculated max index based off of source_move - so it would be possible to check
+ // the whole intersected shape for collisions when we didn't need to!
+ return source_move;
+ }
+ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
+ max_dist = Math.max(max_dist, source_move);
+ }
+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
+ // note: JLS states long shift operators ANDS shift by 63
+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
+ return max_dist;
+ }
+ }
+ }
+ }
+
+ return source_move;
+ }
+ }
+
+ 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();
+ 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[] 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).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();
+
+ // note: voxel bitset with set index (x, y, z) indicates that
+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
+
+
+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is
+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that)
+ // note: for intersection, one we find the floor of the min we can use that as the start index
+ // for the next check as source max >= source min
+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
+ // as this implies that coords[coords.length - 1] < source min
+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
+
+ final int floor_min_x = Math.max(
+ 0,
+ findFloor(coords_x, (source.minX - off_x) + COLLISION_EPSILON, 0, size_x)
+ );
+ if (floor_min_x >= size_x) {
+ // cannot intersect
+ return source_move;
+ }
+
+ final int ceil_max_x = Math.min(
+ size_x,
+ findFloor(coords_x, (source.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1
+ );
+ if (floor_min_x >= ceil_max_x) {
+ // cannot intersect
+ return source_move;
+ }
+
+ final int floor_min_z = Math.max(
+ 0,
+ findFloor(coords_z, (source.minZ - off_z) + COLLISION_EPSILON, 0, size_z)
+ );
+ if (floor_min_z >= size_z) {
+ // cannot intersect
+ return source_move;
+ }
+
+ final int ceil_max_z = Math.min(
+ size_z,
+ findFloor(coords_z, (source.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1
+ );
+ if (floor_min_z >= ceil_max_z) {
+ // cannot intersect
+ return source_move;
+ }
+
+ // index = z + y*size_z + x*(size_z*size_y)
+
+ final long[] bitset = cached_shape_data.voxelSet();
+
+ if (source_move > 0.0) {
+ final double source_max = source.maxY - off_y;
+ final int ceil_max_y = findFloor(
+ coords_y, source_max - COLLISION_EPSILON, 0, size_y
+ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max
+
+ // note: only the order of the first loop matters
+
+ // note: we cannot collide with the face at index size on the collision axis for forward movement
+
+ final int mul_x = size_y*size_z;
+ for (int curr_y = ceil_max_y; curr_y < size_y; ++curr_y) {
+ double max_dist = coords_y[curr_y] - source_max;
+ if (max_dist >= source_move) {
+ // if we reach here, then we will never have a case where
+ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1]
+ // thus, we can return immediately
+
+ // this optimization is important since this loop is bounded by size, and _not_ by
+ // a calculated max index based off of source_move - so it would be possible to check
+ // the whole intersected shape for collisions when we didn't need to!
+ return source_move;
+ }
+ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
+ max_dist = Math.min(max_dist, source_move);
+ }
+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
+ // note: JLS states long shift operators ANDS shift by 63
+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
+ return max_dist;
+ }
+ }
+ }
+ }
+
+ return source_move;
+ } else {
+ final double source_min = source.minY - off_y;
+ final int floor_min_y = findFloor(
+ coords_y, source_min + COLLISION_EPSILON, 0, size_y
+ );
+
+ // note: only the order of the first loop matters
+
+ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement
+
+ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the
+ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1]
+ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid
+ final int mul_x = size_y*size_z;
+ for (int curr_y = floor_min_y - 1; curr_y >= 0; --curr_y) {
+ double max_dist = coords_y[curr_y + 1] - source_min;
+ if (max_dist <= source_move) {
+ // if we reach here, then we will never have a case where
+ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1]
+ // thus, we can return immediately
+
+ // this optimization is important since this loop is possibly bounded by size, and _not_ by
+ // a calculated max index based off of source_move - so it would be possible to check
+ // the whole intersected shape for collisions when we didn't need to!
+ return source_move;
+ }
+ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
+ max_dist = Math.max(max_dist, source_move);
+ }
+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
+ // note: JLS states long shift operators ANDS shift by 63
+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
+ return max_dist;
+ }
+ }
+ }
+ }
+
+ return source_move;
+ }
+ }
+
+ 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();
+ 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[] 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).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();
+
+ // note: voxel bitset with set index (x, y, z) indicates that
+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
+
+
+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is
+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that)
+ // note: for intersection, one we find the floor of the min we can use that as the start index
+ // for the next check as source max >= source min
+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
+ // as this implies that coords[coords.length - 1] < source min
+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
+
+ final int floor_min_x = Math.max(
+ 0,
+ findFloor(coords_x, (source.minX - off_x) + COLLISION_EPSILON, 0, size_x)
+ );
+ if (floor_min_x >= size_x) {
+ // cannot intersect
+ return source_move;
+ }
+
+ final int ceil_max_x = Math.min(
+ size_x,
+ findFloor(coords_x, (source.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1
+ );
+ if (floor_min_x >= ceil_max_x) {
+ // cannot intersect
+ return source_move;
+ }
+
+ final int floor_min_y = Math.max(
+ 0,
+ findFloor(coords_y, (source.minY - off_y) + COLLISION_EPSILON, 0, size_y)
+ );
+ if (floor_min_y >= size_y) {
+ // cannot intersect
+ return source_move;
+ }
+
+ final int ceil_max_y = Math.min(
+ size_y,
+ findFloor(coords_y, (source.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1
+ );
+ if (floor_min_y >= ceil_max_y) {
+ // cannot intersect
+ return source_move;
+ }
+
+ // index = z + y*size_z + x*(size_z*size_y)
+
+ final long[] bitset = cached_shape_data.voxelSet();
+
+ if (source_move > 0.0) {
+ final double source_max = source.maxZ - off_z;
+ final int ceil_max_z = findFloor(
+ coords_z, source_max - COLLISION_EPSILON, 0, size_z
+ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max
+
+ // note: only the order of the first loop matters
+
+ // note: we cannot collide with the face at index size on the collision axis for forward movement
+
+ final int mul_x = size_y*size_z;
+ for (int curr_z = ceil_max_z; curr_z < size_z; ++curr_z) {
+ double max_dist = coords_z[curr_z] - source_max;
+ if (max_dist >= source_move) {
+ // if we reach here, then we will never have a case where
+ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1]
+ // thus, we can return immediately
+
+ // this optimization is important since this loop is bounded by size, and _not_ by
+ // a calculated max index based off of source_move - so it would be possible to check
+ // the whole intersected shape for collisions when we didn't need to!
+ return source_move;
+ }
+ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
+ max_dist = Math.min(max_dist, source_move);
+ }
+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
+ // note: JLS states long shift operators ANDS shift by 63
+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
+ return max_dist;
+ }
+ }
+ }
+ }
+
+ return source_move;
+ } else {
+ final double source_min = source.minZ - off_z;
+ final int floor_min_z = findFloor(
+ coords_z, source_min + COLLISION_EPSILON, 0, size_z
+ );
+
+ // note: only the order of the first loop matters
+
+ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement
+
+ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the
+ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1]
+ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid
+ final int mul_x = size_y*size_z;
+ for (int curr_z = floor_min_z - 1; curr_z >= 0; --curr_z) {
+ double max_dist = coords_z[curr_z + 1] - source_min;
+ if (max_dist <= source_move) {
+ // if we reach here, then we will never have a case where
+ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1]
+ // thus, we can return immediately
+
+ // this optimization is important since this loop is possibly bounded by size, and _not_ by
+ // a calculated max index based off of source_move - so it would be possible to check
+ // the whole intersected shape for collisions when we didn't need to!
+ return source_move;
+ }
+ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
+ max_dist = Math.max(max_dist, source_move);
+ }
+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
+ // note: JLS states long shift operators ANDS shift by 63
+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
+ return max_dist;
+ }
+ }
+ }
+ }
+
+ return source_move;
+ }
+ }
+
+ // does not use epsilon
+ public static boolean strictlyContains(final net.minecraft.world.phys.shapes.VoxelShape voxel, final net.minecraft.world.phys.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();
+ if (single_aabb != null) {
+ return single_aabb.contains(x, y, z);
+ }
+
+ if (voxel.isEmpty()) {
+ // bitset is clear, no point in searching
+ return false;
+ }
+
+ // 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();
+
+ 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).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();
+
+ // note: should mirror AABB#contains, which is that for any point X that X >= min and X < max.
+ // specifically, it cannot collide on the max bounds of the shape
+
+ final int index_x = findFloor(coords_x, x, 0, size_x);
+ if (index_x < 0 || index_x >= size_x) {
+ return false;
+ }
+
+ final int index_y = findFloor(coords_y, y, 0, size_y);
+ if (index_y < 0 || index_y >= size_y) {
+ return false;
+ }
+
+ final int index_z = findFloor(coords_z, z, 0, size_z);
+ if (index_z < 0 || index_z >= size_z) {
+ return false;
+ }
+
+ // index = z + y*size_z + x*(size_z*size_y)
+
+ final int index = index_z + index_y*size_z + index_x*(size_z*size_y);
+
+ final long[] bitset = cached_shape_data.voxelSet();
+
+ return (bitset[index >>> 6] & (1L << index)) != 0L;
+ }
+
+ private static int makeBitset(final boolean ft, final boolean tf, final boolean tt) {
+ // idx ff -> 0
+ // idx ft -> 1
+ // idx tf -> 2
+ // idx tt -> 3
+ 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) {
+ final int sizeX = mergedX.voxels;
+ final int sizeY = mergedY.voxels;
+ final int sizeZ = mergedZ.voxels;
+
+ final long[] s1Voxels = shapeDataFirst.voxelSet();
+ final long[] s2Voxels = shapeDataSecond.voxelSet();
+
+ final int s1Mul1 = shapeDataFirst.sizeZ();
+ final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY();
+
+ final int s2Mul1 = shapeDataSecond.sizeZ();
+ 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);
+
+ boolean empty = true;
+
+ int mergedIdx = 0;
+ for (int idxX = 0; idxX < sizeX; ++idxX) {
+ final int s1x = mergedX.firstIndices[idxX];
+ final int s2x = mergedX.secondIndices[idxX];
+ boolean setX = false;
+ for (int idxY = 0; idxY < sizeY; ++idxY) {
+ final int s1y = mergedY.firstIndices[idxY];
+ final int s2y = mergedY.secondIndices[idxY];
+ boolean setY = false;
+ for (int idxZ = 0; idxZ < sizeZ; ++idxZ) {
+ final int s1z = mergedZ.firstIndices[idxZ];
+ final int s2z = mergedZ.secondIndices[idxZ];
+
+ int idx;
+
+ 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);
+
+ // idx ff -> 0
+ // idx ft -> 1
+ // idx tf -> 2
+ // idx tt -> 3
+
+ final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0;
+ setY |= res;
+ setX |= res;
+
+ if (res) {
+ empty = false;
+ // inline and optimize fill operation
+ ret.zMin = Math.min(ret.zMin, idxZ);
+ ret.zMax = Math.max(ret.zMax, idxZ + 1);
+ ret.storage.set(mergedIdx);
+ }
+
+ ++mergedIdx;
+ }
+ if (setY) {
+ ret.yMin = Math.min(ret.yMin, idxY);
+ ret.yMax = Math.max(ret.yMax, idxY + 1);
+ }
+ }
+ if (setX) {
+ ret.xMin = Math.min(ret.xMin, idxX);
+ ret.xMax = Math.max(ret.xMax, idxX + 1);
+ }
+ }
+
+ 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,
+ final int booleanOp) {
+ final int sizeX = mergedX.voxels;
+ final int sizeY = mergedY.voxels;
+ final int sizeZ = mergedZ.voxels;
+
+ final long[] s1Voxels = shapeDataFirst.voxelSet();
+ final long[] s2Voxels = shapeDataSecond.voxelSet();
+
+ final int s1Mul1 = shapeDataFirst.sizeZ();
+ final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY();
+
+ final int s2Mul1 = shapeDataSecond.sizeZ();
+ final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY();
+
+ // note: indices may contain -1, but nothing > size
+ for (int idxX = 0; idxX < sizeX; ++idxX) {
+ final int s1x = mergedX.firstIndices[idxX];
+ final int s2x = mergedX.secondIndices[idxX];
+ for (int idxY = 0; idxY < sizeY; ++idxY) {
+ final int s1y = mergedY.firstIndices[idxY];
+ final int s2y = mergedY.secondIndices[idxY];
+ for (int idxZ = 0; idxZ < sizeZ; ++idxZ) {
+ final int s1z = mergedZ.firstIndices[idxZ];
+ final int s2z = mergedZ.secondIndices[idxZ];
+
+ int idx;
+
+ 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);
+
+ // idx ff -> 0
+ // idx ft -> 1
+ // idx tf -> 2
+ // idx tt -> 3
+
+ final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0;
+
+ if (res) {
+ return false;
+ }
+ }
+ }
+ }
+
+ 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) {
+ 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) {
+ final boolean ff = operator.apply(false, false);
+ if (ff) {
+ // technically, should be an infinite box but that's clearly an error
+ throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true");
+ }
+
+ final boolean tt = operator.apply(true, true);
+
+ if (first == second) {
+ return tt ? first : net.minecraft.world.phys.shapes.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();
+ }
+ if (second.isEmpty()) {
+ return tf ? first : net.minecraft.world.phys.shapes.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 boolean intersect;
+
+ final boolean hasAABBF = aabbF != null;
+ final boolean hasAABBS = aabbS != null;
+ if (hasAABBF | hasAABBS) {
+ if (hasAABBF & hasAABBS) {
+ intersect = voxelShapeIntersect(aabbF, aabbS);
+ } else if (hasAABBF) {
+ intersect = voxelShapeIntersectNoEmpty(second, aabbF);
+ } else {
+ intersect = voxelShapeIntersectNoEmpty(first, aabbS);
+ }
+ } else {
+ // expect cached bounds
+ intersect = voxelShapeIntersect(first.bounds(), second.bounds());
+ }
+
+ if (!intersect) {
+ if (!tf & !ft) {
+ return net.minecraft.world.phys.shapes.Shapes.empty();
+ }
+ if (!tf | !ft) {
+ return tf ? first : second;
+ }
+ }
+ }
+
+ 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(),
+ ft, tf
+ );
+ if (mergedX == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
+ return net.minecraft.world.phys.shapes.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(),
+ ft, tf
+ );
+ if (mergedY == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
+ return net.minecraft.world.phys.shapes.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(),
+ ft, tf
+ );
+ if (mergedZ == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
+ return net.minecraft.world.phys.shapes.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 net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape mergedShape = merge(
+ shapeDataFirst, shapeDataSecond,
+ mergedX, mergedY, mergedZ,
+ makeBitset(ft, tf, tt)
+ );
+
+ if (mergedShape == null) {
+ return net.minecraft.world.phys.shapes.Shapes.empty();
+ }
+
+ return new net.minecraft.world.phys.shapes.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) {
+ final boolean ff = operator.apply(false, false);
+ if (ff) {
+ // technically, should be an infinite box but that's clearly an error
+ throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true");
+ }
+ final boolean firstEmpty = first.isEmpty();
+ final boolean secondEmpty = second.isEmpty();
+ if (firstEmpty | secondEmpty) {
+ return operator.apply(!firstEmpty, !secondEmpty);
+ }
+
+ final boolean tt = operator.apply(true, true);
+
+ if (first == second) {
+ return tt;
+ }
+
+ final boolean ft = operator.apply(false, true);
+ 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 boolean intersect;
+
+ final boolean hasAABBF = aabbF != null;
+ final boolean hasAABBS = aabbS != null;
+ if (hasAABBF | hasAABBS) {
+ if (hasAABBF & hasAABBS) {
+ intersect = voxelShapeIntersect(aabbF, aabbS);
+ } else if (hasAABBF) {
+ intersect = voxelShapeIntersectNoEmpty(second, aabbF);
+ } else {
+ // hasAABBS -> true
+ intersect = voxelShapeIntersectNoEmpty(first, aabbS);
+ }
+
+ if (!intersect) {
+ // is only non-empty if we take from first or second, as there is no overlap AND both shapes are non-empty
+ return tf | ft;
+ } else if (tt) {
+ // intersect = true && tt = true -> non-empty merged shape
+ return true;
+ }
+ } else {
+ // expect cached bounds
+ intersect = voxelShapeIntersect(first.bounds(), second.bounds());
+ if (!intersect) {
+ // is only non-empty if we take from first or second, as there is no intersection
+ return tf | ft;
+ }
+ }
+
+ 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(),
+ ft, tf
+ );
+ if (mergedX == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
+ 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(),
+ ft, tf
+ );
+ if (mergedY == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
+ 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(),
+ ft, tf
+ );
+ if (mergedZ == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
+ 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();
+
+ return !isMergeEmpty(
+ shapeDataFirst, shapeDataSecond,
+ mergedX, mergedY, mergedZ,
+ makeBitset(ft, tf, tt)
+ );
+ }
+
+ private static final class MergedVoxelCoordinateList {
+
+ private static final int[][] SIMPLE_INDICES_CACHE = new int[64][];
+ static {
+ for (int i = 0; i < SIMPLE_INDICES_CACHE.length; ++i) {
+ SIMPLE_INDICES_CACHE[i] = getIndices(i);
+ }
+ }
+
+ 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];
+
+ for (int i = 1; i < length; ++i) {
+ ret[i] = i;
+ }
+
+ return ret;
+ }
+
+ // indices above voxel size are always set to -1
+ public final double[] coordinates;
+ public final double coordinateOffset;
+ public final int[] firstIndices;
+ public final int[] secondIndices;
+ public final int voxels;
+
+ private MergedVoxelCoordinateList(final double[] coordinates, final double coordinateOffset,
+ final int[] firstIndices, final int[] secondIndices, final int voxels) {
+ this.coordinates = coordinates;
+ this.coordinateOffset = coordinateOffset;
+ this.firstIndices = firstIndices;
+ this.secondIndices = secondIndices;
+ this.voxels = voxels;
+ }
+
+ public it.unimi.dsi.fastutil.doubles.DoubleList wrapCoords() {
+ if (this.coordinateOffset == 0.0) {
+ return it.unimi.dsi.fastutil.doubles.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);
+ }
+
+ // assume coordinates.length > 1
+ public static ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.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);
+ }
+
+ // 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) {
+ if (firstCoordinates == secondCoordinates && firstOffset == secondOffset) {
+ return getForSingle(firstCoordinates, firstOffset);
+ }
+
+ final int firstCount = firstCoordinates.length;
+ final int secondCount = secondCoordinates.length;
+
+ final int voxelsFirst = firstCount - 1;
+ final int voxelsSecond = secondCount - 1;
+
+ final int maxCount = firstCount + secondCount;
+
+ final double[] coordinates = new double[maxCount];
+ final int[] firstIndices = new int[maxCount];
+ final int[] secondIndices = new int[maxCount];
+
+ final boolean notTF = !tf;
+ final boolean notFT = !ft;
+
+ int firstIndex = 0;
+ int secondIndex = 0;
+ int resultSize = 0;
+
+ // note: operations on NaN are false
+ double last = Double.NaN;
+
+ for (;;) {
+ final boolean noneLeftFirst = firstIndex >= firstCount;
+ final boolean noneLeftSecond = secondIndex >= secondCount;
+
+ if ((noneLeftFirst & noneLeftSecond) | (noneLeftSecond & notTF) | (noneLeftFirst & notFT)) {
+ break;
+ }
+
+ final boolean firstZero = firstIndex == 0;
+ final boolean secondZero = secondIndex == 0;
+
+ final double select;
+
+ if (noneLeftFirst) {
+ // noneLeftSecond -> false
+ // notFT -> false
+ select = secondCoordinates[secondIndex] + secondOffset;
+ ++secondIndex;
+ } else if (noneLeftSecond) {
+ // noneLeftFirst -> false
+ // notTF -> false
+ select = firstCoordinates[firstIndex] + firstOffset;
+ ++firstIndex;
+ } else {
+ // noneLeftFirst | noneLeftSecond -> false
+ // notTF -> ??
+ // notFT -> ??
+ final boolean breakFirst = notTF & secondZero;
+ final boolean breakSecond = notFT & firstZero;
+
+ final double first = firstCoordinates[firstIndex] + firstOffset;
+ final double second = secondCoordinates[secondIndex] + secondOffset;
+ final boolean useFirst = first < (second + COLLISION_EPSILON);
+ final boolean cont = (useFirst & breakFirst) | (!useFirst & breakSecond);
+
+ select = useFirst ? first : second;
+ firstIndex += useFirst ? 1 : 0;
+ secondIndex += 1 ^ (useFirst ? 1 : 0);
+
+ if (cont) {
+ continue;
+ }
+ }
+
+ int prevFirst = firstIndex - 1;
+ prevFirst = prevFirst >= voxelsFirst ? -1 : prevFirst;
+ int prevSecond = secondIndex - 1;
+ prevSecond = prevSecond >= voxelsSecond ? -1 : prevSecond;
+
+ if (last >= (select - COLLISION_EPSILON)) {
+ // note: any operations on NaN is false
+ firstIndices[resultSize - 1] = prevFirst;
+ secondIndices[resultSize - 1] = prevSecond;
+ } else {
+ firstIndices[resultSize] = prevFirst;
+ secondIndices[resultSize] = prevSecond;
+ coordinates[resultSize] = select;
+
+ ++resultSize;
+ last = select;
+ }
+ }
+
+ return resultSize <= 1 ? EMPTY : new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.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();
+
+ final boolean isEmpty1 = cachedShapeData1.isEmpty();
+ final boolean isEmpty2 = cachedShapeData2.isEmpty();
+
+ if (isEmpty1 & isEmpty2) {
+ return true;
+ } else if (isEmpty1 ^ isEmpty2) {
+ return false;
+ }
+
+ if (cachedShapeData1.hasSingleAABB() != cachedShapeData2.hasSingleAABB()) {
+ return false;
+ }
+
+ if (cachedShapeData1.sizeX() != cachedShapeData2.sizeX()) {
+ return false;
+ }
+ if (cachedShapeData1.sizeY() != cachedShapeData2.sizeY()) {
+ return false;
+ }
+ if (cachedShapeData1.sizeZ() != cachedShapeData2.sizeZ()) {
+ return false;
+ }
+
+ return java.util.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) {
+ 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));
+ }
+
+ 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 double performAABBCollisionsX(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.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);
+ value = collideX(target, currentBoundingBox, value);
+ }
+
+ return value;
+ }
+
+ public static double performAABBCollisionsY(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.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);
+ value = collideY(target, currentBoundingBox, value);
+ }
+
+ return value;
+ }
+
+ public static double performAABBCollisionsZ(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.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);
+ value = collideZ(target, currentBoundingBox, value);
+ }
+
+ return 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) {
+ 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);
+ value = collideX(target, currentBoundingBox, value);
+ }
+
+ return 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) {
+ 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);
+ value = collideY(target, currentBoundingBox, value);
+ }
+
+ return 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) {
+ 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);
+ value = collideZ(target, currentBoundingBox, value);
+ }
+
+ return 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) {
+ double x = moveVector.x;
+ double y = moveVector.y;
+ double z = moveVector.z;
+
+ if (y != 0.0) {
+ y = performVoxelCollisionsY(axisalignedbb, y, potentialCollisions);
+ if (y != 0.0) {
+ axisalignedbb = offsetY(axisalignedbb, y);
+ }
+ }
+
+ final boolean xSmaller = Math.abs(x) < Math.abs(z);
+
+ if (xSmaller && z != 0.0) {
+ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions);
+ if (z != 0.0) {
+ axisalignedbb = offsetZ(axisalignedbb, z);
+ }
+ }
+
+ if (x != 0.0) {
+ x = performVoxelCollisionsX(axisalignedbb, x, potentialCollisions);
+ if (!xSmaller && x != 0.0) {
+ axisalignedbb = offsetX(axisalignedbb, x);
+ }
+ }
+
+ if (!xSmaller && z != 0.0) {
+ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions);
+ }
+
+ return new net.minecraft.world.phys.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) {
+ double x = moveVector.x;
+ double y = moveVector.y;
+ double z = moveVector.z;
+
+ if (y != 0.0) {
+ y = performAABBCollisionsY(axisalignedbb, y, potentialCollisions);
+ if (y != 0.0) {
+ axisalignedbb = offsetY(axisalignedbb, y);
+ }
+ }
+
+ final boolean xSmaller = Math.abs(x) < Math.abs(z);
+
+ if (xSmaller && z != 0.0) {
+ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions);
+ if (z != 0.0) {
+ axisalignedbb = offsetZ(axisalignedbb, z);
+ }
+ }
+
+ if (x != 0.0) {
+ x = performAABBCollisionsX(axisalignedbb, x, potentialCollisions);
+ if (!xSmaller && x != 0.0) {
+ axisalignedbb = offsetX(axisalignedbb, x);
+ }
+ }
+
+ if (!xSmaller && z != 0.0) {
+ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions);
+ }
+
+ return new net.minecraft.world.phys.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) {
+ if (voxels.isEmpty()) {
+ // fast track only AABBs
+ return performAABBCollisions(moveVector, axisalignedbb, aabbs);
+ }
+
+ double x = moveVector.x;
+ double y = moveVector.y;
+ double z = moveVector.z;
+
+ if (y != 0.0) {
+ y = performAABBCollisionsY(axisalignedbb, y, aabbs);
+ y = performVoxelCollisionsY(axisalignedbb, y, voxels);
+ if (y != 0.0) {
+ axisalignedbb = offsetY(axisalignedbb, y);
+ }
+ }
+
+ final boolean xSmaller = Math.abs(x) < Math.abs(z);
+
+ if (xSmaller && z != 0.0) {
+ z = performAABBCollisionsZ(axisalignedbb, z, aabbs);
+ z = performVoxelCollisionsZ(axisalignedbb, z, voxels);
+ if (z != 0.0) {
+ axisalignedbb = offsetZ(axisalignedbb, z);
+ }
+ }
+
+ if (x != 0.0) {
+ x = performAABBCollisionsX(axisalignedbb, x, aabbs);
+ x = performVoxelCollisionsX(axisalignedbb, x, voxels);
+ if (!xSmaller && x != 0.0) {
+ axisalignedbb = offsetX(axisalignedbb, x);
+ }
+ }
+
+ if (!xSmaller && z != 0.0) {
+ z = performAABBCollisionsZ(axisalignedbb, z, aabbs);
+ z = performVoxelCollisionsZ(axisalignedbb, z, voxels);
+ }
+
+ return new net.minecraft.world.phys.Vec3(x, y, z);
+ }
+
+ public static boolean isCollidingWithBorder(final net.minecraft.world.level.border.WorldBorder worldborder, final net.minecraft.world.phys.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,
+ final double boxMinX, final double boxMaxX,
+ final double boxMinZ, final double boxMaxZ) {
+ final double borderMinX = Math.floor(worldborder.getMinX()); // -X
+ final double borderMaxX = Math.ceil(worldborder.getMaxX()); // +X
+
+ final double borderMinZ = Math.floor(worldborder.getMinZ()); // -Z
+ 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;
+ }
+
+ /* Math.max/min specify that any NaN argument results in a NaN return, unlike these functions */
+ private static double min(final double x, final double y) {
+ return x < y ? x : y;
+ }
+
+ private static double max(final double x, final double y) {
+ return x > y ? x : y;
+ }
+
+ public static final int COLLISION_FLAG_LOAD_CHUNKS = 1 << 0;
+ public static final int COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS = 1 << 1;
+ 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) {
+ 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)) {
+ if (checkOnly) {
+ return true;
+ } else {
+ final net.minecraft.world.phys.shapes.VoxelShape borderShape = worldBorder.getCollisionShape();
+ intoVoxel.add(borderShape);
+ ret = true;
+ }
+ }
+ }
+
+ final int minSection = ((ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel)world).moonrise$getMinSection();
+
+ 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 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 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 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);
+
+ // special cases:
+ if (minBlockY > maxBlockY) {
+ // no point in checking
+ return ret;
+ }
+
+ 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 boolean loadChunks = (collisionFlags & COLLISION_FLAG_LOAD_CHUNKS) != 0;
+ 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.ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, loadChunks);
+
+ if (chunk == null) {
+ if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) {
+ if (checkOnly) {
+ return true;
+ } else {
+ intoAABB.add(getBoxForChunk(currChunkX, currChunkZ));
+ ret = true;
+ }
+ }
+ continue;
+ }
+
+ final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.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 == null || section.hasOnlyAir()) {
+ // empty
+ continue;
+ }
+
+ final boolean hasSpecial = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getSpecialCollidingBlocks() != 0;
+ 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);
+ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) {
+ final int blockZ = currZ | (currChunkZ << 4);
+ for (int currX = minXIterate; currX <= maxXIterate; ++currX) {
+ final int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8);
+ final int blockX = currX | (currChunkX << 4);
+
+ 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 net.minecraft.world.level.block.state.BlockState blockData = blocks.get(localBlockIndex);
+
+ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$emptyCollisionShape()) {
+ continue;
+ }
+
+ net.minecraft.world.phys.shapes.VoxelShape blockCollision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$getConstantCollisionShape();
+
+ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == net.minecraft.world.level.block.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();
+ if (singleAABB != null) {
+ singleAABB = singleAABB.move((double)blockX, (double)blockY, (double)blockZ);
+ if (!voxelShapeIntersect(aabb, singleAABB)) {
+ continue;
+ }
+
+ if (predicate != null) {
+ mutablePos.set(blockX, blockY, blockZ);
+ if (!predicate.test(blockData, mutablePos)) {
+ continue;
+ }
+ }
+
+ if (checkOnly) {
+ return true;
+ } else {
+ ret = true;
+ intoAABB.add(singleAABB);
+ continue;
+ }
+ }
+
+ if (blockCollision.isEmpty()) {
+ continue;
+ }
+
+ final net.minecraft.world.phys.shapes.VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ);
+
+ if (!voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) {
+ continue;
+ }
+
+ if (predicate != null) {
+ mutablePos.set(blockX, blockY, blockZ);
+ if (!predicate.test(blockData, mutablePos)) {
+ continue;
+ }
+ }
+
+ if (checkOnly) {
+ return true;
+ } else {
+ ret = true;
+ intoVoxel.add(blockCollisionOffset);
+ continue;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ 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) {
+ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0;
+
+ boolean ret = false;
+
+ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with.
+ // 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()) {
+ entities = world.getEntities(entity, aabb, predicate);
+ } else {
+ entities = ((ca.spottedleaf.moonrise.patches.chunk_system.world.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);
+
+ if (otherEntity.isSpectator()) {
+ continue;
+ }
+
+ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) {
+ if (checkOnly) {
+ return true;
+ } else {
+ into.add(otherEntity.getBoundingBox());
+ ret = true;
+ }
+ }
+ }
+
+ 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) {
+ if ((collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0) {
+ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate)
+ || getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate);
+ } else {
+ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate)
+ | getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate);
+ }
+ }
+
+ public static final class LazyEntityCollisionContext extends net.minecraft.world.phys.shapes.EntityCollisionContext {
+
+ private net.minecraft.world.phys.shapes.CollisionContext delegate;
+ private boolean delegated;
+
+ public LazyEntityCollisionContext(final net.minecraft.world.entity.Entity entity) {
+ super(false, 0.0, null, null, entity);
+ }
+
+ public boolean isDelegated() {
+ final boolean delegated = this.delegated;
+ this.delegated = false;
+ return delegated;
+ }
+
+ public net.minecraft.world.phys.shapes.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;
+ }
+
+ @Override
+ public boolean isDescending() {
+ return this.getDelegate().isDescending();
+ }
+
+ @Override
+ public boolean isAbove(final net.minecraft.world.phys.shapes.VoxelShape shape, final net.minecraft.core.BlockPos pos, final boolean defaultValue) {
+ return this.getDelegate().isAbove(shape, pos, defaultValue);
+ }
+
+ @Override
+ public boolean isHoldingItem(final net.minecraft.world.item.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) {
+ return this.getDelegate().canStandOnFluid(state, fluidState);
+ }
+ }
+
+ private CollisionUtil() {
+ throw new RuntimeException();
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java
new file mode 100644
index 0000000000000000000000000000000000000000..eb7200657d5c7ac37ee93868ba43be0aefecac6d
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java
@@ -0,0 +1,23 @@
+package ca.spottedleaf.moonrise.patches.collisions;
+
+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 float resistance;
+ public final boolean outOfWorld;
+ public Boolean shouldExplode; // null -> not called yet
+ public net.minecraft.world.phys.shapes.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) {
+ this.key = key;
+ this.immutablePos = immutablePos;
+ this.blockState = blockState;
+ this.fluidState = fluidState;
+ this.resistance = resistance;
+ this.outOfWorld = outOfWorld;
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..02b29c563a298e06186de010de68a716bccba494
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java
@@ -0,0 +1,26 @@
+package ca.spottedleaf.moonrise.patches.collisions.block;
+
+public interface CollisionBlockState {
+
+ // note: this does not consider canOcclude, it is only based on the cached collision shape (i.e hasCache())
+ // and whether Shapes.faceShapeOccludes(EMPTY, cached shape) is true
+ public boolean moonrise$occludesFullBlock();
+
+ // whether the cached collision shape exists and is empty
+ public boolean moonrise$emptyCollisionShape();
+
+ // indicates that occludesFullBlock is cached for the collision shape
+ public boolean moonrise$hasCache();
+
+ // note: this is HashCommon#murmurHash3(incremental id); and since murmurHash3 has an inverse function the returned
+ // value is still unique
+ public int moonrise$uniqueId1();
+
+ // note: this is HashCommon#murmurHash3(incremental id); and since murmurHash3 has an inverse function the returned
+ // 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();
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java
new file mode 100644
index 0000000000000000000000000000000000000000..5a6b16be4b8c0cc92d017bc592bc4818dba17da7
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java
@@ -0,0 +1,10 @@
+package ca.spottedleaf.moonrise.patches.collisions.shape;
+
+public record CachedShapeData(
+ int sizeX, int sizeY, int sizeZ,
+ long[] voxelSet,
+ int minFullX, int minFullY, int minFullZ,
+ int maxFullX, int maxFullY, int maxFullZ,
+ boolean isEmpty, boolean hasSingleAABB
+) {
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..5fe1dad9dad368911aedbe6ba7fcd8f9b0189d32
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java
@@ -0,0 +1,35 @@
+package ca.spottedleaf.moonrise.patches.collisions.shape;
+
+public record CachedToAABBs(
+ java.util.List<net.minecraft.world.phys.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;
+ 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());
+
+ 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);
+ }
+
+ 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) {
+ if (offX == 0.0 && offY == 0.0 && offZ == 0.0) {
+ return cache;
+ }
+
+ final double resX = cache.offX + offX;
+ 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);
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..a09efadea9b733840bbe69830dd8f2a303fe656f
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java
@@ -0,0 +1,7 @@
+package ca.spottedleaf.moonrise.patches.collisions.shape;
+
+public interface CollisionDiscreteVoxelShape {
+
+ public ca.spottedleaf.moonrise.patches.collisions.shape.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
new file mode 100644
index 0000000000000000000000000000000000000000..70371eb87c11a106e8513cdbc8d938dda088f745
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java
@@ -0,0 +1,36 @@
+package ca.spottedleaf.moonrise.patches.collisions.shape;
+
+public interface CollisionVoxelShape {
+
+ public double moonrise$offsetX();
+
+ public double moonrise$offsetY();
+
+ public double moonrise$offsetZ();
+
+ public double[] moonrise$rootCoordinatesX();
+
+ public double[] moonrise$rootCoordinatesY();
+
+ public double[] moonrise$rootCoordinatesZ();
+
+ public ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getCachedVoxelData();
+
+ // rets null if not possible to represent this shape as one AABB
+ public net.minecraft.world.phys.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 boolean moonrise$isFullBlock();
+
+ public boolean moonrise$occludesFullBlock();
+
+ 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);
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..4217426d3eca5e5cd2bc37e509f84da1d6fed0b2
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java
@@ -0,0 +1,8 @@
+package ca.spottedleaf.moonrise.patches.collisions.shape;
+
+public record MergedORCache(
+ net.minecraft.world.phys.shapes.VoxelShape key,
+ net.minecraft.world.phys.shapes.VoxelShape result
+) {
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java
new file mode 100644
index 0000000000000000000000000000000000000000..f62359e5d6aa9a9cdb015441dbdb6182dc302f02
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.collisions.util;
+
+public interface CollisionDirection {
+
+ // note: this is HashCommon#murmurHash3(some unique id) and since murmurHash3 has an inverse function the returned
+ // value is still unique
+ public int moonrise$uniqueId();
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..673103f160cbe577c6e05f998706af4e6850011b
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java
@@ -0,0 +1,225 @@
+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
new file mode 100644
index 0000000000000000000000000000000000000000..128267ff40b38c7b3ea0feb5133825cc6aae075b
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java
@@ -0,0 +1,4 @@
+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) {
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..e851e81e13edbad6316df63fcb7095d48f85c5b0
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java
@@ -0,0 +1,9 @@
+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/EntityTrackerEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java
new file mode 100644
index 0000000000000000000000000000000000000000..5f5734c00ce8245a1ff69b2d4c3036579d5392e0
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java
@@ -0,0 +1,11 @@
+package ca.spottedleaf.moonrise.patches.entity_tracker;
+
+import net.minecraft.server.level.ChunkMap;
+
+public interface EntityTrackerEntity {
+
+ public ChunkMap.TrackedEntity moonrise$getTrackedEntity();
+
+ public void moonrise$setTrackedEntity(final ChunkMap.TrackedEntity trackedEntity);
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..1fa07bef57d82c6d5242aaaf66011f0913515231
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java
@@ -0,0 +1,13 @@
+package ca.spottedleaf.moonrise.patches.entity_tracker;
+
+import ca.spottedleaf.moonrise.common.misc.NearbyPlayers;
+
+public interface EntityTrackerTrackedEntity {
+
+ public void moonrise$tick(final NearbyPlayers.TrackedChunk chunk);
+
+ public void moonrise$removeNonTickThreadPlayers();
+
+ public void moonrise$clearPlayers();
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..2bfdf3721db9a45e36538d71cbefcb1d339e6c58
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.starlight.blockstate;
+
+public interface StarlightAbstractBlockState {
+
+ public boolean starlight$isConditionallyFullOpaque();
+
+ public int starlight$getOpacityIfCached();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java
new file mode 100644
index 0000000000000000000000000000000000000000..ed80017c8f257b981d626a37ffc5480d9b326558
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java
@@ -0,0 +1,18 @@
+package ca.spottedleaf.moonrise.patches.starlight.chunk;
+
+import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray;
+
+public interface StarlightChunk {
+
+ public SWMRNibbleArray[] starlight$getBlockNibbles();
+ public void starlight$setBlockNibbles(final SWMRNibbleArray[] nibbles);
+
+ public SWMRNibbleArray[] starlight$getSkyNibbles();
+ public void starlight$setSkyNibbles(final SWMRNibbleArray[] nibbles);
+
+ public boolean[] starlight$getSkyEmptinessMap();
+ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap);
+
+ public boolean[] starlight$getBlockEmptinessMap();
+ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap);
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..154443ac1ee1d6d18b8ff0f40a307d638b213aeb
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
@@ -0,0 +1,277 @@
+package ca.spottedleaf.moonrise.patches.starlight.light;
+
+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.Level;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunkSection;
+import net.minecraft.world.level.chunk.LightChunkGetter;
+import net.minecraft.world.level.chunk.PalettedContainer;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.phys.shapes.Shapes;
+import net.minecraft.world.phys.shapes.VoxelShape;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public final class BlockStarLightEngine extends StarLightEngine {
+
+ public BlockStarLightEngine(final Level world) {
+ super(false, world);
+ }
+
+ @Override
+ protected boolean[] getEmptinessMap(final ChunkAccess chunk) {
+ return ((StarlightChunk)chunk).starlight$getBlockEmptinessMap();
+ }
+
+ @Override
+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) {
+ ((StarlightChunk)chunk).starlight$setBlockEmptinessMap(to);
+ }
+
+ @Override
+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) {
+ return ((StarlightChunk)chunk).starlight$getBlockNibbles();
+ }
+
+ @Override
+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) {
+ ((StarlightChunk)chunk).starlight$setBlockNibbles(to);
+ }
+
+ @Override
+ protected boolean canUseChunk(final ChunkAccess chunk) {
+ return chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect());
+ }
+
+ @Override
+ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) {
+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+ if (nibble != null) {
+ // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically
+ // because a block was removed - which can decrease light. with sky data, block breaking can only result
+ // in increases, and thus the existing sky block check will actually correctly propagate light through
+ // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove
+ // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running
+ // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence
+ // of vanilla data management we "hide" them.
+ nibble.setHidden();
+ }
+ }
+
+ @Override
+ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) {
+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) {
+ return;
+ }
+
+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+ if (nibble == null) {
+ if (!initRemovedNibbles) {
+ throw new IllegalStateException();
+ } else {
+ this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray());
+ }
+ } else {
+ nibble.setNonNull();
+ }
+ }
+
+ @Override
+ protected final void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) {
+ // blocks can change opacity
+ // blocks can change emitted light
+ // blocks can change direction of propagation
+
+ final int encodeOffset = this.coordinateOffset;
+ final int emittedMask = this.emittedLightMask;
+
+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
+ final BlockState blockState = this.getBlockState(worldX, worldY, worldZ);
+ final int emittedLevel = blockState.getLightEmission() & emittedMask;
+
+ this.setLightLevel(worldX, worldY, worldZ, emittedLevel);
+ // this accounts for change in emitted light that would cause an increase
+ if (emittedLevel != 0) {
+ this.appendToIncreaseQueue(
+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | (emittedLevel & 0xFL) << (6 + 6 + 16)
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0)
+ );
+ }
+ // this also accounts for a change in emitted light that would cause a decrease
+ // this also accounts for the change of direction of propagation (i.e old block was full transparent, new block is full opaque or vice versa)
+ // as it checks all neighbours (even if current level is 0)
+ this.appendToDecreaseQueue(
+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | (currentLevel & 0xFL) << (6 + 6 + 16)
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ // always keep sided transparent false here, new block might be conditionally transparent which would
+ // prevent us from decreasing sources in the directions where the new block is opaque
+ // if it turns out we were wrong to de-propagate the source, the re-propagate logic WILL always
+ // catch that and fix it.
+ );
+ // re-propagating neighbours (done by the decrease queue) will also account for opacity changes in this block
+ }
+
+ 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) {
+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
+ int level = centerState.getLightEmission() & 0xF;
+
+ 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) {
+ return level;
+ } else {
+ conditionallyOpaqueState = null;
+ }
+ opacity = Math.max(1, opacity);
+
+ for (final AxisDirection direction : AXIS_DIRECTIONS) {
+ final int offX = worldX + direction.x;
+ final int offY = worldY + direction.y;
+ final int offZ = worldZ + direction.z;
+
+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
+
+ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8));
+
+ if ((neighbourLevel - 1) <= level) {
+ // don't need to test transparency, we know it wont affect the result.
+ continue;
+ }
+
+ final BlockState neighbourState = this.getBlockState(offX, offY, offZ);
+ if (((StarlightAbstractBlockState)neighbourState).starlight$isConditionallyFullOpaque()) {
+ // 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);
+ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) {
+ // not allowed to propagate
+ continue;
+ }
+ }
+
+ // passed transparency,
+
+ final int calculated = neighbourLevel - opacity;
+ level = Math.max(calculated, level);
+ if (level > expect) {
+ return level;
+ }
+ }
+
+ return level;
+ }
+
+ @Override
+ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions) {
+ for (final BlockPos pos : positions) {
+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
+ }
+
+ this.performLightDecrease(lightAccess);
+ }
+
+ protected List<BlockPos> getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) {
+ final List<BlockPos> sources = new ArrayList<>();
+
+ final int offX = chunk.getPos().x << 4;
+ final int offZ = chunk.getPos().z << 4;
+
+ 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()) {
+ // no sources in empty sections
+ continue;
+ }
+ if (!section.maybeHas((final BlockState state) -> {
+ return state.getLightEmission() > 0;
+ })) {
+ // no light sources in palette
+ continue;
+ }
+ final PalettedContainer<BlockState> states = section.states;
+ final int offY = sectionY << 4;
+
+ for (int index = 0; index < (16 * 16 * 16); ++index) {
+ final BlockState state = states.get(index);
+ if (state.getLightEmission() <= 0) {
+ continue;
+ }
+
+ // index = x | (z << 4) | (y << 8)
+ sources.add(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15)));
+ }
+ }
+
+ return sources;
+ }
+
+ @Override
+ public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) {
+ // setup sources
+ 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;
+
+ if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) {
+ // some other source is brighter
+ continue;
+ }
+
+ this.appendToIncreaseQueue(
+ ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | (emittedLight & 0xFL) << (6 + 6 + 16)
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0)
+ );
+
+
+ // propagation wont set this for us
+ this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight);
+ }
+
+ if (needsEdgeChecks) {
+ // not required to propagate here, but this will reduce the hit of the edge checks
+ this.performLightIncrease(lightAccess);
+
+ // verify neighbour edges
+ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection);
+ } else {
+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection);
+
+ this.performLightIncrease(lightAccess);
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java
new file mode 100644
index 0000000000000000000000000000000000000000..4ca68a903e67606fc4ef0bfa9862a73797121c8b
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java
@@ -0,0 +1,440 @@
+package ca.spottedleaf.moonrise.patches.starlight.light;
+
+import net.minecraft.world.level.chunk.DataLayer;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+
+// SWMR -> Single Writer Multi Reader Nibble Array
+public final class SWMRNibbleArray {
+
+ /*
+ * Null nibble - nibble does not exist, and should not be written to. Just like vanilla - null
+ * nibbles are always 0 - and they are never written to directly. Only initialised/uninitialised
+ * nibbles can be written to.
+ *
+ * Uninitialised nibble - They are all 0, but the backing array isn't initialised.
+ *
+ * Initialised nibble - Has light data.
+ */
+
+ protected static final int INIT_STATE_NULL = 0; // null
+ protected static final int INIT_STATE_UNINIT = 1; // uninitialised
+ protected static final int INIT_STATE_INIT = 2; // initialised
+ protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL
+
+ public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block
+ // this allows us to maintain only 1 byte array when we're not updating
+ static final ThreadLocal<ArrayDeque<byte[]>> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new);
+
+ private static byte[] allocateBytes() {
+ final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst();
+ if (inPool != null) {
+ return inPool;
+ }
+
+ return new byte[ARRAY_SIZE];
+ }
+
+ private static void freeBytes(final byte[] bytes) {
+ WORKING_BYTES_POOL.get().addFirst(bytes);
+ }
+
+ public static SWMRNibbleArray fromVanilla(final DataLayer nibble) {
+ if (nibble == null) {
+ return new SWMRNibbleArray(null, true);
+ } else if (nibble.isEmpty()) {
+ return new SWMRNibbleArray();
+ } else {
+ return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later
+ }
+ }
+
+ protected int stateUpdating;
+ protected volatile int stateVisible;
+
+ protected byte[] storageUpdating;
+ protected boolean updatingDirty; // only returns whether storageUpdating is dirty
+ protected volatile byte[] storageVisible;
+
+ public SWMRNibbleArray() {
+ this(null, false); // lazy init
+ }
+
+ public SWMRNibbleArray(final byte[] bytes) {
+ this(bytes, false);
+ }
+
+ public SWMRNibbleArray(final byte[] bytes, final boolean isNullNibble) {
+ if (bytes != null && bytes.length != ARRAY_SIZE) {
+ throw new IllegalArgumentException("Data of wrong length: " + bytes.length);
+ }
+ this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT;
+ this.storageUpdating = this.storageVisible = bytes;
+ }
+
+ public SWMRNibbleArray(final byte[] bytes, final int state) {
+ if (bytes != null && bytes.length != ARRAY_SIZE) {
+ throw new IllegalArgumentException("Data of wrong length: " + bytes.length);
+ }
+ if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) {
+ throw new IllegalArgumentException("Data cannot be null and have state be initialised");
+ }
+ this.stateUpdating = this.stateVisible = state;
+ this.storageUpdating = this.storageVisible = bytes;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("State: ");
+ switch (this.stateVisible) {
+ case INIT_STATE_NULL:
+ stringBuilder.append("null");
+ break;
+ case INIT_STATE_UNINIT:
+ stringBuilder.append("uninitialised");
+ break;
+ case INIT_STATE_INIT:
+ stringBuilder.append("initialised");
+ break;
+ case INIT_STATE_HIDDEN:
+ stringBuilder.append("hidden");
+ break;
+ default:
+ stringBuilder.append("unknown");
+ break;
+ }
+ stringBuilder.append("\nData:\n");
+
+ final byte[] data = this.storageVisible;
+ if (data != null) {
+ for (int i = 0; i < 4096; ++i) {
+ // Copied from NibbleArray#toString
+ final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF);
+
+ stringBuilder.append(Integer.toHexString(level));
+ if ((i & 15) == 15) {
+ stringBuilder.append("\n");
+ }
+
+ if ((i & 255) == 255) {
+ stringBuilder.append("\n");
+ }
+ }
+ } else {
+ stringBuilder.append("null");
+ }
+
+ return stringBuilder.toString();
+ }
+
+ public SaveState getSaveState() {
+ synchronized (this) {
+ final int state = this.stateVisible;
+ final byte[] data = this.storageVisible;
+ if (state == INIT_STATE_NULL) {
+ return null;
+ }
+ if (state == INIT_STATE_UNINIT) {
+ return new SaveState(null, state);
+ }
+ final boolean zero = isAllZero(data);
+ if (zero) {
+ return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null;
+ } else {
+ return new SaveState(data.clone(), state);
+ }
+ }
+ }
+
+ protected static boolean isAllZero(final byte[] data) {
+ for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) {
+ byte whole = data[i << 4];
+
+ for (int k = 1; k < (1 << 4); ++k) {
+ whole |= data[(i << 4) | k];
+ }
+
+ if (whole != 0) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ // operation type: updating on src, updating on other
+ public void extrudeLower(final SWMRNibbleArray other) {
+ if (other.stateUpdating == INIT_STATE_NULL) {
+ throw new IllegalArgumentException();
+ }
+
+ if (other.storageUpdating == null) {
+ this.setUninitialised();
+ return;
+ }
+
+ final byte[] src = other.storageUpdating;
+ final byte[] into;
+
+ if (!this.updatingDirty) {
+ if (this.storageUpdating != null) {
+ into = this.storageUpdating = allocateBytes();
+ } else {
+ this.storageUpdating = into = allocateBytes();
+ this.stateUpdating = INIT_STATE_INIT;
+ }
+ this.updatingDirty = true;
+ } else {
+ into = this.storageUpdating;
+ }
+
+ final int start = 0;
+ final int end = (15 | (15 << 4)) >>> 1;
+
+ /* x | (z << 4) | (y << 8) */
+ for (int y = 0; y <= 15; ++y) {
+ System.arraycopy(src, start, into, y << (8 - 1), end - start + 1);
+ }
+ }
+
+ // operation type: updating
+ public void setFull() {
+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
+ this.stateUpdating = INIT_STATE_INIT;
+ }
+ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1);
+ this.updatingDirty = true;
+ }
+
+ // operation type: updating
+ public void setZero() {
+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
+ this.stateUpdating = INIT_STATE_INIT;
+ }
+ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0);
+ this.updatingDirty = true;
+ }
+
+ // operation type: updating
+ public void setNonNull() {
+ if (this.stateUpdating == INIT_STATE_HIDDEN) {
+ this.stateUpdating = INIT_STATE_INIT;
+ return;
+ }
+ if (this.stateUpdating != INIT_STATE_NULL) {
+ return;
+ }
+ this.stateUpdating = INIT_STATE_UNINIT;
+ }
+
+ // operation type: updating
+ public void setNull() {
+ this.stateUpdating = INIT_STATE_NULL;
+ if (this.updatingDirty && this.storageUpdating != null) {
+ freeBytes(this.storageUpdating);
+ }
+ this.storageUpdating = null;
+ this.updatingDirty = false;
+ }
+
+ // operation type: updating
+ public void setUninitialised() {
+ this.stateUpdating = INIT_STATE_UNINIT;
+ if (this.storageUpdating != null && this.updatingDirty) {
+ freeBytes(this.storageUpdating);
+ }
+ this.storageUpdating = null;
+ this.updatingDirty = false;
+ }
+
+ // operation type: updating
+ public void setHidden() {
+ if (this.stateUpdating == INIT_STATE_HIDDEN) {
+ return;
+ }
+ if (this.stateUpdating != INIT_STATE_INIT) {
+ this.setNull();
+ } else {
+ this.stateUpdating = INIT_STATE_HIDDEN;
+ }
+ }
+
+ // operation type: updating
+ public boolean isDirty() {
+ return this.stateUpdating != this.stateVisible || this.updatingDirty;
+ }
+
+ // operation type: updating
+ public boolean isNullNibbleUpdating() {
+ return this.stateUpdating == INIT_STATE_NULL;
+ }
+
+ // operation type: visible
+ public boolean isNullNibbleVisible() {
+ return this.stateVisible == INIT_STATE_NULL;
+ }
+
+ // opeartion type: updating
+ public boolean isUninitialisedUpdating() {
+ return this.stateUpdating == INIT_STATE_UNINIT;
+ }
+
+ // operation type: visible
+ public boolean isUninitialisedVisible() {
+ return this.stateVisible == INIT_STATE_UNINIT;
+ }
+
+ // operation type: updating
+ public boolean isInitialisedUpdating() {
+ return this.stateUpdating == INIT_STATE_INIT;
+ }
+
+ // operation type: visible
+ public boolean isInitialisedVisible() {
+ return this.stateVisible == INIT_STATE_INIT;
+ }
+
+ // operation type: updating
+ public boolean isHiddenUpdating() {
+ return this.stateUpdating == INIT_STATE_HIDDEN;
+ }
+
+ // operation type: updating
+ public boolean isHiddenVisible() {
+ return this.stateVisible == INIT_STATE_HIDDEN;
+ }
+
+ // operation type: updating
+ protected void swapUpdatingAndMarkDirty() {
+ if (this.updatingDirty) {
+ return;
+ }
+
+ if (this.storageUpdating == null) {
+ this.storageUpdating = allocateBytes();
+ Arrays.fill(this.storageUpdating, (byte)0);
+ } else {
+ System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE);
+ }
+
+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
+ this.stateUpdating = INIT_STATE_INIT;
+ }
+ this.updatingDirty = true;
+ }
+
+ // operation type: updating
+ public boolean updateVisible() {
+ if (!this.isDirty()) {
+ return false;
+ }
+
+ synchronized (this) {
+ if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) {
+ this.storageVisible = null;
+ } else {
+ if (this.storageVisible == null) {
+ this.storageVisible = this.storageUpdating.clone();
+ } else {
+ if (this.storageUpdating != this.storageVisible) {
+ System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE);
+ }
+ }
+
+ if (this.storageUpdating != this.storageVisible) {
+ freeBytes(this.storageUpdating);
+ }
+ this.storageUpdating = this.storageVisible;
+ }
+ this.updatingDirty = false;
+ this.stateVisible = this.stateUpdating;
+ }
+
+ return true;
+ }
+
+ // operation type: visible
+ public DataLayer toVanillaNibble() {
+ synchronized (this) {
+ switch (this.stateVisible) {
+ case INIT_STATE_HIDDEN:
+ case INIT_STATE_NULL:
+ return null;
+ case INIT_STATE_UNINIT:
+ return new DataLayer();
+ case INIT_STATE_INIT:
+ return new DataLayer(this.storageVisible.clone());
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ /* x | (z << 4) | (y << 8) */
+
+ // operation type: updating
+ public int getUpdating(final int x, final int y, final int z) {
+ return this.getUpdating((x & 15) | ((z & 15) << 4) | ((y & 15) << 8));
+ }
+
+ // operation type: updating
+ public int getUpdating(final int index) {
+ // indices range from 0 -> 4096
+ final byte[] bytes = this.storageUpdating;
+ if (bytes == null) {
+ return 0;
+ }
+ final byte value = bytes[index >>> 1];
+
+ // if we are an even index, we want lower 4 bits
+ // if we are an odd index, we want upper 4 bits
+ return ((value >>> ((index & 1) << 2)) & 0xF);
+ }
+
+ // operation type: visible
+ public int getVisible(final int x, final int y, final int z) {
+ return this.getVisible((x & 15) | ((z & 15) << 4) | ((y & 15) << 8));
+ }
+
+ // operation type: visible
+ public int getVisible(final int index) {
+ // indices range from 0 -> 4096
+ final byte[] visibleBytes = this.storageVisible;
+ if (visibleBytes == null) {
+ return 0;
+ }
+ final byte value = visibleBytes[index >>> 1];
+
+ // if we are an even index, we want lower 4 bits
+ // if we are an odd index, we want upper 4 bits
+ return ((value >>> ((index & 1) << 2)) & 0xF);
+ }
+
+ // operation type: updating
+ public void set(final int x, final int y, final int z, final int value) {
+ this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value);
+ }
+
+ // operation type: updating
+ public void set(final int index, final int value) {
+ if (!this.updatingDirty) {
+ this.swapUpdatingAndMarkDirty();
+ }
+ final int shift = (index & 1) << 2;
+ final int i = index >>> 1;
+
+ this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift));
+ }
+
+ public static final class SaveState {
+
+ public final byte[] data;
+ public final int state;
+
+ public SaveState(final byte[] data, final int state) {
+ this.data = data;
+ this.state = state;
+ }
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..fdbc015f498164c9d2c578cd84a73def568142a4
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
@@ -0,0 +1,711 @@
+package ca.spottedleaf.moonrise.patches.starlight.light;
+
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState;
+import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
+import it.unimi.dsi.fastutil.shorts.ShortCollection;
+import it.unimi.dsi.fastutil.shorts.ShortIterator;
+import net.minecraft.core.BlockPos;
+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.LevelChunkSection;
+import net.minecraft.world.level.chunk.LightChunkGetter;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.phys.shapes.Shapes;
+import net.minecraft.world.phys.shapes.VoxelShape;
+import java.util.Arrays;
+import java.util.Set;
+
+public final class SkyStarLightEngine extends StarLightEngine {
+
+ /*
+ Specification for managing the initialisation and de-initialisation of skylight nibble arrays:
+
+ Skylight nibble initialisation requires that non-empty chunk sections have 1 radius nibbles non-null.
+
+ This presents some problems, as vanilla is only guaranteed to have 0 radius neighbours loaded when editing blocks.
+ However starlight fixes this so that it has 1 radius loaded. Still, we don't actually have guarantees
+ that we have the necessary chunks loaded to de-initialise neighbour sections (but we do have enough to de-initialise
+ our own) - we need a radius of 2 to de-initialise neighbour nibbles.
+ How do we solve this?
+
+ Each chunk will store the last known "emptiness" of sections for each of their 1 radius neighbour chunk sections.
+ If the chunk does not have full data, then its nibbles are NOT de-initialised. This is because obviously the
+ chunk did not go through the light stage yet - or its neighbours are not lit. In either case, once the last
+ known "emptiness" of neighbouring sections is filled with data, the chunk will run a full check of the data
+ to see if any of its nibbles need to be de-initialised.
+
+ The emptiness map allows us to de-initialise neighbour nibbles if the neighbour has it filled with data,
+ and if it doesn't have data then we know it will correctly de-initialise once it fills up.
+
+ Unlike vanilla, we store whether nibbles are uninitialised on disk - so we don't need any dumb hacking
+ around those.
+ */
+
+ protected final int[] heightMapBlockChange = new int[16 * 16];
+ {
+ Arrays.fill(this.heightMapBlockChange, Integer.MIN_VALUE); // clear heightmap
+ }
+
+ protected final boolean[] nullPropagationCheckCache;
+
+ public SkyStarLightEngine(final Level world) {
+ super(true, world);
+ this.nullPropagationCheckCache = new boolean[WorldUtil.getTotalLightSections(world)];
+ }
+
+ @Override
+ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) {
+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) {
+ return;
+ }
+ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+ if (nibble == null) {
+ if (!initRemovedNibbles) {
+ throw new IllegalStateException();
+ } else {
+ this.setNibbleInCache(chunkX, chunkY, chunkZ, nibble = new SWMRNibbleArray(null, true));
+ }
+ }
+ this.initNibble(nibble, chunkX, chunkY, chunkZ, extrude);
+ }
+
+ @Override
+ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) {
+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+ if (nibble != null) {
+ nibble.setNull();
+ }
+ }
+
+ protected final void initNibble(final SWMRNibbleArray currNibble, final int chunkX, final int chunkY, final int chunkZ, final boolean extrude) {
+ if (!currNibble.isNullNibbleUpdating()) {
+ // already initialised
+ return;
+ }
+
+ final boolean[] emptinessMap = this.getEmptinessMap(chunkX, chunkZ);
+
+ // are we above this chunk's lowest empty section?
+ int lowestY = this.minLightSection - 1;
+ for (int currY = this.maxSection; currY >= this.minSection; --currY) {
+ if (emptinessMap == null) {
+ // cannot delay nibble init for lit chunks, as we need to init to propagate into them.
+ final LevelChunkSection current = this.getChunkSection(chunkX, currY, chunkZ);
+ if (current == null || current.hasOnlyAir()) {
+ continue;
+ }
+ } else {
+ if (emptinessMap[currY - this.minSection]) {
+ continue;
+ }
+ }
+
+ // should always be full lit here
+ lowestY = currY;
+ break;
+ }
+
+ if (chunkY > lowestY) {
+ // we need to set this one to full
+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+ nibble.setNonNull();
+ nibble.setFull();
+ return;
+ }
+
+ if (extrude) {
+ // this nibble is going to depend solely on the skylight data above it
+ // find first non-null data above (there does exist one, as we just found it above)
+ for (int currY = chunkY + 1; currY <= this.maxLightSection; ++currY) {
+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, currY, chunkZ);
+ if (nibble != null && !nibble.isNullNibbleUpdating()) {
+ currNibble.setNonNull();
+ currNibble.extrudeLower(nibble);
+ break;
+ }
+ }
+ } else {
+ currNibble.setNonNull();
+ }
+ }
+
+ protected final void rewriteNibbleCacheForSkylight(final ChunkAccess chunk) {
+ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) {
+ final SWMRNibbleArray nibble = this.nibbleCache[index];
+ if (nibble != null && nibble.isNullNibbleUpdating()) {
+ // stop propagation in these areas
+ this.nibbleCache[index] = null;
+ nibble.updateVisible();
+ }
+ }
+ }
+
+ // rets whether neighbours were init'd
+
+ protected final boolean checkNullSection(final int chunkX, final int chunkY, final int chunkZ,
+ final boolean extrudeInitialised) {
+ // null chunk sections may have nibble neighbours in the horizontal 1 radius that are
+ // non-null. Propagation to these neighbours is necessary.
+ // What makes this easy is we know none of these neighbours are non-empty (otherwise
+ // this nibble would be initialised). So, we don't have to initialise
+ // the neighbours in the full 1 radius, because there's no worry that any "paths"
+ // to the neighbours on this horizontal plane are blocked.
+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.nullPropagationCheckCache[chunkY - this.minLightSection]) {
+ return false;
+ }
+ this.nullPropagationCheckCache[chunkY - this.minLightSection] = true;
+
+ // check horizontal neighbours
+ boolean needInitNeighbours = false;
+ neighbour_search:
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, chunkY, dz + chunkZ);
+ if (nibble != null && !nibble.isNullNibbleUpdating()) {
+ needInitNeighbours = true;
+ break neighbour_search;
+ }
+ }
+ }
+
+ if (needInitNeighbours) {
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ this.initNibble(dx + chunkX, chunkY, dz + chunkZ, (dx | dz) == 0 ? extrudeInitialised : true, true);
+ }
+ }
+ }
+
+ return needInitNeighbours;
+ }
+
+ protected final int getLightLevelExtruded(final int worldX, final int worldY, final int worldZ) {
+ final int chunkX = worldX >> 4;
+ int chunkY = worldY >> 4;
+ final int chunkZ = worldZ >> 4;
+
+ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+ if (nibble != null) {
+ return nibble.getUpdating(worldX, worldY, worldZ);
+ }
+
+ for (;;) {
+ if (++chunkY > this.maxLightSection) {
+ return 15;
+ }
+
+ nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+
+ if (nibble != null) {
+ return nibble.getUpdating(worldX, 0, worldZ);
+ }
+ }
+ }
+
+ @Override
+ protected boolean[] getEmptinessMap(final ChunkAccess chunk) {
+ return ((StarlightChunk)chunk).starlight$getSkyEmptinessMap();
+ }
+
+ @Override
+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) {
+ ((StarlightChunk)chunk).starlight$setSkyEmptinessMap(to);
+ }
+
+ @Override
+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) {
+ return ((StarlightChunk)chunk).starlight$getSkyNibbles();
+ }
+
+ @Override
+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) {
+ ((StarlightChunk)chunk).starlight$setSkyNibbles(to);
+ }
+
+ @Override
+ protected boolean canUseChunk(final ChunkAccess chunk) {
+ // can only use chunks for sky stuff if their sections have been init'd
+ return chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect());
+ }
+
+ @Override
+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection,
+ final int toSection) {
+ Arrays.fill(this.nullPropagationCheckCache, false);
+ this.rewriteNibbleCacheForSkylight(chunk);
+ final int chunkX = chunk.getPos().x;
+ final int chunkZ = chunk.getPos().z;
+ for (int y = toSection; y >= fromSection; --y) {
+ this.checkNullSection(chunkX, y, chunkZ, true);
+ }
+
+ super.checkChunkEdges(lightAccess, chunk, fromSection, toSection);
+ }
+
+ @Override
+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) {
+ Arrays.fill(this.nullPropagationCheckCache, false);
+ this.rewriteNibbleCacheForSkylight(chunk);
+ final int chunkX = chunk.getPos().x;
+ final int chunkZ = chunk.getPos().z;
+ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) {
+ final int y = (int)iterator.nextShort();
+ this.checkNullSection(chunkX, y, chunkZ, true);
+ }
+
+ super.checkChunkEdges(lightAccess, chunk, sections);
+ }
+
+ @Override
+ protected void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) {
+ // blocks can change opacity
+ // blocks can change direction of propagation
+
+ // same logic applies from BlockStarLightEngine#checkBlock
+
+ final int encodeOffset = this.coordinateOffset;
+
+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
+
+ if (currentLevel == 15) {
+ // must re-propagate clobbered source
+ this.appendToIncreaseQueue(
+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | (currentLevel & 0xFL) << (6 + 6 + 16)
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the block is conditionally transparent
+ );
+ } else {
+ this.setLightLevel(worldX, worldY, worldZ, 0);
+ }
+
+ this.appendToDecreaseQueue(
+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | (currentLevel & 0xFL) << (6 + 6 + 16)
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ );
+ }
+
+ 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) {
+ if (expect == 15) {
+ return expect;
+ }
+
+ 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;
+ }
+ } else {
+ conditionallyOpaqueState = null;
+ opacity = Math.max(1, opacity);
+ }
+
+ int level = 0;
+
+ for (final AxisDirection direction : AXIS_DIRECTIONS) {
+ final int offX = worldX + direction.x;
+ final int offY = worldY + direction.y;
+ final int offZ = worldZ + direction.z;
+
+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
+
+ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8));
+
+ if ((neighbourLevel - 1) <= level) {
+ // don't need to test transparency, we know it wont affect the result.
+ continue;
+ }
+
+ final BlockState neighbourState = this.getBlockState(offX, offY, offZ);
+
+ if (((StarlightAbstractBlockState)neighbourState).starlight$isConditionallyFullOpaque()) {
+ // 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);
+ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) {
+ // not allowed to propagate
+ continue;
+ }
+ }
+
+ final int calculated = neighbourLevel - opacity;
+ level = Math.max(calculated, level);
+ if (level > expect) {
+ return level;
+ }
+ }
+
+ return level;
+ }
+
+ @Override
+ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions) {
+ this.rewriteNibbleCacheForSkylight(atChunk);
+ Arrays.fill(this.nullPropagationCheckCache, false);
+
+ final BlockGetter world = lightAccess.getLevel();
+ final int chunkX = atChunk.getPos().x;
+ final int chunkZ = atChunk.getPos().z;
+ final int heightMapOffset = chunkX * -16 + (chunkZ * (-16 * 16));
+
+ // setup heightmap for changes
+ for (final BlockPos pos : positions) {
+ final int index = pos.getX() + (pos.getZ() << 4) + heightMapOffset;
+ final int curr = this.heightMapBlockChange[index];
+ if (pos.getY() > curr) {
+ this.heightMapBlockChange[index] = pos.getY();
+ }
+ }
+
+ // note: light sets are delayed while processing skylight source changes due to how
+ // nibbles are initialised, as we want to avoid clobbering nibble values so what when
+ // below nibbles are initialised they aren't reading from partially modified nibbles
+
+ // now we can recalculate the sources for the changed columns
+ for (int index = 0; index < (16 * 16); ++index) {
+ final int maxY = this.heightMapBlockChange[index];
+ if (maxY == Integer.MIN_VALUE) {
+ // not changed
+ continue;
+ }
+ this.heightMapBlockChange[index] = Integer.MIN_VALUE; // restore default for next caller
+
+ final int columnX = (index & 15) | (chunkX << 4);
+ final int columnZ = (index >>> 4) | (chunkZ << 4);
+
+ // try and propagate from the above y
+ // delay light set until after processing all sources to setup
+ final int maxPropagationY = this.tryPropagateSkylight(world, columnX, maxY, columnZ, true, true);
+
+ // maxPropagationY is now the highest block that could not be propagated to
+
+ // remove all sources below that are 15
+ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection;
+ final int encodeOffset = this.coordinateOffset;
+
+ if (this.getLightLevelExtruded(columnX, maxPropagationY, columnZ) == 15) {
+ // ensure section is checked
+ this.checkNullSection(columnX >> 4, maxPropagationY >> 4, columnZ >> 4, true);
+
+ for (int currY = maxPropagationY; currY >= (this.minLightSection << 4); --currY) {
+ if ((currY & 15) == 15) {
+ // ensure section is checked
+ this.checkNullSection(columnX >> 4, (currY >> 4), columnZ >> 4, true);
+ }
+
+ // ensure section below is always checked
+ final SWMRNibbleArray nibble = this.getNibbleFromCache(columnX >> 4, currY >> 4, columnZ >> 4);
+ if (nibble == null) {
+ // advance currY to the the top of the section below
+ currY = (currY) & (~15);
+ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually
+ // end up there
+ continue;
+ }
+
+ if (nibble.getUpdating(columnX, currY, columnZ) != 15) {
+ break;
+ }
+
+ // delay light set until after processing all sources to setup
+ this.appendToDecreaseQueue(
+ ((columnX + (columnZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | (15L << (6 + 6 + 16))
+ | (propagateDirection << (6 + 6 + 16 + 4))
+ // do not set transparent blocks for the same reason we don't in the checkBlock method
+ );
+ }
+ }
+ }
+
+ // delayed light sets are processed here, and must be processed before checkBlock as checkBlock reads
+ // immediate light value
+ this.processDelayedIncreases();
+ this.processDelayedDecreases();
+
+ for (final BlockPos pos : positions) {
+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
+ }
+
+ this.performLightDecrease(lightAccess);
+ }
+
+ protected final int[] heightMapGen = new int[32 * 32];
+
+ @Override
+ protected void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) {
+ this.rewriteNibbleCacheForSkylight(chunk);
+ Arrays.fill(this.nullPropagationCheckCache, false);
+
+ final BlockGetter world = lightAccess.getLevel();
+ final ChunkPos chunkPos = chunk.getPos();
+ final int chunkX = chunkPos.x;
+ final int chunkZ = chunkPos.z;
+
+ final LevelChunkSection[] sections = chunk.getSections();
+
+ int highestNonEmptySection = this.maxSection;
+ while (highestNonEmptySection == (this.minSection - 1) ||
+ sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].hasOnlyAir()) {
+ this.checkNullSection(chunkX, highestNonEmptySection, chunkZ, false);
+ // try propagate FULL to neighbours
+
+ // check neighbours to see if we need to propagate into them
+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
+ final int neighbourX = chunkX + direction.x;
+ final int neighbourZ = chunkZ + direction.z;
+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(neighbourX, highestNonEmptySection, neighbourZ);
+ if (neighbourNibble == null) {
+ // unloaded neighbour
+ // most of the time we fall here
+ continue;
+ }
+
+ // it looks like we need to propagate into the neighbour
+
+ final int incX;
+ final int incZ;
+ final int startX;
+ final int startZ;
+
+ if (direction.x != 0) {
+ // x direction
+ incX = 0;
+ incZ = 1;
+
+ if (direction.x < 0) {
+ // negative
+ startX = chunkX << 4;
+ } else {
+ startX = chunkX << 4 | 15;
+ }
+ startZ = chunkZ << 4;
+ } else {
+ // z direction
+ incX = 1;
+ incZ = 0;
+
+ if (direction.z < 0) {
+ // negative
+ startZ = chunkZ << 4;
+ } else {
+ startZ = chunkZ << 4 | 15;
+ }
+ startX = chunkX << 4;
+ }
+
+ final int encodeOffset = this.coordinateOffset;
+ final long propagateDirection = 1L << direction.ordinal(); // we only want to check in this direction
+
+ for (int currY = highestNonEmptySection << 4, maxY = currY | 15; currY <= maxY; ++currY) {
+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
+ this.appendToIncreaseQueue(
+ ((currX + (currZ << 6) + (currY << (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))
+ // no transparent flag, we know for a fact there are no blocks here that could be directionally transparent (as the section is EMPTY)
+ );
+ }
+ }
+ }
+
+ if (highestNonEmptySection-- == (this.minSection - 1)) {
+ break;
+ }
+ }
+
+ if (highestNonEmptySection >= this.minSection) {
+ // fill out our other sources
+ final int minX = chunkPos.x << 4;
+ final int maxX = chunkPos.x << 4 | 15;
+ final int minZ = chunkPos.z << 4;
+ final int maxZ = chunkPos.z << 4 | 15;
+ final int startY = highestNonEmptySection << 4 | 15;
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ this.tryPropagateSkylight(world, currX, startY + 1, currZ, false, false);
+ }
+ }
+ } // else: apparently the chunk is empty
+
+ if (needsEdgeChecks) {
+ // not required to propagate here, but this will reduce the hit of the edge checks
+ this.performLightIncrease(lightAccess);
+
+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) {
+ this.checkNullSection(chunkX, y, chunkZ, false);
+ }
+ // no need to rewrite the nibble cache again
+ super.checkChunkEdges(lightAccess, chunk, this.minLightSection, highestNonEmptySection);
+ } else {
+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) {
+ this.checkNullSection(chunkX, y, chunkZ, false);
+ }
+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, highestNonEmptySection);
+
+ this.performLightIncrease(lightAccess);
+ }
+ }
+
+ protected final void processDelayedIncreases() {
+ // copied from performLightIncrease
+ final long[] queue = this.increaseQueue;
+ final int decodeOffsetX = -this.encodeOffsetX;
+ final int decodeOffsetY = -this.encodeOffsetY;
+ final int decodeOffsetZ = -this.encodeOffsetZ;
+
+ for (int i = 0, len = this.increaseQueueInitialLength; i < len; ++i) {
+ final long queueValue = queue[i];
+
+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF);
+
+ this.setLightLevel(posX, posY, posZ, propagatedLightLevel);
+ }
+ }
+
+ protected final void processDelayedDecreases() {
+ // copied from performLightDecrease
+ final long[] queue = this.decreaseQueue;
+ final int decodeOffsetX = -this.encodeOffsetX;
+ final int decodeOffsetY = -this.encodeOffsetY;
+ final int decodeOffsetZ = -this.encodeOffsetZ;
+
+ for (int i = 0, len = this.decreaseQueueInitialLength; i < len; ++i) {
+ final long queueValue = queue[i];
+
+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
+
+ this.setLightLevel(posX, posY, posZ, 0);
+ }
+ }
+
+ // delaying the light set is useful for block changes since they need to worry about initialising nibblearrays
+ // while also queueing light at the same time (initialising nibblearrays might depend on nibbles above, so
+ // 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.
+
+ if (this.getLightLevelExtruded(worldX, startY + 1, worldZ) != 15) {
+ return startY;
+ }
+
+ // ensure this section is always checked
+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised);
+
+ BlockState above = this.getBlockState(worldX, startY + 1, worldZ);
+
+ for (;startY >= (this.minLightSection << 4); --startY) {
+ if ((startY & 15) == 15) {
+ // ensure this section is always checked
+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised);
+ }
+ final BlockState current = this.getBlockState(worldX, startY, worldZ);
+
+ 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);
+ if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
+ // above wont let us propagate
+ break;
+ }
+ } else {
+ 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);
+
+ 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.
+ 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) {
+ // we skip empty sections here, as this is just an easy way of making sure the above block
+ // can propagate through air.
+
+ // nothing can propagate in null sections, remove the queue entry for it
+ --this.increaseQueueInitialLength;
+
+ // advance currY to the the top of the section below
+ startY = (startY) & (~15);
+ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually
+ // end up there
+
+ // make sure this is marked as AIR
+ above = AIR_BLOCK_STATE;
+ } else if (!delayLightSet) {
+ this.setLightLevel(worldX, startY, worldZ, 15);
+ }
+ }
+
+ return startY;
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..382c9e445af0d6ad2428fc22d0f63017c58191e2
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
@@ -0,0 +1,1573 @@
+package ca.spottedleaf.moonrise.patches.starlight.light;
+
+import ca.spottedleaf.concurrentutil.util.IntegerUtil;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.shorts.ShortCollection;
+import it.unimi.dsi.fastutil.shorts.ShortIterator;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.core.SectionPos;
+import net.minecraft.world.level.BlockGetter;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.LevelHeightAccessor;
+import net.minecraft.world.level.LightLayer;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunkSection;
+import net.minecraft.world.level.chunk.LightChunkGetter;
+import net.minecraft.world.phys.shapes.Shapes;
+import net.minecraft.world.phys.shapes.VoxelShape;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+
+public abstract class StarLightEngine {
+
+ protected static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState();
+
+ protected static final AxisDirection[] DIRECTIONS = AxisDirection.values();
+ protected static final AxisDirection[] AXIS_DIRECTIONS = DIRECTIONS;
+ protected static final AxisDirection[] ONLY_HORIZONTAL_DIRECTIONS = new AxisDirection[] {
+ AxisDirection.POSITIVE_X, AxisDirection.NEGATIVE_X,
+ AxisDirection.POSITIVE_Z, AxisDirection.NEGATIVE_Z
+ };
+
+ 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);
+
+ static {
+ POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X;
+ POSITIVE_Z.opposite = NEGATIVE_Z; NEGATIVE_Z.opposite = POSITIVE_Z;
+ POSITIVE_Y.opposite = NEGATIVE_Y; NEGATIVE_Y.opposite = POSITIVE_Y;
+ }
+
+ protected AxisDirection opposite;
+
+ public final int x;
+ public final int y;
+ public final int z;
+ public final Direction nms;
+ public final long everythingButThisDirection;
+ public final long everythingButTheOppositeDirection;
+
+ AxisDirection(final int x, final int y, final int z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.nms = Direction.fromDelta(x, y, z);
+ 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)));
+ }
+
+ public AxisDirection getOpposite() {
+ return this.opposite;
+ }
+ }
+
+ // I'd like to thank https://www.seedofandromeda.com/blogs/29-fast-flood-fill-lighting-in-a-blocky-voxel-game-pt-1
+ // for explaining how light propagates via breadth-first search
+
+ // While the above is a good start to understanding the general idea of what the general principles are, it's not
+ // exactly how the vanilla light engine should behave for minecraft.
+
+ // similar to the above, except the chunk section indices vary from [-1, 1], or [0, 2]
+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
+ // index = x + (z * 5) + (y * 25)
+ // null index indicates the chunk section doesn't exist (empty or out of bounds)
+ protected final LevelChunkSection[] sectionCache;
+
+ // the exact same as above, except for storing fast access to SWMRNibbleArray
+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
+ // index = x + (z * 5) + (y * 25)
+ protected final SWMRNibbleArray[] nibbleCache;
+
+ // the exact same as above, except for storing fast access to nibbles to call change callbacks for
+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
+ // index = x + (z * 5) + (y * 25)
+ protected final boolean[] notifyUpdateCache;
+
+ // always initialsed during start of lighting.
+ // index = x + (z * 5)
+ protected final ChunkAccess[] chunkCache = new ChunkAccess[5 * 5];
+
+ // 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 int encodeOffsetX;
+ protected int encodeOffsetY;
+ protected int encodeOffsetZ;
+
+ protected int coordinateOffset;
+
+ protected int chunkOffsetX;
+ protected int chunkOffsetY;
+ protected int chunkOffsetZ;
+
+ protected int chunkIndexOffset;
+ protected int chunkSectionIndexOffset;
+
+ protected final boolean skylightPropagator;
+ protected final int emittedLightMask;
+ protected final boolean isClientSide;
+
+ protected final Level world;
+ protected final int minLightSection;
+ protected final int maxLightSection;
+ protected final int minSection;
+ protected final int maxSection;
+
+ protected StarLightEngine(final boolean skylightPropagator, final Level world) {
+ this.skylightPropagator = skylightPropagator;
+ this.emittedLightMask = skylightPropagator ? 0 : 0xF;
+ this.isClientSide = world.isClientSide;
+ this.world = world;
+ this.minLightSection = WorldUtil.getMinLightSection(world);
+ this.maxLightSection = WorldUtil.getMaxLightSection(world);
+ this.minSection = WorldUtil.getMinSection(world);
+ this.maxSection = WorldUtil.getMaxSection(world);
+
+ this.sectionCache = new LevelChunkSection[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
+ this.nibbleCache = new SWMRNibbleArray[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
+ this.notifyUpdateCache = new boolean[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
+ }
+
+ protected final void setupEncodeOffset(final int centerX, final int centerY, final int centerZ) {
+ // 31 = center + encodeOffset
+ this.encodeOffsetX = 31 - centerX;
+ this.encodeOffsetY = (-(this.minLightSection - 1) << 4); // we want 0 to be the smallest encoded value
+ this.encodeOffsetZ = 31 - centerZ;
+
+ // coordinateIndex = x | (z << 6) | (y << 12)
+ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << 6) + (this.encodeOffsetY << 12);
+
+ // 2 = (centerX >> 4) + chunkOffset
+ this.chunkOffsetX = 2 - (centerX >> 4);
+ this.chunkOffsetY = -(this.minLightSection - 1); // lowest should be 0
+ this.chunkOffsetZ = 2 - (centerZ >> 4);
+
+ // chunk index = x + (5 * z)
+ this.chunkIndexOffset = this.chunkOffsetX + (5 * this.chunkOffsetZ);
+
+ // chunk section index = x + (5 * z) + ((5*5) * y)
+ this.chunkSectionIndexOffset = this.chunkIndexOffset + ((5 * 5) * this.chunkOffsetY);
+ }
+
+ protected final void setupCaches(final LightChunkGetter chunkProvider, final int centerX, final int centerY, final int centerZ,
+ final boolean relaxed, final boolean tryToLoadChunksFor2Radius) {
+ final int centerChunkX = centerX >> 4;
+ final int centerChunkY = centerY >> 4;
+ final int centerChunkZ = centerZ >> 4;
+
+ this.setupEncodeOffset(centerChunkX * 16 + 7, centerChunkY * 16 + 7, centerChunkZ * 16 + 7);
+
+ final int radius = tryToLoadChunksFor2Radius ? 2 : 1;
+
+ for (int dz = -radius; dz <= radius; ++dz) {
+ for (int dx = -radius; dx <= radius; ++dx) {
+ final int cx = centerChunkX + dx;
+ final int cz = centerChunkZ + dz;
+ final boolean isTwoRadius = Math.max(IntegerUtil.branchlessAbs(dx), IntegerUtil.branchlessAbs(dz)) == 2;
+ final ChunkAccess chunk = (ChunkAccess)chunkProvider.getChunkForLighting(cx, cz);
+
+ if (chunk == null) {
+ if (relaxed | isTwoRadius) {
+ continue;
+ }
+ throw new IllegalArgumentException("Trying to propagate light update before 1 radius neighbours ready");
+ }
+
+ if (!this.canUseChunk(chunk)) {
+ continue;
+ }
+
+ this.setChunkInCache(cx, cz, chunk);
+ this.setEmptinessMapCache(cx, cz, this.getEmptinessMap(chunk));
+ if (!isTwoRadius) {
+ this.setBlocksForChunkInCache(cx, cz, chunk.getSections());
+ this.setNibblesForChunkInCache(cx, cz, this.getNibblesOnChunk(chunk));
+ }
+ }
+ }
+ }
+
+ protected final ChunkAccess getChunkInCache(final int chunkX, final int chunkZ) {
+ return this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset];
+ }
+
+ protected final void setChunkInCache(final int chunkX, final int chunkZ, final ChunkAccess chunk) {
+ this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk;
+ }
+
+ protected final LevelChunkSection getChunkSection(final int chunkX, final int chunkY, final int chunkZ) {
+ return this.sectionCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset];
+ }
+
+ protected final void setChunkSectionInCache(final int chunkX, final int chunkY, final int chunkZ, final LevelChunkSection section) {
+ this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section;
+ }
+
+ protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final LevelChunkSection[] sections) {
+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
+ this.setChunkSectionInCache(chunkX, cy, chunkZ,
+ sections == null ? null : (cy >= this.minSection && cy <= this.maxSection ? sections[cy - this.minSection] : null));
+ }
+ }
+
+ protected final SWMRNibbleArray getNibbleFromCache(final int chunkX, final int chunkY, final int chunkZ) {
+ return this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset];
+ }
+
+ protected final SWMRNibbleArray[] getNibblesForChunkFromCache(final int chunkX, final int chunkZ) {
+ final SWMRNibbleArray[] ret = new SWMRNibbleArray[this.maxLightSection - this.minLightSection + 1];
+
+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
+ ret[cy - this.minLightSection] = this.nibbleCache[chunkX + 5*chunkZ + (cy * (5 * 5)) + this.chunkSectionIndexOffset];
+ }
+
+ return ret;
+ }
+
+ protected final void setNibbleInCache(final int chunkX, final int chunkY, final int chunkZ, final SWMRNibbleArray nibble) {
+ this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset] = nibble;
+ }
+
+ protected final void setNibblesForChunkInCache(final int chunkX, final int chunkZ, final SWMRNibbleArray[] nibbles) {
+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
+ this.setNibbleInCache(chunkX, cy, chunkZ, nibbles == null ? null : nibbles[cy - this.minLightSection]);
+ }
+ }
+
+ protected final void updateVisible(final LightChunkGetter lightAccess) {
+ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) {
+ final SWMRNibbleArray nibble = this.nibbleCache[index];
+ if (!this.notifyUpdateCache[index] && (nibble == null || !nibble.isDirty())) {
+ continue;
+ }
+
+ final int chunkX = (index % 5) - this.chunkOffsetX;
+ final int chunkZ = ((index / 5) % 5) - this.chunkOffsetZ;
+ final int ySections = this.maxSection - this.minSection + 1;
+ final int chunkY = ((index / (5*5)) % (ySections + 2 + 2)) - this.chunkOffsetY;
+ if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) {
+ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, chunkY, chunkZ));
+ }
+ }
+ }
+
+ protected final void destroyCaches() {
+ Arrays.fill(this.sectionCache, null);
+ Arrays.fill(this.nibbleCache, null);
+ Arrays.fill(this.chunkCache, null);
+ Arrays.fill(this.emptinessMapCache, null);
+ if (this.isClientSide) {
+ Arrays.fill(this.notifyUpdateCache, false);
+ }
+ }
+
+ protected final BlockState getBlockState(final int worldX, final int worldY, final int worldZ) {
+ final LevelChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
+
+ if (section != null) {
+ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15);
+ }
+
+ return AIR_BLOCK_STATE;
+ }
+
+ protected final BlockState getBlockState(final int sectionIndex, final int localIndex) {
+ final LevelChunkSection section = this.sectionCache[sectionIndex];
+
+ if (section != null) {
+ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.states.get(localIndex);
+ }
+
+ return AIR_BLOCK_STATE;
+ }
+
+ protected final int getLightLevel(final int worldX, final int worldY, final int worldZ) {
+ final SWMRNibbleArray nibble = this.nibbleCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
+
+ return nibble == null ? 0 : nibble.getUpdating((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8));
+ }
+
+ protected final int getLightLevel(final int sectionIndex, final int localIndex) {
+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
+
+ return nibble == null ? 0 : nibble.getUpdating(localIndex);
+ }
+
+ protected final void setLightLevel(final int worldX, final int worldY, final int worldZ, final int level) {
+ final int sectionIndex = (worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset;
+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
+
+ if (nibble != null) {
+ nibble.set((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8), level);
+ if (this.isClientSide) {
+ int cx1 = (worldX - 1) >> 4;
+ int cx2 = (worldX + 1) >> 4;
+ int cy1 = (worldY - 1) >> 4;
+ int cy2 = (worldY + 1) >> 4;
+ int cz1 = (worldZ - 1) >> 4;
+ int cz2 = (worldZ + 1) >> 4;
+ for (int x = cx1; x <= cx2; ++x) {
+ for (int y = cy1; y <= cy2; ++y) {
+ for (int z = cz1; z <= cz2; ++z) {
+ this.notifyUpdateCache[x + 5 * z + (5 * 5) * y + this.chunkSectionIndexOffset] = true;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ protected final void postLightUpdate(final int worldX, final int worldY, final int worldZ) {
+ if (this.isClientSide) {
+ int cx1 = (worldX - 1) >> 4;
+ int cx2 = (worldX + 1) >> 4;
+ int cy1 = (worldY - 1) >> 4;
+ int cy2 = (worldY + 1) >> 4;
+ int cz1 = (worldZ - 1) >> 4;
+ int cz2 = (worldZ + 1) >> 4;
+ for (int x = cx1; x <= cx2; ++x) {
+ for (int y = cy1; y <= cy2; ++y) {
+ for (int z = cz1; z <= cz2; ++z) {
+ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
+ }
+ }
+ }
+ }
+ }
+
+ protected final void setLightLevel(final int sectionIndex, final int localIndex, final int worldX, final int worldY, final int worldZ, final int level) {
+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
+
+ if (nibble != null) {
+ nibble.set(localIndex, level);
+ if (this.isClientSide) {
+ int cx1 = (worldX - 1) >> 4;
+ int cx2 = (worldX + 1) >> 4;
+ int cy1 = (worldY - 1) >> 4;
+ int cy2 = (worldY + 1) >> 4;
+ int cz1 = (worldZ - 1) >> 4;
+ int cz2 = (worldZ + 1) >> 4;
+ for (int x = cx1; x <= cx2; ++x) {
+ for (int y = cy1; y <= cy2; ++y) {
+ for (int z = cz1; z <= cz2; ++z) {
+ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ protected final boolean[] getEmptinessMap(final int chunkX, final int chunkZ) {
+ return this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset];
+ }
+
+ protected final void setEmptinessMapCache(final int chunkX, final int chunkZ, final boolean[] emptinessMap) {
+ this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = emptinessMap;
+ }
+
+ public static SWMRNibbleArray[] getFilledEmptyLight(final LevelHeightAccessor world) {
+ return getFilledEmptyLight(WorldUtil.getTotalLightSections(world));
+ }
+
+ private static SWMRNibbleArray[] getFilledEmptyLight(final int totalLightSections) {
+ final SWMRNibbleArray[] ret = new SWMRNibbleArray[totalLightSections];
+
+ for (int i = 0, len = ret.length; i < len; ++i) {
+ ret[i] = new SWMRNibbleArray(null, true);
+ }
+
+ return ret;
+ }
+
+ protected abstract boolean[] getEmptinessMap(final ChunkAccess chunk);
+
+ protected abstract void setEmptinessMap(final ChunkAccess chunk, final boolean[] to);
+
+ protected abstract SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk);
+
+ protected abstract void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to);
+
+ protected abstract boolean canUseChunk(final ChunkAccess chunk);
+
+ public final void blocksChangedInChunk(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ,
+ final Set<BlockPos> positions, final Boolean[] changedSections) {
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
+ try {
+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
+ if (chunk == null) {
+ return;
+ }
+ if (changedSections != null) {
+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, changedSections, false);
+ if (ret != null) {
+ this.setEmptinessMap(chunk, ret);
+ }
+ }
+ if (!positions.isEmpty()) {
+ this.propagateBlockChanges(lightAccess, chunk, positions);
+ }
+ this.updateVisible(lightAccess);
+ } finally {
+ this.destroyCaches();
+ }
+ }
+
+ // subclasses should not initialise caches, as this will always be done by the super call
+ // subclasses should not invoke updateVisible, as this will always be done by the super call
+ protected abstract void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions);
+
+ protected abstract void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ);
+
+ // if ret > expect, then the real value is at least ret (early returns if ret > expect, rather than calculating actual)
+ // if ret == expect, then expect is the correct light value for pos
+ // if ret < expect, then ret is the real light value
+ protected abstract int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
+ final int expect);
+
+ protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16];
+ protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16];
+
+ protected void checkChunkEdge(final LightChunkGetter lightAccess, final ChunkAccess chunk,
+ final int chunkX, final int chunkY, final int chunkZ) {
+ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+ if (currNibble == null) {
+ return;
+ }
+
+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
+ final int neighbourOffX = direction.x;
+ final int neighbourOffZ = direction.z;
+
+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX,
+ chunkY, chunkZ + neighbourOffZ);
+
+ if (neighbourNibble == null) {
+ continue;
+ }
+
+ if (!currNibble.isInitialisedUpdating() && !neighbourNibble.isInitialisedUpdating()) {
+ // both are zero, nothing to check.
+ continue;
+ }
+
+ // this chunk
+ final int incX;
+ final int incZ;
+ final int startX;
+ final int startZ;
+
+ if (neighbourOffX != 0) {
+ // x direction
+ incX = 0;
+ incZ = 1;
+
+ if (direction.x < 0) {
+ // negative
+ startX = chunkX << 4;
+ } else {
+ startX = chunkX << 4 | 15;
+ }
+ startZ = chunkZ << 4;
+ } else {
+ // z direction
+ incX = 1;
+ incZ = 0;
+
+ if (neighbourOffZ < 0) {
+ // negative
+ startZ = chunkZ << 4;
+ } else {
+ startZ = chunkZ << 4 | 15;
+ }
+ startX = chunkX << 4;
+ }
+
+ int centerDelayedChecks = 0;
+ int neighbourDelayedChecks = 0;
+ for (int currY = chunkY << 4, maxY = currY | 15; currY <= maxY; ++currY) {
+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
+ final int neighbourX = currX + neighbourOffX;
+ final int neighbourZ = currZ + neighbourOffZ;
+
+ final int currentIndex = (currX & 15) |
+ ((currZ & 15)) << 4 |
+ ((currY & 15) << 8);
+ final int currentLevel = currNibble.getUpdating(currentIndex);
+
+ final int neighbourIndex =
+ (neighbourX & 15) |
+ ((neighbourZ & 15)) << 4 |
+ ((currY & 15) << 8);
+ final int neighbourLevel = neighbourNibble.getUpdating(neighbourIndex);
+
+ // the checks are delayed because the checkBlock method clobbers light values - which then
+ // affect later calculate light value operations. While they don't affect it in a behaviourly significant
+ // way, they do have a negative performance impact due to simply queueing more values
+
+ if (this.calculateLightValue(lightAccess, currX, currY, currZ, currentLevel) != currentLevel) {
+ this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex;
+ }
+
+ if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel) != neighbourLevel) {
+ this.chunkCheckDelayedUpdatesNeighbour[neighbourDelayedChecks++] = neighbourIndex;
+ }
+ }
+ }
+
+ final int currentChunkOffX = chunkX << 4;
+ final int currentChunkOffZ = chunkZ << 4;
+ final int neighbourChunkOffX = (chunkX + direction.x) << 4;
+ final int neighbourChunkOffZ = (chunkZ + direction.z) << 4;
+ final int chunkOffY = chunkY << 4;
+ for (int i = 0, len = Math.max(centerDelayedChecks, neighbourDelayedChecks); i < len; ++i) {
+ // try to queue neighbouring data together
+ // index = x | (z << 4) | (y << 8)
+ if (i < centerDelayedChecks) {
+ final int value = this.chunkCheckDelayedUpdatesCenter[i];
+ this.checkBlock(lightAccess, currentChunkOffX | (value & 15),
+ chunkOffY | (value >>> 8),
+ currentChunkOffZ | ((value >>> 4) & 0xF));
+ }
+ if (i < neighbourDelayedChecks) {
+ final int value = this.chunkCheckDelayedUpdatesNeighbour[i];
+ this.checkBlock(lightAccess, neighbourChunkOffX | (value & 15),
+ chunkOffY | (value >>> 8),
+ neighbourChunkOffZ | ((value >>> 4) & 0xF));
+ }
+ }
+ }
+ }
+
+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) {
+ final ChunkPos chunkPos = chunk.getPos();
+ final int chunkX = chunkPos.x;
+ final int chunkZ = chunkPos.z;
+
+ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) {
+ this.checkChunkEdge(lightAccess, chunk, chunkX, iterator.nextShort(), chunkZ);
+ }
+
+ this.performLightDecrease(lightAccess);
+ }
+
+ // subclasses should not initialise caches, as this will always be done by the super call
+ // subclasses should not invoke updateVisible, as this will always be done by the super call
+ // verifies that light levels on this chunks edges are consistent with this chunk's neighbours
+ // edges. if they are not, they are decreased (effectively performing the logic in checkBlock).
+ // This does not resolve skylight source problems.
+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) {
+ final ChunkPos chunkPos = chunk.getPos();
+ final int chunkX = chunkPos.x;
+ final int chunkZ = chunkPos.z;
+
+ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) {
+ this.checkChunkEdge(lightAccess, chunk, chunkX, currSectionY, chunkZ);
+ }
+
+ this.performLightDecrease(lightAccess);
+ }
+
+ // pulls light from neighbours, and adds them into the increase queue. does not actually propagate.
+ protected final void propagateNeighbourLevels(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) {
+ final ChunkPos chunkPos = chunk.getPos();
+ final int chunkX = chunkPos.x;
+ final int chunkZ = chunkPos.z;
+
+ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) {
+ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, currSectionY, chunkZ);
+ if (currNibble == null) {
+ continue;
+ }
+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
+ final int neighbourOffX = direction.x;
+ final int neighbourOffZ = direction.z;
+
+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX,
+ currSectionY, chunkZ + neighbourOffZ);
+
+ if (neighbourNibble == null || !neighbourNibble.isInitialisedUpdating()) {
+ // can't pull from 0
+ continue;
+ }
+
+ // neighbour chunk
+ final int incX;
+ final int incZ;
+ final int startX;
+ final int startZ;
+
+ if (neighbourOffX != 0) {
+ // x direction
+ incX = 0;
+ incZ = 1;
+
+ if (direction.x < 0) {
+ // negative
+ startX = (chunkX << 4) - 1;
+ } else {
+ startX = (chunkX << 4) + 16;
+ }
+ startZ = chunkZ << 4;
+ } else {
+ // z direction
+ incX = 1;
+ incZ = 0;
+
+ if (neighbourOffZ < 0) {
+ // negative
+ startZ = (chunkZ << 4) - 1;
+ } else {
+ startZ = (chunkZ << 4) + 16;
+ }
+ startX = chunkX << 4;
+ }
+
+ final long propagateDirection = 1L << direction.getOpposite().ordinal(); // we only want to check in this direction towards this chunk
+ final int encodeOffset = this.coordinateOffset;
+
+ for (int currY = currSectionY << 4, maxY = currY | 15; currY <= maxY; ++currY) {
+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
+ final int level = neighbourNibble.getUpdating(
+ (currX & 15)
+ | ((currZ & 15) << 4)
+ | ((currY & 15) << 8)
+ );
+
+ if (level <= 1) {
+ // nothing to propagate
+ continue;
+ }
+
+ this.appendToIncreaseQueue(
+ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((level & 0xFL) << (6 + 6 + 16))
+ | (propagateDirection << (6 + 6 + 16 + 4))
+ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the current block is transparent, must check.
+ );
+ }
+ }
+ }
+ }
+ }
+
+ public static Boolean[] getEmptySectionsForChunk(final ChunkAccess chunk) {
+ final LevelChunkSection[] sections = chunk.getSections();
+ final Boolean[] ret = new Boolean[sections.length];
+
+ for (int i = 0; i < sections.length; ++i) {
+ if (sections[i] == null || sections[i].hasOnlyAir()) {
+ ret[i] = Boolean.TRUE;
+ } else {
+ ret[i] = Boolean.FALSE;
+ }
+ }
+
+ return ret;
+ }
+
+ public final void forceHandleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptinessChanges) {
+ final int chunkX = chunk.getPos().x;
+ final int chunkZ = chunk.getPos().z;
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
+ try {
+ // force current chunk into cache
+ this.setChunkInCache(chunkX, chunkZ, chunk);
+ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections());
+ this.setNibblesForChunkInCache(chunkX, chunkZ, this.getNibblesOnChunk(chunk));
+ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk));
+
+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false);
+ if (ret != null) {
+ this.setEmptinessMap(chunk, ret);
+ }
+ this.updateVisible(lightAccess);
+ } finally {
+ this.destroyCaches();
+ }
+ }
+
+ public final void handleEmptySectionChanges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ,
+ final Boolean[] emptinessChanges) {
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
+ try {
+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
+ if (chunk == null) {
+ return;
+ }
+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false);
+ if (ret != null) {
+ this.setEmptinessMap(chunk, ret);
+ }
+ this.updateVisible(lightAccess);
+ } finally {
+ this.destroyCaches();
+ }
+ }
+
+ protected abstract void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles);
+
+ protected abstract void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ);
+
+ // subclasses should not initialise caches, as this will always be done by the super call
+ // subclasses should not invoke updateVisible, as this will always be done by the super call
+ // subclasses are guaranteed that this is always called before a changed block set
+ // newChunk specifies whether the changes describe a "first load" of a chunk or changes to existing, already loaded chunks
+ // rets non-null when the emptiness map changed and needs to be updated
+ protected final boolean[] handleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk,
+ final Boolean[] emptinessChanges, final boolean unlit) {
+ final Level world = (Level)lightAccess.getLevel();
+ final int chunkX = chunk.getPos().x;
+ final int chunkZ = chunk.getPos().z;
+
+ boolean[] chunkEmptinessMap = this.getEmptinessMap(chunkX, chunkZ);
+ boolean[] ret = null;
+ final boolean needsInit = unlit || chunkEmptinessMap == null;
+ if (needsInit) {
+ this.setEmptinessMapCache(chunkX, chunkZ, ret = chunkEmptinessMap = new boolean[WorldUtil.getTotalSections(world)]);
+ }
+
+ // update emptiness map
+ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) {
+ Boolean valueBoxed = emptinessChanges[sectionIndex];
+ if (valueBoxed == null) {
+ if (!needsInit) {
+ continue;
+ }
+ final LevelChunkSection section = this.getChunkSection(chunkX, sectionIndex + this.minSection, chunkZ);
+ emptinessChanges[sectionIndex] = valueBoxed = section == null || section.hasOnlyAir() ? Boolean.TRUE : Boolean.FALSE;
+ }
+ chunkEmptinessMap[sectionIndex] = valueBoxed.booleanValue();
+ }
+
+ // now init neighbour nibbles
+ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) {
+ final Boolean valueBoxed = emptinessChanges[sectionIndex];
+ final int sectionY = sectionIndex + this.minSection;
+ if (valueBoxed == null) {
+ continue;
+ }
+
+ final boolean empty = valueBoxed.booleanValue();
+
+ if (empty) {
+ continue;
+ }
+
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ // if we're not empty, we also need to initialise nibbles
+ // note: if we're unlit, we absolutely do not want to extrude, as light data isn't set up
+ final boolean extrude = (dx | dz) != 0 || !unlit;
+ for (int dy = 1; dy >= -1; --dy) {
+ this.initNibble(dx + chunkX, dy + sectionY, dz + chunkZ, extrude, false);
+ }
+ }
+ }
+ }
+
+ // check for de-init and lazy-init
+ // lazy init is when chunks are being lit, so at the time they weren't loaded when their neighbours were running
+ // init checks.
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ // does this neighbour have 1 radius loaded?
+ boolean neighboursLoaded = true;
+ neighbour_loaded_search:
+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
+ if (this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ) == null) {
+ neighboursLoaded = false;
+ break neighbour_loaded_search;
+ }
+ }
+ }
+
+ for (int sectionY = this.maxLightSection; sectionY >= this.minLightSection; --sectionY) {
+ // check neighbours to see if we need to de-init this one
+ boolean allEmpty = true;
+ neighbour_search:
+ for (int dy2 = -1; dy2 <= 1; ++dy2) {
+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
+ final int y = sectionY + dy2;
+ if (y < this.minSection || y > this.maxSection) {
+ // empty
+ continue;
+ }
+ final boolean[] emptinessMap = this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ);
+ if (emptinessMap != null) {
+ if (!emptinessMap[y - this.minSection]) {
+ allEmpty = false;
+ break neighbour_search;
+ }
+ } else {
+ final LevelChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ);
+ if (section != null && !section.hasOnlyAir()) {
+ allEmpty = false;
+ break neighbour_search;
+ }
+ }
+ }
+ }
+ }
+
+ if (allEmpty & neighboursLoaded) {
+ // can only de-init when neighbours are loaded
+ // de-init is fine to delay, as de-init is just an optimisation - it's not required for lighting
+ // to be correct
+
+ // all were empty, so de-init
+ this.setNibbleNull(dx + chunkX, sectionY, dz + chunkZ);
+ } else if (!allEmpty) {
+ // must init
+ final boolean extrude = (dx | dz) != 0 || !unlit;
+ this.initNibble(dx + chunkX, sectionY, dz + chunkZ, extrude, false);
+ }
+ }
+ }
+ }
+
+ return ret;
+ }
+
+ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ) {
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
+ try {
+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
+ if (chunk == null) {
+ return;
+ }
+ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection);
+ this.updateVisible(lightAccess);
+ } finally {
+ this.destroyCaches();
+ }
+ }
+
+ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) {
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
+ try {
+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
+ if (chunk == null) {
+ return;
+ }
+ this.checkChunkEdges(lightAccess, chunk, sections);
+ this.updateVisible(lightAccess);
+ } finally {
+ this.destroyCaches();
+ }
+ }
+
+ // subclasses should not initialise caches, as this will always be done by the super call
+ // subclasses should not invoke updateVisible, as this will always be done by the super call
+ // needsEdgeChecks applies when possibly loading vanilla data, which means we need to validate the current
+ // chunks light values with respect to neighbours
+ // subclasses should note that the emptiness changes are propagated BEFORE this is called, so this function
+ // does not need to detect empty chunks itself (and it should do no handling for them either!)
+ protected abstract void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks);
+
+ public final void light(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptySections) {
+ final int chunkX = chunk.getPos().x;
+ final int chunkZ = chunk.getPos().z;
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
+
+ try {
+ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.maxLightSection - this.minLightSection + 1);
+ // force current chunk into cache
+ this.setChunkInCache(chunkX, chunkZ, chunk);
+ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections());
+ this.setNibblesForChunkInCache(chunkX, chunkZ, nibbles);
+ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk));
+
+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptySections, true);
+ if (ret != null) {
+ this.setEmptinessMap(chunk, ret);
+ }
+ this.lightChunk(lightAccess, chunk, true);
+ this.setNibbles(chunk, nibbles);
+ this.updateVisible(lightAccess);
+ } finally {
+ this.destroyCaches();
+ }
+ }
+
+ public final void relightChunks(final LightChunkGetter lightAccess, final Set<ChunkPos> chunks,
+ final Consumer<ChunkPos> chunkLightCallback, final IntConsumer onComplete) {
+ // it's recommended for maximum performance that the set is ordered according to a BFS from the center of
+ // the region of chunks to relight
+ // it's required that tickets are added for each chunk to keep them loaded
+ final Long2ObjectOpenHashMap<SWMRNibbleArray[]> nibblesByChunk = new Long2ObjectOpenHashMap<>();
+ final Long2ObjectOpenHashMap<boolean[]> emptinessMapByChunk = new Long2ObjectOpenHashMap<>();
+
+ final int[] neighbourLightOrder = new int[] {
+ // d = 0
+ 0, 0,
+ // d = 1
+ -1, 0,
+ 0, -1,
+ 1, 0,
+ 0, 1,
+ // d = 2
+ -1, 1,
+ 1, 1,
+ -1, -1,
+ 1, -1,
+ };
+
+ int lightCalls = 0;
+
+ for (final ChunkPos chunkPos : chunks) {
+ final int chunkX = chunkPos.x;
+ final int chunkZ = chunkPos.z;
+ final ChunkAccess chunk = (ChunkAccess)lightAccess.getChunkForLighting(chunkX, chunkZ);
+ if (chunk == null || !this.canUseChunk(chunk)) {
+ throw new IllegalStateException();
+ }
+
+ for (int i = 0, len = neighbourLightOrder.length; i < len; i += 2) {
+ final int dx = neighbourLightOrder[i];
+ final int dz = neighbourLightOrder[i + 1];
+ final int neighbourX = dx + chunkX;
+ final int neighbourZ = dz + chunkZ;
+
+ final ChunkAccess neighbour = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX, neighbourZ);
+ if (neighbour == null || !this.canUseChunk(neighbour)) {
+ continue;
+ }
+
+ if (nibblesByChunk.get(CoordinateUtils.getChunkKey(neighbourX, neighbourZ)) != null) {
+ // lit already called for neighbour, no need to light it now
+ continue;
+ }
+
+ // light neighbour chunk
+ this.setupEncodeOffset(neighbourX * 16 + 7, 128, neighbourZ * 16 + 7);
+ try {
+ // insert all neighbouring chunks for this neighbour that we have data for
+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
+ final int neighbourX2 = neighbourX + dx2;
+ final int neighbourZ2 = neighbourZ + dz2;
+ final long key = CoordinateUtils.getChunkKey(neighbourX2, neighbourZ2);
+ final ChunkAccess neighbour2 = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX2, neighbourZ2);
+ if (neighbour2 == null || !this.canUseChunk(neighbour2)) {
+ continue;
+ }
+
+ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(key);
+ if (nibbles == null) {
+ // we haven't lit this chunk
+ continue;
+ }
+
+ this.setChunkInCache(neighbourX2, neighbourZ2, neighbour2);
+ this.setBlocksForChunkInCache(neighbourX2, neighbourZ2, neighbour2.getSections());
+ this.setNibblesForChunkInCache(neighbourX2, neighbourZ2, nibbles);
+ this.setEmptinessMapCache(neighbourX2, neighbourZ2, emptinessMapByChunk.get(key));
+ }
+ }
+
+ final long key = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
+
+ // now insert the neighbour chunk and light it
+ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.world);
+ nibblesByChunk.put(key, nibbles);
+
+ this.setChunkInCache(neighbourX, neighbourZ, neighbour);
+ this.setBlocksForChunkInCache(neighbourX, neighbourZ, neighbour.getSections());
+ this.setNibblesForChunkInCache(neighbourX, neighbourZ, nibbles);
+
+ final boolean[] neighbourEmptiness = this.handleEmptySectionChanges(lightAccess, neighbour, getEmptySectionsForChunk(neighbour), true);
+ emptinessMapByChunk.put(key, neighbourEmptiness);
+ if (chunks.contains(new ChunkPos(neighbourX, neighbourZ))) {
+ this.setEmptinessMap(neighbour, neighbourEmptiness);
+ }
+
+ this.lightChunk(lightAccess, neighbour, false);
+ } finally {
+ this.destroyCaches();
+ }
+ }
+
+ // done lighting all neighbours, so the chunk is now fully lit
+
+ // make sure nibbles are fully updated before calling back
+ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ for (final SWMRNibbleArray nibble : nibbles) {
+ nibble.updateVisible();
+ }
+
+ this.setNibbles(chunk, nibbles);
+
+ for (int y = this.minLightSection; y <= this.maxLightSection; ++y) {
+ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkZ));
+ }
+
+ // now do callback
+ if (chunkLightCallback != null) {
+ chunkLightCallback.accept(chunkPos);
+ }
+ ++lightCalls;
+ }
+
+ if (onComplete != null) {
+ onComplete.accept(lightCalls);
+ }
+ }
+
+ // contains:
+ // lower (6 + 6 + 16) = 28 bits: encoded coordinate position (x | (z << 6) | (y << (6 + 6))))
+ // next 4 bits: propagated light level (0, 15]
+ // next 6 bits: propagation direction bitset
+ // next 24 bits: unused
+ // last 3 bits: state flags
+ // state flags:
+ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading light
+ // updates for block sources
+ protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 2;
+ // whether the propagation needs to check if its current level is equal to the expected level
+ // used only in increase propagation
+ protected static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 1;
+ // whether the propagation needs to consider if its block is conditionally transparent
+ protected static final long FLAG_HAS_SIDED_TRANSPARENT_BLOCKS = Long.MIN_VALUE;
+
+ protected long[] increaseQueue = new long[16 * 16 * 16];
+ protected int increaseQueueInitialLength;
+ protected long[] decreaseQueue = new long[16 * 16 * 16];
+ protected int decreaseQueueInitialLength;
+
+ protected final long[] resizeIncreaseQueue() {
+ return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2);
+ }
+
+ protected final long[] resizeDecreaseQueue() {
+ return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2);
+ }
+
+ protected final void appendToIncreaseQueue(final long value) {
+ final int idx = this.increaseQueueInitialLength++;
+ long[] queue = this.increaseQueue;
+ if (idx >= queue.length) {
+ queue = this.resizeIncreaseQueue();
+ queue[idx] = value;
+ } else {
+ queue[idx] = value;
+ }
+ }
+
+ protected final void appendToDecreaseQueue(final long value) {
+ final int idx = this.decreaseQueueInitialLength++;
+ long[] queue = this.decreaseQueue;
+ if (idx >= queue.length) {
+ queue = this.resizeDecreaseQueue();
+ queue[idx] = value;
+ } else {
+ queue[idx] = value;
+ }
+ }
+
+ protected static final AxisDirection[][] OLD_CHECK_DIRECTIONS = new AxisDirection[1 << 6][];
+ protected static final int ALL_DIRECTIONS_BITSET = (1 << 6) - 1;
+ static {
+ for (int i = 0; i < OLD_CHECK_DIRECTIONS.length; ++i) {
+ final List<AxisDirection> directions = new ArrayList<>();
+ for (int bitset = i, len = Integer.bitCount(i), index = 0; index < len; ++index, bitset ^= IntegerUtil.getTrailingBit(bitset)) {
+ directions.add(AXIS_DIRECTIONS[IntegerUtil.trailingZeros(bitset)]);
+ }
+ OLD_CHECK_DIRECTIONS[i] = directions.toArray(new AxisDirection[0]);
+ }
+ }
+
+ protected final void performLightIncrease(final LightChunkGetter lightAccess) {
+ final BlockGetter world = lightAccess.getLevel();
+ long[] queue = this.increaseQueue;
+ int queueReadIndex = 0;
+ int queueLength = this.increaseQueueInitialLength;
+ this.increaseQueueInitialLength = 0;
+ final int decodeOffsetX = -this.encodeOffsetX;
+ final int decodeOffsetY = -this.encodeOffsetY;
+ final int decodeOffsetZ = -this.encodeOffsetZ;
+ final int encodeOffset = this.coordinateOffset;
+ final int sectionOffset = this.chunkSectionIndexOffset;
+
+ while (queueReadIndex < queueLength) {
+ final long queueValue = queue[queueReadIndex++];
+
+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xFL);
+ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63L)];
+
+ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) {
+ if (this.getLightLevel(posX, posY, posZ) != propagatedLightLevel) {
+ // not at the level we expect, so something changed.
+ continue;
+ }
+ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) {
+ // these are used to restore block sources after a propagation decrease
+ this.setLightLevel(posX, posY, posZ, propagatedLightLevel);
+ }
+
+ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) {
+ // we don't need to worry about our state here.
+ for (final AxisDirection propagate : checkDirections) {
+ final int offX = posX + propagate.x;
+ final int offY = posY + propagate.y;
+ final int offZ = posZ + propagate.z;
+
+ 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 SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
+ final int currentLevel;
+ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) {
+ continue; // already at the level we want or unloaded
+ }
+
+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
+ 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(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) {
+ continue;
+ }
+
+ 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))
+ | (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();
+
+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
+ continue;
+ }
+
+ 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 SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
+ final int currentLevel;
+
+ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) {
+ continue; // already at the level we want
+ }
+
+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
+ 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;
+ }
+
+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
+ if (targetLevel <= currentLevel) {
+ continue;
+ }
+
+ 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))
+ | (flags);
+ }
+ continue;
+ }
+ }
+ }
+ }
+ }
+
+ protected final void performLightDecrease(final LightChunkGetter lightAccess) {
+ final BlockGetter world = lightAccess.getLevel();
+ long[] queue = this.decreaseQueue;
+ long[] increaseQueue = this.increaseQueue;
+ int queueReadIndex = 0;
+ int queueLength = this.decreaseQueueInitialLength;
+ this.decreaseQueueInitialLength = 0;
+ int increaseQueueLength = this.increaseQueueInitialLength;
+ final int decodeOffsetX = -this.encodeOffsetX;
+ final int decodeOffsetY = -this.encodeOffsetY;
+ final int decodeOffsetZ = -this.encodeOffsetZ;
+ final int encodeOffset = this.coordinateOffset;
+ final int sectionOffset = this.chunkSectionIndexOffset;
+ final int emittedMask = this.emittedLightMask;
+
+ while (queueReadIndex < queueLength) {
+ final long queueValue = queue[queueReadIndex++];
+
+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF);
+ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63)];
+
+ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) {
+ // we don't need to worry about our state here.
+ for (final AxisDirection propagate : checkDirections) {
+ final int offX = posX + propagate.x;
+ final int offY = posY + propagate.y;
+ final int offZ = posZ + propagate.z;
+
+ 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 SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
+ final int lightLevel;
+
+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) {
+ // already at lowest (or unloaded), nothing we can do
+ continue;
+ }
+
+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
+ 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);
+
+ 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));
+ 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;
+ }
+
+ 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 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);
+ }
+
+ 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;
+ }
+ 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 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();
+
+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
+ continue;
+ }
+
+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
+ final int lightLevel;
+
+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) {
+ // already at lowest (or unloaded), nothing we can do
+ continue;
+ }
+
+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
+ 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);
+
+ 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));
+ 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;
+ }
+
+ 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 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);
+ }
+
+ 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;
+ }
+ continue;
+ }
+ }
+ }
+ }
+
+ // propagate sources we clobbered
+ this.increaseQueueInitialLength = increaseQueueLength;
+ this.performLightIncrease(lightAccess);
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..c64ab41198a5e0c7cbcbe6452af11f82f5938862
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
@@ -0,0 +1,930 @@
+package ca.spottedleaf.moonrise.patches.starlight.light;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+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.ChunkSystemChunkStatus;
+import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.shorts.ShortCollection;
+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.SectionPos;
+import net.minecraft.server.level.ChunkLevel;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.TicketType;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.DataLayer;
+import net.minecraft.world.level.chunk.LightChunkGetter;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.level.lighting.LayerLightEventListener;
+import net.minecraft.world.level.lighting.LevelLightEngine;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+
+public final class StarLightInterface {
+
+ public static final TicketType<Long> CHUNK_WORK_TICKET = TicketType.create("starlight:chunk_work_ticket", Long::compareTo);
+ public static final int LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(ChunkStatus.LIGHT);
+ // ticket level = ChunkLevel.byStatus(FullChunkStatus.FULL) - input
+ public static final int REGION_LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.FULL) - LIGHT_TICKET_LEVEL;
+
+ /**
+ * Can be {@code null}, indicating the light is all empty.
+ */
+ public final Level world;
+ public final LightChunkGetter lightAccess;
+
+ private final ArrayDeque<SkyStarLightEngine> cachedSkyPropagators;
+ private final ArrayDeque<BlockStarLightEngine> cachedBlockPropagators;
+
+ private final LightQueue lightQueue;
+
+ private final LayerLightEventListener skyReader;
+ private final LayerLightEventListener blockReader;
+ private final boolean isClientSide;
+
+ public final int minSection;
+ public final int maxSection;
+ public final int minLightSection;
+ public final int maxLightSection;
+
+ public final LevelLightEngine lightEngine;
+
+ private final boolean hasBlockLight;
+ private final boolean hasSkyLight;
+
+ public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) {
+ this.lightAccess = lightAccess;
+ this.world = lightAccess == null ? null : (Level)lightAccess.getLevel();
+ this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null;
+ this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null;
+ this.isClientSide = !(this.world instanceof ServerLevel);
+ if (this.world == null) {
+ this.minSection = -4;
+ this.maxSection = 19;
+ this.minLightSection = -5;
+ this.maxLightSection = 20;
+ } else {
+ this.minSection = WorldUtil.getMinSection(this.world);
+ this.maxSection = WorldUtil.getMaxSection(this.world);
+ this.minLightSection = WorldUtil.getMinLightSection(this.world);
+ this.maxLightSection = WorldUtil.getMaxLightSection(this.world);
+ }
+
+ if (this.world instanceof ServerLevel) {
+ this.lightQueue = new ServerLightQueue(this);
+ } else {
+ this.lightQueue = new ClientLightQueue(this);
+ }
+
+ this.lightEngine = lightEngine;
+ this.hasBlockLight = hasBlockLight;
+ this.hasSkyLight = hasSkyLight;
+ this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() {
+ @Override
+ public void checkBlock(final BlockPos blockPos) {
+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable());
+ }
+
+ @Override
+ public void propagateLightSources(final ChunkPos chunkPos) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasLightWork() {
+ // not really correct...
+ return StarLightInterface.this.hasUpdates();
+ }
+
+ @Override
+ public int runLightUpdates() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DataLayer getDataLayerData(final SectionPos pos) {
+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
+ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) {
+ return null;
+ }
+
+ final int sectionY = pos.getY();
+
+ if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) {
+ return null;
+ }
+
+ if (((StarlightChunk)chunk).starlight$getSkyEmptinessMap() == null) {
+ return null;
+ }
+
+ return ((StarlightChunk)chunk).starlight$getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble();
+ }
+
+ @Override
+ public int getLightValue(final BlockPos blockPos) {
+ return StarLightInterface.this.getSkyLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4));
+ }
+
+ @Override
+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) {
+ StarLightInterface.this.sectionChange(pos, notReady);
+ }
+ };
+ this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() {
+ @Override
+ public void checkBlock(final BlockPos blockPos) {
+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable());
+ }
+
+ @Override
+ public void propagateLightSources(final ChunkPos chunkPos) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasLightWork() {
+ // not really correct...
+ return StarLightInterface.this.hasUpdates();
+ }
+
+ @Override
+ public int runLightUpdates() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DataLayer getDataLayerData(final SectionPos pos) {
+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
+
+ if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) {
+ return null;
+ }
+
+ return ((StarlightChunk)chunk).starlight$getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble();
+ }
+
+ @Override
+ public int getLightValue(final BlockPos blockPos) {
+ return StarLightInterface.this.getBlockLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4));
+ }
+
+ @Override
+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) {
+ StarLightInterface.this.sectionChange(pos, notReady);
+ }
+ };
+ }
+
+ public ClientLightQueue getClientLightQueue() {
+ if (this.lightQueue instanceof ClientLightQueue clientLightQueue) {
+ return clientLightQueue;
+ }
+ return null;
+ }
+
+ public ServerLightQueue getServerLightQueue() {
+ if (this.lightQueue instanceof ServerLightQueue serverLightQueue) {
+ return serverLightQueue;
+ }
+ return null;
+ }
+
+ public boolean hasSkyLight() {
+ return this.hasSkyLight;
+ }
+
+ public boolean hasBlockLight() {
+ return this.hasBlockLight;
+ }
+
+ public int getSkyLightValue(final BlockPos blockPos, final ChunkAccess chunk) {
+ if (!this.hasSkyLight) {
+ return 0;
+ }
+ final int x = blockPos.getX();
+ int y = blockPos.getY();
+ final int z = blockPos.getZ();
+
+ final int minSection = this.minSection;
+ final int maxSection = this.maxSection;
+ final int minLightSection = this.minLightSection;
+ final int maxLightSection = this.maxLightSection;
+
+ if (chunk == null || (!this.isClientSide && !chunk.isLightCorrect()) || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) {
+ return 15;
+ }
+
+ int sectionY = y >> 4;
+
+ if (sectionY > maxLightSection) {
+ return 15;
+ }
+
+ if (sectionY < minLightSection) {
+ sectionY = minLightSection;
+ y = sectionY << 4;
+ }
+
+ final SWMRNibbleArray[] nibbles = ((StarlightChunk)chunk).starlight$getSkyNibbles();
+ final SWMRNibbleArray immediate = nibbles[sectionY - minLightSection];
+
+ if (!immediate.isNullNibbleVisible()) {
+ return immediate.getVisible(x, y, z);
+ }
+
+ final boolean[] emptinessMap = ((StarlightChunk)chunk).starlight$getSkyEmptinessMap();
+
+ if (emptinessMap == null) {
+ return 15;
+ }
+
+ // are we above this chunk's lowest empty section?
+ int lowestY = minLightSection - 1;
+ for (int currY = maxSection; currY >= minSection; --currY) {
+ if (emptinessMap[currY - minSection]) {
+ continue;
+ }
+
+ // should always be full lit here
+ lowestY = currY;
+ break;
+ }
+
+ if (sectionY > lowestY) {
+ return 15;
+ }
+
+ // this nibble is going to depend solely on the skylight data above it
+ // find first non-null data above (there does exist one, as we just found it above)
+ for (int currY = sectionY + 1; currY <= maxLightSection; ++currY) {
+ final SWMRNibbleArray nibble = nibbles[currY - minLightSection];
+ if (!nibble.isNullNibbleVisible()) {
+ return nibble.getVisible(x, 0, z);
+ }
+ }
+
+ // should never reach here
+ return 15;
+ }
+
+ public int getBlockLightValue(final BlockPos blockPos, final ChunkAccess chunk) {
+ if (!this.hasBlockLight) {
+ return 0;
+ }
+ final int y = blockPos.getY();
+ final int cy = y >> 4;
+
+ final int minLightSection = this.minLightSection;
+ final int maxLightSection = this.maxLightSection;
+
+ if (cy < minLightSection || cy > maxLightSection) {
+ return 0;
+ }
+
+ if (chunk == null) {
+ return 0;
+ }
+
+ final SWMRNibbleArray nibble = ((StarlightChunk)chunk).starlight$getBlockNibbles()[cy - minLightSection];
+ return nibble.getVisible(blockPos.getX(), y, blockPos.getZ());
+ }
+
+ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) {
+ final ChunkAccess chunk = this.getAnyChunkNow(pos.getX() >> 4, pos.getZ() >> 4);
+
+ final int sky = this.getSkyLightValue(pos, chunk) - ambientDarkness;
+ // Don't fetch the block light level if the skylight level is 15, since the value will never be higher.
+ if (sky == 15) {
+ return 15;
+ }
+ final int block = this.getBlockLightValue(pos, chunk);
+ return Math.max(sky, block);
+ }
+
+ public LayerLightEventListener getSkyReader() {
+ return this.skyReader;
+ }
+
+ public LayerLightEventListener getBlockReader() {
+ return this.blockReader;
+ }
+
+ public boolean isClientSide() {
+ return this.isClientSide;
+ }
+
+ public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) {
+ if (this.world == null) {
+ // empty world
+ return null;
+ }
+ return ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ);
+ }
+
+ public boolean hasUpdates() {
+ return !this.lightQueue.isEmpty();
+ }
+
+ public Level getWorld() {
+ return this.world;
+ }
+
+ public LightChunkGetter getLightAccess() {
+ return this.lightAccess;
+ }
+
+ public SkyStarLightEngine getSkyLightEngine() {
+ if (this.cachedSkyPropagators == null) {
+ return null;
+ }
+ final SkyStarLightEngine ret;
+ synchronized (this.cachedSkyPropagators) {
+ ret = this.cachedSkyPropagators.pollFirst();
+ }
+
+ if (ret == null) {
+ return new SkyStarLightEngine(this.world);
+ }
+ return ret;
+ }
+
+ public void releaseSkyLightEngine(final SkyStarLightEngine engine) {
+ if (this.cachedSkyPropagators == null) {
+ return;
+ }
+ synchronized (this.cachedSkyPropagators) {
+ this.cachedSkyPropagators.addFirst(engine);
+ }
+ }
+
+ public BlockStarLightEngine getBlockLightEngine() {
+ if (this.cachedBlockPropagators == null) {
+ return null;
+ }
+ final BlockStarLightEngine ret;
+ synchronized (this.cachedBlockPropagators) {
+ ret = this.cachedBlockPropagators.pollFirst();
+ }
+
+ if (ret == null) {
+ return new BlockStarLightEngine(this.world);
+ }
+ return ret;
+ }
+
+ public void releaseBlockLightEngine(final BlockStarLightEngine engine) {
+ if (this.cachedBlockPropagators == null) {
+ return;
+ }
+ synchronized (this.cachedBlockPropagators) {
+ this.cachedBlockPropagators.addFirst(engine);
+ }
+ }
+
+ public LightQueue.ChunkTasks blockChange(final BlockPos pos) {
+ if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world
+ return null;
+ }
+
+ return this.lightQueue.queueBlockChange(pos);
+ }
+
+ public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) {
+ if (this.world == null) { // empty world
+ return null;
+ }
+
+ return this.lightQueue.queueSectionChange(pos, newEmptyValue);
+ }
+
+ public void forceLoadInChunk(final ChunkAccess chunk, final Boolean[] emptySections) {
+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
+
+ try {
+ if (skyEngine != null) {
+ skyEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections);
+ }
+ if (blockEngine != null) {
+ blockEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections);
+ }
+ } finally {
+ this.releaseSkyLightEngine(skyEngine);
+ this.releaseBlockLightEngine(blockEngine);
+ }
+ }
+
+ public void loadInChunk(final int chunkX, final int chunkZ, final Boolean[] emptySections) {
+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
+
+ try {
+ if (skyEngine != null) {
+ skyEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections);
+ }
+ if (blockEngine != null) {
+ blockEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections);
+ }
+ } finally {
+ this.releaseSkyLightEngine(skyEngine);
+ this.releaseBlockLightEngine(blockEngine);
+ }
+ }
+
+ public void lightChunk(final ChunkAccess chunk, final Boolean[] emptySections) {
+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
+
+ try {
+ if (skyEngine != null) {
+ skyEngine.light(this.lightAccess, chunk, emptySections);
+ }
+ if (blockEngine != null) {
+ blockEngine.light(this.lightAccess, chunk, emptySections);
+ }
+ } finally {
+ this.releaseSkyLightEngine(skyEngine);
+ this.releaseBlockLightEngine(blockEngine);
+ }
+ }
+
+ public void relightChunks(final Set<ChunkPos> chunks, final Consumer<ChunkPos> chunkLightCallback,
+ final IntConsumer onComplete) {
+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
+
+ try {
+ if (skyEngine != null) {
+ skyEngine.relightChunks(this.lightAccess, chunks, blockEngine == null ? chunkLightCallback : null,
+ blockEngine == null ? onComplete : null);
+ }
+ if (blockEngine != null) {
+ blockEngine.relightChunks(this.lightAccess, chunks, chunkLightCallback, onComplete);
+ }
+ } finally {
+ this.releaseSkyLightEngine(skyEngine);
+ this.releaseBlockLightEngine(blockEngine);
+ }
+ }
+
+ public void checkChunkEdges(final int chunkX, final int chunkZ) {
+ this.checkSkyEdges(chunkX, chunkZ);
+ this.checkBlockEdges(chunkX, chunkZ);
+ }
+
+ public void checkSkyEdges(final int chunkX, final int chunkZ) {
+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
+
+ try {
+ if (skyEngine != null) {
+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ);
+ }
+ } finally {
+ this.releaseSkyLightEngine(skyEngine);
+ }
+ }
+
+ public void checkBlockEdges(final int chunkX, final int chunkZ) {
+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
+ try {
+ if (blockEngine != null) {
+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ);
+ }
+ } finally {
+ this.releaseBlockLightEngine(blockEngine);
+ }
+ }
+
+ public void propagateChanges() {
+ final LightQueue lightQueue = this.lightQueue;
+ if (lightQueue instanceof ClientLightQueue clientLightQueue) {
+ clientLightQueue.drainTasks();
+ } // else: invalid usage, although we won't throw because mods...
+ }
+
+ public static abstract class LightQueue {
+
+ protected final StarLightInterface lightInterface;
+
+ public LightQueue(final StarLightInterface lightInterface) {
+ this.lightInterface = lightInterface;
+ }
+
+ public abstract boolean isEmpty();
+
+ public abstract ChunkTasks queueBlockChange(final BlockPos pos);
+
+ public abstract ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue);
+
+ public abstract ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections);
+
+ public abstract ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections);
+
+ public static abstract class ChunkTasks implements Runnable {
+
+ public final long chunkCoordinate;
+
+ protected final StarLightInterface lightEngine;
+ protected final LightQueue queue;
+ protected final MultiThreadedQueue<Runnable> onComplete = new MultiThreadedQueue<>();
+ protected final Set<BlockPos> changedPositions = new HashSet<>();
+ protected Boolean[] changedSectionSet;
+ protected ShortOpenHashSet queuedEdgeChecksSky;
+ protected ShortOpenHashSet queuedEdgeChecksBlock;
+ protected List<BooleanSupplier> lightTasks;
+
+ public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue) {
+ this.chunkCoordinate = chunkCoordinate;
+ this.lightEngine = lightEngine;
+ this.queue = queue;
+ }
+
+ @Override
+ public abstract void run();
+
+ public void queueOrRunTask(final Runnable run) {
+ if (!this.onComplete.add(run)) {
+ run.run();
+ }
+ }
+
+ protected void addChangedPosition(final BlockPos pos) {
+ this.changedPositions.add(pos.immutable());
+ }
+
+ protected void setChangedSection(final int y, final Boolean newEmptyValue) {
+ if (this.changedSectionSet == null) {
+ this.changedSectionSet = new Boolean[this.lightEngine.maxSection - this.lightEngine.minSection + 1];
+ }
+ this.changedSectionSet[y - this.lightEngine.minSection] = newEmptyValue;
+ }
+
+ protected void addLightTask(final BooleanSupplier lightTask) {
+ if (this.lightTasks == null) {
+ this.lightTasks = new ArrayList<>();
+ }
+ this.lightTasks.add(lightTask);
+ }
+
+ protected void addEdgeChecksSky(final ShortCollection values) {
+ if (this.queuedEdgeChecksSky == null) {
+ this.queuedEdgeChecksSky = new ShortOpenHashSet(Math.max(8, values.size()));
+ }
+ this.queuedEdgeChecksSky.addAll(values);
+ }
+
+ protected void addEdgeChecksBlock(final ShortCollection values) {
+ if (this.queuedEdgeChecksBlock == null) {
+ this.queuedEdgeChecksBlock = new ShortOpenHashSet(Math.max(8, values.size()));
+ }
+ this.queuedEdgeChecksBlock.addAll(values);
+ }
+
+ protected final void runTasks() {
+ boolean litChunk = false;
+ if (this.lightTasks != null) {
+ for (final BooleanSupplier run : this.lightTasks) {
+ if (run.getAsBoolean()) {
+ litChunk = true;
+ break;
+ }
+ }
+ }
+
+ if (!litChunk) {
+ final SkyStarLightEngine skyEngine = this.lightEngine.getSkyLightEngine();
+ final BlockStarLightEngine blockEngine = this.lightEngine.getBlockLightEngine();
+ try {
+ final long coordinate = this.chunkCoordinate;
+ final int chunkX = CoordinateUtils.getChunkX(coordinate);
+ final int chunkZ = CoordinateUtils.getChunkZ(coordinate);
+
+ final Set<BlockPos> positions = this.changedPositions;
+ final Boolean[] sectionChanges = this.changedSectionSet;
+
+ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
+ skyEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges);
+ }
+ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
+ blockEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges);
+ }
+
+ if (skyEngine != null && this.queuedEdgeChecksSky != null) {
+ skyEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksSky);
+ }
+ if (blockEngine != null && this.queuedEdgeChecksBlock != null) {
+ blockEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksBlock);
+ }
+ } finally {
+ this.lightEngine.releaseSkyLightEngine(skyEngine);
+ this.lightEngine.releaseBlockLightEngine(blockEngine);
+ }
+ }
+
+ Runnable run;
+ while ((run = this.onComplete.pollOrBlockAdds()) != null) {
+ run.run();
+ }
+ }
+ }
+ }
+
+ public static final class ClientLightQueue extends LightQueue {
+
+ private final Long2ObjectLinkedOpenHashMap<ClientChunkTasks> chunkTasks = new Long2ObjectLinkedOpenHashMap<>();
+
+ public ClientLightQueue(final StarLightInterface lightInterface) {
+ super(lightInterface);
+ }
+
+ @Override
+ public synchronized boolean isEmpty() {
+ return this.chunkTasks.isEmpty();
+ }
+
+ // must hold synchronized lock on this object
+ private ClientChunkTasks getOrCreate(final long key) {
+ return this.chunkTasks.computeIfAbsent(key, (final long keyInMap) -> {
+ return new ClientChunkTasks(keyInMap, ClientLightQueue.this.lightInterface, ClientLightQueue.this);
+ });
+ }
+
+ @Override
+ public synchronized ClientChunkTasks queueBlockChange(final BlockPos pos) {
+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
+ tasks.addChangedPosition(pos);
+ return tasks;
+ }
+
+ @Override
+ public synchronized ClientChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) {
+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
+
+ tasks.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue));
+
+ return tasks;
+ }
+
+ @Override
+ public synchronized ClientChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
+
+ tasks.addEdgeChecksSky(sections);
+
+ return tasks;
+ }
+
+ @Override
+ public synchronized ClientChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
+
+ tasks.addEdgeChecksBlock(sections);
+
+ return tasks;
+ }
+
+ public synchronized ClientChunkTasks removeFirstTask() {
+ if (this.chunkTasks.isEmpty()) {
+ return null;
+ }
+ return this.chunkTasks.removeFirst();
+ }
+
+ public void drainTasks() {
+ ClientChunkTasks task;
+ while ((task = this.removeFirstTask()) != null) {
+ task.runTasks();
+ }
+ }
+
+ public static final class ClientChunkTasks extends ChunkTasks {
+
+ public ClientChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final ClientLightQueue queue) {
+ super(chunkCoordinate, lightEngine, queue);
+ }
+
+ @Override
+ public void run() {
+ this.runTasks();
+ }
+ }
+ }
+
+ public static final class ServerLightQueue extends LightQueue {
+
+ private final ConcurrentLong2ReferenceChainedHashTable<ServerChunkTasks> chunkTasks = new ConcurrentLong2ReferenceChainedHashTable<>();
+
+ public ServerLightQueue(final StarLightInterface lightInterface) {
+ super(lightInterface);
+ }
+
+ public void lowerPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.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) {
+ 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) {
+ 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) {
+ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ if (task != null) {
+ return task.getPriority();
+ }
+
+ return PrioritisedExecutor.Priority.COMPLETING;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.chunkTasks.isEmpty();
+ }
+
+ @Override
+ public ServerChunkTasks queueBlockChange(final BlockPos pos) {
+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
+ if (valueInMap == null) {
+ valueInMap = new ServerChunkTasks(
+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
+ );
+ }
+ valueInMap.addChangedPosition(pos);
+ return valueInMap;
+ });
+
+ ret.schedule();
+
+ return ret;
+ }
+
+ @Override
+ public ServerChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) {
+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
+ if (valueInMap == null) {
+ valueInMap = new ServerChunkTasks(
+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
+ );
+ }
+
+ valueInMap.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue));
+
+ return valueInMap;
+ });
+
+ ret.schedule();
+
+ return ret;
+ }
+
+ public ServerChunkTasks queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final PrioritisedExecutor.Priority priority) {
+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
+ if (valueInMap == null) {
+ valueInMap = new ServerChunkTasks(
+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this, priority
+ );
+ }
+
+ valueInMap.addLightTask(lightTask);
+
+ return valueInMap;
+ });
+
+ ret.schedule();
+
+ return ret;
+ }
+
+ @Override
+ public ServerChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
+ if (valueInMap == null) {
+ valueInMap = new ServerChunkTasks(
+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
+ );
+ }
+
+ valueInMap.addEdgeChecksSky(sections);
+
+ return valueInMap;
+ });
+
+ ret.schedule();
+
+ return ret;
+ }
+
+ @Override
+ public ServerChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
+ if (valueInMap == null) {
+ valueInMap = new ServerChunkTasks(
+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
+ );
+ }
+
+ valueInMap.addEdgeChecksBlock(sections);
+
+ return valueInMap;
+ });
+
+ ret.schedule();
+
+ return ret;
+ }
+
+ public static final class ServerChunkTasks extends ChunkTasks {
+
+ private final AtomicBoolean ticketAdded = new AtomicBoolean();
+ private final PrioritisedExecutor.PrioritisedTask task;
+
+ public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine,
+ final ServerLightQueue queue) {
+ this(chunkCoordinate, lightEngine, queue, PrioritisedExecutor.Priority.NORMAL);
+ }
+
+ public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine,
+ final ServerLightQueue queue, final PrioritisedExecutor.Priority priority) {
+ super(chunkCoordinate, lightEngine, queue);
+ this.task = ((ChunkSystemServerLevel)(ServerLevel)lightEngine.getWorld()).moonrise$getChunkTaskScheduler().radiusAwareScheduler.createTask(
+ CoordinateUtils.getChunkX(chunkCoordinate), CoordinateUtils.getChunkZ(chunkCoordinate),
+ ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$getWriteRadius(), this, priority
+ );
+ }
+
+ public boolean markTicketAdded() {
+ return !this.ticketAdded.get() && !this.ticketAdded.getAndSet(true);
+ }
+
+ public void schedule() {
+ this.task.queue();
+ }
+
+ public boolean cancel() {
+ return this.task.cancel();
+ }
+
+ public PrioritisedExecutor.Priority getPriority() {
+ return this.task.getPriority();
+ }
+
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ this.task.lowerPriority(priority);
+ }
+
+ public void setPriority(final PrioritisedExecutor.Priority priority) {
+ this.task.setPriority(priority);
+ }
+
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
+ this.task.raisePriority(priority);
+ }
+
+ @Override
+ public void run() {
+ ((ServerLightQueue)this.queue).chunkTasks.remove(this.chunkCoordinate, this);
+
+ this.runTasks();
+ }
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..7fe59ab70557aa6a484a02db2b2007fdd9e4bbb8
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java
@@ -0,0 +1,29 @@
+package ca.spottedleaf.moonrise.patches.starlight.light;
+
+import net.minecraft.core.SectionPos;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.LightLayer;
+import net.minecraft.world.level.chunk.DataLayer;
+import net.minecraft.world.level.chunk.LevelChunk;
+import java.util.Collection;
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+
+public interface StarLightLightingProvider {
+
+ public StarLightInterface starlight$getLightEngine();
+
+ public void starlight$clientUpdateLight(final LightLayer lightType, final SectionPos pos,
+ final DataLayer nibble, final boolean trustEdges);
+
+ public void starlight$clientRemoveLightData(final ChunkPos chunkPos);
+
+ public void starlight$clientChunkLoad(final ChunkPos pos, final LevelChunk chunk);
+
+ public default int starlight$serverRelightChunks(final Collection<ChunkPos> chunks,
+ final Consumer<ChunkPos> chunkLightCallback,
+ final IntConsumer onComplete) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..57692a503e147a00ac4e1586cd78e12b71a80d3f
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
@@ -0,0 +1,188 @@
+package ca.spottedleaf.moonrise.patches.starlight.util;
+
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
+import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray;
+import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine;
+import com.mojang.logging.LogUtils;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.ListTag;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import org.slf4j.Logger;
+
+public final class SaveUtil {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ private 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 void saveLightHook(final Level world, final ChunkAccess chunk, final CompoundTag nbt) {
+ try {
+ saveLightHookReal(world, chunk, nbt);
+ } catch (final Throwable ex) {
+ // failing to inject is not fatal so we catch anything here. if it fails, it will have correctly set lit to false
+ // for Vanilla to relight on load and it will not set our lit tag so we will relight on load
+ LOGGER.warn("Failed to inject light data into save data for chunk " + chunk.getPos() + ", chunk light will be recalculated on its next load", ex);
+ }
+ }
+
+ private static void saveLightHookReal(final Level world, final ChunkAccess chunk, final CompoundTag tag) {
+ if (tag == null) {
+ return;
+ }
+
+ final int minSection = WorldUtil.getMinLightSection(world);
+ final int maxSection = WorldUtil.getMaxLightSection(world);
+
+ SWMRNibbleArray[] blockNibbles = ((StarlightChunk)chunk).starlight$getBlockNibbles();
+ SWMRNibbleArray[] skyNibbles = ((StarlightChunk)chunk).starlight$getSkyNibbles();
+
+ boolean lit = chunk.isLightCorrect() || !(world instanceof ServerLevel);
+ // diff start - store our tag for whether light data is init'd
+ if (lit) {
+ tag.putBoolean("isLightOn", false);
+ }
+ // diff end - store our tag for whether light data is init'd
+ ChunkStatus status = ChunkStatus.byName(tag.getString("Status"));
+
+ CompoundTag[] sections = new CompoundTag[maxSection - minSection + 1];
+
+ ListTag sectionsStored = tag.getList("sections", 10);
+
+ for (int i = 0; i < sectionsStored.size(); ++i) {
+ CompoundTag sectionStored = sectionsStored.getCompound(i);
+ int k = sectionStored.getByte("Y");
+
+ // strip light data
+ sectionStored.remove("BlockLight");
+ sectionStored.remove("SkyLight");
+
+ if (!sectionStored.isEmpty()) {
+ sections[k - minSection] = sectionStored;
+ }
+ }
+
+ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) {
+ for (int i = minSection; i <= maxSection; ++i) {
+ SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState();
+ SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState();
+ if (blockNibble != null || skyNibble != null) {
+ CompoundTag section = sections[i - minSection];
+ if (section == null) {
+ section = new CompoundTag();
+ section.putByte("Y", (byte)i);
+ sections[i - minSection] = section;
+ }
+
+ // we store under the same key so mod programs editing nbt
+ // can still read the data, hopefully.
+ // however, for compatibility we store chunks as unlit so vanilla
+ // is forced to re-light them if it encounters our data. It's too much of a burden
+ // to try and maintain compatibility with a broken and inferior skylight management system.
+
+ if (blockNibble != null) {
+ if (blockNibble.data != null) {
+ section.putByteArray("BlockLight", blockNibble.data);
+ }
+ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state);
+ }
+
+ if (skyNibble != null) {
+ if (skyNibble.data != null) {
+ section.putByteArray("SkyLight", skyNibble.data);
+ }
+ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state);
+ }
+ }
+ }
+ }
+
+ // rewrite section list
+ sectionsStored.clear();
+ for (CompoundTag section : sections) {
+ if (section != null) {
+ sectionsStored.add(section);
+ }
+ }
+ tag.put("sections", sectionsStored);
+ if (lit) {
+ tag.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // only mark as fully lit after we have successfully injected our data
+ }
+ }
+
+ public static void loadLightHook(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) {
+ try {
+ loadLightHookReal(world, pos, tag, into);
+ } catch (final Throwable ex) {
+ // failing to inject is not fatal so we catch anything here. if it fails, then we simply relight. Not a problem, we get correct
+ // lighting in both cases.
+ LOGGER.warn("Failed to load light for chunk " + pos + ", light will be recalculated", ex);
+ }
+ }
+
+ private static void loadLightHookReal(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) {
+ if (into == null) {
+ return;
+ }
+ final int minSection = WorldUtil.getMinLightSection(world);
+ final int maxSection = WorldUtil.getMaxLightSection(world);
+
+ into.setLightCorrect(false); // mark as unlit in case we fail parsing
+
+ SWMRNibbleArray[] blockNibbles = StarLightEngine.getFilledEmptyLight(world);
+ SWMRNibbleArray[] skyNibbles = StarLightEngine.getFilledEmptyLight(world);
+
+
+ // start copy from the original method
+ boolean lit = tag.get("isLightOn") != null && tag.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION;
+ boolean canReadSky = world.dimensionType().hasSkyLight();
+ ChunkStatus status = ChunkStatus.byName(tag.getString("Status"));
+ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { // diff - we add the status check here
+ ListTag sections = tag.getList("sections", 10);
+
+ for (int i = 0; i < sections.size(); ++i) {
+ CompoundTag sectionData = sections.getCompound(i);
+ int y = sectionData.getByte("Y");
+
+ if (sectionData.contains("BlockLight", 7)) {
+ // this is where our diff is
+ blockNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety
+ } else {
+ blockNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG));
+ }
+
+ if (canReadSky) {
+ if (sectionData.contains("SkyLight", 7)) {
+ // we store under the same key so mod programs editing nbt
+ // can still read the data, hopefully.
+ // however, for compatibility we store chunks as unlit so vanilla
+ // is forced to re-light them if it encounters our data. It's too much of a burden
+ // to try and maintain compatibility with a broken and inferior skylight management system.
+ skyNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety
+ } else {
+ skyNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG));
+ }
+ }
+ }
+ }
+ // end copy from vanilla
+
+ ((StarlightChunk)into).starlight$setBlockNibbles(blockNibbles);
+ ((StarlightChunk)into).starlight$setSkyNibbles(skyNibbles);
+ into.setLightCorrect(lit); // now we set lit here, only after we've correctly parsed data
+ }
+
+ private SaveUtil() {}
+}
diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java
index 46bf42d5ea9e7b046f962531c5962d287cf44a41..362765d977aaa1996f9cef3404c0676d7bbddf38 100644
--- a/src/main/java/io/papermc/paper/command/PaperCommand.java
+++ b/src/main/java/io/papermc/paper/command/PaperCommand.java
@@ -42,6 +42,8 @@ public final class PaperCommand extends Command {
commands.put(Set.of("dumpitem"), new DumpItemCommand());
commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand());
commands.put(Set.of("dumplisteners"), new DumpListenersCommand());
+ commands.put(Set.of("fixlight"), new FixLightCommand()); // Paper - rewrite chunk system
+ commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand()); // Paper - rewrite chunk system
return commands.entrySet().stream()
.flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue())))
diff --git a/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
new file mode 100644
index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd01fba0fc
--- /dev/null
+++ b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
@@ -0,0 +1,277 @@
+package io.papermc.paper.command.subcommands;
+
+import ca.spottedleaf.moonrise.common.util.JsonUtil;
+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;
+import io.papermc.paper.command.CommandUtil;
+import io.papermc.paper.command.PaperSubcommand;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+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.ProtoChunk;
+import org.bukkit.Bukkit;
+import org.bukkit.command.CommandSender;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+import static net.kyori.adventure.text.Component.text;
+import static net.kyori.adventure.text.format.NamedTextColor.BLUE;
+import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA;
+import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
+import static net.kyori.adventure.text.format.NamedTextColor.RED;
+
+@DefaultQualifier(NonNull.class)
+public final class ChunkDebugCommand implements PaperSubcommand {
+ @Override
+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
+ switch (subCommand) {
+ case "debug" -> this.doDebug(sender, args);
+ case "chunkinfo" -> this.doChunkInfo(sender, args);
+ case "holderinfo" -> this.doHolderInfo(sender, args);
+ }
+ return true;
+ }
+
+ @Override
+ public List<String> tabComplete(final CommandSender sender, final String subCommand, final String[] args) {
+ switch (subCommand) {
+ case "debug" -> {
+ if (args.length == 1) {
+ return CommandUtil.getListMatchingLast(sender, args, "help", "chunks");
+ }
+ }
+ case "holderinfo" -> {
+ List<String> worldNames = new ArrayList<>();
+ worldNames.add("*");
+ for (org.bukkit.World world : Bukkit.getWorlds()) {
+ worldNames.add(world.getName());
+ }
+ if (args.length == 1) {
+ return CommandUtil.getListMatchingLast(sender, args, worldNames);
+ }
+ }
+ case "chunkinfo" -> {
+ List<String> worldNames = new ArrayList<>();
+ worldNames.add("*");
+ for (org.bukkit.World world : Bukkit.getWorlds()) {
+ worldNames.add(world.getName());
+ }
+ if (args.length == 1) {
+ return CommandUtil.getListMatchingLast(sender, args, worldNames);
+ }
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ private void doChunkInfo(final CommandSender sender, final String[] args) {
+ List<org.bukkit.World> worlds;
+ if (args.length < 1 || args[0].equals("*")) {
+ worlds = Bukkit.getWorlds();
+ } else {
+ worlds = new ArrayList<>(args.length);
+ for (final String arg : args) {
+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg);
+ if (world == null) {
+ sender.sendMessage(text("World '" + arg + "' is invalid", RED));
+ return;
+ }
+ worlds.add(world);
+ }
+ }
+
+ int accumulatedTotal = 0;
+ int accumulatedInactive = 0;
+ int accumulatedBorder = 0;
+ int accumulatedTicking = 0;
+ int accumulatedEntityTicking = 0;
+
+ for (final org.bukkit.World bukkitWorld : worlds) {
+ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle();
+
+ int total = 0;
+ int inactive = 0;
+ int full = 0;
+ int blockTicking = 0;
+ int entityTicking = 0;
+
+ for (final NewChunkHolder holder : ((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders()) {
+ final NewChunkHolder.ChunkCompletion completion = holder.getLastChunkCompletion();
+ final ChunkAccess chunk = completion == null ? null : completion.chunk();
+
+ if (!(chunk instanceof LevelChunk fullChunk)) {
+ continue;
+ }
+
+ ++total;
+
+ switch (holder.getChunkStatus()) {
+ case INACCESSIBLE: {
+ ++inactive;
+ break;
+ }
+ case FULL: {
+ ++full;
+ break;
+ }
+ case BLOCK_TICKING: {
+ ++blockTicking;
+ break;
+ }
+ case ENTITY_TICKING: {
+ ++entityTicking;
+ break;
+ }
+ }
+ }
+
+ accumulatedTotal += total;
+ accumulatedInactive += inactive;
+ accumulatedBorder += full;
+ accumulatedTicking += blockTicking;
+ accumulatedEntityTicking += entityTicking;
+
+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":")));
+ sender.sendMessage(text().color(DARK_AQUA).append(
+ text("Total: ", BLUE), text(total),
+ text(" Inactive: ", BLUE), text(inactive),
+ text(" Full: ", BLUE), text(full),
+ text(" Block Ticking: ", BLUE), text(blockTicking),
+ text(" Entity Ticking: ", BLUE), text(entityTicking)
+ ));
+ }
+ if (worlds.size() > 1) {
+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA)));
+ sender.sendMessage(text().color(DARK_AQUA).append(
+ text("Total: ", BLUE), text(accumulatedTotal),
+ text(" Inactive: ", BLUE), text(accumulatedInactive),
+ text(" Full: ", BLUE), text(accumulatedBorder),
+ text(" Block Ticking: ", BLUE), text(accumulatedTicking),
+ text(" Entity Ticking: ", BLUE), text(accumulatedEntityTicking)
+ ));
+ }
+ }
+
+ private void doHolderInfo(final CommandSender sender, final String[] args) {
+ List<org.bukkit.World> worlds;
+ if (args.length < 1 || args[0].equals("*")) {
+ worlds = Bukkit.getWorlds();
+ } else {
+ worlds = new ArrayList<>(args.length);
+ for (final String arg : args) {
+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg);
+ if (world == null) {
+ sender.sendMessage(text("World '" + arg + "' is invalid", RED));
+ return;
+ }
+ worlds.add(world);
+ }
+ }
+
+ int accumulatedTotal = 0;
+ int accumulatedCanUnload = 0;
+ int accumulatedNull = 0;
+ int accumulatedReadOnly = 0;
+ int accumulatedProtoChunk = 0;
+ int accumulatedFullChunk = 0;
+
+ for (final org.bukkit.World bukkitWorld : worlds) {
+ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle();
+
+ int total = 0;
+ int canUnload = 0;
+ int nullChunks = 0;
+ int readOnly = 0;
+ int protoChunk = 0;
+ int fullChunk = 0;
+
+ for (final NewChunkHolder holder : ((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders()) {
+ final NewChunkHolder.ChunkCompletion completion = holder.getLastChunkCompletion();
+ final ChunkAccess chunk = completion == null ? null : completion.chunk();
+
+ ++total;
+
+ if (chunk == null) {
+ ++nullChunks;
+ } else if (chunk instanceof ImposterProtoChunk) {
+ ++readOnly;
+ } else if (chunk instanceof ProtoChunk) {
+ ++protoChunk;
+ } else if (chunk instanceof LevelChunk) {
+ ++fullChunk;
+ }
+
+ if (holder.isSafeToUnload() == null) {
+ ++canUnload;
+ }
+ }
+
+ accumulatedTotal += total;
+ accumulatedCanUnload += canUnload;
+ accumulatedNull += nullChunks;
+ accumulatedReadOnly += readOnly;
+ accumulatedProtoChunk += protoChunk;
+ accumulatedFullChunk += fullChunk;
+
+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":")));
+ sender.sendMessage(text().color(DARK_AQUA).append(
+ text("Total: ", BLUE), text(total),
+ text(" Unloadable: ", BLUE), text(canUnload),
+ text(" Null: ", BLUE), text(nullChunks),
+ text(" ReadOnly: ", BLUE), text(readOnly),
+ text(" Proto: ", BLUE), text(protoChunk),
+ text(" Full: ", BLUE), text(fullChunk)
+ ));
+ }
+ if (worlds.size() > 1) {
+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA)));
+ sender.sendMessage(text().color(DARK_AQUA).append(
+ text("Total: ", BLUE), text(accumulatedTotal),
+ text(" Unloadable: ", BLUE), text(accumulatedCanUnload),
+ text(" Null: ", BLUE), text(accumulatedNull),
+ text(" ReadOnly: ", BLUE), text(accumulatedReadOnly),
+ text(" Proto: ", BLUE), text(accumulatedProtoChunk),
+ text(" Full: ", BLUE), text(accumulatedFullChunk)
+ ));
+ }
+ }
+
+ private void doDebug(final CommandSender sender, final String[] args) {
+ if (args.length < 1) {
+ sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED));
+ return;
+ }
+
+ final String debugType = args[0].toLowerCase(Locale.ROOT);
+ switch (debugType) {
+ case "chunks" -> {
+ if (args.length >= 2 && args[1].toLowerCase(Locale.ROOT).equals("help")) {
+ sender.sendMessage(text("Use /paper debug chunks to dump loaded chunk information to a file", RED));
+ break;
+ }
+ final File file = ChunkTaskScheduler.getChunkDebugFile();
+ sender.sendMessage(text("Writing chunk information dump to " + file, GREEN));
+ try {
+ JsonUtil.writeJson(ChunkTaskScheduler.debugAllWorlds(MinecraftServer.getServer()), file);
+ sender.sendMessage(text("Successfully written chunk information!", GREEN));
+ } catch (Throwable thr) {
+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
+ sender.sendMessage(text("Failed to dump chunk information, see console", RED));
+ }
+ }
+ // "help" & default
+ default -> sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED));
+ }
+ }
+
+}
diff --git a/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java
new file mode 100644
index 0000000000000000000000000000000000000000..85950a1aa732ab8c01ad28bec9e0de140e1a172e
--- /dev/null
+++ b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java
@@ -0,0 +1,116 @@
+package io.papermc.paper.command.subcommands;
+
+import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider;
+import io.papermc.paper.command.PaperSubcommand;
+import io.papermc.paper.util.MCUtil;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.level.ThreadedLevelLightEngine;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import org.bukkit.command.CommandSender;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+import java.text.DecimalFormat;
+
+import static net.kyori.adventure.text.Component.text;
+import static net.kyori.adventure.text.format.NamedTextColor.BLUE;
+import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA;
+import static net.kyori.adventure.text.format.NamedTextColor.RED;
+
+@DefaultQualifier(NonNull.class)
+public final class FixLightCommand implements PaperSubcommand {
+
+ private static final ThreadLocal<DecimalFormat> ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> {
+ return new DecimalFormat("#,##0.0");
+ });
+
+ @Override
+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
+ this.doFixLight(sender, args);
+ return true;
+ }
+
+ private void doFixLight(final CommandSender sender, final String[] args) {
+ if (!(sender instanceof Player)) {
+ sender.sendMessage(text("Only players can use this command", RED));
+ return;
+ }
+ @Nullable Runnable post = null;
+ int radius = 2;
+ if (args.length > 0) {
+ try {
+ final int parsed = Integer.parseInt(args[0]);
+ if (parsed < 0) {
+ sender.sendMessage(text("Radius cannot be negative!", RED));
+ return;
+ }
+ final int maxRadius = 32;
+ radius = Math.min(maxRadius, parsed);
+ if (radius != parsed) {
+ post = () -> sender.sendMessage(text("Radius '" + parsed + "' was not in the required range [0, " + maxRadius + "], it was lowered to the maximum (" + maxRadius + " chunks).", RED));
+ }
+ } catch (final Exception e) {
+ sender.sendMessage(text("'" + args[0] + "' is not a valid number.", RED));
+ return;
+ }
+ }
+
+ CraftPlayer player = (CraftPlayer) sender;
+ ServerPlayer handle = player.getHandle();
+ ServerLevel world = (ServerLevel) handle.level();
+ ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine();
+ this.starlightFixLight(handle, world, lightengine, radius, post);
+ }
+
+ private void starlightFixLight(
+ final ServerPlayer sender,
+ final ServerLevel world,
+ final ThreadedLevelLightEngine lightengine,
+ final int radius,
+ final @Nullable Runnable done
+ ) {
+ final long start = System.nanoTime();
+ final java.util.LinkedHashSet<ChunkPos> chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos
+
+ final int[] pending = new int[1];
+ for (java.util.Iterator<ChunkPos> iterator = chunks.iterator(); iterator.hasNext(); ) {
+ final ChunkPos chunkPos = iterator.next();
+
+ final @Nullable ChunkAccess chunk = (ChunkAccess) world.getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z);
+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) {
+ // cannot relight this chunk
+ iterator.remove();
+ continue;
+ }
+
+ ++pending[0];
+ }
+
+ final int[] relitChunks = new int[1];
+ ((StarLightLightingProvider)lightengine).starlight$serverRelightChunks(chunks,
+ (final ChunkPos chunkPos) -> {
+ ++relitChunks[0];
+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append(
+ text("Relit chunk ", BLUE), text(chunkPos.toString()),
+ text(", progress: ", BLUE), text(ONE_DECIMAL_PLACES.get().format(100.0 * (double) (relitChunks[0]) / (double) pending[0]) + "%")
+ ));
+ },
+ (final int totalRelit) -> {
+ final long end = System.nanoTime();
+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append(
+ text("Relit ", BLUE), text(totalRelit),
+ text(" chunks. Took ", BLUE), text(ONE_DECIMAL_PLACES.get().format(1.0e-6 * (end - start)) + "ms")
+ ));
+ if (done != null) {
+ done.run();
+ }
+ }
+ );
+ sender.getBukkitEntity().sendMessage(text().color(BLUE).append(text("Relighting "), text(pending[0], DARK_AQUA), text(" chunks")));
+ }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
index 9db431ddb43e94d5e8dc3875d8d9c6fa5a05bbea..214ea75502d4abf9ebbc99a3811e4d2f8465227e 100644
--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
@@ -30,6 +30,45 @@ public class GlobalConfiguration extends ConfigurationPart {
public static GlobalConfiguration get() {
return instance;
}
+
+ public ChunkLoadingBasic chunkLoadingBasic;
+
+ public class ChunkLoadingBasic extends ConfigurationPart {
+ @Comment("The maximum rate in chunks per second that the server will send to any individual player. Set to -1 to disable this limit.")
+ public double playerMaxChunkSendRate = 75.0;
+
+ @Comment(
+ "The maximum rate at which chunks will load for any individual player. " +
+ "Note that this setting also affects chunk generations, since a chunk load is always first issued to test if a" +
+ "chunk is already generated. Set to -1 to disable this limit."
+ )
+ public double playerMaxChunkLoadRate = 100.0;
+
+ @Comment("The maximum rate at which chunks will generate for any individual player. Set to -1 to disable this limit.")
+ public double playerMaxChunkGenerateRate = -1.0;
+ }
+
+ public ChunkLoadingAdvanced chunkLoadingAdvanced;
+
+ public class ChunkLoadingAdvanced extends ConfigurationPart {
+ @Comment(
+ "Set to true if the server will match the chunk send radius that clients have configured" +
+ "in their view distance settings if the client is less-than the server's send distance."
+ )
+ public boolean autoConfigSendDistance = true;
+
+ @Comment(
+ "Specifies the maximum amount of concurrent chunk loads that an individual player can have." +
+ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit."
+ )
+ public int playerMaxConcurrentChunkLoads = 0;
+
+ @Comment(
+ "Specifies the maximum amount of concurrent chunk generations that an individual player can have." +
+ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit."
+ )
+ public int playerMaxConcurrentChunkGenerates = 0;
+ }
static void set(GlobalConfiguration instance) {
GlobalConfiguration.instance = instance;
}
@@ -138,21 +177,6 @@ public class GlobalConfiguration extends ConfigurationPart {
public int incomingPacketThreshold = 300;
}
- public ChunkLoading chunkLoading;
-
- public class ChunkLoading extends ConfigurationPart {
- public int minLoadRadius = 2;
- public int maxConcurrentSends = 2;
- public boolean autoconfigSendDistance = true;
- public double targetPlayerChunkSendRate = 100.0;
- public double globalMaxChunkSendRate = -1.0;
- public boolean enableFrustumPriority = false;
- public double globalMaxChunkLoadRate = -1.0;
- public double playerMaxConcurrentLoads = 20.0;
- public double globalMaxConcurrentLoads = 500.0;
- public double playerMaxChunkLoadRate = -1.0;
- }
-
public UnsupportedSettings unsupportedSettings;
public class UnsupportedSettings extends ConfigurationPart {
@@ -211,7 +235,7 @@ public class GlobalConfiguration extends ConfigurationPart {
@PostProcess
private void postProcess() {
- //io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.init(this);
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.init(this);
}
}
diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
new file mode 100644
index 0000000000000000000000000000000000000000..8424cf9d4617b4732d44cc460d25b04481068989
--- /dev/null
+++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
@@ -0,0 +1,10 @@
+package io.papermc.paper.threadedregions;
+
+// placeholder class for Folia
+public class TickRegions {
+
+ public static int getRegionChunkShift() {
+ return ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ThreadedTicketLevelPropagator.SECTION_SHIFT;
+ }
+
+}
diff --git a/src/main/java/net/minecraft/core/Direction.java b/src/main/java/net/minecraft/core/Direction.java
index 03c45ee77276462818a6f774b5945b25924aa3f0..f15dd2ccb99ade10ac1e49b63e6f4080bd39b3c9 100644
--- a/src/main/java/net/minecraft/core/Direction.java
+++ b/src/main/java/net/minecraft/core/Direction.java
@@ -27,7 +27,7 @@ import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.joml.Vector4f;
-public enum Direction implements StringRepresentable {
+public enum Direction implements StringRepresentable, ca.spottedleaf.moonrise.patches.collisions.util.CollisionDirection { // Paper - optimise collisions
DOWN(0, 1, -1, "down", Direction.AxisDirection.NEGATIVE, Direction.Axis.Y, new Vec3i(0, -1, 0)),
UP(1, 0, -1, "up", Direction.AxisDirection.POSITIVE, Direction.Axis.Y, new Vec3i(0, 1, 0)),
NORTH(2, 3, 2, "north", Direction.AxisDirection.NEGATIVE, Direction.Axis.Z, new Vec3i(0, 0, -1)),
@@ -60,6 +60,46 @@ public enum Direction implements StringRepresentable {
private final int adjY;
private final int adjZ;
// Paper end - Perf: Inline shift direction fields
+ // Paper start - optimise collisions
+ private static final int RANDOM_OFFSET = 2017601568;
+ private Direction opposite;
+ private Quaternionf rotation;
+ private int id;
+ private int stepX;
+ private int stepY;
+ private int stepZ;
+
+ private Quaternionf getRotationUncached() {
+ switch ((Direction)(Object)this) {
+ case DOWN: {
+ return new Quaternionf().rotationX(3.1415927F);
+ }
+ case UP: {
+ return new Quaternionf();
+ }
+ case NORTH: {
+ return new Quaternionf().rotationXYZ(1.5707964F, 0.0F, 3.1415927F);
+ }
+ case SOUTH: {
+ return new Quaternionf().rotationX(1.5707964F);
+ }
+ case WEST: {
+ return new Quaternionf().rotationXYZ(1.5707964F, 0.0F, 1.5707964F);
+ }
+ case EAST: {
+ return new Quaternionf().rotationXYZ(1.5707964F, 0.0F, -1.5707964F);
+ }
+ default: {
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ @Override
+ public final int moonrise$uniqueId() {
+ return this.id;
+ }
+ // Paper end - optimise collisions
private Direction(
final int id,
@@ -134,14 +174,13 @@ public enum Direction implements StringRepresentable {
}
public Quaternionf getRotation() {
- return switch (this) {
- case DOWN -> new Quaternionf().rotationX((float) Math.PI);
- case UP -> new Quaternionf();
- case NORTH -> new Quaternionf().rotationXYZ((float) (Math.PI / 2), 0.0F, (float) Math.PI);
- case SOUTH -> new Quaternionf().rotationX((float) (Math.PI / 2));
- case WEST -> new Quaternionf().rotationXYZ((float) (Math.PI / 2), 0.0F, (float) (Math.PI / 2));
- case EAST -> new Quaternionf().rotationXYZ((float) (Math.PI / 2), 0.0F, (float) (-Math.PI / 2));
- };
+ // Paper start - optimise collisions
+ try {
+ return (Quaternionf)this.rotation.clone();
+ } catch (final CloneNotSupportedException ex) {
+ throw new InternalError(ex);
+ }
+ // Paper end - optimise collisions
}
public int get3DDataValue() {
@@ -165,7 +204,7 @@ public enum Direction implements StringRepresentable {
}
public Direction getOpposite() {
- return from3DDataValue(this.oppositeIndex);
+ return this.opposite; // Paper - optimise collisions
}
public Direction getClockWise(Direction.Axis axis) {
@@ -551,4 +590,17 @@ public enum Direction implements StringRepresentable {
return this.faces.length;
}
}
+
+ // Paper start - optimise collisions
+ static {
+ for (final Direction direction : VALUES) {
+ ((Direction)(Object)direction).opposite = from3DDataValue(((Direction)(Object)direction).oppositeIndex);
+ ((Direction)(Object)direction).rotation = ((Direction)(Object)direction).getRotationUncached();
+ ((Direction)(Object)direction).id = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(direction.ordinal() + RANDOM_OFFSET) + RANDOM_OFFSET);
+ ((Direction)(Object)direction).stepX = ((Direction)(Object)direction).normal.getX();
+ ((Direction)(Object)direction).stepY = ((Direction)(Object)direction).normal.getY();
+ ((Direction)(Object)direction).stepZ = ((Direction)(Object)direction).normal.getZ();
+ }
+ }
+ // Paper end - optimise collisions
}
diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java
index c33f85b570f159ab465b5a10a8044a81f2797f43..244a19ecd0234fa1d7a6ecfea20751595688605d 100644
--- a/src/main/java/net/minecraft/server/Main.java
+++ b/src/main/java/net/minecraft/server/Main.java
@@ -320,6 +320,7 @@ public class Main {
convertable_conversionsession.saveDataTag(iregistrycustom_dimension, savedata);
*/
+ Class.forName(net.minecraft.world.entity.npc.VillagerTrades.class.getName()); // Paper - load this sync so it won't fail later async
final DedicatedServer dedicatedserver = (DedicatedServer) MinecraftServer.spin((thread) -> {
DedicatedServer dedicatedserver1 = new DedicatedServer(optionset, worldLoader.get(), thread, convertable_conversionsession, resourcepackrepository, worldstem, dedicatedserversettings, DataFixers.getDataFixer(), services, LoggerChunkProgressListener::createFromGameruleRadius);
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index c97fb8eec949c693d4e822d8cf2e666a0858f20c..4a4ce372dd105d07f58d7d93d856cb3969969806 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -198,7 +198,7 @@ import org.bukkit.event.server.ServerLoadEvent;
import co.aikar.timings.MinecraftTimings; // Paper
-public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource, AutoCloseable {
+public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource, AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer { // Paper - rewrite chunk system
private static MinecraftServer SERVER; // Paper
public static final Logger LOGGER = LogUtils.getLogger();
@@ -322,7 +322,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
AtomicReference<S> atomicreference = new AtomicReference();
- Thread thread = new Thread(() -> {
+ Thread thread = new ca.spottedleaf.moonrise.common.util.TickThread(() -> { // Paper - rewrite chunk system
((MinecraftServer) atomicreference.get()).runServer();
}, "Server thread");
@@ -341,6 +341,77 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
return s0;
}
+ // Paper start - rewrite chunk system
+ private volatile Throwable chunkSystemCrash;
+
+ @Override
+ public final void moonrise$setChunkSystemCrash(final Throwable throwable) {
+ this.chunkSystemCrash = throwable;
+ }
+
+ private static final long CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME = 25L * 1000L; // 25us
+ private static final long MAX_CHUNK_EXEC_TIME = 1000L; // 1us
+ private static final long TASK_EXECUTION_FAILURE_BACKOFF = 5L * 1000L; // 5us
+
+ private long lastMidTickExecute;
+ private long lastMidTickExecuteFailure;
+
+ private boolean tickMidTickTasks() {
+ // give all worlds a fair chance at by targeting them all.
+ // if we execute too many tasks, that's fine - we have logic to correctly handle overuse of allocated time.
+ boolean executed = false;
+ for (final ServerLevel world : this.getAllLevels()) {
+ long currTime = System.nanoTime();
+ if (currTime - ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getLastMidTickFailure() <= TASK_EXECUTION_FAILURE_BACKOFF) {
+ continue;
+ }
+ if (!world.getChunkSource().pollTask()) {
+ // we need to back off if this fails
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$setLastMidTickFailure(currTime);
+ } else {
+ executed = true;
+ }
+ }
+
+ return executed;
+ }
+
+ @Override
+ public final void moonrise$executeMidTickTasks() {
+ final long startTime = System.nanoTime();
+ if ((startTime - this.lastMidTickExecute) <= CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME || (startTime - this.lastMidTickExecuteFailure) <= TASK_EXECUTION_FAILURE_BACKOFF) {
+ // it's shown to be bad to constantly hit the queue (chunk loads slow to a crawl), even if no tasks are executed.
+ // so, backoff to prevent this
+ return;
+ }
+
+ for (;;) {
+ final boolean moreTasks = this.tickMidTickTasks();
+ final long currTime = System.nanoTime();
+ final long diff = currTime - startTime;
+
+ if (!moreTasks || diff >= MAX_CHUNK_EXEC_TIME) {
+ if (!moreTasks) {
+ this.lastMidTickExecuteFailure = currTime;
+ }
+
+ // note: negative values reduce the time
+ long overuse = diff - MAX_CHUNK_EXEC_TIME;
+ if (overuse >= (10L * 1000L * 1000L)) { // 10ms
+ // make sure something like a GC or dumb plugin doesn't screw us over...
+ overuse = 10L * 1000L * 1000L; // 10ms
+ }
+
+ final double overuseCount = (double)overuse/(double)MAX_CHUNK_EXEC_TIME;
+ final long extraSleep = (long)Math.round(overuseCount*CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME);
+
+ this.lastMidTickExecute = currTime + extraSleep;
+ return;
+ }
+ }
+ }
+ // Paper end - rewrite chunk system
+
public MinecraftServer(OptionSet options, WorldLoader.DataLoadContext worldLoader, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, Proxy proxy, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) {
super("Server");
SERVER = this; // Paper - better singleton
@@ -657,7 +728,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.forceDifficulty();
for (ServerLevel worldserver : this.getAllLevels()) {
this.prepareLevels(worldserver.getChunkSource().chunkMap.progressListener, worldserver);
- worldserver.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API
+ // Paper - rewrite chunk system
this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(worldserver.getWorld()));
}
@@ -870,6 +941,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
public abstract boolean shouldRconBroadcast();
public boolean saveAllChunks(boolean suppressLogs, boolean flush, boolean force) {
+ // Paper start - add close param
+ return this.saveAllChunks(suppressLogs, flush, force, false);
+ }
+ public boolean saveAllChunks(boolean suppressLogs, boolean flush, boolean force, boolean close) {
+ // Paper end - add close param
boolean flag3 = false;
for (Iterator iterator = this.getAllLevels().iterator(); iterator.hasNext(); flag3 = true) {
@@ -879,7 +955,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
MinecraftServer.LOGGER.info("Saving chunks for level '{}'/{}", worldserver, worldserver.dimension().location());
}
- worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force);
+ worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force, close); // Paper - add close param
}
// CraftBukkit start - moved to WorldServer.save
@@ -980,7 +1056,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
}
- while (this.levels.values().stream().anyMatch((worldserver1) -> {
+ while (false && this.levels.values().stream().anyMatch((worldserver1) -> { // Paper - rewrite chunk system
return worldserver1.getChunkSource().chunkMap.hasWork();
})) {
this.nextTickTimeNanos = Util.getNanos() + TimeUtil.NANOSECONDS_PER_MILLISECOND;
@@ -997,19 +1073,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.waitUntilNextTick();
}
- this.saveAllChunks(false, true, false);
- iterator = this.getAllLevels().iterator();
-
- while (iterator.hasNext()) {
- worldserver = (ServerLevel) iterator.next();
- if (worldserver != null) {
- try {
- worldserver.close();
- } catch (IOException ioexception) {
- MinecraftServer.LOGGER.error("Exception closing the level", ioexception);
- }
- }
- }
+ this.saveAllChunks(false, true, true, true); // Paper - rewrite chunk system
this.isSaving = false;
this.resources.close();
@@ -1029,6 +1093,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
// Spigot end
+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.deinit(); // Paper - rewrite chunk system
}
public String getLocalIp() {
@@ -1203,6 +1268,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.tickServer(flag ? () -> {
return false;
} : this::haveTime);
+ // Paper start - rewrite chunk system
+ final Throwable crash = this.chunkSystemCrash;
+ if (crash != null) {
+ this.chunkSystemCrash = null;
+ throw new RuntimeException("Chunk system crash propagated to tick()", crash);
+ }
+ // Paper end - rewrite chunk system
this.profiler.popPush("nextTickWait");
this.mayHaveDelayedTasks = true;
this.delayedTasksMaxNextTickTimeNanos = Math.max(Util.getNanos() + i, this.nextTickTimeNanos);
@@ -1392,6 +1464,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
private boolean pollTaskInternal() {
if (super.pollTask()) {
+ this.moonrise$executeMidTickTasks(); // Paper - rewrite chunk system
return true;
} else {
boolean ret = false; // Paper - force execution of all worlds, do not just bias the first
@@ -1593,7 +1666,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// Paper start - Folia scheduler API
((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) Bukkit.getGlobalRegionScheduler()).tick();
getAllLevels().forEach(level -> {
- for (final Entity entity : level.getEntities().getAll()) {
+ for (final Entity entity : level.moonrise$getEntityLookup().getAllCopy()) { // Paper - rewrite chunk system
if (entity.isRemoved()) {
continue;
}
@@ -2655,6 +2728,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
+ // Paper start - rewrite chunk system
+ @Override
+ public boolean isSameThread() {
+ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThread();
+ }
+ // Paper end - rewrite chunk system
+
// CraftBukkit start
public boolean isDebugging() {
return false;
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
index 0761d5bc5f2813bb4a9f664ac7a05b9744d0a778..7d2896918ff5fed37e5de5a22c37b0c7f32634a8 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
@@ -476,7 +476,33 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
return world.dimension() == net.minecraft.world.level.Level.NETHER ? this.getProperties().allowNether : true;
}
+ private static final java.util.concurrent.atomic.AtomicInteger ASYNC_DEBUG_CHUNKS_COUNT = new java.util.concurrent.atomic.AtomicInteger(); // Paper - rewrite chunk system
+
public void handleConsoleInput(String command, CommandSourceStack commandSource) {
+ // Paper start - rewrite chunk system
+ if (command.equalsIgnoreCase("paper debug chunks --async")) {
+ LOGGER.info("Scheduling async debug chunks");
+ Runnable run = () -> {
+ LOGGER.info("Async debug chunks executing");
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(this, false);
+ CommandSender sender = MinecraftServer.getServer().console;
+ java.io.File file = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getChunkDebugFile();
+ sender.sendMessage(net.kyori.adventure.text.Component.text("Writing chunk information dump to " + file, net.kyori.adventure.text.format.NamedTextColor.GREEN));
+ try {
+ ca.spottedleaf.moonrise.common.util.JsonUtil.writeJson(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.debugAllWorlds(this), file);
+ sender.sendMessage(net.kyori.adventure.text.Component.text("Successfully written chunk information!", net.kyori.adventure.text.format.NamedTextColor.GREEN));
+ } catch (Throwable thr) {
+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
+ sender.sendMessage(net.kyori.adventure.text.Component.text("Failed to dump chunk information, see console", net.kyori.adventure.text.format.NamedTextColor.RED));
+ }
+ };
+ Thread t = new Thread(run);
+ t.setName("Async debug thread #" + ASYNC_DEBUG_CHUNKS_COUNT.getAndIncrement());
+ t.setDaemon(true);
+ t.start();
+ return;
+ }
+ // Paper end - rewrite chunk system
this.serverCommandQueue.add(new ConsoleInput(command, commandSource)); // Paper - Perf: use proper queue
}
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
index 9a009a688c02e990723917766c51e1c0e71e338d..4db96543e2072e47040bb25a9d97ea6a69c4a43d 100644
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -32,46 +32,125 @@ import net.minecraft.world.level.lighting.LevelLightEngine;
import net.minecraft.server.MinecraftServer;
// CraftBukkit end
-public class ChunkHolder extends GenerationChunkHolder {
+public class ChunkHolder extends GenerationChunkHolder implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder { // Paper - rewrite chunk system
public static final ChunkResult<LevelChunk> UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk");
private static final CompletableFuture<ChunkResult<LevelChunk>> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK);
private final LevelHeightAccessor levelHeightAccessor;
- private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
- private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
- private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
- public int oldTicketLevel;
- private int ticketLevel;
- private int queueLevel;
+ // Paper - rewrite chunk system
private boolean hasChangedSections;
private final ShortSet[] changedBlocksPerSection;
private final BitSet blockChangedLightSectionFilter;
private final BitSet skyChangedLightSectionFilter;
private final LevelLightEngine lightEngine;
- private final ChunkHolder.LevelChangeListener onLevelChange;
+ // Paper - rewrite chunk system
public final ChunkHolder.PlayerProvider playerProvider;
- private boolean wasAccessibleSinceLastSave;
- private CompletableFuture<?> pendingFullStateConfirmation;
- private CompletableFuture<?> sendSync;
- private CompletableFuture<?> saveSync;
+ // Paper - rewrite chunk system
+
+ // Paper start - rewrite chunk system
+ private ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder;
+
+ private static final ServerPlayer[] EMPTY_PLAYER_ARRAY = new ServerPlayer[0];
+ private final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> playersSentChunkTo = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_ARRAY);
+
+ private ChunkMap getChunkMap() {
+ return (ChunkMap)this.playerProvider;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder moonrise$getRealChunkHolder() {
+ return this.newChunkHolder;
+ }
+
+ @Override
+ public final void moonrise$setRealChunkHolder(final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder) {
+ this.newChunkHolder = newChunkHolder;
+ }
+
+ @Override
+ public final void moonrise$addReceivedChunk(final ServerPlayer player) {
+ if (!this.playersSentChunkTo.add(player)) {
+ throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player);
+ }
+ }
+
+ @Override
+ public final void moonrise$removeReceivedChunk(final ServerPlayer player) {
+ if (!this.playersSentChunkTo.remove(player)) {
+ throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player);
+ }
+ }
+
+ @Override
+ public final boolean moonrise$hasChunkBeenSent() {
+ return this.playersSentChunkTo.size() != 0;
+ }
+
+ @Override
+ public final boolean moonrise$hasChunkBeenSent(final ServerPlayer to) {
+ return this.playersSentChunkTo.contains(to);
+ }
+
+ @Override
+ public final List<ServerPlayer> moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge) {
+ final List<ServerPlayer> ret = new java.util.ArrayList<>();
+ final ServerPlayer[] raw = this.playersSentChunkTo.getRawDataUnchecked();
+ for (int i = 0, len = this.playersSentChunkTo.size(); i < len; ++i) {
+ final ServerPlayer player = raw[i];
+ if (onlyOnWatchDistanceEdge && !((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getPlayerChunkLoader().isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) {
+ continue;
+ }
+ ret.add(player);
+ }
+
+ return ret;
+ }
+
+ @Override
+ public final LevelChunk moonrise$getFullChunk() {
+ if (this.newChunkHolder.isFullChunkReady()) {
+ if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) {
+ return levelChunk;
+ } // else: race condition: chunk unload
+ }
+ return null;
+ }
+
+ private boolean isRadiusLoaded(final int radius) {
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getChunkTaskScheduler()
+ .chunkHolderManager;
+ final ChunkPos pos = this.pos;
+ final int chunkX = pos.x;
+ final int chunkZ = pos.z;
+ for (int dz = -radius; dz <= radius; ++dz) {
+ for (int dx = -radius; dx <= radius; ++dx) {
+ if ((dx | dz) == 0) {
+ continue;
+ }
+
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = manager.getChunkHolder(dx + chunkX, dz + chunkZ);
+
+ if (holder == null || !holder.isFullChunkReady()) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+ // Paper end - rewrite chunk system
public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) {
super(pos);
- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
+ // Paper - rewrite chunk system
this.blockChangedLightSectionFilter = new BitSet();
this.skyChangedLightSectionFilter = new BitSet();
- this.pendingFullStateConfirmation = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
- this.sendSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
- this.saveSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
+ // Paper - rewrite chunk system
this.levelHeightAccessor = world;
this.lightEngine = lightingProvider;
- this.onLevelChange = levelUpdateListener;
+ // Paper - rewrite chunk system
this.playerProvider = playersWatchingChunkProvider;
- this.oldTicketLevel = ChunkLevel.MAX_LEVEL + 1;
- this.ticketLevel = this.oldTicketLevel;
- this.queueLevel = this.oldTicketLevel;
+ // Paper - rewrite chunk system
this.setTicketLevel(level);
this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()];
}
@@ -79,7 +158,7 @@ public class ChunkHolder extends GenerationChunkHolder {
// CraftBukkit start
public LevelChunk getFullChunkNow() {
// Note: We use the oldTicketLevel for isLoaded checks.
- if (!ChunkLevel.fullStatus(this.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) return null;
+ if (!this.newChunkHolder.isFullChunkReady()) return null; // Paper - rewrite chunk system
return this.getFullChunkNowUnchecked();
}
@@ -89,63 +168,64 @@ public class ChunkHolder extends GenerationChunkHolder {
// CraftBukkit end
public CompletableFuture<ChunkResult<LevelChunk>> getTickingChunkFuture() {
- return this.tickingChunkFuture;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public CompletableFuture<ChunkResult<LevelChunk>> getEntityTickingChunkFuture() {
- return this.entityTickingChunkFuture;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public CompletableFuture<ChunkResult<LevelChunk>> getFullChunkFuture() {
- return this.fullChunkFuture;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Nullable
public final LevelChunk getTickingChunk() { // Paper - final for inline
- return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error
+ // Paper start - rewrite chunk system
+ if (this.newChunkHolder.isTickingReady()) {
+ if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) {
+ return levelChunk;
+ } // else: race condition: chunk unload
+ }
+ return null;
+ // Paper end - rewrite chunk system
}
@Nullable
public LevelChunk getChunkToSend() {
- return !this.sendSync.isDone() ? null : this.getTickingChunk();
+ // Paper start - rewrite chunk system
+ final LevelChunk ret = this.moonrise$getFullChunk();
+ if (ret != null && this.isRadiusLoaded(1)) {
+ return ret;
+ }
+ return null;
+ // Paper end - rewrite chunk system
}
public CompletableFuture<?> getSendSyncFuture() {
- return this.sendSync;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void addSendDependency(CompletableFuture<?> postProcessingFuture) {
- if (this.sendSync.isDone()) {
- this.sendSync = postProcessingFuture;
- } else {
- this.sendSync = this.sendSync.thenCombine(postProcessingFuture, (object, object1) -> {
- return null;
- });
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public CompletableFuture<?> getSaveSyncFuture() {
- return this.saveSync;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public boolean isReadyForSaving() {
- return this.getGenerationRefCount() == 0 && this.saveSync.isDone();
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void addSaveDependency(CompletableFuture<?> savingFuture) {
- if (this.saveSync.isDone()) {
- this.saveSync = savingFuture;
- } else {
- this.saveSync = this.saveSync.thenCombine(savingFuture, (object, object1) -> {
- return null;
- });
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void blockChanged(BlockPos pos) {
- LevelChunk chunk = this.getTickingChunk();
+ LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system
if (chunk != null) {
int i = this.levelHeightAccessor.getSectionIndex(pos.getY());
@@ -165,7 +245,7 @@ public class ChunkHolder extends GenerationChunkHolder {
if (ichunkaccess != null) {
ichunkaccess.setUnsaved(true);
- LevelChunk chunk = this.getTickingChunk();
+ LevelChunk chunk = this.getChunkToSend(); // Paper - rewrite chunk system
if (chunk != null) {
int j = this.lightEngine.getMinLightSection();
@@ -191,7 +271,7 @@ public class ChunkHolder extends GenerationChunkHolder {
List list;
if (!this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) {
- list = this.playerProvider.getPlayers(this.pos, true);
+ list = this.moonrise$getPlayers(true); // Paper - rewrite chunk system
if (!list.isEmpty()) {
ClientboundLightUpdatePacket packetplayoutlightupdate = new ClientboundLightUpdatePacket(chunk.getPos(), this.lightEngine, this.skyChangedLightSectionFilter, this.blockChangedLightSectionFilter);
@@ -203,7 +283,7 @@ public class ChunkHolder extends GenerationChunkHolder {
}
if (this.hasChangedSections) {
- list = this.playerProvider.getPlayers(this.pos, false);
+ list = this.moonrise$getPlayers(false); // Paper - rewrite chunk system
for (int i = 0; i < this.changedBlocksPerSection.length; ++i) {
ShortSet shortset = this.changedBlocksPerSection[i];
@@ -269,193 +349,40 @@ public class ChunkHolder extends GenerationChunkHolder {
@Override
public int getTicketLevel() {
- return this.ticketLevel;
+ return this.newChunkHolder.getTicketLevel(); // Paper - rewrite chunk system
}
@Override
public int getQueueLevel() {
- return this.queueLevel;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void setQueueLevel(int level) {
- this.queueLevel = level;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void setTicketLevel(int level) {
- this.ticketLevel = level;
+ // Paper - rewrite chunk system
}
private void scheduleFullChunkPromotion(ChunkMap chunkLoadingManager, CompletableFuture<ChunkResult<LevelChunk>> chunkFuture, Executor executor, FullChunkStatus target) {
- this.pendingFullStateConfirmation.cancel(false);
- CompletableFuture<Void> completablefuture1 = new CompletableFuture();
-
- completablefuture1.thenRunAsync(() -> {
- chunkLoadingManager.onFullChunkStatusChange(this.pos, target);
- }, executor);
- this.pendingFullStateConfirmation = completablefuture1;
- chunkFuture.thenAccept((chunkresult) -> {
- chunkresult.ifSuccess((chunk) -> {
- completablefuture1.complete(null); // CraftBukkit - decompile error
- });
- });
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void demoteFullChunk(ChunkMap chunkLoadingManager, FullChunkStatus target) {
- this.pendingFullStateConfirmation.cancel(false);
- chunkLoadingManager.onFullChunkStatusChange(this.pos, target);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
protected void updateFutures(ChunkMap chunkLoadingManager, Executor executor) {
- FullChunkStatus fullchunkstatus = ChunkLevel.fullStatus(this.oldTicketLevel);
- FullChunkStatus fullchunkstatus1 = ChunkLevel.fullStatus(this.ticketLevel);
- boolean flag = fullchunkstatus.isOrAfter(FullChunkStatus.FULL);
- boolean flag1 = fullchunkstatus1.isOrAfter(FullChunkStatus.FULL);
- // CraftBukkit start
- // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins.
- if (flag && !flag1) {
- this.getFullChunkFuture().thenAccept((either) -> {
- LevelChunk chunk = (LevelChunk) either.orElse(null);
- if (chunk != null) {
- chunkLoadingManager.callbackExecutor.execute(() -> {
- // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick
- // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag.
- // These actions may however happen deferred, so we manually set the needsSaving flag already here.
- chunk.setUnsaved(true);
- chunk.unloadCallback();
- });
- }
- }).exceptionally((throwable) -> {
- // ensure exceptions are printed, by default this is not the case
- MinecraftServer.LOGGER.error("Failed to schedule unload callback for chunk " + ChunkHolder.this.pos, throwable);
- return null;
- });
-
- // Run callback right away if the future was already done
- chunkLoadingManager.callbackExecutor.run();
- }
- // CraftBukkit end
-
- this.wasAccessibleSinceLastSave |= flag1;
- if (!flag && flag1) {
- int expectCreateCount = ++this.fullChunkCreateCount; // Paper
- this.fullChunkFuture = chunkLoadingManager.prepareAccessibleChunk(this);
- this.scheduleFullChunkPromotion(chunkLoadingManager, this.fullChunkFuture, executor, FullChunkStatus.FULL);
- // Paper start - cache ticking ready status
- this.fullChunkFuture.thenAccept(chunkResult -> {
- chunkResult.ifSuccess(chunk -> {
- if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
- ChunkHolder.this.isFullChunkReady = true;
- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkBorder(chunk, this);
- }
- });
- });
- // Paper end - cache ticking ready status
- this.addSaveDependency(this.fullChunkFuture);
- }
-
- if (flag && !flag1) {
- // Paper start
- if (this.isFullChunkReady) {
- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
- }
- // Paper end
- this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
- }
-
- boolean flag2 = fullchunkstatus.isOrAfter(FullChunkStatus.BLOCK_TICKING);
- boolean flag3 = fullchunkstatus1.isOrAfter(FullChunkStatus.BLOCK_TICKING);
-
- if (!flag2 && flag3) {
- this.tickingChunkFuture = chunkLoadingManager.prepareTickingChunk(this);
- this.scheduleFullChunkPromotion(chunkLoadingManager, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING);
- // Paper start - cache ticking ready status
- this.tickingChunkFuture.thenAccept(chunkResult -> {
- chunkResult.ifSuccess(chunk -> {
- // note: Here is a very good place to add callbacks to logic waiting on this.
- ChunkHolder.this.isTickingReady = true;
- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkTicking(chunk, this);
- });
- });
- // Paper end
- this.addSaveDependency(this.tickingChunkFuture);
- }
-
- if (flag2 && !flag3) {
- // Paper start
- if (this.isTickingReady) {
- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
- }
- // Paper end
- this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage
- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
- }
-
- boolean flag4 = fullchunkstatus.isOrAfter(FullChunkStatus.ENTITY_TICKING);
- boolean flag5 = fullchunkstatus1.isOrAfter(FullChunkStatus.ENTITY_TICKING);
-
- if (!flag4 && flag5) {
- if (this.entityTickingChunkFuture != ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE) {
- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException());
- }
-
- this.entityTickingChunkFuture = chunkLoadingManager.prepareEntityTickingChunk(this);
- this.scheduleFullChunkPromotion(chunkLoadingManager, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING);
- // Paper start - cache ticking ready status
- this.entityTickingChunkFuture.thenAccept(chunkResult -> {
- chunkResult.ifSuccess(chunk -> {
- ChunkHolder.this.isEntityTickingReady = true;
- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkEntityTicking(chunk, this);
- });
- });
- // Paper end
- this.addSaveDependency(this.entityTickingChunkFuture);
- }
-
- if (flag4 && !flag5) {
- // Paper start
- if (this.isEntityTickingReady) {
- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
- }
- // Paper end
- this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage
- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
- }
-
- if (!fullchunkstatus1.isOrAfter(fullchunkstatus)) {
- this.demoteFullChunk(chunkLoadingManager, fullchunkstatus1);
- }
-
- this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel);
- this.oldTicketLevel = this.ticketLevel;
- // CraftBukkit start
- // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins.
- if (!fullchunkstatus.isOrAfter(FullChunkStatus.FULL) && fullchunkstatus1.isOrAfter(FullChunkStatus.FULL)) {
- this.getFullChunkFuture().thenAccept((either) -> {
- LevelChunk chunk = (LevelChunk) either.orElse(null);
- if (chunk != null) {
- chunkLoadingManager.callbackExecutor.execute(() -> {
- chunk.loadCallback();
- });
- }
- }).exceptionally((throwable) -> {
- // ensure exceptions are printed, by default this is not the case
- MinecraftServer.LOGGER.error("Failed to schedule load callback for chunk " + ChunkHolder.this.pos, throwable);
- return null;
- });
-
- // Run callback right away if the future was already done
- chunkLoadingManager.callbackExecutor.run();
- }
- // CraftBukkit end
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public boolean wasAccessibleSinceLastSave() {
- return this.wasAccessibleSinceLastSave;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void refreshAccessibility() {
- this.wasAccessibleSinceLastSave = ChunkLevel.fullStatus(this.ticketLevel).isOrAfter(FullChunkStatus.FULL);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@FunctionalInterface
diff --git a/src/main/java/net/minecraft/server/level/ChunkLevel.java b/src/main/java/net/minecraft/server/level/ChunkLevel.java
index d9ad32acdf46a43a649334a3b736aeb7b3af21d1..fae17a075d7efaf24d916877dd5968eb9652bb66 100644
--- a/src/main/java/net/minecraft/server/level/ChunkLevel.java
+++ b/src/main/java/net/minecraft/server/level/ChunkLevel.java
@@ -7,9 +7,9 @@ import net.minecraft.world.level.chunk.status.ChunkStep;
import org.jetbrains.annotations.Contract;
public class ChunkLevel {
- private static final int FULL_CHUNK_LEVEL = 33;
- private static final int BLOCK_TICKING_LEVEL = 32;
- private static final int ENTITY_TICKING_LEVEL = 31;
+ public static final int FULL_CHUNK_LEVEL = 33;
+ public static final int BLOCK_TICKING_LEVEL = 32;
+ public static final int ENTITY_TICKING_LEVEL = 31;
private static final ChunkStep FULL_CHUNK_STEP = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
public static final int RADIUS_AROUND_FULL_CHUNK = FULL_CHUNK_STEP.accumulatedDependencies().getRadius();
public static final int MAX_LEVEL = 33 + RADIUS_AROUND_FULL_CHUNK;
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index 6e084fd84c76c32319c6a321650ca2bb8332c704..f69a769b45be94e990976bb2b3d51ea0cd26a052 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -122,10 +122,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
public static final int MIN_VIEW_DISTANCE = 2;
public static final int MAX_VIEW_DISTANCE = 32;
public static final int FORCED_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING);
- public final Long2ObjectLinkedOpenHashMap<ChunkHolder> updatingChunkMap = new Long2ObjectLinkedOpenHashMap();
- public volatile Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunkMap;
- private final Long2ObjectLinkedOpenHashMap<ChunkHolder> pendingUnloads;
- private final List<ChunkGenerationTask> pendingGenerationTasks;
+ // Paper - rewrite chunk system
public final ServerLevel level;
private final ThreadedLevelLightEngine lightEngine;
private final BlockableEventLoop<Runnable> mainThreadExecutor;
@@ -135,21 +132,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
private final PoiManager poiManager;
public final LongSet toDrop;
private boolean modified;
- private final ChunkTaskPriorityQueueSorter queueSorter;
- private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> worldgenMailbox;
- private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> mainThreadMailbox;
+ // Paper - rewrite chunk system
public final ChunkProgressListener progressListener;
private final ChunkStatusUpdateListener chunkStatusListener;
public final ChunkMap.ChunkDistanceManager distanceManager;
- private final AtomicInteger tickingGenerated;
+ public final AtomicInteger tickingGenerated; // Paper - public
private final String storageName;
private final PlayerMap playerMap;
public final Int2ObjectMap<ChunkMap.TrackedEntity> entityMap;
private final Long2ByteMap chunkTypeCache;
private final Long2LongMap chunkSaveCooldowns;
- private final Queue<Runnable> unloadQueue;
+ // Paper - rewrite chunk system
public int serverViewDistance;
- private final WorldGenContext worldGenContext;
+ public final WorldGenContext worldGenContext; // Paper - public
// CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback()
public final CallbackExecutor callbackExecutor = new CallbackExecutor();
@@ -174,22 +169,20 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
// Paper start
public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
- return this.pendingUnloads.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ return null; // Paper - rewrite chunk system
}
// Paper end
public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
- this.visibleChunkMap = this.updatingChunkMap.clone();
- this.pendingUnloads = new Long2ObjectLinkedOpenHashMap();
- this.pendingGenerationTasks = new ArrayList();
+ // Paper - rewrite chunk system
this.toDrop = new LongOpenHashSet();
this.tickingGenerated = new AtomicInteger();
this.playerMap = new PlayerMap();
this.entityMap = new Int2ObjectOpenHashMap();
this.chunkTypeCache = new Long2ByteOpenHashMap();
this.chunkSaveCooldowns = new Long2LongOpenHashMap();
- this.unloadQueue = Queues.newConcurrentLinkedQueue();
+ // Paper - rewrite chunk system
Path path = session.getDimensionPath(world.dimension());
this.storageName = path.getFileName().toString();
@@ -220,15 +213,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.chunkStatusListener = chunkStatusChangeListener;
ProcessorMailbox<Runnable> threadedmailbox1 = ProcessorMailbox.create(executor, "light");
- this.queueSorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(threadedmailbox, mailbox, threadedmailbox1), executor, Integer.MAX_VALUE);
- this.worldgenMailbox = this.queueSorter.getProcessor(threadedmailbox, false);
- this.mainThreadMailbox = this.queueSorter.getProcessor(mailbox, false);
- this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), threadedmailbox1, this.queueSorter.getProcessor(threadedmailbox1, false));
+ // Paper - rewrite chunk system
+ this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), threadedmailbox1, null); // Paper - rewrite chunk system
this.distanceManager = new ChunkMap.ChunkDistanceManager(executor, mainThreadExecutor);
this.overworldDataStorage = persistentStateManagerFactory;
this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world.getServer(), world);
this.setServerViewDistance(viewDistance);
- this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, this.mainThreadMailbox);
+ this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, null); // Paper - rewrite chunk system
}
// Paper start
@@ -259,23 +250,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
boolean isChunkTracked(ServerPlayer player, int chunkX, int chunkZ) {
- return player.getChunkTrackingView().contains(chunkX, chunkZ) && !player.connection.chunkSender.isPending(ChunkPos.asLong(chunkX, chunkZ));
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ); // Paper - rewrite chunk system
}
private boolean isChunkOnTrackedBorder(ServerPlayer player, int chunkX, int chunkZ) {
- if (!this.isChunkTracked(player, chunkX, chunkZ)) {
- return false;
- } else {
- for (int k = -1; k <= 1; ++k) {
- for (int l = -1; l <= 1; ++l) {
- if ((k != 0 || l != 0) && !this.isChunkTracked(player, chunkX + k, chunkZ + l)) {
- return true;
- }
- }
- }
-
- return false;
- }
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ, true); // Paper - rewrite chunk system
}
protected ThreadedLevelLightEngine getLightEngine() {
@@ -284,20 +263,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
@Nullable
protected ChunkHolder getUpdatingChunkIfPresent(long pos) {
- return (ChunkHolder) this.updatingChunkMap.get(pos);
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos);
+ return holder == null ? null : holder.vanillaChunkHolder;
+ // Paper end - rewrite chunk system
}
@Nullable
public ChunkHolder getVisibleChunkIfPresent(long pos) {
- return (ChunkHolder) this.visibleChunkMap.get(pos);
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos);
+ return holder == null ? null : holder.vanillaChunkHolder;
+ // Paper end - rewrite chunk system
}
protected IntSupplier getChunkQueueLevel(long pos) {
- return () -> {
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
-
- return playerchunk == null ? ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1 : Math.min(playerchunk.getQueueLevel(), ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1);
- };
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public String getChunkDebugData(ChunkPos chunkPos) {
@@ -326,55 +307,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
private CompletableFuture<ChunkResult<List<ChunkAccess>>> getChunkRangeFuture(ChunkHolder centerChunk, int margin, IntFunction<ChunkStatus> distanceToStatus) {
- if (margin == 0) {
- ChunkStatus chunkstatus = (ChunkStatus) distanceToStatus.apply(0);
-
- return centerChunk.scheduleChunkGenerationTask(chunkstatus, this).thenApply((chunkresult) -> {
- return chunkresult.map(List::of);
- });
- } else {
- List<CompletableFuture<ChunkResult<ChunkAccess>>> list = new ArrayList();
- ChunkPos chunkcoordintpair = centerChunk.getPos();
-
- for (int j = -margin; j <= margin; ++j) {
- for (int k = -margin; k <= margin; ++k) {
- int l = Math.max(Math.abs(k), Math.abs(j));
- long i1 = ChunkPos.asLong(chunkcoordintpair.x + k, chunkcoordintpair.z + j);
- ChunkHolder playerchunk1 = this.getUpdatingChunkIfPresent(i1);
-
- if (playerchunk1 == null) {
- return ChunkMap.UNLOADED_CHUNK_LIST_FUTURE;
- }
-
- ChunkStatus chunkstatus1 = (ChunkStatus) distanceToStatus.apply(l);
-
- list.add(playerchunk1.scheduleChunkGenerationTask(chunkstatus1, this));
- }
- }
-
- return Util.sequence(list).thenApply((list1) -> {
- List<ChunkAccess> list2 = Lists.newArrayList();
- Iterator iterator = list1.iterator();
-
- while (iterator.hasNext()) {
- ChunkResult<ChunkAccess> chunkresult = (ChunkResult) iterator.next();
-
- if (chunkresult == null) {
- throw this.debugFuturesAndCreateReportedException(new IllegalStateException("At least one of the chunk futures were null"), "n/a");
- }
-
- ChunkAccess ichunkaccess = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error
-
- if (ichunkaccess == null) {
- return ChunkMap.UNLOADED_CHUNK_LIST_RESULT;
- }
-
- list2.add(ichunkaccess);
- }
-
- return ChunkResult.of(list2);
- });
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public ReportedException debugFuturesAndCreateReportedException(IllegalStateException exception, String details) {
@@ -404,90 +337,23 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public CompletableFuture<ChunkResult<LevelChunk>> prepareEntityTickingChunk(ChunkHolder holder) {
- return this.getChunkRangeFuture(holder, 2, (i) -> {
- return ChunkStatus.FULL;
- }).thenApplyAsync((chunkresult) -> {
- return chunkresult.map((list) -> {
- return (LevelChunk) list.get(list.size() / 2);
- });
- }, this.mainThreadExecutor);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Nullable
ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k) {
- if (!ChunkLevel.isLoaded(k) && !ChunkLevel.isLoaded(level)) {
- return holder;
- } else {
- if (holder != null) {
- holder.setTicketLevel(level);
- }
-
- if (holder != null) {
- if (!ChunkLevel.isLoaded(level)) {
- this.toDrop.add(pos);
- } else {
- this.toDrop.remove(pos);
- }
- }
-
- if (ChunkLevel.isLoaded(level) && holder == null) {
- holder = (ChunkHolder) this.pendingUnloads.remove(pos);
- if (holder != null) {
- holder.setTicketLevel(level);
- } else {
- holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this);
- // Paper start
- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderCreate(this.level, holder);
- // Paper end
- }
-
- this.updatingChunkMap.put(pos, holder);
- this.modified = true;
- }
-
- return holder;
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Override
public void close() throws IOException {
- try {
- this.queueSorter.close();
- this.poiManager.close();
- } finally {
- super.close();
- }
-
+ throw new UnsupportedOperationException("Use ServerChunkCache#close"); // Paper - rewrite chunk system
}
protected void saveAllChunks(boolean flush) {
- if (flush) {
- List<ChunkHolder> list = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper
- MutableBoolean mutableboolean = new MutableBoolean();
-
- do {
- mutableboolean.setFalse();
- list.stream().map((playerchunk) -> {
- BlockableEventLoop iasynctaskhandler = this.mainThreadExecutor;
-
- Objects.requireNonNull(playerchunk);
- iasynctaskhandler.managedBlock(playerchunk::isReadyForSaving);
- return playerchunk.getLatestChunk();
- }).filter((ichunkaccess) -> {
- return ichunkaccess instanceof ImposterProtoChunk || ichunkaccess instanceof LevelChunk;
- }).filter(this::save).forEach((ichunkaccess) -> {
- mutableboolean.setTrue();
- });
- } while (mutableboolean.isTrue());
-
- this.processUnloads(() -> {
- return true;
- });
- this.flushWorker();
- } else {
- ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded);
- }
-
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.saveAllChunks(
+ flush, false, false
+ );
}
protected void tick(BooleanSupplier shouldKeepTicking) {
@@ -504,133 +370,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public boolean hasWork() {
- return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || ca.spottedleaf.moonrise.common.util.ChunkSystem.hasAnyChunkHolders(this.level) || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void processUnloads(BooleanSupplier shouldKeepTicking) {
- LongIterator longiterator = this.toDrop.iterator();
- int i = 0;
-
- while (longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000)) {
- long j = longiterator.nextLong();
- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.get(j);
-
- if (playerchunk != null) {
- if (playerchunk.getGenerationRefCount() != 0) {
- continue;
- }
-
- this.updatingChunkMap.remove(j);
- this.pendingUnloads.put(j, playerchunk);
- this.modified = true;
- ++i;
- this.scheduleUnload(j, playerchunk);
- }
-
- longiterator.remove();
- }
-
- int k = Math.max(0, this.unloadQueue.size() - 2000);
-
- Runnable runnable;
-
- while ((shouldKeepTicking.getAsBoolean() || k > 0) && (runnable = (Runnable) this.unloadQueue.poll()) != null) {
- --k;
- runnable.run();
- }
-
- int l = 0;
- Iterator<ChunkHolder> objectiterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
-
- while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) {
- if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) {
- ++l;
- }
- }
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.processUnloads(); // Paper - rewrite chunk system
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.autoSave(); // Paper - rewrite chunk system
}
private void scheduleUnload(long pos, ChunkHolder holder) {
- CompletableFuture completablefuture = holder.getSaveSyncFuture();
- Runnable runnable = () -> {
- if (!holder.isReadyForSaving()) {
- this.scheduleUnload(pos, holder);
- } else {
- ChunkAccess ichunkaccess = holder.getLatestChunk();
-
- // Paper start
- boolean removed;
- if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) {
- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, holder);
- // Paper end
- LevelChunk chunk;
-
- if (ichunkaccess instanceof LevelChunk) {
- chunk = (LevelChunk) ichunkaccess;
- chunk.setLoaded(false);
- }
-
- this.save(ichunkaccess);
- if (ichunkaccess instanceof LevelChunk) {
- chunk = (LevelChunk) ichunkaccess;
- this.level.unload(chunk);
- }
-
- this.lightEngine.updateChunkStatus(ichunkaccess.getPos());
- this.lightEngine.tryScheduleUpdate();
- this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null);
- this.chunkSaveCooldowns.remove(ichunkaccess.getPos().toLong());
- } else if (removed) { // Paper start
- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, holder);
- } // Paper end
-
- }
- };
- Queue queue = this.unloadQueue;
-
- Objects.requireNonNull(this.unloadQueue);
- completablefuture.thenRunAsync(runnable, queue::add).whenComplete((ovoid, throwable) -> {
- if (throwable != null) {
- ChunkMap.LOGGER.error("Failed to save chunk {}", holder.getPos(), throwable);
- }
-
- });
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
protected boolean promoteChunkMap() {
- if (!this.modified) {
- return false;
- } else {
- this.visibleChunkMap = this.updatingChunkMap.clone();
- this.modified = false;
- return true;
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private CompletableFuture<ChunkAccess> scheduleChunkLoad(ChunkPos pos) {
- return this.readChunk(pos).thenApply((optional) -> {
- return optional.filter((nbttagcompound) -> {
- boolean flag = ChunkMap.isChunkDataValid(nbttagcompound);
-
- if (!flag) {
- ChunkMap.LOGGER.error("Chunk file at {} is missing level data, skipping", pos);
- }
-
- return flag;
- });
- }).thenApplyAsync((optional) -> {
- this.level.getProfiler().incrementCounter("chunkLoad");
- if (optional.isPresent()) {
- ProtoChunk protochunk = ChunkSerializer.read(this.level, this.poiManager, this.storageInfo(), pos, (CompoundTag) optional.get());
-
- this.markPosition(pos, protochunk.getPersistedStatus().getChunkType());
- return protochunk;
- } else {
- return this.createEmptyChunk(pos);
- }
- }, this.mainThreadExecutor).exceptionallyAsync((throwable) -> {
- return this.handleChunkLoadFailure(throwable, pos);
- }, this.mainThreadExecutor);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private static boolean isChunkDataValid(CompoundTag nbt) {
@@ -690,137 +448,44 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
@Override
public GenerationChunkHolder acquireGeneration(long pos) {
- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.get(pos);
-
- playerchunk.increaseGenerationRefCount();
- return playerchunk;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Override
public void releaseGeneration(GenerationChunkHolder chunkHolder) {
- chunkHolder.decreaseGenerationRefCount();
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Override
public CompletableFuture<ChunkAccess> applyStep(GenerationChunkHolder chunkHolder, ChunkStep step, StaticCache2D<GenerationChunkHolder> chunks) {
- ChunkPos chunkcoordintpair = chunkHolder.getPos();
-
- if (step.targetStatus() == ChunkStatus.EMPTY) {
- return this.scheduleChunkLoad(chunkcoordintpair);
- } else {
- try {
- GenerationChunkHolder generationchunkholder1 = (GenerationChunkHolder) chunks.get(chunkcoordintpair.x, chunkcoordintpair.z);
- ChunkAccess ichunkaccess = generationchunkholder1.getChunkIfPresentUnchecked(step.targetStatus().getParent());
-
- if (ichunkaccess == null) {
- throw new IllegalStateException("Parent chunk missing");
- } else {
- CompletableFuture<ChunkAccess> completablefuture = step.apply(this.worldGenContext, chunks, ichunkaccess);
-
- this.progressListener.onStatusChange(chunkcoordintpair, step.targetStatus());
- return completablefuture;
- }
- } catch (Exception exception) {
- exception.getStackTrace();
- CrashReport crashreport = CrashReport.forThrowable(exception, "Exception generating new chunk");
- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk to be generated");
-
- crashreportsystemdetails.setDetail("Status being generated", () -> {
- return step.targetStatus().getName();
- });
- crashreportsystemdetails.setDetail("Location", (Object) String.format(Locale.ROOT, "%d,%d", chunkcoordintpair.x, chunkcoordintpair.z));
- crashreportsystemdetails.setDetail("Position hash", (Object) ChunkPos.asLong(chunkcoordintpair.x, chunkcoordintpair.z));
- crashreportsystemdetails.setDetail("Generator", (Object) this.generator());
- this.mainThreadExecutor.execute(() -> {
- throw new ReportedException(crashreport);
- });
- throw new ReportedException(crashreport);
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Override
public ChunkGenerationTask scheduleGenerationTask(ChunkStatus requestedStatus, ChunkPos pos) {
- ChunkGenerationTask chunkgenerationtask = ChunkGenerationTask.create(this, requestedStatus, pos);
-
- this.pendingGenerationTasks.add(chunkgenerationtask);
- return chunkgenerationtask;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void runGenerationTask(ChunkGenerationTask chunkLoader) {
- this.worldgenMailbox.tell(ChunkTaskPriorityQueueSorter.message(chunkLoader.getCenter(), () -> {
- CompletableFuture<?> completablefuture = chunkLoader.runUntilWait();
-
- if (completablefuture != null) {
- completablefuture.thenRun(() -> {
- this.runGenerationTask(chunkLoader);
- });
- }
- }));
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Override
public void runGenerationTasks() {
- this.pendingGenerationTasks.forEach(this::runGenerationTask);
- this.pendingGenerationTasks.clear();
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public CompletableFuture<ChunkResult<LevelChunk>> prepareTickingChunk(ChunkHolder holder) {
- CompletableFuture<ChunkResult<List<ChunkAccess>>> completablefuture = this.getChunkRangeFuture(holder, 1, (i) -> {
- return ChunkStatus.FULL;
- });
- CompletableFuture<ChunkResult<LevelChunk>> completablefuture1 = completablefuture.thenApplyAsync((chunkresult) -> {
- return chunkresult.map((list) -> {
- return (LevelChunk) list.get(list.size() / 2);
- });
- }, (runnable) -> {
- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable));
- }).thenApplyAsync((chunkresult) -> {
- return chunkresult.ifSuccess((chunk) -> {
- chunk.postProcessGeneration();
- this.level.startTickingChunk(chunk);
- CompletableFuture<?> completablefuture2 = holder.getSendSyncFuture();
-
- if (completablefuture2.isDone()) {
- this.onChunkReadyToSend(chunk);
- } else {
- completablefuture2.thenAcceptAsync((object) -> {
- this.onChunkReadyToSend(chunk);
- }, this.mainThreadExecutor);
- }
-
- });
- }, this.mainThreadExecutor);
-
- completablefuture1.handle((chunkresult, throwable) -> {
- this.tickingGenerated.getAndIncrement();
- return null;
- });
- return completablefuture1;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void onChunkReadyToSend(LevelChunk chunk) {
- ChunkPos chunkcoordintpair = chunk.getPos();
- Iterator iterator = this.playerMap.getAllPlayers().iterator();
-
- while (iterator.hasNext()) {
- ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-
- if (entityplayer.getChunkTrackingView().contains(chunkcoordintpair)) {
- ChunkMap.markChunkPendingToSend(entityplayer, chunk);
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public CompletableFuture<ChunkResult<LevelChunk>> prepareAccessibleChunk(ChunkHolder holder) {
- return this.getChunkRangeFuture(holder, 1, ChunkLevel::getStatusAroundFullChunk).thenApplyAsync((chunkresult) -> {
- return chunkresult.map((list) -> {
- return (LevelChunk) list.get(list.size() / 2);
- });
- }, (runnable) -> {
- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable));
- });
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public int getTickingGenerated() {
@@ -828,135 +493,84 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
private boolean saveChunkIfNeeded(ChunkHolder chunkHolder) {
- if (chunkHolder.wasAccessibleSinceLastSave() && chunkHolder.isReadyForSaving()) {
- ChunkAccess ichunkaccess = chunkHolder.getLatestChunk();
-
- if (!(ichunkaccess instanceof ImposterProtoChunk) && !(ichunkaccess instanceof LevelChunk)) {
- return false;
- } else {
- long i = ichunkaccess.getPos().toLong();
- long j = this.chunkSaveCooldowns.getOrDefault(i, -1L);
- long k = System.currentTimeMillis();
-
- if (k < j) {
- return false;
- } else {
- boolean flag = this.save(ichunkaccess);
-
- chunkHolder.refreshAccessibility();
- if (flag) {
- this.chunkSaveCooldowns.put(i, k + 10000L);
- }
-
- return flag;
- }
- }
- } else {
- return false;
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public boolean save(ChunkAccess chunk) {
- this.poiManager.flush(chunk.getPos());
- if (!chunk.isUnsaved()) {
- return false;
- } else {
- chunk.setUnsaved(false);
- ChunkPos chunkcoordintpair = chunk.getPos();
-
- try {
- ChunkStatus chunkstatus = chunk.getPersistedStatus();
-
- if (chunkstatus.getChunkType() != ChunkType.LEVELCHUNK) {
- if (this.isExistingChunkFull(chunkcoordintpair)) {
- return false;
- }
-
- if (chunkstatus == ChunkStatus.EMPTY && chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid)) {
- return false;
- }
- }
-
- this.level.getProfiler().incrementCounter("chunkSave");
- CompoundTag nbttagcompound = ChunkSerializer.write(this.level, chunk);
-
- this.write(chunkcoordintpair, nbttagcompound).exceptionally((throwable) -> {
- this.level.getServer().reportChunkSaveFailure(throwable, this.storageInfo(), chunkcoordintpair);
- return null;
- });
- this.markPosition(chunkcoordintpair, chunkstatus.getChunkType());
- return true;
- } catch (Exception exception) {
- this.level.getServer().reportChunkSaveFailure(exception, this.storageInfo(), chunkcoordintpair);
- return false;
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private boolean isExistingChunkFull(ChunkPos pos) {
- byte b0 = this.chunkTypeCache.get(pos.toLong());
-
- if (b0 != 0) {
- return b0 == 1;
- } else {
- CompoundTag nbttagcompound;
-
- try {
- nbttagcompound = (CompoundTag) ((Optional) this.readChunk(pos).join()).orElse((Object) null);
- if (nbttagcompound == null) {
- this.markPositionReplaceable(pos);
- return false;
- }
- } catch (Exception exception) {
- ChunkMap.LOGGER.error("Failed to read chunk {}", pos, exception);
- this.markPositionReplaceable(pos);
- return false;
- }
-
- ChunkType chunktype = ChunkSerializer.getChunkTypeFromTag(nbttagcompound);
-
- return this.markPosition(pos, chunktype) == 1;
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void setServerViewDistance(int watchDistance) { // Paper - public
- int j = Mth.clamp(watchDistance, 2, 32);
-
- if (j != this.serverViewDistance) {
- this.serverViewDistance = j;
- this.distanceManager.updatePlayerTickets(this.serverViewDistance);
- Iterator iterator = this.playerMap.getAllPlayers().iterator();
-
- while (iterator.hasNext()) {
- ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-
- this.updateChunkTracking(entityplayer);
- }
+ // Paper start - rewrite chunk system
+ final int clamped = Mth.clamp(watchDistance, 2, ca.spottedleaf.moonrise.common.util.MoonriseConstants.MAX_VIEW_DISTANCE);
+ if (clamped == this.serverViewDistance) {
+ return;
}
+ this.serverViewDistance = clamped;
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().setLoadDistance(this.serverViewDistance + 1);
+ // Paper end - rewrite chunk system
}
public int getPlayerViewDistance(ServerPlayer player) { // Paper - public
- return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance);
+ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getSendViewDistance(player); // Paper - rewrite chunk system
}
private void markChunkPendingToSend(ServerPlayer player, ChunkPos pos) {
- LevelChunk chunk = this.getChunkToSend(pos.toLong());
-
- if (chunk != null) {
- ChunkMap.markChunkPendingToSend(player, chunk);
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private static void markChunkPendingToSend(ServerPlayer player, LevelChunk chunk) {
- player.connection.chunkSender.markChunkPendingToSend(chunk);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private static void dropChunk(ServerPlayer player, ChunkPos pos) {
- player.connection.chunkSender.dropChunk(player, pos);
+ // Paper - rewrite chunk system
}
+ // Paper start - rewrite chunk system
+ @Override
+ public CompletableFuture<Optional<CompoundTag>> read(final ChunkPos pos) {
+ if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) {
+ try {
+ return CompletableFuture.completedFuture(
+ Optional.ofNullable(
+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.loadData(
+ this.level, pos.x, pos.z, ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA,
+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread()
+ )
+ )
+ );
+ } catch (final Throwable thr) {
+ return CompletableFuture.failedFuture(thr);
+ }
+ }
+ return super.read(pos);
+ }
+
+ @Override
+ public CompletableFuture<Void> write(final ChunkPos pos, final CompoundTag tag) {
+ if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) {
+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.scheduleSave(
+ this.level, pos.x, pos.z, tag,
+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA);
+ return null;
+ }
+ super.write(pos, tag);
+ return null;
+ }
+
+ @Override
+ public void flushWorker() {
+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.flush();
+ }
+ // Paper end - rewrite chunk system
+
@Nullable
public LevelChunk getChunkToSend(long pos) {
ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
@@ -1022,7 +636,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
// CraftBukkit start
- private CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) {
+ public CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) { // Paper - public
return this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, this.generator().getTypeNameForDataFixer(), chunkcoordintpair, this.level);
// CraftBukkit end
}
@@ -1113,19 +727,21 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.updatePlayerPos(player);
if (!flag1) {
this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player);
+ ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$addPlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation
}
player.setChunkTrackingView(ChunkTrackingView.EMPTY);
- this.updateChunkTracking(player);
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.addPlayerToDistanceMaps(this.level, player); // Paper - rewrite chunk system
} else {
SectionPos sectionposition = player.getLastSectionPos();
this.playerMap.removePlayer(player);
if (!flag2) {
this.distanceManager.removePlayer(sectionposition, player);
+ ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$removePlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation
}
- this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY);
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.removePlayerFromDistanceMaps(this.level, player); // Paper - rewrite chunk system
}
}
@@ -1137,17 +753,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public void move(ServerPlayer player) {
- ObjectIterator objectiterator = this.entityMap.values().iterator();
-
- while (objectiterator.hasNext()) {
- ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next();
-
- if (playerchunkmap_entitytracker.entity == player) {
- playerchunkmap_entitytracker.updatePlayers(this.level.players());
- } else {
- playerchunkmap_entitytracker.updatePlayer(player);
- }
- }
+ // Paper - optimise entity tracker
SectionPos sectionposition = player.getLastSectionPos();
SectionPos sectionposition1 = SectionPos.of((EntityAccess) player);
@@ -1157,6 +763,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
if (flag2 || flag != flag1) {
this.updatePlayerPos(player);
+ ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, sectionposition, sectionposition1, flag, flag1); // Paper - chunk tick iteration optimisation
if (!flag) {
this.distanceManager.removePlayer(sectionposition, player);
}
@@ -1173,70 +780,30 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.playerMap.unIgnorePlayer(player);
}
- this.updateChunkTracking(player);
+ // Paper - rewrite chunk system
}
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.updateMaps(this.level, player); // Paper - rewrite chunk system
}
private void updateChunkTracking(ServerPlayer player) {
- ChunkPos chunkcoordintpair = player.chunkPosition();
- int i = this.getPlayerViewDistance(player);
- ChunkTrackingView chunktrackingview = player.getChunkTrackingView();
-
- if (chunktrackingview instanceof ChunkTrackingView.Positioned chunktrackingview_a) {
- if (chunktrackingview_a.center().equals(chunkcoordintpair) && chunktrackingview_a.viewDistance() == i) {
- return;
- }
- }
-
- this.applyChunkTrackingView(player, ChunkTrackingView.of(chunkcoordintpair, i));
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void applyChunkTrackingView(ServerPlayer player, ChunkTrackingView chunkFilter) {
- if (player.level() == this.level) {
- ChunkTrackingView chunktrackingview1 = player.getChunkTrackingView();
-
- if (chunkFilter instanceof ChunkTrackingView.Positioned) {
- label15:
- {
- ChunkTrackingView.Positioned chunktrackingview_a = (ChunkTrackingView.Positioned) chunkFilter;
-
- if (chunktrackingview1 instanceof ChunkTrackingView.Positioned) {
- ChunkTrackingView.Positioned chunktrackingview_a1 = (ChunkTrackingView.Positioned) chunktrackingview1;
-
- if (chunktrackingview_a1.center().equals(chunktrackingview_a.center())) {
- break label15;
- }
- }
-
- player.connection.send(new ClientboundSetChunkCacheCenterPacket(chunktrackingview_a.center().x, chunktrackingview_a.center().z));
- }
- }
-
- ChunkTrackingView.difference(chunktrackingview1, chunkFilter, (chunkcoordintpair) -> {
- this.markChunkPendingToSend(player, chunkcoordintpair);
- }, (chunkcoordintpair) -> {
- ChunkMap.dropChunk(player, chunkcoordintpair);
- });
- player.setChunkTrackingView(chunkFilter);
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Override
public List<ServerPlayer> getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge) {
- Set<ServerPlayer> set = this.playerMap.getAllPlayers();
- Builder<ServerPlayer> builder = ImmutableList.builder();
- Iterator iterator = set.iterator();
-
- while (iterator.hasNext()) {
- ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-
- if (onlyOnWatchDistanceEdge && this.isChunkOnTrackedBorder(entityplayer, chunkPos.x, chunkPos.z) || !onlyOnWatchDistanceEdge && this.isChunkTracked(entityplayer, chunkPos.x, chunkPos.z)) {
- builder.add(entityplayer);
- }
+ // Paper start - rewrite chunk system
+ final ChunkHolder holder = this.getVisibleChunkIfPresent(chunkPos.toLong());
+ if (holder == null) {
+ return new ArrayList<>();
+ } else {
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)holder).moonrise$getPlayers(onlyOnWatchDistanceEdge);
}
-
- return builder.build();
+ // Paper end - rewrite chunk system
}
public void addEntity(Entity entity) {
@@ -1263,6 +830,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
ChunkMap.TrackedEntity playerchunkmap_entitytracker = new ChunkMap.TrackedEntity(entity, i, j, entitytypes.trackDeltas());
this.entityMap.put(entity.getId(), playerchunkmap_entitytracker);
+ // Paper start - optimise entity tracker
+ if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity() != null) {
+ throw new IllegalStateException("Entity is already tracked");
+ }
+ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(playerchunkmap_entitytracker);
+ // Paper end - optimise entity tracker
playerchunkmap_entitytracker.updatePlayers(this.level.players());
if (entity instanceof ServerPlayer) {
ServerPlayer entityplayer = (ServerPlayer) entity;
@@ -1303,16 +876,49 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
playerchunkmap_entitytracker1.broadcastRemoved();
}
+ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(null); // Paper - optimise entity tracker
}
- protected void tick() {
- Iterator iterator = this.playerMap.getAllPlayers().iterator();
+ // 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();;
- while (iterator.hasNext()) {
- ServerPlayer entityplayer = (ServerPlayer) iterator.next();
+ final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.world.entity.Entity> trackerEntities = entityLookup.trackerEntities;
+ final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked();
+ for (int i = 0, len = trackerEntities.size(); i < len; ++i) {
+ final Entity entity = trackerEntitiesRaw[i];
+ 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(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();
- this.updateChunkTracking(entityplayer);
+ 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$clearPlayers();
+ }
+ }
+ // Paper end - optimise entity tracker
+
+ protected void tick() {
+ // Paper start - optimise entity tracker
+ if (true) {
+ this.newTrackerTick();
+ return;
}
+ // Paper end - optimise entity tracker
+ // Paper - rewrite chunk system
List<ServerPlayer> list = Lists.newArrayList();
List<ServerPlayer> list1 = this.level.players();
@@ -1419,27 +1025,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public void waitForLightBeforeSending(ChunkPos centerPos, int radius) {
- int j = radius + 1;
-
- ChunkPos.rangeClosed(centerPos, j).forEach((chunkcoordintpair1) -> {
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(chunkcoordintpair1.toLong());
-
- if (playerchunk != null) {
- playerchunk.addSendDependency(this.lightEngine.waitForPendingTasks(chunkcoordintpair1.x, chunkcoordintpair1.z));
- }
-
- });
+ // Paper - rewrite chunk system
}
- public class ChunkDistanceManager extends DistanceManager { // Paper - public
+ public class ChunkDistanceManager extends DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager { // Paper - public // Paper - rewrite chunk system
protected ChunkDistanceManager(final Executor workerExecutor, final Executor mainThreadExecutor) {
super(workerExecutor, mainThreadExecutor);
}
+ // Paper start - rewrite chunk system
+ @Override
+ public final ChunkMap moonrise$getChunkMap() {
+ return ChunkMap.this;
+ }
+ // Paper end - rewrite chunk system
+
@Override
protected boolean isChunkToRemove(long pos) {
- return ChunkMap.this.toDrop.contains(pos);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Nullable
@@ -1455,7 +1059,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
}
- public class TrackedEntity {
+ public class TrackedEntity implements ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity { // Paper - optimise entity tracker
public final ServerEntity serverEntity;
final Entity entity;
@@ -1463,6 +1067,84 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
SectionPos lastSectionPos;
public final Set<ServerPlayerConnection> seenBy = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Paper - Perf: optimise map impl
+ // Paper start - optimise entity tracker
+ private long lastChunkUpdate = -1L;
+ private ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk lastTrackedChunk;
+
+ @Override
+ public final void moonrise$tick(final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk chunk) {
+ if (chunk == null) {
+ this.moonrise$clearPlayers();
+ return;
+ }
+
+ final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> players = chunk.getPlayers(ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.VIEW_DISTANCE);
+
+ if (players == null) {
+ this.moonrise$clearPlayers();
+ return;
+ }
+
+ final long lastChunkUpdate = this.lastChunkUpdate;
+ final long currChunkUpdate = chunk.getUpdateCount();
+ final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk lastTrackedChunk = this.lastTrackedChunk;
+ this.lastChunkUpdate = currChunkUpdate;
+ this.lastTrackedChunk = chunk;
+
+ final ServerPlayer[] playersRaw = players.getRawDataUnchecked();
+
+ for (int i = 0, len = players.size(); i < len; ++i) {
+ final ServerPlayer player = playersRaw[i];
+ this.updatePlayer(player);
+ }
+
+ if (lastChunkUpdate != currChunkUpdate || lastTrackedChunk != chunk) {
+ // need to purge any players possible not in the chunk list
+ for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) {
+ final ServerPlayer player = conn.getPlayer();
+ if (!players.contains(player)) {
+ this.removePlayer(player);
+ }
+ }
+ }
+ }
+
+ @Override
+ public final void moonrise$removeNonTickThreadPlayers() {
+ boolean foundToRemove = false;
+ for (final ServerPlayerConnection conn : this.seenBy) {
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(conn.getPlayer())) {
+ foundToRemove = true;
+ break;
+ }
+ }
+
+ if (!foundToRemove) {
+ return;
+ }
+
+ for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) {
+ ServerPlayer player = conn.getPlayer();
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) {
+ this.removePlayer(player);
+ }
+ }
+ }
+
+ @Override
+ public final void moonrise$clearPlayers() {
+ this.lastChunkUpdate = -1;
+ this.lastTrackedChunk = null;
+ if (this.seenBy.isEmpty()) {
+ return;
+ }
+ for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) {
+ ServerPlayer player = conn.getPlayer();
+ this.removePlayer(player);
+ }
+ }
+ // Paper end - optimise entity tracker
+
public TrackedEntity(final Entity entity, final int i, final int j, final boolean flag) {
this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast, this.seenBy); // CraftBukkit
this.entity = entity;
diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
index 56aa26bb8d0bfbd3c799163fa620259e6455c89b..2d2596f04f5addac38037a14a02c6e0622d0c485 100644
--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
@@ -36,64 +36,58 @@ import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.LevelChunk;
import org.slf4j.Logger;
-public abstract class DistanceManager {
+public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager, ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager { // Paper - rewrite chunk system // Paper - chunk tick iteration optimisation
static final Logger LOGGER = LogUtils.getLogger();
static final int PLAYER_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING);
private static final int INITIAL_TICKET_LIST_CAPACITY = 4;
final Long2ObjectMap<ObjectSet<ServerPlayer>> playersPerChunk = new Long2ObjectOpenHashMap();
- public final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> tickets = new Long2ObjectOpenHashMap();
- private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker();
- private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8);
- private final TickingTracker tickingTicketsTracker = new TickingTracker();
- private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(32);
- final Set<ChunkHolder> chunksToUpdateFutures = Sets.newHashSet();
- final ChunkTaskPriorityQueueSorter ticketThrottler;
- final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> ticketThrottlerInput;
- final ProcessorHandle<ChunkTaskPriorityQueueSorter.Release> ticketThrottlerReleaser;
- final LongSet ticketsToRelease = new LongOpenHashSet();
- final Executor mainThreadExecutor;
+ // Paper - rewrite chunk system
+ // Paper - chunk tick iteration optimisation
+ // Paper - rewrite chunk system
private long ticketTickCounter;
- public int simulationDistance = 10;
+ // Paper - rewrite chunk system
protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor) {
Objects.requireNonNull(mainThreadExecutor);
ProcessorHandle<Runnable> mailbox = ProcessorHandle.of("player ticket throttler", mainThreadExecutor::execute);
ChunkTaskPriorityQueueSorter chunktaskqueuesorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(mailbox), workerExecutor, 4);
- this.ticketThrottler = chunktaskqueuesorter;
- this.ticketThrottlerInput = chunktaskqueuesorter.getProcessor(mailbox, true);
- this.ticketThrottlerReleaser = chunktaskqueuesorter.getReleaseProcessor(mailbox);
- this.mainThreadExecutor = mainThreadExecutor;
+ // Paper - rewrite chunk system
}
- protected void purgeStaleTickets() {
- ++this.ticketTickCounter;
- ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
-
- while (objectiterator.hasNext()) {
- Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next();
- Iterator<Ticket<?>> iterator = ((SortedArraySet) entry.getValue()).iterator();
- boolean flag = false;
-
- while (iterator.hasNext()) {
- Ticket<?> ticket = (Ticket) iterator.next();
+ // Paper start - rewrite chunk system
+ public ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager getChunkHolderManager() {
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getChunkTaskScheduler().chunkHolderManager;
+ }
+ // Paper end - rewrite chunk system
+ // Paper start - chunk tick iteration optimisation
+ private final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<ServerPlayer> spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>();
- if (ticket.timedOut(this.ticketTickCounter)) {
- iterator.remove();
- flag = true;
- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket);
- }
- }
+ @Override
+ public final void moonrise$addPlayer(final ServerPlayer player, final SectionPos pos) {
+ this.spawnChunkTracker.add(player, pos.x(), pos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE);
+ }
- if (flag) {
- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false);
- }
+ @Override
+ public final void moonrise$removePlayer(final ServerPlayer player, final SectionPos pos) {
+ this.spawnChunkTracker.remove(player);
+ }
- if (((SortedArraySet) entry.getValue()).isEmpty()) {
- objectiterator.remove();
- }
+ @Override
+ public final void moonrise$updatePlayer(final ServerPlayer player,
+ final SectionPos oldPos, final SectionPos newPos,
+ final boolean oldIgnore, final boolean newIgnore) {
+ if (newIgnore) {
+ this.spawnChunkTracker.remove(player);
+ } else {
+ this.spawnChunkTracker.addOrUpdate(player, newPos.x(), newPos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE);
}
+ }
+ // Paper end - chunk tick iteration optimisation
+
+ protected void purgeStaleTickets() {
+ this.getChunkHolderManager().tick(); // Paper - rewrite chunk system
}
@@ -110,86 +104,15 @@ public abstract class DistanceManager {
protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k);
public boolean runAllUpdates(ChunkMap chunkLoadingManager) {
- this.naturalSpawnChunkCounter.runAllUpdates();
- this.tickingTicketsTracker.runAllUpdates();
- this.playerTicketManager.runAllUpdates();
- int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE);
- boolean flag = i != 0;
-
- if (flag) {
- ;
- }
-
- if (!this.chunksToUpdateFutures.isEmpty()) {
- this.chunksToUpdateFutures.forEach((playerchunk) -> {
- playerchunk.updateHighestAllowedStatus(chunkLoadingManager);
- });
- this.chunksToUpdateFutures.forEach((playerchunk) -> {
- playerchunk.updateFutures(chunkLoadingManager, this.mainThreadExecutor);
- });
- this.chunksToUpdateFutures.clear();
- return true;
- } else {
- if (!this.ticketsToRelease.isEmpty()) {
- LongIterator longiterator = this.ticketsToRelease.iterator();
-
- while (longiterator.hasNext()) {
- long j = longiterator.nextLong();
-
- if (this.getTickets(j).stream().anyMatch((ticket) -> {
- return ticket.getType() == TicketType.PLAYER;
- })) {
- ChunkHolder playerchunk = chunkLoadingManager.getUpdatingChunkIfPresent(j);
-
- if (playerchunk == null) {
- throw new IllegalStateException();
- }
-
- CompletableFuture<ChunkResult<LevelChunk>> completablefuture = playerchunk.getEntityTickingChunkFuture();
-
- completablefuture.thenAccept((chunkresult) -> {
- this.mainThreadExecutor.execute(() -> {
- this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> {
- }, j, false));
- });
- });
- }
- }
-
- this.ticketsToRelease.clear();
- }
-
- return flag;
- }
+ return this.getChunkHolderManager().processTicketUpdates(); // Paper - rewrite chunk system
}
boolean addTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
- SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i);
- int j = DistanceManager.getTicketLevelAt(arraysetsorted);
- Ticket<?> ticket1 = (Ticket) arraysetsorted.addOrGet(ticket);
-
- ticket1.setCreatedTick(this.ticketTickCounter);
- if (ticket.getTicketLevel() < j) {
- this.ticketTracker.update(i, ticket.getTicketLevel(), true);
- }
-
- return ticket == ticket1; // CraftBukkit
+ return this.getChunkHolderManager().addTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system
}
boolean removeTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
- SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i);
-
- boolean removed = false; // CraftBukkit
- if (arraysetsorted.remove(ticket)) {
- removed = true; // CraftBukkit
- }
-
- if (arraysetsorted.isEmpty()) {
- this.tickets.remove(i);
- }
-
- this.ticketTracker.update(i, DistanceManager.getTicketLevelAt(arraysetsorted), false);
- return removed; // CraftBukkit
+ return this.getChunkHolderManager().removeTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system
}
public <T> void addTicket(TicketType<T> type, ChunkPos pos, int level, T argument) {
@@ -208,13 +131,7 @@ public abstract class DistanceManager {
}
public <T> boolean addRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) {
- // CraftBukkit end
- Ticket<T> ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0);
- long j = chunkcoordintpair.toLong();
-
- boolean added = this.addTicket(j, ticket); // CraftBukkit
- this.tickingTicketsTracker.addTicket(j, ticket);
- return added; // CraftBukkit
+ return this.getChunkHolderManager().addTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system
}
public <T> void removeRegionTicket(TicketType<T> type, ChunkPos pos, int radius, T argument) {
@@ -223,32 +140,21 @@ public abstract class DistanceManager {
}
public <T> boolean removeRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) {
- // CraftBukkit end
- Ticket<T> ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0);
- long j = chunkcoordintpair.toLong();
-
- boolean removed = this.removeTicket(j, ticket); // CraftBukkit
- this.tickingTicketsTracker.removeTicket(j, ticket);
- return removed; // CraftBukkit
+ return this.getChunkHolderManager().removeTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system
}
private SortedArraySet<Ticket<?>> getTickets(long position) {
- return (SortedArraySet) this.tickets.computeIfAbsent(position, (j) -> {
- return SortedArraySet.create(4);
- });
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
protected void updateChunkForced(ChunkPos pos, boolean forced) {
- Ticket<ChunkPos> ticket = new Ticket<>(TicketType.FORCED, ChunkMap.FORCED_TICKET_LEVEL, pos);
- long i = pos.toLong();
-
+ // Paper start - rewrite chunk system
if (forced) {
- this.addTicket(i, ticket);
- this.tickingTicketsTracker.addTicket(i, ticket);
+ this.getChunkHolderManager().addTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos);
} else {
- this.removeTicket(i, ticket);
- this.tickingTicketsTracker.removeTicket(i, ticket);
+ this.getChunkHolderManager().removeTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos);
}
+ // Paper end - rewrite chunk system
}
@@ -259,9 +165,8 @@ public abstract class DistanceManager {
((ObjectSet) this.playersPerChunk.computeIfAbsent(i, (j) -> {
return new ObjectOpenHashSet();
})).add(player);
- this.naturalSpawnChunkCounter.update(i, 0, true);
- this.playerTicketManager.update(i, 0, true);
- this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair);
+ // Paper - chunk tick iteration optimisation
+ // Paper - rewrite chunk system
}
public void removePlayer(SectionPos pos, ServerPlayer player) {
@@ -273,151 +178,81 @@ public abstract class DistanceManager {
if (objectset != null) objectset.remove(player); // Paper - some state corruption happens here, don't crash, clean up gracefully
if (objectset == null || objectset.isEmpty()) { // Paper
this.playersPerChunk.remove(i);
- this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false);
- this.playerTicketManager.update(i, Integer.MAX_VALUE, false);
- this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair);
+ // Paper - chunk tick iteration optimisation
+ // Paper - rewrite chunk system
}
}
private int getPlayerTicketLevel() {
- return Math.max(0, ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING) - this.simulationDistance);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public boolean inEntityTickingRange(long chunkPos) {
- return ChunkLevel.isEntityTicking(this.tickingTicketsTracker.getLevel(chunkPos));
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.getChunkHolderManager().getChunkHolder(chunkPos);
+ return chunkHolder != null && chunkHolder.isEntityTickingReady();
+ // Paper end - rewrite chunk system
}
public boolean inBlockTickingRange(long chunkPos) {
- return ChunkLevel.isBlockTicking(this.tickingTicketsTracker.getLevel(chunkPos));
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.getChunkHolderManager().getChunkHolder(chunkPos);
+ return chunkHolder != null && chunkHolder.isTickingReady();
+ // Paper end - rewrite chunk system
}
protected String getTicketDebugString(long pos) {
- SortedArraySet<Ticket<?>> arraysetsorted = (SortedArraySet) this.tickets.get(pos);
-
- return arraysetsorted != null && !arraysetsorted.isEmpty() ? ((Ticket) arraysetsorted.first()).toString() : "no_ticket";
+ return this.getChunkHolderManager().getTicketDebugString(pos); // Paper - rewrite chunk system
}
protected void updatePlayerTickets(int viewDistance) {
- this.playerTicketManager.updateViewDistance(viewDistance);
+ this.moonrise$getChunkMap().setServerViewDistance(viewDistance); // Paper - rewrite chunk system
}
public void updateSimulationDistance(int simulationDistance) {
- if (simulationDistance != this.simulationDistance) {
- this.simulationDistance = simulationDistance;
- this.tickingTicketsTracker.replacePlayerTicketsLevel(this.getPlayerTicketLevel());
- }
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getPlayerChunkLoader().setTickDistance(simulationDistance); // Paper - rewrite chunk system
}
public int getNaturalSpawnChunkCount() {
- this.naturalSpawnChunkCounter.runAllUpdates();
- return this.naturalSpawnChunkCounter.chunks.size();
+ return this.spawnChunkTracker.getTotalPositions(); // Paper - chunk tick iteration optimisation
}
public boolean hasPlayersNearby(long chunkPos) {
- this.naturalSpawnChunkCounter.runAllUpdates();
- return this.naturalSpawnChunkCounter.chunks.containsKey(chunkPos);
+ return this.spawnChunkTracker.hasObjectsNear(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)); // Paper - chunk tick iteration optimisation
}
public String getDebugStatus() {
- return this.ticketThrottler.getDebugStatus();
+ return "No DistanceManager stats available"; // Paper - rewrite chunk system
}
private void dumpTickets(String path) {
- try {
- FileOutputStream fileoutputstream = new FileOutputStream(new File(path));
-
- try {
- ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().iterator();
-
- while (objectiterator.hasNext()) {
- Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next();
- ChunkPos chunkcoordintpair = new ChunkPos(entry.getLongKey());
- Iterator iterator = ((SortedArraySet) entry.getValue()).iterator();
-
- while (iterator.hasNext()) {
- Ticket<?> ticket = (Ticket) iterator.next();
-
- fileoutputstream.write((chunkcoordintpair.x + "\t" + chunkcoordintpair.z + "\t" + String.valueOf(ticket.getType()) + "\t" + ticket.getTicketLevel() + "\t\n").getBytes(StandardCharsets.UTF_8));
- }
- }
- } catch (Throwable throwable) {
- try {
- fileoutputstream.close();
- } catch (Throwable throwable1) {
- throwable.addSuppressed(throwable1);
- }
-
- throw throwable;
- }
-
- fileoutputstream.close();
- } catch (IOException ioexception) {
- DistanceManager.LOGGER.error("Failed to dump tickets to {}", path, ioexception);
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@VisibleForTesting
TickingTracker tickingTracker() {
- return this.tickingTicketsTracker;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void removeTicketsOnClosing() {
- ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve
- ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
-
- while (objectiterator.hasNext()) {
- Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next();
- Iterator<Ticket<?>> iterator = ((SortedArraySet) entry.getValue()).iterator();
- boolean flag = false;
-
- while (iterator.hasNext()) {
- Ticket<?> ticket = (Ticket) iterator.next();
-
- if (!immutableset.contains(ticket.getType())) {
- iterator.remove();
- flag = true;
- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket);
- }
- }
-
- if (flag) {
- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false);
- }
-
- if (((SortedArraySet) entry.getValue()).isEmpty()) {
- objectiterator.remove();
- }
- }
+ // Paper - rewrite chunk system
}
public boolean hasTickets() {
- return !this.tickets.isEmpty();
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
// CraftBukkit start
public <T> void removeAllTicketsFor(TicketType<T> ticketType, int ticketLevel, T ticketIdentifier) {
- Ticket<T> target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier);
-
- for (java.util.Iterator<Entry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
- Entry<SortedArraySet<Ticket<?>>> entry = iterator.next();
- SortedArraySet<Ticket<?>> tickets = entry.getValue();
- if (tickets.remove(target)) {
- // copied from removeTicket
- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false);
-
- // can't use entry after it's removed
- if (tickets.isEmpty()) {
- iterator.remove();
- }
- }
- }
+ this.getChunkHolderManager().removeAllTicketsFor(ticketType, ticketLevel, ticketIdentifier); // Paper - rewrite chunk system
}
// CraftBukkit end
+ /* // Paper - rewrite chunk system
private class ChunkTicketTracker extends ChunkTracker {
private static final int MAX_LEVEL = ChunkLevel.MAX_LEVEL + 1;
@@ -463,7 +298,7 @@ public abstract class DistanceManager {
public int runDistanceUpdates(int distance) {
return this.runUpdates(distance);
}
- }
+ }*/ // Paper - rewrite chunk system
private class FixedPlayerDistanceChunkTracker extends ChunkTracker {
@@ -543,6 +378,7 @@ public abstract class DistanceManager {
}
}
+ /* // Paper - rewrite chunk system
private class PlayerTicketTracker extends DistanceManager.FixedPlayerDistanceChunkTracker {
private int viewDistance = 0;
@@ -637,5 +473,5 @@ public abstract class DistanceManager {
private boolean haveTicketFor(int distance) {
return distance <= this.viewDistance;
}
- }
+ }*/ // Paper - rewrite chunk system
}
diff --git a/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java b/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java
index 3dc1daa3c6a04d3ff1a2353773b465fc380994a2..3575782f13a7f3c52e64dc5046803305d5c8ce12 100644
--- a/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java
@@ -27,249 +27,105 @@ public abstract class GenerationChunkHolder {
public static final ChunkResult<ChunkAccess> UNLOADED_CHUNK = ChunkResult.error("Unloaded chunk");
public static final CompletableFuture<ChunkResult<ChunkAccess>> UNLOADED_CHUNK_FUTURE = CompletableFuture.completedFuture(UNLOADED_CHUNK);
protected final ChunkPos pos;
- @Nullable
- private volatile ChunkStatus highestAllowedStatus;
- private final AtomicReference<ChunkStatus> startedWork = new AtomicReference<>();
- private final AtomicReferenceArray<CompletableFuture<ChunkResult<ChunkAccess>>> futures = new AtomicReferenceArray<>(CHUNK_STATUSES.size());
- private final AtomicReference<ChunkGenerationTask> task = new AtomicReference<>();
- private final AtomicInteger generationRefCount = new AtomicInteger();
+ // Paper - rewrite chunk system
public GenerationChunkHolder(ChunkPos pos) {
this.pos = pos;
}
public CompletableFuture<ChunkResult<ChunkAccess>> scheduleChunkGenerationTask(ChunkStatus requestedStatus, ChunkMap chunkLoadingManager) {
- if (this.isStatusDisallowed(requestedStatus)) {
- return UNLOADED_CHUNK_FUTURE;
- } else {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.getOrCreateFuture(requestedStatus);
- if (completableFuture.isDone()) {
- return completableFuture;
- } else {
- ChunkGenerationTask chunkGenerationTask = this.task.get();
- if (chunkGenerationTask == null || requestedStatus.isAfter(chunkGenerationTask.targetStatus)) {
- this.rescheduleChunkTask(chunkLoadingManager, requestedStatus);
- }
-
- return completableFuture;
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
CompletableFuture<ChunkResult<ChunkAccess>> applyStep(ChunkStep step, GeneratingChunkMap chunkLoadingManager, StaticCache2D<GenerationChunkHolder> chunks) {
- if (this.isStatusDisallowed(step.targetStatus())) {
- return UNLOADED_CHUNK_FUTURE;
- } else {
- return this.acquireStatusBump(step.targetStatus()) ? chunkLoadingManager.applyStep(this, step, chunks).handle((chunk, throwable) -> {
- if (throwable != null) {
- CrashReport crashReport = CrashReport.forThrowable(throwable, "Exception chunk generation/loading");
- MinecraftServer.setFatalException(new ReportedException(crashReport));
- } else {
- this.completeFuture(step.targetStatus(), chunk);
- }
-
- return ChunkResult.of(chunk);
- }) : this.getOrCreateFuture(step.targetStatus());
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
protected void updateHighestAllowedStatus(ChunkMap chunkLoadingManager) {
- ChunkStatus chunkStatus = this.highestAllowedStatus;
- ChunkStatus chunkStatus2 = ChunkLevel.generationStatus(this.getTicketLevel());
- this.highestAllowedStatus = chunkStatus2;
- boolean bl = chunkStatus != null && (chunkStatus2 == null || chunkStatus2.isBefore(chunkStatus));
- if (bl) {
- this.failAndClearPendingFuturesBetween(chunkStatus2, chunkStatus);
- if (this.task.get() != null) {
- this.rescheduleChunkTask(chunkLoadingManager, this.findHighestStatusWithPendingFuture(chunkStatus2));
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void replaceProtoChunk(ImposterProtoChunk chunk) {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = CompletableFuture.completedFuture(ChunkResult.of(chunk));
-
- for (int i = 0; i < this.futures.length() - 1; i++) {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture2 = this.futures.get(i);
- Objects.requireNonNull(completableFuture2);
- ChunkAccess chunkAccess = completableFuture2.getNow(NOT_DONE_YET).orElse(null);
- if (!(chunkAccess instanceof ProtoChunk)) {
- throw new IllegalStateException("Trying to replace a ProtoChunk, but found " + chunkAccess);
- }
-
- if (!this.futures.compareAndSet(i, completableFuture2, completableFuture)) {
- throw new IllegalStateException("Future changed by other thread while trying to replace it");
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
void removeTask(ChunkGenerationTask loader) {
- this.task.compareAndSet(loader, null);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void rescheduleChunkTask(ChunkMap chunkLoadingManager, @Nullable ChunkStatus requestedStatus) {
- ChunkGenerationTask chunkGenerationTask;
- if (requestedStatus != null) {
- chunkGenerationTask = chunkLoadingManager.scheduleGenerationTask(requestedStatus, this.getPos());
- } else {
- chunkGenerationTask = null;
- }
-
- ChunkGenerationTask chunkGenerationTask3 = this.task.getAndSet(chunkGenerationTask);
- if (chunkGenerationTask3 != null) {
- chunkGenerationTask3.markForCancellation();
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private CompletableFuture<ChunkResult<ChunkAccess>> getOrCreateFuture(ChunkStatus status) {
- if (this.isStatusDisallowed(status)) {
- return UNLOADED_CHUNK_FUTURE;
- } else {
- int i = status.getIndex();
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(i);
-
- while (completableFuture == null) {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture2 = new CompletableFuture<>();
- completableFuture = this.futures.compareAndExchange(i, null, completableFuture2);
- if (completableFuture == null) {
- if (this.isStatusDisallowed(status)) {
- this.failAndClearPendingFuture(i, completableFuture2);
- return UNLOADED_CHUNK_FUTURE;
- }
-
- return completableFuture2;
- }
- }
-
- return completableFuture;
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void failAndClearPendingFuturesBetween(@Nullable ChunkStatus from, ChunkStatus to) {
- int i = from == null ? 0 : from.getIndex() + 1;
- int j = to.getIndex();
-
- for (int k = i; k <= j; k++) {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(k);
- if (completableFuture != null) {
- this.failAndClearPendingFuture(k, completableFuture);
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void failAndClearPendingFuture(int statusIndex, CompletableFuture<ChunkResult<ChunkAccess>> previousFuture) {
- if (previousFuture.complete(UNLOADED_CHUNK) && !this.futures.compareAndSet(statusIndex, previousFuture, null)) {
- throw new IllegalStateException("Nothing else should replace the future here");
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void completeFuture(ChunkStatus status, ChunkAccess chunk) {
- ChunkResult<ChunkAccess> chunkResult = ChunkResult.of(chunk);
- int i = status.getIndex();
-
- while (true) {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(i);
- if (completableFuture == null) {
- if (this.futures.compareAndSet(i, null, CompletableFuture.completedFuture(chunkResult))) {
- return;
- }
- } else {
- if (completableFuture.complete(chunkResult)) {
- return;
- }
-
- if (completableFuture.getNow(NOT_DONE_YET).isSuccess()) {
- throw new IllegalStateException("Trying to complete a future but found it to be completed successfully already");
- }
-
- Thread.yield();
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Nullable
private ChunkStatus findHighestStatusWithPendingFuture(@Nullable ChunkStatus checkUpperBound) {
- if (checkUpperBound == null) {
- return null;
- } else {
- ChunkStatus chunkStatus = checkUpperBound;
-
- for (ChunkStatus chunkStatus2 = this.startedWork.get();
- chunkStatus2 == null || chunkStatus.isAfter(chunkStatus2);
- chunkStatus = chunkStatus.getParent()
- ) {
- if (this.futures.get(chunkStatus.getIndex()) != null) {
- return chunkStatus;
- }
-
- if (chunkStatus == ChunkStatus.EMPTY) {
- break;
- }
- }
-
- return null;
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private boolean acquireStatusBump(ChunkStatus nextStatus) {
- ChunkStatus chunkStatus = nextStatus == ChunkStatus.EMPTY ? null : nextStatus.getParent();
- ChunkStatus chunkStatus2 = this.startedWork.compareAndExchange(chunkStatus, nextStatus);
- if (chunkStatus2 == chunkStatus) {
- return true;
- } else if (chunkStatus2 != null && !nextStatus.isAfter(chunkStatus2)) {
- return false;
- } else {
- throw new IllegalStateException("Unexpected last startedWork status: " + chunkStatus2 + " while trying to start: " + nextStatus);
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private boolean isStatusDisallowed(ChunkStatus status) {
- ChunkStatus chunkStatus = this.highestAllowedStatus;
- return chunkStatus == null || status.isAfter(chunkStatus);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void increaseGenerationRefCount() {
- this.generationRefCount.incrementAndGet();
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void decreaseGenerationRefCount() {
- int i = this.generationRefCount.decrementAndGet();
- if (i < 0) {
- throw new IllegalStateException("More releases than claims. Count: " + i);
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public int getGenerationRefCount() {
- return this.generationRefCount.get();
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Nullable
public ChunkAccess getChunkIfPresentUnchecked(ChunkStatus requestedStatus) {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(requestedStatus.getIndex());
- return completableFuture == null ? null : completableFuture.getNow(NOT_DONE_YET).orElse(null);
+ // Paper start - rewrite chunk system
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkIfPresentUnchecked(requestedStatus);
+ // Paper end - rewrite chunk system
}
@Nullable
public ChunkAccess getChunkIfPresent(ChunkStatus requestedStatus) {
- return this.isStatusDisallowed(requestedStatus) ? null : this.getChunkIfPresentUnchecked(requestedStatus);
+ // Paper start - rewrite chunk system
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkIfPresent(requestedStatus);
+ // Paper end - rewrite chunk system
}
@Nullable
public ChunkAccess getLatestChunk() {
- ChunkStatus chunkStatus = this.startedWork.get();
- if (chunkStatus == null) {
- return null;
- } else {
- ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(chunkStatus);
- return chunkAccess != null ? chunkAccess : this.getChunkIfPresentUnchecked(chunkStatus.getParent());
- }
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion();
+ return lastCompletion == null ? null : lastCompletion.chunk();
+ // Paper end - rewrite chunk system
}
@Nullable
public ChunkStatus getPersistedStatus() {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(ChunkStatus.EMPTY.getIndex());
- ChunkAccess chunkAccess = completableFuture == null ? null : completableFuture.getNow(NOT_DONE_YET).orElse(null);
- return chunkAccess == null ? null : chunkAccess.getPersistedStatus();
+ // Paper start - rewrite chunk system
+ final ChunkAccess chunk = this.getLatestChunk();
+ return chunk == null ? null : chunk.getPersistedStatus();
+ // Paper end - rewrite chunk system
}
public ChunkPos getPos() {
@@ -277,7 +133,7 @@ public abstract class GenerationChunkHolder {
}
public FullChunkStatus getFullStatus() {
- return ChunkLevel.fullStatus(this.getTicketLevel());
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkStatus(); // Paper - rewrite chunk system
}
public abstract int getTicketLevel();
@@ -286,26 +142,15 @@ public abstract class GenerationChunkHolder {
@VisibleForDebug
public List<Pair<ChunkStatus, CompletableFuture<ChunkResult<ChunkAccess>>>> getAllFutures() {
- List<Pair<ChunkStatus, CompletableFuture<ChunkResult<ChunkAccess>>>> list = new ArrayList<>();
-
- for (int i = 0; i < CHUNK_STATUSES.size(); i++) {
- list.add(Pair.of(CHUNK_STATUSES.get(i), this.futures.get(i)));
- }
-
- return list;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Nullable
@VisibleForDebug
public ChunkStatus getLatestStatus() {
- for (int i = CHUNK_STATUSES.size() - 1; i >= 0; i--) {
- ChunkStatus chunkStatus = CHUNK_STATUSES.get(i);
- ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(chunkStatus);
- if (chunkAccess != null) {
- return chunkStatus;
- }
- }
-
- return null;
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion();
+ return lastCompletion == null ? null : lastCompletion.genStatus();
+ // Paper end - rewrite chunk system
}
}
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
index 60f678c26fb5144386d8697ddfd5b6d841563b6f..9ccb3ffc375298fda4dca97803e65e39df8493eb 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -46,7 +46,7 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemp
import net.minecraft.world.level.storage.DimensionDataStorage;
import net.minecraft.world.level.storage.LevelStorageSource;
-public class ServerChunkCache extends ChunkSource {
+public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache { // Paper - rewrite chunk system
public static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger(); // Paper
private static final List<ChunkStatus> CHUNK_STATUSES = ChunkStatus.getStatusList();
@@ -71,6 +71,62 @@ public class ServerChunkCache extends ChunkSource {
private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<net.minecraft.world.level.chunk.LevelChunk> fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>();
long chunkFutureAwaitCounter;
// Paper end
+ // Paper start - rewrite chunk system
+
+ @Override
+ public final void moonrise$setFullChunk(final int chunkX, final int chunkZ, final LevelChunk chunk) {
+ final long key = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ if (chunk == null) {
+ this.fullChunks.remove(key);
+ } else {
+ this.fullChunks.put(key, chunk);
+ }
+ }
+
+ @Override
+ public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) {
+ return this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+
+ private ChunkAccess syncLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus) {
+ 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,
+ completable::complete
+ );
+
+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, chunkX, chunkZ)) {
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.pushChunkWait(this.level, chunkX, chunkZ);
+ this.mainThreadProcessor.managedBlock(completable::isDone);
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.popChunkWait();
+ }
+
+ final ChunkAccess ret = completable.join();
+ if (ret == null) {
+ throw new IllegalStateException("Chunk not loaded when requested");
+ }
+
+ return ret;
+ }
+
+ private ChunkAccess getChunkFallback(final int chunkX, final int chunkZ, final ChunkStatus toStatus,
+ final boolean load) {
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler();
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager;
+
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder currentChunk = chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ final ChunkAccess ifPresent = currentChunk == null ? null : currentChunk.getChunkIfPresent(toStatus);
+
+ if (ifPresent != null && (toStatus != ChunkStatus.FULL || currentChunk.isFullChunkReady())) {
+ return ifPresent;
+ }
+
+ return load ? this.syncLoad(chunkX, chunkZ, toStatus) : null;
+ }
+ // Paper end - rewrite chunk system
+ private ServerChunkCache.ChunkAndHolder[] iterationCopy; // Paper - chunk tick iteration optimisations
public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory) {
this.level = world;
@@ -97,13 +153,7 @@ public class ServerChunkCache extends ChunkSource {
}
// CraftBukkit end
// Paper start
- public void addLoadedChunk(LevelChunk chunk) {
- this.fullChunks.put(chunk.coordinateKey, chunk);
- }
-
- public void removeLoadedChunk(LevelChunk chunk) {
- this.fullChunks.remove(chunk.coordinateKey);
- }
+ // Paper - rewrite chunk system
@Nullable
public ChunkAccess getChunkAtImmediately(int x, int z) {
@@ -174,63 +224,25 @@ public class ServerChunkCache extends ChunkSource {
@Nullable
@Override
public ChunkAccess getChunk(int x, int z, ChunkStatus leastStatus, boolean create) {
- if (Thread.currentThread() != this.mainThread) {
- return (ChunkAccess) CompletableFuture.supplyAsync(() -> {
- return this.getChunk(x, z, leastStatus, create);
- }, this.mainThreadProcessor).join();
- } else {
- // Paper start - Perf: Optimise getChunkAt calls for loaded chunks
- LevelChunk ifLoaded = this.getChunkAtIfLoadedMainThread(x, z);
- if (ifLoaded != null) {
- return ifLoaded;
- }
- // Paper end - Perf: Optimise getChunkAt calls for loaded chunks
- ProfilerFiller gameprofilerfiller = this.level.getProfiler();
-
- gameprofilerfiller.incrementCounter("getChunk");
- long k = ChunkPos.asLong(x, z);
+ // Paper start - rewrite chunk system
+ if (leastStatus == ChunkStatus.FULL) {
+ final LevelChunk ret = this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x, z));
- for (int l = 0; l < 4; ++l) {
- if (k == this.lastChunkPos[l] && leastStatus == this.lastChunkStatus[l]) {
- ChunkAccess ichunkaccess = this.lastChunk[l];
-
- if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime
- return ichunkaccess;
- }
- }
+ if (ret != null) {
+ return ret;
}
- gameprofilerfiller.incrementCounter("getChunkCacheMiss");
- CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create);
- ServerChunkCache.MainThreadExecutor chunkproviderserver_b = this.mainThreadProcessor;
-
- Objects.requireNonNull(completablefuture);
- if (!completablefuture.isDone()) { // Paper
- com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads
- this.level.timings.syncChunkLoad.startTiming(); // Paper
- chunkproviderserver_b.managedBlock(completablefuture::isDone);
- this.level.timings.syncChunkLoad.stopTiming(); // Paper
- } // Paper
- ChunkResult<ChunkAccess> chunkresult = (ChunkResult) completablefuture.join();
- ChunkAccess ichunkaccess1 = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error
-
- if (ichunkaccess1 == null && create) {
- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("Chunk not there when requested: " + chunkresult.getError()));
- } else {
- this.storeInCache(k, ichunkaccess1, leastStatus);
- return ichunkaccess1;
- }
+ return create ? this.getChunkFallback(x, z, leastStatus, create) : null;
}
+
+ return this.getChunkFallback(x, z, leastStatus, create);
+ // Paper end - rewrite chunk system
}
@Nullable
@Override
public LevelChunk getChunkNow(int chunkX, int chunkZ) {
- if (Thread.currentThread() != this.mainThread) {
- return null;
- } else {
- return this.getChunkAtIfLoadedMainThread(chunkX, chunkZ); // Paper - Perf: Optimise getChunkAt calls for loaded chunks
- }
+ return this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); // Paper - rewrite chunk system
}
private void clearCache() {
@@ -261,56 +273,59 @@ public class ServerChunkCache extends ChunkSource {
}
private CompletableFuture<ChunkResult<ChunkAccess>> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
- ChunkPos chunkcoordintpair = new ChunkPos(chunkX, chunkZ);
- long k = chunkcoordintpair.toLong();
- int l = ChunkLevel.byStatus(leastStatus);
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k);
+ // Paper start - rewrite chunk system
+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Scheduling chunk load off-main");
- // CraftBukkit start - don't add new ticket for currently unloading chunk
- boolean currentlyUnloading = false;
- if (playerchunk != null) {
- FullChunkStatus oldChunkState = ChunkLevel.fullStatus(playerchunk.oldTicketLevel);
- FullChunkStatus currentChunkState = ChunkLevel.fullStatus(playerchunk.getTicketLevel());
- currentlyUnloading = (oldChunkState.isOrAfter(FullChunkStatus.FULL) && !currentChunkState.isOrAfter(FullChunkStatus.FULL));
+ final int minLevel = ChunkLevel.byStatus(leastStatus);
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
+
+ final boolean needsFullScheduling = leastStatus == ChunkStatus.FULL && (chunkHolder == null || !chunkHolder.getChunkStatus().isOrAfter(FullChunkStatus.FULL));
+
+ if ((chunkHolder == null || chunkHolder.getTicketLevel() > minLevel || needsFullScheduling) && !create) {
+ return ChunkHolder.UNLOADED_CHUNK_FUTURE;
}
- if (create && !currentlyUnloading) {
- // CraftBukkit end
- this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair);
- if (this.chunkAbsent(playerchunk, l)) {
- ProfilerFiller gameprofilerfiller = this.level.getProfiler();
-
- gameprofilerfiller.push("chunkLoad");
- this.runDistanceManagerUpdates();
- playerchunk = this.getVisibleChunkIfPresent(k);
- gameprofilerfiller.pop();
- if (this.chunkAbsent(playerchunk, l)) {
- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("No chunk holder after ticket has been added"));
+
+ final ChunkAccess ifPresent = chunkHolder == null ? null : chunkHolder.getChunkIfPresent(leastStatus);
+ if (needsFullScheduling || ifPresent == null) {
+ // schedule
+ final CompletableFuture<ChunkResult<ChunkAccess>> ret = new CompletableFuture<>();
+ final Consumer<ChunkAccess> complete = (ChunkAccess chunk) -> {
+ if (chunk == null) {
+ ret.complete(ChunkHolder.UNLOADED_CHUNK);
+ } else {
+ ret.complete(ChunkResult.of(chunk));
}
- }
- }
+ };
- return this.chunkAbsent(playerchunk, l) ? GenerationChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.scheduleChunkGenerationTask(leastStatus, this.chunkMap);
- }
+ ((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,
+ complete
+ );
- private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) {
- return holder == null || holder.oldTicketLevel > maxLevel; // CraftBukkit using oldTicketLevel for isLoaded checks
+ return ret;
+ } else {
+ // can return now
+ return CompletableFuture.completedFuture(ChunkResult.of(ifPresent));
+ }
+ // Paper end - rewrite chunk system
}
@Override
public boolean hasChunk(int x, int z) {
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent((new ChunkPos(x, z)).toLong());
- int k = ChunkLevel.byStatus(ChunkStatus.FULL);
-
- return !this.chunkAbsent(playerchunk, k);
+ return this.getChunkNow(x, z) != null; // Paper - rewrite chunk system
}
@Nullable
@Override
public LightChunk getChunkForLighting(int chunkX, int chunkZ) {
- long k = ChunkPos.asLong(chunkX, chunkZ);
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k);
-
- return playerchunk == null ? null : playerchunk.getChunkIfPresentUnchecked(ChunkStatus.INITIALIZE_LIGHT.getParent());
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
+ if (newChunkHolder == null) {
+ return null;
+ }
+ return newChunkHolder.getChunkIfPresentUnchecked(ChunkStatus.INITIALIZE_LIGHT.getParent());
+ // Paper end - rewrite chunk system
}
@Override
@@ -323,16 +338,7 @@ public class ServerChunkCache extends ChunkSource {
}
public boolean runDistanceManagerUpdates() { // Paper - public
- boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
- boolean flag1 = this.chunkMap.promoteChunkMap();
-
- this.chunkMap.runGenerationTasks();
- if (!flag && !flag1) {
- return false;
- } else {
- this.clearCache();
- return true;
- }
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); // Paper - rewrite chunk system
}
// Paper start
@@ -342,13 +348,14 @@ public class ServerChunkCache extends ChunkSource {
// Paper end
public boolean isPositionTicking(long pos) {
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
-
- return playerchunk == null ? false : (!this.level.shouldTickBlocksAt(pos) ? false : ((ChunkResult) playerchunk.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).isSuccess());
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos);
+ return newChunkHolder != null && newChunkHolder.isTickingReady();
+ // Paper end - rewrite chunk system
}
public void save(boolean flush) {
- this.runDistanceManagerUpdates();
+ // Paper - rewrite chunk system
try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings
this.chunkMap.saveAllChunks(flush);
} // Paper - Timings
@@ -361,16 +368,12 @@ public class ServerChunkCache extends ChunkSource {
}
public void close(boolean save) throws IOException {
- if (save) {
- this.save(true);
- }
- // CraftBukkit end
- this.lightEngine.close();
- this.chunkMap.close();
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.close(save, true); // Paper - rewrite chunk system
}
// CraftBukkit start - modelled on below
public void purgeUnload() {
+ if (true) return; // Paper - rewrite chunk system
this.level.getProfiler().push("purge");
this.distanceManager.purgeStaleTickets();
this.runDistanceManagerUpdates();
@@ -394,6 +397,7 @@ public class ServerChunkCache extends ChunkSource {
this.level.getProfiler().popPush("chunks");
if (tickChunks) {
this.level.timings.chunks.startTiming(); // Paper - timings
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().tick(); // Paper - rewrite chunk system
this.tickChunks();
this.level.timings.chunks.stopTiming(); // Paper - timings
this.chunkMap.tick();
@@ -408,6 +412,7 @@ public class ServerChunkCache extends ChunkSource {
}
private void tickChunks() {
+ long chunksTicked = 0; // Paper - rewrite chunk system
long i = this.level.getGameTime();
long j = i - this.lastInhabitedUpdate;
@@ -417,18 +422,29 @@ public class ServerChunkCache extends ChunkSource {
gameprofilerfiller.push("pollingChunks");
gameprofilerfiller.push("filteringLoadedChunks");
- List<ServerChunkCache.ChunkAndHolder> list = Lists.newArrayListWithCapacity(this.chunkMap.size());
- Iterator iterator = this.chunkMap.getChunks().iterator();
- if (this.level.getServer().tickRateManager().runsNormally()) this.level.timings.chunkTicks.startTiming(); // Paper
+ // Paper start - chunk tick iteration optimisations
+ List<ServerChunkCache.ChunkAndHolder> list;
+ {
+ final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> tickingChunks =
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel) this.level).moonrise$getTickingChunks();
- while (iterator.hasNext()) {
- ChunkHolder playerchunk = (ChunkHolder) iterator.next();
- LevelChunk chunk = playerchunk.getTickingChunk();
+ final ServerChunkCache.ChunkAndHolder[] raw = tickingChunks.getRawDataUnchecked();
+ final int size = tickingChunks.size();
- if (chunk != null) {
- list.add(new ServerChunkCache.ChunkAndHolder(chunk, playerchunk));
+ if (this.iterationCopy == null || this.iterationCopy.length < size) {
+ this.iterationCopy = new ServerChunkCache.ChunkAndHolder[raw.length];
}
+ System.arraycopy(raw, 0, this.iterationCopy, 0, size);
+
+ list = it.unimi.dsi.fastutil.objects.ObjectArrayList.wrap(
+ this.iterationCopy, size
+ );
}
+ // Paper end - chunk tick iteration optimisations
+ Iterator iterator = null; // Paper - chunk tick iteration optimisations
+ if (this.level.getServer().tickRateManager().runsNormally()) this.level.timings.chunkTicks.startTiming(); // Paper
+
+ // Paper - chunk tick iteration optimisations
if (this.level.tickRateManager().runsNormally()) {
gameprofilerfiller.popPush("naturalSpawnCount");
@@ -460,14 +476,19 @@ public class ServerChunkCache extends ChunkSource {
LevelChunk chunk1 = chunkproviderserver_a.chunk;
ChunkPos chunkcoordintpair = chunk1.getPos();
- if (this.level.isNaturalSpawningAllowed(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkcoordintpair)) {
+ if (true && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkcoordintpair)) { // Paper - rewrite chunk system
chunk1.incrementInhabitedTime(j);
if (flag && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkcoordintpair, true)) { // Spigot
NaturalSpawner.spawnForChunk(this.level, chunk1, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1);
}
- if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) {
+ if (true) { // Paper - rewrite chunk system
this.level.tickChunk(chunk1, l);
+ // Paper start - rewrite chunk system
+ if ((++chunksTicked & 7L) == 0L) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.level.getServer()).moonrise$executeMidTickTasks();
+ }
+ // Paper end - rewrite chunk system
}
}
}
@@ -482,22 +503,35 @@ public class ServerChunkCache extends ChunkSource {
}
gameprofilerfiller.popPush("broadcast");
- list.forEach((chunkproviderserver_a1) -> {
- this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing
- chunkproviderserver_a1.holder.broadcastChanges(chunkproviderserver_a1.chunk);
- this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing
- });
+ // Paper start - chunk tick iteration optimisations
+ this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing
+ {
+ final it.unimi.dsi.fastutil.objects.ObjectArrayList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> chunks = (it.unimi.dsi.fastutil.objects.ObjectArrayList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder>)list;
+ final ServerChunkCache.ChunkAndHolder[] raw = chunks.elements();
+ final int size = chunks.size();
+
+ Objects.checkFromToIndex(0, size, raw.length);
+ for (int idx = 0; idx < size; ++idx) {
+ final ServerChunkCache.ChunkAndHolder holder = raw[idx];
+ raw[idx] = null;
+
+ holder.holder().broadcastChanges(holder.chunk());
+ }
+ }
+ this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing
+ // Paper end - chunk tick iteration optimisations
gameprofilerfiller.pop();
gameprofilerfiller.pop();
}
}
private void getFullChunk(long pos, Consumer<LevelChunk> chunkConsumer) {
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
-
- if (playerchunk != null) {
- ((ChunkResult) playerchunk.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).ifSuccess(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));
+ if (fullChunk != null) {
+ chunkConsumer.accept(fullChunk);
}
+ // Paper end - rewrite chunk system
}
@@ -591,6 +625,12 @@ public class ServerChunkCache extends ChunkSource {
this.chunkMap.setServerViewDistance(watchDistance);
}
+ // Paper start - rewrite chunk system
+ public void setSendViewDistance(int viewDistance) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().setSendDistance(viewDistance);
+ }
+ // Paper end - rewrite chunk system
+
public void setSimulationDistance(int simulationDistance) {
this.distanceManager.updateSimulationDistance(simulationDistance);
}
@@ -669,21 +709,19 @@ public class ServerChunkCache extends ChunkSource {
@Override
// CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
public boolean pollTask() {
- try {
- if (ServerChunkCache.this.runDistanceManagerUpdates()) {
+ // Paper start - rewrite chunk system
+ final ServerChunkCache serverChunkCache = ServerChunkCache.this;
+ if (serverChunkCache.runDistanceManagerUpdates()) {
return true;
} else {
- ServerChunkCache.this.lightEngine.tryScheduleUpdate();
- return super.pollTask();
+ return super.pollTask() | ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)serverChunkCache.level).moonrise$getChunkTaskScheduler().executeMainThreadTask();
}
- } finally {
- ServerChunkCache.this.chunkMap.callbackExecutor.run();
- }
+ // Paper end - rewrite chunk system
// CraftBukkit end
}
}
- private static record ChunkAndHolder(LevelChunk chunk, ChunkHolder holder) {
+ public static record ChunkAndHolder(LevelChunk chunk, ChunkHolder holder) { // Paper - rewrite chunk system - public
}
}
diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java
index 1d849ce4e2c85f149af25318b8ffb6dcef6c6788..12d86f27d04bffed8c3844e36b42fbc2f84dacff 100644
--- a/src/main/java/net/minecraft/server/level/ServerEntity.java
+++ b/src/main/java/net/minecraft/server/level/ServerEntity.java
@@ -97,6 +97,11 @@ public class ServerEntity {
}
public void sendChanges() {
+ // Paper start - optimise collisions
+ if (((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)this.entity).moonrise$isHardColliding()) {
+ this.teleportDelay = 9999;
+ }
+ // Paper end - optimise collisions
List<Entity> list = this.entity.getPassengers();
if (!list.equals(this.lastPassengers)) {
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index c97292f22a3402dbd59cef4af554954dc1d4f91a..c759555241649cf5b355268066375341a54477f2 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -184,7 +184,7 @@ import org.bukkit.event.weather.LightningStrikeEvent;
import org.bukkit.event.world.TimeSkipEvent;
// CraftBukkit end
-public class ServerLevel extends Level implements WorldGenLevel {
+public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader { // Paper - rewrite chunk system
public static final BlockPos END_SPAWN_POINT = new BlockPos(100, 50, 0);
public static final IntProvider RAIN_DELAY = UniformInt.of(12000, 180000);
@@ -200,7 +200,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
public final PrimaryLevelData serverLevelData; // CraftBukkit - type
private int lastSpawnChunkRadius;
final EntityTickList entityTickList;
- public final PersistentEntitySectionManager<Entity> entityManager;
+ // Paper - rewrite chunk system
private final GameEventDispatcher gameEventDispatcher;
public boolean noSave;
private final SleepStatus sleepStatus;
@@ -339,6 +339,195 @@ public class ServerLevel extends Level implements WorldGenLevel {
return player != null && player.level() == this ? player : null;
}
// Paper end - optimise getPlayerByUUID
+ // Paper start - rewrite chunk system
+ private boolean markedClosing;
+ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder();
+ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader chunkLoader = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader((ServerLevel)(Object)this);
+ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController entityDataController;
+ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.PoiDataController poiDataController;
+ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController chunkDataController;
+ private final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler;
+ private long lastMidTickFailure;
+ private long tickedBlocksOrFluids;
+ private final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = new ca.spottedleaf.moonrise.common.misc.NearbyPlayers((ServerLevel)(Object)this);
+ private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDERS = new ServerChunkCache.ChunkAndHolder[0];
+ private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> loadedChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS);
+ private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> tickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS);
+ private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> entityTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS);
+
+ @Override
+ public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) {
+ return this.chunkSource.getChunkNow(chunkX, chunkZ);
+ }
+
+ @Override
+ public final ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) {
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ if (newChunkHolder == null) {
+ return null;
+ }
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion();
+ return lastCompletion == null ? null : lastCompletion.chunk();
+ }
+
+ @Override
+ public final ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus leastStatus) {
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
+ if (newChunkHolder == null) {
+ return null;
+ }
+ return newChunkHolder.getChunkIfPresentUnchecked(leastStatus);
+ }
+
+ @Override
+ public final void moonrise$midTickTasks() {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks();
+ }
+
+ @Override
+ public final ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus status) {
+ return this.moonrise$getChunkTaskScheduler().syncLoadNonFull(chunkX, chunkZ, status);
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler moonrise$getChunkTaskScheduler() {
+ return this.chunkTaskScheduler;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.ChunkDataController moonrise$getChunkDataController() {
+ return this.chunkDataController;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.ChunkDataController moonrise$getPoiChunkDataController() {
+ return this.poiDataController;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.ChunkDataController moonrise$getEntityChunkDataController() {
+ return this.entityDataController;
+ }
+
+ @Override
+ public final int moonrise$getRegionChunkShift() {
+ return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift();
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader() {
+ return this.chunkLoader;
+ }
+
+ @Override
+ public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
+ final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+ this.moonrise$loadChunksAsync(
+ (pos.getX() - radiusBlocks) >> 4,
+ (pos.getX() + radiusBlocks) >> 4,
+ (pos.getZ() - radiusBlocks) >> 4,
+ (pos.getZ() + radiusBlocks) >> 4,
+ priority, onLoad
+ );
+ }
+
+ @Override
+ public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
+ final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+ this.moonrise$loadChunksAsync(
+ (pos.getX() - radiusBlocks) >> 4,
+ (pos.getX() + radiusBlocks) >> 4,
+ (pos.getZ() - radiusBlocks) >> 4,
+ (pos.getZ() + radiusBlocks) >> 4,
+ chunkStatus, priority, onLoad
+ );
+ }
+
+ @Override
+ public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
+ final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+ this.moonrise$loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, priority, onLoad);
+ }
+
+ @Override
+ public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
+ final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = this.moonrise$getChunkTaskScheduler();
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager;
+
+ final int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
+ final java.util.concurrent.atomic.AtomicInteger loadedChunks = new java.util.concurrent.atomic.AtomicInteger();
+ final Long holderIdentifier = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getNextChunkLoadId();
+ final int ticketLevel = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getTicketLevel(chunkStatus);
+
+ final List<ChunkAccess> ret = new ArrayList<>(requiredChunks);
+
+ final java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (final ChunkAccess chunk) -> {
+ if (chunk != null) {
+ synchronized (ret) {
+ ret.add(chunk);
+ }
+ chunkHolderManager.addTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunk.getPos(), ticketLevel, holderIdentifier);
+ }
+ if (loadedChunks.incrementAndGet() == requiredChunks) {
+ try {
+ onLoad.accept(java.util.Collections.unmodifiableList(ret));
+ } finally {
+ for (int i = 0, len = ret.size(); i < len; ++i) {
+ final ChunkPos chunkPos = ret.get(i).getPos();
+
+ chunkHolderManager.removeTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunkPos, ticketLevel, holderIdentifier);
+ }
+ }
+ }
+ };
+
+ for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
+ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
+ chunkTaskScheduler.scheduleChunkLoad(cx, cz, chunkStatus, true, priority, consumer);
+ }
+ }
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() {
+ return this.viewDistanceHolder;
+ }
+
+ @Override
+ public final long moonrise$getLastMidTickFailure() {
+ return this.lastMidTickFailure;
+ }
+
+ @Override
+ public final void moonrise$setLastMidTickFailure(final long time) {
+ this.lastMidTickFailure = time;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.common.misc.NearbyPlayers moonrise$getNearbyPlayers() {
+ return this.nearbyPlayers;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getLoadedChunks() {
+ return this.loadedChunks;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getTickingChunks() {
+ return this.tickingChunks;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getEntityTickingChunks() {
+ return this.entityTickingChunks;
+ }
+ // Paper end - rewrite chunk system
// Add env and gen to constructor, IWorldDataServer -> WorldDataServer
public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
@@ -385,14 +574,13 @@ public class ServerLevel extends Level implements WorldGenLevel {
DataFixer datafixer = minecraftserver.getFixerUpper();
EntityPersistentStorage<Entity> entitypersistentstorage = new EntityStorage(new SimpleRegionStorage(new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"), convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, DataFixTypes.ENTITY_CHUNK), this, minecraftserver);
- this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage);
+ // Paper - rewrite chunk system
StructureTemplateManager structuretemplatemanager = minecraftserver.getStructureManager();
int j = this.spigotConfig.viewDistance; // Spigot
int k = this.spigotConfig.simulationDistance; // Spigot
- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager;
+ // Paper - rewrite chunk system
- Objects.requireNonNull(this.entityManager);
- this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, persistententitysectionmanager::updateChunkStatus, () -> {
+ this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, null, () -> { // Paper - rewrite chunk system
return minecraftserver.overworld().getDataStorage();
});
this.chunkSource.getGeneratorState().ensureStructuresGenerated();
@@ -420,6 +608,19 @@ public class ServerLevel extends Level implements WorldGenLevel {
this.randomSequences = (RandomSequences) Objects.requireNonNullElseGet(randomsequences, () -> {
return (RandomSequences) this.getDataStorage().computeIfAbsent(RandomSequences.factory(l), "random_sequences");
});
+ // Paper start - rewrite chunk system
+ this.entityDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController(
+ new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController.EntityRegionFileStorage(
+ new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"),
+ convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"),
+ minecraftserver.forceSynchronousWrites()
+ )
+ );
+ this.poiDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.PoiDataController((ServerLevel)(Object)this);
+ this.chunkDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController((ServerLevel)(Object)this);
+ this.moonrise$setEntityLookup(new ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup((ServerLevel)(Object)this, ((ServerLevel)(Object)this).new EntityCallbacks()));
+ this.chunkTaskScheduler = new ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler((ServerLevel)(Object)this, ca.spottedleaf.moonrise.common.util.MoonriseCommon.WORKER_POOL);
+ // Paper end - rewrite chunk system
this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit
}
@@ -553,7 +754,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
gameprofilerfiller.push("checkDespawn");
entity.checkDespawn();
gameprofilerfiller.pop();
- if (this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) {
+ if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - rewrite chunk system
Entity entity1 = entity.getVehicle();
if (entity1 != null) {
@@ -578,13 +779,16 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
gameprofilerfiller.push("entityManagement");
- this.entityManager.tick();
+ // Paper - rewrite chunk system
gameprofilerfiller.pop();
}
@Override
public boolean shouldTickBlocksAt(long chunkPos) {
- return this.chunkSource.chunkMap.getDistanceManager().inBlockTickingRange(chunkPos);
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos);
+ return holder != null && holder.isTickingReady();
+ // Paper end - rewrite chunk system
}
protected void tickTime() {
@@ -626,6 +830,63 @@ public class ServerLevel extends Level implements WorldGenLevel {
});
}
+ // Paper start - optimise random ticking
+ 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 ChunkPos cpos = chunk.getPos();
+ final int offsetX = cpos.x << 4;
+ final int offsetZ = cpos.z << 4;
+
+ for (int sectionIndex = 0, sectionsLen = sections.length; sectionIndex < sectionsLen; sectionIndex++) {
+ 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()) {
+ continue;
+ }
+
+ final ca.spottedleaf.moonrise.common.list.IBlockDataList tickList = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getTickingBlockList();
+ if (tickList.size() == 0) {
+ continue;
+ }
+
+ for (int i = 0; i < tickSpeed; ++i) {
+ final int tickingBlocks = tickList.size();
+ final int index = random.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));
+
+ // 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);
+
+ state.randomTick((ServerLevel)(Object)this, pos, random);
+ if (tickFluids) {
+ final FluidState fluidState = state.getFluidState();
+ if (fluidState.isRandomlyTicking()) {
+ fluidState.randomTick((ServerLevel)(Object)this, pos, random);
+ }
+ }
+ }
+ }
+
+ return;
+ }
+ // Paper end - optimise random ticking
+
public void tickChunk(LevelChunk chunk, int randomTickSpeed) {
ChunkPos chunkcoordintpair = chunk.getPos();
boolean flag = this.isRaining();
@@ -675,35 +936,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
gameprofilerfiller.popPush("tickBlocks");
timings.chunkTicksBlocks.startTiming(); // Paper
if (randomTickSpeed > 0) {
- LevelChunkSection[] achunksection = chunk.getSections();
-
- for (int i1 = 0; i1 < achunksection.length; ++i1) {
- LevelChunkSection chunksection = achunksection[i1];
-
- if (chunksection.isRandomlyTicking()) {
- int j1 = chunk.getSectionYFromSectionIndex(i1);
- int k1 = SectionPos.sectionToBlockCoord(j1);
-
- for (int l1 = 0; l1 < randomTickSpeed; ++l1) {
- BlockPos blockposition1 = this.getBlockRandomPos(j, k1, k, 15);
-
- gameprofilerfiller.push("randomTick");
- BlockState iblockdata = chunksection.getBlockState(blockposition1.getX() - j, blockposition1.getY() - k1, blockposition1.getZ() - k);
-
- if (iblockdata.isRandomlyTicking()) {
- iblockdata.randomTick(this, blockposition1, this.random);
- }
-
- FluidState fluid = iblockdata.getFluidState();
-
- if (fluid.isRandomlyTicking()) {
- fluid.randomTick(this, blockposition1, this.random);
- }
-
- gameprofilerfiller.pop();
- }
- }
- }
+ this.optimiseRandomTick(chunk, randomTickSpeed); // Paper - optimise random ticking
}
timings.chunkTicksBlocks.stopTiming(); // Paper
@@ -976,6 +1209,11 @@ public class ServerLevel extends Level implements WorldGenLevel {
if (fluid1.is(fluid)) {
fluid1.tick(this, pos);
}
+ // Paper start - rewrite chunk system
+ if ((++this.tickedBlocksOrFluids & 7L) != 0L) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks();
+ }
+ // Paper end - rewrite chunk system
}
@@ -985,6 +1223,11 @@ public class ServerLevel extends Level implements WorldGenLevel {
if (iblockdata.is(block)) {
iblockdata.tick(this, pos, this.random);
}
+ // Paper start - rewrite chunk system
+ if ((++this.tickedBlocksOrFluids & 7L) != 0L) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks();
+ }
+ // Paper end - rewrite chunk system
}
@@ -1061,6 +1304,11 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) {
+ // Paper start - add close param
+ this.save(progressListener, flush, savingDisabled, false);
+ }
+ public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled, boolean close) {
+ // Paper end - add close param
ServerChunkCache chunkproviderserver = this.getChunkSource();
if (!savingDisabled) {
@@ -1076,16 +1324,21 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
timings.worldSaveChunks.startTiming(); // Paper
- chunkproviderserver.save(flush);
+ if (!close) { chunkproviderserver.save(flush); } // Paper - add close param
timings.worldSaveChunks.stopTiming(); // Paper
}// Paper
- if (flush) {
- this.entityManager.saveAll();
- } else {
- this.entityManager.autoSave();
- }
+ // Paper - rewrite chunk system
}
+ // Paper start - add close param
+ if (close) {
+ try {
+ chunkproviderserver.close(!savingDisabled);
+ } catch (IOException never) {
+ throw new RuntimeException(never);
+ }
+ }
+ // Paper end - add close param
// CraftBukkit start - moved from MinecraftServer.saveChunks
ServerLevel worldserver1 = this;
@@ -1218,7 +1471,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
this.removePlayerImmediately((ServerPlayer) entity, Entity.RemovalReason.DISCARDED);
}
- this.entityManager.addNewEntity(player);
+ this.moonrise$getEntityLookup().addNewEntity(player); // Paper - rewrite chunk system
}
// CraftBukkit start
@@ -1249,7 +1502,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
// CraftBukkit end
- return this.entityManager.addNewEntity(entity);
+ return this.moonrise$getEntityLookup().addNewEntity(entity); // Paper - rewrite chunk system
}
}
@@ -1260,11 +1513,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
public boolean tryAddFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
// CraftBukkit end
- Stream<UUID> stream = entity.getSelfAndPassengers().map(Entity::getUUID); // CraftBukkit - decompile error
- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager;
-
- Objects.requireNonNull(this.entityManager);
- if (stream.anyMatch(persistententitysectionmanager::isLoaded)) {
+ if (entity.getSelfAndPassengers().map(Entity::getUUID).anyMatch(this.moonrise$getEntityLookup()::hasEntity)) { // Paper - rewrite chunk system
return false;
} else {
this.addFreshEntityWithPassengers(entity, reason); // CraftBukkit
@@ -1850,7 +2099,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
}
- bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityManager.gatherStats()));
+ bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.moonrise$getEntityLookup().getDebugInfo())); // Paper - rewrite chunk system
bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size()));
bufferedwriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count()));
bufferedwriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count()));
@@ -1899,7 +2148,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
BufferedWriter bufferedwriter2 = Files.newBufferedWriter(path1);
try {
- playerchunkmap.dumpChunks(bufferedwriter2);
+ //playerchunkmap.dumpChunks(bufferedwriter2); // Paper - rewrite chunk system
} catch (Throwable throwable4) {
if (bufferedwriter2 != null) {
try {
@@ -1920,7 +2169,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
BufferedWriter bufferedwriter3 = Files.newBufferedWriter(path2);
try {
- this.entityManager.dumpSections(bufferedwriter3);
+ //this.entityManager.dumpSections(bufferedwriter3); // Paper - rewrite chunk system
} catch (Throwable throwable6) {
if (bufferedwriter3 != null) {
try {
@@ -2062,7 +2311,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
@VisibleForTesting
public String getWatchdogStats() {
- return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.entityManager.gatherStats(), ServerLevel.getTypeCount(this.entityManager.getEntityGetter().getAll(), (entity) -> {
+ return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.moonrise$getEntityLookup().getDebugInfo(), ServerLevel.getTypeCount(this.moonrise$getEntityLookup().getAll(), (entity) -> { // Paper - rewrite chunk system
return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString();
}), this.blockEntityTickers.size(), ServerLevel.getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), this.getBlockTicks().count(), this.getFluidTicks().count(), this.gatherChunkSourceStats());
}
@@ -2092,15 +2341,25 @@ public class ServerLevel extends Level implements WorldGenLevel {
@Override
public LevelEntityGetter<Entity> getEntities() {
org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
- return this.entityManager.getEntityGetter();
+ return this.moonrise$getEntityLookup(); // Paper - rewrite chunk system
}
public void addLegacyChunkEntities(Stream<Entity> entities) {
- this.entityManager.addLegacyChunkEntities(entities);
+ // Paper start - add chunkpos param
+ this.addLegacyChunkEntities(entities, null);
+ }
+ public void addLegacyChunkEntities(Stream<Entity> entities, ChunkPos chunkPos) {
+ // Paper end - add chunkpos param
+ this.moonrise$getEntityLookup().addLegacyChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system
}
public void addWorldGenChunkEntities(Stream<Entity> entities) {
- this.entityManager.addWorldGenChunkEntities(entities);
+ // Paper start - add chunkpos param
+ this.addWorldGenChunkEntities(entities, null);
+ }
+ public void addWorldGenChunkEntities(Stream<Entity> entities, ChunkPos chunkPos) {
+ // Paper end - add chunkpos param
+ this.moonrise$getEntityLookup().addWorldGenChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system
}
public void startTickingChunk(LevelChunk chunk) {
@@ -2120,34 +2379,47 @@ public class ServerLevel extends Level implements WorldGenLevel {
@Override
public void close() throws IOException {
super.close();
- this.entityManager.close();
+ // Paper - rewrite chunk system
}
@Override
public String gatherChunkSourceStats() {
String s = this.chunkSource.gatherStats();
- return "Chunks[S] W: " + s + " E: " + this.entityManager.gatherStats();
+ return "Chunks[S] W: " + s + " E: " + this.moonrise$getEntityLookup().getDebugInfo(); // Paper - rewrite chunk system
}
public boolean areEntitiesLoaded(long chunkPos) {
- return this.entityManager.areEntitiesLoaded(chunkPos);
+ return this.moonrise$getAnyChunkIfLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)) != null; // Paper - rewrite chunk system
}
private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) {
- return this.areEntitiesLoaded(chunkPos) && this.chunkSource.isPositionTicking(chunkPos);
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos);
+ // isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded
+ return chunkHolder != null && chunkHolder.isTickingReady();
+ // Paper end - rewrite chunk system
}
public boolean isPositionEntityTicking(BlockPos pos) {
- return this.entityManager.canPositionTick(pos) && this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(ChunkPos.asLong(pos));
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos));
+ return chunkHolder != null && chunkHolder.isEntityTickingReady();
+ // Paper end - rewrite chunk system
}
public boolean isNaturalSpawningAllowed(BlockPos pos) {
- return this.entityManager.canPositionTick(pos);
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos));
+ return chunkHolder != null && chunkHolder.isEntityTickingReady();
+ // Paper end - rewrite chunk system
}
public boolean isNaturalSpawningAllowed(ChunkPos pos) {
- return this.entityManager.canPositionTick(pos);
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos));
+ return chunkHolder != null && chunkHolder.isEntityTickingReady();
+ // Paper end - rewrite chunk system
}
@Override
@@ -2173,7 +2445,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
CrashReportCategory crashreportsystemdetails = super.fillReportDetails(report);
crashreportsystemdetails.setDetail("Loaded entity count", () -> {
- return String.valueOf(this.entityManager.count());
+ return String.valueOf(this.moonrise$getEntityLookup().getEntityCount()); // Paper - rewrite chunk system
});
return crashreportsystemdetails;
}
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
index e45567e8112483d947e2ff12c01219b85b09b205..e05bd64abb644fa38dbfe1fe97737eaf8f535970 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
@@ -200,7 +200,7 @@ import org.bukkit.event.player.PlayerToggleSneakEvent;
import org.bukkit.inventory.MainHand;
// CraftBukkit end
-public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+public class ServerPlayer extends net.minecraft.world.entity.player.Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system
private static final Logger LOGGER = LogUtils.getLogger();
private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32;
@@ -297,6 +297,36 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
public @Nullable String clientBrandName = null; // Paper - Brand support
public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event
+ // Paper start - rewrite chunk system
+ private ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader;
+ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder();
+
+ @Override
+ public final boolean moonrise$isRealPlayer() {
+ return this.isRealPlayer;
+ }
+
+ @Override
+ public final void moonrise$setRealPlayer(final boolean real) {
+ this.isRealPlayer = real;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData moonrise$getChunkLoader() {
+ return this.chunkLoader;
+ }
+
+ @Override
+ public final void moonrise$setChunkLoader(final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData loader) {
+ this.chunkLoader = loader;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() {
+ return this.viewDistanceHolder;
+ }
+ // Paper end - rewrite chunk system
+
public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile);
this.chatVisibility = ChatVisiblity.FULL;
diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
index 63fae619e9b4ed49585f88ea7c167b0ee5efd859..cc779de06773451d51f54040fc899e4f45110bc1 100644
--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
@@ -23,15 +23,128 @@ import net.minecraft.world.level.chunk.LightChunkGetter;
import net.minecraft.world.level.lighting.LevelLightEngine;
import org.slf4j.Logger;
-public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable {
+public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable, ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider { // Paper - rewrite chunk system
public static final int DEFAULT_BATCH_SIZE = 1000;
private static final Logger LOGGER = LogUtils.getLogger();
- private final ProcessorMailbox<Runnable> taskMailbox;
- private final ObjectList<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> lightTasks = new ObjectArrayList<>();
+ // Paper - rewrite chunk sytem
private final ChunkMap chunkMap;
- private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> sorterMailbox;
+ // Paper - rewrite chunk sytem
private final int taskPerBatch = 1000;
- private final AtomicBoolean scheduled = new AtomicBoolean();
+ // Paper - rewrite chunk sytem
+
+ // Paper start - rewrite chunk system
+ private final java.util.concurrent.atomic.AtomicLong chunkWorkCounter = new java.util.concurrent.atomic.AtomicLong();
+ private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ,
+ final java.util.function.Supplier<ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.LightQueue.ChunkTasks> supplier) {
+ final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld();
+
+ final ChunkAccess center = this.starlight$getLightEngine().getAnyChunkNow(chunkX, chunkZ);
+ if (center == null || !center.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) {
+ // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing
+ // chunk scheduling, we could be lighting and generating a chunk at the same time
+ return;
+ }
+
+ final ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.ServerLightQueue.ServerChunkTasks scheduledTask = (ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.ServerLightQueue.ServerChunkTasks)supplier.get();
+
+ if (scheduledTask == null) {
+ // not scheduled
+ return;
+ }
+
+ if (!scheduledTask.markTicketAdded()) {
+ // ticket already added
+ return;
+ }
+
+ final Long ticketId = Long.valueOf(this.chunkWorkCounter.getAndIncrement());
+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
+ world.getChunkSource().addRegionTicket(ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId);
+
+ scheduledTask.queueOrRunTask(() -> {
+ world.getChunkSource().removeRegionTicket(ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId);
+ });
+ }
+
+ @Override
+ public final int starlight$serverRelightChunks(final java.util.Collection<net.minecraft.world.level.ChunkPos> chunks0,
+ final java.util.function.Consumer<net.minecraft.world.level.ChunkPos> chunkLightCallback,
+ final java.util.function.IntConsumer onComplete) {
+ final java.util.Set<net.minecraft.world.level.ChunkPos> chunks = new java.util.LinkedHashSet<>(chunks0);
+ final java.util.Map<net.minecraft.world.level.ChunkPos, Long> ticketIds = new java.util.HashMap<>();
+ final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld();
+
+ for (final java.util.Iterator<net.minecraft.world.level.ChunkPos> iterator = chunks.iterator(); iterator.hasNext();) {
+ final ChunkPos pos = iterator.next();
+
+ final Long id = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getNextChunkRelightId();
+ world.getChunkSource().addRegionTicket(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, id);
+ ticketIds.put(pos, id);
+
+ final ChunkAccess chunk = (ChunkAccess)world.getChunkSource().getChunkForLighting(pos.x, pos.z);
+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) {
+ // cannot relight this chunk
+ iterator.remove();
+ ticketIds.remove(pos);
+ world.getChunkSource().removeRegionTicket(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, id);
+ continue;
+ }
+ }
+
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().radiusAwareScheduler.queueInfiniteRadiusTask(() -> {
+ ThreadedLevelLightEngine.this.starlight$getLightEngine().relightChunks(
+ chunks,
+ (final ChunkPos pos) -> {
+ if (chunkLightCallback != null) {
+ chunkLightCallback.accept(pos);
+ }
+
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().scheduleChunkTask(pos.x, pos.z, () -> {
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(
+ pos.x, pos.z
+ );
+
+ if (chunkHolder == null) {
+ return;
+ }
+
+ final java.util.List<ServerPlayer> players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)chunkHolder.vanillaChunkHolder).moonrise$getPlayers(false);
+
+ if (players.isEmpty()) {
+ return;
+ }
+
+ final net.minecraft.network.protocol.Packet<?> relightPacket = new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(
+ pos, (ThreadedLevelLightEngine)(Object)ThreadedLevelLightEngine.this,
+ null, null
+ );
+
+ for (final ServerPlayer player : players) {
+ final net.minecraft.server.network.ServerGamePacketListenerImpl conn = player.connection;
+ if (conn != null) {
+ conn.send(relightPacket);
+ }
+ }
+ });
+ },
+ (final int relight) -> {
+ if (onComplete != null) {
+ onComplete.accept(relight);
+ }
+
+ for (final java.util.Map.Entry<ChunkPos, Long> entry : ticketIds.entrySet()) {
+ world.getChunkSource().removeRegionTicket(
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, entry.getKey(),
+ ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, entry.getValue()
+ );
+ }
+ }
+ );
+ });
+
+ return chunks.size();
+ }
+ // Paper end - rewrite chunk system
public ThreadedLevelLightEngine(
LightChunkGetter chunkProvider,
@@ -42,8 +155,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
) {
super(chunkProvider, true, hasBlockLight);
this.chunkMap = chunkLoadingManager;
- this.sorterMailbox = executor;
- this.taskMailbox = processor;
+ // Paper - rewrite chunk sytem
}
@Override
@@ -57,164 +169,73 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
@Override
public void checkBlock(BlockPos pos) {
- BlockPos blockPos = pos.immutable();
- this.addTask(
- SectionPos.blockToSectionCoord(pos.getX()),
- SectionPos.blockToSectionCoord(pos.getZ()),
- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
- Util.name(() -> super.checkBlock(blockPos), () -> "checkBlock " + blockPos)
- );
+ // Paper start - rewrite chunk system
+ final BlockPos posCopy = pos.immutable();
+ this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> {
+ return ThreadedLevelLightEngine.this.starlight$getLightEngine().blockChange(posCopy);
+ });
+ // Paper end - rewrite chunk system
}
protected void updateChunkStatus(ChunkPos pos) {
- this.addTask(pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
- super.retainData(pos, false);
- super.setLightEnabled(pos, false);
-
- for (int i = this.getMinLightSection(); i < this.getMaxLightSection(); i++) {
- super.queueSectionData(LightLayer.BLOCK, SectionPos.of(pos, i), null);
- super.queueSectionData(LightLayer.SKY, SectionPos.of(pos, i), null);
- }
-
- for (int j = this.levelHeightAccessor.getMinSection(); j < this.levelHeightAccessor.getMaxSection(); j++) {
- super.updateSectionStatus(SectionPos.of(pos, j), true);
- }
- }, () -> "updateChunkStatus " + pos + " true"));
+ // Paper - rewrite chunk system
}
@Override
public void updateSectionStatus(SectionPos pos, boolean notReady) {
- this.addTask(
- pos.x(),
- pos.z(),
- () -> 0,
- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
- Util.name(() -> super.updateSectionStatus(pos, notReady), () -> "updateSectionStatus " + pos + " " + notReady)
- );
+ // Paper start - rewrite chunk system
+ this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> {
+ return ThreadedLevelLightEngine.this.starlight$getLightEngine().sectionChange(pos, notReady);
+ });
+ // Paper end - rewrite chunk system
}
@Override
public void propagateLightSources(ChunkPos chunkPos) {
- this.addTask(
- chunkPos.x,
- chunkPos.z,
- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
- Util.name(() -> super.propagateLightSources(chunkPos), () -> "propagateLight " + chunkPos)
- );
+ // Paper - rewrite chunk system
}
@Override
public void setLightEnabled(ChunkPos pos, boolean retainData) {
- this.addTask(
- pos.x,
- pos.z,
- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
- Util.name(() -> super.setLightEnabled(pos, retainData), () -> "enableLight " + pos + " " + retainData)
- );
+ // Paper start - rewrite chunk system
}
@Override
public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) {
- this.addTask(
- pos.x(),
- pos.z(),
- () -> 0,
- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
- Util.name(() -> super.queueSectionData(lightType, pos, nibbles), () -> "queueData " + pos)
- );
+ // Paper start - rewrite chunk system
}
private void addTask(int x, int z, ThreadedLevelLightEngine.TaskType stage, Runnable task) {
- this.addTask(x, z, this.chunkMap.getChunkQueueLevel(ChunkPos.asLong(x, z)), stage, task);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void addTask(int x, int z, IntSupplier completedLevelSupplier, ThreadedLevelLightEngine.TaskType stage, Runnable task) {
- this.sorterMailbox.tell(ChunkTaskPriorityQueueSorter.message(() -> {
- this.lightTasks.add(Pair.of(stage, task));
- if (this.lightTasks.size() >= 1000) {
- this.runUpdate();
- }
- }, ChunkPos.asLong(x, z), completedLevelSupplier));
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Override
public void retainData(ChunkPos pos, boolean retainData) {
- this.addTask(
- pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> super.retainData(pos, retainData), () -> "retainData " + pos)
- );
+ // Paper start - rewrite chunk system
}
public CompletableFuture<ChunkAccess> initializeLight(ChunkAccess chunk, boolean bl) {
- ChunkPos chunkPos = chunk.getPos();
- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
- LevelChunkSection[] levelChunkSections = chunk.getSections();
-
- for (int i = 0; i < chunk.getSectionsCount(); i++) {
- LevelChunkSection levelChunkSection = levelChunkSections[i];
- if (!levelChunkSection.hasOnlyAir()) {
- int j = this.levelHeightAccessor.getSectionYFromSectionIndex(i);
- super.updateSectionStatus(SectionPos.of(chunkPos, j), false);
- }
- }
- }, () -> "initializeLight: " + chunkPos));
- return CompletableFuture.supplyAsync(() -> {
- super.setLightEnabled(chunkPos, bl);
- super.retainData(chunkPos, false);
- return chunk;
- }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task));
+ return CompletableFuture.completedFuture(chunk); // Paper start - rewrite chunk system
}
public CompletableFuture<ChunkAccess> lightChunk(ChunkAccess chunk, boolean excludeBlocks) {
- ChunkPos chunkPos = chunk.getPos();
- chunk.setLightCorrect(false);
- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
- if (!excludeBlocks) {
- super.propagateLightSources(chunkPos);
- }
- }, () -> "lightChunk " + chunkPos + " " + excludeBlocks));
- return CompletableFuture.supplyAsync(() -> {
- chunk.setLightCorrect(true);
- return chunk;
- }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task));
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void tryScheduleUpdate() {
- if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) {
- this.taskMailbox.tell(() -> {
- this.runUpdate();
- this.scheduled.set(false);
- });
- }
+ // Paper - rewrite chunk system
}
private void runUpdate() {
- int i = Math.min(this.lightTasks.size(), 1000);
- ObjectListIterator<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> objectListIterator = this.lightTasks.iterator();
-
- int j;
- for (j = 0; objectListIterator.hasNext() && j < i; j++) {
- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair = objectListIterator.next();
- if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.PRE_UPDATE) {
- pair.getSecond().run();
- }
- }
-
- objectListIterator.back(j);
- super.runLightUpdates();
-
- for (int var5 = 0; objectListIterator.hasNext() && var5 < i; var5++) {
- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair2 = objectListIterator.next();
- if (pair2.getFirst() == ThreadedLevelLightEngine.TaskType.POST_UPDATE) {
- pair2.getSecond().run();
- }
-
- objectListIterator.remove();
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public CompletableFuture<?> waitForPendingTasks(int x, int z) {
- return CompletableFuture.runAsync(() -> {
- }, callback -> this.addTask(x, z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, callback));
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
static enum TaskType {
diff --git a/src/main/java/net/minecraft/server/level/Ticket.java b/src/main/java/net/minecraft/server/level/Ticket.java
index eba83b085435150e5954fd5d41dda9ce1d0601ad..daf543b51d8875b374688957ae4bc466f5512bcd 100644
--- a/src/main/java/net/minecraft/server/level/Ticket.java
+++ b/src/main/java/net/minecraft/server/level/Ticket.java
@@ -2,13 +2,25 @@ package net.minecraft.server.level;
import java.util.Objects;
-public final class Ticket<T> implements Comparable<Ticket<?>> {
+public final class Ticket<T> implements Comparable<Ticket<?>>, ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket<T> { // Paper - rewrite chunk system
private final TicketType<T> type;
private final int ticketLevel;
public final T key;
- private long createdTick;
+ // Paper start - rewrite chunk system
+ private long removeDelay;
- protected Ticket(TicketType<T> type, int level, T argument) {
+ @Override
+ public final long moonrise$getRemoveDelay() {
+ return this.removeDelay;
+ }
+
+ @Override
+ public final void moonrise$setRemoveDelay(final long removeDelay) {
+ this.removeDelay = removeDelay;
+ }
+ // Paper end - rewerite chunk system
+
+ public Ticket(TicketType<T> type, int level, T argument) { // Paper - public
this.type = type;
this.ticketLevel = level;
this.key = argument;
@@ -41,7 +53,7 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
@Override
public String toString() {
- return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] at " + this.createdTick;
+ return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] to die in " + this.removeDelay; // Paper - rewrite chunk system
}
public TicketType<T> getType() {
@@ -53,11 +65,10 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
}
protected void setCreatedTick(long tickCreated) {
- this.createdTick = tickCreated;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
protected boolean timedOut(long currentTick) {
- long l = this.type.timeout();
- return l != 0L && currentTick - this.createdTick > l;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
}
diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
index b26a4a38144ec1b171db911bbf949b53ed35708f..5a8a33638ceb1d980ffc3e6dd86e7eb11dfd9375 100644
--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java
+++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
@@ -85,6 +85,36 @@ public class WorldGenRegion implements WorldGenLevel {
private final AtomicLong subTickCount = new AtomicLong();
private static final ResourceLocation WORLDGEN_REGION_RANDOM = ResourceLocation.withDefaultNamespace("worldgen_region_random");
+ // Paper start - rewrite chunk system
+ /**
+ * During feature generation, light data is not initialised and will always return 15 in Starlight. Vanilla
+ * can possibly return 0 if partially initialised, which allows some mushroom blocks to generate.
+ * In general, the brightness value from the light engine should not be used until the chunk is ready. To emulate
+ * Vanilla behavior better, we return 0 as the brightness during world gen unless the target chunk is finished
+ * lighting.
+ */
+ @Override
+ public int getBrightness(final net.minecraft.world.level.LightLayer lightLayer, final BlockPos blockPos) {
+ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4);
+ if (!chunk.isLightCorrect()) {
+ return 0;
+ }
+ return this.getLightEngine().getLayerListener(lightLayer).getLightValue(blockPos);
+ }
+
+ /**
+ * See above
+ */
+ @Override
+ public int getRawBrightness(final BlockPos blockPos, final int subtract) {
+ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4);
+ if (!chunk.isLightCorrect()) {
+ return 0;
+ }
+ return this.getLightEngine().getRawBrightness(blockPos, subtract);
+ }
+ // Paper end - rewrite chunk system
+
public WorldGenRegion(ServerLevel world, StaticCache2D<GenerationChunkHolder> chunks, ChunkStep generationStep, ChunkAccess centerPos) {
this.generatingStep = generationStep;
this.cache = chunks;
diff --git a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java
index cdd66e6ce96e2613afe7f06ca8da3cfaa6704b2d..32634e45ac8433648e49e47e20081e15ad41ff15 100644
--- a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java
+++ b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java
@@ -78,7 +78,7 @@ public class PlayerChunkSender {
}
}
- private static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) {
+ public static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) { // Paper - public
handler.send(new ClientboundLevelChunkWithLightPacket(chunk, world.getLightEngine(), null, null));
// Paper start - PlayerChunkLoadEvent
if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) {
diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/net/minecraft/util/BitStorage.java
index 68648c5a5e3ff079f832092af0f2f801c42d1ede..19661e106612b8e4e152085fb398db7bd06acc23 100644
--- a/src/main/java/net/minecraft/util/BitStorage.java
+++ b/src/main/java/net/minecraft/util/BitStorage.java
@@ -2,7 +2,7 @@ package net.minecraft.util;
import java.util.function.IntConsumer;
-public interface BitStorage {
+public interface BitStorage extends ca.spottedleaf.moonrise.patches.block_counting.BlockCountingBitStorage { // Paper - block counting
int getAndSet(int index, int value);
void set(int index, int value);
@@ -20,4 +20,22 @@ public interface BitStorage {
void unpack(int[] out);
BitStorage copy();
+
+ // 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<>();
+
+ 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 ret;
+ }
+ // Paper end - block counting
}
diff --git a/src/main/java/net/minecraft/util/SimpleBitStorage.java b/src/main/java/net/minecraft/util/SimpleBitStorage.java
index 9f438d9c6eb05e43d24e4af68188a3d4c46a938c..8acf2f2491a8d9d13392c5e89b2bd5c9918285e1 100644
--- a/src/main/java/net/minecraft/util/SimpleBitStorage.java
+++ b/src/main/java/net/minecraft/util/SimpleBitStorage.java
@@ -362,6 +362,40 @@ public class SimpleBitStorage implements BitStorage {
return new SimpleBitStorage(this.bits, this.size, (long[])this.data.clone());
}
+ // Paper start - block counting
+ @Override
+ public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> moonrise$countEntries() {
+ final int valuesPerLong = this.valuesPerLong;
+ final int bits = this.bits;
+ final long mask = this.mask;
+ 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;
+
+ ret.computeIfAbsent(paletteIdx, (final int key) -> {
+ return new it.unimi.dsi.fastutil.ints.IntArrayList();
+ }).add(index);
+
+ ++li;
+ ++index;
+ } while (li < valuesPerLong && index < size);
+ }
+
+ return ret;
+ }
+ // Paper end - block counting
+
public static class InitializationException extends RuntimeException {
InitializationException(String message) {
super(message);
diff --git a/src/main/java/net/minecraft/util/SortedArraySet.java b/src/main/java/net/minecraft/util/SortedArraySet.java
index ea72dcb064a35bc6245bc5c94d592efedd8faf41..87ee8e51dfa7657ed7d83fcbceef48bf857043e1 100644
--- a/src/main/java/net/minecraft/util/SortedArraySet.java
+++ b/src/main/java/net/minecraft/util/SortedArraySet.java
@@ -8,12 +8,89 @@ import java.util.Iterator;
import java.util.NoSuchElementException;
import javax.annotation.Nullable;
-public class SortedArraySet<T> extends AbstractSet<T> {
+public class SortedArraySet<T> extends AbstractSet<T> implements ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet<T> { // Paper - rewrite chunk system
private static final int DEFAULT_INITIAL_CAPACITY = 10;
private final Comparator<T> comparator;
T[] contents;
int size;
+ // Paper start - rewrite chunk system
+ @Override
+ public final boolean removeIf(final java.util.function.Predicate<? super T> filter) {
+ // prev. impl used an iterator, which could be n^2 and creates garbage
+ int i = 0;
+ final int len = this.size;
+ final T[] backingArray = this.contents;
+
+ for (;;) {
+ if (i >= len) {
+ return false;
+ }
+ if (!filter.test(backingArray[i])) {
+ ++i;
+ continue;
+ }
+ break;
+ }
+
+ // we only want to write back to backingArray if we really need to
+
+ int lastIndex = i; // this is where new elements are shifted to
+
+ for (; i < len; ++i) {
+ final T curr = backingArray[i];
+ if (!filter.test(curr)) { // if test throws we're screwed
+ backingArray[lastIndex++] = curr;
+ }
+ }
+
+ // cleanup end
+ Arrays.fill(backingArray, lastIndex, len, null);
+ this.size = lastIndex;
+ return true;
+ }
+
+ @Override
+ public final T moonrise$replace(final T object) {
+ final int index = this.findIndex(object);
+ if (index >= 0) {
+ final T old = this.contents[index];
+ this.contents[index] = object;
+ return old;
+ } else {
+ this.addInternal(object, getInsertionPosition(index));
+ return object;
+ }
+ }
+
+ @Override
+ public final T moonrise$removeAndGet(final T object) {
+ int i = this.findIndex(object);
+ if (i >= 0) {
+ final T ret = this.contents[i];
+ this.removeInternal(i);
+ return ret;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public final SortedArraySet<T> moonrise$copy() {
+ final SortedArraySet<T> ret = SortedArraySet.create(this.comparator, 0);
+
+ ret.size = this.size;
+ ret.contents = Arrays.copyOf(this.contents, this.size);
+
+ return ret;
+ }
+
+ @Override
+ public Object[] moonrise$copyBackingArray() {
+ return this.contents.clone();
+ }
+ // Paper end - rewrite chunk system
+
private SortedArraySet(int initialCapacity, Comparator<T> comparator) {
this.comparator = comparator;
if (initialCapacity < 0) {
diff --git a/src/main/java/net/minecraft/util/ZeroBitStorage.java b/src/main/java/net/minecraft/util/ZeroBitStorage.java
index 50040c497a819cd1229042ab3cb057d34a32cacc..15c5164d0ef41a978c16ee317fa73e97f2480207 100644
--- a/src/main/java/net/minecraft/util/ZeroBitStorage.java
+++ b/src/main/java/net/minecraft/util/ZeroBitStorage.java
@@ -62,4 +62,22 @@ public class ZeroBitStorage implements BitStorage {
public BitStorage copy() {
return this;
}
+
+ // Paper start - block counting
+ @Override
+ public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> moonrise$countEntries() {
+ final int size = this.size;
+
+ final int[] raw = new int[size];
+ for (int i = 0; i < size; ++i) {
+ raw[i] = i;
+ }
+
+ final it.unimi.dsi.fastutil.ints.IntArrayList coordinates = it.unimi.dsi.fastutil.ints.IntArrayList.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);
+ ret.put(0, coordinates);
+ return ret;
+ }
+ // Paper end - block counting
}
diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
index cf43f02aa705fdd71ebcdc9d028bacae73a11f42..9928c5f0814aa2206dfe7540a3fe2465b4446359 100644
--- a/src/main/java/net/minecraft/world/entity/Entity.java
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
@@ -167,7 +167,7 @@ import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.plugin.PluginManager;
// CraftBukkit end
-public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, CommandSource, ScoreHolder {
+public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, CommandSource, ScoreHolder, ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity, ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity { // Paper - rewrite chunk system // Paper - optimise entity tracker
// CraftBukkit start
private static final int CURRENT_LEVEL = 2;
@@ -455,6 +455,97 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
return this.dimensions.makeBoundingBox(x, y, z);
}
// Paper end
+ // Paper start - rewrite chunk system
+ private final boolean isHardColliding = this.moonrise$isHardCollidingUncached();
+ private net.minecraft.server.level.FullChunkStatus chunkStatus;
+ private int sectionX = Integer.MIN_VALUE;
+ private int sectionY = Integer.MIN_VALUE;
+ private int sectionZ = Integer.MIN_VALUE;
+ private boolean updatingSectionStatus;
+
+ @Override
+ public final boolean moonrise$isHardColliding() {
+ return this.isHardColliding;
+ }
+
+ @Override
+ public final net.minecraft.server.level.FullChunkStatus moonrise$getChunkStatus() {
+ return this.chunkStatus;
+ }
+
+ @Override
+ public final void moonrise$setChunkStatus(final net.minecraft.server.level.FullChunkStatus status) {
+ this.chunkStatus = status;
+ }
+
+ @Override
+ public final int moonrise$getSectionX() {
+ return this.sectionX;
+ }
+
+ @Override
+ public final void moonrise$setSectionX(final int x) {
+ this.sectionX = x;
+ }
+
+ @Override
+ public final int moonrise$getSectionY() {
+ return this.sectionY;
+ }
+
+ @Override
+ public final void moonrise$setSectionY(final int y) {
+ this.sectionY = y;
+ }
+
+ @Override
+ public final int moonrise$getSectionZ() {
+ return this.sectionZ;
+ }
+
+ @Override
+ public final void moonrise$setSectionZ(final int z) {
+ this.sectionZ = z;
+ }
+
+ @Override
+ public final boolean moonrise$isUpdatingSectionStatus() {
+ return this.updatingSectionStatus;
+ }
+
+ @Override
+ public final void moonrise$setUpdatingSectionStatus(final boolean to) {
+ this.updatingSectionStatus = to;
+ }
+
+ @Override
+ public final boolean moonrise$hasAnyPlayerPassengers() {
+ if (this.passengers.isEmpty()) {
+ return false;
+ }
+ return this.getIndirectPassengersStream().anyMatch((entity) -> entity instanceof Player);
+ }
+ // Paper end - rewrite chunk system
+ // Paper start - optimise entity tracker
+ private net.minecraft.server.level.ChunkMap.TrackedEntity trackedEntity;
+
+ @Override
+ public final net.minecraft.server.level.ChunkMap.TrackedEntity moonrise$getTrackedEntity() {
+ return this.trackedEntity;
+ }
+
+ @Override
+ public final void moonrise$setTrackedEntity(final net.minecraft.server.level.ChunkMap.TrackedEntity trackedEntity) {
+ this.trackedEntity = trackedEntity;
+ }
+
+ private static void collectIndirectPassengers(final List<Entity> into, final List<Entity> from) {
+ for (final Entity passenger : from) {
+ into.add(passenger);
+ collectIndirectPassengers(into, ((Entity)(Object)passenger).passengers);
+ }
+ }
+ // Paper end - optimise entity tracker
public Entity(EntityType<?> type, Level world) {
this.id = Entity.ENTITY_COUNTER.incrementAndGet();
@@ -1278,41 +1369,82 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
private Vec3 collide(Vec3 movement) {
- AABB axisalignedbb = this.getBoundingBox();
- List<VoxelShape> list = this.level().getEntityCollisions(this, axisalignedbb.expandTowards(movement));
- Vec3 vec3d1 = movement.lengthSqr() == 0.0D ? movement : Entity.collideBoundingBox(this, movement, axisalignedbb, this.level(), list);
- boolean flag = movement.x != vec3d1.x;
- boolean flag1 = movement.y != vec3d1.y;
- boolean flag2 = movement.z != vec3d1.z;
- boolean flag3 = flag1 && movement.y < 0.0D;
-
- if (this.maxUpStep() > 0.0F && (flag3 || this.onGround()) && (flag || flag2)) {
- AABB axisalignedbb1 = flag3 ? axisalignedbb.move(0.0D, vec3d1.y, 0.0D) : axisalignedbb;
- AABB axisalignedbb2 = axisalignedbb1.expandTowards(movement.x, (double) this.maxUpStep(), movement.z);
-
- if (!flag3) {
- axisalignedbb2 = axisalignedbb2.expandTowards(0.0D, -9.999999747378752E-6D, 0.0D);
- }
+ // Paper start - optimise collisions
+ final boolean xZero = movement.x == 0.0;
+ final boolean yZero = movement.y == 0.0;
+ final boolean zZero = movement.z == 0.0;
+ if (xZero & yZero & zZero) {
+ return movement;
+ }
- List<VoxelShape> list1 = Entity.collectColliders(this, this.level, list, axisalignedbb2);
- float f = (float) vec3d1.y;
- float[] afloat = Entity.collectCandidateStepUpHeights(axisalignedbb1, list1, this.maxUpStep(), f);
- float[] afloat1 = afloat;
- int i = afloat.length;
+ final Level world = this.level;
+ final AABB currBoundingBox = this.getBoundingBox();
- for (int j = 0; j < i; ++j) {
- float f1 = afloat1[j];
- Vec3 vec3d2 = Entity.collideWithShapes(new Vec3(movement.x, (double) f1, movement.z), axisalignedbb1, list1);
+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(currBoundingBox)) {
+ return movement;
+ }
- if (vec3d2.horizontalDistanceSqr() > vec3d1.horizontalDistanceSqr()) {
- double d0 = axisalignedbb.minY - axisalignedbb1.minY;
+ 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;
- return vec3d2.add(0.0D, -d0, 0.0D);
+ 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);
+ }
+ } 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);
}
}
- return vec3d1;
+ 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
+ );
+
+ if (potentialCollisionsVoxel.isEmpty() && potentialCollisionsBB.isEmpty()) {
+ return movement;
+ }
+
+ final Vec3 limitedMoveVector = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(movement, currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB);
+
+ 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);
+
+ 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);
+
+ if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) {
+ vec3d2 = vec3d4;
+ }
+ }
+
+ 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));
+ }
+
+ return limitedMoveVector;
+ } else {
+ return limitedMoveVector;
+ }
+ // Paper end - optimise collisions
}
private static float[] collectCandidateStepUpHeights(AABB collisionBox, List<VoxelShape> collisions, float f, float stepHeight) {
@@ -2628,18 +2760,75 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
public boolean isInWall() {
+ // Paper start - optimise collisions
if (this.noPhysics) {
return false;
- } else {
- float f = this.dimensions.width() * 0.8F;
- AABB axisalignedbb = AABB.ofSize(this.getEyePosition(), (double) f, 1.0E-6D, (double) f);
+ }
- return BlockPos.betweenClosedStream(axisalignedbb).anyMatch((blockposition) -> {
- BlockState iblockdata = this.level().getBlockState(blockposition);
+ final float reducedWith = this.dimensions.width() * 0.8F;
+ final AABB box = AABB.ofSize(this.getEyePosition(), reducedWith, 1.0E-6D, reducedWith);
+
+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(box)) {
+ return false;
+ }
- return !iblockdata.isAir() && iblockdata.isSuffocating(this.level(), blockposition) && Shapes.joinIsNotEmpty(iblockdata.getCollisionShape(this.level(), blockposition).move((double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ()), Shapes.create(axisalignedbb), BooleanOp.AND);
- });
+ final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos();
+
+ 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 net.minecraft.world.level.chunk.ChunkSource chunkProvider = this.level.getChunkSource();
+
+ 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 BlockState state = chunk.getBlockState(tempPos);
+
+ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)state).moonrise$emptyCollisionShape() || !state.isSuffocating(this.level, tempPos)) {
+ continue;
+ }
+
+ // Yes, it does not use the Entity context stuff.
+ final VoxelShape collisionShape = state.getCollisionShape(this.level, tempPos);
+
+ if (collisionShape.isEmpty()) {
+ continue;
+ }
+
+ final AABB toCollide = box.move(-(double)fx, -(double)fy, -(double)fz);
+
+ 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;
+ }
+ }
}
+
+ return false;
+ // Paper end - optimise collisions
}
public InteractionResult interact(Player player, InteractionHand hand) {
@@ -4024,14 +4213,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
public Iterable<Entity> getIndirectPassengers() {
- // Paper start - Optimize indirect passenger iteration
- if (this.passengers.isEmpty()) { return ImmutableList.of(); }
- ImmutableList.Builder<Entity> indirectPassengers = ImmutableList.builder();
- for (Entity passenger : this.passengers) {
- indirectPassengers.add(passenger);
- indirectPassengers.addAll(passenger.getIndirectPassengers());
+ // Paper start - optimise entity tracker
+ final List<Entity> ret = new ArrayList<>();
+
+ if (this.passengers.isEmpty()) {
+ return ret;
}
- return indirectPassengers.build();
+
+ collectIndirectPassengers(ret, this.passengers);
+
+ return ret;
+ // Paper end - optimise entity tracker
}
private Iterable<Entity> getIndirectPassengers_old() {
// Paper end - Optimize indirect passenger iteration
@@ -4396,6 +4588,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
this.setPosRaw(x, y, z, false);
}
public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) {
+ // Paper start - rewrite chunk system
+ if (this.updatingSectionStatus) {
+ LOGGER.error(
+ "Refusing to update position for entity " + this + " to position " + new Vec3(x, y, z)
+ + " since it is processing a section status update", new Throwable()
+ );
+ return;
+ }
+ // Paper end - rewrite chunk system
if (!checkPosition(this, x, y, z)) {
return;
}
@@ -4527,6 +4728,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
@Override
public final void setRemoved(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
+ // Paper start - rewrite chunk system
+ if (!((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this.level).moonrise$getEntityLookup().canRemoveEntity((Entity)(Object)this)) {
+ LOGGER.warn("Entity " + this + " is currently prevented from being removed from the world since it is processing section status updates", new Throwable());
+ return;
+ }
+ // Paper end - rewrite chunk system
CraftEventFactory.callEntityRemoveEvent(this, cause);
// CraftBukkit end
final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers
@@ -4538,7 +4745,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
this.stopRiding();
}
- this.getPassengers().forEach(Entity::stopRiding);
+ if (this.removalReason != Entity.RemovalReason.UNLOADED_TO_CHUNK) { this.getPassengers().forEach(Entity::stopRiding); } // Paper - rewrite chunk system
this.levelCallback.onRemove(entity_removalreason);
// Paper start - Folia schedulers
if (!(this instanceof ServerPlayer) && entity_removalreason != RemovalReason.CHANGED_DIMENSION && !alreadyRemoved) {
@@ -4569,7 +4776,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
@Override
public boolean shouldBeSaved() {
- return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !this.hasExactlyOnePlayerPassenger());
+ return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)this).moonrise$hasAnyPlayerPassengers()); // Paper - rewrite chunk system
}
@Override
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 fb63036d26d2b5370472b741b23bebd71e247463..d7a6eab60bf26916f78f858e224573560e581fef 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
@@ -38,12 +38,153 @@ import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
import net.minecraft.world.level.chunk.storage.SectionStorage;
import net.minecraft.world.level.chunk.storage.SimpleRegionStorage;
-public class PoiManager extends SectionStorage<PoiSection> {
+public class PoiManager extends SectionStorage<PoiSection> implements ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager { // Paper - rewrite chunk system
public static final int MAX_VILLAGE_DISTANCE = 6;
public static final int VILLAGE_SECTION_SIZE = 1;
private final PoiManager.DistanceTracker distanceTracker;
private final LongSet loadedChunks = new LongOpenHashSet();
+ // Paper start - rewrite chunk system
+ private final net.minecraft.server.level.ServerLevel world;
+
+ // the vanilla tracker needs to be replaced because it does not support level removes, and we need level removes
+ // to support poi unloading
+ private final ca.spottedleaf.moonrise.common.misc.Delayed26WayDistancePropagator3D villageDistanceTracker = new ca.spottedleaf.moonrise.common.misc.Delayed26WayDistancePropagator3D();
+
+ private static final int POI_DATA_SOURCE = 7;
+
+ private static int convertBetweenLevels(final int level) {
+ return POI_DATA_SOURCE - level;
+ }
+
+ private void updateDistanceTracking(long section) {
+ if (this.isVillageCenter(section)) {
+ this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE);
+ } else {
+ this.villageDistanceTracker.removeSource(section);
+ }
+ }
+
+ @Override
+ public Optional<PoiSection> get(final long pos) {
+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
+ final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos);
+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
+
+ 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);
+
+ return ret == null ? Optional.empty() : ret.getSectionForVanilla(chunkY);
+ }
+
+ @Override
+ public Optional<PoiSection> getOrLoad(final long pos) {
+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
+ final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos);
+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
+
+ 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;
+
+ if (chunkY >= ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world) && chunkY <= ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world)) {
+ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true);
+ if (ret != null) {
+ return ret.getSectionForVanilla(chunkY);
+ } else {
+ return manager.loadPoiChunk(chunkX, chunkZ).getSectionForVanilla(chunkY);
+ }
+ }
+ // retain vanilla behavior: do not load section if out of bounds!
+ return Optional.empty();
+ }
+
+ @Override
+ protected PoiSection getOrCreate(final long pos) {
+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
+ final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos);
+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
+
+ 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);
+ if (ret != null) {
+ return ret.getOrCreateSection(chunkY);
+ } else {
+ return manager.loadPoiChunk(chunkX, chunkZ).getOrCreateSection(chunkY);
+ }
+ }
+
+ @Override
+ public final net.minecraft.server.level.ServerLevel moonrise$getWorld() {
+ return this.world;
+ }
+
+ @Override
+ 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);
+ 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);
+ this.updateDistanceTracking(sectionPos);
+ }
+ }
+
+ @Override
+ 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;
+ 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) {
+ 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));
+ }
+ }
+ }
+
+ @Override
+ public final void moonrise$checkConsistency(final net.minecraft.world.level.chunk.ChunkAccess chunk) {
+ final int chunkX = chunk.getPos().x;
+ final int chunkZ = chunk.getPos().z;
+
+ final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(chunk);
+ final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(chunk);
+ final LevelChunkSection[] sections = chunk.getSections();
+ for (int section = minY; section <= maxY; ++section) {
+ this.checkConsistencyWithBlocks(SectionPos.of(chunkX, section, chunkZ), sections[section - minY]);
+ }
+ }
+
+ @Override
+ public final void moonrise$close() throws java.io.IOException {}
+
+ @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));
+ }
+
+ @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);
+ }
+ // Paper end - rewrite chunk system
+
public PoiManager(
RegionStorageInfo storageKey,
Path directory,
@@ -62,6 +203,7 @@ public class PoiManager extends SectionStorage<PoiSection> {
world
);
this.distanceTracker = new PoiManager.DistanceTracker();
+ this.world = (net.minecraft.server.level.ServerLevel)world; // Paper - rewrite chunk system
}
public void add(BlockPos pos, Holder<PoiType> type) {
@@ -195,8 +337,8 @@ public class PoiManager extends SectionStorage<PoiSection> {
}
public int sectionsToVillage(SectionPos pos) {
- this.distanceTracker.runAllUpdates();
- return this.distanceTracker.getLevel(pos.asLong());
+ this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system
+ return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(pos))); // Paper - rewrite chunk system
}
boolean isVillageCenter(long pos) {
@@ -210,19 +352,26 @@ public class PoiManager extends SectionStorage<PoiSection> {
@Override
public void tick(BooleanSupplier shouldKeepTicking) {
- super.tick(shouldKeepTicking);
- this.distanceTracker.runAllUpdates();
+ this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system
}
@Override
- protected void setDirty(long pos) {
- super.setDirty(pos);
- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
+ public void setDirty(long pos) { // Paper - public
+ // Paper start - rewrite chunk system
+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
+ 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 chunk = manager.getPoiChunkIfLoaded(chunkX, chunkZ, false);
+ if (chunk != null) {
+ chunk.setDirty(true);
+ }
+ this.updateDistanceTracking(pos);
+ // Paper end - rewrite chunk system
}
@Override
protected void onSectionLoad(long pos) {
- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
+ this.updateDistanceTracking(pos); // Paper - rewrite chunk system
}
public void checkConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection chunkSection) {
@@ -259,7 +408,7 @@ public class PoiManager extends SectionStorage<PoiSection> {
.map(sectionPos -> Pair.of(sectionPos, this.getOrLoad(sectionPos.asLong())))
.filter(pair -> !pair.getSecond().map(PoiSection::isValid).orElse(false))
.map(pair -> pair.getFirst().chunk())
- .filter(chunkPos -> this.loadedChunks.add(chunkPos.toLong()))
+ // Paper - rewrite chunk system
.forEach(chunkPos -> world.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.EMPTY));
}
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 971fb29a2c3dc713cb8ab1d2eed054cc16f9c93c..a6c0e89cb645693034f8e90ac2de8f2da457453c 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
@@ -23,7 +23,7 @@ import net.minecraft.core.SectionPos;
import net.minecraft.util.VisibleForDebug;
import org.slf4j.Logger;
-public class PoiSection {
+public class PoiSection implements ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection { // Paper - rewrite chunk system
private static final Logger LOGGER = LogUtils.getLogger();
private final Short2ObjectMap<PoiRecord> records = new Short2ObjectOpenHashMap<>();
private final Map<Holder<PoiType>, Set<PoiRecord>> byType = Maps.newHashMap();
@@ -42,6 +42,20 @@ public class PoiSection {
.orElseGet(Util.prefix("Failed to read POI section: ", LOGGER::error), () -> new PoiSection(updateListener, false, ImmutableList.of()));
}
+ // Paper start - rewrite chunk system
+ private final Optional<PoiSection> noAllocOptional = Optional.of((PoiSection)(Object)this);;
+
+ @Override
+ public final boolean moonrise$isEmpty() {
+ return this.isValid && this.records.isEmpty() && this.byType.isEmpty();
+ }
+
+ @Override
+ public final Optional<PoiSection> moonrise$asOptional() {
+ return this.noAllocOptional;
+ }
+ // Paper end - rewrite chunk system
+
public PoiSection(Runnable updateListener) {
this(updateListener, true, ImmutableList.of());
}
diff --git a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
index 2f398750bfee5758ad8b1367b6fc14364e4de776..c292f58ba4b29395484dbbf8591e455f449581d8 100644
--- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
+++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
@@ -359,7 +359,7 @@ public class ArmorStand extends LivingEntity {
@Override
protected void pushEntities() {
if (!this.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return; // Paper - Option to prevent armor stands from doing entity lookups
- List<Entity> list = this.level().getEntities((Entity) this, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS);
+ List<AbstractMinecart> list = this.level().getEntitiesOfClass(AbstractMinecart.class, this.getBoundingBox(), RIDABLE_MINECARTS); // Paper - optimise collisions
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
diff --git a/src/main/java/net/minecraft/world/level/ClipContext.java b/src/main/java/net/minecraft/world/level/ClipContext.java
index 3fa2964b979053ecbefc946c7fe76828de86d8f1..28bf0518f7d17099d7e4990defbeda6757b4477c 100644
--- a/src/main/java/net/minecraft/world/level/ClipContext.java
+++ b/src/main/java/net/minecraft/world/level/ClipContext.java
@@ -18,7 +18,7 @@ public class ClipContext {
private final Vec3 from;
private final Vec3 to;
private final ClipContext.Block block;
- private final ClipContext.Fluid fluid;
+ public final ClipContext.Fluid fluid; // Paper - optimise collisions - public
private final CollisionContext collisionContext;
public ClipContext(Vec3 start, Vec3 end, ClipContext.Block shapeType, ClipContext.Fluid fluidHandling, Entity entity) {
diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java
index bd20bea7f76a7307f1698fb2dfef37125032d166..141b748abe80402731cdaf14a3d36aa7cef4f4bd 100644
--- a/src/main/java/net/minecraft/world/level/EntityGetter.java
+++ b/src/main/java/net/minecraft/world/level/EntityGetter.java
@@ -18,7 +18,7 @@ import net.minecraft.world.phys.shapes.BooleanOp;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
-public interface EntityGetter {
+public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter { // Paper - rewrite chunk system
List<Entity> getEntities(@Nullable Entity except, AABB box, Predicate<? super Entity> predicate);
<T extends Entity> List<T> getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate);
@@ -33,21 +33,44 @@ public interface EntityGetter {
return this.getEntities(except, box, EntitySelector.NO_SPECTATORS);
}
- default boolean isUnobstructed(@Nullable Entity except, VoxelShape shape) {
- if (shape.isEmpty()) {
- return true;
- } else {
- for (Entity entity : this.getEntities(except, shape.bounds())) {
- if (!entity.isRemoved()
- && entity.blocksBuilding
- && (except == null || !entity.isPassengerOfSameVehicle(except))
- && Shapes.joinIsNotEmpty(shape, Shapes.create(entity.getBoundingBox()), BooleanOp.AND)) {
- return false;
+ // Paper start - rewrite chunk system
+ @Override
+ default List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate) {
+ return this.getEntities(entity, box, predicate);
+ }
+ // Paper end - rewrite chunk system
+
+ // Paper start - optimise collisions
+ default boolean isUnobstructed(@Nullable Entity entity, VoxelShape voxel) {
+ if (voxel.isEmpty()) {
+ return false;
+ }
+
+ final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation();
+ final List<Entity> entities = this.getEntities(
+ entity,
+ singleAABB == null ? voxel.bounds() : singleAABB.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)
+ );
+
+ for (int i = 0, len = entities.size(); i < len; ++i) {
+ final Entity otherEntity = entities.get(i);
+
+ if (otherEntity.isRemoved() || !otherEntity.blocksBuilding || (entity != null && otherEntity.isPassengerOfSameVehicle(entity))) {
+ continue;
+ }
+
+ if (singleAABB == null) {
+ final AABB entityBB = otherEntity.getBoundingBox();
+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(entityBB) || !ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(voxel, entityBB)) {
+ continue;
}
}
- return true;
+ return false;
}
+
+ return true;
+ // Paper end - optimise collisions
}
default <T extends Entity> List<T> getEntitiesOfClass(Class<T> entityClass, AABB box) {
@@ -55,23 +78,41 @@ public interface EntityGetter {
}
default List<VoxelShape> getEntityCollisions(@Nullable Entity entity, AABB box) {
- if (box.getSize() < 1.0E-7) {
- return List.of();
+ // Paper start - optimise collisions
+ // first behavior change is to correctly check for empty AABB
+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(box)) {
+ // reduce indirection by always returning type with same class
+ return new java.util.ArrayList<>();
+ }
+
+ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with.
+ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems
+ // specifically with boat collisions.
+ box = box.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON);
+
+ final List<Entity> entities;
+ if (entity != null && ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$isHardColliding()) {
+ entities = this.getEntities(entity, box, null);
} else {
- Predicate<Entity> predicate = entity == null ? EntitySelector.CAN_BE_COLLIDED_WITH : EntitySelector.NO_SPECTATORS.and(entity::canCollideWith);
- List<Entity> list = this.getEntities(entity, box.inflate(1.0E-7), predicate);
- if (list.isEmpty()) {
- return List.of();
- } else {
- Builder<VoxelShape> builder = ImmutableList.builderWithExpectedSize(list.size());
-
- for (Entity entity2 : list) {
- builder.add(Shapes.create(entity2.getBoundingBox()));
- }
+ entities = ((ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter)this).moonrise$getHardCollidingEntities(entity, box, null);
+ }
- return builder.build();
+ final List<VoxelShape> ret = new java.util.ArrayList<>(Math.min(25, entities.size()));
+
+ for (int i = 0, len = entities.size(); i < len; ++i) {
+ final Entity otherEntity = entities.get(i);
+
+ if (otherEntity.isSpectator()) {
+ continue;
+ }
+
+ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) {
+ ret.add(Shapes.create(otherEntity.getBoundingBox()));
}
}
+
+ return ret;
+ // Paper end - optimise collisions
}
// Paper start - Affects Spawning API
diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java
index 32651ed15e5961a8b27fc0dc8fb54ef05b6064fe..6c66ee69beb55e5c5755bbf4d13c256541ce4468 100644
--- a/src/main/java/net/minecraft/world/level/Explosion.java
+++ b/src/main/java/net/minecraft/world/level/Explosion.java
@@ -75,6 +75,247 @@ public class Explosion {
public float yield;
// CraftBukkit end
+ // Paper start - optimise collisions
+ private static final double[] CACHED_RAYS;
+ static {
+ final it.unimi.dsi.fastutil.doubles.DoubleArrayList rayCoords = new it.unimi.dsi.fastutil.doubles.DoubleArrayList();
+
+ for (int x = 0; x <= 15; ++x) {
+ for (int y = 0; y <= 15; ++y) {
+ for (int z = 0; z <= 15; ++z) {
+ if ((x == 0 || x == 15) || (y == 0 || y == 15) || (z == 0 || z == 15)) {
+ double xDir = (double)((float)x / 15.0F * 2.0F - 1.0F);
+ double yDir = (double)((float)y / 15.0F * 2.0F - 1.0F);
+ double zDir = (double)((float)z / 15.0F * 2.0F - 1.0F);
+
+ double mag = Math.sqrt(
+ xDir * xDir + yDir * yDir + zDir * zDir
+ );
+
+ rayCoords.add((xDir / mag) * (double)0.3F);
+ rayCoords.add((yDir / mag) * (double)0.3F);
+ rayCoords.add((zDir / mag) * (double)0.3F);
+ }
+ }
+ }
+ }
+
+ CACHED_RAYS = rayCoords.toDoubleArray();
+ }
+
+ private static final int CHUNK_CACHE_SHIFT = 2;
+ private static final int CHUNK_CACHE_MASK = (1 << CHUNK_CACHE_SHIFT) - 1;
+ private static final int CHUNK_CACHE_WIDTH = 1 << CHUNK_CACHE_SHIFT;
+
+ private static final int BLOCK_EXPLOSION_CACHE_SHIFT = 3;
+ private static final int BLOCK_EXPLOSION_CACHE_MASK = (1 << BLOCK_EXPLOSION_CACHE_SHIFT) - 1;
+ private static final int BLOCK_EXPLOSION_CACHE_WIDTH = 1 << BLOCK_EXPLOSION_CACHE_SHIFT;
+
+ // resistance = (res + 0.3F) * 0.3F;
+ // so for resistance = 0, we need res = -0.3F
+ private static final Float ZERO_RESISTANCE = Float.valueOf(-0.3f);
+ private it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache> blockCache = null;
+ private long[] chunkPosCache = null;
+ private net.minecraft.world.level.chunk.LevelChunk[] chunkCache = null;
+ private ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache getOrCacheExplosionBlock(final int x, final int y, final int z,
+ final long key, final boolean calculateResistance) {
+ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache ret = this.blockCache.get(key);
+ if (ret != null) {
+ return ret;
+ }
+
+ BlockPos pos = new BlockPos(x, y, z);
+
+ if (!this.level.isInWorldBounds(pos)) {
+ ret = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache(key, pos, null, null, 0.0f, true);
+ } else {
+ net.minecraft.world.level.chunk.LevelChunk chunk;
+ long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x >> 4, z >> 4);
+ int chunkCacheKey = ((x >> 4) & CHUNK_CACHE_MASK) | (((z >> 4) << CHUNK_CACHE_SHIFT) & (CHUNK_CACHE_MASK << CHUNK_CACHE_SHIFT));
+ if (this.chunkPosCache[chunkCacheKey] == chunkKey) {
+ chunk = this.chunkCache[chunkCacheKey];
+ } else {
+ this.chunkPosCache[chunkCacheKey] = chunkKey;
+ this.chunkCache[chunkCacheKey] = chunk = this.level.getChunk(x >> 4, z >> 4);
+ }
+
+ BlockState blockState = ((ca.spottedleaf.moonrise.patches.chunk_getblock.GetBlockChunk)chunk).moonrise$getBlock(x, y, z);
+ FluidState fluidState = blockState.getFluidState();
+
+ Optional<Float> resistance = !calculateResistance ? Optional.empty() : this.damageCalculator.getBlockExplosionResistance((Explosion)(Object)this, this.level, pos, blockState, fluidState);
+
+ ret = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache(
+ key, pos, blockState, fluidState,
+ (resistance.orElse(ZERO_RESISTANCE).floatValue() + 0.3f) * 0.3f,
+ false
+ );
+ }
+
+ this.blockCache.put(key, ret);
+
+ return ret;
+ }
+
+ private boolean clipsAnything(final Vec3 from, final Vec3 to,
+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext context,
+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache,
+ final BlockPos.MutableBlockPos currPos) {
+ // assume that context.delegated = false
+ final double adjX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.x - to.x);
+ final double adjY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.y - to.y);
+ final double adjZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.z - to.z);
+
+ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) {
+ return false;
+ }
+
+ final double toXAdj = to.x - adjX;
+ final double toYAdj = to.y - adjY;
+ final double toZAdj = to.z - adjZ;
+ final double fromXAdj = from.x + adjX;
+ final double fromYAdj = from.y + adjY;
+ final double fromZAdj = from.z + adjZ;
+
+ int currX = Mth.floor(fromXAdj);
+ int currY = Mth.floor(fromYAdj);
+ int currZ = Mth.floor(fromZAdj);
+
+ final double diffX = toXAdj - fromXAdj;
+ final double diffY = toYAdj - fromYAdj;
+ final double diffZ = toZAdj - fromZAdj;
+
+ final double dxDouble = Math.signum(diffX);
+ final double dyDouble = Math.signum(diffY);
+ final double dzDouble = Math.signum(diffZ);
+
+ final int dx = (int)dxDouble;
+ final int dy = (int)dyDouble;
+ final int dz = (int)dzDouble;
+
+ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX;
+ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY;
+ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ;
+
+ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj));
+ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj));
+ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj));
+
+ for (;;) {
+ currPos.set(currX, currY, currZ);
+
+ // ClipContext.Block.COLLIDER -> BlockBehaviour.BlockStateBase::getCollisionShape
+ // ClipContext.Fluid.NONE -> ignore fluids
+
+ // read block from cache
+ final long key = BlockPos.asLong(currX, currY, currZ);
+
+ final int cacheKey =
+ (currX & BLOCK_EXPLOSION_CACHE_MASK) |
+ (currY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) |
+ (currZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT);
+ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache cachedBlock = blockCache[cacheKey];
+ if (cachedBlock == null || cachedBlock.key != key) {
+ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(currX, currY, currZ, key, false);
+ }
+
+ final BlockState blockState = cachedBlock.blockState;
+ if (blockState != null && !((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$emptyCollisionShape()) {
+ net.minecraft.world.phys.shapes.VoxelShape collision = cachedBlock.cachedCollisionShape;
+ if (collision == null) {
+ collision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$getConstantCollisionShape();
+ if (collision == null) {
+ collision = blockState.getCollisionShape(this.level, currPos, context);
+ if (!context.isDelegated()) {
+ // if it was not delegated during this call, assume that for any future ones it will not be delegated
+ // again, and cache the result
+ cachedBlock.cachedCollisionShape = collision;
+ }
+ } else {
+ cachedBlock.cachedCollisionShape = collision;
+ }
+ }
+
+ if (!collision.isEmpty() && collision.clip(from, to, currPos) != null) {
+ return true;
+ }
+ }
+
+ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) {
+ return false;
+ }
+
+ // inc the smallest normalized coordinate
+
+ if (normalizedCurrX < normalizedCurrY) {
+ if (normalizedCurrX < normalizedCurrZ) {
+ currX += dx;
+ normalizedCurrX += normalizedDiffX;
+ } else {
+ // x < y && x >= z <--> z < y && z <= x
+ currZ += dz;
+ normalizedCurrZ += normalizedDiffZ;
+ }
+ } else if (normalizedCurrY < normalizedCurrZ) {
+ // y <= x && y < z
+ currY += dy;
+ normalizedCurrY += normalizedDiffY;
+ } else {
+ // y <= x && z <= y <--> z <= y && z <= x
+ currZ += dz;
+ normalizedCurrZ += normalizedDiffZ;
+ }
+ }
+ }
+
+ private float getSeenFraction(final Vec3 source, final Entity target,
+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache,
+ final BlockPos.MutableBlockPos blockPos) {
+ final AABB boundingBox = target.getBoundingBox();
+ final double diffX = boundingBox.maxX - boundingBox.minX;
+ final double diffY = boundingBox.maxY - boundingBox.minY;
+ final double diffZ = boundingBox.maxZ - boundingBox.minZ;
+
+ final double incX = 1.0 / (diffX * 2.0 + 1.0);
+ final double incY = 1.0 / (diffY * 2.0 + 1.0);
+ final double incZ = 1.0 / (diffZ * 2.0 + 1.0);
+
+ if (incX < 0.0 || incY < 0.0 || incZ < 0.0) {
+ return 0.0f;
+ }
+
+ final double offX = (1.0 - Math.floor(1.0 / incX) * incX) * 0.5 + boundingBox.minX;
+ final double offY = boundingBox.minY;
+ final double offZ = (1.0 - Math.floor(1.0 / incZ) * incZ) * 0.5 + boundingBox.minZ;
+
+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext context = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(target);
+
+ int totalRays = 0;
+ int missedRays = 0;
+
+ for (double dx = 0.0; dx <= 1.0; dx += incX) {
+ final double fromX = Math.fma(dx, diffX, offX);
+ for (double dy = 0.0; dy <= 1.0; dy += incY) {
+ final double fromY = Math.fma(dy, diffY, offY);
+ for (double dz = 0.0; dz <= 1.0; dz += incZ) {
+ ++totalRays;
+
+ final Vec3 from = new Vec3(
+ fromX,
+ fromY,
+ Math.fma(dz, diffZ, offZ)
+ );
+
+ if (!this.clipsAnything(from, source, context, blockCache, blockPos)) {
+ ++missedRays;
+ }
+ }
+ }
+ }
+
+ return (float)missedRays / (float)totalRays;
+ }
+ // Paper end - optimise collisions
+
public static DamageSource getDefaultDamageSource(Level world, @Nullable Entity source) {
return world.damageSources().explosion(source, Explosion.getIndirectSourceEntityInternal(source));
}
@@ -167,68 +408,107 @@ public class Explosion {
}
// CraftBukkit end
this.level.gameEvent(this.source, (Holder) GameEvent.EXPLODE, new Vec3(this.x, this.y, this.z));
- Set<BlockPos> set = Sets.newHashSet();
+
+ // Paper start - collision optimisations
+ this.blockCache = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>();
+
+ this.chunkPosCache = new long[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH];
+ java.util.Arrays.fill(this.chunkPosCache, ChunkPos.INVALID_CHUNK_POS);
+
+ this.chunkCache = new net.minecraft.world.level.chunk.LevelChunk[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH];
+
+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH];
+
+ // use initial cache value that is most likely to be used: the source position
+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache initialCache;
+ {
+ final int blockX = Mth.floor(this.x);
+ final int blockY = Mth.floor(this.y);
+ final int blockZ = Mth.floor(this.z);
+
+ final long key = BlockPos.asLong(blockX, blockY, blockZ);
+
+ initialCache = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true);
+ }
+ // Paper end - collision optimisations
+
boolean flag = true;
int i;
int j;
- for (int k = 0; k < 16; ++k) {
- for (i = 0; i < 16; ++i) {
- for (j = 0; j < 16; ++j) {
- if (k == 0 || k == 15 || i == 0 || i == 15 || j == 0 || j == 15) {
- double d0 = (double) ((float) k / 15.0F * 2.0F - 1.0F);
- double d1 = (double) ((float) i / 15.0F * 2.0F - 1.0F);
- double d2 = (double) ((float) j / 15.0F * 2.0F - 1.0F);
- double d3 = Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2);
-
- d0 /= d3;
- d1 /= d3;
- d2 /= d3;
- float f = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F);
- double d4 = this.x;
- double d5 = this.y;
- double d6 = this.z;
-
- for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) {
- BlockPos blockposition = BlockPos.containing(d4, d5, d6);
- BlockState iblockdata = this.level.getBlockState(blockposition);
- if (!iblockdata.isDestroyable()) continue; // Paper - Protect Bedrock and End Portal/Frames from being destroyed
- FluidState fluid = iblockdata.getFluidState(); // Paper - Perf: Optimize call to getFluid for explosions
-
- if (!this.level.isInWorldBounds(blockposition)) {
- break;
+ // Paper start - collision optimisations
+ for (int ray = 0, len = CACHED_RAYS.length; ray < len;) {
+ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache cachedBlock = initialCache;
+
+ double currX = this.x;
+ double currY = this.y;
+ double currZ = this.z;
+
+ final double incX = CACHED_RAYS[ray];
+ final double incY = CACHED_RAYS[ray + 1];
+ final double incZ = CACHED_RAYS[ray + 2];
+
+ ray += 3;
+ float power = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F);
+ do {
+ final int blockX = Mth.floor(currX);
+ final int blockY = Mth.floor(currY);
+ final int blockZ = Mth.floor(currZ);
+
+ final long key = BlockPos.asLong(blockX, blockY, blockZ);
+
+ if (cachedBlock.key != key) {
+ final int cacheKey =
+ (blockX & BLOCK_EXPLOSION_CACHE_MASK) |
+ (blockY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) |
+ (blockZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT);
+ cachedBlock = blockCache[cacheKey];
+ if (cachedBlock == null || cachedBlock.key != key) {
+ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true);
+ }
}
- Optional<Float> optional = this.damageCalculator.getBlockExplosionResistance(this, this.level, blockposition, iblockdata, fluid);
-
- if (optional.isPresent()) {
- f -= ((Float) optional.get() + 0.3F) * 0.3F;
+ if (cachedBlock.outOfWorld) {
+ break;
}
-
- if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockposition, iblockdata, f)) {
- set.add(blockposition);
- // Paper start - prevent headless pistons from forming
- if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) {
- net.minecraft.world.level.block.entity.BlockEntity extension = this.level.getBlockEntity(blockposition);
- if (extension instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity blockEntity && blockEntity.isSourcePiston()) {
- net.minecraft.core.Direction direction = iblockdata.getValue(net.minecraft.world.level.block.piston.PistonHeadBlock.FACING);
- set.add(blockposition.relative(direction.getOpposite()));
+ // Paper end - collision optimisations
+ BlockState iblockdata = cachedBlock.blockState; // Paper - optimise collisions
+ // Paper - collision optimisations
+
+ // Paper start - collision optimisations
+ power -= cachedBlock.resistance;
+
+ if (power > 0.0f && cachedBlock.shouldExplode == null && iblockdata.isDestroyable()) { // Paper - Protect Bedrock and End Portal/Frames from being destroyed
+ // note: we expect shouldBlockExplode to be pure with respect to power, as Vanilla currently is.
+ // basically, it is unused, which allows us to cache the result
+ final boolean shouldExplode = this.damageCalculator.shouldBlockExplode((Explosion)(Object)this, this.level, cachedBlock.immutablePos, cachedBlock.blockState, power);
+ cachedBlock.shouldExplode = shouldExplode ? Boolean.TRUE : Boolean.FALSE;
+ if (shouldExplode) {
+ if (this.fire || !cachedBlock.blockState.isAir()) {
+ this.toBlow.add(cachedBlock.immutablePos);
+ // Paper start - prevent headless pistons from forming
+ if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) {
+ net.minecraft.world.level.block.entity.BlockEntity extension = this.level.getBlockEntity(cachedBlock.immutablePos); // Paper - optimise collisions
+ if (extension instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity blockEntity && blockEntity.isSourcePiston()) {
+ net.minecraft.core.Direction direction = iblockdata.getValue(net.minecraft.world.level.block.piston.PistonHeadBlock.FACING);
+ this.toBlow.add(cachedBlock.immutablePos.relative(direction.getOpposite())); // Paper - optimise collisions
+ }
+ }
+ // Paper end - prevent headless pistons from forming
}
}
- // Paper end - prevent headless pistons from forming
}
- d4 += d0 * 0.30000001192092896D;
- d5 += d1 * 0.30000001192092896D;
- d6 += d2 * 0.30000001192092896D;
- }
+ power -= 0.22500001F;
+ currX += incX;
+ currY += incY;
+ currZ += incZ;
+ } while (power > 0.0f);
+ // Paper end - collision optimisations
}
- }
- }
- }
- this.toBlow.addAll(set);
+ // Paper - optimise collisions
float f2 = this.radius * 2.0F;
i = Mth.floor(this.x - (double) f2 - 1.0D);
@@ -241,6 +521,10 @@ public class Explosion {
Vec3 vec3d = new Vec3(this.x, this.y, this.z);
Iterator iterator = list.iterator();
+ // Paper start - optimise collisions
+ final BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos();
+ // Paper end - optimise collisions
+
while (iterator.hasNext()) {
Entity entity = (Entity) iterator.next();
@@ -257,6 +541,7 @@ public class Explosion {
d8 /= d11;
d9 /= d11;
d10 /= d11;
+ final double seenFraction; // Paper - optimise collisions
if (this.damageCalculator.shouldDamageEntity(this, entity)) {
// CraftBukkit start
@@ -272,6 +557,8 @@ public class Explosion {
entity.lastDamageCancelled = false;
+ seenFraction = (double)this.getBlockDensity(vec3d, entity, blockCache, blockPos); // Paper - optimise collisions
+
if (entity instanceof EnderDragon) {
for (EnderDragonPart entityComplexPart : ((EnderDragon) entity).subEntities) {
// Calculate damage separately for each EntityComplexPart
@@ -280,16 +567,21 @@ public class Explosion {
}
}
} else {
- entity.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity));
+ // Paper start - optimise collisions
+ // inline getEntityDamageAmount so that we can avoid double calling getSeenPercent, which is the MOST
+ // expensive part of this loop!!!!
+ final double factor = (1.0 - d7) * seenFraction;
+ entity.hurt(this.damageSource, (float)((factor * factor + factor) / 2.0 * 7.0 * (double)f2 + 1.0));
+ // Paper end - optimise collisions
}
if (entity.lastDamageCancelled) { // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Skip entity if damage event was cancelled
continue;
}
// CraftBukkit end
- }
+ } else { seenFraction = (double)this.getBlockDensity(vec3d, entity, blockCache, blockPos); } // Paper - optimise collisions
- double d12 = (1.0D - d7) * this.getBlockDensity(vec3d, entity) * (double) this.damageCalculator.getKnockbackMultiplier(entity); // Paper - Optimize explosions
+ double d12 = (1.0D - d7) * seenFraction * (double) this.damageCalculator.getKnockbackMultiplier(entity); // Paper - Optimize explosions // Paper - optimise collisions
double d13;
if (entity instanceof LivingEntity) {
@@ -327,7 +619,11 @@ public class Explosion {
}
}
}
-
+ // Paper start - optimise collisions
+ this.blockCache = null;
+ this.chunkPosCache = null;
+ this.chunkCache = null;
+ // Paper end - optimise collisions
}
public void finalizeExplosion(boolean particles) {
@@ -544,14 +840,14 @@ public class Explosion {
private BlockInteraction() {}
}
// Paper start - Optimize explosions
- private float getBlockDensity(Vec3 vec3d, Entity entity) {
+ private float getBlockDensity(Vec3 vec3d, Entity entity, ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache, BlockPos.MutableBlockPos blockPos) { // Paper - optimise collisions
if (!this.level.paperConfig().environment.optimizeExplosions) {
- return getSeenPercent(vec3d, entity);
+ return this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise collisions
}
CacheKey key = new CacheKey(this, entity.getBoundingBox());
Float blockDensity = this.level.explosionDensityCache.get(key);
if (blockDensity == null) {
- blockDensity = getSeenPercent(vec3d, entity);
+ blockDensity = this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise collisions
this.level.explosionDensityCache.put(key, blockDensity);
}
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
index e5abde76c354c3dd9940dd4e5ae3fe8b6a2b4680..347334130e99dbf938d570bd36440a96f92d475a 100644
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
@@ -81,6 +81,7 @@ import net.minecraft.world.level.storage.LevelData;
import net.minecraft.world.level.storage.WritableLevelData;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
+import net.minecraft.world.phys.shapes.VoxelShape;
import net.minecraft.world.scores.Scoreboard;
// CraftBukkit start
@@ -102,7 +103,7 @@ import org.bukkit.entity.SpawnCategory;
import org.bukkit.event.block.BlockPhysicsEvent;
// CraftBukkit end
-public abstract class Level implements LevelAccessor, AutoCloseable {
+public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel, ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter, ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel { // Paper - rewrite chunk system // Paper - optimise collisions
public static final Codec<ResourceKey<Level>> RESOURCE_KEY_CODEC = ResourceKey.codec(Registries.DIMENSION);
public static final ResourceKey<Level> OVERWORLD = ResourceKey.create(Registries.DIMENSION, ResourceLocation.withDefaultNamespace("overworld"));
@@ -199,6 +200,483 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
public abstract ResourceKey<LevelStem> getTypeKey();
+ // Paper start - rewrite chunk system
+ private ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup;
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup moonrise$getEntityLookup() {
+ return this.entityLookup;
+ }
+
+ @Override
+ public void moonrise$setEntityLookup(final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup) {
+ if (this.entityLookup != null && !(this.entityLookup instanceof ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl.DefaultEntityLookup)) {
+ throw new IllegalStateException("Entity lookup already initialised");
+ }
+ this.entityLookup = entityLookup;
+ }
+
+ @Override
+ public final <T extends Entity> List<T> getEntitiesOfClass(final Class<T> entityClass, final AABB boundingBox, final Predicate<? super T> predicate) {
+ this.getProfiler().incrementCounter("getEntities");
+ final List<T> ret = new java.util.ArrayList<>();
+
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(entityClass, null, boundingBox, ret, predicate);
+
+ return ret;
+ }
+
+ @Override
+ public final List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate) {
+ this.getProfiler().incrementCounter("getEntities");
+ final List<Entity> ret = new java.util.ArrayList<>();
+
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getHardCollidingEntities(entity, box, ret, predicate);
+
+ return ret;
+ }
+
+ @Override
+ public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) {
+ return this.getChunkSource().getChunk(chunkX, chunkZ, false);
+ }
+
+ @Override
+ public ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) {
+ return this.getChunkSource().getChunk(chunkX, chunkZ, ChunkStatus.EMPTY, false);
+ }
+
+ @Override
+ public ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus) {
+ return this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, false);
+ }
+
+ @Override
+ public void moonrise$midTickTasks() {
+ // no-op on ClientLevel
+ }
+ /**
+ * @reason Turn all getChunk(x, z, status) calls into virtual invokes, instead of interface invokes:
+ * 1. The interface invoke is expensive
+ * 2. The method makes other interface invokes (again, expensive)
+ * Instead, we just directly call getChunk(x, z, status, true) which avoids the interface invokes entirely.
+ * @author Spottedleaf
+ */
+ @Override
+ public ChunkAccess getChunk(final int x, final int z, final ChunkStatus status) {
+ return ((Level)(Object)this).getChunk(x, z, status, true);
+ }
+
+ @Override
+ public BlockPos getHeightmapPos(Heightmap.Types types, BlockPos blockPos) {
+ return new BlockPos(blockPos.getX(), this.getHeight(types, blockPos.getX(), blockPos.getZ()), blockPos.getZ());
+ }
+ // 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) {
+ final AABB boundingBox = entity.getBoundingBox();
+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(boundingBox)) {
+ return false;
+ }
+
+ final List<Entity> entities = this.getEntities(
+ entity,
+ boundingBox.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON),
+ null
+ );
+
+ for (int i = 0, len = entities.size(); i < len; ++i) {
+ final Entity otherEntity = entities.get(i);
+
+ if (otherEntity.isSpectator() || otherEntity.isRemoved() || !otherEntity.blocksBuilding || otherEntity.isPassengerOfSameVehicle(entity)) {
+ continue;
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+
+ private static net.minecraft.world.phys.BlockHitResult miss(final ClipContext clipContext) {
+ 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));
+ }
+
+ private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState();
+
+ private static net.minecraft.world.phys.BlockHitResult fastClip(final Vec3 from, final Vec3 to, final Level level,
+ final ClipContext clipContext) {
+ final double adjX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.x - to.x);
+ final double adjY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.y - to.y);
+ final double adjZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.z - to.z);
+
+ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) {
+ return miss(clipContext);
+ }
+
+ final double toXAdj = to.x - adjX;
+ final double toYAdj = to.y - adjY;
+ final double toZAdj = to.z - adjZ;
+ final double fromXAdj = from.x + adjX;
+ final double fromYAdj = from.y + adjY;
+ final double fromZAdj = from.z + adjZ;
+
+ int currX = Mth.floor(fromXAdj);
+ int currY = Mth.floor(fromYAdj);
+ int currZ = Mth.floor(fromZAdj);
+
+ final BlockPos.MutableBlockPos currPos = new BlockPos.MutableBlockPos();
+
+ final double diffX = toXAdj - fromXAdj;
+ final double diffY = toYAdj - fromYAdj;
+ final double diffZ = toZAdj - fromZAdj;
+
+ final double dxDouble = Math.signum(diffX);
+ final double dyDouble = Math.signum(diffY);
+ final double dzDouble = Math.signum(diffZ);
+
+ final int dx = (int)dxDouble;
+ final int dy = (int)dyDouble;
+ final int dz = (int)dzDouble;
+
+ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX;
+ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY;
+ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ;
+
+ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj));
+ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj));
+ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj));
+
+ net.minecraft.world.level.chunk.LevelChunkSection[] lastChunk = null;
+ net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> lastSection = null;
+ int lastChunkX = Integer.MIN_VALUE;
+ int lastChunkY = Integer.MIN_VALUE;
+ int lastChunkZ = Integer.MIN_VALUE;
+
+ final int minSection = ((ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel)level).moonrise$getMinSection();
+
+ for (;;) {
+ currPos.set(currX, currY, currZ);
+
+ final int newChunkX = currX >> 4;
+ final int newChunkY = currY >> 4;
+ final int newChunkZ = currZ >> 4;
+
+ final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ));
+ final int chunkYDiff = newChunkY ^ lastChunkY;
+
+ if ((chunkDiff | chunkYDiff) != 0) {
+ if (chunkDiff != 0) {
+ lastChunk = level.getChunk(newChunkX, newChunkZ).getSections();
+ }
+ final int sectionY = newChunkY - minSection;
+ lastSection = sectionY >= 0 && sectionY < lastChunk.length ? lastChunk[sectionY].states : null;
+
+ lastChunkX = newChunkX;
+ lastChunkY = newChunkY;
+ lastChunkZ = newChunkZ;
+ }
+
+ final BlockState blockState;
+ if (lastSection != null && !(blockState = lastSection.get((currX & 15) | ((currZ & 15) << 4) | ((currY & 15) << (4+4)))).isAir()) {
+ final VoxelShape blockCollision = clipContext.getBlockShape(blockState, level, currPos);
+
+ final net.minecraft.world.phys.BlockHitResult blockHit = blockCollision.isEmpty() ? null : level.clipWithInteractionOverride(from, to, currPos, blockCollision, blockState);
+
+ final VoxelShape fluidCollision;
+ final FluidState fluidState;
+ if (clipContext.fluid != ClipContext.Fluid.NONE && (fluidState = blockState.getFluidState()) != AIR_FLUIDSTATE) {
+ fluidCollision = clipContext.getFluidShape(fluidState, level, currPos);
+
+ final net.minecraft.world.phys.BlockHitResult fluidHit = fluidCollision.clip(from, to, currPos);
+
+ if (fluidHit != null) {
+ if (blockHit == null) {
+ return fluidHit;
+ }
+
+ return from.distanceToSqr(blockHit.getLocation()) <= from.distanceToSqr(fluidHit.getLocation()) ? blockHit : fluidHit;
+ }
+ }
+
+ if (blockHit != null) {
+ return blockHit;
+ }
+ } // else: usually fall here
+
+ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) {
+ return miss(clipContext);
+ }
+
+ // inc the smallest normalized coordinate
+
+ if (normalizedCurrX < normalizedCurrY) {
+ if (normalizedCurrX < normalizedCurrZ) {
+ currX += dx;
+ normalizedCurrX += normalizedDiffX;
+ } else {
+ // x < y && x >= z <--> z < y && z <= x
+ currZ += dz;
+ normalizedCurrZ += normalizedDiffZ;
+ }
+ } else if (normalizedCurrY < normalizedCurrZ) {
+ // y <= x && y < z
+ currY += dy;
+ normalizedCurrY += normalizedDiffY;
+ } else {
+ // y <= x && z <= y <--> z <= y && z <= x
+ currZ += dz;
+ normalizedCurrZ += normalizedDiffZ;
+ }
+ }
+ }
+
+ /**
+ * @reason Route to optimized call
+ * @author Spottedleaf
+ */
+ @Override
+ public final 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);
+ }
+
+ /**
+ * @reason Route to faster logic
+ * @author Spottedleaf
+ */
+ @Override
+ public final 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) -> {
+ return state.isSuffocating((Level)(Object)Level.this, pos);
+ }
+ );
+ }
+
+ private static VoxelShape inflateAABBToVoxel(final AABB aabb, final double x, final double y, final double z) {
+ return net.minecraft.world.phys.shapes.Shapes.create(
+ aabb.minX - x,
+ aabb.minY - y,
+ aabb.minZ - z,
+
+ aabb.maxX + x,
+ aabb.maxY + y,
+ aabb.maxZ + z
+ );
+ }
+
+ /**
+ * @reason Use optimised OR operator join strategy, avoid streams
+ * @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) {
+ if (boundsShape.isEmpty()) {
+ return java.util.Optional.empty();
+ }
+
+ final double expandByX = rangeX * 0.5;
+ final double expandByY = rangeY * 0.5;
+ final double expandByZ = rangeZ * 0.5;
+
+ // note: it is useless to look at shapes outside of range / 2.0
+ final AABB collectionVolume = boundsShape.bounds().inflate(expandByX, expandByY, expandByZ);
+
+ final List<AABB> aabbs = new java.util.ArrayList<>();
+ final List<VoxelShape> voxels = new java.util.ArrayList<>();
+
+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder(
+ (Level)(Object)this, entity, collectionVolume, voxels, aabbs,
+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER,
+ null
+ );
+
+ final WorldBorder worldBorder = this.getWorldBorder();
+ if (worldBorder != null) {
+ aabbs.removeIf((final AABB aabb) -> {
+ return !worldBorder.isWithinBounds(aabb);
+ });
+ voxels.removeIf((final VoxelShape shape) -> {
+ return !worldBorder.isWithinBounds(shape.bounds());
+ });
+ }
+
+ // push voxels into aabbs
+ for (int i = 0, len = voxels.size(); i < len; ++i) {
+ aabbs.addAll(voxels.get(i).toAabbs());
+ }
+
+ // expand AABBs
+ final VoxelShape first = aabbs.isEmpty() ? net.minecraft.world.phys.shapes.Shapes.empty() : inflateAABBToVoxel(aabbs.get(0), expandByX, expandByY, expandByZ);
+ final VoxelShape[] rest = new VoxelShape[Math.max(0, aabbs.size() - 1)];
+
+ for (int i = 1, len = aabbs.size(); i < len; ++i) {
+ rest[i - 1] = inflateAABBToVoxel(aabbs.get(i), expandByX, expandByY, expandByZ);
+ }
+
+ // use optimized implementation of ORing the shapes together
+ final VoxelShape joined = net.minecraft.world.phys.shapes.Shapes.or(first, rest);
+
+ // find free space
+ // can use unoptimized join here (instead of join()), as closestPointTo uses toAabbs()
+ final VoxelShape freeSpace = net.minecraft.world.phys.shapes.Shapes.joinUnoptimized(boundsShape, joined, net.minecraft.world.phys.shapes.BooleanOp.ONLY_FIRST);
+
+ return freeSpace.closestPointTo(fromPosition);
+ }
+
+ /**
+ * @reason Route to faster logic
+ * @author Spottedleaf
+ */
+ @Override
+ public final java.util.Optional<net.minecraft.core.BlockPos> findSupportingBlock(final Entity entity, final AABB aabb) {
+ 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 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();
+ 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;
+
+ final ChunkSource chunkSource = this.getChunkSource();
+
+ for (int currZ = minBlockZ; currZ <= maxBlockZ; ++currZ) {
+ pos.setZ(currZ);
+ for (int currX = minBlockX; currX <= maxBlockX; ++currX) {
+ pos.setX(currX);
+
+ final int newChunkX = currX >> 4;
+ final int newChunkZ = currZ >> 4;
+
+ if (((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)) != 0) {
+ lastChunkX = newChunkX;
+ lastChunkZ = newChunkZ;
+ lastChunk = (LevelChunk)chunkSource.getChunk(newChunkX, newChunkZ, ChunkStatus.FULL, false);
+ }
+
+ if (lastChunk == 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 double distance = pos.distToCenterSqr(entityPos);
+ if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(pos) >= 0)) {
+ 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()) {
+ 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;
+ }
+
+ selected = pos.immutable();
+ selectedDistance = distance;
+ continue;
+ }
+
+ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(blockCollision, shiftedAABB)) {
+ continue;
+ }
+
+ selected = pos.immutable();
+ selectedDistance = distance;
+ continue;
+ }
+ }
+ }
+ }
+
+ return java.util.Optional.ofNullable(selected);
+ }
+ // Paper end - optimise collisions
+ // Paper start - optimise random ticking
+ @Override
+ public abstract Holder<Biome> getUncachedNoiseBiome(final int x, final int y, final int z);
+
+ /**
+ * @reason Make getChunk and getUncachedNoiseBiome virtual calls instead of interface calls
+ * by implementing the superclass method in this class.
+ * @author Spottedleaf
+ */
+ @Override
+ public Holder<Biome> getNoiseBiome(final int x, final int y, final int z) {
+ final ChunkAccess chunk = this.getChunk(x >> 2, z >> 2, ChunkStatus.BIOMES, false);
+
+ return chunk != null ? chunk.getNoiseBiome(x, y, z) : this.getUncachedNoiseBiome(x, y, z);
+ }
+ // 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) { // Paper - create paper world config
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
@@ -281,6 +759,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
this.timings = new co.aikar.timings.WorldTimingsHandler(this); // Paper - code below can generate new world and access timings
this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime);
this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime);
+ this.entityLookup = new ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl.DefaultEntityLookup(this); // Paper - rewrite chunk system
+ // Paper start - optimise collisions
+ this.minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this);
+ this.maxSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this);
+ // Paper end - optimise collisions
}
// Paper start - Cancel hit for vanished players
@@ -549,7 +1032,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
this.setBlocksDirty(blockposition, iblockdata1, iblockdata2);
}
- if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement
+ if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.FULL)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement // Paper - rewrite chunk system - change from ticking to full
this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i);
}
@@ -813,6 +1296,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
// Iterator<TickingBlockEntity> iterator = this.blockEntityTickers.iterator();
boolean flag = this.tickRateManager().runsNormally();
+ int tickedEntities = 0; // Paper - rewrite chunk system
+
int tilesThisCycle = 0;
var toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<TickingBlockEntity>(); // Paper - Fix MC-117075; use removeAll
toRemove.add(null); // Paper - Fix MC-117075
@@ -828,6 +1313,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
// Spigot end
} else if (flag && this.shouldTickBlocksAt(tickingblockentity.getPos())) {
tickingblockentity.tick();
+ // Paper start - rewrite chunk system
+ if ((++tickedEntities & 7) == 0) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)(Level)(Object)this).moonrise$midTickTasks();
+ }
+ // Paper end - rewrite chunk system
}
}
this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075
@@ -850,12 +1340,20 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD);
// Paper end - Prevent block entity and entity crashes
}
+ this.moonrise$midTickTasks(); // Paper - rewrite chunk system
}
// Paper start - Option to prevent armor stands from doing entity lookups
@Override
public boolean noCollision(@Nullable Entity entity, AABB box) {
if (entity instanceof net.minecraft.world.entity.decoration.ArmorStand && !entity.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return false;
- return LevelAccessor.super.noCollision(entity, box);
+ // Paper start - optimise collisions
+ final int flags = entity == null ? (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER | ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_ONLY) : ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_ONLY;
+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder((Level)(Object)this, entity, box, null, null, flags, null)) {
+ return false;
+ }
+
+ return !ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getEntityHardCollisions((Level)(Object)this, entity, box, null, flags, null);
+ // Paper end - optimise collisions
}
// Paper end - Option to prevent armor stands from doing entity lookups
@@ -949,7 +1447,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
}
// Paper end - Perf: Optimize capturedTileEntities lookup
// CraftBukkit end
- return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && Thread.currentThread() != this.thread ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE));
+ return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThread() ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); // Paper - rewrite chunk system
}
public void setBlockEntity(BlockEntity blockEntity) {
@@ -1039,28 +1537,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
@Override
public List<Entity> getEntities(@Nullable Entity except, AABB box, Predicate<? super Entity> predicate) {
this.getProfiler().incrementCounter("getEntities");
- List<Entity> list = Lists.newArrayList();
+ // Paper start - rewrite chunk system
+ final List<Entity> ret = new java.util.ArrayList<>();
- this.getEntities().get(box, (entity1) -> {
- if (entity1 != except && predicate.test(entity1)) {
- list.add(entity1);
- }
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(except, box, ret, predicate);
- if (entity1 instanceof EnderDragon) {
- EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity1).getSubEntities();
- int i = aentitycomplexpart.length;
-
- for (int j = 0; j < i; ++j) {
- EnderDragonPart entitycomplexpart = aentitycomplexpart[j];
-
- if (entity1 != except && predicate.test(entitycomplexpart)) {
- list.add(entitycomplexpart);
- }
- }
- }
-
- });
- return list;
+ return ret;
+ // Paper end - rewrite chunk system
}
@Override
@@ -1075,36 +1558,77 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
this.getEntities(filter, box, predicate, result, Integer.MAX_VALUE);
}
- public <T extends Entity> void getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate, List<? super T> result, int limit) {
+ // Paper start - rewrite chunk system
+ public <T extends Entity> void getEntities(final EntityTypeTest<Entity, T> entityTypeTest,
+ final AABB boundingBox, final Predicate<? super T> predicate,
+ final List<? super T> into, final int maxCount) {
this.getProfiler().incrementCounter("getEntities");
- this.getEntities().get(filter, box, (entity) -> {
- if (predicate.test(entity)) {
- result.add(entity);
- if (result.size() >= limit) {
- return AbortableIterationConsumer.Continuation.ABORT;
- }
+
+ if (entityTypeTest instanceof net.minecraft.world.entity.EntityType<T> byType) {
+ if (maxCount != Integer.MAX_VALUE) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(byType, boundingBox, into, predicate, maxCount);
+ return;
+ } else {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(byType, boundingBox, into, predicate);
+ return;
}
+ }
- if (entity instanceof EnderDragon entityenderdragon) {
- EnderDragonPart[] aentitycomplexpart = entityenderdragon.getSubEntities();
- int j = aentitycomplexpart.length;
+ if (entityTypeTest == null) {
+ if (maxCount != Integer.MAX_VALUE) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate, maxCount);
+ return;
+ } else {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate);
+ return;
+ }
+ }
- for (int k = 0; k < j; ++k) {
- EnderDragonPart entitycomplexpart = aentitycomplexpart[k];
- T t0 = filter.tryCast(entitycomplexpart); // CraftBukkit - decompile error
+ final Class<? extends Entity> base = entityTypeTest.getBaseClass();
- if (t0 != null && predicate.test(t0)) {
- result.add(t0);
- if (result.size() >= limit) {
- return AbortableIterationConsumer.Continuation.ABORT;
- }
- }
+ final Predicate<? super T> modifiedPredicate;
+ if (predicate == null) {
+ modifiedPredicate = (final T obj) -> {
+ return entityTypeTest.tryCast(obj) != null;
+ };
+ } else {
+ modifiedPredicate = (final Entity obj) -> {
+ final T casted = entityTypeTest.tryCast(obj);
+ if (casted == null) {
+ return false;
}
+
+ return predicate.test(casted);
+ };
+ }
+
+ if (base == null || base == Entity.class) {
+ if (maxCount != Integer.MAX_VALUE) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount);
+ return;
+ } else {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate);
+ return;
}
+ } else {
+ if (maxCount != Integer.MAX_VALUE) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount);
+ return;
+ } else {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate);
+ return;
+ }
+ }
+ }
- return AbortableIterationConsumer.Continuation.CONTINUE;
- });
+ public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) {
+ ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices slices = ((ServerLevel)this).moonrise$getEntityLookup().getChunk(chunkX, chunkZ);
+ if (slices == null) {
+ return new org.bukkit.entity.Entity[0];
+ }
+ return slices.getChunkEntities();
}
+ // Paper end - rewrite chunk system
@Nullable
public abstract Entity getEntity(int id);
diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java
index a0ae26d6197e1069ca09982b4f8b706c55ae8491..1a4dc4b2561dbaf01246b4fb46266b1ac84008b8 100644
--- a/src/main/java/net/minecraft/world/level/LevelReader.java
+++ b/src/main/java/net/minecraft/world/level/LevelReader.java
@@ -22,7 +22,18 @@ import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.phys.AABB;
-public interface LevelReader extends BlockAndTintGetter, CollisionGetter, SignalGetter, BiomeManager.NoiseBiomeSource {
+public interface LevelReader extends ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader, BlockAndTintGetter, CollisionGetter, SignalGetter, BiomeManager.NoiseBiomeSource { // Paper - rewrite chunk system
+
+ // Paper start - rewrite chunk system
+ @Override
+ public default ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status) {
+ if (status == null || status.isOrAfter(ChunkStatus.FULL)) {
+ throw new IllegalArgumentException("Status: " + status.toString());
+ }
+ return ((LevelReader)this).getChunk(chunkX, chunkZ, status, true);
+ }
+ // Paper end - rewrite chunk system
+
@Nullable
ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create);
diff --git a/src/main/java/net/minecraft/world/level/biome/Biome.java b/src/main/java/net/minecraft/world/level/biome/Biome.java
index 15f82c9a1ce1fef2e951d1b3c7a65e64b82061ea..90c165c890a2d998e3b0af9b4310e3995ede6f64 100644
--- a/src/main/java/net/minecraft/world/level/biome/Biome.java
+++ b/src/main/java/net/minecraft/world/level/biome/Biome.java
@@ -111,20 +111,7 @@ public final class Biome {
@Deprecated
public float getTemperature(BlockPos blockPos) {
- long l = blockPos.asLong();
- Long2FloatLinkedOpenHashMap long2FloatLinkedOpenHashMap = this.temperatureCache.get();
- float f = long2FloatLinkedOpenHashMap.get(l);
- if (!Float.isNaN(f)) {
- return f;
- } else {
- float g = this.getHeightAdjustedTemperature(blockPos);
- if (long2FloatLinkedOpenHashMap.size() == 1024) {
- long2FloatLinkedOpenHashMap.removeFirstFloat();
- }
-
- long2FloatLinkedOpenHashMap.put(l, g);
- return g;
- }
+ return this.getHeightAdjustedTemperature(blockPos); // Paper - optimise random ticking
}
public boolean shouldFreeze(LevelReader world, BlockPos blockPos) {
diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java
index d775ab8b0d37797f29e650842191d40691fb7afc..a7108b2be0746aa1f0e574d8c6f5ffad6d369835 100644
--- a/src/main/java/net/minecraft/world/level/block/Block.java
+++ b/src/main/java/net/minecraft/world/level/block/Block.java
@@ -280,7 +280,7 @@ public class Block extends BlockBehaviour implements ItemLike {
}
public static boolean isShapeFullBlock(VoxelShape shape) {
- return (Boolean) Block.SHAPE_FULL_BLOCK_CACHE.getUnchecked(shape);
+ return ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$isFullBlock(); // Paper - optimise collisions
}
public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) {}
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 fe7dc02ea55c2bcd39d12bfd4d315f0b8c7014c3..ded6d148110fe3fbb6272ce44582a28472dd49a6 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
@@ -762,7 +762,7 @@ public abstract class BlockBehaviour implements FeatureElement {
boolean test(BlockState state, BlockGetter world, BlockPos pos);
}
- public abstract static class BlockStateBase extends StateHolder<Block, BlockState> {
+ public abstract static class BlockStateBase extends StateHolder<Block, BlockState> implements ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState, ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState { // Paper - rewrite chunk system // Paper - optimise collisions
private final int lightEmission;
private final boolean useShapeForLightOcclusion;
@@ -794,6 +794,76 @@ public abstract class BlockBehaviour implements FeatureElement {
private FluidState fluidState;
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;
+ private static final Direction[] DIRECTIONS_CACHED = Direction.values();
+ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger();
+ private final int id1 = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement() + RANDOM_OFFSET) + RANDOM_OFFSET);
+ 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 VoxelShape constantCollisionShape;
+ private AABB constantAABBCollision;
+
+ private static void initCaches(final VoxelShape shape) {
+ ((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();
+ }
+ }
+
+ @Override
+ public final boolean moonrise$hasCache() {
+ return this.cache != null;
+ }
+
+ @Override
+ public final boolean moonrise$occludesFullBlock() {
+ return this.occludesFullBlock;
+ }
+
+ @Override
+ public final boolean moonrise$emptyCollisionShape() {
+ return this.emptyCollisionShape;
+ }
+
+ @Override
+ public final int moonrise$uniqueId1() {
+ return this.id1;
+ }
+
+ @Override
+ public final int moonrise$uniqueId2() {
+ return this.id2;
+ }
+
+ @Override
+ public final VoxelShape moonrise$getConstantCollisionShape() {
+ 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) {
super(block, propertyMap, codec);
this.fluidState = Fluids.EMPTY.defaultFluidState();
@@ -858,6 +928,43 @@ public abstract class BlockBehaviour implements FeatureElement {
this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here
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();
+ // 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);
+ }
+ }
+ } else {
+ this.occludesFullBlock = false;
+ this.emptyCollisionShape = false;
+ this.constantCollisionShape = null;
+ this.constantAABBCollision = null;
+ }
+ // Paper end - optimise collisions
}
public Block getBlock() {
diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
index db4d95ce98eb1490d5306d1f74b282d27264871a..fba548c4e6d589323ec3ea5f6b269a6fe9faf6a1 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
@@ -57,7 +57,7 @@ import net.minecraft.world.ticks.SerializableTickContainer;
import net.minecraft.world.ticks.TickContainerAccess;
import org.slf4j.Logger;
-public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiomeSource, LightChunk, StructureAccess {
+public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiomeSource, LightChunk, StructureAccess, ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
public static final int NO_FILLED_SECTION = -1;
private static final Logger LOGGER = LogUtils.getLogger();
@@ -77,7 +77,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
@Nullable
protected BlendingData blendingData;
public final Map<Heightmap.Types, Heightmap> heightmaps = Maps.newEnumMap(Heightmap.Types.class);
- protected ChunkSkyLightSources skyLightSources;
+ // Paper - rewrite chunk system
private final Map<Structure, StructureStart> structureStarts = Maps.newHashMap();
private final Map<Structure, LongSet> structuresRefences = Maps.newHashMap();
protected final Map<BlockPos, CompoundTag> pendingBlockEntities = Maps.newHashMap();
@@ -90,6 +90,57 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
public org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer(ChunkAccess.DATA_TYPE_REGISTRY);
// CraftBukkit end
+ // Paper start - rewrite chunk system
+ private volatile ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles;
+ private volatile ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] skyNibbles;
+ private volatile boolean[] skyEmptinessMap;
+ private volatile boolean[] blockEmptinessMap;
+
+ @Override
+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() {
+ return this.blockNibbles;
+ }
+
+ @Override
+ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
+ this.blockNibbles = nibbles;
+ }
+
+ @Override
+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() {
+ return this.skyNibbles;
+ }
+
+ @Override
+ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
+ this.skyNibbles = nibbles;
+ }
+
+ @Override
+ public boolean[] starlight$getSkyEmptinessMap() {
+ return this.skyEmptinessMap;
+ }
+
+ @Override
+ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) {
+ this.skyEmptinessMap = emptinessMap;
+ }
+
+ @Override
+ public boolean[] starlight$getBlockEmptinessMap() {
+ return this.blockEmptinessMap;
+ }
+
+ @Override
+ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) {
+ this.blockEmptinessMap = emptinessMap;
+ }
+ // Paper end - rewrite chunk system
+ // Paper start - get block chunk optimisation
+ private final int minSection;
+ private final int maxSection;
+ // Paper end - get block chunk optimisation
+
public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry<Biome> biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) {
this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups
this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key
@@ -99,7 +150,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
this.inhabitedTime = inhabitedTime;
this.postProcessing = new ShortList[heightLimitView.getSectionsCount()];
this.blendingData = blendingData;
- this.skyLightSources = new ChunkSkyLightSources(heightLimitView);
+ // Paper - rewrite chunk system
if (sectionArray != null) {
if (this.sections.length == sectionArray.length) {
System.arraycopy(sectionArray, 0, this.sections, 0, this.sections.length);
@@ -111,6 +162,16 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
ChunkAccess.replaceMissingSections(biomeRegistry, this.sections);
// CraftBukkit start
this.biomeRegistry = biomeRegistry;
+ // Paper start - rewrite chunk system
+ if (!((Object)this instanceof ImposterProtoChunk)) {
+ this.starlight$setBlockNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(heightLimitView));
+ this.starlight$setSkyNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(heightLimitView));
+ }
+ // Paper end - rewrite chunk system
+ // Paper start - get block chunk optimisation
+ this.minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(levelHeightAccessor);
+ this.maxSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(levelHeightAccessor);
+ // Paper end - get block chunk optimisation
}
public final Registry<Biome> biomeRegistry;
// CraftBukkit end
@@ -442,22 +503,22 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
@Override
public Holder<Biome> getNoiseBiome(int biomeX, int biomeY, int biomeZ) {
- try {
- int l = QuartPos.fromBlock(this.getMinBuildHeight());
- int i1 = l + QuartPos.fromBlock(this.getHeight()) - 1;
- int j1 = Mth.clamp(biomeY, l, i1);
- int k1 = this.getSectionIndex(QuartPos.toBlock(j1));
-
- return this.sections[k1].getNoiseBiome(biomeX & 3, j1 & 3, biomeZ & 3);
- } catch (Throwable throwable) {
- CrashReport crashreport = CrashReport.forThrowable(throwable, "Getting biome");
- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Biome being got");
-
- crashreportsystemdetails.setDetail("Location", () -> {
- return CrashReportCategory.formatLocation(this, biomeX, biomeY, biomeZ);
- });
- throw new ReportedException(crashreport);
+ // Paper start - get block chunk optimisation
+ int sectionY = (biomeY >> 2) - this.minSection;
+ int rel = biomeY & 3;
+
+ final LevelChunkSection[] sections = this.sections;
+
+ if (sectionY < 0) {
+ sectionY = 0;
+ rel = 0;
+ } else if (sectionY >= sections.length) {
+ sectionY = sections.length - 1;
+ rel = 3;
}
+
+ return sections[sectionY].getNoiseBiome(biomeX & 3, rel, biomeZ & 3);
+ // Paper end - get block chunk optimisation
}
// CraftBukkit start
@@ -514,12 +575,12 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
}
public void initializeLightSources() {
- this.skyLightSources.fillFrom(this);
+ // Paper - rewrite chunk system
}
@Override
public ChunkSkyLightSources getSkyLightSources() {
- return this.skyLightSources;
+ return null; // Paper - rewrite chunk system
}
public static record TicksToSave(SerializableTickContainer<Block> blocks, SerializableTickContainer<Fluid> fluids) {
diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
index 29697fad32dad3377eebc82d280ba48d3c1ad516..488938c32a48437721a71d294c77468f00c035b9 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
@@ -119,7 +119,7 @@ public abstract class ChunkGenerator {
return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("init_biomes", () -> {
chunk.fillBiomesFromNoise(this.biomeSource, noiseConfig.sampler());
return chunk;
- }), Util.backgroundExecutor());
+ }), Runnable::run); // Paper - rewrite chunk system
}
public abstract void applyCarvers(WorldGenRegion chunkRegion, long seed, RandomState noiseConfig, BiomeManager biomeAccess, StructureManager structureAccessor, ChunkAccess chunk, GenerationStep.Carving carverStep);
@@ -314,7 +314,7 @@ public abstract class ChunkGenerator {
return Pair.of(placement.getLocatePos(pos), holder);
}
- ChunkAccess ichunkaccess = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS);
+ ChunkAccess ichunkaccess = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader)world).moonrise$syncLoadNonFull(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); // Paper - rewrite chunk system
structurestart = structureAccessor.getStartForStructure(SectionPos.bottomOf(ichunkaccess), (Structure) holder.value(), ichunkaccess);
} while (structurestart == null);
diff --git a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
index dcc0acd259920463a4464213b9a5e793603852f9..ef4161884574d3d137e12591d983dc95a960cb19 100644
--- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
@@ -13,7 +13,7 @@ import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.material.Fluids;
-public class EmptyLevelChunk extends LevelChunk {
+public class EmptyLevelChunk extends LevelChunk implements ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
private final Holder<Biome> biome;
public EmptyLevelChunk(Level world, ChunkPos pos, Holder<Biome> biomeEntry) {
@@ -21,6 +21,40 @@ public class EmptyLevelChunk extends LevelChunk {
this.biome = biomeEntry;
}
+ // Paper start - rewrite chunk system
+ @Override
+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() {
+ return ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(this.getLevel());
+ }
+
+ @Override
+ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {}
+
+ @Override
+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() {
+ return ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(this.getLevel());
+ }
+
+ @Override
+ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {}
+
+ @Override
+ public boolean[] starlight$getSkyEmptinessMap() {
+ return null;
+ }
+
+ @Override
+ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) {}
+
+ @Override
+ public boolean[] starlight$getBlockEmptinessMap() {
+ return null;
+ }
+
+ @Override
+ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) {}
+ // Paper end - rewrite chunk system
+
@Override
public BlockState getBlockState(BlockPos pos) {
return Blocks.VOID_AIR.defaultBlockState();
diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
index 365074be989aa4a178114fd5e9810f1a68640196..4af698930712389881601069a921f054c07935f2 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
@@ -31,7 +31,7 @@ import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.ticks.BlackholeTickAccess;
import net.minecraft.world.ticks.TickContainerAccess;
-public class ImposterProtoChunk extends ProtoChunk {
+public class ImposterProtoChunk extends ProtoChunk implements ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
private final LevelChunk wrapped;
private final boolean allowWrites;
@@ -47,6 +47,48 @@ public class ImposterProtoChunk extends ProtoChunk {
this.allowWrites = propagateToWrapped;
}
+ // Paper start - rewrite chunk system
+ @Override
+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() {
+ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getBlockNibbles();
+ }
+
+ @Override
+ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setBlockNibbles(nibbles);
+ }
+
+ @Override
+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() {
+ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getSkyNibbles();
+ }
+
+ @Override
+ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setSkyNibbles(nibbles);
+ }
+
+ @Override
+ public boolean[] starlight$getSkyEmptinessMap() {
+ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getSkyEmptinessMap();
+ }
+
+ @Override
+ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) {
+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setSkyEmptinessMap(emptinessMap);
+ }
+
+ @Override
+ public boolean[] starlight$getBlockEmptinessMap() {
+ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getBlockEmptinessMap();
+ }
+
+ @Override
+ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) {
+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setBlockEmptinessMap(emptinessMap);
+ }
+ // Paper end - rewrite chunk system
+
@Nullable
@Override
public BlockEntity getBlockEntity(BlockPos pos) {
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 602ad80c2b93d320bf2a25832d25a58cb8c72e4b..3fa7bfa09b9d529b5cb9cad923f21343159cfa35 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 {
+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
static final Logger LOGGER = LogUtils.getLogger();
private static final TickingBlockEntity NULL_TICKER = new TickingBlockEntity() {
@@ -109,6 +109,14 @@ public class LevelChunk extends ChunkAccess {
this.postLoad = entityLoader;
this.blockTicks = blockTickScheduler;
this.fluidTicks = fluidTickScheduler;
+ // Paper start - get block chunk optimisation
+ this.minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(level);
+ this.maxSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(level);
+
+ final boolean empty = ((Object)this instanceof EmptyLevelChunk);
+ this.debug = !empty && this.level.isDebug();
+ this.defaultBlockState = empty ? VOID_AIR_BLOCKSTATE : AIR_BLOCKSTATE;
+ // Paper end - get block chunk optimisation
}
// CraftBukkit start
@@ -119,6 +127,39 @@ public class LevelChunk extends ChunkAccess {
// Paper start
boolean loadedTicketLevel;
// Paper end
+ // Paper start - rewrite chunk system
+ private boolean postProcessingDone;
+ private net.minecraft.server.level.ServerChunkCache.ChunkAndHolder chunkAndHolder;
+
+ @Override
+ public final boolean moonrise$isPostProcessingDone() {
+ return this.postProcessingDone;
+ }
+
+ @Override
+ public final net.minecraft.server.level.ServerChunkCache.ChunkAndHolder moonrise$getChunkAndHolder() {
+ return this.chunkAndHolder;
+ }
+
+ @Override
+ public final void moonrise$setChunkAndHolder(final net.minecraft.server.level.ServerChunkCache.ChunkAndHolder holder) {
+ this.chunkAndHolder = holder;
+ }
+ // Paper end - rewrite chunk system
+ // Paper start - get block chunk optimisation
+ private static final BlockState AIR_BLOCKSTATE = Blocks.AIR.defaultBlockState();
+ private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState();
+ private static final BlockState VOID_AIR_BLOCKSTATE = Blocks.VOID_AIR.defaultBlockState();
+ private final int minSection;
+ private final int maxSection;
+ private final boolean debug;
+ private final BlockState defaultBlockState;
+
+ @Override
+ public final BlockState moonrise$getBlock(final int x, final int y, final int z) {
+ return this.getBlockStateFinal(x, y, z);
+ }
+ // Paper end - get block chunk optimisation
public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) {
this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData());
@@ -148,13 +189,19 @@ public class LevelChunk extends ChunkAccess {
}
}
- this.skyLightSources = protoChunk.skyLightSources;
+ // Paper - rewrite chunk system
this.setLightCorrect(protoChunk.isLightCorrect());
this.unsaved = true;
this.needsDecoration = true; // CraftBukkit
// CraftBukkit start
this.persistentDataContainer = protoChunk.persistentDataContainer; // SPIGOT-6814: copy PDC to account for 1.17 to 1.18 chunk upgrading.
// CraftBukkit end
+ // Paper start - rewrite chunk system
+ this.starlight$setBlockNibbles(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getBlockNibbles());
+ this.starlight$setSkyNibbles(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getSkyNibbles());
+ this.starlight$setSkyEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getSkyEmptinessMap());
+ this.starlight$setBlockEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getBlockEmptinessMap());
+ // Paper end - rewrite chunk system
}
@Override
@@ -337,7 +384,7 @@ public class LevelChunk extends ChunkAccess {
ProfilerFiller gameprofilerfiller = this.level.getProfiler();
gameprofilerfiller.push("updateSkyLightSources");
- this.skyLightSources.update(this, j, i, l);
+ // Paper - rewrite chunk system
gameprofilerfiller.popPush("queueCheckLight");
this.level.getChunkSource().getLightEngine().checkBlock(blockposition);
gameprofilerfiller.pop();
@@ -597,11 +644,12 @@ public class LevelChunk extends ChunkAccess {
// CraftBukkit start
public void loadCallback() {
+ if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Paper
// Paper start
this.loadedTicketLevel = true;
// Paper end
org.bukkit.Server server = this.level.getCraftServer();
- this.level.getChunkSource().addLoadedChunk(this); // Paper
+ // Paper - rewrite chunk system
if (server != null) {
/*
* If it's a new world, the first few chunks are generated inside
@@ -610,6 +658,7 @@ public class LevelChunk extends ChunkAccess {
*/
org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(bukkitChunk, this.needsDecoration));
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.locX, this.locZ).getEntityChunk().callEntitiesLoadEvent(); // Paper - rewrite chunk system
if (this.needsDecoration) {
try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper
@@ -638,13 +687,15 @@ public class LevelChunk extends ChunkAccess {
}
public void unloadCallback() {
+ if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Paper
org.bukkit.Server server = this.level.getCraftServer();
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.locX, this.locZ).getEntityChunk().callEntitiesUnloadEvent(); // Paper - rewrite chunk system
org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
- org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, this.isUnsaved());
+ org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, true); // Paper - rewrite chunk system - force save to true so that mustNotSave is correctly set below
server.getPluginManager().callEvent(unloadEvent);
// note: saving can be prevented, but not forced if no saving is actually required
this.mustNotSave = !unloadEvent.isSaveChunk();
- this.level.getChunkSource().removeLoadedChunk(this); // Paper
+ // Paper - rewrite chunk system
// Paper start
this.loadedTicketLevel = false;
// Paper end
@@ -652,8 +703,27 @@ public class LevelChunk extends ChunkAccess {
@Override
public boolean isUnsaved() {
- return super.isUnsaved() && !this.mustNotSave;
+ // Paper start - rewrite chunk system
+ final long gameTime = this.level.getGameTime();
+ if (((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$isDirty(gameTime)
+ || ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$isDirty(gameTime)) {
+ return true;
+ }
+
+ return super.isUnsaved();
+ // Paper end - rewrite chunk system
+ }
+
+ // Paper start - rewrite chunk system
+ @Override
+ public void setUnsaved(final boolean needsSaving) {
+ if (!needsSaving) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$clearDirty();
+ ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$clearDirty();
+ }
+ super.setUnsaved(needsSaving);
}
+ // Paper end - rewrite chunk system
// CraftBukkit end
public boolean isEmpty() {
@@ -759,6 +829,7 @@ public class LevelChunk extends ChunkAccess {
this.pendingBlockEntities.clear();
this.upgradeData.upgrade(this);
+ this.postProcessingDone = true; // Paper - rewrite chunk system
}
@Nullable
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 90d1c3e23e753c29660f7d993b3c90ac022941c3..d7b8d984122ba6b6ef5a0be6e012a8828a1af22d 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
@@ -13,7 +13,7 @@ import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.FluidState;
-public class LevelChunkSection {
+public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection { // Paper - block counting
public static final int SECTION_WIDTH = 16;
public static final int SECTION_HEIGHT = 16;
@@ -26,6 +26,28 @@ public class LevelChunkSection {
// CraftBukkit start - read/write
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);
+ static {
+ for (int 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();
+
+ @Override
+ public final int moonrise$getSpecialCollidingBlocks() {
+ return this.specialCollidingBlocks;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.common.list.IBlockDataList moonrise$getTickingBlockList() {
+ return this.tickingBlocks;
+ }
+ // Paper end - block counting
+
public LevelChunkSection(PalettedContainer<BlockState> datapaletteblock, PalettedContainer<Holder<Biome>> palettedcontainerro) {
// CraftBukkit end
this.states = datapaletteblock;
@@ -92,6 +114,22 @@ public class LevelChunkSection {
++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
+
return iblockdata1;
}
@@ -112,40 +150,65 @@ public class LevelChunkSection {
}
public void recalcBlockCounts() {
- class a implements PalettedContainer.CountConsumer<BlockState> {
+ // Paper start - block counting
+ // reset, then recalculate
+ this.nonEmptyBlockCount = (short)0;
+ this.tickingBlockCount = (short)0;
+ this.tickingFluidCount = (short)0;
+ this.specialCollidingBlocks = (short)0;
+ this.tickingBlocks.clear();
+
+ if (this.maybeHas((final BlockState state) -> !state.isAir())) {
+ final PalettedContainer.Data<BlockState> data = this.states.data;
+ final Palette<BlockState> palette = data.palette();
+ 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;
+ if (paletteSize == 1) {
+ counts = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1);
+ counts.put(0, FULL_LIST);
+ } else {
+ counts = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingBitStorage)storage).moonrise$countEntries();
+ }
- public int nonEmptyBlockCount;
- public int tickingBlockCount;
- public int tickingFluidCount;
+ 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();
+ final int paletteIdx = entry.getIntKey();
+ final it.unimi.dsi.fastutil.ints.IntArrayList coordinates = entry.getValue();
+ final int paletteCount = coordinates.size();
- a(final LevelChunkSection chunksection) {}
+ final BlockState state = palette.valueFor(paletteIdx);
- public void accept(BlockState iblockdata, int i) {
- FluidState fluid = iblockdata.getFluidState();
+ if (state.isAir()) {
+ continue;
+ }
- if (!iblockdata.isAir()) {
- this.nonEmptyBlockCount += i;
- if (iblockdata.isRandomlyTicking()) {
- this.tickingBlockCount += i;
+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(state)) {
+ this.specialCollidingBlocks += paletteCount;
+ }
+ this.nonEmptyBlockCount += paletteCount;
+ if (state.isRandomlyTicking()) {
+ this.tickingBlockCount += paletteCount;
+ final int[] raw = coordinates.elements();
+
+ java.util.Objects.checkFromToIndex(0, paletteCount, raw.length);
+ for (int i = 0; i < paletteCount; ++i) {
+ this.tickingBlocks.add(raw[i], state);
}
}
+ final FluidState fluid = state.getFluidState();
+
if (!fluid.isEmpty()) {
- this.nonEmptyBlockCount += i;
+ //this.nonEmptyBlockCount += count; // fix vanilla bug: make non empty block count correct
if (fluid.isRandomlyTicking()) {
- this.tickingFluidCount += i;
+ this.tickingFluidCount += paletteCount;
}
}
-
}
}
-
- a a0 = new a(this);
-
- this.states.count(a0);
- this.nonEmptyBlockCount = (short) a0.nonEmptyBlockCount;
- this.tickingBlockCount = (short) a0.tickingBlockCount;
- this.tickingFluidCount = (short) a0.tickingFluidCount;
+ // Paper end - block counting
}
public PalettedContainer<BlockState> getStates() {
@@ -163,6 +226,7 @@ public class LevelChunkSection {
datapaletteblock.read(buf);
this.biomes = datapaletteblock;
+ this.recalcBlockCounts(); // Paper - block counting
}
public void readBiomes(FriendlyByteBuf buf) {
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 2fa0097a9374a89177e4f1068d1bfed30b8ff122..339cac6b34b9f2f53852cfcee821bec9e0286c50 100644
--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
@@ -28,7 +28,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
private static final int MIN_PALETTE_BITS = 0;
private final PaletteResize<T> dummyPaletteResize = (newSize, added) -> 0;
public final IdMap<T> registry;
- private volatile PalettedContainer.Data<T> data;
+ public volatile PalettedContainer.Data<T> data; // Paper - optimise collisions - public
private final PalettedContainer.Strategy strategy;
// private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused
@@ -155,7 +155,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
return this.get(this.strategy.getIndex(x, y, z));
}
- protected T get(int index) {
+ public T get(int index) { // Paper - public
PalettedContainer.Data<T> data = this.data;
return data.palette.valueFor(data.storage.get(index));
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
index 7f302405a88766c2112539d24d3dd2e513f94985..207dc31afcf5ca5a59ab27ee263aa10f94a79559 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
@@ -143,7 +143,7 @@ public class ProtoChunk extends ChunkAccess {
}
if (LightEngine.hasDifferentLightProperties(this, pos, blockState, state)) {
- this.skyLightSources.update(this, m, j, o);
+ // Paper - rewrite chunk system
this.lightEngine.checkBlock(pos);
}
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java
index b1058bf0dcda544a074f4d3772d7899b94f98927..b7bf82f6b6023bd628d3e7ea84d2d6755a0d931a 100644
--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java
+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java
@@ -54,7 +54,7 @@ public record ChunkPyramid(ImmutableList<ChunkStep> steps) {
.step(ChunkStatus.CARVERS, builder -> builder)
.step(ChunkStatus.FEATURES, builder -> builder)
.step(ChunkStatus.INITIALIZE_LIGHT, builder -> builder.setTask(ChunkStatusTasks::initializeLight))
- .step(ChunkStatus.LIGHT, builder -> builder.addRequirement(ChunkStatus.INITIALIZE_LIGHT, 1).setTask(ChunkStatusTasks::light))
+ .step(ChunkStatus.LIGHT, builder -> builder.setTask(ChunkStatusTasks::light)) // Paper - rewrite chunk system - starlight does not need neighbours
.step(ChunkStatus.SPAWN, builder -> builder)
.step(ChunkStatus.FULL, builder -> builder.setTask(ChunkStatusTasks::full))
.build();
diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
index 0baa4adf2a4401f9c955352f27e6f99957d1dff4..3723c07183e7b894cccf4d01bedf1d0d832c1910 100644
--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
@@ -11,7 +11,7 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.levelgen.Heightmap;
import org.jetbrains.annotations.VisibleForTesting;
-public class ChunkStatus {
+public class ChunkStatus implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus { // Paper - rewrite chunk system
public static final int MAX_STRUCTURE_DISTANCE = 8;
private static final EnumSet<Heightmap.Types> WORLDGEN_HEIGHTMAPS = EnumSet.of(Heightmap.Types.OCEAN_FLOOR_WG, Heightmap.Types.WORLD_SURFACE_WG);
public static final EnumSet<Heightmap.Types> FINAL_HEIGHTMAPS = EnumSet.of(
@@ -51,8 +51,68 @@ public class ChunkStatus {
return list;
}
+ // Paper start - rewrite chunk system
+ private boolean isParallelCapable;
+ private boolean emptyLoadTask;
+ private int writeRadius;
+ private ChunkStatus nextStatus;
+ private java.util.concurrent.atomic.AtomicBoolean warnedAboutNoImmediateComplete;
+
+ @Override
+ public final boolean moonrise$isParallelCapable() {
+ return this.isParallelCapable;
+ }
+
+ @Override
+ public final void moonrise$setParallelCapable(final boolean value) {
+ this.isParallelCapable = value;
+ }
+
+ @Override
+ public final int moonrise$getWriteRadius() {
+ return this.writeRadius;
+ }
+
+ @Override
+ public final void moonrise$setWriteRadius(final int value) {
+ this.writeRadius = value;
+ }
+
+ @Override
+ public final ChunkStatus moonrise$getNextStatus() {
+ return this.nextStatus;
+ }
+
+ @Override
+ public final boolean moonrise$isEmptyLoadStatus() {
+ return this.emptyLoadTask;
+ }
+
+ @Override
+ public void moonrise$setEmptyLoadStatus(final boolean value) {
+ this.emptyLoadTask = value;
+ }
+
+ @Override
+ public final boolean moonrise$isEmptyGenStatus() {
+ return (Object)this == ChunkStatus.EMPTY;
+ }
+
+ @Override
+ public final java.util.concurrent.atomic.AtomicBoolean moonrise$getWarnedAboutNoImmediateComplete() {
+ return this.warnedAboutNoImmediateComplete;
+ }
+ // Paper end - rewrite chunk system
+
@VisibleForTesting
protected ChunkStatus(@Nullable ChunkStatus previous, EnumSet<Heightmap.Types> heightMapTypes, ChunkType chunkType) {
+ this.isParallelCapable = false;
+ this.writeRadius = -1;
+ this.nextStatus = (ChunkStatus)(Object)this;
+ if (previous != null) {
+ previous.nextStatus = (ChunkStatus)(Object)this;
+ }
+ this.warnedAboutNoImmediateComplete = new java.util.concurrent.atomic.AtomicBoolean();
this.parent = previous == null ? this : previous;
this.chunkType = chunkType;
this.heightmapsAfter = heightMapTypes;
diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
index ae16cf5c803caae636860dd9b1a83abe479ca5a4..b993c4b2595e2879b25753c2e34530f3622c18fa 100644
--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
@@ -154,7 +154,7 @@ public class ChunkStatusTasks {
chunk1 = ((ImposterProtoChunk) protochunk).getWrapped();
} else {
chunk1 = new LevelChunk(worldserver, protochunk, ($) -> { // Paper - decompile fix
- ChunkStatusTasks.postLoadProtoChunk(worldserver, protochunk.getEntities());
+ ChunkStatusTasks.postLoadProtoChunk(worldserver, protochunk.getEntities(), protochunk.getPos()); // Paper - rewrite chunk system
});
generationchunkholder.replaceProtoChunk(new ImposterProtoChunk(chunk1, false));
}
@@ -175,7 +175,7 @@ public class ChunkStatusTasks {
});
}
- private static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities) {
+ public static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities, ChunkPos pos) { // Paper - public, add ChunkPos param
if (!entities.isEmpty()) {
// CraftBukkit start - these are spawned serialized (DefinedStructure) and we don't call an add event below at the moment due to ordering complexities
world.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(entities, world).filter((entity) -> {
@@ -191,7 +191,7 @@ public class ChunkStatusTasks {
}
checkDupeUUID(world, entity); // Paper - duplicate uuid resolving
return !needsRemoval;
- }));
+ }), pos); // Paper - rewrite chunk system
// CraftBukkit end
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java
index f6e08a8334633ff1532616d051bed46b702d0091..4e56398a6fb8b97199f4c74ebebc1055fb718dcf 100644
--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java
+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java
@@ -11,9 +11,50 @@ import net.minecraft.util.profiling.jfr.callback.ProfiledDuration;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ProtoChunk;
-public record ChunkStep(
- ChunkStatus targetStatus, ChunkDependencies directDependencies, ChunkDependencies accumulatedDependencies, int blockStateWriteRadius, ChunkStatusTask task
-) {
+// Paper start - rewerite chunk system - convert record to class
+public final class ChunkStep implements ca.spottedleaf.moonrise.patches.chunk_system.status.ChunkSystemChunkStep { // Paper - rewrite chunk system
+ private final ChunkStatus targetStatus;
+ private final ChunkDependencies directDependencies;
+ private final ChunkDependencies accumulatedDependencies;
+ private final int blockStateWriteRadius;
+ private final ChunkStatusTask task;
+
+ private final ChunkStatus[] byRadius; // Paper - rewrite chunk system
+
+ public ChunkStep(
+ ChunkStatus targetStatus, ChunkDependencies directDependencies, ChunkDependencies accumulatedDependencies, int blockStateWriteRadius, ChunkStatusTask task
+ ) {
+ this.targetStatus = targetStatus;
+ this.directDependencies = directDependencies;
+ this.accumulatedDependencies = accumulatedDependencies;
+ this.blockStateWriteRadius = blockStateWriteRadius;
+ this.task = task;
+
+ // Paper start - rewrite chunk system
+ this.byRadius = new ChunkStatus[this.getAccumulatedRadiusOf(ChunkStatus.EMPTY) + 1];
+ this.byRadius[0] = targetStatus.getParent();
+
+ for (ChunkStatus status = targetStatus.getParent(); status != ChunkStatus.EMPTY; status = status.getParent()) {
+ final int radius = this.getAccumulatedRadiusOf(status);
+
+ for (int j = 0; j <= radius; ++j) {
+ if (this.byRadius[j] == null) {
+ this.byRadius[j] = status;
+ }
+ }
+ }
+ // Paper end - rewrite chunk system
+ }
+
+ // Paper start - rewrite chunk system
+ @Override
+ public final ChunkStatus moonrise$getRequiredStatusAtRadius(final int radius) {
+ return this.byRadius[radius];
+ }
+ // Paper end - rewrite chunk system
+
+ // Paper start - rewerite chunk system - convert record to class
+
public int getAccumulatedRadiusOf(ChunkStatus status) {
return status == this.targetStatus ? 0 : this.accumulatedDependencies.getRadiusOf(status);
}
@@ -39,6 +80,56 @@ public record ChunkStep(
return chunk;
}
+ // Paper start - rewerite chunk system - convert record to class
+ public ChunkStatus targetStatus() {
+ return targetStatus;
+ }
+
+ public ChunkDependencies directDependencies() {
+ return directDependencies;
+ }
+
+ public ChunkDependencies accumulatedDependencies() {
+ return accumulatedDependencies;
+ }
+
+ public int blockStateWriteRadius() {
+ return blockStateWriteRadius;
+ }
+
+ public ChunkStatusTask task() {
+ return task;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) return true;
+ if (obj == null || obj.getClass() != this.getClass()) return false;
+ var that = (net.minecraft.world.level.chunk.status.ChunkStep) obj;
+ return java.util.Objects.equals(this.targetStatus, that.targetStatus) &&
+ java.util.Objects.equals(this.directDependencies, that.directDependencies) &&
+ java.util.Objects.equals(this.accumulatedDependencies, that.accumulatedDependencies) &&
+ this.blockStateWriteRadius == that.blockStateWriteRadius &&
+ java.util.Objects.equals(this.task, that.task);
+ }
+
+ @Override
+ public int hashCode() {
+ return java.util.Objects.hash(targetStatus, directDependencies, accumulatedDependencies, blockStateWriteRadius, task);
+ }
+
+ @Override
+ public String toString() {
+ return "ChunkStep[" +
+ "targetStatus=" + targetStatus + ", " +
+ "directDependencies=" + directDependencies + ", " +
+ "accumulatedDependencies=" + accumulatedDependencies + ", " +
+ "blockStateWriteRadius=" + blockStateWriteRadius + ", " +
+ "task=" + task + ']';
+ }
+ // Paper end - rewerite chunk system - convert record to class
+
+
public static class Builder {
private final ChunkStatus status;
@Nullable
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
index d42585bccb03f8ee1be5e37cfbe8520af4cc5454..cb5196f0ff7b9a59927c60b9d24c987a150e69f1 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
@@ -165,7 +165,7 @@ public class ChunkSerializer {
achunksection[k] = chunksection;
SectionPos sectionposition = SectionPos.of(chunkPos, b0);
- poiStorage.checkConsistencyWithBlocks(sectionposition, chunksection);
+ // Paper - rewrite chunk system - moved to final load stage
}
boolean flag3 = nbttagcompound1.contains("BlockLight", 7);
@@ -287,6 +287,8 @@ public class ChunkSerializer {
}
}
+ ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.loadLightHook(world, chunkPos, nbt, (ChunkAccess)object1); // Paper - rewrite chunk system - note: it's ok to pass the raw value instead of wrapped
+
if (chunktype == ChunkType.LEVELCHUNK) {
return new ImposterProtoChunk((LevelChunk) object1, false);
} else {
@@ -341,14 +343,44 @@ public class ChunkSerializer {
}
// CraftBukkit end
+ // Paper start - async chunk saving
+ // must be called sync
+ public static ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData getAsyncSaveData(ServerLevel world, ChunkAccess chunk) {
+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(world, chunk.locX, chunk.locZ, "Preparing async chunk save data");
+
+ final CompoundTag tickLists = new CompoundTag();
+ ChunkSerializer.saveTicks(world, tickLists, chunk.getTicksForSerialization());
+
+ ListTag blockEntitiesSerialized = new ListTag();
+ for (final BlockPos blockPos : chunk.getBlockEntitiesPos()) {
+ final CompoundTag blockEntityNbt = chunk.getBlockEntityNbtForSaving(blockPos, world.registryAccess());
+ if (blockEntityNbt != null) {
+ blockEntitiesSerialized.add(blockEntityNbt);
+ }
+ }
+
+ return new ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData(
+ tickLists.get(BLOCK_TICKS_TAG),
+ tickLists.get(FLUID_TICKS_TAG),
+ blockEntitiesSerialized,
+ world.getGameTime()
+ );
+ }
+ // Paper end - async chunk saving
+
public static CompoundTag write(ServerLevel world, ChunkAccess chunk) {
+ // Paper start - async chunk saving
+ return saveChunk(world, chunk, null);
+ }
+ public static CompoundTag saveChunk(ServerLevel world, ChunkAccess chunk, ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData asyncsavedata) {
+ // Paper end - async chunk saving
ChunkPos chunkcoordintpair = chunk.getPos();
CompoundTag nbttagcompound = NbtUtils.addCurrentDataVersion(new CompoundTag());
nbttagcompound.putInt("xPos", chunkcoordintpair.x);
nbttagcompound.putInt("yPos", chunk.getMinSection());
nbttagcompound.putInt("zPos", chunkcoordintpair.z);
- nbttagcompound.putLong("LastUpdate", world.getGameTime());
+ nbttagcompound.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime() : world.getGameTime()); // Paper - async chunk saving
nbttagcompound.putLong("InhabitedTime", chunk.getInhabitedTime());
nbttagcompound.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString());
BlendingData blendingdata = chunk.getBlendingData();
@@ -424,8 +456,17 @@ public class ChunkSerializer {
nbttagcompound.putBoolean("isLightOn", true);
}
- ListTag nbttaglist1 = new ListTag();
- Iterator iterator = chunk.getBlockEntitiesPos().iterator();
+ // Paper start - async chunk saving
+ ListTag nbttaglist1;
+ Iterator<BlockPos> iterator;
+ if (asyncsavedata != null) {
+ nbttaglist1 = asyncsavedata.blockEntities();
+ iterator = java.util.Collections.emptyIterator();
+ } else {
+ nbttaglist1 = new ListTag();
+ iterator = chunk.getBlockEntitiesPos().iterator();
+ }
+ // Paper end - async chunk saving
CompoundTag nbttagcompound2;
@@ -461,7 +502,14 @@ public class ChunkSerializer {
nbttagcompound.put("CarvingMasks", nbttagcompound2);
}
+ // Paper start
+ if (asyncsavedata != null) {
+ nbttagcompound.put(BLOCK_TICKS_TAG, asyncsavedata.blockTickList());
+ nbttagcompound.put(FLUID_TICKS_TAG, asyncsavedata.fluidTickList());
+ } else {
ChunkSerializer.saveTicks(world, nbttagcompound, chunk.getTicksForSerialization());
+ }
+ // Paper end
nbttagcompound.put("PostProcessing", ChunkSerializer.packOffsets(chunk.getPostProcessing()));
CompoundTag nbttagcompound3 = new CompoundTag();
Iterator iterator1 = chunk.getHeightmaps().iterator();
@@ -481,6 +529,7 @@ public class ChunkSerializer {
nbttagcompound.put("ChunkBukkitValues", chunk.persistentDataContainer.toTagCompound());
}
// CraftBukkit end
+ ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.saveLightHook(world, chunk, nbttagcompound); // Paper - rewrite chunk system
return nbttagcompound;
}
@@ -506,7 +555,7 @@ public class ChunkSerializer {
return nbttaglist == null && nbttaglist1 == null ? null : (chunk) -> {
if (nbttaglist != null) {
- world.addLegacyChunkEntities(EntityType.loadEntitiesRecursive(nbttaglist, world));
+ world.addLegacyChunkEntities(EntityType.loadEntitiesRecursive(nbttaglist, world), chunk.getPos()); // Paper - rewrite chunk system
}
if (nbttaglist1 != null) {
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
index f0f5e9bb5ac65250f0a151f9f90b58468335a8c2..0cdc224656a2baa09b7dfbb249b6a96320ac43e0 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
@@ -28,21 +28,31 @@ import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.levelgen.structure.LegacyStructureDataHandler;
import net.minecraft.world.level.storage.DimensionDataStorage;
-public class ChunkStorage implements AutoCloseable {
+public class ChunkStorage implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkStorage { // Paper - rewrite chunk system
public static final int LAST_MONOLYTH_STRUCTURE_DATA_VERSION = 1493;
- private final IOWorker worker;
+ // Paper - rewrite chunk system
protected final DataFixer fixerUpper;
@Nullable
private volatile LegacyStructureDataHandler legacyStructureHandler;
+ // Paper start - rewrite chunk system
+ private static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger();
+ private final RegionFileStorage storage;
+
+ @Override
+ public final RegionFileStorage moonrise$getRegionStorage() {
+ return this.storage;
+ }
+ // Paper end - rewrite chunk system
+
public ChunkStorage(RegionStorageInfo storageKey, Path directory, DataFixer dataFixer, boolean dsync) {
this.fixerUpper = dataFixer;
- this.worker = new IOWorker(storageKey, directory, dsync);
+ this.storage = new IOWorker(storageKey, directory, dsync).storage; // Paper - rewrite chunk system
}
public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) {
- return this.worker.isOldChunkAround(chunkPos, checkRadius);
+ return true; // Paper - rewrite chunk system
}
// CraftBukkit start
@@ -102,7 +112,9 @@ public class ChunkStorage implements AutoCloseable {
if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) {
LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(resourcekey, supplier);
+ synchronized (persistentstructurelegacy) { // Paper - rewrite chunk system
nbttagcompound = persistentstructurelegacy.updateFromLegacy(nbttagcompound);
+ } // Paper - rewrite chunk system
}
}
@@ -169,7 +181,13 @@ public class ChunkStorage implements AutoCloseable {
}
public CompletableFuture<Optional<CompoundTag>> read(ChunkPos chunkPos) {
- return this.worker.loadAsync(chunkPos);
+ // Paper start - rewrite chunk system
+ try {
+ return CompletableFuture.completedFuture(Optional.ofNullable(this.storage.read(chunkPos)));
+ } catch (final Throwable throwable) {
+ return CompletableFuture.failedFuture(throwable);
+ }
+ // Paper end - rewrite chunk system
}
public CompletableFuture<Void> write(ChunkPos chunkPos, CompoundTag nbt) {
@@ -181,29 +199,54 @@ public class ChunkStorage implements AutoCloseable {
}
// Paper end - guard against serializing mismatching coordinates
this.handleLegacyStructureIndex(chunkPos);
- return this.worker.store(chunkPos, nbt);
+ // Paper start - rewrite chunk system
+ try {
+ this.storage.write(chunkPos, nbt);
+ return CompletableFuture.completedFuture(null);
+ } catch (final Throwable throwable) {
+ return CompletableFuture.failedFuture(throwable);
+ }
+ // Paper end - rewrite chunk system
}
protected void handleLegacyStructureIndex(ChunkPos chunkPos) {
if (this.legacyStructureHandler != null) {
+ synchronized (this.legacyStructureHandler) { // Paper - rewrite chunk system
this.legacyStructureHandler.removeIndex(chunkPos.toLong());
+ } // Paper - rewrite chunk system
}
}
public void flushWorker() {
- this.worker.synchronize(true).join();
+ // Paper start - rewrite chunk system
+ try {
+ this.storage.flush();
+ } catch (final IOException ex) {
+ LOGGER.error("Failed to flush chunk storage", ex);
+ }
+ // Paper end - rewrite chunk system
}
public void close() throws IOException {
- this.worker.close();
+ this.storage.close(); // Paper - rewrite chunk system
}
public ChunkScanAccess chunkScanner() {
- return this.worker;
+ // Paper start - rewrite chunk system
+ // TODO ChunkMap implementation?
+ return (chunkPos, streamTagVisitor) -> {
+ try {
+ this.storage.scanChunk(chunkPos, streamTagVisitor);
+ return java.util.concurrent.CompletableFuture.completedFuture(null);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ };
+ // Paper end - rewrite chunk system
}
- protected RegionStorageInfo storageInfo() {
- return this.worker.storageInfo();
+ public RegionStorageInfo storageInfo() { // Paper - public
+ return this.storage.info(); // Paper - rewrite chunk system
}
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java
index 36b8a9ac385e43f3212aca1b1f5bd7115bd00431..503ac0374e0c9f9993ad37bb8bd8cf1570d3615a 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java
@@ -70,12 +70,12 @@ public class EntityStorage implements EntityPersistentStorage<Entity> {
}
}
- private static ChunkPos readChunkPos(CompoundTag chunkNbt) {
+ public static ChunkPos readChunkPos(CompoundTag chunkNbt) { // Paper - public
int[] is = chunkNbt.getIntArray("Position");
return new ChunkPos(is[0], is[1]);
}
- private static void writeChunkPos(CompoundTag chunkNbt, ChunkPos pos) {
+ public static void writeChunkPos(CompoundTag chunkNbt, ChunkPos pos) { // Paper - public
chunkNbt.put("Position", new IntArrayTag(new int[]{pos.x, pos.z}));
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java b/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java
index 053504cc6c98be3b70bd1722e279d861694e015d..316bf111fe94ce7a71af71cd32c94fcf528d4365 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java
@@ -32,7 +32,7 @@ public class IOWorker implements ChunkScanAccess, AutoCloseable {
private static final Logger LOGGER = LogUtils.getLogger();
private final AtomicBoolean shutdownRequested = new AtomicBoolean();
private final ProcessorMailbox<StrictQueue.IntRunnable> mailbox;
- private final RegionFileStorage storage;
+ public final RegionFileStorage storage; // Paper - public
private final Map<ChunkPos, IOWorker.PendingStore> pendingWrites = Maps.newLinkedHashMap();
private final Long2ObjectLinkedOpenHashMap<CompletableFuture<BitSet>> regionCacheForBlender = new Long2ObjectLinkedOpenHashMap<>();
private static final int REGION_CACHE_SIZE = 1024;
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 4c1212c6ef48594e766fa9e35a6e15916602d587..18054304e08c8a6346c0135a0e6a68e77fe5c37c 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
@@ -17,7 +17,7 @@ import net.minecraft.nbt.StreamTagVisitor;
import net.minecraft.util.ExceptionCollector;
import net.minecraft.world.level.ChunkPos;
-public final class RegionFileStorage implements AutoCloseable {
+public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage { // Paper - rewrite chunk system
public static final String ANVIL_EXTENSION = ".mca";
private static final int MAX_CACHE_SIZE = 256;
@@ -26,33 +26,122 @@ public final class RegionFileStorage implements AutoCloseable {
private final Path folder;
private final boolean sync;
- RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) {
+ // 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 String getRegionFileName(final int chunkX, final int chunkZ) {
+ return "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT) + ".mca";
+ }
+
+ private boolean doesRegionFilePossiblyExist(final long position) {
+ synchronized (this.nonExistingRegionFiles) {
+ if (this.nonExistingRegionFiles.contains(position)) {
+ this.nonExistingRegionFiles.addAndMoveToFirst(position);
+ return false;
+ }
+ return true;
+ }
+ }
+
+ private void createRegionFile(final long position) {
+ synchronized (this.nonExistingRegionFiles) {
+ this.nonExistingRegionFiles.remove(position);
+ }
+ }
+
+ private void markNonExisting(final long position) {
+ synchronized (this.nonExistingRegionFiles) {
+ if (this.nonExistingRegionFiles.addAndMoveToFirst(position)) {
+ while (this.nonExistingRegionFiles.size() >= MAX_NON_EXISTING_CACHE) {
+ this.nonExistingRegionFiles.removeLastLong();
+ }
+ }
+ }
+ }
+
+ @Override
+ public final boolean moonrise$doesRegionFileNotExistNoIO(final int chunkX, final int chunkZ) {
+ return !this.doesRegionFilePossiblyExist(ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT));
+ }
+
+ @Override
+ public synchronized final RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) {
+ return this.regionCache.getAndMoveToFirst(ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT));
+ }
+
+ @Override
+ public synchronized final RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException {
+ final long key = ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
+
+ RegionFile ret = this.regionCache.getAndMoveToFirst(key);
+ if (ret != null) {
+ return ret;
+ }
+
+ if (!this.doesRegionFilePossiblyExist(key)) {
+ return null;
+ }
+
+ if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper
+ this.regionCache.removeLast().close();
+ }
+
+ final Path regionPath = this.folder.resolve(getRegionFileName(chunkX, chunkZ));
+
+ if (!java.nio.file.Files.exists(regionPath)) {
+ this.markNonExisting(key);
+ return null;
+ }
+
+ this.createRegionFile(key);
+
+ FileUtil.createDirectoriesSafe(this.folder);
+
+ ret = new RegionFile(this.info, regionPath, this.folder, this.sync);
+
+ this.regionCache.putAndMoveToFirst(key, ret);
+
+ return ret;
+ }
+ // Paper end - rewrite chunk system
+
+ protected RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { // Paper - protected
this.folder = directory;
this.sync = dsync;
this.info = storageKey;
}
- private RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit
- long i = ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ());
- RegionFile regionfile = (RegionFile) this.regionCache.getAndMoveToFirst(i);
+ public RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit // Paper - public
+ // Paper start - rewrite chunk system
+ if (existingOnly) {
+ return this.moonrise$getRegionFileIfExists(chunkcoordintpair.x, chunkcoordintpair.z);
+ }
+ synchronized (this) {
+ final long key = ChunkPos.asLong(chunkcoordintpair.x >> REGION_SHIFT, chunkcoordintpair.z >> REGION_SHIFT);
- if (regionfile != null) {
- return regionfile;
- } else {
- if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - Sanitise RegionFileCache and make configurable
- ((RegionFile) this.regionCache.removeLast()).close();
+ RegionFile ret = this.regionCache.getAndMoveToFirst(key);
+ if (ret != null) {
+ return ret;
+ }
+
+ if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper
+ this.regionCache.removeLast().close();
}
+ final Path regionPath = this.folder.resolve(getRegionFileName(chunkcoordintpair.x, chunkcoordintpair.z));
+
+ this.createRegionFile(key);
+
FileUtil.createDirectoriesSafe(this.folder);
- Path path = this.folder;
- int j = chunkcoordintpair.getRegionX();
- Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca");
- if (existingOnly && !java.nio.file.Files.exists(path1)) return null; // CraftBukkit
- RegionFile regionfile1 = new RegionFile(this.info, path1, this.folder, this.sync);
- this.regionCache.putAndMoveToFirst(i, regionfile1);
- return regionfile1;
+ ret = new RegionFile(this.info, regionPath, this.folder, this.sync);
+
+ this.regionCache.putAndMoveToFirst(key, ret);
+
+ return ret;
}
+ // Paper end - rewrite chunk system
}
@Nullable
@@ -132,8 +221,14 @@ public final class RegionFileStorage implements AutoCloseable {
}
- protected void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException {
- RegionFile regionfile = this.getRegionFile(pos, false); // CraftBukkit
+ public void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException { // Paper - public
+ RegionFile regionfile = this.getRegionFile(pos, nbt == null); // CraftBukkit // Paper - rewrite chunk system
+ // Paper start - rewrite chunk system
+ if (regionfile == null) {
+ // if the RegionFile doesn't exist, no point in deleting from it
+ return;
+ }
+ // Paper end - rewrite chunk system
// Paper start - Chunk save reattempt
int attempts = 0;
Exception lastException = null;
@@ -182,30 +277,37 @@ public final class RegionFileStorage implements AutoCloseable {
}
public void close() throws IOException {
- ExceptionCollector<IOException> exceptionsuppressor = new ExceptionCollector<>();
- ObjectIterator objectiterator = this.regionCache.values().iterator();
-
- while (objectiterator.hasNext()) {
- RegionFile regionfile = (RegionFile) objectiterator.next();
-
- try {
- regionfile.close();
- } catch (IOException ioexception) {
- exceptionsuppressor.add(ioexception);
+ // Paper start - rewrite chunk system
+ synchronized (this) {
+ final ExceptionCollector<IOException> exceptionCollector = new ExceptionCollector<>();
+ for (final RegionFile regionFile : this.regionCache.values()) {
+ try {
+ regionFile.close();
+ } catch (final IOException ex) {
+ exceptionCollector.add(ex);
+ }
}
- }
- exceptionsuppressor.throwIfPresent();
+ exceptionCollector.throwIfPresent();
+ }
+ // Paper end - rewrite chunk system
}
public void flush() throws IOException {
- ObjectIterator objectiterator = this.regionCache.values().iterator();
-
- while (objectiterator.hasNext()) {
- RegionFile regionfile = (RegionFile) objectiterator.next();
+ // Paper start - rewrite chunk system
+ synchronized (this) {
+ final ExceptionCollector<IOException> exceptionCollector = new ExceptionCollector<>();
+ for (final RegionFile regionFile : this.regionCache.values()) {
+ try {
+ regionFile.flush();
+ } catch (final IOException ex) {
+ exceptionCollector.add(ex);
+ }
+ }
- regionfile.flush();
+ exceptionCollector.throwIfPresent();
}
+ // Paper end - rewrite chunk system
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
index 092773bd39d77a0dbe22db97c11aecb4a297111c..c7ed3eb80f6e8b918434153093644776866aa220 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
@@ -31,10 +31,10 @@ import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import org.slf4j.Logger;
-public class SectionStorage<R> implements AutoCloseable {
+public abstract class SectionStorage<R> implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage { // Paper - rewrite chunk system
private static final Logger LOGGER = LogUtils.getLogger();
private static final String SECTIONS_TAG = "Sections";
- private final SimpleRegionStorage simpleRegionStorage;
+ // Paper - rewrite chunk system
private final Long2ObjectMap<Optional<R>> storage = new Long2ObjectOpenHashMap<>();
private final LongLinkedOpenHashSet dirty = new LongLinkedOpenHashSet();
private final Function<Runnable, Codec<R>> codec;
@@ -43,6 +43,15 @@ public class SectionStorage<R> implements AutoCloseable {
private final ChunkIOErrorReporter errorReporter;
protected final LevelHeightAccessor levelHeightAccessor;
+ // Paper start - rewrite chunk system
+ private final RegionFileStorage regionStorage;
+
+ @Override
+ public final RegionFileStorage moonrise$getRegionStorage() {
+ return this.regionStorage;
+ }
+ // Paper end - rewrite chunk system
+
public SectionStorage(
SimpleRegionStorage storageAccess,
Function<Runnable, Codec<R>> codecFactory,
@@ -51,12 +60,13 @@ public class SectionStorage<R> implements AutoCloseable {
ChunkIOErrorReporter errorHandler,
LevelHeightAccessor world
) {
- this.simpleRegionStorage = storageAccess;
+ // Paper - rewrite chunk system
this.codec = codecFactory;
this.factory = factory;
this.registryAccess = registryManager;
this.errorReporter = errorHandler;
this.levelHeightAccessor = world;
+ this.regionStorage = storageAccess.worker.storage; // Paper - rewrite chunk system
}
protected void tick(BooleanSupplier shouldKeepTicking) {
@@ -121,44 +131,17 @@ public class SectionStorage<R> implements AutoCloseable {
}
private CompletableFuture<Optional<CompoundTag>> tryRead(ChunkPos pos) {
- return this.simpleRegionStorage.read(pos).exceptionally(throwable -> {
- if (throwable instanceof IOException iOException) {
- LOGGER.error("Error reading chunk {} data from disk", pos, iOException);
- this.errorReporter.reportChunkLoadFailure(iOException, this.simpleRegionStorage.storageInfo(), pos);
- return Optional.empty();
- } else {
- throw new CompletionException(throwable);
- }
- });
+ // Paper start - rewrite chunk system
+ try {
+ return CompletableFuture.completedFuture(Optional.ofNullable(this.moonrise$read(pos.x, pos.z)));
+ } catch (final Throwable thr) {
+ return CompletableFuture.failedFuture(thr);
+ }
+ // Paper end - rewrite chunk system
}
private void readColumn(ChunkPos pos, RegistryOps<Tag> ops, @Nullable CompoundTag nbt) {
- if (nbt == null) {
- for (int i = this.levelHeightAccessor.getMinSection(); i < this.levelHeightAccessor.getMaxSection(); i++) {
- this.storage.put(getKey(pos, i), Optional.empty());
- }
- } else {
- Dynamic<Tag> dynamic = new Dynamic<>(ops, nbt);
- int j = getVersion(dynamic);
- int k = SharedConstants.getCurrentVersion().getDataVersion().getVersion();
- boolean bl = j != k;
- Dynamic<Tag> dynamic2 = this.simpleRegionStorage.upgradeChunkTag(dynamic, j);
- OptionalDynamic<Tag> optionalDynamic = dynamic2.get("Sections");
-
- for (int l = this.levelHeightAccessor.getMinSection(); l < this.levelHeightAccessor.getMaxSection(); l++) {
- long m = getKey(pos, l);
- Optional<R> optional = optionalDynamic.get(Integer.toString(l))
- .result()
- .flatMap(dynamicx -> this.codec.apply(() -> this.setDirty(m)).parse(dynamicx).resultOrPartial(LOGGER::error));
- this.storage.put(m, optional);
- optional.ifPresent(sections -> {
- this.onSectionLoad(m);
- if (bl) {
- this.setDirty(m);
- }
- });
- }
- }
+ throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system
}
private void writeColumn(ChunkPos pos) {
@@ -166,10 +149,13 @@ public class SectionStorage<R> implements AutoCloseable {
Dynamic<Tag> dynamic = this.writeColumn(pos, registryOps);
Tag tag = dynamic.getValue();
if (tag instanceof CompoundTag) {
- this.simpleRegionStorage.write(pos, (CompoundTag)tag).exceptionally(throwable -> {
- this.errorReporter.reportChunkSaveFailure(throwable, this.simpleRegionStorage.storageInfo(), pos);
- return null;
- });
+ // Paper start - rewrite chunk system
+ try {
+ this.moonrise$write(pos.x, pos.z, (net.minecraft.nbt.CompoundTag)tag);
+ } catch (final IOException ex) {
+ LOGGER.error("Error writing poi chunk data to disk for chunk " + pos, ex);
+ }
+ // Paper end - rewrite chunk system
} else {
LOGGER.error("Expected compound tag, got {}", tag);
}
@@ -209,7 +195,7 @@ public class SectionStorage<R> implements AutoCloseable {
protected void onSectionLoad(long pos) {
}
- protected void setDirty(long pos) {
+ public void setDirty(long pos) { // Paper - public
Optional<R> optional = this.storage.get(pos);
if (optional != null && !optional.isEmpty()) {
this.dirty.add(pos);
@@ -236,6 +222,6 @@ public class SectionStorage<R> implements AutoCloseable {
@Override
public void close() throws IOException {
- this.simpleRegionStorage.close();
+ this.moonrise$close(); // Paper - rewrite chunk system
}
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
index e0e843f4f69013379ed70cb63d9b4f72163b828b..aafb05c5e63903f5790a6bcb862c8d79588be5a6 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
@@ -14,7 +14,7 @@ import net.minecraft.util.datafix.DataFixTypes;
import net.minecraft.world.level.ChunkPos;
public class SimpleRegionStorage implements AutoCloseable {
- private final IOWorker worker;
+ public final IOWorker worker; // Paper - public
private final DataFixer fixerUpper;
private final DataFixTypes dataFixType;
diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
index 74a285b8b018a9c94ccea519f1ce8b9e2ef3cb64..d8b4196adf955f8d414688dc451caac2d9c609d9 100644
--- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
+++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
@@ -9,52 +9,38 @@ import javax.annotation.Nullable;
import net.minecraft.world.entity.Entity;
public class EntityTickList {
- private Int2ObjectMap<Entity> active = new Int2ObjectLinkedOpenHashMap<>();
- private Int2ObjectMap<Entity> passive = new Int2ObjectLinkedOpenHashMap<>();
- @Nullable
- private Int2ObjectMap<Entity> iterated;
+ private final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<net.minecraft.world.entity.Entity> entities = new ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<>(); // Paper - rewrite chunk system
private void ensureActiveIsNotIterated() {
- if (this.iterated == this.active) {
- this.passive.clear();
-
- for (Entry<Entity> entry : Int2ObjectMaps.fastIterable(this.active)) {
- this.passive.put(entry.getIntKey(), entry.getValue());
- }
-
- Int2ObjectMap<Entity> int2ObjectMap = this.active;
- this.active = this.passive;
- this.passive = int2ObjectMap;
- }
+ // Paper - rewrite chunk system
}
public void add(Entity entity) {
this.ensureActiveIsNotIterated();
- this.active.put(entity.getId(), entity);
+ this.entities.add(entity); // Paper - rewrite chunk system
}
public void remove(Entity entity) {
this.ensureActiveIsNotIterated();
- this.active.remove(entity.getId());
+ this.entities.remove(entity); // Paper - rewrite chunk system
}
public boolean contains(Entity entity) {
- return this.active.containsKey(entity.getId());
+ return this.entities.contains(entity); // Paper - rewrite chunk system
}
public void forEach(Consumer<Entity> action) {
- if (this.iterated != null) {
- throw new UnsupportedOperationException("Only one concurrent iteration supported");
- } else {
- this.iterated = this.active;
-
- try {
- for (Entity entity : this.active.values()) {
- action.accept(entity);
- }
- } finally {
- this.iterated = null;
+ // Paper start - rewrite chunk system
+ // To ensure nothing weird happens with dimension travelling, do not iterate over new entries...
+ // (by dfl iterator() is configured to not iterate over new entries)
+ final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet.Iterator<Entity> iterator = this.entities.iterator();
+ try {
+ while (iterator.hasNext()) {
+ action.accept(iterator.next());
}
+ } finally {
+ iterator.finishedIterating();
}
+ // Paper end - rewrite chunk system
}
}
diff --git a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
index fb0be805c86e311927f55e8f090592465195384e..996899cb18e6c29665b9de7a1cc97c9a4187924b 100644
--- a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
+++ b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
@@ -86,7 +86,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("init_biomes", () -> {
this.doCreateBiomes(blender, noiseConfig, structureAccessor, chunk);
return chunk;
- }), Util.backgroundExecutor());
+ }), Runnable::run); // Paper - rewrite chunk system
}
private void doCreateBiomes(Blender blender, RandomState noiseConfig, StructureManager structureAccessor, ChunkAccess chunk) {
@@ -311,7 +311,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
}
return ichunkaccess1;
- }), Util.backgroundExecutor());
+ }), Runnable::run); // Paper - rewrite chunk system
}
private ChunkAccess doFill(Blender blender, StructureManager structureAccessor, RandomState noiseConfig, ChunkAccess chunk, int minimumCellY, int cellHeight) {
diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
index c6181e14d85d454506534f9bbe856156c0d4a062..3694c5d2d522216cd2e6e91e502a56a08595ca84 100644
--- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
@@ -47,8 +47,13 @@ public class StructureCheck {
private final BiomeSource biomeSource;
private final long seed;
private final DataFixer fixerUpper;
- private final Long2ObjectMap<Object2IntMap<Structure>> loadedChunks = new Long2ObjectOpenHashMap<>();
- private final Map<Structure, Long2BooleanMap> featureChecks = new HashMap<>();
+ // Paper start - rewrite chunk system
+ // make sure to purge entries from the maps to prevent memory leaks
+ private static final int CHUNK_TOTAL_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups
+ private static final int PER_FEATURE_CHECK_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups
+ private final ca.spottedleaf.moonrise.common.map.SynchronisedLong2ObjectMap<it.unimi.dsi.fastutil.objects.Object2IntMap<Structure>> loadedChunksSafe = new ca.spottedleaf.moonrise.common.map.SynchronisedLong2ObjectMap<>(CHUNK_TOTAL_LIMIT);
+ private final java.util.concurrent.ConcurrentHashMap<Structure, ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap> featureChecksSafe = new java.util.concurrent.ConcurrentHashMap<>();
+ // Paper end - rewrite chunk system
public StructureCheck(
ChunkScanAccess chunkIoWorker,
@@ -90,7 +95,7 @@ public class StructureCheck {
public StructureCheckResult checkStart(ChunkPos pos, Structure type, StructurePlacement placement, boolean skipReferencedStructures) {
long l = pos.toLong();
- Object2IntMap<Structure> object2IntMap = this.loadedChunks.get(l);
+ Object2IntMap<Structure> object2IntMap = this.loadedChunksSafe.get(l); // Paper - rewrite chunk system
if (object2IntMap != null) {
return this.checkStructureInfo(object2IntMap, type, skipReferencedStructures);
} else {
@@ -100,9 +105,11 @@ public class StructureCheck {
} else if (!placement.applyAdditionalChunkRestrictions(pos.x, pos.z, this.seed, this.getSaltOverride(type))) { // Paper - add missing structure seed configs
return StructureCheckResult.START_NOT_PRESENT;
} else {
- boolean bl = this.featureChecks
- .computeIfAbsent(type, structure2 -> new Long2BooleanOpenHashMap())
- .computeIfAbsent(l, chunkPos -> this.canCreateStructure(pos, type));
+ // Paper start - rewrite chunk system
+ boolean bl = this.featureChecksSafe
+ .computeIfAbsent(type, structure2 -> new ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap(PER_FEATURE_CHECK_LIMIT))
+ .getOrCompute(l, chunkPos -> this.canCreateStructure(pos, type));
+ // Paper end - rewrite chunk system
return !bl ? StructureCheckResult.START_NOT_PRESENT : StructureCheckResult.CHUNK_LOAD_NEEDED;
}
}
@@ -228,15 +235,25 @@ public class StructureCheck {
}
private void storeFullResults(long pos, Object2IntMap<Structure> referencesByStructure) {
- this.loadedChunks.put(pos, deduplicateEmptyMap(referencesByStructure));
- this.featureChecks.values().forEach(generationPossibilityByChunkPos -> generationPossibilityByChunkPos.remove(pos));
+ // Paper start - rewrite chunk system
+ this.loadedChunksSafe.put(pos, deduplicateEmptyMap(referencesByStructure));
+ // once we insert into loadedChunks, we don't really need to be very careful about removing everything
+ // from this map, as everything that checks this map uses loadedChunks first
+ // so, one way or another it's a race condition that doesn't matter
+ for (ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap value : this.featureChecksSafe.values()) {
+ value.remove(pos);
+ }
+ // Paper end - rewrite chunk system
}
public void incrementReference(ChunkPos pos, Structure structure) {
- this.loadedChunks.compute(pos.toLong(), (posx, referencesByStructure) -> {
- if (referencesByStructure == null || referencesByStructure.isEmpty()) {
+ this.loadedChunksSafe.compute(pos.toLong(), (posx, referencesByStructure) -> { // Paper start - rewrite chunk system
+ if (referencesByStructure == null) {
referencesByStructure = new Object2IntOpenHashMap<>();
+ } else {
+ referencesByStructure = referencesByStructure instanceof Object2IntOpenHashMap<Structure> fastClone ? fastClone.clone() : new Object2IntOpenHashMap<>(referencesByStructure);
}
+ // Paper end - rewrite chunk system
referencesByStructure.computeInt(structure, (feature, references) -> references == null ? 1 : references + 1);
return referencesByStructure;
diff --git a/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java b/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java
index 82e4fad11121167445df97060fb717fa86191297..b3e2bb9245be1bb2f587117b0f6016cba18e217f 100644
--- a/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java
+++ b/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java
@@ -9,145 +9,103 @@ import net.minecraft.world.level.LightLayer;
import net.minecraft.world.level.chunk.DataLayer;
import net.minecraft.world.level.chunk.LightChunkGetter;
-public class LevelLightEngine implements LightEventListener {
+public class LevelLightEngine implements LightEventListener, ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider {
public static final int LIGHT_SECTION_PADDING = 1;
protected final LevelHeightAccessor levelHeightAccessor;
- @Nullable
- private final LightEngine<?, ?> blockEngine;
- @Nullable
- private final LightEngine<?, ?> skyEngine;
+ // Paper start - rewrite chunk system
+ protected final ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface lightEngine;
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface starlight$getLightEngine() {
+ return this.lightEngine;
+ }
+
+ @Override
+ public void starlight$clientUpdateLight(final LightLayer lightType, final SectionPos pos,
+ final DataLayer nibble, final boolean trustEdges) {
+ throw new IllegalStateException("This hook is for the CLIENT ONLY"); // Paper - not implemented on server
+ }
+
+ @Override
+ public void starlight$clientRemoveLightData(final ChunkPos chunkPos) {
+ throw new IllegalStateException("This hook is for the CLIENT ONLY"); // Paper - not implemented on server
+ }
+
+ @Override
+ public void starlight$clientChunkLoad(final ChunkPos pos, final net.minecraft.world.level.chunk.LevelChunk chunk) {
+ throw new IllegalStateException("This hook is for the CLIENT ONLY"); // Paper - not implemented on server
+ }
+ // Paper end - rewrite chunk system
public LevelLightEngine(LightChunkGetter chunkProvider, boolean hasBlockLight, boolean hasSkyLight) {
this.levelHeightAccessor = chunkProvider.getLevel();
- this.blockEngine = hasBlockLight ? new BlockLightEngine(chunkProvider) : null;
- this.skyEngine = hasSkyLight ? new SkyLightEngine(chunkProvider) : null;
+ // Paper start - rewrite chunk system
+ if (chunkProvider.getLevel() instanceof net.minecraft.world.level.Level) {
+ this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(chunkProvider, hasSkyLight, hasBlockLight, (LevelLightEngine)(Object)this);
+ } else {
+ this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(null, hasSkyLight, hasBlockLight, (LevelLightEngine)(Object)this);
+ }
+ // Paper end - rewrite chunk system
}
@Override
public void checkBlock(BlockPos pos) {
- if (this.blockEngine != null) {
- this.blockEngine.checkBlock(pos);
- }
-
- if (this.skyEngine != null) {
- this.skyEngine.checkBlock(pos);
- }
+ this.lightEngine.blockChange(pos.immutable()); // Paper - rewrite chunk system
}
@Override
public boolean hasLightWork() {
- return this.skyEngine != null && this.skyEngine.hasLightWork() || this.blockEngine != null && this.blockEngine.hasLightWork();
+ return this.lightEngine.hasUpdates(); // Paper - rewrite chunk system
}
@Override
public int runLightUpdates() {
- int i = 0;
- if (this.blockEngine != null) {
- i += this.blockEngine.runLightUpdates();
- }
-
- if (this.skyEngine != null) {
- i += this.skyEngine.runLightUpdates();
- }
-
- return i;
+ final boolean hadUpdates = this.hasLightWork();
+ this.lightEngine.propagateChanges();
+ return hadUpdates ? 1 : 0; // Paper - rewrite chunk system
}
@Override
public void updateSectionStatus(SectionPos pos, boolean notReady) {
- if (this.blockEngine != null) {
- this.blockEngine.updateSectionStatus(pos, notReady);
- }
-
- if (this.skyEngine != null) {
- this.skyEngine.updateSectionStatus(pos, notReady);
- }
+ this.lightEngine.sectionChange(pos, notReady); // Paper - rewrite chunk system
}
@Override
public void setLightEnabled(ChunkPos pos, boolean retainData) {
- if (this.blockEngine != null) {
- this.blockEngine.setLightEnabled(pos, retainData);
- }
-
- if (this.skyEngine != null) {
- this.skyEngine.setLightEnabled(pos, retainData);
- }
+ // Paper - rewrite chunk system
}
@Override
public void propagateLightSources(ChunkPos chunkPos) {
- if (this.blockEngine != null) {
- this.blockEngine.propagateLightSources(chunkPos);
- }
-
- if (this.skyEngine != null) {
- this.skyEngine.propagateLightSources(chunkPos);
- }
+ // Paper - rewrite chunk system
}
public LayerLightEventListener getLayerListener(LightLayer lightType) {
- if (lightType == LightLayer.BLOCK) {
- return (LayerLightEventListener)(this.blockEngine == null ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : this.blockEngine);
- } else {
- return (LayerLightEventListener)(this.skyEngine == null ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : this.skyEngine);
- }
+ return lightType == LightLayer.BLOCK ? this.lightEngine.getBlockReader() : this.lightEngine.getSkyReader(); // Paper - rewrite chunk system
}
public String getDebugData(LightLayer lightType, SectionPos pos) {
- if (lightType == LightLayer.BLOCK) {
- if (this.blockEngine != null) {
- return this.blockEngine.getDebugData(pos.asLong());
- }
- } else if (this.skyEngine != null) {
- return this.skyEngine.getDebugData(pos.asLong());
- }
-
- return "n/a";
+ return "n/a"; // Paper - rewrite chunk system
}
public LayerLightSectionStorage.SectionType getDebugSectionType(LightLayer lightType, SectionPos pos) {
- if (lightType == LightLayer.BLOCK) {
- if (this.blockEngine != null) {
- return this.blockEngine.getDebugSectionType(pos.asLong());
- }
- } else if (this.skyEngine != null) {
- return this.skyEngine.getDebugSectionType(pos.asLong());
- }
-
- return LayerLightSectionStorage.SectionType.EMPTY;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) {
- if (lightType == LightLayer.BLOCK) {
- if (this.blockEngine != null) {
- this.blockEngine.queueSectionData(pos.asLong(), nibbles);
- }
- } else if (this.skyEngine != null) {
- this.skyEngine.queueSectionData(pos.asLong(), nibbles);
- }
+ // Paper - rewrite chunk system
}
public void retainData(ChunkPos pos, boolean retainData) {
- if (this.blockEngine != null) {
- this.blockEngine.retainData(pos, retainData);
- }
-
- if (this.skyEngine != null) {
- this.skyEngine.retainData(pos, retainData);
- }
+ // Paper - rewrite chunk system
}
public int getRawBrightness(BlockPos pos, int ambientDarkness) {
- int i = this.skyEngine == null ? 0 : this.skyEngine.getLightValue(pos) - ambientDarkness;
- int j = this.blockEngine == null ? 0 : this.blockEngine.getLightValue(pos);
- return Math.max(j, i);
+ return this.lightEngine.getRawBrightness(pos, ambientDarkness); // Paper - rewrite chunk system
}
public boolean lightOnInSection(SectionPos sectionPos) {
- long l = sectionPos.asLong();
- return this.blockEngine == null
- || this.blockEngine.storage.lightOnInSection(l) && (this.skyEngine == null || this.skyEngine.storage.lightOnInSection(l));
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system // Paper - not implemented on server
}
public int getLightSectionCount() {
diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java
index c8f7c43134e7c51ce8af5b3c1a28c11db67715a2..29123f3a2f211c08d1a9ccf62ca9bc9822f90111 100644
--- a/src/main/java/net/minecraft/world/phys/AABB.java
+++ b/src/main/java/net/minecraft/world/phys/AABB.java
@@ -326,7 +326,7 @@ public class AABB {
}
@Nullable
- private static Direction getDirection(
+ public static Direction getDirection( // Paper - optimise collisions - public
AABB box, Vec3 intersectingVector, double[] traceDistanceResult, @Nullable Direction approachDirection, double deltaX, double deltaY, double deltaZ
) {
if (deltaX > 1.0E-7) {
diff --git a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
index 4fee67f7214b464b9e09862778e3ef187fcb8b72..31a54af04ab072a433d6df9fe37beb12243fea80 100644
--- a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
+++ b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
@@ -20,7 +20,7 @@ public class ArrayVoxelShape extends VoxelShape {
);
}
- ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) {
+ public ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) { // Paper - optimise collisions - public
super(shape);
int i = shape.getXSize() + 1;
int j = shape.getYSize() + 1;
@@ -34,6 +34,7 @@ public class ArrayVoxelShape extends VoxelShape {
new IllegalArgumentException("Lengths of point arrays must be consistent with the size of the VoxelShape.")
);
}
+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this).moonrise$initCache(); // Paper - optimise collisions
}
@Override
diff --git a/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
index e8f3307727e7e3da9a7629cafc6e1ee53790b75d..97ef481156ec5d821779f126ab98a8f28cbaf30b 100644
--- a/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
+++ b/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
@@ -4,13 +4,13 @@ import java.util.BitSet;
import net.minecraft.core.Direction;
public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape {
- private final BitSet storage;
- private int xMin;
- private int yMin;
- private int zMin;
- private int xMax;
- private int yMax;
- private int zMax;
+ public final BitSet storage; // Paper - optimise collisions - public
+ public int xMin; // Paper - optimise collisions - public
+ public int yMin; // Paper - optimise collisions - public
+ public int zMin; // Paper - optimise collisions - public
+ public int xMax; // Paper - optimise collisions - public
+ public int yMax; // Paper - optimise collisions - public
+ public int zMax; // Paper - optimise collisions - public
public BitSetDiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) {
super(sizeX, sizeY, sizeZ);
@@ -150,47 +150,109 @@ public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape {
return bitSetDiscreteVoxelShape;
}
- protected static void forAllBoxes(DiscreteVoxelShape voxelSet, DiscreteVoxelShape.IntLineConsumer callback, boolean coalesce) {
- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = new BitSetDiscreteVoxelShape(voxelSet);
+ // Paper start - optimise collisions
+ public static void forAllBoxes(final DiscreteVoxelShape shape, final DiscreteVoxelShape.IntLineConsumer consumer, final boolean mergeAdjacent) {
+ // Paper - remove debug
+ // called with the shape of a VoxelShape, so we can expect the cache to exist
+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cache = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape) shape).moonrise$getOrCreateCachedShapeData();
+
+ final int sizeX = cache.sizeX();
+ final int sizeY = cache.sizeY();
+ final int sizeZ = cache.sizeZ();
+
+ int indexX;
+ int indexY = 0;
+ int indexZ;
+
+ int incY = sizeZ;
+ int incX = sizeZ * sizeY;
+
+ long[] bitset = cache.voxelSet();
+
+ // index = z + y*size_z + x*(size_z*size_y)
+
+ if (!mergeAdjacent) {
+ // due to the odd selection of loop order (which does affect behavior, unfortunately) we can't simply
+ // increment an index in the Z loop, and have to perform this trash (keeping track of 3 counters) to avoid
+ // the multiplication
+ for (int y = 0; y < sizeY; ++y, indexY += incY) {
+ indexX = indexY;
+ for (int x = 0; x < sizeX; ++x, indexX += incX) {
+ indexZ = indexX;
+ for (int z = 0; z < sizeZ; ++z, ++indexZ) {
+ if ((bitset[indexZ >>> 6] & (1L << indexZ)) != 0L) {
+ consumer.consume(x, y, z, x + 1, y + 1, z + 1);
+ }
+ }
+ }
+ }
+ } else {
+ // same notes about loop order as the above
+ // this branch is actually important to optimise, as it affects uncached toAabbs() (which affects optimize())
- for (int i = 0; i < bitSetDiscreteVoxelShape.ySize; i++) {
- for (int j = 0; j < bitSetDiscreteVoxelShape.xSize; j++) {
- int k = -1;
+ // only clone when we may write to it
+ bitset = ca.spottedleaf.moonrise.common.util.MixinWorkarounds.clone(bitset);
- for (int l = 0; l <= bitSetDiscreteVoxelShape.zSize; l++) {
- if (bitSetDiscreteVoxelShape.isFullWide(j, i, l)) {
- if (coalesce) {
- if (k == -1) {
- k = l;
- }
- } else {
- callback.consume(j, i, l, j + 1, i + 1, l + 1);
+ for (int y = 0; y < sizeY; ++y, indexY += incY) {
+ indexX = indexY;
+ for (int x = 0; x < sizeX; ++x, indexX += incX) {
+ for (int zIdx = indexX, endIndex = indexX + sizeZ; zIdx < endIndex; ) {
+ final int firstSetZ = ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.firstSet(bitset, zIdx, endIndex);
+
+ if (firstSetZ == -1) {
+ break;
+ }
+
+ int lastSetZ = ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.firstClear(bitset, firstSetZ, endIndex);
+ if (lastSetZ == -1) {
+ lastSetZ = endIndex;
}
- } else if (k != -1) {
- int m = j;
- int n = i;
- bitSetDiscreteVoxelShape.clearZStrip(k, l, j, i);
-
- while (bitSetDiscreteVoxelShape.isZStripFull(k, l, m + 1, i)) {
- bitSetDiscreteVoxelShape.clearZStrip(k, l, m + 1, i);
- m++;
+
+ ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.clearRange(bitset, firstSetZ, lastSetZ);
+
+ // try to merge neighbouring on the X axis
+ int endX = x + 1; // exclusive
+ for (int neighbourIdxStart = firstSetZ + incX, neighbourIdxEnd = lastSetZ + incX;
+ endX < sizeX && ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.isRangeSet(bitset, neighbourIdxStart, neighbourIdxEnd);
+ neighbourIdxStart += incX, neighbourIdxEnd += incX) {
+
+ ++endX;
+ ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.clearRange(bitset, neighbourIdxStart, neighbourIdxEnd);
}
- while (bitSetDiscreteVoxelShape.isXZRectangleFull(j, m + 1, k, l, n + 1)) {
- for (int o = j; o <= m; o++) {
- bitSetDiscreteVoxelShape.clearZStrip(k, l, o, n + 1);
+ // try to merge neighbouring on the Y axis
+
+ int endY; // exclusive
+ int firstSetZY, lastSetZY;
+ y_merge:
+ for (endY = y + 1, firstSetZY = firstSetZ + incY, lastSetZY = lastSetZ + incY; endY < sizeY;
+ firstSetZY += incY, lastSetZY += incY) {
+
+ // test the whole XZ range
+ for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX;
+ ++testX, start += incX, end += incX) {
+ if (!ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.isRangeSet(bitset, start, end)) {
+ break y_merge;
+ }
}
- n++;
+ ++endY;
+
+ // passed, so we can clear it
+ for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX;
+ ++testX, start += incX, end += incX) {
+ ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.clearRange(bitset, start, end);
+ }
}
- callback.consume(j, i, k, m + 1, n + 1, l);
- k = -1;
+ consumer.consume(x, y, firstSetZ - indexX, endX, endY, lastSetZ - indexX);
+ zIdx = lastSetZ;
}
}
}
}
}
+ // Paper end - optimise collisions
private boolean isZStripFull(int z1, int z2, int x, int y) {
return x < this.xSize && y < this.ySize && this.storage.nextClearBit(this.getIndex(x, y, z1)) >= this.getIndex(x, y, z2);
diff --git a/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java
index d812949c7329ae2696b38dc792fa011ba87decb9..7743495c7ec3fc5e17947144457cef7bbe0f4b38 100644
--- a/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java
+++ b/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java
@@ -7,6 +7,7 @@ import net.minecraft.util.Mth;
public final class CubeVoxelShape extends VoxelShape {
protected CubeVoxelShape(DiscreteVoxelShape voxels) {
super(voxels);
+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this).moonrise$initCache(); // Paper - optimise collisions
}
@Override
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 01693ba050b12b9debcdaefceeff9cbcd503b369..1d36f8dcffd22cf844448d3d8351fb8718cf5227 100644
--- a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
+++ b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
@@ -3,12 +3,79 @@ package net.minecraft.world.phys.shapes;
import net.minecraft.core.AxisCycle;
import net.minecraft.core.Direction;
-public abstract class DiscreteVoxelShape {
+public abstract class DiscreteVoxelShape implements ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape { // Paper - optimise collisions
private static final Direction.Axis[] AXIS_VALUES = Direction.Axis.values();
protected final int xSize;
protected final int ySize;
protected final int zSize;
+ // Paper start - optimise collisions
+ // ignore race conditions on field read/write: the shape is static, so it doesn't matter
+ private ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cachedShapeData;
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getOrCreateCachedShapeData() {
+ if (this.cachedShapeData != null) {
+ return this.cachedShapeData;
+ }
+
+ final DiscreteVoxelShape discreteVoxelShape = (DiscreteVoxelShape)(Object)this;
+
+ final int sizeX = discreteVoxelShape.getXSize();
+ final int sizeY = discreteVoxelShape.getYSize();
+ final int sizeZ = discreteVoxelShape.getZSize();
+
+ final int maxIndex = sizeX * sizeY * sizeZ; // exclusive
+
+ final int longsRequired = (maxIndex + (Long.SIZE - 1)) >>> 6;
+ long[] voxelSet;
+
+ final boolean isEmpty = discreteVoxelShape.isEmpty();
+
+ if (discreteVoxelShape instanceof BitSetDiscreteVoxelShape bitsetShape) {
+ voxelSet = bitsetShape.storage.toLongArray();
+ if (voxelSet.length < longsRequired) {
+ // happens when the later long values are 0L, so we need to resize
+ voxelSet = java.util.Arrays.copyOf(voxelSet, longsRequired);
+ }
+ } else {
+ voxelSet = new long[longsRequired];
+ if (!isEmpty) {
+ final int mulX = sizeZ * sizeY;
+ for (int x = 0; x < sizeX; ++x) {
+ for (int y = 0; y < sizeY; ++y) {
+ for (int z = 0; z < sizeZ; ++z) {
+ if (discreteVoxelShape.isFull(x, y, z)) {
+ // index = z + y*size_z + x*(size_z*size_y)
+ final int index = z + y * sizeZ + x * mulX;
+
+ voxelSet[index >>> 6] |= 1L << index;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ final boolean hasSingleAABB = sizeX == 1 && sizeY == 1 && sizeZ == 1 && !isEmpty && discreteVoxelShape.isFull(0, 0, 0);
+
+ final int minFullX = discreteVoxelShape.firstFull(Direction.Axis.X);
+ final int minFullY = discreteVoxelShape.firstFull(Direction.Axis.Y);
+ final int minFullZ = discreteVoxelShape.firstFull(Direction.Axis.Z);
+
+ final int maxFullX = discreteVoxelShape.lastFull(Direction.Axis.X);
+ final int maxFullY = discreteVoxelShape.lastFull(Direction.Axis.Y);
+ final int maxFullZ = discreteVoxelShape.lastFull(Direction.Axis.Z);
+
+ return this.cachedShapeData = new ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData(
+ sizeX, sizeY, sizeZ, voxelSet,
+ minFullX, minFullY, minFullZ,
+ maxFullX, maxFullY, maxFullZ,
+ isEmpty, hasSingleAABB
+ );
+ }
+ // Paper end - optimise collisions
+
protected DiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) {
if (sizeX >= 0 && sizeY >= 0 && sizeZ >= 0) {
this.xSize = sizeX;
diff --git a/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java b/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java
index 7ec02a7849437a18860aa0df7d9ddd71b2447d4c..5e45e49ab09344cb95736f4124b1c6e002ef5b82 100644
--- a/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java
+++ b/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java
@@ -4,8 +4,8 @@ import it.unimi.dsi.fastutil.doubles.AbstractDoubleList;
import it.unimi.dsi.fastutil.doubles.DoubleList;
public class OffsetDoubleList extends AbstractDoubleList {
- private final DoubleList delegate;
- private final double offset;
+ public final DoubleList delegate; // Paper - optimise collisions - public
+ public final double offset; // Paper - optimise collisions - public
public OffsetDoubleList(DoubleList oldList, double offset) {
this.delegate = oldList;
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 0fdd2cdd8d215ca1523eda8ad7316cdd5f41a6b5..7ede56f77af1d40e10fde2e660d5794e4b37dc5d 100644
--- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
+++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
@@ -16,9 +16,15 @@ public final class Shapes {
public static final double EPSILON = 1.0E-7;
public static final double BIG_EPSILON = 1.0E-6;
private static final VoxelShape BLOCK = Util.make(() -> {
- DiscreteVoxelShape discreteVoxelShape = new BitSetDiscreteVoxelShape(1, 1, 1);
- discreteVoxelShape.fill(0, 0, 0);
- return new CubeVoxelShape(discreteVoxelShape);
+ // Paper start - optimise collisions
+ final DiscreteVoxelShape shape = new BitSetDiscreteVoxelShape(1, 1, 1);
+ shape.fill(0, 0, 0);
+
+ return new ArrayVoxelShape(
+ shape,
+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE
+ );
+ // Paper end - optimise collisions
});
public static final VoxelShape INFINITY = box(
Double.NEGATIVE_INFINITY,
@@ -43,6 +49,30 @@ public final class Shapes {
return BLOCK;
}
+ // Paper start - optimise collisions
+ private static final DoubleArrayList[] PARTS_BY_BITS = new DoubleArrayList[] {
+ DoubleArrayList.wrap(generateCubeParts(1 << 0)),
+ DoubleArrayList.wrap(generateCubeParts(1 << 1)),
+ DoubleArrayList.wrap(generateCubeParts(1 << 2)),
+ DoubleArrayList.wrap(generateCubeParts(1 << 3))
+ };
+
+ private static double[] generateCubeParts(final int parts) {
+ // note: parts is a power of two, so we do not need to worry about loss of precision here
+ // note: parts is from [2^0, 2^3]
+ final double inc = 1.0 / (double)parts;
+
+ final double[] ret = new double[parts + 1];
+ double val = 0.0;
+ for (int i = 0; i <= parts; ++i) {
+ ret[i] = val;
+ val += inc;
+ }
+
+ return ret;
+ }
+ // Paper end - optimise collisions
+
public static VoxelShape box(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
if (!(minX > maxX) && !(minY > maxY) && !(minZ > maxZ)) {
return create(minX, minY, minZ, maxX, maxY, maxZ);
@@ -52,39 +82,42 @@ public final class Shapes {
}
public static VoxelShape create(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
+ // Paper start - optimise collisions
if (!(maxX - minX < 1.0E-7) && !(maxY - minY < 1.0E-7) && !(maxZ - minZ < 1.0E-7)) {
- int i = findBits(minX, maxX);
- int j = findBits(minY, maxY);
- int k = findBits(minZ, maxZ);
- if (i < 0 || j < 0 || k < 0) {
+ final int bitsX = findBits(minX, maxX);
+ final int bitsY = findBits(minY, maxY);
+ final int bitsZ = findBits(minZ, maxZ);
+ if (bitsX >= 0 && bitsY >= 0 && bitsZ >= 0) {
+ if (bitsX == 0 && bitsY == 0 && bitsZ == 0) {
+ return BLOCK;
+ } else {
+ final int sizeX = 1 << bitsX;
+ final int sizeY = 1 << bitsY;
+ final int sizeZ = 1 << bitsZ;
+ final BitSetDiscreteVoxelShape shape = BitSetDiscreteVoxelShape.withFilledBounds(
+ sizeX, sizeY, sizeZ,
+ (int)Math.round(minX * (double)sizeX), (int)Math.round(minY * (double)sizeY), (int)Math.round(minZ * (double)sizeZ),
+ (int)Math.round(maxX * (double)sizeX), (int)Math.round(maxY * (double)sizeY), (int)Math.round(maxZ * (double)sizeZ)
+ );
+ return new ArrayVoxelShape(
+ shape,
+ PARTS_BY_BITS[bitsX],
+ PARTS_BY_BITS[bitsY],
+ PARTS_BY_BITS[bitsZ]
+ );
+ }
+ } else {
return new ArrayVoxelShape(
BLOCK.shape,
- DoubleArrayList.wrap(new double[]{minX, maxX}),
- DoubleArrayList.wrap(new double[]{minY, maxY}),
- DoubleArrayList.wrap(new double[]{minZ, maxZ})
- );
- } else if (i == 0 && j == 0 && k == 0) {
- return block();
- } else {
- int l = 1 << i;
- int m = 1 << j;
- int n = 1 << k;
- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.withFilledBounds(
- l,
- m,
- n,
- (int)Math.round(minX * (double)l),
- (int)Math.round(minY * (double)m),
- (int)Math.round(minZ * (double)n),
- (int)Math.round(maxX * (double)l),
- (int)Math.round(maxY * (double)m),
- (int)Math.round(maxZ * (double)n)
+ minX == 0.0 && maxX == 1.0 ? ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minX, maxX }),
+ minY == 0.0 && maxY == 1.0 ? ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minY, maxY }),
+ minZ == 0.0 && maxZ == 1.0 ? ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minZ, maxZ })
);
- return new CubeVoxelShape(bitSetDiscreteVoxelShape);
}
} else {
- return empty();
+ return EMPTY;
}
+ // Paper end - optimise collisions
}
public static VoxelShape create(AABB box) {
@@ -119,80 +152,54 @@ public final class Shapes {
return join(first, second, BooleanOp.OR);
}
- public static VoxelShape or(VoxelShape first, VoxelShape... others) {
- return Arrays.stream(others).reduce(first, Shapes::or);
+ // Paper start - optimise collisions
+ public static VoxelShape or(VoxelShape shape, VoxelShape... others) {
+ int size = others.length;
+ if (size == 0) {
+ return shape;
+ }
+
+ // reduce complexity of joins by splitting the merges
+
+ // add extra slot for first shape
+ ++size;
+ final VoxelShape[] tmp = Arrays.copyOf(others, size);
+ // insert first shape
+ tmp[size - 1] = shape;
+
+ while (size > 1) {
+ int newSize = 0;
+ for (int i = 0; i < size; i += 2) {
+ final int next = i + 1;
+ if (next >= size) {
+ // nothing to merge with, so leave it for next iteration
+ tmp[newSize++] = tmp[i];
+ break;
+ } else {
+ // merge with adjacent
+ final VoxelShape first = tmp[i];
+ final VoxelShape second = tmp[next];
+
+ tmp[newSize++] = Shapes.or(first, second);
+ }
+ }
+ size = newSize;
+ }
+
+ return tmp[0];
+ // Paper end - optimise collisions
}
public static VoxelShape join(VoxelShape first, VoxelShape second, BooleanOp function) {
- return joinUnoptimized(first, second, function).optimize();
+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.joinOptimized(first, second, function); // Paper - optimise collisions
}
public static VoxelShape joinUnoptimized(VoxelShape one, VoxelShape two, BooleanOp function) {
- if (function.apply(false, false)) {
- throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException());
- } else if (one == two) {
- return function.apply(true, true) ? one : empty();
- } else {
- boolean bl = function.apply(true, false);
- boolean bl2 = function.apply(false, true);
- if (one.isEmpty()) {
- return bl2 ? two : empty();
- } else if (two.isEmpty()) {
- return bl ? one : empty();
- } else {
- IndexMerger indexMerger = createIndexMerger(1, one.getCoords(Direction.Axis.X), two.getCoords(Direction.Axis.X), bl, bl2);
- IndexMerger indexMerger2 = createIndexMerger(indexMerger.size() - 1, one.getCoords(Direction.Axis.Y), two.getCoords(Direction.Axis.Y), bl, bl2);
- IndexMerger indexMerger3 = createIndexMerger(
- (indexMerger.size() - 1) * (indexMerger2.size() - 1), one.getCoords(Direction.Axis.Z), two.getCoords(Direction.Axis.Z), bl, bl2
- );
- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.join(
- one.shape, two.shape, indexMerger, indexMerger2, indexMerger3, function
- );
- return (VoxelShape)(indexMerger instanceof DiscreteCubeMerger
- && indexMerger2 instanceof DiscreteCubeMerger
- && indexMerger3 instanceof DiscreteCubeMerger
- ? new CubeVoxelShape(bitSetDiscreteVoxelShape)
- : new ArrayVoxelShape(bitSetDiscreteVoxelShape, indexMerger.getList(), indexMerger2.getList(), indexMerger3.getList()));
- }
- }
+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.joinUnoptimized(one, two, function); // Paper - optimise collisions
}
public static boolean joinIsNotEmpty(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) {
- if (predicate.apply(false, false)) {
- throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException());
- } else {
- boolean bl = shape1.isEmpty();
- boolean bl2 = shape2.isEmpty();
- if (!bl && !bl2) {
- if (shape1 == shape2) {
- return predicate.apply(true, true);
- } else {
- boolean bl3 = predicate.apply(true, false);
- boolean bl4 = predicate.apply(false, true);
-
- for (Direction.Axis axis : AxisCycle.AXIS_VALUES) {
- if (shape1.max(axis) < shape2.min(axis) - 1.0E-7) {
- return bl3 || bl4;
- }
-
- if (shape2.max(axis) < shape1.min(axis) - 1.0E-7) {
- return bl3 || bl4;
- }
- }
-
- IndexMerger indexMerger = createIndexMerger(1, shape1.getCoords(Direction.Axis.X), shape2.getCoords(Direction.Axis.X), bl3, bl4);
- IndexMerger indexMerger2 = createIndexMerger(
- indexMerger.size() - 1, shape1.getCoords(Direction.Axis.Y), shape2.getCoords(Direction.Axis.Y), bl3, bl4
- );
- IndexMerger indexMerger3 = createIndexMerger(
- (indexMerger.size() - 1) * (indexMerger2.size() - 1), shape1.getCoords(Direction.Axis.Z), shape2.getCoords(Direction.Axis.Z), bl3, bl4
- );
- return joinIsNotEmpty(indexMerger, indexMerger2, indexMerger3, shape1.shape, shape2.shape, predicate);
- }
- } else {
- return predicate.apply(!bl, !bl2);
- }
- }
+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isJoinNonEmpty(shape1, shape2, predicate); // Paper - optimise collisions
}
private static boolean joinIsNotEmpty(
@@ -219,70 +226,120 @@ public final class Shapes {
return maxDist;
}
- public static boolean blockOccudes(VoxelShape shape, VoxelShape neighbor, Direction direction) {
- if (shape == block() && neighbor == block()) {
+ // Paper start - optimise collisions
+ public static boolean blockOccudes(final VoxelShape first, final VoxelShape second, final Direction direction) {
+ final boolean firstBlock = first == BLOCK;
+ final boolean secondBlock = second == BLOCK;
+
+ if (firstBlock & secondBlock) {
return true;
- } else if (neighbor.isEmpty()) {
+ }
+
+ if (first.isEmpty() | second.isEmpty()) {
+ return false;
+ }
+
+ // we optimise getOpposite, so we can use it
+ // secondly, use our cache to retrieve sliced shape
+ final VoxelShape newFirst = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getFaceShapeClamped(direction);
+ if (newFirst.isEmpty()) {
return false;
- } else {
- Direction.Axis axis = direction.getAxis();
- Direction.AxisDirection axisDirection = direction.getAxisDirection();
- VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? shape : neighbor;
- VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? neighbor : shape;
- BooleanOp booleanOp = axisDirection == Direction.AxisDirection.POSITIVE ? BooleanOp.ONLY_FIRST : BooleanOp.ONLY_SECOND;
- return DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7)
- && DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7)
- && !joinIsNotEmpty(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), booleanOp);
}
+ final VoxelShape newSecond = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getFaceShapeClamped(direction.getOpposite());
+ if (newSecond.isEmpty()) {
+ return false;
+ }
+
+ return !joinIsNotEmpty(newFirst, newSecond, BooleanOp.ONLY_FIRST);
+ // Paper end - optimise collisions
}
public static VoxelShape getFaceShape(VoxelShape shape, Direction direction) {
- 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 ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$getFaceShapeClamped(direction); // Paper - optimise collisions
+ }
- return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i));
- }
+ // Paper start - optimise collisions
+ private static boolean mergedMayOccludeBlock(final VoxelShape shape1, final VoxelShape shape2) {
+ // if the combined bounds of the two shapes cannot occlude, then neither can the merged
+ final AABB bounds1 = shape1.bounds();
+ final AABB bounds2 = shape2.bounds();
+
+ final double minX = Math.min(bounds1.minX, bounds2.minX);
+ final double minY = Math.min(bounds1.minY, bounds2.minY);
+ final double minZ = Math.min(bounds1.minZ, bounds2.minZ);
+
+ final double maxX = Math.max(bounds1.maxX, bounds2.maxX);
+ final double maxY = Math.max(bounds1.maxY, bounds2.maxY);
+ final double maxZ = Math.max(bounds1.maxZ, bounds2.maxZ);
+
+ return (minX <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && maxX >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) &&
+ (minY <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && maxY >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) &&
+ (minZ <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && maxZ >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON));
}
+ // Paper end - optimise collisions
- public static boolean mergedFaceOccludes(VoxelShape one, VoxelShape two, Direction direction) {
- if (one != block() && two != block()) {
- Direction.Axis axis = direction.getAxis();
- Direction.AxisDirection axisDirection = direction.getAxisDirection();
- VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? one : two;
- VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? two : one;
- if (!DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7)) {
- voxelShape = empty();
- }
+ // Paper start - optimise collisions
+ public static boolean mergedFaceOccludes(final VoxelShape first, final VoxelShape second, final Direction direction) {
+ // see if any of the shapes on their own occludes, only if cached
+ if (((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$occludesFullBlockIfCached() || ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$occludesFullBlockIfCached()) {
+ return true;
+ }
- if (!DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7)) {
- voxelShape2 = empty();
- }
+ if (first.isEmpty() & second.isEmpty()) {
+ return false;
+ }
- return !joinIsNotEmpty(
- block(),
- joinUnoptimized(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), BooleanOp.OR),
- BooleanOp.ONLY_FIRST
- );
- } else {
+ // we optimise getOpposite, so we can use it
+ // secondly, use our cache to retrieve sliced shape
+ final VoxelShape newFirst = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getFaceShapeClamped(direction);
+ final VoxelShape newSecond = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getFaceShapeClamped(direction.getOpposite());
+
+ // see if any of the shapes on their own occludes, only if cached
+ if (((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newFirst).moonrise$occludesFullBlockIfCached() || ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newSecond).moonrise$occludesFullBlockIfCached()) {
return true;
}
+
+ final boolean firstEmpty = newFirst.isEmpty();
+ final boolean secondEmpty = newSecond.isEmpty();
+
+ if (firstEmpty & secondEmpty) {
+ return false;
+ }
+
+ if (firstEmpty | secondEmpty) {
+ return secondEmpty ? ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newFirst).moonrise$occludesFullBlock() : ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newSecond).moonrise$occludesFullBlock();
+ }
+
+ if (newFirst == newSecond) {
+ return ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newFirst).moonrise$occludesFullBlock();
+ }
+
+ return mergedMayOccludeBlock(newFirst, newSecond) && ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newFirst).moonrise$orUnoptimized(newSecond)).moonrise$occludesFullBlock();
}
+ // Paper end - optimise collisions
+
+ // Paper start - optimise collisions
+ public static boolean faceShapeOccludes(final VoxelShape shape1, final VoxelShape shape2) {
+ if (((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape1).moonrise$occludesFullBlockIfCached() || ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape2).moonrise$occludesFullBlockIfCached()) {
+ return true;
+ }
+
+ final boolean s1Empty = shape1.isEmpty();
+ final boolean s2Empty = shape2.isEmpty();
+ if (s1Empty & s2Empty) {
+ return false;
+ }
+
+ if (s1Empty | s2Empty) {
+ return s2Empty ? ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape1).moonrise$occludesFullBlock() : ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape2).moonrise$occludesFullBlock();
+ }
+
+ if (shape1 == shape2) {
+ return ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape1).moonrise$occludesFullBlock();
+ }
- public static boolean faceShapeOccludes(VoxelShape one, VoxelShape two) {
- return one == block()
- || two == block()
- || (!one.isEmpty() || !two.isEmpty()) && !joinIsNotEmpty(block(), joinUnoptimized(one, two, BooleanOp.OR), BooleanOp.ONLY_FIRST);
+ return mergedMayOccludeBlock(shape1, shape2) && ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape1).moonrise$orUnoptimized(shape2)).moonrise$occludesFullBlock();
+ // Paper end - optimise collisions
}
@VisibleForTesting
diff --git a/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java b/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java
index b07f1c58e00d232e7c83e6df3499e4b677645609..b88c71f27996d24d29048e06a69a004617eb53a2 100644
--- a/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java
+++ b/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java
@@ -12,6 +12,7 @@ public class SliceShape extends VoxelShape {
super(makeSlice(shape.shape, axis, sliceWidth));
this.delegate = shape;
this.axis = axis;
+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this).moonrise$initCache(); // Paper - optimise collisions
}
private static DiscreteVoxelShape makeSlice(DiscreteVoxelShape voxelSet, Direction.Axis axis, int sliceWidth) {
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 d440003ca4403511a964f61bcf67ac2cd75c5359..11824d39e72fa003b3a56aa9b8d679fe8e23a1a4 100644
--- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
+++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
@@ -15,38 +15,505 @@ import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
-public abstract class VoxelShape {
- protected final DiscreteVoxelShape shape;
+public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape { // Paper - optimise collisions
+ public final DiscreteVoxelShape shape; // Paper - optimise collisions - public
@Nullable
private VoxelShape[] faces;
+ // Paper start - optimise collisions
+ private double offsetX;
+ private double offsetY;
+ private double offsetZ;
+ private AABB singleAABBRepresentation;
+ private double[] rootCoordinatesX;
+ private double[] rootCoordinatesY;
+ private double[] rootCoordinatesZ;
+ private ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cachedShapeData;
+ private boolean isEmpty;
+ private ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cachedToAABBs;
+ private AABB cachedBounds;
+ private Boolean isFullBlock;
+ private Boolean occludesFullBlock;
+
+ // must be power of two
+ private static final int MERGED_CACHE_SIZE = 16;
+ private ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache[] mergedORCache;
+
+ @Override
+ public final double moonrise$offsetX() {
+ return this.offsetX;
+ }
+
+ @Override
+ public final double moonrise$offsetY() {
+ return this.offsetY;
+ }
+
+ @Override
+ public final double moonrise$offsetZ() {
+ return this.offsetZ;
+ }
+
+ @Override
+ public final AABB moonrise$getSingleAABBRepresentation() {
+ return this.singleAABBRepresentation;
+ }
+
+ @Override
+ public final double[] moonrise$rootCoordinatesX() {
+ return this.rootCoordinatesX;
+ }
+
+ @Override
+ public final double[] moonrise$rootCoordinatesY() {
+ return this.rootCoordinatesY;
+ }
+
+ @Override
+ public final double[] moonrise$rootCoordinatesZ() {
+ return this.rootCoordinatesZ;
+ }
+
+ private static double[] extractRawArray(final DoubleList list) {
+ if (list instanceof it.unimi.dsi.fastutil.doubles.DoubleArrayList rawList) {
+ final double[] raw = rawList.elements();
+ final int expected = rawList.size();
+ if (raw.length == expected) {
+ return raw;
+ } else {
+ return java.util.Arrays.copyOf(raw, expected);
+ }
+ } else {
+ return list.toDoubleArray();
+ }
+ }
+
+ @Override
+ public final void moonrise$initCache() {
+ this.cachedShapeData = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape)this.shape).moonrise$getOrCreateCachedShapeData();
+ this.isEmpty = this.cachedShapeData.isEmpty();
+
+ final DoubleList xList = this.getCoords(Direction.Axis.X);
+ final DoubleList yList = this.getCoords(Direction.Axis.Y);
+ final DoubleList zList = this.getCoords(Direction.Axis.Z);
+
+ if (xList instanceof OffsetDoubleList offsetDoubleList) {
+ this.offsetX = offsetDoubleList.offset;
+ this.rootCoordinatesX = extractRawArray(offsetDoubleList.delegate);
+ } else {
+ this.rootCoordinatesX = extractRawArray(xList);
+ }
+
+ if (yList instanceof OffsetDoubleList offsetDoubleList) {
+ this.offsetY = offsetDoubleList.offset;
+ this.rootCoordinatesY = extractRawArray(offsetDoubleList.delegate);
+ } else {
+ this.rootCoordinatesY = extractRawArray(yList);
+ }
+
+ if (zList instanceof OffsetDoubleList offsetDoubleList) {
+ this.offsetZ = offsetDoubleList.offset;
+ this.rootCoordinatesZ = extractRawArray(offsetDoubleList.delegate);
+ } else {
+ this.rootCoordinatesZ = extractRawArray(zList);
+ }
+
+ if (this.cachedShapeData.hasSingleAABB()) {
+ this.singleAABBRepresentation = new AABB(
+ this.rootCoordinatesX[0] + this.offsetX, this.rootCoordinatesY[0] + this.offsetY, this.rootCoordinatesZ[0] + this.offsetZ,
+ this.rootCoordinatesX[1] + this.offsetX, this.rootCoordinatesY[1] + this.offsetY, this.rootCoordinatesZ[1] + this.offsetZ
+ );
+ this.cachedBounds = this.singleAABBRepresentation;
+ }
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getCachedVoxelData() {
+ return this.cachedShapeData;
+ }
+
+ private VoxelShape[] faceShapeClampedCache;
+
+ @Override
+ public final VoxelShape moonrise$getFaceShapeClamped(final Direction direction) {
+ if (this.isEmpty) {
+ return (VoxelShape)(Object)this;
+ }
+ if ((VoxelShape)(Object)this == Shapes.block()) {
+ return (VoxelShape)(Object)this;
+ }
+
+ VoxelShape[] cache = this.faceShapeClampedCache;
+ if (cache != null) {
+ final VoxelShape ret = cache[direction.ordinal()];
+ if (ret != null) {
+ return ret;
+ }
+ }
+
+
+ if (cache == null) {
+ this.faceShapeClampedCache = cache = new VoxelShape[6];
+ }
+
+ final Direction.Axis axis = direction.getAxis();
+
+ final VoxelShape ret;
+
+ 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));
+ } 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));
+ } else {
+ ret = Shapes.empty();
+ }
+ }
+
+ cache[direction.ordinal()] = ret;
+
+ 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;
+ return false;
+ }
+
+ if (this.moonrise$isFullBlock()) {
+ this.occludesFullBlock = Boolean.TRUE;
+ return true;
+ }
+
+ final AABB singleAABB = this.singleAABBRepresentation;
+ if (singleAABB != null) {
+ // check if the bounding box encloses the full cube
+ final boolean ret =
+ (singleAABB.minY <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && singleAABB.maxY >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) &&
+ (singleAABB.minX <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && singleAABB.maxX >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) &&
+ (singleAABB.minZ <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && singleAABB.maxZ >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON));
+ this.occludesFullBlock = Boolean.valueOf(ret);
+ return ret;
+ }
+
+ final boolean ret = !Shapes.joinIsNotEmpty(Shapes.block(), ((VoxelShape)(Object)this), BooleanOp.ONLY_FIRST);
+ this.occludesFullBlock = Boolean.valueOf(ret);
+ return ret;
+ }
+
+ @Override
+ public final boolean moonrise$occludesFullBlock() {
+ final Boolean ret = this.occludesFullBlock;
+ if (ret != null) {
+ return ret.booleanValue();
+ }
+
+ return this.computeOccludesFullBlock();
+ }
+
+ @Override
+ public final boolean moonrise$occludesFullBlockIfCached() {
+ final Boolean ret = this.occludesFullBlock;
+ return ret != null ? ret.booleanValue() : false;
+ }
+
+ private static int hash(final VoxelShape key) {
+ return it.unimi.dsi.fastutil.HashCommon.mix(System.identityHashCode(key));
+ }
+
+ @Override
+ public final VoxelShape moonrise$orUnoptimized(final VoxelShape other) {
+ // don't cache simple cases
+ if (((VoxelShape)(Object)this) == other) {
+ return other;
+ }
+
+ if (this.isEmpty) {
+ return other;
+ }
+
+ if (other.isEmpty()) {
+ return (VoxelShape)(Object)this;
+ }
+
+ // try this cache first
+ final int thisCacheKey = hash(other) & (MERGED_CACHE_SIZE - 1);
+ final ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache cached = this.mergedORCache == null ? null : this.mergedORCache[thisCacheKey];
+ if (cached != null && cached.key() == other) {
+ return cached.result();
+ }
+
+ // try other cache
+ final int otherCacheKey = hash((VoxelShape)(Object)this) & (MERGED_CACHE_SIZE - 1);
+ final ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache otherCache = ((VoxelShape)(Object)other).mergedORCache == null ? null : ((VoxelShape)(Object)other).mergedORCache[otherCacheKey];
+ if (otherCache != null && otherCache.key() == (VoxelShape)(Object)this) {
+ return otherCache.result();
+ }
+
+ // note: unsure if joinUnoptimized(1, 2, OR) == joinUnoptimized(2, 1, OR) for all cases
+ final VoxelShape result = Shapes.joinUnoptimized((VoxelShape)(Object)this, other, BooleanOp.OR);
+
+ if (cached != null && otherCache == null) {
+ // try to use second cache instead of replacing an entry in this cache
+ if (((VoxelShape)(Object)other).mergedORCache == null) {
+ ((VoxelShape)(Object)other).mergedORCache = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache[MERGED_CACHE_SIZE];
+ }
+ ((VoxelShape)(Object)other).mergedORCache[otherCacheKey] = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache((VoxelShape)(Object)this, result);
+ } else {
+ // line is not occupied or other cache line is full
+ // always bias to replace this cache, as this cache is the first we check
+ if (this.mergedORCache == null) {
+ this.mergedORCache = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache[MERGED_CACHE_SIZE];
+ }
+ this.mergedORCache[thisCacheKey] = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache(other, result);
+ }
+
+ 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);
+ }
+ return new OffsetDoubleList(src, by);
+ }
+
+ private List<AABB> toAabbsUncached() {
+ final List<AABB> ret = new java.util.ArrayList<>();
+ if (this.singleAABBRepresentation != null) {
+ ret.add(this.singleAABBRepresentation);
+ } else {
+ final double[] coordsX = this.rootCoordinatesX;
+ final double[] coordsY = this.rootCoordinatesY;
+ final double[] coordsZ = this.rootCoordinatesZ;
+
+ final double offX = this.offsetX;
+ final double offY = this.offsetY;
+ final double offZ = this.offsetZ;
+
+ this.shape.forAllBoxes((final int minX, final int minY, final int minZ,
+ final int maxX, final int maxY, final int maxZ) -> {
+ ret.add(new AABB(
+ coordsX[minX] + offX,
+ coordsY[minY] + offY,
+ coordsZ[minZ] + offZ,
+
+
+ coordsX[maxX] + offX,
+ coordsY[maxY] + offY,
+ coordsZ[maxZ] + offZ
+ ));
+ }, true);
+ }
+
+ // cache result
+ this.cachedToAABBs = new ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs(ret, false, 0.0, 0.0, 0.0);
+
+ return ret;
+ }
+
+ private boolean computeFullBlock() {
+ Boolean ret;
+ if (this.isEmpty) {
+ ret = Boolean.FALSE;
+ } else if ((VoxelShape)(Object)this == Shapes.block()) {
+ ret = Boolean.TRUE;
+ } else {
+ final AABB singleAABB = this.singleAABBRepresentation;
+ if (singleAABB == null) {
+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeData = this.cachedShapeData;
+ final int sMinX = shapeData.minFullX();
+ final int sMinY = shapeData.minFullY();
+ final int sMinZ = shapeData.minFullZ();
+
+ final int sMaxX = shapeData.maxFullX();
+ final int sMaxY = shapeData.maxFullY();
+ final int sMaxZ = shapeData.maxFullZ();
+
+ if (Math.abs(this.rootCoordinatesX[sMinX] + this.offsetX) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
+ Math.abs(this.rootCoordinatesY[sMinY] + this.offsetY) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
+ Math.abs(this.rootCoordinatesZ[sMinZ] + this.offsetZ) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
+
+ Math.abs(1.0 - (this.rootCoordinatesX[sMaxX] + this.offsetX)) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
+ Math.abs(1.0 - (this.rootCoordinatesY[sMaxY] + this.offsetY)) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
+ Math.abs(1.0 - (this.rootCoordinatesZ[sMaxZ] + this.offsetZ)) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) {
+
+ // index = z + y*sizeZ + x*(sizeZ*sizeY)
+
+ final int sizeY = shapeData.sizeY();
+ final int sizeZ = shapeData.sizeZ();
+
+ final long[] bitset = shapeData.voxelSet();
+
+ ret = Boolean.TRUE;
+
+ check_full:
+ for (int x = sMinX; x < sMaxX; ++x) {
+ for (int y = sMinY; y < sMaxY; ++y) {
+ final int baseIndex = y*sizeZ + x*(sizeZ*sizeY);
+ if (!ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.isRangeSet(bitset, baseIndex + sMinZ, baseIndex + sMaxZ)) {
+ ret = Boolean.FALSE;
+ break check_full;
+ }
+ }
+ }
+ } else {
+ ret = Boolean.FALSE;
+ }
+ } else {
+ ret = Boolean.valueOf(
+ Math.abs(singleAABB.minX) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
+ Math.abs(singleAABB.minY) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
+ Math.abs(singleAABB.minZ) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
+
+ Math.abs(1.0 - singleAABB.maxX) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
+ Math.abs(1.0 - singleAABB.maxY) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
+ Math.abs(1.0 - singleAABB.maxZ) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON
+ );
+ }
+ }
+
+ this.isFullBlock = ret;
+
+ return ret.booleanValue();
+ }
+
+ @Override
+ public final boolean moonrise$isFullBlock() {
+ final Boolean ret = this.isFullBlock;
+
+ if (ret != null) {
+ return ret.booleanValue();
+ }
+
+ return this.computeFullBlock();
+ }
+
+ private static BlockHitResult clip(final AABB aabb, final Vec3 from, final Vec3 to, final BlockPos offset) {
+ final double[] minDistanceArr = new double[] { 1.0 };
+ final double diffX = to.x - from.x;
+ final double diffY = to.y - from.y;
+ final double diffZ = to.z - from.z;
+
+ final Direction direction = AABB.getDirection(aabb.move(offset), from, minDistanceArr, null, diffX, diffY, diffZ);
+
+ if (direction == null) {
+ return null;
+ }
+
+ final double minDistance = minDistanceArr[0];
+ return new BlockHitResult(from.add(minDistance * diffX, minDistance * diffY, minDistance * diffZ), direction, offset, false);
+ }
+ // Paper end - optimise collisions
+
protected VoxelShape(DiscreteVoxelShape voxels) {
this.shape = voxels;
}
public double min(Direction.Axis axis) {
- int i = this.shape.firstFull(axis);
- return i >= this.shape.getSize(axis) ? Double.POSITIVE_INFINITY : this.get(axis, i);
+ // Paper start - optimise collisions
+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeData = this.cachedShapeData;
+ switch (axis) {
+ case X: {
+ final int idx = shapeData.minFullX();
+ return idx >= shapeData.sizeX() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX);
+ }
+ case Y: {
+ final int idx = shapeData.minFullY();
+ return idx >= shapeData.sizeY() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY);
+ }
+ case Z: {
+ final int idx = shapeData.minFullZ();
+ return idx >= shapeData.sizeZ() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ);
+ }
+ default: {
+ // should never get here
+ return Double.POSITIVE_INFINITY;
+ }
+ }
+ // Paper end - optimise collisions
}
public double max(Direction.Axis axis) {
- int i = this.shape.lastFull(axis);
- return i <= 0 ? Double.NEGATIVE_INFINITY : this.get(axis, i);
+ // Paper start - optimise collisions
+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeData = this.cachedShapeData;
+ switch (axis) {
+ case X: {
+ final int idx = shapeData.maxFullX();
+ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX);
+ }
+ case Y: {
+ final int idx = shapeData.maxFullY();
+ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY);
+ }
+ case Z: {
+ final int idx = shapeData.maxFullZ();
+ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ);
+ }
+ default: {
+ // should never get here
+ return Double.NEGATIVE_INFINITY;
+ }
+ }
+ // Paper end - optimise collisions
}
public AABB bounds() {
- if (this.isEmpty()) {
- throw (UnsupportedOperationException)Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape."));
- } else {
- return new AABB(
- 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) {
+ throw Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape."));
+ }
+ AABB cached = this.cachedBounds;
+ if (cached != null) {
+ return cached;
}
+
+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeData = this.cachedShapeData;
+
+ final double[] coordsX = this.rootCoordinatesX;
+ final double[] coordsY = this.rootCoordinatesY;
+ final double[] coordsZ = this.rootCoordinatesZ;
+
+ final double offX = this.offsetX;
+ final double offY = this.offsetY;
+ final double offZ = this.offsetZ;
+
+ // note: if not empty, then there is one full AABB so no bounds checks are needed on the minFull/maxFull indices
+ cached = new AABB(
+ coordsX[shapeData.minFullX()] + offX,
+ coordsY[shapeData.minFullY()] + offY,
+ coordsZ[shapeData.minFullZ()] + offZ,
+
+ coordsX[shapeData.maxFullX()] + offX,
+ coordsY[shapeData.maxFullY()] + offY,
+ coordsZ[shapeData.maxFullZ()] + offZ
+ );
+
+ this.cachedBounds = cached;
+ return cached;
+ // Paper end - optimise collisions
}
public VoxelShape singleEncompassing() {
@@ -69,28 +536,95 @@ public abstract class VoxelShape {
public abstract DoubleList getCoords(Direction.Axis axis);
public boolean isEmpty() {
- return this.shape.isEmpty();
+ return this.isEmpty; // Paper - optimise collisions
}
public VoxelShape move(double x, double y, double z) {
- return (VoxelShape)(this.isEmpty()
- ? Shapes.empty()
- : new ArrayVoxelShape(
- this.shape,
- new OffsetDoubleList(this.getCoords(Direction.Axis.X), x),
- new OffsetDoubleList(this.getCoords(Direction.Axis.Y), y),
- new OffsetDoubleList(this.getCoords(Direction.Axis.Z), z)
- ));
+ // Paper start - optimise collisions
+ if (this.isEmpty) {
+ return Shapes.empty();
+ }
+
+ 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)
+ );
+
+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cachedToAABBs = this.cachedToAABBs;
+ if (cachedToAABBs != null) {
+ ((VoxelShape)(Object)ret).cachedToAABBs = ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs.offset(cachedToAABBs, x, y, z);
+ }
+
+ return ret;
+ // Paper end - optimise collisions
}
public VoxelShape optimize() {
- VoxelShape[] voxelShapes = new VoxelShape[]{Shapes.empty()};
- this.forAllBoxes(
- (minX, minY, minZ, maxX, maxY, maxZ) -> voxelShapes[0] = Shapes.joinUnoptimized(
- voxelShapes[0], Shapes.box(minX, minY, minZ, maxX, maxY, maxZ), BooleanOp.OR
- )
- );
- return voxelShapes[0];
+ // Paper start - optimise collisions
+ if (this.isEmpty) {
+ return Shapes.empty();
+ }
+
+ if (this.singleAABBRepresentation != null) {
+ // note: the isFullBlock() is fuzzy, and Shapes.create() is also fuzzy which would return block()
+ return this.moonrise$isFullBlock() ? Shapes.block() : (VoxelShape)(Object)this;
+ }
+
+ final List<AABB> aabbs = this.toAabbs();
+
+ if (aabbs.size() == 1) {
+ final AABB singleAABB = aabbs.get(0);
+ final VoxelShape ret = Shapes.create(singleAABB);
+
+ // forward AABB cache
+ if (((VoxelShape)(Object)ret).cachedToAABBs == null) {
+ ((VoxelShape)(Object)ret).cachedToAABBs = this.cachedToAABBs;
+ }
+
+ return ret;
+ } else {
+ // reduce complexity of joins by splitting the merges (old complexity: n^2, new: nlogn)
+
+ // set up flat array so that this merge is done in-place
+ final VoxelShape[] tmp = new VoxelShape[aabbs.size()];
+
+ // initialise as unmerged
+ for (int i = 0, len = aabbs.size(); i < len; ++i) {
+ tmp[i] = Shapes.create(aabbs.get(i));
+ }
+
+ int size = aabbs.size();
+ while (size > 1) {
+ int newSize = 0;
+ for (int i = 0; i < size; i += 2) {
+ final int next = i + 1;
+ if (next >= size) {
+ // nothing to merge with, so leave it for next iteration
+ tmp[newSize++] = tmp[i];
+ break;
+ } else {
+ // merge with adjacent
+ final VoxelShape first = tmp[i];
+ final VoxelShape second = tmp[next];
+
+ tmp[newSize++] = Shapes.joinUnoptimized(first, second, BooleanOp.OR);
+ }
+ }
+ size = newSize;
+ }
+
+ final VoxelShape ret = tmp[0];
+
+ // forward AABB cache
+ if (((VoxelShape)(Object)ret).cachedToAABBs == null) {
+ ((VoxelShape)(Object)ret).cachedToAABBs = this.cachedToAABBs;
+ }
+
+ return ret;
+ }
+ // Paper end - optimise collisions
}
public void forAllEdges(Shapes.DoubleLineConsumer consumer) {
@@ -127,9 +661,24 @@ public abstract class VoxelShape {
}
public List<AABB> toAabbs() {
- List<AABB> list = Lists.newArrayList();
- this.forAllBoxes((x1, y1, z1, x2, y2, z2) -> list.add(new AABB(x1, y1, z1, x2, y2, z2)));
- return list;
+ // Paper start - optimise collisions
+ ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cachedToAABBs = this.cachedToAABBs;
+ if (cachedToAABBs != null) {
+ if (!cachedToAABBs.isOffset()) {
+ return cachedToAABBs.aabbs();
+ }
+
+ // all we need to do is offset the cache
+ cachedToAABBs = cachedToAABBs.removeOffset();
+ // update cache
+ this.cachedToAABBs = cachedToAABBs;
+
+ return cachedToAABBs.aabbs();
+ }
+
+ // make new cache
+ return this.toAabbsUncached();
+ // Paper end - optimise collisions
}
public double min(Direction.Axis axis, double from, double to) {
@@ -155,42 +704,63 @@ public abstract class VoxelShape {
}
@Nullable
- public BlockHitResult clip(Vec3 start, Vec3 end, BlockPos pos) {
- if (this.isEmpty()) {
+ // Paper start - optimise collisions
+ public BlockHitResult clip(final Vec3 from, final Vec3 to, final BlockPos offset) {
+ if (this.isEmpty) {
return null;
- } else {
- Vec3 vec3 = end.subtract(start);
- if (vec3.lengthSqr() < 1.0E-7) {
- return null;
- } else {
- Vec3 vec32 = start.add(vec3.scale(0.001));
- return this.shape
- .isFullWide(
- this.findIndex(Direction.Axis.X, vec32.x - (double)pos.getX()),
- this.findIndex(Direction.Axis.Y, vec32.y - (double)pos.getY()),
- this.findIndex(Direction.Axis.Z, vec32.z - (double)pos.getZ())
- )
- ? new BlockHitResult(vec32, Direction.getNearest(vec3.x, vec3.y, vec3.z).getOpposite(), pos, true)
- : AABB.clip(this.toAabbs(), start, end, pos);
+ }
+
+ final Vec3 directionOpposite = to.subtract(from);
+ if (directionOpposite.lengthSqr() < ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) {
+ return null;
+ }
+
+ final Vec3 fromBehind = from.add(directionOpposite.scale(0.001));
+ final double fromBehindOffsetX = fromBehind.x - (double)offset.getX();
+ final double fromBehindOffsetY = fromBehind.y - (double)offset.getY();
+ final double fromBehindOffsetZ = fromBehind.z - (double)offset.getZ();
+
+ 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 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 AABB.clip(((VoxelShape)(Object)this).toAabbs(), from, to, offset);
+ // Paper end - optimise collisions
}
- public Optional<Vec3> closestPointTo(Vec3 target) {
- if (this.isEmpty()) {
+ // Paper start - optimise collisions
+ public Optional<Vec3> closestPointTo(Vec3 point) {
+ if (this.isEmpty) {
return Optional.empty();
- } else {
- Vec3[] vec3s = new Vec3[1];
- this.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> {
- double d = Mth.clamp(target.x(), minX, maxX);
- double e = Mth.clamp(target.y(), minY, maxY);
- double f = Mth.clamp(target.z(), minZ, maxZ);
- if (vec3s[0] == null || target.distanceToSqr(d, e, f) < target.distanceToSqr(vec3s[0])) {
- vec3s[0] = new Vec3(d, e, f);
- }
- });
- return Optional.of(vec3s[0]);
}
+
+ Vec3 ret = null;
+ double retDistance = Double.MAX_VALUE;
+
+ final List<AABB> aabbs = this.toAabbs();
+ for (int i = 0, len = aabbs.size(); i < len; ++i) {
+ final AABB aabb = aabbs.get(i);
+ final double x = Mth.clamp(point.x, aabb.minX, aabb.maxX);
+ final double y = Mth.clamp(point.y, aabb.minY, aabb.maxY);
+ final double z = Mth.clamp(point.z, aabb.minZ, aabb.maxZ);
+
+ double dist = point.distanceToSqr(x, y, z);
+ if (dist < retDistance) {
+ ret = new Vec3(x, y, z);
+ retDistance = dist;
+ }
+ }
+
+ return Optional.ofNullable(ret);
+ // Paper end - optimise collisions
}
public VoxelShape getFaceShape(Direction facing) {
@@ -226,8 +796,29 @@ public abstract class VoxelShape {
}
}
- public double collide(Direction.Axis axis, AABB box, double maxDist) {
- return this.collideX(AxisCycle.between(axis, Direction.Axis.X), box, maxDist);
+ // Paper start - optimise collisions
+ public double collide(final Direction.Axis axis, final AABB source, final double source_move) {
+ if (this.isEmpty) {
+ return source_move;
+ }
+ if (Math.abs(source_move) < ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) {
+ return 0.0;
+ }
+ switch (axis) {
+ case X: {
+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.collideX((VoxelShape)(Object)this, source, source_move);
+ }
+ case Y: {
+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.collideY((VoxelShape)(Object)this, source, source_move);
+ }
+ case Z: {
+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.collideZ((VoxelShape)(Object)this, source, source_move);
+ }
+ default: {
+ throw new RuntimeException("Unknown axis: " + axis);
+ }
+ }
+ // Paper end - optimise collisions
}
protected double collideX(AxisCycle axisCycle, AABB box, double maxDist) {
diff --git a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java
index 47c2b2da9799690291396effb9e1b06d71efc6fd..c42c0d1e4da30aa15f32d4ca524aeabd26fc50cf 100644
--- a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java
+++ b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java
@@ -18,7 +18,7 @@ import net.minecraft.core.BlockPos;
import net.minecraft.nbt.ListTag;
import net.minecraft.world.level.ChunkPos;
-public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickContainerAccess<T> {
+public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickContainerAccess<T>, ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks { // Paper - rewrite chunk system
private final Queue<ScheduledTick<T>> tickQueue = new PriorityQueue<>(ScheduledTick.DRAIN_ORDER);
@Nullable
private List<SavedTick<T>> pendingTicks;
@@ -26,6 +26,30 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
@Nullable
private BiConsumer<LevelChunkTicks<T>, ScheduledTick<T>> onTickAdded;
+ // Paper start - rewrite chunk system
+ /*
+ * Since ticks are saved using relative delays, we need to consider the entire tick list dirty when there are scheduled ticks
+ * and the last saved tick is not equal to the current tick
+ */
+ /*
+ * In general, it would be nice to be able to "re-pack" ticks once the chunk becomes non-ticking again, but that is a
+ * bit out of scope for the chunk system
+ */
+
+ private boolean dirty;
+ private long lastSaved = Long.MIN_VALUE;
+
+ @Override
+ public final boolean moonrise$isDirty(final long tick) {
+ return this.dirty || (!this.tickQueue.isEmpty() && tick != this.lastSaved);
+ }
+
+ @Override
+ public final void moonrise$clearDirty() {
+ this.dirty = false;
+ }
+ // Paper end - rewrite chunk system
+
public LevelChunkTicks() {
}
@@ -50,7 +74,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
public ScheduledTick<T> poll() {
ScheduledTick<T> scheduledTick = this.tickQueue.poll();
if (scheduledTick != null) {
- this.ticksPerPosition.remove(scheduledTick);
+ this.ticksPerPosition.remove(scheduledTick); this.dirty = true; // Paper - rewrite chunk system
}
return scheduledTick;
@@ -59,7 +83,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
@Override
public void schedule(ScheduledTick<T> orderedTick) {
if (this.ticksPerPosition.add(orderedTick)) {
- this.scheduleUnchecked(orderedTick);
+ this.scheduleUnchecked(orderedTick); this.dirty = true; // Paper - rewrite chunk system
}
}
@@ -81,7 +105,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
while (iterator.hasNext()) {
ScheduledTick<T> scheduledTick = iterator.next();
if (predicate.test(scheduledTick)) {
- iterator.remove();
+ iterator.remove(); this.dirty = true; // Paper - rewrite chunk system
this.ticksPerPosition.remove(scheduledTick);
}
}
@@ -98,6 +122,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
@Override
public ListTag save(long l, Function<T, String> function) {
+ this.lastSaved = l; // Paper - rewrite chunk system
ListTag listTag = new ListTag();
if (this.pendingTicks != null) {
for (SavedTick<T> savedTick : this.pendingTicks) {
@@ -114,6 +139,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
public void unpack(long time) {
if (this.pendingTicks != null) {
+ this.lastSaved = time; // Paper - rewrite chunk system
int i = -this.pendingTicks.size();
for (SavedTick<T> savedTick : this.pendingTicks) {
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
index 69c7fe5bf5b914276a9f7a0e57ce668e569d91f9..33322b57b4c6922f4daad0f584733f0f24083911 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
@@ -82,6 +82,12 @@ public class CraftChunk implements Chunk {
}
public ChunkAccess getHandle(ChunkStatus chunkStatus) {
+ // Paper start - rewrite chunk system
+ net.minecraft.world.level.chunk.LevelChunk full = this.worldServer.getChunkIfLoaded(this.x, this.z);
+ if (full != null) {
+ return full;
+ }
+ // Paper end - rewrite chunk system
ChunkAccess chunkAccess = this.worldServer.getChunk(this.x, this.z, chunkStatus);
// SPIGOT-7332: Get unwrapped extension
@@ -116,60 +122,12 @@ public class CraftChunk implements Chunk {
@Override
public boolean isEntitiesLoaded() {
- return this.getCraftWorld().getHandle().entityManager.areEntitiesLoaded(ChunkPos.asLong(this.x, this.z));
+ return this.getCraftWorld().getHandle().areEntitiesLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(this.x, this.z)); // Paper - rewrite chunk system
}
@Override
public Entity[] getEntities() {
- if (!this.isLoaded()) {
- this.getWorld().getChunkAt(this.x, this.z); // Transient load for this tick
- }
-
- PersistentEntitySectionManager<net.minecraft.world.entity.Entity> entityManager = this.getCraftWorld().getHandle().entityManager;
- long pair = ChunkPos.asLong(this.x, this.z);
-
- if (entityManager.areEntitiesLoaded(pair)) {
- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream()
- .map(net.minecraft.world.entity.Entity::getBukkitEntity)
- .filter(Objects::nonNull).toArray(Entity[]::new);
- }
-
- entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading
-
- // SPIGOT-6772: Use entity mailbox and re-schedule entities if they get unloaded
- ProcessorMailbox<Runnable> mailbox = ((EntityStorage) entityManager.permanentStorage).entityDeserializerQueue;
- BooleanSupplier supplier = () -> {
- // only execute inbox if our entities are not present
- if (entityManager.areEntitiesLoaded(pair)) {
- return true;
- }
-
- if (!entityManager.isPending(pair)) {
- // Our entities got unloaded, this should normally not happen.
- entityManager.ensureChunkQueuedForLoad(pair); // Re-start entity loading
- }
-
- // tick loading inbox, which loads the created entities to the world
- // (if present)
- entityManager.tick();
- // check if our entities are loaded
- return entityManager.areEntitiesLoaded(pair);
- };
-
- // now we wait until the entities are loaded,
- // the converting from NBT to entity object is done on the main Thread which is why we wait
- while (!supplier.getAsBoolean()) {
- if (mailbox.size() != 0) {
- mailbox.run();
- } else {
- Thread.yield();
- LockSupport.parkNanos("waiting for entity loading", 100000L);
- }
- }
-
- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream()
- .map(net.minecraft.world.entity.Entity::getBukkitEntity)
- .filter(Objects::nonNull).toArray(Entity[]::new);
+ return this.getCraftWorld().getHandle().getChunkEntities(this.x, this.z); // Paper - rewrite chunk system
}
@Override
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index 6321ae23f07bc3e07577914e6cf035e0c2cd4992..cc1f3c7fec4fa794da0b19f44ced9fd482dfdfc7 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -1428,7 +1428,7 @@ public final class CraftServer implements Server {
// Paper - Put world into worldlist before initing the world; move up
this.getServer().prepareLevels(internal.getChunkSource().chunkMap.progressListener, internal);
- internal.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API
+ // Paper - rewrite chunk system
this.pluginManager.callEvent(new WorldLoadEvent(internal.getWorld()));
return internal.getWorld();
@@ -1473,7 +1473,7 @@ public final class CraftServer implements Server {
}
handle.getChunkSource().close(save);
- handle.entityManager.close(save); // SPIGOT-6722: close entityManager
+ // Paper - rewrite chunk system
handle.convertable.close();
} catch (Exception ex) {
this.getLogger().log(Level.SEVERE, null, ex);
@@ -2509,7 +2509,7 @@ public final class CraftServer implements Server {
@Override
public boolean isPrimaryThread() {
- return Thread.currentThread().equals(this.console.serverThread) || this.console.hasStopped() || !org.spigotmc.AsyncCatcher.enabled; // All bets are off if we have shut down (e.g. due to watchdog)
+ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThread(); // Paper - rewrite chunk system
}
// Paper start - Adventure
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index 94640aa827c9b2e1d0174eb012fdb37c0851f501..5ad2ceb1274648631689215702a12463c681152c 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -456,10 +456,14 @@ public class CraftWorld extends CraftRegionAccessor implements World {
ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
if (playerChunk == null) return false;
- playerChunk.getTickingChunkFuture().thenAccept(either -> {
- either.ifSuccess(chunk -> {
+ // Paper start - chunk system
+ net.minecraft.world.level.chunk.LevelChunk chunk = playerChunk.getChunkToSend();
+ if (chunk == null) {
+ return false;
+ }
+ // Paper end - chunk system
List<ServerPlayer> playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false);
- if (playersInRange.isEmpty()) return;
+ if (playersInRange.isEmpty()) return true; // Paper - chunk system
ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, this.world.getLightEngine(), null, null);
for (ServerPlayer player : playersInRange) {
@@ -467,8 +471,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
player.connection.send(refreshPacket);
}
- });
- });
+ // Paper - chunk system
return true;
}
@@ -572,20 +575,8 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public Collection<Plugin> getPluginChunkTickets(int x, int z) {
DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager;
- SortedArraySet<Ticket<?>> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z));
- if (tickets == null) {
- return Collections.emptyList();
- }
-
- ImmutableList.Builder<Plugin> ret = ImmutableList.builder();
- for (Ticket<?> ticket : tickets) {
- if (ticket.getType() == TicketType.PLUGIN_TICKET) {
- ret.add((Plugin) ticket.key);
- }
- }
-
- return ret.build();
+ return chunkDistanceManager.getChunkHolderManager().getPluginChunkTickets(x, z); // Paper - rewrite chunk system
}
@Override
@@ -593,7 +584,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
Map<Plugin, ImmutableList.Builder<Chunk>> ret = new HashMap<>();
DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager;
- for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) {
+ for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { // Paper - rewrite chunk system
long chunkKey = chunkTickets.getLongKey();
SortedArraySet<Ticket<?>> tickets = chunkTickets.getValue();
@@ -1290,12 +1281,12 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public int getViewDistance() {
- return this.world.getChunkSource().chunkMap.serverViewDistance;
+ return this.getHandle().moonrise$getPlayerChunkLoader().getAPIViewDistance(); // Paper - rewrite chunk system
}
@Override
public int getSimulationDistance() {
- return this.world.getChunkSource().chunkMap.getDistanceManager().simulationDistance;
+ return this.getHandle().moonrise$getPlayerChunkLoader().getAPITickDistance(); // Paper - rewrite chunk system
}
public BlockMetadataStore getBlockMetadata() {
@@ -2433,17 +2424,20 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public void setSimulationDistance(final int simulationDistance) {
- throw new UnsupportedOperationException("Not implemented yet");
+ if (simulationDistance < 2 || simulationDistance > 32) {
+ throw new IllegalArgumentException("Simulation distance " + simulationDistance + " is out of range of [2, 32]");
+ }
+ this.getHandle().chunkSource.setSimulationDistance(simulationDistance); // Paper - rewrite chunk system
}
@Override
public int getSendViewDistance() {
- return this.getViewDistance();
+ return this.getHandle().moonrise$getPlayerChunkLoader().getAPISendViewDistance(); // Paper - rewrite chunk system
}
@Override
public void setSendViewDistance(final int viewDistance) {
- throw new UnsupportedOperationException("Not implemented yet");
+ this.getHandle().chunkSource.setSendViewDistance(viewDistance); // Paper - rewrite chunk system
}
// Paper start - implement pointers
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
index 12010a078542b7e89f0f4c0e7983eb15e75c4f1f..2f56cbcc1d1af98f58c310ff8b4ce33cc950e977 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -3497,7 +3497,9 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
@Override
public void setViewDistance(final int viewDistance) {
- throw new UnsupportedOperationException("Not implemented yet");
+ // Paper - rewrite chunk system - TODO do this better
+ ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
+ .moonrise$getViewDistanceHolder().setLoadViewDistance(viewDistance + 1);
}
@Override
@@ -3507,7 +3509,9 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
@Override
public void setSimulationDistance(final int simulationDistance) {
- throw new UnsupportedOperationException("Not implemented yet");
+ // Paper - rewrite chunk system - TODO do this better
+ ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
+ .moonrise$getViewDistanceHolder().setTickViewDistance(simulationDistance);
}
@Override
@@ -3517,6 +3521,8 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
@Override
public void setSendViewDistance(final int viewDistance) {
- throw new UnsupportedOperationException("Not implemented yet");
+ // Paper - rewrite chunk system - TODO do this better
+ ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
+ .moonrise$getViewDistanceHolder().setSendViewDistance(viewDistance);
}
}
diff --git a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
index 5717c0e1d6df07a4613356dc78d970d2101c68d7..cab7ca4218e5903b6a5e518af55457b9a1b5111c 100644
--- a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
+++ b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
@@ -263,7 +263,7 @@ public class CustomChunkGenerator extends InternalChunkGenerator {
return ichunkaccess1;
};
- return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), net.minecraft.Util.backgroundExecutor()) : future.thenApply(function);
+ return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), Runnable::run) : future.thenApply(function); // Paper - rewrite chunk system
}
@Override
diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
index fceed3d08ee6f4c171685986bb19d2be592eedc6..bf18f9ad7dec2b09ebfcb5ec6566f2556e842f22 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
@@ -829,5 +829,12 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel {
public ChunkAccess getChunkIfLoadedImmediately(final int x, final int z) {
return this.handle.getChunkIfLoadedImmediately(x, z);
}
+
+ // Paper start - rewrite chunk system
+ @Override
+ public java.util.List<net.minecraft.world.entity.Entity> moonrise$getHardCollidingEntities(final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB box, final java.util.function.Predicate<? super net.minecraft.world.entity.Entity> predicate) {
+ return this.handle.moonrise$getHardCollidingEntities(entity, box, predicate);
+ }
+ // Paper end - rewrite chunk system
// Paper end
}
diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java
index ef2598760458833021ef1bee92137f42c9fe591f..1f23e775eba1c34e01145bd91b0ce26fed6ca9de 100644
--- a/src/main/java/org/spigotmc/AsyncCatcher.java
+++ b/src/main/java/org/spigotmc/AsyncCatcher.java
@@ -9,7 +9,7 @@ public class AsyncCatcher
public static void catchOp(String reason)
{
- if ( AsyncCatcher.enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread )
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) // Paper // Paper - rewrite chunk system
{
MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); // Paper
throw new IllegalStateException( "Asynchronous " + reason + "!" );
diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
index ad282d34919716b75acd10426cd071da9d064a51..529df2a41dd93d6e1505053bd04032dbf0cdaa31 100644
--- a/src/main/java/org/spigotmc/WatchdogThread.java
+++ b/src/main/java/org/spigotmc/WatchdogThread.java
@@ -8,7 +8,7 @@ import java.util.logging.Logger;
import net.minecraft.server.MinecraftServer;
import org.bukkit.Bukkit;
-public class WatchdogThread extends Thread
+public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThread // Paper - rewrite chunk system
{
private static WatchdogThread instance;
@@ -115,6 +115,7 @@ public class WatchdogThread extends Thread
// Paper end - Different message for short timeout
log.log( Level.SEVERE, "------------------------------" );
log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(MinecraftServer.getServer(), isLongTimeout); // Paper - rewrite chunk system
WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
log.log( Level.SEVERE, "------------------------------" );
//