diff options
author | Nassim Jahnke <[email protected]> | 2024-11-29 17:07:35 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2024-11-29 17:07:35 +0100 |
commit | 64828f3a60d3efbe2e77665e0530d687a8408f56 (patch) | |
tree | 28ffdb57a00ff12f2aa1155ed8cd72cdc781319b /patches/server/1037-Moonrise-optimisation-patches.patch | |
parent | 9953384d89548c0c53a57e2a60abace9ad19b177 (diff) | |
download | Paper-64828f3a60d3efbe2e77665e0530d687a8408f56.tar.gz Paper-64828f3a60d3efbe2e77665e0530d687a8408f56.zip |
Clean up thread pool usage (#11681)
Using an unbound LinkedBlockingQueue means you *have* to set core and max core thread pool size the same, as they will never go above the minimum pool size by just passing them through. So this fixes the async command executor pool to actually use 2 threads, and also cleans up other usage to be explicitly "fixed" thread pool sizes, and splits off one more in Minecraft's Util class
Diffstat (limited to 'patches/server/1037-Moonrise-optimisation-patches.patch')
-rw-r--r-- | patches/server/1037-Moonrise-optimisation-patches.patch | 36361 |
1 files changed, 36361 insertions, 0 deletions
diff --git a/patches/server/1037-Moonrise-optimisation-patches.patch b/patches/server/1037-Moonrise-optimisation-patches.patch new file mode 100644 index 0000000000..b2bbfd3c7a --- /dev/null +++ b/patches/server/1037-Moonrise-optimisation-patches.patch @@ -0,0 +1,36361 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf <[email protected]> +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 + - Bitstorage optimisations + - Block/Biome Palette read optimisations + - StateHolder (BlockState/FluidState) property access optimisations + - Basic Fluid property read optimisations + - Entity/Level random replacement + +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 f7cd0aa43d0b9249d0a317fab41fefa0d951bca0..fc029c8fb22a7c8eeb23bfc171812f6da91c60fa 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java +@@ -2,11 +2,17 @@ package ca.spottedleaf.moonrise.common.util; + + import ca.spottedleaf.concurrentutil.util.Priority; + import ca.spottedleaf.moonrise.common.PlatformHooks; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk; ++import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader; ++import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache; ++import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel; + import com.mojang.logging.LogUtils; + import net.minecraft.server.level.ChunkHolder; + import net.minecraft.server.level.FullChunkStatus; + import net.minecraft.server.level.ServerLevel; + import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.level.progress.ChunkProgressListener; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.level.chunk.ChunkAccess; + import net.minecraft.world.level.chunk.LevelChunk; +@@ -18,203 +24,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, Priority.NORMAL); + } + + public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final 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 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 Priority priority, final Consumer<ChunkAccess> onComplete) { +- if (!org.bukkit.Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) { +- 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, 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 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.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) { +- 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, 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) { +@@ -233,55 +82,96 @@ public final class ChunkSystem { + } + + public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { ++ // Update progress listener for LevelLoadingScreen ++ final ChunkProgressListener progressListener = level.getChunkSource().chunkMap.progressListener; ++ if (progressListener != null) { ++ ChunkSystem.scheduleChunkTask(level, holder.getPos().x, holder.getPos().z, () -> { ++ progressListener.onStatusChange(holder.getPos(), null); ++ }); ++ } ++ } + ++ public static void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) { ++ ((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()); ++ } ++ ((ServerLevel)chunk.getLevel()).startTickingChunk(chunk); ++ ((ServerLevel)chunk.getLevel()).getChunkSource().chunkMap.tickingGenerated.incrementAndGet(); ++ ((ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$markChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration + } + + public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { +- ++ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove( ++ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() ++ ); ++ ((ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$removeChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration + } + + public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { +- ++ ((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) - 1; ++ return RegionizedPlayerChunkLoader.getAPISendViewDistance(player); + } + + public static int getLoadViewDistance(final ServerPlayer player) { +- final ServerLevel level = player.serverLevel(); +- if (level == null) { +- return org.bukkit.Bukkit.getViewDistance() + 1; +- } +- return level.chunkSource.chunkMap.serverViewDistance + 1; ++ 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/ThreadUnsafeRandom.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java +index 12eb3add0931a4d77acdf6e875c42dda9c313dc3..5239993a681d6113eec99fa627b85508656ed7ac 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java +@@ -9,7 +9,7 @@ import net.minecraft.world.level.levelgen.PositionalRandomFactory; + /** + * Avoid costly CAS of superclass + */ +-public final class ThreadUnsafeRandom implements BitRandomSource { ++public class ThreadUnsafeRandom implements BitRandomSource { // Paper - replace random + + private static final long MULTIPLIER = 25214903917L; + private static final long ADDEND = 11L; +diff --git a/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java +index c8f2457ab3b28f2c3a6b500bcea40261669c24a4..ca8b6a926dfff3fdd6b04228809a4480366120b2 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java ++++ b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java +@@ -236,7 +236,7 @@ public final class PaperHooks implements PlatformHooks { + + @Override + public void postLoadProtoChunk(final ServerLevel world, final ProtoChunk chunk) { +- net.minecraft.world.level.chunk.status.ChunkStatusTasks.postLoadProtoChunk(world, chunk.getEntities()); ++ net.minecraft.world.level.chunk.status.ChunkStatusTasks.postLoadProtoChunk(world, chunk.getEntities(), chunk.getPos()); // Paper - rewrite chunk system - add ChunkPos param + } + + @Override +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..93bc56daec4526f373c84763b8c7ccb4a30e800b +--- /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.shorts.ShortArrayList; ++ ++public interface BlockCountingBitStorage { ++ ++ public Int2ObjectOpenHashMap<ShortArrayList> moonrise$countEntries(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0d1443a113c07d7655e7b927a899447f70db8fa9 +--- /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.ShortList; ++ ++public interface BlockCountingChunkSection { ++ ++ public boolean moonrise$hasSpecialCollidingBlocks(); ++ ++ public ShortList moonrise$getTickingBlockList(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java +new file mode 100644 +index 0000000000000000000000000000000000000000..89e75b454695e174c5619104eeb15eb923a2d9a7 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java +@@ -0,0 +1,12 @@ ++package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess; ++ ++public interface PropertyAccess<T> { ++ ++ public int moonrise$getId(); ++ ++ public int moonrise$getIdFor(final T value); ++ ++ public T moonrise$getById(final int id); ++ ++ public void moonrise$setById(final T[] values); ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..01da52b9e8a786824f199a057b62ce0431ecbc43 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java +@@ -0,0 +1,7 @@ ++package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess; ++ ++public interface PropertyAccessStateHolder { ++ ++ public long moonrise$getTableIndex(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..866f38eb0f379ffbe2888023a7d1c290f521a231 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java +@@ -0,0 +1,230 @@ ++package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util; ++ ++import ca.spottedleaf.concurrentutil.util.IntegerUtil; ++import ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess; ++import ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccessStateHolder; ++import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.objects.AbstractObjectSet; ++import it.unimi.dsi.fastutil.objects.AbstractReference2ObjectMap; ++import it.unimi.dsi.fastutil.objects.ObjectIterator; ++import it.unimi.dsi.fastutil.objects.ObjectSet; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; ++import it.unimi.dsi.fastutil.objects.ReferenceArrayList; ++import java.util.ArrayList; ++import java.util.Collection; ++import java.util.Collections; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Map; ++import net.minecraft.world.level.block.state.StateHolder; ++import net.minecraft.world.level.block.state.properties.Property; ++ ++public final class ZeroCollidingReferenceStateTable<O, S> { ++ ++ private final Int2ObjectOpenHashMap<Indexer> propertyToIndexer; ++ private S[] lookup; ++ private final Collection<Property<?>> properties; ++ ++ public ZeroCollidingReferenceStateTable(final Collection<Property<?>> properties) { ++ this.propertyToIndexer = new Int2ObjectOpenHashMap<>(properties.size()); ++ this.properties = new ReferenceArrayList<>(properties); ++ ++ final List<Property<?>> sortedProperties = new ArrayList<>(properties); ++ ++ // important that each table sees the same property order given the same _set_ of properties, ++ // as each table will calculate the index for the block state ++ sortedProperties.sort((final Property<?> p1, final Property<?> p2) -> { ++ return Integer.compare( ++ ((PropertyAccess<?>)p1).moonrise$getId(), ++ ((PropertyAccess<?>)p2).moonrise$getId() ++ ); ++ }); ++ ++ int currentMultiple = 1; ++ for (final Property<?> property : sortedProperties) { ++ final int totalValues = property.getPossibleValues().size(); ++ ++ this.propertyToIndexer.put( ++ ((PropertyAccess<?>)property).moonrise$getId(), ++ new Indexer( ++ totalValues, ++ currentMultiple, ++ IntegerUtil.getUnsignedDivisorMagic((long)currentMultiple, 32), ++ IntegerUtil.getUnsignedDivisorMagic((long)totalValues, 32) ++ ) ++ ); ++ ++ currentMultiple *= totalValues; ++ } ++ } ++ ++ public <T extends Comparable<T>> boolean hasProperty(final Property<T> property) { ++ return this.propertyToIndexer.containsKey(((PropertyAccess<T>)property).moonrise$getId()); ++ } ++ ++ public long getIndex(final StateHolder<O, S> stateHolder) { ++ long ret = 0L; ++ ++ for (final Map.Entry<Property<?>, Comparable<?>> entry : stateHolder.getValues().entrySet()) { ++ final Property<?> property = entry.getKey(); ++ final Comparable<?> value = entry.getValue(); ++ ++ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<?>)property).moonrise$getId()); ++ ++ ret += (((PropertyAccess)property).moonrise$getIdFor(value)) * indexer.multiple; ++ } ++ ++ return ret; ++ } ++ ++ public boolean isLoaded() { ++ return this.lookup != null; ++ } ++ ++ public void loadInTable(final Map<Map<Property<?>, Comparable<?>>, S> universe) { ++ if (this.lookup != null) { ++ throw new IllegalStateException(); ++ } ++ ++ this.lookup = (S[])new StateHolder[universe.size()]; ++ ++ for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : universe.entrySet()) { ++ final S value = entry.getValue(); ++ if (value == null) { ++ continue; ++ } ++ this.lookup[(int)((PropertyAccessStateHolder)(StateHolder<O, S>)value).moonrise$getTableIndex()] = value; ++ } ++ ++ for (final S value : this.lookup) { ++ if (value == null) { ++ throw new IllegalStateException(); ++ } ++ } ++ } ++ ++ public <T extends Comparable<T>> T get(final long index, final Property<T> property) { ++ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<T>)property).moonrise$getId()); ++ if (indexer == null) { ++ return null; ++ } ++ ++ final long divided = (index * indexer.multipleDivMagic) >>> 32; ++ final long modded = (((divided * indexer.modMagic) & 0xFFFFFFFFL) * indexer.totalValues) >>> 32; ++ // equiv to: divided = index / multiple ++ // modded = divided % totalValues ++ ++ return ((PropertyAccess<T>)property).moonrise$getById((int)modded); ++ } ++ ++ public <T extends Comparable<T>> S set(final long index, final Property<T> property, final T with) { ++ final int newValueId = ((PropertyAccess<T>)property).moonrise$getIdFor(with); ++ if (newValueId < 0) { ++ return null; ++ } ++ ++ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<T>)property).moonrise$getId()); ++ if (indexer == null) { ++ return null; ++ } ++ ++ final long divided = (index * indexer.multipleDivMagic) >>> 32; ++ final long modded = (((divided * indexer.modMagic) & 0xFFFFFFFFL) * indexer.totalValues) >>> 32; ++ // equiv to: divided = index / multiple ++ // modded = divided % totalValues ++ ++ // subtract out the old value, add in the new ++ final long newIndex = (((long)newValueId - modded) * indexer.multiple) + index; ++ ++ return this.lookup[(int)newIndex]; ++ } ++ ++ public <T extends Comparable<T>> S trySet(final long index, final Property<T> property, final T with, final S dfl) { ++ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<T>)property).moonrise$getId()); ++ if (indexer == null) { ++ return dfl; ++ } ++ ++ final int newValueId = ((PropertyAccess<T>)property).moonrise$getIdFor(with); ++ if (newValueId < 0) { ++ return null; ++ } ++ ++ final long divided = (index * indexer.multipleDivMagic) >>> 32; ++ final long modded = (((divided * indexer.modMagic) & 0xFFFFFFFFL) * indexer.totalValues) >>> 32; ++ // equiv to: divided = index / multiple ++ // modded = divided % totalValues ++ ++ // subtract out the old value, add in the new ++ final long newIndex = (((long)newValueId - modded) * indexer.multiple) + index; ++ ++ return this.lookup[(int)newIndex]; ++ } ++ ++ public Collection<Property<?>> getProperties() { ++ return Collections.unmodifiableCollection(this.properties); ++ } ++ ++ public Map<Property<?>, Comparable<?>> getMapView(final long stateIndex) { ++ return new MapView(stateIndex); ++ } ++ ++ private static final record Indexer( ++ int totalValues, int multiple, long multipleDivMagic, long modMagic ++ ) {} ++ ++ private class MapView extends AbstractReference2ObjectMap<Property<?>, Comparable<?>> { ++ private final long stateIndex; ++ private EntrySet entrySet; ++ ++ MapView(final long stateIndex) { ++ this.stateIndex = stateIndex; ++ } ++ ++ @Override ++ public boolean containsKey(final Object key) { ++ return key instanceof Property<?> prop && ZeroCollidingReferenceStateTable.this.hasProperty(prop); ++ } ++ ++ @Override ++ public int size() { ++ return ZeroCollidingReferenceStateTable.this.properties.size(); ++ } ++ ++ @Override ++ public ObjectSet<Entry<Property<?>, Comparable<?>>> reference2ObjectEntrySet() { ++ if (this.entrySet == null) ++ this.entrySet = new EntrySet(); ++ return this.entrySet; ++ } ++ ++ @Override ++ public Comparable<?> get(final Object key) { ++ return key instanceof Property<?> prop ? ZeroCollidingReferenceStateTable.this.get(this.stateIndex, prop) : null; ++ } ++ ++ class EntrySet extends AbstractObjectSet<Entry<Property<?>, Comparable<?>>> { ++ @Override ++ public ObjectIterator<Reference2ObjectMap.Entry<Property<?>, Comparable<?>>> iterator() { ++ final Iterator<Property<?>> propIterator = ZeroCollidingReferenceStateTable.this.properties.iterator(); ++ return new ObjectIterator<>() { ++ @Override ++ public boolean hasNext() { ++ return propIterator.hasNext(); ++ } ++ ++ @Override ++ public Entry<Property<?>, Comparable<?>> next() { ++ Property<?> prop = propIterator.next(); ++ return new AbstractReference2ObjectMap.BasicEntry<>(prop, ZeroCollidingReferenceStateTable.this.get(MapView.this.stateIndex, prop)); ++ } ++ }; ++ } ++ ++ @Override ++ public int size() { ++ return ZeroCollidingReferenceStateTable.this.properties.size(); ++ } ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java +new file mode 100644 +index 0000000000000000000000000000000000000000..44bb25554634af2ec0b2e9b3d9231304d5dff034 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java +@@ -0,0 +1,39 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system; ++ ++import ca.spottedleaf.moonrise.common.PlatformHooks; ++import net.minecraft.SharedConstants; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.Tag; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.datafix.fixes.References; ++ ++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 PlatformHooks.get().convertNBT(References.POI_CHUNK, world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion()); ++ } ++ ++ public static CompoundTag convertEntityChunkCompoundTag(final CompoundTag data, final ServerLevel world) { ++ final int dataVersion = getDataVersion(data, DEFAULT_ENTITY_CHUNK_DATA_VERSION); ++ ++ return PlatformHooks.get().convertNBT(References.ENTITY_CHUNK, world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion()); ++ } ++ ++ private ChunkSystemConverters() {} ++} +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..c7da23900228aab3a5673eb5adfada5091140319 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java +@@ -0,0 +1,44 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.entity; ++ ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.monster.Shulker; ++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 ChunkData moonrise$getChunkData(); ++ ++ public void moonrise$setChunkData(final ChunkData chunkData); ++ ++ 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..a814512fcfb85312474ae2c2c21443843bf57831 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java +@@ -0,0 +1,31 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.io; ++ ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.world.level.chunk.storage.RegionFile; ++import java.io.IOException; ++ ++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; ++ ++ public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite( ++ final int chunkX, final int chunkZ, final CompoundTag compound ++ ) throws IOException; ++ ++ public void moonrise$finishWrite( ++ final int chunkX, final int chunkZ, final MoonriseRegionFileIO.RegionDataController.WriteData writeData ++ ) throws IOException; ++ ++ public MoonriseRegionFileIO.RegionDataController.ReadData moonrise$readData( ++ final int chunkX, final int chunkZ ++ ) throws IOException; ++ ++ // if the return value is null, then the caller needs to re-try with a new call to readData() ++ public CompoundTag moonrise$finishRead( ++ final int chunkX, final int chunkZ, final MoonriseRegionFileIO.RegionDataController.ReadData readData ++ ) throws IOException; ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1acea58838f057ab87efd103cbecb6f5aeaef393 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java +@@ -0,0 +1,1700 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.io; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.completable.CallbackCompletable; ++import ca.spottedleaf.concurrentutil.completable.Completable; ++import ca.spottedleaf.concurrentutil.executor.Cancellable; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.queue.PrioritisedTaskQueue; ++import ca.spottedleaf.concurrentutil.function.BiLong1Function; ++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.TickThread; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.chunk.storage.RegionFile; ++import net.minecraft.world.level.chunk.storage.RegionFileStorage; ++import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; ++import java.io.DataInputStream; ++import java.io.DataOutputStream; ++import java.io.IOException; ++import java.lang.invoke.VarHandle; ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.CompletionException; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.function.BiConsumer; ++import java.util.function.Consumer; ++ ++public final class MoonriseRegionFileIO { ++ ++ private static final int REGION_FILE_SHIFT = 5; ++ private static final Logger LOGGER = LoggerFactory.getLogger(MoonriseRegionFileIO.class); ++ ++ /** ++ * The types of RegionFiles controlled by the I/O thread(s). ++ */ ++ public static enum RegionFileType { ++ CHUNK_DATA, ++ POI_DATA, ++ ENTITY_DATA; ++ } ++ ++ public static RegionDataController getControllerFor(final ServerLevel world, final RegionFileType type) { ++ switch (type) { ++ case CHUNK_DATA: ++ return ((ChunkSystemServerLevel)world).moonrise$getChunkDataController(); ++ case POI_DATA: ++ return ((ChunkSystemServerLevel)world).moonrise$getPoiChunkDataController(); ++ case ENTITY_DATA: ++ return ((ChunkSystemServerLevel)world).moonrise$getEntityChunkDataController(); ++ default: ++ throw new IllegalStateException("Unknown controller type " + type); ++ } ++ } ++ ++ private static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values(); ++ ++ /** ++ * Collects RegionFile data for a certain chunk. ++ */ ++ public static final class RegionFileData { ++ ++ private final boolean[] hasResult = new boolean[CACHED_REGIONFILE_TYPES.length]; ++ private final CompoundTag[] data = new CompoundTag[CACHED_REGIONFILE_TYPES.length]; ++ private final Throwable[] throwables = new Throwable[CACHED_REGIONFILE_TYPES.length]; ++ ++ /** ++ * Sets the result associated with the specified RegionFile type. Note that ++ * results can only be set once per RegionFile type. ++ * ++ * @param type The RegionFile type. ++ * @param data The result to set. ++ */ ++ public void setData(final MoonriseRegionFileIO.RegionFileType type, final CompoundTag data) { ++ final int index = type.ordinal(); ++ ++ if (this.hasResult[index]) { ++ throw new IllegalArgumentException("Result already exists for type " + type); ++ } ++ this.hasResult[index] = true; ++ this.data[index] = data; ++ } ++ ++ /** ++ * Sets the result associated with the specified RegionFile type. Note that ++ * results can only be set once per RegionFile type. ++ * ++ * @param type The RegionFile type. ++ * @param throwable The result to set. ++ */ ++ public void setThrowable(final MoonriseRegionFileIO.RegionFileType type, final Throwable throwable) { ++ final int index = type.ordinal(); ++ ++ if (this.hasResult[index]) { ++ throw new IllegalArgumentException("Result already exists for type " + type); ++ } ++ this.hasResult[index] = true; ++ this.throwables[index] = throwable; ++ } ++ ++ /** ++ * Returns whether there is a result for the specified RegionFile type. ++ * ++ * @param type Specified RegionFile type. ++ * ++ * @return Whether a result exists for {@code type}. ++ */ ++ public boolean hasResult(final MoonriseRegionFileIO.RegionFileType type) { ++ return this.hasResult[type.ordinal()]; ++ } ++ ++ /** ++ * Returns the data result for the RegionFile type. ++ * ++ * @param type Specified RegionFile type. ++ * ++ * @throws IllegalArgumentException If the result has not been set for {@code type}. ++ * @return The data result for the specified type. If the result is a {@code Throwable}, ++ * then returns {@code null}. ++ */ ++ public CompoundTag getData(final MoonriseRegionFileIO.RegionFileType type) { ++ final int index = type.ordinal(); ++ ++ if (!this.hasResult[index]) { ++ throw new IllegalArgumentException("Result does not exist for type " + type); ++ } ++ ++ return this.data[index]; ++ } ++ ++ /** ++ * Returns the throwable result for the RegionFile type. ++ * ++ * @param type Specified RegionFile type. ++ * ++ * @throws IllegalArgumentException If the result has not been set for {@code type}. ++ * @return The throwable result for the specified type. If the result is an {@code CompoundTag}, ++ * then returns {@code null}. ++ */ ++ public Throwable getThrowable(final MoonriseRegionFileIO.RegionFileType type) { ++ final int index = type.ordinal(); ++ ++ if (!this.hasResult[index]) { ++ throw new IllegalArgumentException("Result does not exist for type " + type); ++ } ++ ++ return this.throwables[index]; ++ } ++ } ++ ++ public static void flushRegionStorages(final ServerLevel world) throws IOException { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ flushRegionStorages(world, type); ++ } ++ } ++ ++ public static void flushRegionStorages(final ServerLevel world, final RegionFileType type) throws IOException { ++ getControllerFor(world, type).getCache().flush(); ++ } ++ ++ public static void flush(final MinecraftServer server) { ++ for (final ServerLevel world : server.getAllLevels()) { ++ flush(world); ++ } ++ } ++ ++ public static void flush(final ServerLevel world) { ++ for (final RegionFileType regionFileType : CACHED_REGIONFILE_TYPES) { ++ flush(world, regionFileType); ++ } ++ } ++ ++ public static void flush(final ServerLevel world, final RegionFileType type) { ++ final RegionDataController taskController = getControllerFor(world, type); ++ ++ long failures = 1L; // start at 0.13ms ++ ++ while (taskController.hasTasks()) { ++ Thread.yield(); ++ failures = ConcurrentUtil.linearLongBackoff(failures, 125_000L, 5_000_000L); // 125us, 5ms ++ } ++ } ++ ++ public static void partialFlush(final ServerLevel world, final int tasksRemaining) { ++ for (long failures = 1L;;) { // start at 0.13ms ++ long totalTasks = 0L; ++ for (final RegionFileType regionFileType : CACHED_REGIONFILE_TYPES) { ++ totalTasks += getControllerFor(world, regionFileType).getTotalWorkingTasks(); ++ } ++ ++ if (totalTasks > (long)tasksRemaining) { ++ Thread.yield(); ++ failures = ConcurrentUtil.linearLongBackoff(failures, 125_000L, 5_000_000L); // 125us, 5ms ++ } else { ++ return; ++ } ++ } ++ } ++ ++ /** ++ * Returns the priority associated with blocking I/O based on the current thread. The goal is to avoid ++ * dumb plugins from taking away priority from threads we consider crucial. ++ * @return The priroity to use with blocking I/O on the current thread. ++ */ ++ public static Priority getIOBlockingPriorityForCurrentThread() { ++ if (TickThread.isTickThread()) { ++ return Priority.BLOCKING; ++ } ++ return Priority.HIGHEST; ++ } ++ ++ /** ++ * Returns the priority for the specified regionfile type for the specified chunk. ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @return The priority for the chunk ++ */ ++ public static Priority getPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ final RegionDataController taskController = getControllerFor(world, type); ++ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (task == null) { ++ return Priority.COMPLETING; ++ } ++ ++ return task.getPriority(); ++ } ++ ++ /** ++ * Sets the priority for all regionfile types for the specified chunk. Note that great care should ++ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different ++ * priorities. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Priority priority) { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ MoonriseRegionFileIO.setPriority(world, chunkX, chunkZ, type, priority); ++ } ++ } ++ ++ /** ++ * Sets the priority for the specified regionfile type for the specified chunk. Note that great care should ++ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different ++ * priorities. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final Priority priority) { ++ final RegionDataController taskController = getControllerFor(world, type); ++ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (task != null) { ++ task.setPriority(priority); ++ } ++ } ++ ++ /** ++ * Raises the priority for all regionfile types for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param priority New priority. ++ * ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Priority priority) { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ MoonriseRegionFileIO.raisePriority(world, chunkX, chunkZ, type, priority); ++ } ++ } ++ ++ /** ++ * Raises the priority for the specified regionfile type for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @param priority New priority. ++ * ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final Priority priority) { ++ final RegionDataController taskController = getControllerFor(world, type); ++ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (task != null) { ++ task.raisePriority(priority); ++ } ++ } ++ ++ /** ++ * Lowers the priority for all regionfile types for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Priority priority) { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ MoonriseRegionFileIO.lowerPriority(world, chunkX, chunkZ, type, priority); ++ } ++ } ++ ++ /** ++ * Lowers the priority for the specified regionfile type for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final Priority priority) { ++ final RegionDataController taskController = getControllerFor(world, type); ++ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (task != null) { ++ task.lowerPriority(priority); ++ } ++ } ++ ++ /** ++ * Schedules the chunk data to be written asynchronously. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means ++ * saves must be scheduled before a chunk is unloaded. ++ * </li> ++ * <li> ++ * Writes may be called concurrently, although only the "later" write will go through. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param data Chunk's data ++ * @param type The regionfile type to write to. ++ * ++ * @throws IllegalStateException If the file io thread has shutdown. ++ */ ++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, ++ final RegionFileType type) { ++ MoonriseRegionFileIO.scheduleSave(world, chunkX, chunkZ, data, type, Priority.NORMAL); ++ } ++ ++ /** ++ * Schedules the chunk data to be written asynchronously. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means ++ * saves must be scheduled before a chunk is unloaded. ++ * </li> ++ * <li> ++ * Writes may be called concurrently, although only the "later" write will go through. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param data Chunk's data ++ * @param type The regionfile type to write to. ++ * @param priority The minimum priority to schedule at. ++ * ++ * @throws IllegalStateException If the file io thread has shutdown. ++ */ ++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, ++ final RegionFileType type, final Priority priority) { ++ scheduleSave( ++ world, chunkX, chunkZ, ++ (final BiConsumer<CompoundTag, Throwable> consumer) -> { ++ consumer.accept(data, null); ++ }, null, type, priority ++ ); ++ } ++ ++ /** ++ * Schedules the chunk data to be written asynchronously. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means ++ * saves must be scheduled before a chunk is unloaded. ++ * </li> ++ * <li> ++ * Writes may be called concurrently, although only the "later" write will go through. ++ * </li> ++ * <li> ++ * The specified write task, if not null, will have its priority controlled by the scheduler. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param completable Chunk's pending data ++ * @param writeTask The task responsible for completing the pending chunk data ++ * @param type The regionfile type to write to. ++ * @param priority The minimum priority to schedule at. ++ * ++ * @throws IllegalStateException If the file io thread has shutdown. ++ */ ++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CallbackCompletable<CompoundTag> completable, ++ final PrioritisedExecutor.PrioritisedTask writeTask, final RegionFileType type, final Priority priority) { ++ scheduleSave(world, chunkX, chunkZ, completable::addWaiter, writeTask, type, priority); ++ } ++ ++ /** ++ * Schedules the chunk data to be written asynchronously. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means ++ * saves must be scheduled before a chunk is unloaded. ++ * </li> ++ * <li> ++ * Writes may be called concurrently, although only the "later" write will go through. ++ * </li> ++ * <li> ++ * The specified write task, if not null, will have its priority controlled by the scheduler. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param completable Chunk's pending data ++ * @param writeTask The task responsible for completing the pending chunk data ++ * @param type The regionfile type to write to. ++ * @param priority The minimum priority to schedule at. ++ * ++ * @throws IllegalStateException If the file io thread has shutdown. ++ */ ++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final Completable<CompoundTag> completable, ++ final PrioritisedExecutor.PrioritisedTask writeTask, final RegionFileType type, final Priority priority) { ++ scheduleSave(world, chunkX, chunkZ, completable::whenComplete, writeTask, type, priority); ++ } ++ ++ private static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final Consumer<BiConsumer<CompoundTag, Throwable>> scheduler, ++ final PrioritisedExecutor.PrioritisedTask writeTask, final RegionFileType type, final Priority priority) { ++ final RegionDataController taskController = getControllerFor(world, type); ++ ++ final boolean[] created = new boolean[1]; ++ final ChunkIOTask.InProgressWrite write = new ChunkIOTask.InProgressWrite(writeTask); ++ final ChunkIOTask task = taskController.chunkTasks.compute(CoordinateUtils.getChunkKey(chunkX, chunkZ), ++ (final long keyInMap, final ChunkIOTask taskRunning) -> { ++ if (taskRunning == null || taskRunning.failedWrite) { ++ // no task is scheduled or the previous write failed - meaning we need to overwrite it ++ ++ // create task ++ final ChunkIOTask newTask = new ChunkIOTask( ++ world, taskController, chunkX, chunkZ, priority, new ChunkIOTask.InProgressRead() ++ ); ++ ++ newTask.pushPendingWrite(write); ++ ++ created[0] = true; ++ ++ return newTask; ++ } ++ ++ taskRunning.pushPendingWrite(write); ++ ++ return taskRunning; ++ } ++ ); ++ ++ write.schedule(task, scheduler); ++ ++ if (created[0]) { ++ taskController.startTask(task); ++ task.scheduleWriteCompress(); ++ } else { ++ task.raisePriority(priority); ++ } ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call ++ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} ++ * for single load. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ */ ++ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock) { ++ return MoonriseRegionFileIO.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call ++ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} ++ * for single load. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param priority The minimum priority to load the data at. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ */ ++ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock, ++ final Priority priority) { ++ return MoonriseRegionFileIO.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and ++ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} ++ * for single load. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param types The regionfile type(s) to load. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock, ++ final RegionFileType... types) { ++ return MoonriseRegionFileIO.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL, types); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and ++ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} ++ * for single load. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param types The regionfile type(s) to load. ++ * @param priority The minimum priority to load the data at. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock, ++ final Priority priority, final RegionFileType... types) { ++ if (types == null) { ++ throw new NullPointerException("Types cannot be null"); ++ } ++ if (types.length == 0) { ++ throw new IllegalArgumentException("Types cannot be empty"); ++ } ++ ++ final RegionFileData ret = new RegionFileData(); ++ ++ final Cancellable[] reads = new CancellableRead[types.length]; ++ final AtomicInteger completions = new AtomicInteger(); ++ final int expectedCompletions = types.length; ++ ++ for (int i = 0; i < expectedCompletions; ++i) { ++ final RegionFileType type = types[i]; ++ reads[i] = MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, ++ (final CompoundTag data, final Throwable throwable) -> { ++ if (throwable != null) { ++ ret.setThrowable(type, throwable); ++ } else { ++ ret.setData(type, data); ++ } ++ ++ if (completions.incrementAndGet() == expectedCompletions) { ++ onComplete.accept(ret); ++ } ++ }, intendingToBlock, priority); ++ } ++ ++ return new CancellableReads(reads); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call ++ * {@code onComplete}. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete, ++ final boolean intendingToBlock) { ++ return MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, Priority.NORMAL); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call ++ * {@code onComplete}. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param priority Minimum priority to load the data at. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete, ++ final boolean intendingToBlock, final Priority priority) { ++ final RegionDataController taskController = getControllerFor(world, type); ++ ++ final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion(); ++ ++ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final BiLong1Function<ChunkIOTask, ChunkIOTask> compute = (final long keyInMap, final ChunkIOTask running) -> { ++ if (running == null) { ++ // not scheduled ++ ++ // set up task ++ final ChunkIOTask newTask = new ChunkIOTask( ++ world, taskController, chunkX, chunkZ, priority, new ChunkIOTask.InProgressRead() ++ ); ++ newTask.inProgressRead.addToAsyncWaiters(onComplete); ++ ++ callbackInfo.tasksNeedReadScheduling = true; ++ return newTask; ++ } ++ ++ final ChunkIOTask.InProgressWrite pendingWrite = running.inProgressWrite; ++ ++ if (pendingWrite == null) { ++ // need to add to waiters here, because the regionfile thread will use compute() to lock and check for cancellations ++ if (!running.inProgressRead.addToAsyncWaiters(onComplete)) { ++ callbackInfo.data = running.inProgressRead.value; ++ callbackInfo.throwable = running.inProgressRead.throwable; ++ callbackInfo.completeNow = true; ++ return running; ++ } ++ ++ callbackInfo.read = running.inProgressRead; ++ ++ return running; ++ } ++ ++ // at this stage we have to use the in progress write's data to avoid an order issue ++ ++ if (!pendingWrite.addToAsyncWaiters(onComplete)) { ++ // data is ready now ++ callbackInfo.data = pendingWrite.value; ++ callbackInfo.throwable = pendingWrite.throwable; ++ callbackInfo.completeNow = true; ++ return running; ++ } ++ ++ callbackInfo.write = pendingWrite; ++ ++ return running; ++ }; ++ ++ final ChunkIOTask ret = taskController.chunkTasks.compute(key, compute); ++ ++ // needs to be scheduled ++ if (callbackInfo.tasksNeedReadScheduling) { ++ taskController.startTask(ret); ++ ret.scheduleReadIO(); ++ } else if (callbackInfo.completeNow) { ++ try { ++ onComplete.accept(callbackInfo.data == null ? null : callbackInfo.data.copy(), callbackInfo.throwable); ++ } catch (final Throwable thr) { ++ LOGGER.error("Callback " + ConcurrentUtil.genericToString(onComplete) + " synchronously failed to handle chunk data for task " + ret.toString(), thr); ++ } ++ } else { ++ // we're waiting on a task we didn't schedule, so raise its priority to what we want ++ ret.raisePriority(priority); ++ } ++ ++ return new CancellableRead(onComplete, callbackInfo.read, callbackInfo.write); ++ } ++ ++ private static final class ImmediateCallbackCompletion { ++ ++ private CompoundTag data; ++ private Throwable throwable; ++ private boolean completeNow; ++ private boolean tasksNeedReadScheduling; ++ private ChunkIOTask.InProgressRead read; ++ private ChunkIOTask.InProgressWrite write; ++ ++ } ++ ++ /** ++ * Schedules a load task to be executed asynchronously, and blocks on that task. ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param type Regionfile type ++ * @param priority Minimum priority to load the data at. ++ * ++ * @return The chunk data for the chunk. Note that a {@code null} result means the chunk or regionfile does not exist on disk. ++ * ++ * @throws IOException If the load fails for any reason ++ */ ++ public static CompoundTag loadData(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final Priority priority) throws IOException { ++ final CompletableFuture<CompoundTag> ret = new CompletableFuture<>(); ++ ++ MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> { ++ if (thr != null) { ++ ret.completeExceptionally(thr); ++ } else { ++ ret.complete(compound); ++ } ++ }, true, priority); ++ ++ try { ++ return ret.join(); ++ } catch (final CompletionException ex) { ++ throw new IOException(ex); ++ } ++ } ++ ++ private static final class CancellableRead implements Cancellable { ++ ++ private BiConsumer<CompoundTag, Throwable> callback; ++ private ChunkIOTask.InProgressRead read; ++ private ChunkIOTask.InProgressWrite write; ++ ++ private CancellableRead(final BiConsumer<CompoundTag, Throwable> callback, ++ final ChunkIOTask.InProgressRead read, ++ final ChunkIOTask.InProgressWrite write) { ++ this.callback = callback; ++ this.read = read; ++ this.write = write; ++ } ++ ++ @Override ++ public boolean cancel() { ++ final BiConsumer<CompoundTag, Throwable> callback = this.callback; ++ final ChunkIOTask.InProgressRead read = this.read; ++ final ChunkIOTask.InProgressWrite write = this.write; ++ ++ if (callback == null || (read == null && write == null)) { ++ return false; ++ } ++ ++ this.callback = null; ++ this.read = null; ++ this.write = null; ++ ++ if (read != null) { ++ return read.cancel(callback); ++ } ++ if (write != null) { ++ return write.cancel(callback); ++ } ++ ++ // unreachable ++ throw new InternalError(); ++ } ++ } ++ ++ private static final class CancellableReads implements Cancellable { ++ ++ private Cancellable[] reads; ++ private static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class); ++ ++ private CancellableReads(final Cancellable[] reads) { ++ this.reads = reads; ++ } ++ ++ @Override ++ public boolean cancel() { ++ final Cancellable[] reads = (Cancellable[])READS_HANDLE.getAndSet((CancellableReads)this, (Cancellable[])null); ++ ++ if (reads == null) { ++ return false; ++ } ++ ++ boolean ret = false; ++ ++ for (final Cancellable read : reads) { ++ ret |= read.cancel(); ++ } ++ ++ return ret; ++ } ++ } ++ ++ private static final class ChunkIOTask { ++ ++ private final ServerLevel world; ++ private final RegionDataController regionDataController; ++ private final int chunkX; ++ private final int chunkZ; ++ private Priority priority; ++ private PrioritisedExecutor.PrioritisedTask currentTask; ++ ++ private final InProgressRead inProgressRead; ++ private volatile InProgressWrite inProgressWrite; ++ private final ReferenceOpenHashSet<InProgressWrite> allPendingWrites = new ReferenceOpenHashSet<>(); ++ ++ private RegionDataController.ReadData readData; ++ private RegionDataController.WriteData writeData; ++ private boolean failedWrite; ++ ++ public ChunkIOTask(final ServerLevel world, final RegionDataController regionDataController, ++ final int chunkX, final int chunkZ, final Priority priority, final InProgressRead inProgressRead) { ++ this.world = world; ++ this.regionDataController = regionDataController; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.priority = priority; ++ this.inProgressRead = inProgressRead; ++ } ++ ++ public Priority getPriority() { ++ synchronized (this) { ++ return this.priority; ++ } ++ } ++ ++ // must hold lock on this object ++ private void updatePriority(final Priority priority) { ++ this.priority = priority; ++ if (this.currentTask != null) { ++ this.currentTask.setPriority(priority); ++ } ++ for (final InProgressWrite write : this.allPendingWrites) { ++ if (write.writeTask != null) { ++ write.writeTask.setPriority(priority); ++ } ++ } ++ } ++ ++ public boolean setPriority(final Priority priority) { ++ synchronized (this) { ++ if (this.priority == priority) { ++ return false; ++ } ++ ++ this.updatePriority(priority); ++ ++ return true; ++ } ++ } ++ ++ public boolean raisePriority(final Priority priority) { ++ synchronized (this) { ++ if (this.priority.isHigherOrEqualPriority(priority)) { ++ return false; ++ } ++ ++ this.updatePriority(priority); ++ ++ return true; ++ } ++ } ++ ++ public boolean lowerPriority(final Priority priority) { ++ synchronized (this) { ++ if (this.priority.isLowerOrEqualPriority(priority)) { ++ return false; ++ } ++ ++ this.updatePriority(priority); ++ ++ return true; ++ } ++ } ++ ++ private void pushPendingWrite(final InProgressWrite write) { ++ this.inProgressWrite = write; ++ synchronized (this) { ++ this.allPendingWrites.add(write); ++ if (write.writeTask != null) { ++ write.writeTask.setPriority(this.priority); ++ } ++ } ++ } ++ ++ private void pendingWriteComplete(final InProgressWrite write) { ++ synchronized (this) { ++ this.allPendingWrites.remove(write); ++ } ++ } ++ ++ public void scheduleReadIO() { ++ final PrioritisedExecutor.PrioritisedTask task; ++ synchronized (this) { ++ task = this.regionDataController.ioScheduler.createTask(this.chunkX, this.chunkZ, this::performReadIO, this.priority); ++ this.currentTask = task; ++ } ++ task.queue(); ++ } ++ ++ private void performReadIO() { ++ final InProgressRead read = this.inProgressRead; ++ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ); ++ ++ final boolean[] canRead = new boolean[] { true }; ++ ++ if (read.hasNoWaiters()) { ++ // cancelled read? go to task controller to confirm ++ final ChunkIOTask inMap = this.regionDataController.chunkTasks.compute(chunkKey, (final long keyInMap, final ChunkIOTask valueInMap) -> { ++ if (valueInMap == null) { ++ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkIOTask.this.toString() + ", report this!"); ++ } ++ if (valueInMap != ChunkIOTask.this) { ++ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkIOTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); ++ } ++ ++ if (!read.hasNoWaiters()) { ++ return valueInMap; ++ } else { ++ canRead[0] = false; ++ } ++ ++ if (valueInMap.inProgressWrite != null) { ++ return valueInMap; ++ } ++ ++ return null; ++ }); ++ ++ if (inMap == null) { ++ this.regionDataController.endTask(this); ++ // read is cancelled - and no write pending, so we're done ++ return; ++ } ++ // if there is a write in progress, we don't actually have to worry about waiters gaining new entries - ++ // the readers will just use the in progress write, so the value in canRead is good to use without ++ // further synchronisation. ++ } ++ ++ if (canRead[0]) { ++ RegionDataController.ReadData readData = null; ++ Throwable throwable = null; ++ ++ try { ++ readData = this.regionDataController.readData(this.chunkX, this.chunkZ); ++ } catch (final Throwable thr) { ++ throwable = thr; ++ LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr); ++ } ++ ++ if (throwable != null) { ++ this.finishRead(null, throwable); ++ } else { ++ switch (readData.result()) { ++ case NO_DATA: ++ case SYNC_READ: { ++ this.finishRead(readData.syncRead(), null); ++ break; ++ } ++ case HAS_DATA: { ++ this.readData = readData; ++ this.scheduleReadDecompress(); ++ // read will handle write scheduling ++ return; ++ } ++ default: { ++ throw new IllegalStateException("Unknown state: " + readData.result()); ++ } ++ } ++ } ++ } ++ ++ if (!this.tryAbortWrite()) { ++ this.scheduleWriteCompress(); ++ } ++ } ++ ++ private void scheduleReadDecompress() { ++ final PrioritisedExecutor.PrioritisedTask task; ++ synchronized (this) { ++ task = this.regionDataController.compressionExecutor.createTask(this::performReadDecompress, this.priority); ++ this.currentTask = task; ++ } ++ task.queue(); ++ } ++ ++ private void performReadDecompress() { ++ final RegionDataController.ReadData readData = this.readData; ++ this.readData = null; ++ ++ CompoundTag compoundTag = null; ++ Throwable throwable = null; ++ ++ try { ++ compoundTag = this.regionDataController.finishRead(this.chunkX, this.chunkZ, readData); ++ } catch (final Throwable thr) { ++ throwable = thr; ++ LOGGER.error("Failed to decompress chunk data for task: " + this.toString(), thr); ++ } ++ ++ if (compoundTag == null) { ++ // need to re-try from the start ++ this.scheduleReadIO(); ++ return; ++ } ++ ++ this.finishRead(compoundTag, throwable); ++ if (!this.tryAbortWrite()) { ++ this.scheduleWriteCompress(); ++ } ++ } ++ ++ private void finishRead(final CompoundTag compoundTag, final Throwable throwable) { ++ this.inProgressRead.complete(this, compoundTag, throwable); ++ } ++ ++ public void scheduleWriteCompress() { ++ final InProgressWrite inProgressWrite = this.inProgressWrite; ++ ++ final PrioritisedExecutor.PrioritisedTask task; ++ synchronized (this) { ++ task = this.regionDataController.compressionExecutor.createTask(() -> { ++ ChunkIOTask.this.performWriteCompress(inProgressWrite); ++ }, this.priority); ++ this.currentTask = task; ++ } ++ ++ inProgressWrite.addToWaiters(this, (final CompoundTag data, final Throwable throwable) -> { ++ task.queue(); ++ }); ++ } ++ ++ private boolean tryAbortWrite() { ++ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ); ++ if (this.inProgressWrite == null) { ++ final ChunkIOTask inMap = this.regionDataController.chunkTasks.compute(chunkKey, (final long keyInMap, final ChunkIOTask valueInMap) -> { ++ if (valueInMap == null) { ++ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkIOTask.this.toString() + ", report this!"); ++ } ++ if (valueInMap != ChunkIOTask.this) { ++ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkIOTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); ++ } ++ ++ if (valueInMap.inProgressWrite != null) { ++ return valueInMap; ++ } ++ ++ return null; ++ }); ++ ++ if (inMap == null) { ++ this.regionDataController.endTask(this); ++ return true; // set the task value to null, indicating we're done ++ } // else: inProgressWrite changed, so now we have something to write ++ } ++ ++ return false; ++ } ++ ++ private void performWriteCompress(final InProgressWrite inProgressWrite) { ++ final CompoundTag write = inProgressWrite.value; ++ if (!inProgressWrite.isComplete()) { ++ throw new IllegalStateException("Should be writable"); ++ } ++ ++ RegionDataController.WriteData writeData = null; ++ boolean failedWrite = false; ++ ++ try { ++ writeData = this.regionDataController.startWrite(this.chunkX, this.chunkZ, write); ++ } catch (final Throwable thr) { ++ failedWrite = thr instanceof IOException; ++ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr); ++ } ++ ++ if (writeData == null) { ++ // null if a throwable was encountered ++ ++ // we cannot continue to the I/O stage here, so try to complete ++ ++ if (this.tryCompleteWrite(inProgressWrite, failedWrite)) { ++ return; ++ } else { ++ // fetch new data and try again ++ this.scheduleWriteCompress(); ++ return; ++ } ++ } else { ++ // writeData != null && !failedWrite ++ // we can continue to I/O stage ++ this.writeData = writeData; ++ this.scheduleWriteIO(inProgressWrite); ++ return; ++ } ++ } ++ ++ private void scheduleWriteIO(final InProgressWrite inProgressWrite) { ++ final PrioritisedExecutor.PrioritisedTask task; ++ synchronized (this) { ++ task = this.regionDataController.ioScheduler.createTask(this.chunkX, this.chunkZ, () -> { ++ ChunkIOTask.this.runWriteIO(inProgressWrite); ++ }, this.priority); ++ this.currentTask = task; ++ } ++ task.queue(); ++ } ++ ++ private void runWriteIO(final InProgressWrite inProgressWrite) { ++ RegionDataController.WriteData writeData = this.writeData; ++ this.writeData = null; ++ ++ boolean failedWrite = false; ++ ++ try { ++ this.regionDataController.finishWrite(this.chunkX, this.chunkZ, writeData); ++ } catch (final Throwable thr) { ++ failedWrite = thr instanceof IOException; ++ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr); ++ } ++ ++ if (!this.tryCompleteWrite(inProgressWrite, failedWrite)) { ++ // fetch new data and try again ++ this.scheduleWriteCompress(); ++ } ++ return; ++ } ++ ++ private boolean tryCompleteWrite(final InProgressWrite written, final boolean failedWrite) { ++ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ); ++ ++ final boolean[] done = new boolean[] { false }; ++ ++ this.regionDataController.chunkTasks.compute(chunkKey, (final long keyInMap, final ChunkIOTask valueInMap) -> { ++ if (valueInMap == null) { ++ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkIOTask.this.toString() + ", report this!"); ++ } ++ if (valueInMap != ChunkIOTask.this) { ++ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkIOTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); ++ } ++ if (valueInMap.inProgressWrite == written) { ++ valueInMap.failedWrite = failedWrite; ++ done[0] = true; ++ // keep the data in map if we failed the write so we can try to prevent data loss ++ return failedWrite ? valueInMap : null; ++ } ++ // different data than expected, means we need to retry write ++ return valueInMap; ++ }); ++ ++ if (done[0]) { ++ this.regionDataController.endTask(this); ++ return true; ++ } ++ return false; ++ } ++ ++ @Override ++ public String toString() { ++ return "Task for world: '" + WorldUtil.getWorldName(this.world) + "' at (" + this.chunkX + "," ++ + this.chunkZ + ") type: " + this.regionDataController.type.name() + ", hash: " + this.hashCode(); ++ } ++ ++ private static final class InProgressRead { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(InProgressRead.class); ++ ++ private CompoundTag value; ++ private Throwable throwable; ++ private final MultiThreadedQueue<BiConsumer<CompoundTag, Throwable>> callbacks = new MultiThreadedQueue<>(); ++ ++ public boolean hasNoWaiters() { ++ return this.callbacks.isEmpty(); ++ } ++ ++ public boolean addToAsyncWaiters(final BiConsumer<CompoundTag, Throwable> callback) { ++ return this.callbacks.add(callback); ++ } ++ ++ public boolean cancel(final BiConsumer<CompoundTag, Throwable> callback) { ++ return this.callbacks.remove(callback); ++ } ++ ++ public void complete(final ChunkIOTask task, final CompoundTag value, final Throwable throwable) { ++ this.value = value; ++ this.throwable = throwable; ++ ++ BiConsumer<CompoundTag, Throwable> consumer; ++ while ((consumer = this.callbacks.pollOrBlockAdds()) != null) { ++ try { ++ consumer.accept(value == null ? null : value.copy(), throwable); ++ } catch (final Throwable thr) { ++ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data (read) for task " + task.toString(), thr); ++ } ++ } ++ } ++ } ++ ++ private static final class InProgressWrite { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(InProgressWrite.class); ++ ++ private CompoundTag value; ++ private Throwable throwable; ++ private volatile boolean complete; ++ private final MultiThreadedQueue<BiConsumer<CompoundTag, Throwable>> callbacks = new MultiThreadedQueue<>(); ++ ++ private final PrioritisedExecutor.PrioritisedTask writeTask; ++ ++ public InProgressWrite(final PrioritisedExecutor.PrioritisedTask writeTask) { ++ this.writeTask = writeTask; ++ } ++ ++ public boolean isComplete() { ++ return this.complete; ++ } ++ ++ public void schedule(final ChunkIOTask task, final Consumer<BiConsumer<CompoundTag, Throwable>> scheduler) { ++ scheduler.accept((final CompoundTag data, final Throwable throwable) -> { ++ InProgressWrite.this.complete(task, data, throwable); ++ }); ++ } ++ ++ public boolean addToAsyncWaiters(final BiConsumer<CompoundTag, Throwable> callback) { ++ return this.callbacks.add(callback); ++ } ++ ++ public void addToWaiters(final ChunkIOTask task, final BiConsumer<CompoundTag, Throwable> consumer) { ++ if (!this.callbacks.add(consumer)) { ++ this.syncAccept(task, consumer, this.value, this.throwable); ++ } ++ } ++ ++ private void syncAccept(final ChunkIOTask task, final BiConsumer<CompoundTag, Throwable> consumer, final CompoundTag value, final Throwable throwable) { ++ try { ++ consumer.accept(value == null ? null : value.copy(), throwable); ++ } catch (final Throwable thr) { ++ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data (write) for task " + task.toString(), thr); ++ } ++ } ++ ++ public void complete(final ChunkIOTask task, final CompoundTag value, final Throwable throwable) { ++ this.value = value; ++ this.throwable = throwable; ++ this.complete = true; ++ ++ task.pendingWriteComplete(this); ++ ++ BiConsumer<CompoundTag, Throwable> consumer; ++ while ((consumer = this.callbacks.pollOrBlockAdds()) != null) { ++ this.syncAccept(task, consumer, value, throwable); ++ } ++ } ++ ++ public boolean cancel(final BiConsumer<CompoundTag, Throwable> callback) { ++ return this.callbacks.remove(callback); ++ } ++ } ++ } ++ ++ public static abstract class RegionDataController { ++ ++ public final RegionFileType type; ++ private final PrioritisedExecutor compressionExecutor; ++ private final IOScheduler ioScheduler; ++ private final ConcurrentLong2ReferenceChainedHashTable<ChunkIOTask> chunkTasks = new ConcurrentLong2ReferenceChainedHashTable<>(); ++ ++ private final AtomicLong inProgressTasks = new AtomicLong(); ++ ++ public RegionDataController(final RegionFileType type, final PrioritisedExecutor ioExecutor, ++ final PrioritisedExecutor compressionExecutor) { ++ this.type = type; ++ this.compressionExecutor = compressionExecutor; ++ this.ioScheduler = new IOScheduler(ioExecutor); ++ } ++ ++ final void startTask(final ChunkIOTask task) { ++ this.inProgressTasks.getAndIncrement(); ++ } ++ ++ final void endTask(final ChunkIOTask task) { ++ this.inProgressTasks.getAndDecrement(); ++ } ++ ++ public boolean hasTasks() { ++ return this.inProgressTasks.get() != 0L; ++ } ++ ++ public long getTotalWorkingTasks() { ++ return this.inProgressTasks.get(); ++ } ++ ++ public abstract RegionFileStorage getCache(); ++ ++ public static record WriteData(CompoundTag input, WriteResult result, DataOutputStream output, IORunnable write) { ++ public static enum WriteResult { ++ WRITE, ++ DELETE; ++ } ++ } ++ ++ public abstract WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException; ++ ++ public abstract void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException; ++ ++ public static record ReadData(ReadResult result, DataInputStream input, CompoundTag syncRead) { ++ public static enum ReadResult { ++ NO_DATA, ++ HAS_DATA, ++ SYNC_READ; ++ } ++ } ++ ++ public abstract ReadData readData(final int chunkX, final int chunkZ) throws IOException; ++ ++ // if the return value is null, then the caller needs to re-try with a new call to readData() ++ public abstract CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException; ++ ++ public static interface IORunnable { ++ ++ public void run(final RegionFile regionFile) throws IOException; ++ ++ } ++ } ++ ++ private static final class IOScheduler { ++ ++ private final ConcurrentLong2ReferenceChainedHashTable<RegionIOTasks> regionTasks = new ConcurrentLong2ReferenceChainedHashTable<>(); ++ private final PrioritisedExecutor executor; ++ ++ public IOScheduler(final PrioritisedExecutor executor) { ++ this.executor = executor; ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, ++ final Runnable run, final Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask[] ret = new PrioritisedExecutor.PrioritisedTask[1]; ++ final long subOrder = this.executor.generateNextSubOrder(); ++ this.regionTasks.compute(CoordinateUtils.getChunkKey(chunkX >> REGION_FILE_SHIFT, chunkZ >> REGION_FILE_SHIFT), ++ (final long regionKey, final RegionIOTasks existing) -> { ++ final RegionIOTasks res; ++ if (existing != null) { ++ res = existing; ++ } else { ++ res = new RegionIOTasks(regionKey, IOScheduler.this); ++ } ++ ++ ret[0] = res.createTask(run, priority, subOrder); ++ ++ return res; ++ }); ++ ++ return ret[0]; ++ } ++ } ++ ++ private static final class RegionIOTasks implements Runnable { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(RegionIOTasks.class); ++ ++ private final PrioritisedTaskQueue queue = new PrioritisedTaskQueue(); ++ private final long regionKey; ++ private final IOScheduler ioScheduler; ++ private long createdTasks; ++ private long executedTasks; ++ ++ private PrioritisedExecutor.PrioritisedTask task; ++ ++ public RegionIOTasks(final long regionKey, final IOScheduler ioScheduler) { ++ this.regionKey = regionKey; ++ this.ioScheduler = ioScheduler; ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createTask(final Runnable run, final Priority priority, ++ final long subOrder) { ++ ++this.createdTasks; ++ return new WrappedTask(this.queue.createTask(run, priority, subOrder)); ++ } ++ ++ private void adjustTaskPriority() { ++ final PrioritisedTaskQueue.PrioritySubOrderPair priority = this.queue.getHighestPrioritySubOrder(); ++ if (this.task == null) { ++ if (priority == null) { ++ return; ++ } ++ this.task = this.ioScheduler.executor.createTask(this, priority.priority(), priority.subOrder()); ++ this.task.queue(); ++ } else { ++ if (priority == null) { ++ throw new IllegalStateException(); ++ } else { ++ this.task.setPriorityAndSubOrder(priority.priority(), priority.subOrder()); ++ } ++ } ++ } ++ ++ @Override ++ public void run() { ++ final Runnable run; ++ synchronized (this) { ++ run = this.queue.pollTask(); ++ } ++ ++ try { ++ run.run(); ++ } finally { ++ synchronized (this) { ++ this.task = null; ++ this.adjustTaskPriority(); ++ } ++ this.ioScheduler.regionTasks.compute(this.regionKey, (final long keyInMap, final RegionIOTasks tasks) -> { ++ if (tasks != RegionIOTasks.this) { ++ throw new IllegalStateException("Region task mismatch"); ++ } ++ ++tasks.executedTasks; ++ if (tasks.createdTasks != tasks.executedTasks) { ++ return tasks; ++ } ++ ++ if (tasks.task != null) { ++ throw new IllegalStateException("Task may not be null when created==executed"); ++ } ++ ++ return null; ++ }); ++ } ++ } ++ ++ private final class WrappedTask implements PrioritisedExecutor.PrioritisedTask { ++ ++ private final PrioritisedExecutor.PrioritisedTask wrapped; ++ ++ public WrappedTask(final PrioritisedExecutor.PrioritisedTask wrap) { ++ this.wrapped = wrap; ++ } ++ ++ @Override ++ public PrioritisedExecutor getExecutor() { ++ return RegionIOTasks.this.ioScheduler.executor; ++ } ++ ++ @Override ++ public boolean queue() { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.queue()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ ++ @Override ++ public boolean isQueued() { ++ return this.wrapped.isQueued(); ++ } ++ ++ @Override ++ public boolean cancel() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean execute() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public Priority getPriority() { ++ return this.wrapped.getPriority(); ++ } ++ ++ @Override ++ public boolean setPriority(final Priority priority) { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.setPriority(priority) && this.wrapped.isQueued()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ ++ @Override ++ public boolean raisePriority(final Priority priority) { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.raisePriority(priority) && this.wrapped.isQueued()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ ++ @Override ++ public boolean lowerPriority(final Priority priority) { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.lowerPriority(priority) && this.wrapped.isQueued()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ ++ @Override ++ public long getSubOrder() { ++ return this.wrapped.getSubOrder(); ++ } ++ ++ @Override ++ public boolean setSubOrder(final long subOrder) { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.setSubOrder(subOrder) && this.wrapped.isQueued()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ ++ @Override ++ public boolean raiseSubOrder(final long subOrder) { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.raiseSubOrder(subOrder) && this.wrapped.isQueued()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ ++ @Override ++ public boolean lowerSubOrder(final long subOrder) { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.lowerSubOrder(subOrder) && this.wrapped.isQueued()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ ++ @Override ++ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) { ++ synchronized (RegionIOTasks.this) { ++ if (this.wrapped.setPriorityAndSubOrder(priority, subOrder) && this.wrapped.isQueued()) { ++ RegionIOTasks.this.adjustTaskPriority(); ++ return true; ++ } ++ return false; ++ } ++ } ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a36ab89f5c37f5f9ab0152f087bb4cf3560f8581 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java +@@ -0,0 +1,50 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller; ++ ++import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemChunkMap; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkStorage; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.storage.RegionFileStorage; ++import java.io.IOException; ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.CompletionException; ++ ++public final class ChunkDataController extends MoonriseRegionFileIO.RegionDataController { ++ ++ private final ServerLevel world; ++ ++ public ChunkDataController(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { ++ super(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor); ++ this.world = world; ++ } ++ ++ @Override ++ public RegionFileStorage getCache() { ++ return ((ChunkSystemChunkStorage)this.world.getChunkSource().chunkMap).moonrise$getRegionStorage(); ++ } ++ ++ @Override ++ public WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$startWrite(chunkX, chunkZ, compound); ++ } ++ ++ @Override ++ public void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException { ++ ((ChunkSystemChunkMap)this.world.getChunkSource().chunkMap).moonrise$writeFinishCallback(new ChunkPos(chunkX, chunkZ)); ++ ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishWrite(chunkX, chunkZ, writeData); ++ } ++ ++ @Override ++ public ReadData readData(final int chunkX, final int chunkZ) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$readData(chunkX, chunkZ); ++ } ++ ++ @Override ++ public CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishRead(chunkX, chunkZ, readData); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java +new file mode 100644 +index 0000000000000000000000000000000000000000..828c868f68c2a20bf90d0f7ec253fdeb591f15f6 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java +@@ -0,0 +1,73 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller; ++ ++import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.storage.EntityStorage; ++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 MoonriseRegionFileIO.RegionDataController { ++ ++ private final EntityRegionFileStorage storage; ++ ++ public EntityDataController(final EntityRegionFileStorage storage, final ChunkTaskScheduler taskScheduler) { ++ super(MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor); ++ this.storage = storage; ++ } ++ ++ @Override ++ public RegionFileStorage getCache() { ++ return this.storage; ++ } ++ ++ @Override ++ public WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { ++ checkPosition(new ChunkPos(chunkX, chunkZ), compound); ++ ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$startWrite(chunkX, chunkZ, compound); ++ } ++ ++ @Override ++ public void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException { ++ ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishWrite(chunkX, chunkZ, writeData); ++ } ++ ++ @Override ++ public ReadData readData(final int chunkX, final int chunkZ) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$readData(chunkX, chunkZ); ++ } ++ ++ @Override ++ public CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishRead(chunkX, chunkZ, readData); ++ } ++ ++ private static void checkPosition(final ChunkPos pos, final CompoundTag nbt) { ++ final ChunkPos nbtPos = nbt == null ? null : EntityStorage.readChunkPos(nbt); ++ if (nbtPos != null && !pos.equals(nbtPos)) { ++ throw new IllegalArgumentException( ++ "Entity chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos.toString() ++ + " but compound says coordinate is " + nbtPos ++ ); ++ } ++ } ++ ++ public static final class EntityRegionFileStorage extends RegionFileStorage { ++ ++ 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 { ++ checkPosition(pos, nbt); ++ super.write(pos, nbt); ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bd0d782852f9cfe5bc0b5339ecf4d82c10332ec9 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java +@@ -0,0 +1,45 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller; ++ ++import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.chunk.storage.RegionFileStorage; ++import java.io.IOException; ++ ++public final class PoiDataController extends MoonriseRegionFileIO.RegionDataController { ++ ++ private final ServerLevel world; ++ ++ public PoiDataController(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { ++ super(MoonriseRegionFileIO.RegionFileType.POI_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor); ++ this.world = world; ++ } ++ ++ @Override ++ public RegionFileStorage getCache() { ++ return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$getRegionStorage(); ++ } ++ ++ @Override ++ public WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$startWrite(chunkX, chunkZ, compound); ++ } ++ ++ @Override ++ public void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException { ++ ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishWrite(chunkX, chunkZ, writeData); ++ } ++ ++ @Override ++ public ReadData readData(final int chunkX, final int chunkZ) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$readData(chunkX, chunkZ); ++ } ++ ++ @Override ++ public CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException { ++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishRead(chunkX, chunkZ, readData); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..47a4d3376d08dde94a39254bec21473ff27f53e6 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java +@@ -0,0 +1,10 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level; ++ ++import net.minecraft.world.level.ChunkPos; ++import java.io.IOException; ++ ++public interface ChunkSystemChunkMap { ++ ++ public void moonrise$writeFinishCallback(final ChunkPos pos) throws IOException; ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5d4d650186b18eb00782429d53d861564d8e4ba9 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java +@@ -0,0 +1,33 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level; ++ ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.LevelChunk; ++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(); ++ ++ public ChunkData moonrise$getChunkData(final long chunkKey); ++ ++ public ChunkData moonrise$getChunkData(final int chunkX, final int chunkZ); ++ ++ public ChunkData moonrise$requestChunkData(final long chunkKey); ++ ++ public ChunkData moonrise$releaseChunkData(final long chunkKey); ++ ++ public boolean moonrise$areChunksLoaded(final int fromX, final int fromZ, final int toX, final int toZ); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/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..c278f8ef806f0b45c28cc3040c7db052cb51e053 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java +@@ -0,0 +1,62 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level; ++ ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.list.ReferenceList; ++import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; ++import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import net.minecraft.core.BlockPos; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ServerChunkCache; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import java.util.List; ++import java.util.function.Consumer; ++ ++public interface ChunkSystemServerLevel extends ChunkSystemLevel { ++ ++ public ChunkTaskScheduler moonrise$getChunkTaskScheduler(); ++ ++ public MoonriseRegionFileIO.RegionDataController moonrise$getChunkDataController(); ++ ++ public MoonriseRegionFileIO.RegionDataController moonrise$getPoiChunkDataController(); ++ ++ public MoonriseRegionFileIO.RegionDataController moonrise$getEntityChunkDataController(); ++ ++ public int moonrise$getRegionChunkShift(); ++ ++ // Paper ++ ++ public RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader(); ++ ++ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, ++ final Priority priority, ++ final Consumer<List<ChunkAccess>> onLoad); ++ ++ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, ++ final ChunkStatus chunkStatus, final Priority priority, ++ final Consumer<List<ChunkAccess>> onLoad); ++ ++ public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, ++ final 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 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/ChunkData.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8b9dc582627b46843f4b5ea6f8c3df2d8cac46fa +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java +@@ -0,0 +1,21 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; ++ ++import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; ++ ++public final class ChunkData { ++ ++ private int referenceCount = 0; ++ public NearbyPlayers.TrackedChunk nearbyPlayers; // Moonrise - nearby players ++ ++ public ChunkData() { ++ ++ } ++ ++ public int increaseRef() { ++ return ++this.referenceCount; ++ } ++ ++ public int decreaseRef() { ++ return --this.referenceCount; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/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..aacd543f03b35908011d0c2891e978cc093ebcf5 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java +@@ -0,0 +1,12 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; ++ ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; ++import net.minecraft.server.level.ChunkMap; ++ ++public interface ChunkSystemDistanceManager { ++ ++ public ChunkMap moonrise$getChunkMap(); ++ ++ public ChunkHolderManager moonrise$getChunkHolderManager(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/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..5ed6599d1f9a2edf8c904f3602b06d26d857600c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java +@@ -0,0 +1,798 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level.entity; ++ ++import ca.spottedleaf.moonrise.common.PlatformHooks; ++import ca.spottedleaf.moonrise.common.list.EntityList; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; ++import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity; ++import com.google.common.collect.ImmutableList; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; ++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.EntitySpawnReason; ++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; ++ ++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; ++ public final ChunkData chunkData; ++ ++ 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 ChunkData chunkData, 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; ++ this.chunkData = chunkData; ++ } ++ ++ public static List<Entity> readEntities(final ServerLevel world, final CompoundTag compoundTag) { ++ // TODO check this and below on update for format changes ++ return EntityType.loadEntitiesRecursive(compoundTag.getList("Entities", 10), world, EntitySpawnReason.LOAD).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 : PlatformHooks.get().modifySavedEntities(world, chunkPos.x, chunkPos.z, 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()) { ++ PlatformHooks.get().unloadEntity(entity); ++ if (entity.isVehicle()) { ++ // we cannot assume that these entities are contained within this chunk, because entities can ++ // desync - so we need to remove them all ++ for (final Entity passenger : entity.getIndirectPassengers()) { ++ PlatformHooks.get().unloadEntity(passenger); ++ } ++ } ++ } ++ } ++ ++ return this.entities.size() != 0; ++ } ++ ++ public 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); ++ ((ChunkSystemEntity)entity).moonrise$setChunkData(this.chunkData); ++ 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); ++ ((ChunkSystemEntity)entity).moonrise$setChunkData(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..93335de8cf514dc8417e4b9b2d495663deda2904 +--- /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 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.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, final boolean fromDisk, final boolean event); ++ ++ 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, true)) { ++ // 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.addNewEntity(entity, true); ++ } ++ ++ public boolean addNewEntity(final Entity entity, final boolean event) { ++ return this.addEntity(entity, false, event); ++ } ++ ++ public static Visibility getEntityStatus(final Entity entity) { ++ 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 boolean event) { ++ final BlockPos pos = entity.blockPosition(); ++ final int sectionX = pos.getX() >> 4; ++ final int sectionY = Mth.clamp(pos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); ++ final int sectionZ = pos.getZ() >> 4; ++ this.checkThread(sectionX, sectionZ, "Cannot add entity off-main thread"); ++ ++ 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, fromDisk, event)) { ++ 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, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); ++ 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); ++ final 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"); ++ 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) {} ++ } ++} +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..a038215156a163b0b1cbc870ada5b4ac85ed1335 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java +@@ -0,0 +1,129 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.client; ++ ++import ca.spottedleaf.moonrise.common.PlatformHooks; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; ++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, null, ++ 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) { ++ PlatformHooks.get().entityMove( ++ entity, ++ CoordinateUtils.getChunkSectionKey(oldSectionX, oldSectionY, oldSectionZ), ++ CoordinateUtils.getChunkSectionKey(newSectionX, newSectionY, 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, final boolean fromDisk, final boolean event) { ++ 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..2ff58cf753c60913ee73aae015182e9c5560d529 +--- /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, ++ null, 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, final boolean fromDisk, final boolean event) { ++ 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..58d9187adc188b693b6becc400f766e069bf1bf5 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java +@@ -0,0 +1,116 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server; ++ ++import ca.spottedleaf.moonrise.common.PlatformHooks; ++import ca.spottedleaf.moonrise.common.list.ReferenceList; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.TickThread; ++import ca.spottedleaf.moonrise.common.util.ChunkSystem; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++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 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); ++ } ++ PlatformHooks.get().entityMove( ++ entity, ++ CoordinateUtils.getChunkSectionKey(oldSectionX, oldSectionY, oldSectionZ), ++ CoordinateUtils.getChunkSectionKey(newSectionX, newSectionY, newSectionZ) ++ ); ++ } ++ ++ @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); ++ } ++ } ++ ++ @Override ++ protected void entityStartLoaded(final Entity entity) { ++ // Moonrise start - entity tracker ++ this.trackerEntities.add(entity); ++ // Moonrise end - entity tracker ++ } ++ ++ @Override ++ protected void entityEndLoaded(final Entity entity) { ++ // Moonrise start - entity tracker ++ this.trackerEntities.remove(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, final boolean fromDisk, final boolean event) { ++ return ChunkSystem.screenEntity(this.serverWorld, entity, fromDisk, event); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/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..bbf9d6c1c9525d97160806819a57be03eca290f1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java +@@ -0,0 +1,204 @@ ++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.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 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; ++ } ++ ++ // I do not believe asynchronously converting to CompoundTag is worth the scheduling. ++ final DataResult<Tag> serializedResult = PoiSection.Packed.CODEC.encodeStart(registryOps, section.pack()); ++ final int finalSectionY = sectionY; ++ final Tag serialized = serializedResult.resultOrPartial((final String description) -> { ++ LOGGER.error("Failed to serialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); ++ }).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 CompoundTag section = sections.getCompound(key); ++ final DataResult<PoiSection.Packed> deserializeResult = PoiSection.Packed.CODEC.parse(registryOps, section); ++ final int finalSectionY = sectionY; ++ final PoiSection.Packed packed = deserializeResult.resultOrPartial((final String description) -> { ++ LOGGER.error("Failed to deserialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); ++ }).orElse(null); ++ ++ final long coordinateKey = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ); ++ final PoiSection deserialized = packed == null ? null : packed.unpack(() -> { ++ poiManager.setDirty(coordinateKey); ++ }); ++ ++ if (deserialized == null || ((ChunkSystemPoiSection)deserialized).moonrise$isEmpty()) { ++ // completely empty, no point in storing this ++ continue; ++ } ++ ++ 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..524752744e37a2db0e3ea089468bdf497129bfef +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java +@@ -0,0 +1,13 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level.storage; ++ ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.world.level.chunk.storage.RegionFileStorage; ++import java.io.IOException; ++ ++public interface ChunkSystemSectionStorage { ++ ++ 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..5a6defc4c4d30c06d4bba856847feb176950ca1e +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +@@ -0,0 +1,1090 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.player; ++ ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.PlatformHooks; ++import ca.spottedleaf.moonrise.common.misc.AllocatingRateLimiter; ++import ca.spottedleaf.moonrise.common.misc.SingleUserAreaMap; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++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.setSendViewDistance(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); ++ ++ final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ); ++ ++ PlatformHooks.get().onChunkWatch(this.world, chunk, this.player); ++ PlayerChunkSender.sendChunk(this.player.connection, this.world, chunk); ++ return; ++ } ++ throw new IllegalStateException(); ++ } ++ ++ 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) { ++ PlatformHooks.get().onChunkUnWatch(this.world, new ChunkPos(chunkX, chunkZ), this.player); ++ // Note: Check PlayerChunkSender#dropChunk for other logic ++ // Note: drop isAlive() check so that chunks properly unload client-side when the player dies ++ ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager ++ .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$removeReceivedChunk(this.player); ++ this.player.connection.send(new ClientboundForgetLevelChunkPacket(new ChunkPos(chunkX, chunkZ))); ++ // Paper start - PlayerChunkUnloadEvent ++ if (io.papermc.paper.event.packet.PlayerChunkUnloadEvent.getHandlerList().getRegisteredListeners().length > 0) { ++ new io.papermc.paper.event.packet.PlayerChunkUnloadEvent(player.getBukkitEntity().getWorld().getChunkAt(new ChunkPos(chunkX, chunkZ).longKey), 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 ? (!PlatformHooks.get().configAutoConfigSendDistance() || 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 = PlatformHooks.get().configPlayerMaxLoadRate(); ++ ++ return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); ++ } ++ ++ private double getMaxChunkGenRate() { ++ final double configRate = PlatformHooks.get().configPlayerMaxGenRate(); ++ ++ return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); ++ } ++ ++ private double getMaxChunkSendRate() { ++ final double configRate = PlatformHooks.get().configPlayerMaxSendRate(); ++ ++ return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); ++ } ++ ++ private long getMaxChunkLoads() { ++ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L); ++ long configLimit = (long)PlatformHooks.get().configPlayerMaxConcurrentLoads(); ++ if (configLimit == 0L) { ++ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active ++ configLimit = Math.max(5L, radiusChunks / 5L); ++ } 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 = (long)PlatformHooks.get().configPlayerMaxConcurrentGens(); ++ if (configLimit == 0L) { ++ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active ++ configLimit = Math.max(5L, radiusChunks / 5L); ++ } 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, 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(this.world); ++ // 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); ++ ++ // 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 ++ } ++ ++ public LongOpenHashSet getSentChunksRaw() { ++ return this.sentChunks; ++ } ++ } ++} +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..91a6f57f35fc1553159cca138a0619e703b2b014 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +@@ -0,0 +1,1455 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; ++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.PlatformHooks; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.TickThread; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.common.util.ChunkSystem; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; ++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 net.minecraft.world.level.chunk.LevelChunk; ++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 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); ++ } ++ ++ MoonriseRegionFileIO.flush(this.world); ++ ++ if (halt) { ++ LOGGER.info("Waiting 60s for chunk I/O to halt for world '" + WorldUtil.getWorldName(this.world) + "'"); ++ if (!this.taskScheduler.haltIO(true, TimeUnit.SECONDS.toNanos(60L))) { ++ LOGGER.warn("Failed to halt I/O tasks for world '" + WorldUtil.getWorldName(this.world) + "'"); ++ } else { ++ LOGGER.info("Halted I/O scheduler for world '" + WorldUtil.getWorldName(this.world) + "'"); ++ } ++ } ++ ++ // kill regionfile cache ++ for (final MoonriseRegionFileIO.RegionFileType type : MoonriseRegionFileIO.RegionFileType.values()) { ++ try { ++ MoonriseRegionFileIO.getControllerFor(this.world, type).getCache().close(); ++ } catch (final IOException ex) { ++ LOGGER.error("Failed to close '" + type.name() + "' regionfile cache for world '" + WorldUtil.getWorldName(this.world) + "'", ex); ++ } ++ } ++ } ++ ++ 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, PlatformHooks.get().configAutoSaveInterval(this.world)); ++ final int maxToSave = PlatformHooks.get().configMaxAutoSavePerTick(this.world); ++ 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; ++ final int flushInterval = 200; ++ int lastFlush = 0; ++ ++ int savedChunk = 0; ++ int savedEntity = 0; ++ int savedPoi = 0; ++ ++ if (shutdown) { ++ // Normal unload process does not occur during shutdown: fire event manually ++ // for mods that expect ChunkEvent.Unload to fire on shutdown (before LevelEvent.Unload) ++ for (int i = 0, len = holders.size(); i < len; ++i) { ++ final NewChunkHolder holder = holders.get(i); ++ if (holder.getCurrentChunk() instanceof LevelChunk levelChunk) { ++ PlatformHooks.get().chunkUnloadFromWorld(levelChunk); ++ } ++ } ++ } ++ for (int i = 0, len = holders.size(); i < len; ++i) { ++ final NewChunkHolder holder = holders.get(i); ++ try { ++ final NewChunkHolder.SaveStat saveStat = holder.save(shutdown); ++ if (saveStat != null) { ++ if (saveStat.savedChunk()) { ++ ++savedChunk; ++ ++saved; ++ } ++ if (saveStat.savedEntityChunk()) { ++ ++savedEntity; ++ ++saved; ++ } ++ if (saveStat.savedPoiChunk()) { ++ ++savedPoi; ++ ++saved; ++ } ++ } ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save chunk (" + holder.chunkX + "," + holder.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr); ++ } ++ if (flush && (saved - lastFlush) > (flushInterval / 2)) { ++ lastFlush = saved; ++ MoonriseRegionFileIO.partialFlush(this.world, flushInterval / 2); ++ } ++ if (logProgress) { ++ final long currTime = System.nanoTime(); ++ if ((currTime - lastLog) > TimeUnit.SECONDS.toNanos(10L)) { ++ lastLog = currTime; ++ LOGGER.info( ++ "Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi ++ + " poi chunks in world '" + WorldUtil.getWorldName(this.world) + "', progress: " ++ + format.format((double)(i+1)/(double)len * 100.0) ++ ); ++ } ++ } ++ } ++ if (flush) { ++ MoonriseRegionFileIO.flush(this.world); ++ try { ++ MoonriseRegionFileIO.flushRegionStorages(this.world); ++ } catch (final IOException ex) { ++ LOGGER.error("Exception when flushing regions in world '" + WorldUtil.getWorldName(this.world) + "'", ex); ++ } ++ } ++ if (logProgress) { ++ LOGGER.info( ++ "Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi ++ + " poi chunks in world '" + WorldUtil.getWorldName(this.world) + "' in " ++ + format.format(1.0E-9 * (System.nanoTime() - start)) + "s" ++ ); ++ } ++ } ++ ++ 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 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 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 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(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(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(); ++ }, 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.onUnload(); ++ this.autoSaveQueue.remove(holder); ++ ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder); ++ this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ)); ++ } ++ ++ // note: never call while inside the chunk system, this will absolutely break everything ++ 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"); ++ } ++ if (!PlatformHooks.get().allowAsyncTicketUpdates() && !TickThread.isTickThread()) { ++ TickThread.ensureTickThread("Cannot asynchronously process ticket updates"); ++ } ++ ++ 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<>(); ++ ++ this.blockTicketUpdates(); ++ try { ++ ret |= this.ticketLevelPropagator.performUpdates( ++ this.ticketLockArea, this.taskScheduler.schedulingLockArea, ++ scheduledTasks, changedFullStatus ++ ); ++ } finally { ++ this.unblockTicketUpdates(Boolean.FALSE); ++ } ++ } ++ ++ 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..b0bfe655922877676948d9b6ff3fa5a6ebb7a640 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java +@@ -0,0 +1,1038 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.queue.PrioritisedTaskQueue; ++import ca.spottedleaf.concurrentutil.executor.thread.PrioritisedThreadPool; ++import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.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.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.google.gson.JsonArray; ++import com.google.gson.JsonObject; ++import com.mojang.logging.LogUtils; ++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(); ++ ++ public static void init(final boolean useParallelGen) { ++ for (final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor executor : MoonriseCommon.RADIUS_AWARE_GROUP.getAllExecutors()) { ++ executor.setMaxParallelism(useParallelGen ? -1 : 1); ++ } ++ ++ LOGGER.info("Chunk system is using population gen parallelism: " + useParallelGen); ++ } ++ ++ public static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_system:chunk_load", Long::compareTo); ++ 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 RadiusAwarePrioritisedExecutor radiusAwareScheduler; ++ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor parallelGenExecutor; ++ private final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor radiusAwareGenExecutor; ++ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor loadExecutor; ++ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor ioExecutor; ++ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor compressionExecutor; ++ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor saveExecutor; ++ ++ private final PrioritisedTaskQueue mainThreadExecutor = new PrioritisedTaskQueue(); ++ ++ 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) { ++ this.world = world; ++ // 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()); ++ ++ this.parallelGenExecutor = MoonriseCommon.PARALLEL_GEN_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0); ++ this.radiusAwareGenExecutor = MoonriseCommon.RADIUS_AWARE_GROUP.createExecutor(1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0); ++ this.loadExecutor = MoonriseCommon.LOAD_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0); ++ this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, 16); ++ this.ioExecutor = MoonriseCommon.SERVER_REGION_IO_GROUP.createExecutor(-1, MoonriseCommon.IO_QUEUE_HOLD_TIME, 0); ++ // we need a separate executor here so that on shutdown we can continue to process I/O tasks ++ this.compressionExecutor = MoonriseCommon.LOAD_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0); ++ this.saveExecutor = MoonriseCommon.LOAD_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0); ++ this.chunkHolderManager = new ChunkHolderManager(world, this); ++ } ++ ++ 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, 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 Priority priority) { ++ this.chunkHolderManager.raisePriority(x, z, priority); ++ } ++ ++ public void setPriority(final int x, final int z, final Priority priority) { ++ this.chunkHolderManager.setPriority(x, z, priority); ++ } ++ ++ public void lowerPriority(final int x, final int z, final Priority priority) { ++ this.chunkHolderManager.lowerPriority(x, z, priority); ++ } ++ ++ public void scheduleTickingState(final int chunkX, final int chunkZ, final FullChunkStatus toStatus, ++ final boolean addTicket, final 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 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 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); ++ } ++ ++ if (!TickThread.isTickThread()) { ++ return this.world.getChunkSource().getChunk(chunkX, chunkZ, status, true); ++ } ++ ++ ChunkAccess loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status); ++ if (loaded != null) { ++ return loaded; ++ } ++ ++ 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, 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 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 = onComplete == null && !addTicket ? null : (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); ++ } ++ if (loadCallback != null) { ++ 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 (loadCallback != null && !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 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(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 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 Priority requestedPriority = Priority.max( ++ minPriority, chunkHolder.getEffectivePriority(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, ++ chunkStep.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(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 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, Priority.NORMAL); ++ } ++ ++ /** ++ * @deprecated Chunk tasks must be tied to coordinates in the future ++ */ ++ @Deprecated ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final Priority priority) { ++ return this.mainThreadExecutor.queueTask(run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) { ++ return this.createChunkTask(chunkX, chunkZ, run, Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run, ++ final Priority priority) { ++ return this.mainThreadExecutor.createTask(run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) { ++ return this.scheduleChunkTask(chunkX, chunkZ, run, Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run, ++ final Priority priority) { ++ return this.mainThreadExecutor.queueTask(run, priority); ++ } ++ ++ public boolean halt(final boolean sync, final long maxWaitNS) { ++ this.radiusAwareGenExecutor.halt(); ++ this.parallelGenExecutor.halt(); ++ this.loadExecutor.halt(); ++ if (sync) { ++ final long time = System.nanoTime(); ++ 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 boolean haltIO(final boolean sync, final long maxWaitNS) { ++ this.ioExecutor.halt(); ++ this.saveExecutor.halt(); ++ this.compressionExecutor.halt(); ++ if (sync) { ++ final long time = System.nanoTime(); ++ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { ++ if ( ++ !this.ioExecutor.isActive() && ++ !this.saveExecutor.isActive() && ++ !this.compressionExecutor.isActive() ++ ) { ++ return true; ++ } ++ if ((System.nanoTime() - time) >= maxWaitNS) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ public static final ArrayDeque<ChunkInfo> WAITING_CHUNKS = new ArrayDeque<>(); // stack ++ ++ public static final class ChunkInfo { ++ ++ 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..eafa4e6d55cd0f9314ac0f2b96a7f48fbb5e1a4c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java +@@ -0,0 +1,1998 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.completable.CallbackCompletable; ++import ca.spottedleaf.concurrentutil.executor.Cancellable; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.PlatformHooks; ++import ca.spottedleaf.moonrise.common.misc.LazyRunnable; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.TickThread; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.common.util.ChunkSystem; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; ++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.server.level.progress.ChunkProgressListener; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ImposterProtoChunk; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.storage.SerializableChunkData; ++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 ChunkData holderData; ++ ++ 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(), ++ this.holderData, 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(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(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(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(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 Priority priority = null; ++ private boolean priorityLocked; ++ ++ // the priority neighbouring chunks have requested this chunk generate at ++ private Priority neighbourRequestedPriority = null; ++ ++ public Priority getEffectivePriority(final Priority dfl) { ++ final Priority neighbour = this.neighbourRequestedPriority; ++ final Priority us = this.priority; ++ ++ if (neighbour == null) { ++ return us == null ? dfl : us; ++ } ++ if (us == null) { ++ return neighbour; ++ } ++ ++ return Priority.max(us, neighbour); ++ } ++ ++ private void recalculateNeighbourRequestedPriority() { ++ if (this.neighboursWaitingForUs.isEmpty()) { ++ this.neighbourRequestedPriority = null; ++ return; ++ } ++ ++ Priority max = null; ++ ++ for (final NewChunkHolder holder : this.neighboursWaitingForUs.keySet()) { ++ final Priority neighbourPriority = holder.getEffectivePriority(null); ++ if (neighbourPriority != null && (max == null || neighbourPriority.isHigherPriority(max))) { ++ max = neighbourPriority; ++ } ++ } ++ ++ final Priority current = this.getEffectivePriority(Priority.NORMAL); ++ this.neighbourRequestedPriority = max; ++ final Priority next = this.getEffectivePriority(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 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 Priority priority) { ++ if (this.priorityLocked) { ++ return; ++ } ++ final Priority old = this.getEffectivePriority(null); ++ this.priority = priority; ++ final Priority newPriority = this.getEffectivePriority(Priority.NORMAL); ++ ++ if (old != newPriority) { ++ if (this.generationTask != null) { ++ this.generationTask.setPriority(newPriority); ++ } ++ } ++ ++ this.recalculateNeighbourPriorities(); ++ } ++ ++ // must hold scheduling lock ++ public void lowerPriority(final 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 ++ private 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); ++ this.holderData = ((ChunkSystemLevel)this.world).moonrise$requestChunkData(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ ++ 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 onUnload() { ++ this.unloaded = true; ++ ((ChunkSystemLevel)this.world).moonrise$releaseChunkData(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ)); ++ } ++ ++ 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(CallbackCompletable<CompoundTag> completable, PrioritisedExecutor.PrioritisedTask task, ++ LazyRunnable toRun) {} ++ ++ public UnloadTask getUnloadTask(final MoonriseRegionFileIO.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 MoonriseRegionFileIO.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; ++ for (int i = 0; i < this.chunkCompletions.length; ++i) { ++ CHUNK_COMPLETION_ARRAY_HANDLE.setRelease(this.chunkCompletions, i, (ChunkCompletion)null); ++ } ++ this.lastChunkCompletion = null; ++ // entity chunk state ++ this.entityChunk = null; ++ this.pendingEntityChunk = null; ++ ++ // poi chunk state ++ this.poiChunk = null; ++ ++ // priority state ++ this.priorityLocked = false; ++ ++ if (chunk != null) { ++ final LazyRunnable toRun = new LazyRunnable(); ++ this.chunkDataUnload = new UnloadTask(new CallbackCompletable<>(), this.scheduler.saveExecutor.createTask(toRun), toRun); ++ } ++ if (poiChunk != null) { ++ this.poiDataUnload = new UnloadTask(new CallbackCompletable<>(), null, null); ++ } ++ if (entityChunk != null) { ++ this.entityDataUnload = new UnloadTask(new CallbackCompletable<>(), null, null); ++ } ++ ++ return this.unloadState = (chunk != null || entityChunk != null || poiChunk != null) ? new UnloadState(this, chunk, entityChunk, poiChunk) : null; ++ } ++ ++ // data is null if failed or does not need to be saved ++ void completeAsyncUnloadDataSave(final MoonriseRegionFileIO.RegionFileType type, final CompoundTag data) { ++ if (data != null) { ++ MoonriseRegionFileIO.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 = PlatformHooks.get().forceNoSave(chunk); ++ ++ // unload chunk data ++ if (chunk != null) { ++ if (chunk instanceof LevelChunk levelChunk) { ++ levelChunk.setLoaded(false); ++ PlatformHooks.get().chunkUnloadFromWorld(levelChunk); ++ } ++ ++ if (!shouldLevelChunkNotSave) { ++ this.saveChunk(chunk, true); ++ } else { ++ this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.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(); ++ } ++ ++ // Don't really have a choice but to place this hook here ++ PlatformHooks.get().onChunkHolderTicketChange(this.world, this.vanillaChunkHolder, oldLevel, newLevel); ++ } ++ ++ 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); ++ ++ // 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) { ++ // Update progress listener for LevelLoadingScreen ++ if (chunk != null) { ++ final ChunkProgressListener progressListener = this.world.getChunkSource().chunkMap.progressListener; ++ if (progressListener != null) { ++ final ChunkStatus finalStatus = status; ++ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { ++ progressListener.onStatusChange(this.vanillaChunkHolder.getPos(), finalStatus); ++ }); ++ } ++ } ++ ++ // need to tell future statuses to complete if cancelled ++ do { ++ this.completeStatusConsumers0(status, chunk); ++ } 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); ++ } ++ } ++ }, 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); ++ } ++ } ++ }, 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) {} ++ ++ private static final MoonriseRegionFileIO.RegionFileType[] REGION_FILE_TYPES = MoonriseRegionFileIO.RegionFileType.values(); ++ ++ public SaveStat save(final boolean shutdown) { ++ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main"); ++ ++ ChunkAccess chunk = this.getCurrentChunk(); ++ PoiChunk poi = this.getPoiChunk(); ++ ChunkEntitySlices entities = this.getEntityChunk(); ++ boolean executedUnloadTask = false; ++ final boolean[] executedUnloadTasks = new boolean[REGION_FILE_TYPES.length]; ++ ++ if (shutdown) { ++ // make sure that the async unloads complete ++ if (this.unloadState != null) { ++ // must have errored during unload ++ chunk = this.unloadState.chunk(); ++ poi = this.unloadState.poiChunk(); ++ entities = this.unloadState.entityChunk(); ++ } ++ for (final MoonriseRegionFileIO.RegionFileType regionFileType : REGION_FILE_TYPES) { ++ final UnloadTask unloadTask = this.getUnloadTask(regionFileType); ++ if (unloadTask == null) { ++ continue; ++ } ++ ++ final PrioritisedExecutor.PrioritisedTask task = unloadTask.task(); ++ if (task != null && task.isQueued()) { ++ final boolean executed = task.execute(); ++ executedUnloadTask |= executed; ++ executedUnloadTasks[regionFileType.ordinal()] = executed; ++ } ++ } ++ } ++ ++ final boolean forceNoSaveChunk = PlatformHooks.get().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( ++ canSaveChunk | executedUnloadTasks[MoonriseRegionFileIO.RegionFileType.CHUNK_DATA.ordinal()], ++ canSaveEntities | executedUnloadTasks[MoonriseRegionFileIO.RegionFileType.ENTITY_DATA.ordinal()], ++ canSavePOI | executedUnloadTasks[MoonriseRegionFileIO.RegionFileType.POI_DATA.ordinal()] ++ ) ++ : null; ++ } ++ ++ private boolean saveChunk(final ChunkAccess chunk, final boolean unloading) { ++ if (!chunk.isUnsaved()) { ++ if (unloading) { ++ this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, null); ++ } ++ return false; ++ } ++ try { ++ final SerializableChunkData chunkData = SerializableChunkData.copyOf(this.world, chunk); ++ PlatformHooks.get().chunkSyncSave(this.world, chunk, chunkData); ++ ++ chunk.tryMarkSaved(); ++ ++ final CallbackCompletable<CompoundTag> completable = new CallbackCompletable<>(); ++ ++ final Runnable run = () -> { ++ final CompoundTag data = chunkData.write(); ++ ++ completable.complete(data); ++ ++ if (unloading) { ++ NewChunkHolder.this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, data); ++ } ++ }; ++ ++ final PrioritisedExecutor.PrioritisedTask task; ++ if (unloading) { ++ this.chunkDataUnload.toRun().setRunnable(run); ++ task = this.chunkDataUnload.task(); ++ } else { ++ task = this.scheduler.saveExecutor.createTask(run); ++ } ++ ++ task.queue(); ++ ++ MoonriseRegionFileIO.scheduleSave( ++ this.world, this.chunkX, this.chunkZ, completable, task, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, Priority.NORMAL ++ ); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr); ++ } ++ ++ 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 = MoonriseRegionFileIO.loadData(this.world, this.chunkX, this.chunkZ, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, Priority.BLOCKING); ++ } catch (final Exception ex) { ++ LOGGER.error("Cannot merge transient entities for chunk (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', data on disk will be replaced", ex); ++ } ++ } ++ ++ 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; ++ } ++ ++ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, save, MoonriseRegionFileIO.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; ++ } ++ ++ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, save, MoonriseRegionFileIO.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 CallbackCompletable<?> 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 PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task(); ++ if (unloadTask == null) { ++ ret.addProperty("unload_task_priority", "null"); ++ ret.addProperty("unload_task_suborder", Long.valueOf(0L)); ++ } else { ++ ret.addProperty("unload_task_priority", Objects.toString(unloadTask.getPriority())); ++ ret.addProperty("unload_task_suborder", Long.valueOf(unloadTask.getSubOrder())); ++ } ++ ++ 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..6b468c621b74449a6218391f6477cf63cfc98c7c +--- /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.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++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 Priority priority) { ++ if (!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(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(Priority.getPriority(priority)); ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public final Priority getPriority() { ++ final int ret = this.getPriorityVolatile(); ++ if ((ret & PRIORITY_EXECUTED) != 0) { ++ return Priority.COMPLETING; ++ } ++ if ((ret & PRIORITY_SCHEDULED) != 0) { ++ return this.getScheduledPriority(); ++ } ++ return Priority.getPriority(ret); ++ } ++ ++ public final void lowerPriority(final Priority priority) { ++ if (!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 Priority priority) { ++ if (!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 Priority priority) { ++ if (!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 Priority getScheduledPriority(); ++ ++ protected abstract void scheduleTask(final Priority priority); ++ ++ protected abstract void lowerPriorityScheduled(final Priority priority); ++ ++ protected abstract void setPriorityScheduled(final Priority priority); ++ ++ protected abstract void raisePriorityScheduled(final 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..5f4b99d8c5453f8ad2e600a57ea4e7dafa2d45f8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java +@@ -0,0 +1,729 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor; ++ ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import java.util.ArrayList; ++import java.util.Comparator; ++import java.util.List; ++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 PrioritisedExecutor executor; ++ private final DependencyTree[] queues = new DependencyTree[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; ++ private static final int NO_TASKS_QUEUED = -1; ++ private int selectedQueue = NO_TASKS_QUEUED; ++ private boolean canQueueTasks = true; ++ ++ public RadiusAwarePrioritisedExecutor(final PrioritisedExecutor executor, final int maxToSchedule) { ++ this.executor = executor; ++ ++ for (int i = 0; i < this.queues.length; ++i) { ++ this.queues[i] = new DependencyTree(this, executor, maxToSchedule); ++ } ++ } ++ ++ public void setMaxToSchedule(final int maxToSchedule) { ++ final List<PrioritisedExecutor.PrioritisedTask> tasks; ++ ++ synchronized (this) { ++ for (final DependencyTree dependencyTree : this.queues) { ++ dependencyTree.maxToSchedule = maxToSchedule; ++ } ++ ++ if (this.selectedQueue == NO_TASKS_QUEUED || !this.canQueueTasks) { ++ return; ++ } ++ ++ tasks = this.queues[this.selectedQueue].tryPushTasks(); ++ } ++ ++ scheduleTasks(tasks); ++ } ++ ++ private boolean canQueueTasks() { ++ 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 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 (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 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, Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius, ++ final Runnable run, final 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 Priority priority) { ++ return new Task(this, 0, 0, -1, run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run) { ++ return this.createInfiniteRadiusTask(run, Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final 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, Priority.NORMAL); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ private static void scheduleTasks(final List<PrioritisedExecutor.PrioritisedTask> toSchedule) { ++ if (toSchedule != null) { ++ for (int i = 0, len = toSchedule.size(); i < len; ++i) { ++ toSchedule.get(i).queue(); ++ } ++ } ++ } ++ ++ // all accesses must be synchronised by the radius aware object ++ private static final class DependencyTree { ++ ++ private final RadiusAwarePrioritisedExecutor scheduler; ++ private final PrioritisedExecutor executor; ++ private int maxToSchedule; ++ ++ 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) { ++ this.scheduler = scheduler; ++ this.executor = executor; ++ this.maxToSchedule = maxToSchedule; ++ } ++ ++ 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 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 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 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 PrioritisedExecutor getExecutor() { ++ return this.scheduler.executor; ++ } ++ ++ @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 == Priority.COMPLETING) { ++ return false; ++ } ++ ++ toSchedule = this.scheduler.queue(this, this.priority); ++ } ++ ++ scheduleTasks(toSchedule); ++ return true; ++ } ++ ++ @Override ++ public boolean isQueued() { ++ synchronized (this.scheduler) { ++ return (this.queuedTask != null || this.dependencyNode != null) && this.priority != Priority.COMPLETING; ++ } ++ } ++ ++ @Override ++ public boolean cancel() { ++ final PrioritisedExecutor.PrioritisedTask task; ++ synchronized (this.scheduler) { ++ if ((task = this.queuedTask) == null) { ++ if (this.priority == Priority.COMPLETING) { ++ return false; ++ } ++ ++ this.priority = 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 == Priority.COMPLETING) { ++ return false; ++ } ++ ++ this.priority = 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 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 Priority priority) { ++ if (!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 == 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 Priority priority) { ++ if (!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 == 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 Priority priority) { ++ if (!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 == 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; ++ } ++ ++ @Override ++ public long getSubOrder() { ++ // TODO implement ++ return 0; ++ } ++ ++ @Override ++ public boolean setSubOrder(final long subOrder) { ++ // TODO implement ++ return false; ++ } ++ ++ @Override ++ public boolean raiseSubOrder(final long subOrder) { ++ // TODO implement ++ return false; ++ } ++ ++ @Override ++ public boolean lowerSubOrder(final long subOrder) { ++ // TODO implement ++ return false; ++ } ++ ++ @Override ++ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) { ++ // TODO implement ++ return this.setPriority(priority); ++ } ++ } ++} +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..6ab353b0d2465c3680bb3c8d0852ba0f65c00fd2 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java +@@ -0,0 +1,151 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; ++ ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.PlatformHooks; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager; ++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 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() { ++ final PlatformHooks platformHooks = PlatformHooks.get(); ++ ++ // 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) -> { ++ PlatformHooks.get().postLoadProtoChunk(world, protoChunk); ++ }); ++ 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); ++ try { ++ platformHooks.setCurrentlyLoading(this.chunkHolder.vanillaChunkHolder, chunk); ++ chunk.runPostLoad(); ++ // Unlike Vanilla, we load the entity chunk here, as we load the NBT in empty status (unlike Vanilla) ++ // This brings entity addition back in line with older versions of the game ++ // Since we load the NBT in the empty status, this will never block for I/O ++ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getOrCreateEntityChunk(this.chunkX, this.chunkZ, false); ++ chunk.setLoaded(true); ++ chunk.registerAllBlockEntitiesAfterLevelLoad(); ++ chunk.registerTickContainerInLevel(this.world); ++ chunk.setUnsavedListener(this.world.getChunkSource().chunkMap.worldGenContext.unsavedListener()); ++ platformHooks.chunkFullStatusComplete(chunk, (ProtoChunk)this.fromChunk); ++ } finally { ++ platformHooks.setCurrentlyLoading(this.chunkHolder.vanillaChunkHolder, null); ++ } ++ } catch (final Throwable throwable) { ++ this.complete(null, throwable); ++ return; ++ } ++ 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 Priority getPriority() { ++ return this.convertToFullTask.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.convertToFullTask.lowerPriority(priority); ++ } ++ ++ @Override ++ public void setPriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.convertToFullTask.setPriority(priority); ++ } ++ ++ @Override ++ public void raisePriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.convertToFullTask.raisePriority(priority); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4538ccfaea83d217ed85eaf16e82393c7f286489 +--- /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.util.Priority; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.PriorityHolder; ++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 Priority priority) { ++ super(scheduler, world, chunkX, chunkZ); ++ if (!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 Priority getPriority() { ++ return this.priorityHolder.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final Priority priority) { ++ this.priorityHolder.raisePriority(priority); ++ } ++ ++ @Override ++ public void setPriority(final Priority priority) { ++ this.priorityHolder.setPriority(priority); ++ } ++ ++ @Override ++ public void raisePriority(final Priority priority) { ++ this.priorityHolder.raisePriority(priority); ++ } ++ ++ private static final class LightTaskPriorityHolder extends PriorityHolder { ++ ++ private final ChunkLightTask task; ++ ++ private LightTaskPriorityHolder(final 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 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 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 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 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 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..1440c9e2b106616884edcb20201113320817ed9f +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java +@@ -0,0 +1,494 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.PlatformHooks; ++import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemConverters; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; ++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.SerializableChunkData; ++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 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 Priority getPriority() { ++ return this.loadTask.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final 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 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 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 MoonriseRegionFileIO.RegionFileType type, ++ final 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 record ReadChunk(ProtoChunk protoChunk, SerializableChunkData chunkData) {} ++ ++ private static final class ChunkDataLoadTask extends CallbackDataLoadTask<ReadChunk, ChunkAccess> { ++ private ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.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 Priority priority) { ++ return this.scheduler.loadExecutor.createTask(run, priority); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) { ++ return this.scheduler.createChunkTask(this.chunkX, this.chunkZ, run, priority); ++ } ++ ++ @Override ++ protected TaskResult<ChunkAccess, Throwable> completeOnMainOffMain(final ReadChunk data, final Throwable throwable) { ++ if (throwable != null) { ++ return new TaskResult<>(null, throwable); ++ } ++ ++ if (data == null || data.protoChunk() == null) { ++ return new TaskResult<>(this.getEmptyChunk(), null); ++ } ++ ++ if (!PlatformHooks.get().hasMainChunkLoadHook()) { ++ return new TaskResult<>(data.protoChunk(), null); ++ } ++ ++ // need to invoke the callback for loading on the main thread ++ return null; ++ } ++ ++ private ProtoChunk getEmptyChunk() { ++ return new ProtoChunk( ++ new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, this.world, ++ this.world.registryAccess().lookupOrThrow(Registries.BIOME), (BlendingData)null ++ ); ++ } ++ ++ @Override ++ protected TaskResult<ReadChunk, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) { ++ if (throwable != null) { ++ LOGGER.error("Failed to load chunk data for task: " + this.toString() + ", chunk data will be lost", throwable); ++ return new TaskResult<>(null, null); ++ } ++ ++ if (data == null) { ++ return new TaskResult<>(null, null); ++ } ++ ++ try { ++ // run converters ++ final CompoundTag converted = this.world.getChunkSource().chunkMap.upgradeChunkTag(data, new ChunkPos(this.chunkX, this.chunkZ)); // Paper ++ ++ // unpack the data ++ final SerializableChunkData chunkData = SerializableChunkData.parse( ++ this.world, this.world.registryAccess(), converted ++ ); ++ ++ if (chunkData == null) { ++ LOGGER.error("Deserialized chunk for task: " + this.toString() + " produced null, chunk data will be lost?"); ++ } ++ ++ // read into ProtoChunk ++ final ProtoChunk chunk = chunkData == null ? null : chunkData.read( ++ this.world, this.world.getPoiManager(), this.world.getChunkSource().chunkMap.storageInfo(), ++ new ChunkPos(this.chunkX, this.chunkZ) ++ ); ++ ++ return new TaskResult<>(new ReadChunk(chunk, chunkData), null); ++ } catch (final Throwable thr2) { ++ LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2); ++ return new TaskResult<>(null, null); ++ } ++ } ++ ++ @Override ++ protected TaskResult<ChunkAccess, Throwable> runOnMain(final ReadChunk data, final Throwable throwable) { ++ PlatformHooks.get().mainChunkLoad(data.protoChunk(), data.chunkData()); ++ ++ return new TaskResult<>(data.protoChunk(), null); ++ } ++ } ++ ++ public static final class PoiDataLoadTask extends CallbackDataLoadTask<PoiChunk, PoiChunk> { ++ ++ public PoiDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.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 Priority priority) { ++ return this.scheduler.loadExecutor.createTask(run, priority); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final 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 Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.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 Priority priority) { ++ return this.scheduler.loadExecutor.createTask(run, priority); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final 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..002ee365aa70d8e6a6e6bd5c95988bd17db4395a +--- /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.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import net.minecraft.server.level.ServerLevel; ++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 Priority getPriority(); ++ ++ /* Schedule lock is always held for the priority update calls */ ++ ++ public abstract void lowerPriority(final Priority priority); ++ ++ public abstract void setPriority(final Priority priority); ++ ++ public abstract void raisePriority(final 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..25d8da4773dcee5096053e7e3788bfc224d705a7 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java +@@ -0,0 +1,218 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; ++ ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++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 Priority priority) { ++ super(scheduler, world, chunkX, chunkZ); ++ if (!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 Priority getPriority() { ++ return this.generateTask.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.generateTask.lowerPriority(priority); ++ } ++ ++ @Override ++ public void setPriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.generateTask.setPriority(priority); ++ } ++ ++ @Override ++ public void raisePriority(final Priority priority) { ++ if (!Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.generateTask.raisePriority(priority); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bdcd1879457bafcca4e76523aac0555968f37c0b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java +@@ -0,0 +1,674 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; ++ ++import ca.spottedleaf.concurrentutil.completable.CallbackCompletable; ++import ca.spottedleaf.concurrentutil.completable.Completable; ++import ca.spottedleaf.concurrentutil.executor.Cancellable; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; ++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 MoonriseRegionFileIO.RegionFileType type; ++ ++ public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final MoonriseRegionFileIO.RegionFileType type, ++ final 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 Priority priority); ++ ++ protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority); ++ ++ protected abstract TaskResult<OnMain, Throwable> runOffMain(final CompoundTag data, final Throwable throwable); ++ ++ 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 Priority getPriority() { ++ if (this.processOnMain != null) { ++ return this.processOnMain.getPriority(); ++ } else { ++ return this.processOffMain.getPriority(); ++ } ++ } ++ ++ public void lowerPriority(final Priority priority) { ++ // can't lower I/O tasks, we don't know what they affect ++ if (this.processOffMain != null) { ++ this.processOffMain.lowerPriority(priority); ++ } ++ if (this.processOnMain != null) { ++ this.processOnMain.lowerPriority(priority); ++ } ++ } ++ ++ public void setPriority(final Priority priority) { ++ // can't lower I/O tasks, we don't know what they affect ++ this.loadDataFromDiskTask.raisePriority(priority); ++ if (this.processOffMain != null) { ++ this.processOffMain.setPriority(priority); ++ } ++ if (this.processOnMain != null) { ++ this.processOnMain.setPriority(priority); ++ } ++ } ++ ++ public void raisePriority(final Priority priority) { ++ // can't lower I/O tasks, we don't know what they affect ++ this.loadDataFromDiskTask.raisePriority(priority); ++ if (this.processOffMain != null) { ++ 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 MoonriseRegionFileIO.RegionFileType type; ++ private Cancellable dataLoadTask; ++ private Cancellable dataUnloadCancellable; ++ private PrioritisedExecutor.PrioritisedTask 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 MoonriseRegionFileIO.RegionFileType type, ++ final BiConsumer<CompoundTag, Throwable> onComplete, ++ final Priority priority) { ++ if (!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 Priority priority) { ++ if (!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) { ++ MoonriseRegionFileIO.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 Priority priority) { ++ if (!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) { ++ MoonriseRegionFileIO.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 Priority priority) { ++ if (!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) { ++ MoonriseRegionFileIO.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 Priority initialPriority = 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, 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 CallbackCompletable<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 Priority initialPriority) { ++ int priority = this.getPriorityVolatile(); ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled ++ return; ++ } ++ ++ if (!scheduledUnload) { ++ this.dataLoadTask = MoonriseRegionFileIO.loadDataAsync( ++ this.world, this.chunkX, this.chunkZ, this.type, consumer, ++ initialPriority.isHigherPriority(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(Priority.getPriority(priority & ~PRIORITY_FLAGS)); ++ } ++ } else { ++ MoonriseRegionFileIO.setPriority(this.world, this.chunkX, this.chunkZ, this.type, 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/ChunkSystemChunkBuffer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..51c126735ace8fdde89ad97b5cab62f244212db0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java +@@ -0,0 +1,12 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.storage; ++ ++import net.minecraft.world.level.chunk.storage.RegionFile; ++import java.io.IOException; ++ ++public interface ChunkSystemChunkBuffer { ++ public boolean moonrise$getWriteOnClose(); ++ ++ public void moonrise$setWriteOnClose(final boolean value); ++ ++ public void moonrise$write(final RegionFile regionFile) throws IOException; ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/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/storage/ChunkSystemRegionFile.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3bd1b59250dbab15097a64d515999b278636795a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java +@@ -0,0 +1,12 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.storage; ++ ++import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.world.level.ChunkPos; ++import java.io.IOException; ++ ++public interface ChunkSystemRegionFile { ++ ++ public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(final CompoundTag data, final ChunkPos pos) throws IOException; ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/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..93fd23027c00cef76562098306737272fda1350a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java +@@ -0,0 +1,321 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.util; ++ ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.MoonriseConstants; ++import it.unimi.dsi.fastutil.HashCommon; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import it.unimi.dsi.fastutil.longs.LongIterator; ++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[MoonriseConstants.MAX_VIEW_DISTANCE+2+1][]; ++ static { ++ for (int i = 0; i < SEARCH_RADIUS_ITERATION_LIST.length; ++i) { ++ // a BFS around -x, -z, +x, +z will give increasing manhatten distance ++ 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/util/stream/ExternalChunkStreamMarker.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7ef3dcca89ed7578c6c0f5565131889110063056 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java +@@ -0,0 +1,37 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.util.stream; ++ ++import java.io.DataInputStream; ++import java.io.FilterInputStream; ++import java.io.InputStream; ++import java.lang.reflect.Field; ++ ++/** ++ * Used to mark chunk data streams that are on external files ++ */ ++public class ExternalChunkStreamMarker extends DataInputStream { ++ ++ private static final Field IN_FIELD; ++ static { ++ Field field; ++ try { ++ field = FilterInputStream.class.getDeclaredField("in"); ++ field.setAccessible(true); ++ } catch (final Throwable throwable) { ++ field = null; ++ } ++ ++ IN_FIELD = field; ++ } ++ ++ private static InputStream getWrapped(final FilterInputStream in) { ++ try { ++ return (InputStream)IN_FIELD.get(in); ++ } catch (final Throwable throwable) { ++ return in; ++ } ++ } ++ ++ public ExternalChunkStreamMarker(final DataInputStream in) { ++ super(getWrapped(in)); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/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/chunk_tick_iteration/ChunkTickServerLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6af03fd7807d4c71dbf85028d18dc850978ef429 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java +@@ -0,0 +1,19 @@ ++package ca.spottedleaf.moonrise.patches.chunk_tick_iteration; ++ ++import ca.spottedleaf.moonrise.common.list.ReferenceList; ++import net.minecraft.server.level.ServerChunkCache; ++import net.minecraft.world.level.chunk.LevelChunk; ++ ++public interface ChunkTickServerLevel { ++ ++ public ReferenceList<ServerChunkCache.ChunkAndHolder> moonrise$getPlayerTickingChunks(); ++ ++ public void moonrise$markChunkForPlayerTicking(final LevelChunk chunk); ++ ++ public void moonrise$removeChunkForPlayerTicking(final LevelChunk chunk); ++ ++ public void moonrise$addPlayerTickingRequest(final int chunkX, final int chunkZ); ++ ++ public void moonrise$removePlayerTickingRequest(final int chunkX, final int chunkZ); ++ ++} +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..aec503ca1551ce4b15b8975832cd664ebd670722 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +@@ -0,0 +1,2204 @@ ++package ca.spottedleaf.moonrise.patches.collisions; ++ ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter; ++import ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState; ++import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity; ++import ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData; ++import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape; ++import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; ++import ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection; ++import it.unimi.dsi.fastutil.doubles.DoubleArrayList; ++import it.unimi.dsi.fastutil.doubles.DoubleList; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Direction; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.vehicle.AbstractMinecart; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.CollisionGetter; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.border.WorldBorder; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ChunkSource; ++import net.minecraft.world.level.chunk.LevelChunkSection; ++import net.minecraft.world.level.chunk.PalettedContainer; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.material.FluidState; ++import net.minecraft.world.phys.AABB; ++import net.minecraft.world.phys.Vec3; ++import net.minecraft.world.phys.shapes.ArrayVoxelShape; ++import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape; ++import net.minecraft.world.phys.shapes.BooleanOp; ++import net.minecraft.world.phys.shapes.CollisionContext; ++import net.minecraft.world.phys.shapes.DiscreteVoxelShape; ++import net.minecraft.world.phys.shapes.EntityCollisionContext; ++import net.minecraft.world.phys.shapes.OffsetDoubleList; ++import net.minecraft.world.phys.shapes.Shapes; ++import net.minecraft.world.phys.shapes.SliceShape; ++import net.minecraft.world.phys.shapes.VoxelShape; ++import java.util.Arrays; ++import java.util.List; ++import java.util.Objects; ++import java.util.function.BiPredicate; ++import java.util.function.Predicate; ++ ++public final class CollisionUtil { ++ ++ public static final double COLLISION_EPSILON = 1.0E-7; ++ public static final DoubleArrayList ZERO_ONE = DoubleArrayList.wrap(new double[] { 0.0, 1.0 }); ++ ++ public static boolean isSpecialCollidingBlock(final net.minecraft.world.level.block.state.BlockBehaviour.BlockStateBase block) { ++ return block.hasLargeCollisionShape() || block.getBlock() == Blocks.MOVING_PISTON; ++ } ++ ++ public static boolean isEmpty(final AABB aabb) { ++ return (aabb.maxX - aabb.minX) < COLLISION_EPSILON || (aabb.maxY - aabb.minY) < COLLISION_EPSILON || (aabb.maxZ - aabb.minZ) < COLLISION_EPSILON; ++ } ++ ++ 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 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 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 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 AABB box1, final AABB box2) { ++ return (box1.minX - box2.maxX) < -COLLISION_EPSILON && (box1.maxX - box2.minX) > COLLISION_EPSILON && ++ (box1.minY - box2.maxY) < -COLLISION_EPSILON && (box1.maxY - box2.minY) > COLLISION_EPSILON && ++ (box1.minZ - box2.maxZ) < -COLLISION_EPSILON && (box1.maxZ - box2.minZ) > COLLISION_EPSILON; ++ } ++ ++ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON ++ public static double collideX(final AABB target, final AABB source, final double source_move) { ++ if ((source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON && ++ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { ++ if (source_move >= 0.0) { ++ 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 AABB target, final AABB source, final double source_move) { ++ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && ++ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { ++ if (source_move >= 0.0) { ++ 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 AABB target, final AABB source, final double source_move) { ++ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && ++ (source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON) { ++ if (source_move >= 0.0) { ++ 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 ++ public static int findFloor(final double[] values, final double value, int startIndex, int endIndex) { ++ Objects.checkFromToIndex(startIndex, endIndex + 1, values.length); ++ do { ++ final int middle = (startIndex + endIndex) >>> 1; ++ final double middleVal = values[middle]; ++ ++ if (value < middleVal) { ++ endIndex = middle - 1; ++ } else { ++ startIndex = middle + 1; ++ } ++ } while (startIndex <= endIndex); ++ ++ return startIndex - 1; ++ } ++ ++ private static VoxelShape sliceShapeVanilla(final VoxelShape src, final Direction.Axis axis, ++ final int index) { ++ return new SliceShape(src, axis, index); ++ } ++ ++ private static DoubleList offsetList(final double[] src, final double by) { ++ final DoubleArrayList wrap = DoubleArrayList.wrap(src); ++ if (by == 0.0) { ++ return wrap; ++ } ++ return new OffsetDoubleList(wrap, by); ++ } ++ ++ private static VoxelShape sliceShapeOptimised(final VoxelShape src, final Direction.Axis axis, ++ final int index) { ++ // assume index in range ++ final double off_x = ((CollisionVoxelShape)src).moonrise$offsetX(); ++ final double off_y = ((CollisionVoxelShape)src).moonrise$offsetY(); ++ final double off_z = ((CollisionVoxelShape)src).moonrise$offsetZ(); ++ ++ final double[] coords_x = ((CollisionVoxelShape)src).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((CollisionVoxelShape)src).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((CollisionVoxelShape)src).moonrise$rootCoordinatesZ(); ++ ++ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)src).moonrise$getCachedVoxelData(); ++ ++ // note: size = coords.length - 1 ++ final int size_x = cached_shape_data.sizeX(); ++ final int size_y = cached_shape_data.sizeY(); ++ final int size_z = cached_shape_data.sizeZ(); ++ ++ final long[] bitset = cached_shape_data.voxelSet(); ++ ++ final DoubleList list_x; ++ final DoubleList list_y; ++ final DoubleList list_z; ++ final int shape_sx; ++ final int shape_ex; ++ final int shape_sy; ++ final int shape_ey; ++ final int shape_sz; ++ final int shape_ez; ++ ++ switch (axis) { ++ case X: { ++ // validate index ++ if (index < 0 || index >= size_x) { ++ return Shapes.empty(); ++ } ++ ++ // test if input is already "sliced" ++ if (coords_x.length == 2 && (coords_x[0] + off_x) == 0.0 && (coords_x[1] + off_x) == 1.0) { ++ return src; ++ } ++ ++ // test if result would be full box ++ if (coords_y.length == 2 && coords_z.length == 2 && ++ (coords_y[0] + off_y) == 0.0 && (coords_y[1] + off_y) == 1.0 && ++ (coords_z[0] + off_z) == 0.0 && (coords_z[1] + off_z) == 1.0) { ++ // note: size_y == size_z == 1 ++ final int bitIdx = 0 + 0*size_z + index*(size_z*size_y); ++ return (bitset[bitIdx >>> 6] & (1L << bitIdx)) == 0L ? Shapes.empty() : Shapes.block(); ++ } ++ ++ list_x = ZERO_ONE; ++ list_y = offsetList(coords_y, off_y); ++ list_z = offsetList(coords_z, off_z); ++ shape_sx = index; ++ shape_ex = index + 1; ++ shape_sy = 0; ++ shape_ey = size_y; ++ shape_sz = 0; ++ shape_ez = size_z; ++ ++ break; ++ } ++ case Y: { ++ // validate index ++ if (index < 0 || index >= size_y) { ++ return Shapes.empty(); ++ } ++ ++ // test if input is already "sliced" ++ if (coords_y.length == 2 && (coords_y[0] + off_y) == 0.0 && (coords_y[1] + off_y) == 1.0) { ++ return src; ++ } ++ ++ // test if result would be full box ++ if (coords_x.length == 2 && coords_z.length == 2 && ++ (coords_x[0] + off_x) == 0.0 && (coords_x[1] + off_x) == 1.0 && ++ (coords_z[0] + off_z) == 0.0 && (coords_z[1] + off_z) == 1.0) { ++ // note: size_x == size_z == 1 ++ final int bitIdx = 0 + index*size_z + 0*(size_z*size_y); ++ return (bitset[bitIdx >>> 6] & (1L << bitIdx)) == 0L ? Shapes.empty() : Shapes.block(); ++ } ++ ++ list_x = offsetList(coords_x, off_x); ++ list_y = ZERO_ONE; ++ list_z = offsetList(coords_z, off_z); ++ shape_sx = 0; ++ shape_ex = size_x; ++ shape_sy = index; ++ shape_ey = index + 1; ++ shape_sz = 0; ++ shape_ez = size_z; ++ ++ break; ++ } ++ case Z: { ++ // validate index ++ if (index < 0 || index >= size_z) { ++ return Shapes.empty(); ++ } ++ ++ // test if input is already "sliced" ++ if (coords_z.length == 2 && (coords_z[0] + off_z) == 0.0 && (coords_z[1] + off_z) == 1.0) { ++ return src; ++ } ++ ++ // test if result would be full box ++ if (coords_x.length == 2 && coords_y.length == 2 && ++ (coords_x[0] + off_x) == 0.0 && (coords_x[1] + off_x) == 1.0 && ++ (coords_y[0] + off_y) == 0.0 && (coords_y[1] + off_y) == 1.0) { ++ // note: size_x == size_y == 1 ++ final int bitIdx = index + 0*size_z + 0*(size_z*size_y); ++ return (bitset[bitIdx >>> 6] & (1L << bitIdx)) == 0L ? Shapes.empty() : Shapes.block(); ++ } ++ ++ list_x = offsetList(coords_x, off_x); ++ list_y = offsetList(coords_y, off_y); ++ list_z = ZERO_ONE; ++ shape_sx = 0; ++ shape_ex = size_x; ++ shape_sy = 0; ++ shape_ey = size_y; ++ shape_sz = index; ++ shape_ez = index + 1; ++ ++ break; ++ } ++ default: { ++ throw new IllegalStateException("Unknown axis: " + axis); ++ } ++ } ++ ++ final int local_len_x = shape_ex - shape_sx; ++ final int local_len_y = shape_ey - shape_sy; ++ final int local_len_z = shape_ez - shape_sz; ++ ++ final BitSetDiscreteVoxelShape shape = new BitSetDiscreteVoxelShape(local_len_x, local_len_y, local_len_z); ++ ++ final int bitset_mul_x = size_z*size_y; ++ final int idx_off = shape_sz + shape_sy*size_z + shape_sx*bitset_mul_x; ++ final int shape_mul_x = local_len_y*local_len_z; ++ for (int x = 0; x < local_len_x; ++x) { ++ boolean setX = false; ++ for (int y = 0; y < local_len_y; ++y) { ++ boolean setY = false; ++ for (int z = 0; z < local_len_z; ++z) { ++ final int unslicedIdx = idx_off + z + y*size_z + x*bitset_mul_x; ++ if ((bitset[unslicedIdx >>> 6] & (1L << unslicedIdx)) == 0L) { ++ continue; ++ } ++ ++ setY = true; ++ setX = true; ++ shape.zMin = Math.min(shape.zMin, z); ++ shape.zMax = Math.max(shape.zMax, z + 1); ++ ++ shape.storage.set( ++ z + y*local_len_z + x*shape_mul_x ++ ); ++ } ++ ++ if (setY) { ++ shape.yMin = Math.min(shape.yMin, y); ++ shape.yMax = Math.max(shape.yMax, y + 1); ++ } ++ } ++ if (setX) { ++ shape.xMin = Math.min(shape.xMin, x); ++ shape.xMax = Math.max(shape.xMax, x + 1); ++ } ++ } ++ ++ return shape.isEmpty() ? Shapes.empty() : new ArrayVoxelShape( ++ shape, list_x, list_y, list_z ++ ); ++ } ++ ++ private static final boolean DEBUG_SLICE_SHAPE = false; ++ ++ public static VoxelShape sliceShape(final VoxelShape src, final Direction.Axis axis, ++ final int index) { ++ final VoxelShape ret = sliceShapeOptimised(src, axis, index); ++ if (DEBUG_SLICE_SHAPE) { ++ final VoxelShape vanilla = sliceShapeVanilla(src, axis, index); ++ if (!equals(ret, vanilla)) { ++ // special case: SliceShape is not empty when it should be! ++ if (areAnyFull(ret.shape) || areAnyFull(vanilla.shape)) { ++ equals(ret, vanilla); ++ sliceShapeOptimised(src, axis, index); ++ throw new IllegalStateException("Slice shape mismatch"); ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public static boolean voxelShapeIntersectNoEmpty(final VoxelShape voxel, final AABB aabb) { ++ if (voxel.isEmpty()) { ++ return false; ++ } ++ ++ // 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 = ((CollisionVoxelShape)voxel).moonrise$offsetX(); ++ final double off_y = ((CollisionVoxelShape)voxel).moonrise$offsetY(); ++ final double off_z = ((CollisionVoxelShape)voxel).moonrise$offsetZ(); ++ ++ final double[] coords_x = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ(); ++ ++ final CachedShapeData cached_shape_data = ((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 VoxelShape target, final AABB source, final double source_move) { ++ final AABB single_aabb = ((CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation(); ++ if (single_aabb != null) { ++ return collideX(single_aabb, source, source_move); ++ } ++ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true ++ ++ // offsets that should be applied to coords ++ final double off_x = ((CollisionVoxelShape)target).moonrise$offsetX(); ++ final double off_y = ((CollisionVoxelShape)target).moonrise$offsetY(); ++ final double off_z = ((CollisionVoxelShape)target).moonrise$offsetZ(); ++ ++ final double[] coords_x = ((CollisionVoxelShape)target).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((CollisionVoxelShape)target).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((CollisionVoxelShape)target).moonrise$rootCoordinatesZ(); ++ ++ final CachedShapeData cached_shape_data = ((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 VoxelShape target, final AABB source, final double source_move) { ++ final AABB single_aabb = ((CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation(); ++ if (single_aabb != null) { ++ return collideY(single_aabb, source, source_move); ++ } ++ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true ++ ++ // offsets that should be applied to coords ++ final double off_x = ((CollisionVoxelShape)target).moonrise$offsetX(); ++ final double off_y = ((CollisionVoxelShape)target).moonrise$offsetY(); ++ final double off_z = ((CollisionVoxelShape)target).moonrise$offsetZ(); ++ ++ final double[] coords_x = ((CollisionVoxelShape)target).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((CollisionVoxelShape)target).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((CollisionVoxelShape)target).moonrise$rootCoordinatesZ(); ++ ++ final CachedShapeData cached_shape_data = ((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 VoxelShape target, final AABB source, final double source_move) { ++ final AABB single_aabb = ((CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation(); ++ if (single_aabb != null) { ++ return collideZ(single_aabb, source, source_move); ++ } ++ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true ++ ++ // offsets that should be applied to coords ++ final double off_x = ((CollisionVoxelShape)target).moonrise$offsetX(); ++ final double off_y = ((CollisionVoxelShape)target).moonrise$offsetY(); ++ final double off_z = ((CollisionVoxelShape)target).moonrise$offsetZ(); ++ ++ final double[] coords_x = ((CollisionVoxelShape)target).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((CollisionVoxelShape)target).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((CollisionVoxelShape)target).moonrise$rootCoordinatesZ(); ++ ++ final CachedShapeData cached_shape_data = ((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 VoxelShape voxel, final Vec3 point) { ++ return strictlyContains(voxel, point.x, point.y, point.z); ++ } ++ ++ // does not use epsilon ++ public static boolean strictlyContains(final VoxelShape voxel, double x, double y, double z) { ++ final AABB single_aabb = ((CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation(); ++ if (single_aabb != null) { ++ return single_aabb.contains(x, y, z); ++ } ++ ++ if (voxel.isEmpty()) { ++ // bitset is clear, no point in searching ++ return false; ++ } ++ ++ // offset input ++ x -= ((CollisionVoxelShape)voxel).moonrise$offsetX(); ++ y -= ((CollisionVoxelShape)voxel).moonrise$offsetY(); ++ z -= ((CollisionVoxelShape)voxel).moonrise$offsetZ(); ++ ++ final double[] coords_x = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ(); ++ ++ final CachedShapeData cached_shape_data = ((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 BitSetDiscreteVoxelShape merge(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond, ++ final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY, ++ final MergedVoxelCoordinateList mergedZ, ++ final int booleanOp) { ++ final int sizeX = mergedX.voxels; ++ final int sizeY = mergedY.voxels; ++ final int sizeZ = mergedZ.voxels; ++ ++ 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 BitSetDiscreteVoxelShape ret = new 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 idx1; ++ int idx2; ++ ++ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx1 = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx1) & 1L); ++ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx2 = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx2) & 1L); ++ ++ // idx ff -> 0 ++ // idx ft -> 1 ++ // 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 CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond, ++ final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY, ++ final MergedVoxelCoordinateList mergedZ, ++ final int booleanOp) { ++ final int sizeX = mergedX.voxels; ++ final int sizeY = mergedY.voxels; ++ final int sizeZ = mergedZ.voxels; ++ ++ 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 idx1; ++ int idx2; ++ ++ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx1 = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx1) & 1L); ++ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx2 = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx2) & 1L); ++ ++ // idx ff -> 0 ++ // idx ft -> 1 ++ // idx tf -> 2 ++ // idx tt -> 3 ++ ++ final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0; ++ ++ if (res) { ++ return false; ++ } ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ public static VoxelShape joinOptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { ++ return joinUnoptimized(first, second, operator).optimize(); ++ } ++ ++ public static VoxelShape joinUnoptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { ++ final boolean ff = operator.apply(false, false); ++ if (ff) { ++ // technically, should be an infinite box but that's clearly an error ++ throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true"); ++ } ++ ++ final boolean tt = operator.apply(true, true); ++ ++ if (first == second) { ++ return tt ? first : Shapes.empty(); ++ } ++ ++ final boolean ft = operator.apply(false, true); ++ final boolean tf = operator.apply(true, false); ++ ++ if (first.isEmpty()) { ++ return ft ? second : Shapes.empty(); ++ } ++ if (second.isEmpty()) { ++ return tf ? first : Shapes.empty(); ++ } ++ ++ if (!tt) { ++ // try to check for no intersection, since tt = false ++ final AABB aabbF = ((CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation(); ++ final AABB aabbS = ((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 Shapes.empty(); ++ } ++ if (!tf | !ft) { ++ return tf ? first : second; ++ } ++ } ++ } ++ ++ final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge( ++ ((CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)first).moonrise$offsetX(), ++ ((CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)second).moonrise$offsetX(), ++ ft, tf ++ ); ++ if (mergedX == null) { ++ return Shapes.empty(); ++ } ++ final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge( ++ ((CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)first).moonrise$offsetY(), ++ ((CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)second).moonrise$offsetY(), ++ ft, tf ++ ); ++ if (mergedY == null) { ++ return Shapes.empty(); ++ } ++ final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge( ++ ((CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)first).moonrise$offsetZ(), ++ ((CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)second).moonrise$offsetZ(), ++ ft, tf ++ ); ++ if (mergedZ == null) { ++ return Shapes.empty(); ++ } ++ ++ final CachedShapeData shapeDataFirst = ((CollisionVoxelShape)first).moonrise$getCachedVoxelData(); ++ final CachedShapeData shapeDataSecond = ((CollisionVoxelShape)second).moonrise$getCachedVoxelData(); ++ ++ final BitSetDiscreteVoxelShape mergedShape = merge( ++ shapeDataFirst, shapeDataSecond, ++ mergedX, mergedY, mergedZ, ++ makeBitset(ft, tf, tt) ++ ); ++ ++ if (mergedShape == null) { ++ return Shapes.empty(); ++ } ++ ++ return new ArrayVoxelShape( ++ mergedShape, mergedX.wrapCoords(), mergedY.wrapCoords(), mergedZ.wrapCoords() ++ ); ++ } ++ ++ public static boolean isJoinNonEmpty(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { ++ final boolean ff = operator.apply(false, false); ++ if (ff) { ++ // technically, should be an infinite box but that's clearly an error ++ 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 AABB aabbF = ((CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation(); ++ final AABB aabbS = ((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 MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge( ++ ((CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)first).moonrise$offsetX(), ++ ((CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)second).moonrise$offsetX(), ++ ft, tf ++ ); ++ if (mergedX == null) { ++ return false; ++ } ++ final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge( ++ ((CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)first).moonrise$offsetY(), ++ ((CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)second).moonrise$offsetY(), ++ ft, tf ++ ); ++ if (mergedY == null) { ++ return false; ++ } ++ final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge( ++ ((CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)first).moonrise$offsetZ(), ++ ((CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)second).moonrise$offsetZ(), ++ ft, tf ++ ); ++ if (mergedZ == null) { ++ return false; ++ } ++ ++ final CachedShapeData shapeDataFirst = ((CollisionVoxelShape)first).moonrise$getCachedVoxelData(); ++ final CachedShapeData shapeDataSecond = ((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 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 DoubleList wrapCoords() { ++ if (this.coordinateOffset == 0.0) { ++ return DoubleArrayList.wrap(this.coordinates, this.voxels + 1); ++ } ++ return new OffsetDoubleList(DoubleArrayList.wrap(this.coordinates, this.voxels + 1), this.coordinateOffset); ++ } ++ ++ // assume coordinates.length > 1 ++ public static MergedVoxelCoordinateList getForSingle(final double[] coordinates, final double offset) { ++ final int voxels = coordinates.length - 1; ++ final int[] indices = voxels < SIMPLE_INDICES_CACHE.length ? SIMPLE_INDICES_CACHE[voxels] : getIndices(voxels); ++ ++ return new MergedVoxelCoordinateList(coordinates, offset, indices, indices, voxels); ++ } ++ ++ // assume coordinates.length > 1 ++ public static MergedVoxelCoordinateList merge(final double[] firstCoordinates, final double firstOffset, ++ final double[] secondCoordinates, final double secondOffset, ++ final boolean ft, final boolean tf) { ++ if (firstCoordinates == secondCoordinates && firstOffset == secondOffset) { ++ return getForSingle(firstCoordinates, firstOffset); ++ } ++ ++ 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 ? null : new MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1); ++ } ++ } ++ ++ public static boolean equals(final DiscreteVoxelShape shape1, final DiscreteVoxelShape shape2) { ++ final CachedShapeData cachedShapeData1 = ((CollisionDiscreteVoxelShape)shape1).moonrise$getOrCreateCachedShapeData(); ++ final CachedShapeData cachedShapeData2 = ((CollisionDiscreteVoxelShape)shape2).moonrise$getOrCreateCachedShapeData(); ++ ++ final boolean isEmpty1 = cachedShapeData1.isEmpty(); ++ final boolean isEmpty2 = cachedShapeData2.isEmpty(); ++ ++ if (isEmpty1 & isEmpty2) { ++ return true; ++ } else if (isEmpty1 ^ isEmpty2) { ++ return false; ++ } // else: isEmpty1 = isEmpty2 = 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 Arrays.equals(cachedShapeData1.voxelSet(), cachedShapeData2.voxelSet()); ++ } ++ ++ // useful only for testing ++ public static boolean equals(final VoxelShape shape1, final VoxelShape shape2) { ++ if (shape1.isEmpty() & shape2.isEmpty()) { ++ return true; ++ } else if (shape1.isEmpty() ^ shape2.isEmpty()) { ++ return false; ++ } ++ ++ if (!equals(shape1.shape, shape2.shape)) { ++ return false; ++ } ++ ++ return shape1.getCoords(Direction.Axis.X).equals(shape2.getCoords(Direction.Axis.X)) && ++ shape1.getCoords(Direction.Axis.Y).equals(shape2.getCoords(Direction.Axis.Y)) && ++ shape1.getCoords(Direction.Axis.Z).equals(shape2.getCoords(Direction.Axis.Z)); ++ } ++ ++ public static boolean areAnyFull(final DiscreteVoxelShape shape) { ++ if (shape.isEmpty()) { ++ return false; ++ } ++ ++ final int sizeX = shape.getXSize(); ++ final int sizeY = shape.getYSize(); ++ final int sizeZ = shape.getZSize(); ++ ++ for (int x = 0; x < sizeX; ++x) { ++ for (int y = 0; y < sizeY; ++y) { ++ for (int z = 0; z < sizeZ; ++z) { ++ if (shape.isFull(x, y, z)) { ++ return true; ++ } ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ public static String shapeMismatch(final DiscreteVoxelShape shape1, final DiscreteVoxelShape shape2) { ++ final CachedShapeData cachedShapeData1 = ((CollisionDiscreteVoxelShape)shape1).moonrise$getOrCreateCachedShapeData(); ++ final CachedShapeData cachedShapeData2 = ((CollisionDiscreteVoxelShape)shape2).moonrise$getOrCreateCachedShapeData(); ++ ++ final boolean isEmpty1 = cachedShapeData1.isEmpty(); ++ final boolean isEmpty2 = cachedShapeData2.isEmpty(); ++ ++ if (isEmpty1 & isEmpty2) { ++ return null; ++ } else if (isEmpty1 ^ isEmpty2) { ++ return null; ++ } // else: isEmpty1 = isEmpty2 = false ++ ++ if (cachedShapeData1.sizeX() != cachedShapeData2.sizeX()) { ++ return "size x: " + cachedShapeData1.sizeX() + " != " + cachedShapeData2.sizeX(); ++ } ++ if (cachedShapeData1.sizeY() != cachedShapeData2.sizeY()) { ++ return "size y: " + cachedShapeData1.sizeY() + " != " + cachedShapeData2.sizeY(); ++ } ++ if (cachedShapeData1.sizeZ() != cachedShapeData2.sizeZ()) { ++ return "size z: " + cachedShapeData1.sizeZ() + " != " + cachedShapeData2.sizeZ(); ++ } ++ ++ final StringBuilder ret = new StringBuilder(); ++ ++ final int sizeX = cachedShapeData1.sizeX();; ++ final int sizeY = cachedShapeData1.sizeY(); ++ final int sizeZ = cachedShapeData1.sizeZ(); ++ ++ boolean first = true; ++ ++ for (int x = 0; x < sizeX; ++x) { ++ for (int y = 0; y < sizeY; ++y) { ++ for (int z = 0; z < sizeZ; ++z) { ++ final boolean isFull1 = shape1.isFull(x, y, z); ++ final boolean isFull2 = shape2.isFull(x, y, z); ++ ++ if (isFull1 == isFull2) { ++ continue; ++ } ++ ++ if (first) { ++ first = false; ++ } else { ++ ret.append(", "); ++ } ++ ++ ret.append("(").append(x).append(",").append(y).append(",").append(z) ++ .append("): shape1: ").append(isFull1).append(", shape2: ").append(isFull2); ++ } ++ } ++ } ++ ++ return ret.isEmpty() ? null : ret.toString(); ++ } ++ ++ public static AABB offsetX(final AABB box, final double dx) { ++ return new AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); ++ } ++ ++ public static AABB offsetY(final AABB box, final double dy) { ++ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ); ++ } ++ ++ public static AABB offsetZ(final AABB box, final double dz) { ++ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz); ++ } ++ ++ public static AABB expandRight(final AABB box, final double dx) { // dx > 0.0 ++ return new AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); ++ } ++ ++ public static AABB expandLeft(final AABB box, final double dx) { // dx < 0.0 ++ return new AABB(box.minX - dx, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ); ++ } ++ ++ public static AABB expandUpwards(final AABB box, final double dy) { // dy > 0.0 ++ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, box.maxZ); ++ } ++ ++ public static AABB expandDownwards(final AABB box, final double dy) { // dy < 0.0 ++ return new AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, box.maxZ); ++ } ++ ++ public static AABB expandForwards(final AABB box, final double dz) { // dz > 0.0 ++ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz); ++ } ++ ++ public static AABB expandBackwards(final AABB box, final double dz) { // dz < 0.0 ++ return new AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ); ++ } ++ ++ public static AABB cutRight(final AABB box, final double dx) { // dx > 0.0 ++ return new AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); ++ } ++ ++ public static AABB cutLeft(final AABB box, final double dx) { // dx < 0.0 ++ return new AABB(box.minX + dx, box.minY, box.minZ, box.minX, box.maxY, box.maxZ); ++ } ++ ++ public static AABB cutUpwards(final AABB box, final double dy) { // dy > 0.0 ++ return new AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, box.maxZ); ++ } ++ ++ public static AABB cutDownwards(final AABB box, final double dy) { // dy < 0.0 ++ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, box.maxZ); ++ } ++ ++ public static AABB cutForwards(final AABB box, final double dz) { // dz > 0.0 ++ return new AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz); ++ } ++ ++ public static AABB cutBackwards(final AABB box, final double dz) { // dz < 0.0 ++ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ); ++ } ++ ++ public static double performAABBCollisionsX(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final AABB target = potentialCollisions.get(i); ++ value = collideX(target, currentBoundingBox, value); ++ } ++ ++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value; ++ } ++ ++ public static double performAABBCollisionsY(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final AABB target = potentialCollisions.get(i); ++ value = collideY(target, currentBoundingBox, value); ++ } ++ ++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value; ++ } ++ ++ public static double performAABBCollisionsZ(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final AABB target = potentialCollisions.get(i); ++ value = collideZ(target, currentBoundingBox, value); ++ } ++ ++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value; ++ } ++ ++ public static double performVoxelCollisionsX(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final VoxelShape target = potentialCollisions.get(i); ++ value = collideX(target, currentBoundingBox, value); ++ } ++ ++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value; ++ } ++ ++ public static double performVoxelCollisionsY(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final VoxelShape target = potentialCollisions.get(i); ++ value = collideY(target, currentBoundingBox, value); ++ } ++ ++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value; ++ } ++ ++ public static double performVoxelCollisionsZ(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final VoxelShape target = potentialCollisions.get(i); ++ value = collideZ(target, currentBoundingBox, value); ++ } ++ ++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value; ++ } ++ ++ public static Vec3 performVoxelCollisions(final Vec3 moveVector, AABB axisalignedbb, final List<VoxelShape> potentialCollisions) { ++ double x = moveVector.x; ++ double y = moveVector.y; ++ double z = moveVector.z; ++ ++ 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 Vec3(x, y, z); ++ } ++ ++ public static Vec3 performAABBCollisions(final Vec3 moveVector, AABB axisalignedbb, final List<AABB> potentialCollisions) { ++ double x = moveVector.x; ++ double y = moveVector.y; ++ double z = moveVector.z; ++ ++ 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 Vec3(x, y, z); ++ } ++ ++ public static Vec3 performCollisions(final Vec3 moveVector, AABB axisalignedbb, ++ final List<VoxelShape> voxels, ++ final List<AABB> aabbs) { ++ if (voxels.isEmpty()) { ++ // fast track only AABBs ++ return performAABBCollisions(moveVector, axisalignedbb, aabbs); ++ } ++ ++ 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 Vec3(x, y, z); ++ } ++ ++ public static boolean isCollidingWithBorder(final WorldBorder worldborder, final AABB boundingBox) { ++ return isCollidingWithBorder(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); ++ } ++ ++ public static boolean isCollidingWithBorder(final 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) > CollisionUtil.COLLISION_EPSILON || (borderMaxX - boxMaxX) < -CollisionUtil.COLLISION_EPSILON || ++ (borderMinZ - boxMinZ) > CollisionUtil.COLLISION_EPSILON || (borderMaxZ - boxMaxZ) < -CollisionUtil.COLLISION_EPSILON; ++ } ++ ++ /* Math.max/min specify that any NaN argument results in a NaN return, unlike these functions */ ++ 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 Level world, final Entity entity, final AABB aabb, ++ final List<VoxelShape> intoVoxel, final List<AABB> intoAABB, ++ final int collisionFlags, final BiPredicate<BlockState, BlockPos> predicate) { ++ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; ++ boolean ret = false; ++ ++ if ((collisionFlags & COLLISION_FLAG_CHECK_BORDER) != 0) { ++ final WorldBorder worldBorder = world.getWorldBorder(); ++ if (CollisionUtil.isCollidingWithBorder(worldBorder, aabb) && entity != null && worldBorder.isInsideCloseToBorder(entity, aabb)) { ++ if (checkOnly) { ++ return true; ++ } else { ++ final VoxelShape borderShape = worldBorder.getCollisionShape(); ++ intoVoxel.add(borderShape); ++ ret = true; ++ } ++ } ++ } ++ ++ final int minSection = WorldUtil.getMinSection(world); ++ ++ final int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1; ++ final int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1; ++ ++ final int minBlockY = Math.max((minSection << 4) - 1, Mth.floor(aabb.minY - COLLISION_EPSILON) - 1); ++ final int maxBlockY = Math.min((WorldUtil.getMaxSection(world) << 4) + 16, Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1); ++ ++ final int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1; ++ final int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1; ++ ++ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); ++ final CollisionContext collisionShape = new LazyEntityCollisionContext(entity); ++ final boolean useEntityCollisionShape = LazyEntityCollisionContext.useEntityCollisionShape(world, 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 ChunkSource chunkSource = world.getChunkSource(); ++ ++ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { ++ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { ++ final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, 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 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 LevelChunkSection section = sections[sectionIdx]; ++ if (section.hasOnlyAir()) { ++ // empty ++ continue; ++ } ++ ++ final boolean hasSpecial = ((BlockCountingChunkSection)section).moonrise$hasSpecialCollidingBlocks(); ++ final int sectionAdjust = !hasSpecial ? 1 : 0; ++ ++ final PalettedContainer<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 BlockState blockData = blocks.get(localBlockIndex); ++ ++ if (((CollisionBlockState)blockData).moonrise$emptyContextCollisionShape()) { ++ continue; ++ } ++ ++ VoxelShape blockCollision = ((CollisionBlockState)blockData).moonrise$getConstantContextCollisionShape(); ++ ++ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) { ++ if (useEntityCollisionShape) { ++ mutablePos.set(blockX, blockY, blockZ); ++ blockCollision = collisionShape.getCollisionShape(blockData, world, mutablePos); ++ } else if (blockCollision == null) { ++ mutablePos.set(blockX, blockY, blockZ); ++ blockCollision = blockData.getCollisionShape(world, mutablePos, collisionShape); ++ } ++ ++ AABB singleAABB = ((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 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 Level world, final Entity entity, AABB aabb, ++ final List<AABB> into, final int collisionFlags, final Predicate<Entity> predicate) { ++ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; ++ ++ boolean ret = false; ++ ++ // 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 List<Entity> entities; ++ if (entity != null && ((ChunkSystemEntity)entity).moonrise$isHardColliding()) { ++ entities = world.getEntities(entity, aabb, predicate); ++ } else { ++ entities = ((ChunkSystemEntityGetter)world).moonrise$getHardCollidingEntities(entity, aabb, predicate); ++ } ++ ++ 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))) { ++ if (checkOnly) { ++ return true; ++ } else { ++ into.add(otherEntity.getBoundingBox()); ++ ret = true; ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public static boolean getCollisions(final Level world, final Entity entity, final AABB aabb, ++ final List<VoxelShape> intoVoxel, final List<AABB> intoAABB, final int collisionFlags, ++ final BiPredicate<BlockState, BlockPos> blockPredicate, ++ final Predicate<Entity> entityPredicate) { ++ if ((collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0) { ++ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate) ++ || getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate); ++ } else { ++ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate) ++ | getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate); ++ } ++ } ++ ++ public static final class LazyEntityCollisionContext extends EntityCollisionContext { ++ ++ private CollisionContext delegate; ++ private boolean delegated; ++ ++ public LazyEntityCollisionContext(final Entity entity) { ++ super(false, 0.0, null, null, entity); ++ } ++ ++ public static boolean useEntityCollisionShape(final Level world, final Entity entity) { ++ return entity instanceof AbstractMinecart && AbstractMinecart.useExperimentalMovement(world); ++ } ++ ++ public boolean isDelegated() { ++ final boolean delegated = this.delegated; ++ this.delegated = false; ++ return delegated; ++ } ++ ++ public CollisionContext getDelegate() { ++ this.delegated = true; ++ final Entity entity = this.getEntity(); ++ return this.delegate == null ? this.delegate = (entity == null ? CollisionContext.empty() : CollisionContext.of(entity)) : this.delegate; ++ } ++ ++ @Override ++ public boolean isDescending() { ++ return this.getDelegate().isDescending(); ++ } ++ ++ @Override ++ public boolean isAbove(final VoxelShape shape, final BlockPos pos, final boolean defaultValue) { ++ return this.getDelegate().isAbove(shape, pos, defaultValue); ++ } ++ ++ @Override ++ public boolean isHoldingItem(final Item item) { ++ return this.getDelegate().isHoldingItem(item); ++ } ++ ++ @Override ++ public boolean canStandOnFluid(final FluidState state, final FluidState fluidState) { ++ return this.getDelegate().canStandOnFluid(state, fluidState); ++ } ++ ++ @Override ++ public VoxelShape getCollisionShape(final BlockState blockState, final CollisionGetter collisionGetter, final BlockPos blockPos) { ++ return this.getDelegate().getCollisionShape(blockState, collisionGetter, blockPos); ++ } ++ } ++ ++ 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..35c8aaf0bfa42717f45eed1d1072e1614874de91 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java +@@ -0,0 +1,28 @@ ++package ca.spottedleaf.moonrise.patches.collisions; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.material.FluidState; ++import net.minecraft.world.phys.shapes.VoxelShape; ++ ++public final class ExplosionBlockCache { ++ ++ public final long key; ++ public final BlockPos immutablePos; ++ public final BlockState blockState; ++ public final FluidState fluidState; ++ public final float resistance; ++ public final boolean outOfWorld; ++ public Boolean shouldExplode; // null -> not called yet ++ public VoxelShape cachedCollisionShape; ++ ++ public ExplosionBlockCache(final long key, final BlockPos immutablePos, final BlockState blockState, ++ final FluidState fluidState, final float resistance, final boolean outOfWorld) { ++ this.key = key; ++ this.immutablePos = immutablePos; ++ this.blockState = blockState; ++ 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..a38ab583200ebf68ca68fdddf2d12077720b72b7 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java +@@ -0,0 +1,29 @@ ++package ca.spottedleaf.moonrise.patches.collisions.block; ++ ++import net.minecraft.world.phys.shapes.VoxelShape; ++ ++public interface CollisionBlockState { ++ ++ // note: this does not consider canOcclude, it is only based on the cached collision shape (i.e hasCache()) ++ // 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(); ++ ++ // whether the context-sensitive shape is constant and is empty ++ public boolean moonrise$emptyContextCollisionShape(); ++ ++ // indicates that occludesFullBlock is cached for the collision shape ++ public boolean moonrise$hasCache(); ++ ++ // 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 VoxelShape moonrise$getConstantContextCollisionShape(); ++} +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..9d33ead3a97d86b371e4d9ad9fed80d789bed844 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java +@@ -0,0 +1,39 @@ ++package ca.spottedleaf.moonrise.patches.collisions.shape; ++ ++import net.minecraft.world.phys.AABB; ++import java.util.ArrayList; ++import java.util.List; ++ ++public record CachedToAABBs( ++ List<AABB> aabbs, ++ boolean isOffset, ++ double offX, double offY, double offZ ++) { ++ ++ public CachedToAABBs removeOffset() { ++ final List<AABB> toOffset = this.aabbs; ++ final double offX = this.offX; ++ final double offY = this.offY; ++ final double offZ = this.offZ; ++ ++ final List<AABB> ret = new ArrayList<>(toOffset.size()); ++ ++ for (int i = 0, len = toOffset.size(); i < len; ++i) { ++ ret.add(toOffset.get(i).move(offX, offY, offZ)); ++ } ++ ++ return new CachedToAABBs(ret, false, 0.0, 0.0, 0.0); ++ } ++ ++ public static CachedToAABBs offset(final CachedToAABBs cache, final double offX, final double offY, final double offZ) { ++ if (offX == 0.0 && offY == 0.0 && offZ == 0.0) { ++ return cache; ++ } ++ ++ final double resX = cache.offX + offX; ++ final double resY = cache.offY + offY; ++ final double resZ = cache.offZ + offZ; ++ ++ return new CachedToAABBs(cache.aabbs, true, resX, resY, resZ); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java +new file mode 100644 +index 0000000000000000000000000000000000000000..07fe5e02c2d0a27d2fe37bb45761654dc2d02e5d +--- /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 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..05d7b3f9d8659c259f3ed0537c57e6e43eb6e288 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java +@@ -0,0 +1,40 @@ ++package ca.spottedleaf.moonrise.patches.collisions.shape; ++ ++import net.minecraft.core.Direction; ++import net.minecraft.world.phys.AABB; ++import net.minecraft.world.phys.shapes.VoxelShape; ++ ++public interface CollisionVoxelShape { ++ ++ public double moonrise$offsetX(); ++ ++ public double moonrise$offsetY(); ++ ++ public double moonrise$offsetZ(); ++ ++ public double[] moonrise$rootCoordinatesX(); ++ ++ public double[] moonrise$rootCoordinatesY(); ++ ++ public double[] moonrise$rootCoordinatesZ(); ++ ++ public CachedShapeData moonrise$getCachedVoxelData(); ++ ++ // rets null if not possible to represent this shape as one AABB ++ public AABB moonrise$getSingleAABBRepresentation(); ++ ++ // ONLY USE INTERNALLY, ONLY FOR INITIALISING IN CONSTRUCTOR: VOXELSHAPES ARE STATIC ++ public void moonrise$initCache(); ++ ++ // this returns empty if not clamped to 1.0 or 0.0 depending on direction ++ public VoxelShape moonrise$getFaceShapeClamped(final Direction direction); ++ ++ public boolean moonrise$isFullBlock(); ++ ++ public boolean moonrise$occludesFullBlock(); ++ ++ public boolean moonrise$occludesFullBlockIfCached(); ++ ++ // uses a cache internally ++ public VoxelShape moonrise$orUnoptimized(final VoxelShape other); ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java +new file mode 100644 +index 0000000000000000000000000000000000000000..44831fc18efb7534dc6e4822f3c9b5cdc4dcc33e +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java +@@ -0,0 +1,10 @@ ++package ca.spottedleaf.moonrise.patches.collisions.shape; ++ ++import net.minecraft.world.phys.shapes.VoxelShape; ++ ++public record MergedORCache( ++ VoxelShape key, ++ 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/FluidOcclusionCacheKey.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cf9ffdeff6bf0b62a45f7a44dbfe0dd7d17dc4f4 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java +@@ -0,0 +1,7 @@ ++package ca.spottedleaf.moonrise.patches.collisions.util; ++ ++import net.minecraft.core.Direction; ++import net.minecraft.world.level.block.state.BlockState; ++ ++public record FluidOcclusionCacheKey(BlockState first, BlockState second, Direction direction, boolean result) { ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/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..8e7472157a98de607c03769a91f64c8369fd3ea6 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java +@@ -0,0 +1,15 @@ ++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(); ++ ++ public boolean moonrise$hasPlayers(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4a7abd239a9c59aa98947e7993962d75e9051902 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.fast_palette; ++ ++public interface FastPalette<T> { ++ ++ public default T[] moonrise$getRawPalette(final FastPaletteData<T> src) { ++ return null; ++ } ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4503f3495846a7d7ed082b9e24636044e4fbccd1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.fast_palette; ++ ++public interface FastPaletteData<T> { ++ ++ public T[] moonrise$getPalette(); ++ ++ public void moonrise$setPalette(final T[] palette); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java b/src/main/java/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java +new file mode 100644 +index 0000000000000000000000000000000000000000..107c97089354edd35f330582f5e0c8a18e792a6e +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java +@@ -0,0 +1,5 @@ ++package ca.spottedleaf.moonrise.patches.fluid; ++ ++public interface FluidFluidState { ++ public void moonrise$initCaches(); ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java +new file mode 100644 +index 0000000000000000000000000000000000000000..540c14a6d2c216cd3ef2a9c4056e15712bf8cb8c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.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/starlight/blockstate/StarlightAbstractBlockState.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8e6d79b7c10ef25f5478b72c53c555423d615a2f +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java +@@ -0,0 +1,7 @@ ++package ca.spottedleaf.moonrise.patches.starlight.blockstate; ++ ++public interface StarlightAbstractBlockState { ++ ++ public boolean starlight$isConditionallyFullOpaque(); ++ ++} +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..fa7b784a89626e8528c249d7889a598bd7ee3d49 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java +@@ -0,0 +1,280 @@ ++package ca.spottedleaf.moonrise.patches.starlight.light; ++ ++import ca.spottedleaf.moonrise.common.PlatformHooks; ++import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState; ++import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; ++import net.minecraft.core.BlockPos; ++import net.minecraft.world.level.BlockGetter; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.ChunkAccess; ++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 = (PlatformHooks.get().getLightEmission(blockState, lightAccess.getLevel(), this.lightEmissionPos.set(worldX, worldY, worldZ))) & emittedMask; ++ ++ this.setLightLevel(worldX, worldY, worldZ, emittedLevel); ++ // this accounts for change in emitted light that would cause an increase ++ 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(); ++ ++ @Override ++ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, ++ final int expect) { ++ this.recalcCenterPos.set(worldX, worldY, worldZ); ++ ++ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); ++ final BlockGetter world = lightAccess.getLevel(); ++ int level = (PlatformHooks.get().getLightEmission(centerState, world, this.recalcCenterPos)) & this.emittedLightMask; ++ ++ if (level >= (15 - 1) || level > expect) { ++ return level; ++ } ++ ++ final int opacity = Math.max(1, centerState.getLightBlock()); ++ if (opacity >= 15) { ++ return level; ++ } ++ final BlockState conditionallyOpaqueState; ++ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) { ++ conditionallyOpaqueState = centerState; ++ } else { ++ conditionallyOpaqueState = null; ++ } ++ ++ final int sectionOffset = this.chunkSectionIndexOffset; ++ 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 ++ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(direction.opposite.nms); ++ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(direction.nms); ++ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { ++ // not allowed to propagate ++ continue; ++ } ++ } ++ ++ // 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 PlatformHooks platformHooks = PlatformHooks.get(); ++ ++ final BlockGetter world = lightAccess.getLevel(); ++ final LevelChunkSection[] sections = chunk.getSections(); ++ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { ++ final LevelChunkSection section = sections[sectionY - this.minSection]; ++ if (section.hasOnlyAir()) { ++ // no sources in empty sections ++ continue; ++ } ++ if (!section.maybeHas(platformHooks.maybeHasLightEmission())) { ++ // no light sources in palette ++ continue; ++ } ++ final PalettedContainer<BlockState> states = section.states; ++ final int offY = sectionY << 4; ++ ++ final BlockPos.MutableBlockPos mutablePos = this.lightEmissionPos; ++ for (int index = 0; index < (16 * 16 * 16); ++index) { ++ final BlockState state = states.get(index); ++ mutablePos.set(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15)); ++ ++ if ((platformHooks.getLightEmission(state, world, mutablePos)) == 0) { ++ continue; ++ } ++ ++ // index = x | (z << 4) | (y << 8) ++ sources.add(mutablePos.immutable()); ++ } ++ } ++ ++ return sources; ++ } ++ ++ @Override ++ public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { ++ // setup sources ++ final BlockGetter world = lightAccess.getLevel(); ++ final PlatformHooks platformHooks = PlatformHooks.get(); ++ ++ final int emittedMask = this.emittedLightMask; ++ final List<BlockPos> positions = this.getSources(lightAccess, chunk); ++ for (int i = 0, len = positions.size(); i < len; ++i) { ++ final BlockPos pos = positions.get(i); ++ final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ()); ++ final int emittedLight = platformHooks.getLightEmission(blockState, world, pos) & 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..f9aef289e9a2d6f63c98c72c56ef32b8793f57f4 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java +@@ -0,0 +1,681 @@ ++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)) ++ ); ++ } ++ ++ @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); ++ ++ final BlockState conditionallyOpaqueState; ++ final int opacity = Math.max(1, centerState.getLightBlock()); ++ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) { ++ conditionallyOpaqueState = centerState; ++ } else { ++ conditionallyOpaqueState = null; ++ } ++ ++ 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 ++ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(direction.opposite.nms); ++ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(direction.nms); ++ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { ++ // not allowed to propagate ++ continue; ++ } ++ } ++ ++ 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 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()) { ++ fromShape = above.getFaceOcclusionShape(AxisDirection.NEGATIVE_Y.nms); ++ if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { ++ // above wont let us propagate ++ break; ++ } ++ } else { ++ fromShape = Shapes.empty(); ++ } ++ ++ // does light propagate from the top down? ++ long flags = 0L; ++ if (((StarlightAbstractBlockState)current).starlight$isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = current.getFaceOcclusionShape(AxisDirection.POSITIVE_Y.nms); ++ ++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { ++ // can't propagate here, we're done on this column. ++ break; ++ } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } ++ ++ final int opacity = current.getLightBlock(); ++ 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..8aeb5fb87f94a35659347a09a638420699b52a6f +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java +@@ -0,0 +1,1438 @@ ++package ca.spottedleaf.moonrise.patches.starlight.light; ++ ++import ca.spottedleaf.concurrentutil.util.IntegerUtil; ++import ca.spottedleaf.moonrise.common.PlatformHooks; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState; ++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, Direction.EAST) , NEGATIVE_X(-1, 0, 0, Direction.WEST), ++ POSITIVE_Z(0, 0, 1, Direction.SOUTH), NEGATIVE_Z(0, 0, -1, Direction.NORTH), ++ POSITIVE_Y(0, 1, 0, Direction.UP) , NEGATIVE_Y(0, -1, 0, Direction.DOWN); ++ ++ static { ++ POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X; ++ 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, final Direction nms) { ++ this.x = x; ++ this.y = y; ++ this.z = z; ++ this.nms = nms; ++ this.everythingButThisDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << this.ordinal())); ++ // positive is always even, negative is always odd. Flip the 1 bit to get the negative direction. ++ this.everythingButTheOppositeDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << (this.ordinal() ^ 1))); ++ } ++ ++ 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 lightEmissionPos = 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; ++ } ++ long flags = 0; ++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); ++ ++ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { ++ continue; ++ } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } ++ ++ final int opacity = blockState.getLightBlock(); ++ 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); ++ 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(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; ++ } ++ long flags = 0; ++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); ++ ++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { ++ continue; ++ } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } ++ ++ final int opacity = blockState.getLightBlock(); ++ 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; ++ ++ final PlatformHooks platformHooks = PlatformHooks.get(); ++ ++ 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; ++ } ++ this.lightEmissionPos.set(offX, offY, offZ); ++ long flags = 0; ++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); ++ ++ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { ++ continue; ++ } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } ++ ++ final int opacity = blockState.getLightBlock(); ++ 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 = (platformHooks.getLightEmission(blockState, world, this.lightEmissionPos)) & emittedMask; ++ if (emittedLight != 0) { ++ // re-propagate source ++ // note: do not set recheck level, or else the propagation will fail ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); ++ } ++ increaseQueue[increaseQueueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((emittedLight & 0xFL) << (6 + 6 + 16)) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | (flags | FLAG_WRITE_LEVEL); ++ } ++ ++ currentNibble.set(localIndex, 0); ++ this.postLightUpdate(offX, offY, offZ); ++ ++ 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); ++ 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(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; ++ } ++ this.lightEmissionPos.set(offX, offY, offZ); ++ long flags = 0; ++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); ++ ++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { ++ continue; ++ } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } ++ ++ final int opacity = blockState.getLightBlock(); ++ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); ++ if (lightLevel > targetLevel) { ++ // it looks like another source propagated here, so re-propagate it ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); ++ } ++ increaseQueue[increaseQueueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((lightLevel & 0xFL) << (6 + 6 + 16)) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | (FLAG_RECHECK_LEVEL | flags); ++ continue; ++ } ++ final int emittedLight = (platformHooks.getLightEmission(blockState, world, this.lightEmissionPos)) & emittedMask; ++ if (emittedLight != 0) { ++ // re-propagate source ++ // note: do not set recheck level, or else the propagation will fail ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); ++ } ++ increaseQueue[increaseQueueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((emittedLight & 0xFL) << (6 + 6 + 16)) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | (flags | FLAG_WRITE_LEVEL); ++ } ++ ++ currentNibble.set(localIndex, 0); ++ this.postLightUpdate(offX, offY, offZ); ++ ++ 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..571db5f9bf94745a8afe2cd313e593fb15db5e37 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java +@@ -0,0 +1,931 @@ ++package ca.spottedleaf.moonrise.patches.starlight.light; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; ++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 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 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 Priority priority) { ++ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ if (task != null) { ++ task.raisePriority(priority); ++ } ++ } ++ ++ public Priority getPriority(final int chunkX, final int chunkZ) { ++ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ if (task != null) { ++ return task.getPriority(); ++ } ++ ++ return 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 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, Priority.NORMAL); ++ } ++ ++ public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, ++ final ServerLightQueue queue, final Priority priority) { ++ super(chunkCoordinate, lightEngine, queue); ++ this.task = ((ChunkSystemServerLevel)(ServerLevel)lightEngine.getWorld()).moonrise$getChunkTaskScheduler().radiusAwareScheduler.createTask( ++ CoordinateUtils.getChunkX(chunkCoordinate), CoordinateUtils.getChunkZ(chunkCoordinate), ++ ((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 Priority getPriority() { ++ return this.task.getPriority(); ++ } ++ ++ public void lowerPriority(final Priority priority) { ++ this.task.lowerPriority(priority); ++ } ++ ++ public void setPriority(final Priority priority) { ++ this.task.setPriority(priority); ++ } ++ ++ public void raisePriority(final 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/storage/StarlightSectionData.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..40d004afdc6449530f5bb2d7c7638b8ee3e3a577 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java +@@ -0,0 +1,13 @@ ++package ca.spottedleaf.moonrise.patches.starlight.storage; ++ ++public interface StarlightSectionData { ++ ++ public int starlight$getBlockLightState(); ++ ++ public void starlight$setBlockLightState(final int state); ++ ++ public int starlight$getSkyLightState(); ++ ++ public void starlight$setSkyLightState(final int state); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..689ce367164e79e0426eeecb81dbbc521d4bc742 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java +@@ -0,0 +1,189 @@ ++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; ++ ++// note: keep in-sync with SerializableChunkDataMixin ++public final class SaveUtil { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ public static final int STARLIGHT_LIGHT_VERSION = 9; ++ ++ public static int getLightVersion() { ++ return STARLIGHT_LIGHT_VERSION; ++ } ++ ++ public static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state"; ++ public static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; ++ public static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; ++ ++ public static void saveLightHook(final Level world, final ChunkAccess chunk, final CompoundTag nbt) { ++ try { ++ 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 2d2ff826354670fef356e241d939080c9ed3fd4a..17e23ca4dd2bbfba49ea00aa2b719a95feb931be 100644 +--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java ++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +@@ -217,6 +217,23 @@ public class GlobalConfiguration extends ConfigurationPart { + @PostProcess + private void postProcess() { + ca.spottedleaf.moonrise.common.util.MoonriseCommon.adjustWorkerThreads(this.workerThreads, this.ioThreads); ++ ++ String newChunkSystemGenParallelism = this.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]"); ++ } ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.init(useParallelGen); + } + } + +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 3fde5abde736b2c19d8819d9aec0397a0245ccd1..6548302d4983bf48cc6bc2b7f4833dc76b59fa5e 100644 +--- a/src/main/java/net/minecraft/core/Direction.java ++++ b/src/main/java/net/minecraft/core/Direction.java +@@ -28,7 +28,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)), +@@ -62,6 +62,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, +@@ -147,14 +187,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() { +@@ -178,7 +217,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) { +@@ -600,4 +639,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/core/MappedRegistry.java b/src/main/java/net/minecraft/core/MappedRegistry.java +index 063630c1ffcce099139c59d598fc5a210e21f640..a61153c5d99bdc26f37a10f33baf839e943e17e1 100644 +--- a/src/main/java/net/minecraft/core/MappedRegistry.java ++++ b/src/main/java/net/minecraft/core/MappedRegistry.java +@@ -50,6 +50,19 @@ public class MappedRegistry<T> implements WritableRegistry<T> { + return this.getTags(); + } + ++ // Paper start - fluid method optimisations ++ private void injectFluidRegister( ++ final ResourceKey<?> resourceKey, ++ final T object ++ ) { ++ if (resourceKey.registryKey() == (Object)net.minecraft.core.registries.Registries.FLUID) { ++ for (final net.minecraft.world.level.material.FluidState possibleState : ((net.minecraft.world.level.material.Fluid)object).getStateDefinition().getPossibleStates()) { ++ ((ca.spottedleaf.moonrise.patches.fluid.FluidFluidState)(Object)possibleState).moonrise$initCaches(); ++ } ++ } ++ } ++ // Paper end - fluid method optimisations ++ + public MappedRegistry(ResourceKey<? extends Registry<T>> key, Lifecycle lifecycle) { + this(key, lifecycle, false); + } +@@ -114,6 +127,7 @@ public class MappedRegistry<T> implements WritableRegistry<T> { + this.toId.put(value, i); + this.registrationInfos.put(key, info); + this.registryLifecycle = this.registryLifecycle.add(info.lifecycle()); ++ this.injectFluidRegister(key, value); // Paper - fluid method optimisations + return reference; + } + } +diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java +index fc6ce3485dc890f5105a37fe3e344a1460867556..e114e687f2f4503546687fd6792226a643af8793 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 57b242a4a1de49e8869e8ec83e74d60d877a4003..9cb435573d0b1bdf5488bd5b9cef5d2aba6a1c2d 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -204,7 +204,7 @@ import org.bukkit.event.server.ServerLoadEvent; + // CraftBukkit end + + +-public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource { ++public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource, ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer { // Paper - rewrite chunk system + + private static MinecraftServer SERVER; // Paper + public static final Logger LOGGER = LogUtils.getLogger(); +@@ -331,7 +331,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"); + +@@ -350,6 +350,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 +@@ -670,7 +741,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())); + } + +@@ -883,6 +954,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) { +@@ -892,7 +968,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 +@@ -992,7 +1068,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; +@@ -1009,19 +1085,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(); +@@ -1041,6 +1105,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + } + // Spigot end + ++ // Paper start - rewrite chunk system ++ LOGGER.info("Waiting for I/O tasks to complete..."); ++ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.flush((MinecraftServer)(Object)this); ++ LOGGER.info("All I/O tasks to complete"); ++ if ((Object)this instanceof DedicatedServer) { ++ ca.spottedleaf.moonrise.common.util.MoonriseCommon.haltExecutors(); ++ } ++ // Paper end - rewrite chunk system + } + + public String getLocalIp() { +@@ -1221,6 +1293,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.tickFrame.end(); + gameprofilerfiller.popPush("nextTickWait"); + this.mayHaveDelayedTasks = true; +@@ -1425,6 +1504,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 +@@ -2689,6 +2769,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 cce0e570c8217c8e7cc81642d303e1b96f70f4f3..bf2bae3166df06be240dbbeecce16a24c85897a9 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -456,7 +456,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 b9ab241b930edc63a39dbbcf14cd0b5edacb9ea9..8dd9375f2ad2c65a773a3195aeff1f977e09e7e0 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,64 +168,65 @@ 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.saveSync.isDone(); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Override + protected 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 boolean blockChanged(BlockPos pos) { +- LevelChunk chunk = this.getTickingChunk(); ++ LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system + + if (chunk == null) { + return false; +@@ -172,7 +252,7 @@ public class ChunkHolder extends GenerationChunkHolder { + return false; + } else { + ichunkaccess.markUnsaved(); +- LevelChunk chunk = this.getTickingChunk(); ++ LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system + + if (chunk == null) { + return false; +@@ -207,7 +287,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); + +@@ -219,7 +299,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]; +@@ -285,201 +365,48 @@ 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 + } + + // CraftBukkit start + // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins. + // SPIGOT-7780: Moved out of updateFutures to call all chunk unload events before calling updateHighestAllowedStatus for all chunks + protected void callEventIfUnloading(ChunkMap playerchunkmap) { +- FullChunkStatus oldFullChunkStatus = ChunkLevel.fullStatus(this.oldTicketLevel); +- FullChunkStatus newFullChunkStatus = ChunkLevel.fullStatus(this.ticketLevel); +- boolean oldIsFull = oldFullChunkStatus.isOrAfter(FullChunkStatus.FULL); +- boolean newIsFull = newFullChunkStatus.isOrAfter(FullChunkStatus.FULL); +- if (oldIsFull && !newIsFull) { +- this.getFullChunkFuture().thenAccept((either) -> { +- LevelChunk chunk = (LevelChunk) either.orElse(null); +- if (chunk != null) { +- playerchunkmap.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.markUnsaved(); +- 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 +- playerchunkmap.callbackExecutor.run(); +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + // CraftBukkit end + + 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); +- +- 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 16e55cc94c8f6e204e4b7ab6ad8d32a6c443357f..80bbf77454ff34505196998bcfeaa3e40a4f639c 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -108,7 +108,7 @@ import org.slf4j.Logger; + import org.bukkit.craftbukkit.generator.CustomChunkGenerator; + // CraftBukkit end + +-public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider, GeneratingChunkMap { ++public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider, GeneratingChunkMap, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemChunkMap { // Paper - rewrite chunk system + + private static final ChunkResult<List<ChunkAccess>> UNLOADED_CHUNK_LIST_RESULT = ChunkResult.error("Unloaded chunks found in range"); + private static final CompletableFuture<ChunkResult<List<ChunkAccess>>> UNLOADED_CHUNK_LIST_FUTURE = CompletableFuture.completedFuture(ChunkMap.UNLOADED_CHUNK_LIST_RESULT); +@@ -123,10 +123,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; +@@ -136,22 +133,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + private final PoiManager poiManager; + public final LongSet toDrop; + private boolean modified; +- private final ChunkTaskDispatcher worldgenTaskDispatcher; +- private final ChunkTaskDispatcher lightTaskDispatcher; ++ // 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 nextChunkSaveTime; +- private final LongSet chunksToEagerlySave; +- private final Queue<Runnable> unloadQueue; +- private final AtomicInteger activeChunkWrites; ++ // 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(); +@@ -176,24 +169,26 @@ 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 ++ // Paper start - rewrite chunk system ++ @Override ++ public final void moonrise$writeFinishCallback(final ChunkPos pos) throws IOException { ++ // see ChunkStorage#write ++ this.handleLegacyStructureIndex(pos); ++ } ++ // Paper end - rewrite chunk system + + 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.nextChunkSaveTime = new Long2LongOpenHashMap(); +- this.chunksToEagerlySave = new LongLinkedOpenHashSet(); +- this.unloadQueue = Queues.newConcurrentLinkedQueue(); +- this.activeChunkWrites = new AtomicInteger(); ++ // Paper - rewrite chunk system + Path path = session.getDimensionPath(world.dimension()); + + this.storageName = path.getFileName().toString(); +@@ -221,18 +216,16 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.chunkStatusListener = chunkStatusChangeListener; + ConsecutiveExecutor consecutiveexecutor1 = new ConsecutiveExecutor(executor, "light"); + +- this.worldgenTaskDispatcher = new ChunkTaskDispatcher(consecutiveexecutor, executor); +- this.lightTaskDispatcher = new ChunkTaskDispatcher(consecutiveexecutor1, executor); +- this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), consecutiveexecutor1, this.lightTaskDispatcher); ++ this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), consecutiveexecutor1, 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, mainThreadExecutor, this::setChunkUnsaved); ++ this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, null, this::setChunkUnsaved); // Paper - rewrite chunk system + } + + private void setChunkUnsaved(ChunkPos pos) { +- this.chunksToEagerlySave.add(pos.toLong()); ++ // Paper - rewrite chunk system + } + + // Paper start +@@ -263,23 +256,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() { +@@ -288,20 +269,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) { +@@ -330,56 +313,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 { +- int j = Mth.square(margin * 2 + 1); +- List<CompletableFuture<ChunkResult<ChunkAccess>>> list = new ArrayList(j); +- ChunkPos chunkcoordintpair = centerChunk.getPos(); +- +- for (int k = -margin; k <= margin; ++k) { +- for (int l = -margin; l <= margin; ++l) { +- int i1 = Math.max(Math.abs(l), Math.abs(k)); +- long j1 = ChunkPos.asLong(chunkcoordintpair.x + l, chunkcoordintpair.z + k); +- ChunkHolder playerchunk1 = this.getUpdatingChunkIfPresent(j1); +- +- if (playerchunk1 == null) { +- return ChunkMap.UNLOADED_CHUNK_LIST_FUTURE; +- } +- +- ChunkStatus chunkstatus1 = (ChunkStatus) distanceToStatus.apply(i1); +- +- list.add(playerchunk1.scheduleChunkGenerationTask(chunkstatus1, this)); +- } +- } +- +- return Util.sequence(list).thenApply((list1) -> { +- List<ChunkAccess> list2 = new ArrayList(list1.size()); +- 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) { +@@ -409,104 +343,30 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public CompletableFuture<ChunkResult<LevelChunk>> prepareEntityTickingChunk(ChunkHolder holder) { +- return this.getChunkRangeFuture(holder, 2, (i) -> { +- return ChunkStatus.FULL; +- }).thenApply((chunkresult) -> { +- return chunkresult.map((list) -> { +- return (LevelChunk) list.get(list.size() / 2); +- }); +- }); ++ 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::onLevelChange, 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 + } + + private void onLevelChange(ChunkPos pos, IntSupplier levelGetter, int targetLevel, IntConsumer levelSetter) { +- this.worldgenTaskDispatcher.onLevelChange(pos, levelGetter, targetLevel, levelSetter); +- this.lightTaskDispatcher.onLevelChange(pos, levelGetter, targetLevel, levelSetter); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Override + public void close() throws IOException { +- try { +- this.worldgenTaskDispatcher.close(); +- this.lightTaskDispatcher.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.poiManager.flushAll(); +- this.processUnloads(() -> { +- return true; +- }); +- this.flushWorker(); +- } else { +- this.nextChunkSaveTime.clear(); +- long i = Util.getMillis(); +- Iterator<ChunkHolder> objectiterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper +- +- while (objectiterator.hasNext()) { +- ChunkHolder playerchunk = (ChunkHolder) objectiterator.next(); +- +- this.saveChunkIfNeeded(playerchunk, i); +- } +- } ++ // Paper start - rewrite chunk system ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.saveAllChunks( ++ flush, false, false ++ ); ++ // Paper end - rewrite chunk system + + } + +@@ -524,143 +384,29 @@ 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.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.worldgenTaskDispatcher.hasWork() || this.lightTaskDispatcher.hasWork() || this.distanceManager.hasTickets(); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void processUnloads(BooleanSupplier shouldKeepTicking) { +- for (LongIterator longiterator = this.toDrop.iterator(); longiterator.hasNext(); longiterator.remove()) { +- long i = longiterator.nextLong(); +- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.get(i); +- +- if (playerchunk != null) { +- this.updatingChunkMap.remove(i); +- this.pendingUnloads.put(i, playerchunk); +- this.modified = true; +- this.scheduleUnload(i, playerchunk); +- } +- } +- +- int j = Math.max(0, this.unloadQueue.size() - 2000); +- +- Runnable runnable; +- +- while ((j > 0 || shouldKeepTicking.getAsBoolean()) && (runnable = (Runnable) this.unloadQueue.poll()) != null) { +- --j; +- runnable.run(); +- } +- +- this.saveChunksEagerly(shouldKeepTicking); ++ ((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 saveChunksEagerly(BooleanSupplier shouldKeepTicking) { +- long i = Util.getMillis(); +- int j = 0; +- LongIterator longiterator = this.chunksToEagerlySave.iterator(); +- +- while (j < 20 && this.activeChunkWrites.get() < 128 && shouldKeepTicking.getAsBoolean() && longiterator.hasNext()) { +- long k = longiterator.nextLong(); +- ChunkHolder playerchunk = (ChunkHolder) this.visibleChunkMap.get(k); +- ChunkAccess ichunkaccess = playerchunk != null ? playerchunk.getLatestChunk() : null; +- +- if (ichunkaccess != null && ichunkaccess.isUnsaved()) { +- if (this.saveChunkIfNeeded(playerchunk, i)) { +- ++j; +- longiterator.remove(); +- } +- } else { +- longiterator.remove(); +- } +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + + } + + private void scheduleUnload(long pos, ChunkHolder chunk) { +- CompletableFuture<?> completablefuture = chunk.getSaveSyncFuture(); +- Runnable runnable = () -> { +- CompletableFuture<?> completablefuture1 = chunk.getSaveSyncFuture(); +- +- if (completablefuture1 != completablefuture) { +- this.scheduleUnload(pos, chunk); +- } else { +- ChunkAccess ichunkaccess = chunk.getLatestChunk(); +- // Paper start +- boolean removed; +- if ((removed = this.pendingUnloads.remove(pos, chunk)) && ichunkaccess != null) { +- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunk); +- // Paper end +- LevelChunk chunk1; +- +- if (ichunkaccess instanceof LevelChunk) { +- chunk1 = (LevelChunk) ichunkaccess; +- chunk1.setLoaded(false); +- } +- +- this.save(ichunkaccess); +- if (ichunkaccess instanceof LevelChunk) { +- chunk1 = (LevelChunk) ichunkaccess; +- this.level.unload(chunk1); +- } +- +- this.lightEngine.updateChunkStatus(ichunkaccess.getPos()); +- this.lightEngine.tryScheduleUpdate(); +- this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null); +- this.nextChunkSaveTime.remove(ichunkaccess.getPos().toLong()); +- } else if (removed) { // Paper start +- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunk); +- } // 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 {}", chunk.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) { +- CompletableFuture<Optional<SerializableChunkData>> completablefuture = this.readChunk(pos).thenApplyAsync((optional) -> { +- return optional.map((nbttagcompound) -> { +- SerializableChunkData serializablechunkdata = SerializableChunkData.parse(this.level, this.level.registryAccess(), nbttagcompound); +- +- if (serializablechunkdata == null) { +- ChunkMap.LOGGER.error("Chunk file at {} is missing level data, skipping", pos); +- } +- +- return serializablechunkdata; +- }); +- }, Util.backgroundExecutor().forName("parseChunk")); +- CompletableFuture<?> completablefuture1 = this.poiManager.prefetch(pos); +- +- return completablefuture.thenCombine(completablefuture1, (optional, object) -> { +- return optional; +- }).thenApplyAsync((optional) -> { +- Profiler.get().incrementCounter("chunkLoad"); +- if (optional.isPresent()) { +- ProtoChunk protochunk = ((SerializableChunkData) optional.get()).read(this.level, this.poiManager, this.storageInfo(), pos); +- +- 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 ChunkAccess handleChunkLoadFailure(Throwable throwable, ChunkPos chunkPos) { +@@ -716,139 +462,43 @@ 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 loader) { +- GenerationChunkHolder generationchunkholder = loader.getCenter(); +- ChunkTaskDispatcher chunktaskdispatcher = this.worldgenTaskDispatcher; +- Runnable runnable = () -> { +- CompletableFuture<?> completablefuture = loader.runUntilWait(); +- +- if (completablefuture != null) { +- completablefuture.thenRun(() -> { +- this.runGenerationTask(loader); +- }); +- } +- }; +- long i = generationchunkholder.getPos().toLong(); +- +- Objects.requireNonNull(generationchunkholder); +- chunktaskdispatcher.submit(runnable, i, generationchunkholder::getQueueLevel); ++ 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) -> { +- LevelChunk chunk = (LevelChunk) list.get(list.size() / 2); +- +- chunk.postProcessGeneration(this.level); +- this.level.startTickingChunk(chunk); +- CompletableFuture<?> completablefuture2 = holder.getSendSyncFuture(); +- +- if (completablefuture2.isDone()) { +- this.onChunkReadyToSend(holder, chunk); +- } else { +- completablefuture2.thenAcceptAsync((object) -> { +- this.onChunkReadyToSend(holder, chunk); +- }, this.mainThreadExecutor); +- } +- +- return chunk; +- }); +- }, this.mainThreadExecutor); +- +- completablefuture1.handle((chunkresult, throwable) -> { +- this.tickingGenerated.getAndIncrement(); +- return null; +- }); +- return completablefuture1; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void onChunkReadyToSend(ChunkHolder chunkHolder, 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); +- } +- } +- +- this.level.getChunkSource().onChunkReadyToSend(chunkHolder); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public CompletableFuture<ChunkResult<LevelChunk>> prepareAccessibleChunk(ChunkHolder holder) { +- return this.getChunkRangeFuture(holder, 1, ChunkLevel::getStatusAroundFullChunk).thenApply((chunkresult) -> { +- return chunkresult.map((list) -> { +- return (LevelChunk) list.get(list.size() / 2); +- }); +- }); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public int getTickingGenerated() { +@@ -856,144 +506,80 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + private boolean saveChunkIfNeeded(ChunkHolder chunkHolder, long currentTime) { +- if (chunkHolder.wasAccessibleSinceLastSave() && chunkHolder.isReadyForSaving()) { +- ChunkAccess ichunkaccess = chunkHolder.getLatestChunk(); +- +- if (!(ichunkaccess instanceof ImposterProtoChunk) && !(ichunkaccess instanceof LevelChunk)) { +- return false; +- } else if (!ichunkaccess.isUnsaved()) { +- return false; +- } else { +- long j = ichunkaccess.getPos().toLong(); +- long k = this.nextChunkSaveTime.getOrDefault(j, -1L); +- +- if (currentTime < k) { +- return false; +- } else { +- boolean flag = this.save(ichunkaccess); +- +- chunkHolder.refreshAccessibility(); +- if (flag) { +- this.nextChunkSaveTime.put(j, currentTime + 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.tryMarkSaved()) { +- return false; +- } else { +- 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; +- } +- } +- +- Profiler.get().incrementCounter("chunkSave"); +- this.activeChunkWrites.incrementAndGet(); +- SerializableChunkData serializablechunkdata = SerializableChunkData.copyOf(this.level, chunk); +- +- Objects.requireNonNull(serializablechunkdata); +- CompletableFuture<CompoundTag> completablefuture = CompletableFuture.supplyAsync(serializablechunkdata::write, Util.backgroundExecutor()); +- +- Objects.requireNonNull(completablefuture); +- this.write(chunkcoordintpair, completablefuture::join).handle((ovoid, throwable) -> { +- if (throwable != null) { +- this.level.getServer().reportChunkSaveFailure(throwable, this.storageInfo(), chunkcoordintpair); +- } +- +- this.activeChunkWrites.decrementAndGet(); +- 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 = SerializableChunkData.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) { ++ final CompletableFuture<Optional<CompoundTag>> ret = new CompletableFuture<>(); ++ ++ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.loadDataAsync( ++ this.level, pos.x, pos.z, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, ++ (final CompoundTag data, final Throwable thr) -> { ++ if (thr != null) { ++ ret.completeExceptionally(thr); ++ } else { ++ ret.complete(Optional.ofNullable(data)); ++ } ++ }, false ++ ); ++ ++ return ret; ++ } ++ ++ @Override ++ public CompletableFuture<Void> write(final ChunkPos pos, final Supplier<CompoundTag> tag) { ++ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.scheduleSave( ++ this.level, pos.x, pos.z, tag.get(), ++ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionFileType.CHUNK_DATA ++ ); ++ return null; + } + ++ @Override ++ public void flushWorker() { ++ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.flush(this.level); ++ } ++ // Paper end - rewrite chunk system ++ + @Nullable + public LevelChunk getChunkToSend(long pos) { + ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); +@@ -1059,7 +645,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 + } +@@ -1069,7 +655,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + while (longiterator.hasNext()) { + long i = longiterator.nextLong(); +- ChunkHolder playerchunk = (ChunkHolder) this.visibleChunkMap.get(i); ++ ChunkHolder playerchunk = (ChunkHolder) this.getVisibleChunkIfPresent(i); // Paper - rewrite chunk system + + if (playerchunk != null && this.anyPlayerCloseEnoughForSpawningInternal(playerchunk.getPos())) { + callback.accept(playerchunk); +@@ -1084,7 +670,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + boolean anyPlayerCloseEnoughForSpawning(ChunkPos chunkcoordintpair, boolean reducedRange) { +- return !this.distanceManager.hasPlayersNearby(chunkcoordintpair.toLong()) ? false : this.anyPlayerCloseEnoughForSpawningInternal(chunkcoordintpair, reducedRange); ++ return this.anyPlayerCloseEnoughForSpawningInternal(chunkcoordintpair, reducedRange); // Paper - chunk tick iteration optimisation + // Spigot end + } + +@@ -1096,16 +682,20 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos chunkcoordintpair, boolean reducedRange) { + double blockRange; // Paper - use from event + // Spigot end +- Iterator iterator = this.playerMap.getAllPlayers().iterator(); +- +- ServerPlayer entityplayer; ++ // Paper start - chunk tick iteration optimisation ++ final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers().getPlayers( ++ chunkcoordintpair, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.SPAWN_RANGE ++ ); ++ if (players == null) { ++ return false; ++ } + +- do { +- if (!iterator.hasNext()) { +- return false; +- } ++ final ServerPlayer[] raw = players.getRawDataUnchecked(); ++ final int len = players.size(); + +- entityplayer = (ServerPlayer) iterator.next(); ++ Objects.checkFromIndexSize(0, len, raw.length); ++ for (int i = 0; i < len; ++i) { ++ final ServerPlayer entityplayer = raw[i]; + // Paper start - PlayerNaturallySpawnCreaturesEvent + com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event; + blockRange = 16384.0D; +@@ -1115,33 +705,47 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + blockRange = (double) ((event.getSpawnRadius() << 4) * (event.getSpawnRadius() << 4)); + } + // Paper end - PlayerNaturallySpawnCreaturesEvent +- } while (!this.playerIsCloseEnoughForSpawning(entityplayer, chunkcoordintpair, blockRange)); // Spigot ++ if (this.playerIsCloseEnoughForSpawning(entityplayer, chunkcoordintpair, blockRange)) { ++ return true; ++ } ++ } + +- return true; ++ return false; ++ // Paper end - chunk tick iteration optimisation + } + + public List<ServerPlayer> getPlayersCloseForSpawning(ChunkPos pos) { +- long i = pos.toLong(); ++ // Paper start - chunk tick iteration optimisation ++ final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers().getPlayers( ++ pos, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.SPAWN_RANGE ++ ); ++ if (players == null) { ++ return new ArrayList<>(); ++ } + +- if (!this.distanceManager.hasPlayersNearby(i)) { +- return List.of(); +- } else { +- Builder<ServerPlayer> builder = ImmutableList.builder(); +- Iterator iterator = this.playerMap.getAllPlayers().iterator(); ++ List<ServerPlayer> ret = null; + +- while (iterator.hasNext()) { +- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); ++ final ServerPlayer[] raw = players.getRawDataUnchecked(); ++ final int len = players.size(); + +- if (this.playerIsCloseEnoughForSpawning(entityplayer, pos, 16384.0D)) { // Spigot +- builder.add(entityplayer); ++ Objects.checkFromIndexSize(0, len, raw.length); ++ for (int i = 0; i < len; ++i) { ++ final ServerPlayer player = raw[i]; ++ if (this.playerIsCloseEnoughForSpawning(player, pos, 16384.0D)) { // Spigot ++ if (ret == null) { ++ ret = new ArrayList<>(len - i); ++ ret.add(player); ++ } else { ++ ret.add(player); + } + } +- +- return builder.build(); + } ++ ++ return ret == null ? new ArrayList<>() : ret; ++ // Paper end - chunk tick iteration optimisation + } + +- private boolean playerIsCloseEnoughForSpawning(ServerPlayer entityplayer, ChunkPos chunkcoordintpair, double range) { // Spigot ++ public boolean playerIsCloseEnoughForSpawning(ServerPlayer entityplayer, ChunkPos chunkcoordintpair, double range) { // Spigot // Paper - chunk tick iteration optimisation - public + if (entityplayer.isSpectator()) { + return false; + } else { +@@ -1164,19 +768,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 + } + + } +@@ -1188,17 +794,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); +@@ -1208,6 +804,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); + } +@@ -1224,70 +821,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) { +@@ -1314,6 +871,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; +@@ -1354,16 +917,38 @@ 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.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(((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$getChunkData().nearbyPlayers); ++ if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$hasPlayers() ++ || ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING)) { ++ tracker.serverEntity.sendChanges(); ++ } ++ } ++ } ++ // Paper end - optimise entity tracker + +- this.updateChunkTracking(entityplayer); ++ 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(); +@@ -1466,27 +1051,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 +@@ -1502,7 +1085,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; +@@ -1510,6 +1093,89 @@ 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); ++ } ++ } ++ ++ @Override ++ public final boolean moonrise$hasPlayers() { ++ return !this.seenBy.isEmpty(); ++ } ++ // Paper end - optimise entity tracker ++ + public TrackedEntity(final Entity entity, final int i, final int j, final boolean flag) { + this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast, this.seenBy); // CraftBukkit + this.entity = entity; +@@ -1612,20 +1278,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + private int getEffectiveRange() { +- int i = this.range; +- Iterator iterator = this.entity.getIndirectPassengers().iterator(); ++ // Paper start - optimise entity tracker ++ final Entity entity = this.entity; ++ int range = this.range; + +- while (iterator.hasNext()) { +- Entity entity = (Entity) iterator.next(); +- int j = entity.getType().clientTrackingRange() * 16; +- j = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, j); // Paper ++ if (entity.getPassengers() == ImmutableList.<Entity>of()) { ++ return this.scaledRange(range); ++ } + +- if (j > i) { +- i = j; +- } ++ // note: we change to List ++ final List<Entity> passengers = (List<Entity>)entity.getIndirectPassengers(); ++ for (int i = 0, len = passengers.size(); i < len; ++i) { ++ final Entity passenger = passengers.get(i); ++ // note: max should be branchless ++ range = Math.max(range, ca.spottedleaf.moonrise.common.PlatformHooks.get().modifyEntityTrackingRange(passenger, passenger.getType().clientTrackingRange() << 4)); + } + +- return this.scaledRange(i); ++ return this.scaledRange(range); ++ // Paper end - optimise entity tracker + } + + public void updatePlayers(List<ServerPlayer> players) { +diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java +index f7c2c03749d6be25bf33afd61e1da120770b3432..7a9e7fc688e48d18a6a884f02f768ae652326aae 100644 +--- a/src/main/java/net/minecraft/server/level/DistanceManager.java ++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java +@@ -34,58 +34,57 @@ 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 = new ReferenceOpenHashSet(); +- final ThrottlingChunkTaskDispatcher ticketDispatcher; +- 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) { + TaskScheduler<Runnable> taskscheduler = TaskScheduler.wrapExecutor("player ticket throttler", mainThreadExecutor); + +- this.ticketDispatcher = new ThrottlingChunkTaskDispatcher(taskscheduler, workerExecutor, 4); +- 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 ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager moonrise$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.moonrise$getChunkHolderManager().tick(); // Paper - rewrite chunk system + + } + +@@ -102,105 +101,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()) { +- Iterator iterator = this.chunksToUpdateFutures.iterator(); +- +- ChunkHolder playerchunk; +- +- // CraftBukkit start - SPIGOT-7780: Call chunk unload events before updateHighestAllowedStatus +- while (iterator.hasNext()) { +- playerchunk = (ChunkHolder) iterator.next(); +- playerchunk.callEventIfUnloading(chunkLoadingManager); +- } +- +- iterator = this.chunksToUpdateFutures.iterator(); +- // CraftBukkit end +- +- while (iterator.hasNext()) { +- playerchunk = (ChunkHolder) iterator.next(); +- playerchunk.updateHighestAllowedStatus(chunkLoadingManager); +- } +- +- iterator = this.chunksToUpdateFutures.iterator(); +- +- while (iterator.hasNext()) { +- playerchunk = (ChunkHolder) iterator.next(); +- 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 playerchunk1 = chunkLoadingManager.getUpdatingChunkIfPresent(j); +- +- if (playerchunk1 == null) { +- throw new IllegalStateException(); +- } +- +- CompletableFuture<ChunkResult<LevelChunk>> completablefuture = playerchunk1.getEntityTickingChunkFuture(); +- +- completablefuture.thenAccept((chunkresult) -> { +- this.mainThreadExecutor.execute(() -> { +- this.ticketDispatcher.release(j, () -> { +- }, false); +- }); +- }); +- } +- } +- +- this.ticketsToRelease.clear(); +- } +- +- return flag; +- } ++ return this.moonrise$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.moonrise$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.moonrise$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) { +@@ -219,13 +128,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.moonrise$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) { +@@ -234,32 +137,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.moonrise$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.moonrise$getChunkHolderManager().addTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos); + } else { +- this.removeTicket(i, ticket); +- this.tickingTicketsTracker.removeTicket(i, ticket); ++ this.moonrise$getChunkHolderManager().removeTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos); + } ++ // Paper end - rewrite chunk system + + } + +@@ -270,9 +162,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) { +@@ -284,160 +175,89 @@ 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.moonrise$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.moonrise$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.moonrise$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 LongIterator getSpawnCandidateChunks() { +- this.naturalSpawnChunkCounter.runAllUpdates(); +- return this.naturalSpawnChunkCounter.chunks.keySet().iterator(); ++ return this.spawnChunkTracker.getPositions().iterator(); // Paper - chunk tick iteration optimisation + } + + public String getDebugStatus() { +- return this.ticketDispatcher.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 LongSet getTickingChunks() { +- return this.tickingTicketsTracker.getTickingChunks(); ++ 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.moonrise$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; +@@ -483,7 +303,7 @@ public abstract class DistanceManager { + public int runDistanceUpdates(int distance) { + return this.runUpdates(distance); + } +- } ++ }*/ // Paper - rewrite chunk system + + private class FixedPlayerDistanceChunkTracker extends ChunkTracker { + +@@ -563,6 +383,7 @@ public abstract class DistanceManager { + } + } + ++ /* // Paper - rewrite chunk system + private class PlayerTicketTracker extends DistanceManager.FixedPlayerDistanceChunkTracker { + + private int viewDistance = 0; +@@ -657,5 +478,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 65206fdfa5b94eaca139e433b4865c16b16641f3..bf4463bcb5dc439ac5a3fa08dd60845a5fd7489a 100644 +--- a/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java ++++ b/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java +@@ -27,13 +27,7 @@ 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(); +- private volatile CompletableFuture<Void> generationSaveSyncFuture = CompletableFuture.completedFuture(null); ++ // Paper - rewrite chunk system + + public GenerationChunkHolder(ChunkPos pos) { + this.pos = pos; +@@ -43,243 +37,96 @@ public abstract class GenerationChunkHolder { + } + + 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 + } + + protected abstract void addSaveDependency(CompletableFuture<?> savingFuture); + + public void increaseGenerationRefCount() { +- if (this.generationRefCount.getAndIncrement() == 0) { +- this.generationSaveSyncFuture = new CompletableFuture<>(); +- this.addSaveDependency(this.generationSaveSyncFuture); +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public void decreaseGenerationRefCount() { +- CompletableFuture<Void> completableFuture = this.generationSaveSyncFuture; +- int i = this.generationRefCount.decrementAndGet(); +- if (i == 0) { +- completableFuture.complete(null); +- } +- +- if (i < 0) { +- throw new IllegalStateException("More releases than claims. Count: " + i); +- } ++ 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() { +@@ -287,7 +134,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(); +@@ -296,26 +143,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 6a2af3cd3aebe525a5ff41a801929547d59b8fec..b3ce572547535001959d9bcc6cb567da552c6539 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -52,7 +52,7 @@ import net.minecraft.world.level.storage.DimensionDataStorage; + import net.minecraft.world.level.storage.LevelStorageSource; + import org.slf4j.Logger; + +-public class ServerChunkCache extends ChunkSource { ++public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache { // Paper - rewrite chunk system + + private static final Logger LOGGER = LogUtils.getLogger(); + private final DistanceManager distanceManager; +@@ -81,6 +81,100 @@ public class ServerChunkCache extends ChunkSource { + } + 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.util.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; ++ } ++ ++ final ca.spottedleaf.moonrise.common.PlatformHooks platformHooks = ca.spottedleaf.moonrise.common.PlatformHooks.get(); ++ ++ if (platformHooks.hasCurrentlyLoadingChunk() && currentChunk != null) { ++ final ChunkAccess loading = platformHooks.getCurrentlyLoadingChunk(currentChunk.vanillaChunkHolder); ++ if (loading != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { ++ return loading; ++ } ++ } ++ ++ return load ? this.syncLoad(chunkX, chunkZ, toStatus) : null; ++ } ++ // Paper end - rewrite chunk system ++ // Paper start - chunk tick iteration optimisations ++ private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom shuffleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(0L); ++ private boolean isChunkNearPlayer(final ChunkMap chunkMap, final ChunkPos chunkPos, final LevelChunk levelChunk) { ++ final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)levelChunk).moonrise$getChunkAndHolder().holder()) ++ .moonrise$getRealChunkHolder().holderData; ++ final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk nearbyPlayers = chunkData.nearbyPlayers; ++ if (nearbyPlayers == null) { ++ return false; ++ } ++ ++ final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> players = nearbyPlayers.getPlayers(ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.SPAWN_RANGE); ++ ++ if (players == null) { ++ return false; ++ } ++ ++ final ServerPlayer[] raw = players.getRawDataUnchecked(); ++ final int len = players.size(); ++ ++ Objects.checkFromIndexSize(0, len, raw.length); ++ for (int i = 0; i < len; ++i) { ++ if (chunkMap.playerIsCloseEnoughForSpawning(raw[i], chunkPos, 16384.0D)) { // Spigot (reducedRange = false) ++ return true; ++ } ++ } ++ ++ return false; ++ } ++ // Paper end - 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; +@@ -112,13 +206,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) { +@@ -189,59 +277,42 @@ 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 = Profiler.get(); +- +- 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); +- chunkproviderserver_b.managedBlock(completablefuture::isDone); +- // com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads +- 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 ++ // Paper start - rewrite chunk system ++ final LevelChunk ret = this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ if (!ca.spottedleaf.moonrise.common.PlatformHooks.get().hasCurrentlyLoadingChunk()) { ++ return ret; ++ } ++ ++ if (ret != null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { ++ return ret; + } ++ ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler() ++ .chunkHolderManager.getChunkHolder(chunkX, chunkZ); ++ if (holder == null) { ++ return ret; ++ } ++ ++ return ca.spottedleaf.moonrise.common.PlatformHooks.get().getCurrentlyLoadingChunk(holder.vanillaChunkHolder); ++ // Paper end - rewrite chunk system + } + + private void clearCache() { +@@ -272,56 +343,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); +- +- // 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)); +- } +- if (create && !currentlyUnloading) { +- // CraftBukkit end +- this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair); +- if (this.chunkAbsent(playerchunk, l)) { +- ProfilerFiller gameprofilerfiller = Profiler.get(); +- +- 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")); +- } +- } ++ // Paper start - rewrite chunk system ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Scheduling chunk load off-main"); ++ ++ 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; + } + +- return this.chunkAbsent(playerchunk, l) ? GenerationChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.scheduleChunkGenerationTask(leastStatus, this.chunkMap); +- } ++ 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)); ++ } ++ }; + +- private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) { +- return holder == null || holder.oldTicketLevel > maxLevel; // CraftBukkit using oldTicketLevel for isLoaded checks ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().scheduleChunkLoad( ++ chunkX, chunkZ, leastStatus, true, ++ ca.spottedleaf.concurrentutil.util.Priority.HIGHER, ++ complete ++ ); ++ ++ 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 +@@ -334,30 +408,18 @@ 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 + } + + public boolean isPositionTicking(long pos) { +- if (!this.level.shouldTickBlocksAt(pos)) { +- return false; +- } else { +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); +- +- return playerchunk == null ? 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 + this.chunkMap.saveAllChunks(flush); + } + +@@ -368,17 +430,15 @@ public class ServerChunkCache extends ChunkSource { + } + + public void close(boolean save) throws IOException { +- if (save) { +- this.save(true); +- } + // CraftBukkit end ++ // Paper - rewrite chunk system + this.dataStorage.close(); +- 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 + ProfilerFiller gameprofilerfiller = Profiler.get(); + + gameprofilerfiller.push("purge"); +@@ -403,6 +463,7 @@ public class ServerChunkCache extends ChunkSource { + this.runDistanceManagerUpdates(); + gameprofilerfiller.popPush("chunks"); + if (tickChunks) { ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().tick(); // Paper - rewrite chunk system + this.tickChunks(); + this.chunkMap.tick(); + } +@@ -429,7 +490,10 @@ public class ServerChunkCache extends ChunkSource { + gameprofilerfiller.push("filteringTickingChunks"); + this.collectTickingChunks(list); + gameprofilerfiller.popPush("shuffleChunks"); +- Util.shuffle(list, this.level.random); ++ // Paper start - chunk tick iteration optimisation ++ this.shuffleRandom.setSeed(this.level.random.nextLong()); ++ Util.shuffle(list, this.shuffleRandom); ++ // Paper end - chunk tick iteration optimisation + this.tickChunks(gameprofilerfiller, j, list); + gameprofilerfiller.pop(); + } finally { +@@ -448,7 +512,7 @@ public class ServerChunkCache extends ChunkSource { + + while (iterator.hasNext()) { + ChunkHolder playerchunk = (ChunkHolder) iterator.next(); +- LevelChunk chunk = playerchunk.getTickingChunk(); ++ LevelChunk chunk = playerchunk.getChunkToSend(); // Paper - rewrite chunk system + + if (chunk != null) { + playerchunk.broadcastChanges(chunk); +@@ -460,14 +524,26 @@ public class ServerChunkCache extends ChunkSource { + } + + private void collectTickingChunks(List<LevelChunk> chunks) { +- this.chunkMap.forEachSpawnCandidateChunk((playerchunk) -> { +- LevelChunk chunk = playerchunk.getTickingChunk(); ++ // Paper start - chunk tick iteration optimisation ++ final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> tickingChunks = ++ ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)this.level).moonrise$getPlayerTickingChunks(); ++ ++ final ServerChunkCache.ChunkAndHolder[] raw = tickingChunks.getRawDataUnchecked(); ++ final int size = tickingChunks.size(); + +- if (chunk != null && this.level.isNaturalSpawningAllowed(playerchunk.getPos())) { +- chunks.add(chunk); ++ final ChunkMap chunkMap = this.chunkMap; ++ ++ for (int i = 0; i < size; ++i) { ++ final ServerChunkCache.ChunkAndHolder chunkAndHolder = raw[i]; ++ final LevelChunk levelChunk = chunkAndHolder.chunk(); ++ ++ if (!this.isChunkNearPlayer(chunkMap, levelChunk.getPos(), levelChunk)) { ++ continue; + } + +- }); ++ chunks.add(levelChunk); ++ } ++ // Paper end - chunk tick iteration optimisation + } + + private void tickChunks(ProfilerFiller profiler, long timeDelta, List<LevelChunk> chunks) { +@@ -508,7 +584,7 @@ public class ServerChunkCache extends ChunkSource { + NaturalSpawner.spawnForChunk(this.level, chunk, spawnercreature_d, list1); + } + +- if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { ++ if (true) { // Paper - rewrite chunk system + this.level.tickChunk(chunk, k); + } + } +@@ -521,11 +597,13 @@ public class ServerChunkCache extends ChunkSource { + } + + 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 ++ // note: bypass currentlyLoaded from getChunkNow ++ final LevelChunk fullChunk = this.fullChunks.get(pos); ++ if (fullChunk != null) { ++ chunkConsumer.accept(fullChunk); + } ++ // Paper end - rewrite chunk system + + } + +@@ -619,6 +697,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); + } +@@ -710,21 +794,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 b2fd3e936559c8fcb8b02ae3ef63c4f3bd0edb08..5bbc7ceaafc163f12344e5d5d355ad2ff30ddca2 100644 +--- a/src/main/java/net/minecraft/server/level/ServerEntity.java ++++ b/src/main/java/net/minecraft/server/level/ServerEntity.java +@@ -101,6 +101,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 c5f3e4dff83a85a3a5f29534afd84d4a332b1d3e..9928e14a5a42a2f0deba86e9dcb1f6f9f59412ef 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -185,7 +185,7 @@ import org.bukkit.event.weather.LightningStrikeEvent; + import org.bukkit.event.world.TimeSkipEvent; + // CraftBukkit end + +-public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLevel { ++public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader, ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel { // Paper - rewrite chunk system // Paper - chunk tick iteration + + public static final BlockPos END_SPAWN_POINT = new BlockPos(100, 50, 0); + public static final IntProvider RAIN_DELAY = UniformInt.of(12000, 180000); +@@ -201,7 +201,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + public final PrimaryLevelData serverLevelData; // CraftBukkit - type + private int lastSpawnChunkRadius; + final EntityTickList entityTickList = new EntityTickList(); +- public final PersistentEntitySectionManager<Entity> entityManager; ++ // Paper - rewrite chunk system + private final GameEventDispatcher gameEventDispatcher; + public boolean noSave; + private final SleepStatus sleepStatus; +@@ -272,12 +272,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.util.Priority priority, + java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) { +- if (Thread.currentThread() != this.thread) { +- this.getChunkSource().mainThreadProcessor.execute(() -> { +- this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad); +- }); +- return; +- } ++ // Paper - rewrite chunk system + int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; + int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; + +@@ -296,32 +291,159 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + public final void loadChunks(int minChunkX, int minChunkZ, int maxChunkX, int maxChunkZ, + ca.spottedleaf.concurrentutil.util.Priority priority, + java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) { +- List<net.minecraft.world.level.chunk.ChunkAccess> ret = new java.util.ArrayList<>(); +- it.unimi.dsi.fastutil.ints.IntArrayList ticketLevels = new it.unimi.dsi.fastutil.ints.IntArrayList(); +- ServerChunkCache chunkProvider = this.getChunkSource(); ++ this.moonrise$loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, priority, onLoad); // Paper - rewrite chunk system ++ } ++ // Paper end ++ ++ // Paper start - optimise getPlayerByUUID ++ @Nullable ++ @Override ++ public Player getPlayerByUUID(UUID uuid) { ++ final Player player = this.getServer().getPlayerList().getPlayer(uuid); ++ return player != null && player.level() == this ? player : null; ++ } ++ // Paper end - optimise getPlayerByUUID ++ // Paper start - rewrite chunk system ++ 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(); ++ } + +- int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1); +- int[] loadedChunks = new int[1]; ++ @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); ++ } + +- Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++); ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler moonrise$getChunkTaskScheduler() { ++ return this.chunkTaskScheduler; ++ } + +- java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> { ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController moonrise$getChunkDataController() { ++ return this.chunkDataController; ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController moonrise$getPoiChunkDataController() { ++ return this.poiDataController; ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController 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.util.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.util.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.util.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.util.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) { +- int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel()); +- ret.add(chunk); +- ticketLevels.add(ticketLevel); +- chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier); ++ synchronized (ret) { ++ ret.add(chunk); ++ } ++ chunkHolderManager.addTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunk.getPos(), ticketLevel, holderIdentifier); + } +- if (++loadedChunks[0] == requiredChunks) { ++ if (loadedChunks.incrementAndGet() == requiredChunks) { + try { + onLoad.accept(java.util.Collections.unmodifiableList(ret)); + } finally { + for (int i = 0, len = ret.size(); i < len; ++i) { +- ChunkPos chunkPos = ret.get(i).getPos(); +- int ticketLevel = ticketLevels.getInt(i); ++ final ChunkPos chunkPos = ret.get(i).getPos(); + +- chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); +- chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier); ++ chunkHolderManager.removeTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunkPos, ticketLevel, holderIdentifier); + } + } + } +@@ -329,22 +451,137 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { + for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { +- ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad( +- this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer +- ); ++ chunkTaskScheduler.scheduleChunkLoad(cx, cz, chunkStatus, true, priority, consumer); + } + } + } +- // Paper end + +- // Paper start - optimise getPlayerByUUID +- @Nullable + @Override +- public Player getPlayerByUUID(UUID uuid) { +- final Player player = this.getServer().getPlayerList().getPlayer(uuid); +- return player != null && player.level() == this ? player : null; ++ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() { ++ return this.viewDistanceHolder; + } +- // Paper end - optimise getPlayerByUUID ++ ++ @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; ++ } ++ ++ @Override ++ public final boolean moonrise$areChunksLoaded(final int fromX, final int fromZ, final int toX, final int toZ) { ++ final ServerChunkCache chunkSource = this.chunkSource; ++ ++ for (int currZ = fromZ; currZ <= toZ; ++currZ) { ++ for (int currX = fromX; currX <= toX; ++currX) { ++ if (!chunkSource.hasChunk(currX, currZ)) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ // Paper end - rewrite chunk system ++ // Paper start - chunk tick iteration ++ private static final ServerChunkCache.ChunkAndHolder[] EMPTY_PLAYER_CHUNK_HOLDERS = new ServerChunkCache.ChunkAndHolder[0]; ++ private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> playerTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_CHUNK_HOLDERS); ++ private final it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap playerTickingRequests = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(); ++ ++ @Override ++ public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getPlayerTickingChunks() { ++ return this.playerTickingChunks; ++ } ++ ++ @Override ++ public final void moonrise$markChunkForPlayerTicking(final LevelChunk chunk) { ++ final ChunkPos pos = chunk.getPos(); ++ if (!this.playerTickingRequests.containsKey(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos))) { ++ return; ++ } ++ ++ this.playerTickingChunks.add(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()); ++ } ++ ++ @Override ++ public final void moonrise$removeChunkForPlayerTicking(final LevelChunk chunk) { ++ this.playerTickingChunks.remove(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()); ++ } ++ ++ @Override ++ public final void moonrise$addPlayerTickingRequest(final int chunkX, final int chunkZ) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot add ticking request async"); ++ ++ final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ if (this.playerTickingRequests.addTo(chunkKey, 1) != 0) { ++ // already added ++ return; ++ } ++ ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler() ++ .chunkHolderManager.getChunkHolder(chunkKey); ++ ++ if (chunkHolder == null || !chunkHolder.isTickingReady()) { ++ return; ++ } ++ ++ this.playerTickingChunks.add( ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder() ++ ); ++ } ++ ++ @Override ++ public final void moonrise$removePlayerTickingRequest(final int chunkX, final int chunkZ) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot remove ticking request async"); ++ ++ final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final int val = this.playerTickingRequests.addTo(chunkKey, -1); ++ ++ if (val <= 0) { ++ throw new IllegalStateException("Negative counter"); ++ } ++ ++ if (val != 1) { ++ // still has at least one request ++ return; ++ } ++ ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler() ++ .chunkHolderManager.getChunkHolder(chunkKey); ++ ++ if (chunkHolder == null || !chunkHolder.isTickingReady()) { ++ return; ++ } ++ ++ this.playerTickingChunks.remove( ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder() ++ ); ++ } ++ // Paper end - chunk tick iteration + + // 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) { +@@ -378,14 +615,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + 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(); +@@ -413,6 +649,20 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + this.randomSequences = (RandomSequences) Objects.requireNonNullElseGet(randomsequences, () -> { + return (RandomSequences) this.getDataStorage().computeIfAbsent(RandomSequences.factory(l), "random_sequences"); + }); ++ // Paper start - rewrite chunk system ++ 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); ++ 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.chunkTaskScheduler ++ ); ++ this.poiDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.PoiDataController((ServerLevel)(Object)this, this.chunkTaskScheduler); ++ this.chunkDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController((ServerLevel)(Object)this, this.chunkTaskScheduler); ++ // Paper end - rewrite chunk system + this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit + } + +@@ -535,7 +785,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + gameprofilerfiller.push("checkDespawn"); + entity.checkDespawn(); + gameprofilerfiller.pop(); +- if (entity instanceof ServerPlayer || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { ++ if (true) { // Paper - rewrite chunk system + Entity entity1 = entity.getVehicle(); + + if (entity1 != null) { +@@ -558,13 +808,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + + 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() { +@@ -604,7 +857,60 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + }); + } + ++ // Paper start - optimise random ticking ++ private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); ++ ++ 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 ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; ++ final boolean doubleTickFluids = !ca.spottedleaf.moonrise.common.PlatformHooks.get().configFixMC224294(); ++ ++ 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.isRandomlyTickingBlocks()) { ++ continue; ++ } ++ ++ final ca.spottedleaf.moonrise.common.list.ShortList tickList = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getTickingBlockList(); ++ ++ for (int i = 0; i < tickSpeed; ++i) { ++ final int tickingBlocks = tickList.size(); ++ final int index = simpleRandom.nextInt() & ((16 * 16 * 16) - 1); ++ ++ if (index >= tickingBlocks) { ++ // most of the time we fall here ++ continue; ++ } ++ ++ final int location = (int)tickList.getRaw(index) & 0xFFFF; ++ final BlockState state = states.get(location); ++ ++ // do not use a mutable pos, as some random tick implementations store the input without calling immutable()! ++ final BlockPos pos = new BlockPos((location & 15) | offsetX, ((location >>> (4 + 4)) & 15) | offsetY, ((location >>> 4) & 15) | offsetZ); ++ ++ state.randomTick((ServerLevel)(Object)this, pos, simpleRandom); ++ if (doubleTickFluids) { ++ final FluidState fluidState = state.getFluidState(); ++ if (fluidState.isRandomlyTicking()) { ++ fluidState.randomTick((ServerLevel)(Object)this, pos, simpleRandom); ++ } ++ } ++ } ++ } ++ ++ return; ++ } ++ // Paper end - optimise random ticking ++ + public void tickChunk(LevelChunk chunk, int randomTickSpeed) { ++ final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking + ChunkPos chunkcoordintpair = chunk.getPos(); + boolean flag = this.isRaining(); + int j = chunkcoordintpair.getMinBlockX(); +@@ -612,7 +918,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + ProfilerFiller gameprofilerfiller = Profiler.get(); + + gameprofilerfiller.push("thunder"); +- if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder ++ if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && simpleRandom.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder // Paper - optimise random ticking + BlockPos blockposition = this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15)); + + if (this.isRainingAt(blockposition)) { +@@ -644,7 +950,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + if (!this.paperConfig().environment.disableIceAndSnow) { // Paper - Option to disable ice and snow + for (int l = 0; l < randomTickSpeed; ++l) { +- if (this.random.nextInt(48) == 0) { ++ if (simpleRandom.nextInt(48) == 0) { // Paper - optimise random ticking + this.tickPrecipitation(this.getBlockRandomPos(j, 0, k, 15)); + } + } +@@ -652,35 +958,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + gameprofilerfiller.popPush("tickBlocks"); + 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 + } + + gameprofilerfiller.pop(); +@@ -953,6 +1231,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + if (fluid1.is(fluid)) { + fluid1.tick(this, pos, iblockdata); + } ++ // 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 + + } + +@@ -962,6 +1245,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + 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 + + } + +@@ -1040,6 +1328,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + + 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) { +@@ -1053,14 +1346,19 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + progressListener.progressStage(Component.translatable("menu.savingChunks")); + } + +- chunkproviderserver.save(flush); +- if (flush) { +- this.entityManager.saveAll(); +- } else { +- this.entityManager.autoSave(); +- } ++ if (!close) { chunkproviderserver.save(flush); } // Paper - add close param ++ // 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; +@@ -1200,7 +1498,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + this.removePlayerImmediately((ServerPlayer) entity, Entity.RemovalReason.DISCARDED); + } + +- this.entityManager.addNewEntity(player); ++ this.moonrise$getEntityLookup().addNewEntity(player); // Paper - rewrite chunk system + } + + // CraftBukkit start +@@ -1231,7 +1529,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + // CraftBukkit end + +- return this.entityManager.addNewEntity(entity); ++ return this.moonrise$getEntityLookup().addNewEntity(entity); // Paper - rewrite chunk system + } + } + +@@ -1242,11 +1540,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + 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 +@@ -1906,7 +2200,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + } + +- 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())); +@@ -1955,7 +2249,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + BufferedWriter bufferedwriter2 = Files.newBufferedWriter(path1); + + try { +- playerchunkmap.dumpChunks(bufferedwriter2); ++ //playerchunkmap.dumpChunks(bufferedwriter2); // Paper - rewrite chunk system + } catch (Throwable throwable4) { + if (bufferedwriter2 != null) { + try { +@@ -1976,7 +2270,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + 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 { +@@ -2118,7 +2412,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + @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()); + } +@@ -2148,15 +2442,25 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + @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) { +@@ -2176,34 +2480,47 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + @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 +@@ -2259,7 +2576,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + 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 f6e130aba66935f808dcfd7ef987131092ddfe4e..2b6c5b2387b67f25d8877849ccbfaaa77eab51d3 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -218,7 +218,7 @@ import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; + 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; +@@ -324,6 +324,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 39d34f3728ae8d845d1bffc09f3ab8b64eb4d48b..3e82adf061bd0ec0100ca4d16ec9b157bddf99a7 100644 +--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java ++++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +@@ -22,23 +22,134 @@ 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 ConsecutiveExecutor consecutiveExecutor; +- private final ObjectList<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> lightTasks = new ObjectArrayList<>(); ++ // Paper - rewrite chunk sytem + private final ChunkMap chunkMap; +- private final ChunkTaskDispatcher taskDispatcher; ++ // 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 ++ 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, ChunkMap chunkLoadingManager, boolean hasBlockLight, ConsecutiveExecutor processor, ChunkTaskDispatcher executor + ) { + super(chunkProvider, true, hasBlockLight); + this.chunkMap = chunkLoadingManager; +- this.taskDispatcher = executor; +- this.consecutiveExecutor = processor; ++ // Paper - rewrite chunk sytem + } + + @Override +@@ -52,164 +163,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.getMinSectionY(); j <= this.levelHeightAccessor.getMaxSectionY(); 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.taskDispatcher.submit(() -> { +- 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.consecutiveExecutor.schedule(() -> { +- 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 b7d29389a357f142237cecd75f8ca91cf1eb6b5b..e4b0dc3121101d54394a0c3a413dabf8103b2ea6 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/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 88299abf563a041ade1683b66b43103b0eeeea0d..61f3ee42aaad1641c92df3eb60d699b9dd5679e3 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -1420,7 +1420,7 @@ public abstract class PlayerList { + + public void setViewDistance(int viewDistance) { + this.viewDistance = viewDistance; +- this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); ++ //this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); // Paper - rewrite chunk system + Iterator iterator = this.server.getAllLevels().iterator(); + + while (iterator.hasNext()) { +diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/net/minecraft/util/BitStorage.java +index 68648c5a5e3ff079f832092af0f2f801c42d1ede..e4e153cb8899e70273aa150b8ea26907cf68b15c 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.shorts.ShortArrayList> moonrise$countEntries() { ++ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(); ++ ++ final int size = this.getSize(); ++ for (int index = 0; index < size; ++index) { ++ final int paletteIdx = this.get(index); ++ ret.computeIfAbsent(paletteIdx, (final int key) -> { ++ return new it.unimi.dsi.fastutil.shorts.ShortArrayList(); ++ }).add((short)index); ++ } ++ ++ return ret; ++ } ++ // Paper end - block counting + } +diff --git a/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java b/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java +index 61dee55417bc802e25b9ba2f271d32d8c12844a9..a8a260a3caaa8e5004069b833ecc8b17b2fc8db5 100644 +--- a/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java ++++ b/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java +@@ -7,7 +7,7 @@ import java.util.Iterator; + import javax.annotation.Nullable; + import net.minecraft.core.IdMap; + +-public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K> { ++public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<K> { // Paper - optimise palette reads + private static final int NOT_FOUND = -1; + private static final Object EMPTY_SLOT = null; + private static final float LOADFACTOR = 0.8F; +@@ -17,6 +17,16 @@ public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K> { + private int nextId; + private int size; + ++ // Paper start - optimise palette reads ++ private ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<K> reference; ++ ++ @Override ++ public final K[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<K> src) { ++ this.reference = src; ++ return this.byId; ++ } ++ // Paper end - optimise palette reads ++ + private CrudeIncrementalIntIdentityHashBiMap(int size) { + this.keys = (K[])(new Object[size]); + this.values = new int[size]; +@@ -88,6 +98,12 @@ public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K> { + this.byId = crudeIncrementalIntIdentityHashBiMap.byId; + this.nextId = crudeIncrementalIntIdentityHashBiMap.nextId; + this.size = crudeIncrementalIntIdentityHashBiMap.size; ++ // Paper start - optimise palette reads ++ final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<K> ref = this.reference; ++ if (ref != null) { ++ ref.moonrise$setPalette(this.byId); ++ } ++ // Paper end - optimise palette reads + } + + public void addMapping(K value, int id) { +diff --git a/src/main/java/net/minecraft/util/SimpleBitStorage.java b/src/main/java/net/minecraft/util/SimpleBitStorage.java +index 9f438d9c6eb05e43d24e4af68188a3d4c46a938c..d99ec470b4653beab630999a5b2c1a6428b20c38 100644 +--- a/src/main/java/net/minecraft/util/SimpleBitStorage.java ++++ b/src/main/java/net/minecraft/util/SimpleBitStorage.java +@@ -208,6 +208,20 @@ public class SimpleBitStorage implements BitStorage { + private final int divideAdd; private final long divideAddUnsigned; // Paper - Perf: Optimize SimpleBitStorage + private final int divideShift; + ++ // Paper start - optimise bitstorage read/write operations ++ private static final int[] BETTER_MAGIC = new int[33]; ++ static { ++ // 20 bits of precision ++ // since index is always [0, 4095] (i.e 12 bits), multiplication by a magic value here (20 bits) ++ // fits exactly in an int and allows us to use integer arithmetic ++ for (int bits = 1; bits < BETTER_MAGIC.length; ++bits) { ++ BETTER_MAGIC[bits] = (int)ca.spottedleaf.concurrentutil.util.IntegerUtil.getUnsignedDivisorMagic(64L / bits, 20); ++ } ++ } ++ private final int magic; ++ private final int mulBits; ++ // Paper end - optimise bitstorage read/write operations ++ + public SimpleBitStorage(int elementBits, int size, int[] data) { + this(elementBits, size); + int i = 0; +@@ -261,6 +275,13 @@ public class SimpleBitStorage implements BitStorage { + } else { + this.data = new long[j]; + } ++ // Paper start - optimise bitstorage read/write operations ++ this.magic = BETTER_MAGIC[this.bits]; ++ this.mulBits = (64 / this.bits) * this.bits; ++ if (this.size > 4096) { ++ throw new IllegalStateException("Size > 4096 not supported"); ++ } ++ // Paper end - optimise bitstorage read/write operations + } + + private int cellIndex(int index) { +@@ -273,31 +294,54 @@ public class SimpleBitStorage implements BitStorage { + public final int getAndSet(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage + //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage + //Validate.inclusiveBetween(0L, this.mask, (long)value); // Paper - Perf: Optimize SimpleBitStorage +- int i = this.cellIndex(index); +- long l = this.data[i]; +- int j = (index - i * this.valuesPerLong) * this.bits; +- int k = (int)(l >> j & this.mask); +- this.data[i] = l & ~(this.mask << j) | ((long)value & this.mask) << j; +- return k; ++ // Paper start - optimise bitstorage read/write operations ++ final int full = this.magic * index; // 20 bits of magic + 12 bits of index = barely int ++ final int divQ = full >>> 20; ++ final int divR = (full & 0xFFFFF) * this.mulBits >>> 20; ++ ++ final long[] dataArray = this.data; ++ ++ final long data = dataArray[divQ]; ++ final long mask = this.mask; ++ ++ final long write = data & ~(mask << divR) | ((long)value & mask) << divR; ++ ++ dataArray[divQ] = write; ++ ++ return (int)(data >>> divR & mask); ++ // Paper end - optimise bitstorage read/write operations + } + + @Override + public final void set(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage + //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage + //Validate.inclusiveBetween(0L, this.mask, (long)value); // Paper - Perf: Optimize SimpleBitStorage +- int i = this.cellIndex(index); +- long l = this.data[i]; +- int j = (index - i * this.valuesPerLong) * this.bits; +- this.data[i] = l & ~(this.mask << j) | ((long)value & this.mask) << j; ++ // Paper start - optimise bitstorage read/write operations ++ final int full = this.magic * index; // 20 bits of magic + 12 bits of index = barely int ++ final int divQ = full >>> 20; ++ final int divR = (full & 0xFFFFF) * this.mulBits >>> 20; ++ ++ final long[] dataArray = this.data; ++ ++ final long data = dataArray[divQ]; ++ final long mask = this.mask; ++ ++ final long write = data & ~(mask << divR) | ((long)value & mask) << divR; ++ ++ dataArray[divQ] = write; ++ // Paper end - optimise bitstorage read/write operations + } + + @Override + public final int get(int index) { // Paper - Perf: Optimize SimpleBitStorage + //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage +- int i = this.cellIndex(index); +- long l = this.data[i]; +- int j = (index - i * this.valuesPerLong) * this.bits; +- return (int)(l >> j & this.mask); ++ // Paper start - optimise bitstorage read/write operations ++ final int full = this.magic * index; // 20 bits of magic + 12 bits of index = barely int ++ final int divQ = full >>> 20; ++ final int divR = (full & 0xFFFFF) * this.mulBits >>> 20; ++ ++ return (int)(this.data[divQ] >>> divR & this.mask); ++ // Paper end - optimise bitstorage read/write operations + } + + @Override +@@ -362,6 +406,67 @@ 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.shorts.ShortArrayList> moonrise$countEntries() { ++ final int valuesPerLong = this.valuesPerLong; ++ final int bits = this.bits; ++ final long mask = (1L << bits) - 1L; ++ final int size = this.size; ++ ++ if (bits <= 6) { ++ final it.unimi.dsi.fastutil.shorts.ShortArrayList[] byId = new it.unimi.dsi.fastutil.shorts.ShortArrayList[1 << bits]; ++ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1 << bits); ++ ++ int index = 0; ++ ++ for (long value : this.data) { ++ int li = 0; ++ do { ++ final int paletteIdx = (int)(value & mask); ++ value >>= bits; ++ ++li; ++ ++ final it.unimi.dsi.fastutil.shorts.ShortArrayList coords = byId[paletteIdx]; ++ if (coords != null) { ++ coords.add((short)index++); ++ continue; ++ } else { ++ final it.unimi.dsi.fastutil.shorts.ShortArrayList newCoords = new it.unimi.dsi.fastutil.shorts.ShortArrayList(64); ++ byId[paletteIdx] = newCoords; ++ newCoords.add((short)index++); ++ ret.put(paletteIdx, newCoords); ++ continue; ++ } ++ } while (li < valuesPerLong && index < size); ++ } ++ ++ return ret; ++ } else { ++ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>( ++ 1 << 6 ++ ); ++ ++ int index = 0; ++ ++ for (long value : this.data) { ++ int li = 0; ++ do { ++ final int paletteIdx = (int)(value & mask); ++ value >>= bits; ++ ++li; ++ ++ ret.computeIfAbsent(paletteIdx, (final int key) -> { ++ return new it.unimi.dsi.fastutil.shorts.ShortArrayList(64); ++ }).add((short)index++); ++ } while (li < valuesPerLong && index < size); ++ } ++ ++ 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..1f9c436a632e4f110be61cf76fcfc3b7eb80334e 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.shorts.ShortArrayList> moonrise$countEntries() { ++ final int size = this.size; ++ ++ final short[] raw = new short[size]; ++ for (int i = 0; i < size; ++i) { ++ raw[i] = (short)i; ++ } ++ ++ final it.unimi.dsi.fastutil.shorts.ShortArrayList coordinates = it.unimi.dsi.fastutil.shorts.ShortArrayList.wrap(raw, size); ++ ++ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> 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 5949cbccb569ab1d518508d200e69ad9d7d0ba9a..b810f887e536af938f978ca2af068e6ae89b5e60 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -176,7 +176,7 @@ import org.bukkit.event.player.PlayerTeleportEvent; + import org.bukkit.plugin.PluginManager; + // CraftBukkit end + +-public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, ScoreHolder { ++public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, 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; +@@ -187,7 +187,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + + // Paper start - Share random for entities to make them more random + public static RandomSource SHARED_RANDOM = new RandomRandomSource(); +- private static final class RandomRandomSource extends java.util.Random implements net.minecraft.world.level.levelgen.BitRandomSource { ++ // Paper start - replace random ++ private static final class RandomRandomSource extends ca.spottedleaf.moonrise.common.util.ThreadUnsafeRandom { ++ public RandomRandomSource() { ++ this(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); ++ } ++ ++ public RandomRandomSource(long seed) { ++ super(seed); ++ } ++ ++ // Paper end - replace random + private boolean locked = false; + + @Override +@@ -200,61 +210,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + } + +- @Override +- public RandomSource fork() { +- return new net.minecraft.world.level.levelgen.LegacyRandomSource(this.nextLong()); +- } +- +- @Override +- public net.minecraft.world.level.levelgen.PositionalRandomFactory forkPositional() { +- return new net.minecraft.world.level.levelgen.LegacyRandomSource.LegacyPositionalRandomFactory(this.nextLong()); +- } +- +- // these below are added to fix reobf issues that I don't wanna deal with right now +- @Override +- public int next(int bits) { +- return super.next(bits); +- } +- +- @Override +- public int nextInt(int origin, int bound) { +- return net.minecraft.world.level.levelgen.BitRandomSource.super.nextInt(origin, bound); +- } +- +- @Override +- public long nextLong() { +- return net.minecraft.world.level.levelgen.BitRandomSource.super.nextLong(); +- } +- +- @Override +- public int nextInt() { +- return net.minecraft.world.level.levelgen.BitRandomSource.super.nextInt(); +- } +- +- @Override +- public int nextInt(int bound) { +- return net.minecraft.world.level.levelgen.BitRandomSource.super.nextInt(bound); +- } +- +- @Override +- public boolean nextBoolean() { +- return net.minecraft.world.level.levelgen.BitRandomSource.super.nextBoolean(); +- } +- +- @Override +- public float nextFloat() { +- return net.minecraft.world.level.levelgen.BitRandomSource.super.nextFloat(); +- } +- +- @Override +- public double nextDouble() { +- return net.minecraft.world.level.levelgen.BitRandomSource.super.nextDouble(); +- } +- +- @Override +- public double nextGaussian() { +- return super.nextGaussian(); +- } ++ // Paper - replace random + } + // Paper end - Share random for entities to make them more random + public org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; // Paper - Entity#getEntitySpawnReason +@@ -461,6 +417,156 @@ 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 ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData; ++ private int sectionX = Integer.MIN_VALUE; ++ private int sectionY = Integer.MIN_VALUE; ++ private int sectionZ = Integer.MIN_VALUE; ++ 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 ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$getChunkData() { ++ return this.chunkData; ++ } ++ ++ @Override ++ public final void moonrise$setChunkData(final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData) { ++ this.chunkData = chunkData; ++ } ++ ++ @Override ++ public final int moonrise$getSectionX() { ++ return this.sectionX; ++ } ++ ++ @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 collisions ++ private static float[] calculateStepHeights(final AABB box, final List<VoxelShape> voxels, final List<AABB> aabbs, final float stepHeight, ++ final float collidedY) { ++ final FloatArraySet ret = new FloatArraySet(); ++ ++ for (int i = 0, len = voxels.size(); i < len; ++i) { ++ final VoxelShape shape = voxels.get(i); ++ ++ final double[] yCoords = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$rootCoordinatesY(); ++ final double yOffset = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$offsetY(); ++ ++ for (final double yUnoffset : yCoords) { ++ final double y = yUnoffset + yOffset; ++ ++ final float step = (float)(y - box.minY); ++ ++ if (step > stepHeight) { ++ break; ++ } ++ ++ if (step < 0.0f || !(step != collidedY)) { ++ continue; ++ } ++ ++ ret.add(step); ++ } ++ } ++ ++ for (int i = 0, len = aabbs.size(); i < len; ++i) { ++ final AABB shape = aabbs.get(i); ++ ++ final float step1 = (float)(shape.minY - box.minY); ++ final float step2 = (float)(shape.maxY - box.minY); ++ ++ if (!(step1 < 0.0f) && step1 != collidedY && !(step1 > stepHeight)) { ++ ret.add(step1); ++ } ++ ++ if (!(step2 < 0.0f) && step2 != collidedY && !(step2 > stepHeight)) { ++ ret.add(step2); ++ } ++ } ++ ++ final float[] steps = ret.toFloatArray(); ++ FloatArrays.unstableSort(steps); ++ return steps; ++ } ++ // Paper end - optimise collisions ++ // Paper start - optimise entity tracker ++ private net.minecraft.server.level.ChunkMap.TrackedEntity trackedEntity; ++ ++ @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(); +@@ -1366,41 +1472,76 @@ 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; ++ } ++ ++ final AABB currentBox = this.getBoundingBox(); ++ ++ final List<VoxelShape> potentialCollisionsVoxel = new ArrayList<>(); ++ final List<AABB> potentialCollisionsBB = new ArrayList<>(); + +- 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 AABB initialCollisionBox; ++ if (xZero & zZero) { ++ // note: xZero & zZero -> collision on x/z == 0 -> no step height calculation ++ // this specifically optimises entities standing still ++ initialCollisionBox = movement.y < 0.0 ? ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutDownwards(currentBox, movement.y) : ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutUpwards(currentBox, movement.y); ++ } else { ++ initialCollisionBox = currentBox.expandTowards(movement); ++ } + +- 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); ++ final List<AABB> entityAABBs = new ArrayList<>(); ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getEntityHardCollisions( ++ this.level, (Entity)(Object)this, initialCollisionBox, entityAABBs, 0, null ++ ); + +- if (vec3d2.horizontalDistanceSqr() > vec3d1.horizontalDistanceSqr()) { +- double d0 = axisalignedbb.minY - axisalignedbb1.minY; ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder( ++ this.level, (Entity)(Object)this, initialCollisionBox, potentialCollisionsVoxel, potentialCollisionsBB, ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, null ++ ); ++ potentialCollisionsBB.addAll(entityAABBs); ++ final Vec3 collided = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(movement, currentBox, potentialCollisionsVoxel, potentialCollisionsBB); + +- return vec3d2.add(0.0D, -d0, 0.0D); +- } ++ final boolean collidedX = collided.x != movement.x; ++ final boolean collidedY = collided.y != movement.y; ++ final boolean collidedZ = collided.z != movement.z; ++ ++ final boolean collidedDownwards = collidedY && movement.y < 0.0; ++ ++ final double stepHeight; ++ ++ if ((!collidedDownwards && !this.onGround) || (!collidedX && !collidedZ) || (stepHeight = (double)this.maxUpStep()) <= 0.0) { ++ return collided; ++ } ++ ++ final AABB collidedYBox = collidedDownwards ? currentBox.move(0.0, collided.y, 0.0) : currentBox; ++ AABB stepRetrievalBox = collidedYBox.expandTowards(movement.x, stepHeight, movement.z); ++ if (!collidedDownwards) { ++ stepRetrievalBox = stepRetrievalBox.expandTowards(0.0, (double)-1.0E-5F, 0.0); ++ } ++ ++ final List<VoxelShape> stepVoxels = new ArrayList<>(); ++ final List<AABB> stepAABBs = entityAABBs; ++ ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder( ++ this.level, (Entity)(Object)this, stepRetrievalBox, stepVoxels, stepAABBs, ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, null ++ ); ++ ++ for (final float step : calculateStepHeights(collidedYBox, stepVoxels, stepAABBs, (float)stepHeight, (float)collided.y)) { ++ final Vec3 stepResult = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(movement.x, (double)step, movement.z), collidedYBox, stepVoxels, stepAABBs); ++ if (stepResult.horizontalDistanceSqr() > collided.horizontalDistanceSqr()) { ++ return stepResult.add(0.0, collidedYBox.minY - currentBox.minY, 0.0); + } + } + +- return vec3d1; ++ return collided; ++ // Paper end - optimise collisions + } + + private static float[] collectCandidateStepUpHeights(AABB collisionBox, List<VoxelShape> collisions, float f, float stepHeight) { +@@ -2788,18 +2929,110 @@ 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 double reducedWith = (double)(this.dimensions.width() * 0.8F); ++ final AABB boundingBox = AABB.ofSize(this.getEyePosition(), reducedWith, 1.0E-6D, reducedWith); ++ final Level world = this.level; + +- 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); +- }); ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(boundingBox)) { ++ return false; + } ++ ++ final int minBlockX = Mth.floor(boundingBox.minX); ++ final int minBlockY = Mth.floor(boundingBox.minY); ++ final int minBlockZ = Mth.floor(boundingBox.minZ); ++ ++ final int maxBlockX = Mth.floor(boundingBox.maxX); ++ final int maxBlockY = Mth.floor(boundingBox.maxY); ++ final int maxBlockZ = Mth.floor(boundingBox.maxZ); ++ ++ final int minChunkX = minBlockX >> 4; ++ final int minChunkY = minBlockY >> 4; ++ final int minChunkZ = minBlockZ >> 4; ++ ++ final int maxChunkX = maxBlockX >> 4; ++ final int maxChunkY = maxBlockY >> 4; ++ final int maxChunkZ = maxBlockZ >> 4; ++ ++ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(world); ++ final net.minecraft.world.level.chunk.ChunkSource chunkSource = world.getChunkSource(); ++ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); ++ ++ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { ++ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { ++ final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true).getSections(); ++ ++ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { ++ final int sectionIdx = currChunkY - minSection; ++ if (sectionIdx < 0 || sectionIdx >= sections.length) { ++ continue; ++ } ++ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx]; ++ if (section.hasOnlyAir()) { ++ // empty ++ continue; ++ } ++ ++ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> blocks = section.states; ++ ++ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) : 0; ++ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) : 15; ++ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) : 0; ++ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) : 15; ++ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) : 0; ++ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) : 15; ++ ++ for (int currY = minYIterate; currY <= maxYIterate; ++currY) { ++ final int blockY = currY | (currChunkY << 4); ++ mutablePos.setY(blockY); ++ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { ++ final int blockZ = currZ | (currChunkZ << 4); ++ mutablePos.setZ(blockZ); ++ for (int currX = minXIterate; currX <= maxXIterate; ++currX) { ++ final int blockX = currX | (currChunkX << 4); ++ mutablePos.setX(blockX); ++ ++ final BlockState blockState = blocks.get((currX) | (currZ << 4) | ((currY) << 8)); ++ ++ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$emptyCollisionShape() ++ || !blockState.isSuffocating(world, mutablePos)) { ++ continue; ++ } ++ ++ // Yes, it does not use the Entity context stuff. ++ final VoxelShape collisionShape = blockState.getCollisionShape(world, mutablePos); ++ ++ if (collisionShape.isEmpty()) { ++ continue; ++ } ++ ++ final AABB toCollide = boundingBox.move(-(double)blockX, -(double)blockY, -(double)blockZ); ++ ++ final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)collisionShape).moonrise$getSingleAABBRepresentation(); ++ if (singleAABB != null) { ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(singleAABB, toCollide)) { ++ return true; ++ } ++ continue; ++ } ++ ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(collisionShape, toCollide)) { ++ return true; ++ } ++ continue; ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ return false; ++ // Paper end - optimise collisions + } + + public InteractionResult interact(Player player, InteractionHand hand) { +@@ -4272,14 +4505,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 +@@ -4437,82 +4673,136 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return Mth.lerp(delta, this.yRotO, this.yRot); + } + +- public boolean updateFluidHeightAndDoFluidPushing(TagKey<Fluid> tag, double speed) { ++ // Paper start - optimise collisions ++ public boolean updateFluidHeightAndDoFluidPushing(final TagKey<Fluid> fluid, final double flowScale) { + if (this.touchingUnloadedChunk()) { + return false; +- } else { +- AABB axisalignedbb = this.getBoundingBox().deflate(0.001D); +- int i = Mth.floor(axisalignedbb.minX); +- int j = Mth.ceil(axisalignedbb.maxX); +- int k = Mth.floor(axisalignedbb.minY); +- int l = Mth.ceil(axisalignedbb.maxY); +- int i1 = Mth.floor(axisalignedbb.minZ); +- int j1 = Mth.ceil(axisalignedbb.maxZ); +- double d1 = 0.0D; +- boolean flag = this.isPushedByFluid(); +- boolean flag1 = false; +- Vec3 vec3d = Vec3.ZERO; +- int k1 = 0; +- BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos(); +- +- for (int l1 = i; l1 < j; ++l1) { +- for (int i2 = k; i2 < l; ++i2) { +- for (int j2 = i1; j2 < j1; ++j2) { +- blockposition_mutableblockposition.set(l1, i2, j2); +- FluidState fluid = this.level().getFluidState(blockposition_mutableblockposition); +- +- if (fluid.is(tag)) { +- double d2 = (double) ((float) i2 + fluid.getHeight(this.level(), blockposition_mutableblockposition)); +- +- if (d2 >= axisalignedbb.minY) { +- flag1 = true; +- d1 = Math.max(d2 - axisalignedbb.minY, d1); +- if (flag) { +- Vec3 vec3d1 = fluid.getFlow(this.level(), blockposition_mutableblockposition); +- +- if (d1 < 0.4D) { +- vec3d1 = vec3d1.scale(d1); +- } ++ } ++ ++ final AABB boundingBox = this.getBoundingBox().deflate(1.0E-3); ++ ++ final Level world = this.level; ++ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(world); ++ ++ final int minBlockX = Mth.floor(boundingBox.minX); ++ final int minBlockY = Math.max((minSection << 4), Mth.floor(boundingBox.minY)); ++ final int minBlockZ = Mth.floor(boundingBox.minZ); ++ ++ // note: bounds are exclusive in Vanilla, so we subtract 1 - our loop expects bounds to be inclusive ++ final int maxBlockX = Mth.ceil(boundingBox.maxX) - 1; ++ final int maxBlockY = Math.min((ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(world) << 4) | 15, Mth.ceil(boundingBox.maxY) - 1); ++ final int maxBlockZ = Mth.ceil(boundingBox.maxZ) - 1; ++ ++ final boolean isPushable = this.isPushedByFluid(); ++ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); ++ ++ Vec3 pushVector = Vec3.ZERO; ++ double totalPushes = 0.0; ++ double maxHeightDiff = 0.0; ++ boolean inFluid = false; ++ ++ final int minChunkX = minBlockX >> 4; ++ final int maxChunkX = maxBlockX >> 4; ++ ++ final int minChunkY = minBlockY >> 4; ++ final int maxChunkY = maxBlockY >> 4; ++ ++ final int minChunkZ = minBlockZ >> 4; ++ final int maxChunkZ = maxBlockZ >> 4; ++ ++ final net.minecraft.world.level.chunk.ChunkSource chunkSource = world.getChunkSource(); ++ ++ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { ++ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { ++ final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, false).getSections(); ++ ++ // bound y ++ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { ++ final int sectionIdx = currChunkY - minSection; ++ if (sectionIdx < 0 || sectionIdx >= sections.length) { ++ continue; ++ } ++ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx]; ++ if (section.hasOnlyAir()) { ++ // empty ++ continue; ++ } ++ ++ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> blocks = section.states; ++ ++ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) : 0; ++ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) : 15; ++ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) : 0; ++ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) : 15; ++ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) : 0; ++ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) : 15; + +- vec3d = vec3d.add(vec3d1); +- ++k1; ++ for (int currY = minYIterate; currY <= maxYIterate; ++currY) { ++ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { ++ for (int currX = minXIterate; currX <= maxXIterate; ++currX) { ++ final FluidState fluidState = blocks.get((currX) | (currZ << 4) | ((currY) << 8)).getFluidState(); ++ ++ if (fluidState.isEmpty() || !fluidState.is(fluid)) { ++ continue; + } +- // CraftBukkit start - store last lava contact location +- if (tag == FluidTags.LAVA) { +- this.lastLavaContact = blockposition_mutableblockposition.immutable(); ++ ++ mutablePos.set(currX | (currChunkX << 4), currY | (currChunkY << 4), currZ | (currChunkZ << 4)); ++ ++ final double height = (double)((float)mutablePos.getY() + fluidState.getHeight(world, mutablePos)); ++ final double diff = height - boundingBox.minY; ++ ++ if (diff < 0.0) { ++ continue; ++ } ++ ++ inFluid = true; ++ maxHeightDiff = Math.max(maxHeightDiff, diff); ++ ++ if (!isPushable) { ++ continue; ++ } ++ ++ ++totalPushes; ++ ++ final Vec3 flow = fluidState.getFlow(world, mutablePos); ++ ++ if (diff < 0.4) { ++ pushVector = pushVector.add(flow.scale(diff)); ++ } else { ++ pushVector = pushVector.add(flow); + } +- // CraftBukkit end + } + } + } + } + } ++ } + +- if (vec3d.length() > 0.0D) { +- if (k1 > 0) { +- vec3d = vec3d.scale(1.0D / (double) k1); +- } ++ this.fluidHeight.put(fluid, maxHeightDiff); + +- if (!(this instanceof Player)) { +- vec3d = vec3d.normalize(); +- } ++ if (pushVector.lengthSqr() == 0.0) { ++ return inFluid; ++ } + +- Vec3 vec3d2 = this.getDeltaMovement(); ++ // note: totalPushes != 0 as pushVector != 0 ++ pushVector = pushVector.scale(1.0 / totalPushes); ++ final Vec3 currMovement = this.getDeltaMovement(); + +- vec3d = vec3d.scale(speed); +- double d3 = 0.003D; ++ if (!((Entity)(Object)this instanceof Player)) { ++ pushVector = pushVector.normalize(); ++ } + +- if (Math.abs(vec3d2.x) < 0.003D && Math.abs(vec3d2.z) < 0.003D && vec3d.length() < 0.0045000000000000005D) { +- vec3d = vec3d.normalize().scale(0.0045000000000000005D); +- } ++ pushVector = pushVector.scale(flowScale); ++ if (Math.abs(currMovement.x) < 0.003 && Math.abs(currMovement.z) < 0.003 && pushVector.length() < 0.0045000000000000005) { ++ pushVector = pushVector.normalize().scale(0.0045000000000000005); ++ } + +- this.setDeltaMovement(this.getDeltaMovement().add(vec3d)); +- } ++ this.setDeltaMovement(currMovement.add(pushVector)); + +- this.fluidHeight.put(tag, d1); +- return flag1; +- } ++ // note: inFluid = true here as pushVector != 0 ++ return true; + } ++ // Paper end - optimise collisions + + public boolean touchingUnloadedChunk() { + AABB axisalignedbb = this.getBoundingBox().inflate(1.0D); +@@ -4664,6 +4954,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; + } +@@ -4793,6 +5092,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 +@@ -4804,7 +5109,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); + this.onRemoval(entity_removalreason); + // Paper start - Folia schedulers +@@ -4836,7 +5141,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 96bc0ba60195e5e666d47b3a0b943b733986d96a..5930a430983061afddf20e3208ff2462ca1b78cd 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,137 @@ 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, PoiSection.Packed> { ++public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> 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.level.poi.PoiChunk ret = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getPoiChunkIfLoaded(chunkX, chunkZ, true); ++ ++ return ret == null ? Optional.empty() : ret.getSectionForVanilla(chunkY); ++ } ++ ++ @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); ++ ++ final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world); ++ final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world); ++ ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Unloading poi chunk off-main"); ++ for (int sectionY = minY; sectionY <= maxY; ++sectionY) { ++ final long sectionPos = SectionPos.asLong(chunkX, sectionY, 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; ++ ++ final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world); ++ final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world); ++ ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Loading poi chunk off-main"); ++ for (int sectionY = minY; sectionY <= maxY; ++sectionY) { ++ final PoiSection section = poiChunk.getSection(sectionY); ++ if (section != null && !((ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection)section).moonrise$isEmpty()) { ++ this.onSectionLoad(SectionPos.asLong(chunkX, sectionY, chunkZ)); ++ } ++ } ++ } ++ ++ @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]); ++ } ++ } ++ // Paper end - rewrite chunk system ++ + public PoiManager( + RegionStorageInfo storageKey, + Path directory, +@@ -64,6 +189,7 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> { + 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) { +@@ -197,8 +323,10 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> { + } + + public int sectionsToVillage(SectionPos pos) { +- this.distanceTracker.runAllUpdates(); +- return this.distanceTracker.getLevel(pos.asLong()); ++ // Paper start - rewrite chunk system ++ this.villageDistanceTracker.propagateUpdates(); ++ return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(pos))); ++ // Paper end - rewrite chunk system + } + + boolean isVillageCenter(long pos) { +@@ -212,19 +340,26 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> { + + @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) { +@@ -263,7 +398,7 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> { + .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 b9e0bc8f1e948614d986335de1f3d2df199eea81..712cbfc100e8aaf612d1d651dae64f57f892a768 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,13 +23,27 @@ 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(); + private final Runnable setDirty; + private boolean isValid; + ++ // 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 63f02cdc67d9e88cc6998d0ae9d139c83e85b447..70b8023c3badc745f342d5b0ab54699e3923826a 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java +@@ -364,7 +364,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 e185a33b5b1f8e8e0a0e666b24ba3e9186a8a7ff..5d7a6e4b73f032db356e7ec369b150013e940ee6 100644 +--- a/src/main/java/net/minecraft/world/level/EntityGetter.java ++++ b/src/main/java/net/minecraft/world/level/EntityGetter.java +@@ -15,7 +15,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); +@@ -30,21 +30,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) { +@@ -52,23 +75,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/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 078088a854d466e66411d25d6dd6bcc536db78f3..061d3e77fe8d9322eb660ac1995e025aba51ae1a 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -83,6 +83,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 +@@ -104,7 +105,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 { // 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")); +@@ -130,7 +131,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public float rainLevel; + protected float oThunderLevel; + public float thunderLevel; +- public final RandomSource random = RandomSource.create(); ++ public final RandomSource random = new ca.spottedleaf.moonrise.common.util.ThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); // Paper - replace random + /** @deprecated */ + @Deprecated + private final RandomSource threadSafeRandom = RandomSource.createThreadSafe(); +@@ -206,7 +207,639 @@ 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; ++ private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData> chunkData = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>(); ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup moonrise$getEntityLookup() { ++ return this.entityLookup; ++ } ++ ++ @Override ++ public final 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) { ++ Profiler.get().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) { ++ Profiler.get().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 (LevelChunk)this.getChunkSource().getChunk(chunkX, chunkZ, ChunkStatus.FULL, 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 ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$getChunkData(final long chunkKey) { ++ return this.chunkData.get(chunkKey); ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$getChunkData(final int chunkX, final int chunkZ) { ++ return this.chunkData.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$requestChunkData(final long chunkKey) { ++ return this.chunkData.compute(chunkKey, (final long keyInMap, final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData valueInMap) -> { ++ if (valueInMap == null) { ++ final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData ret = new ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData(); ++ ret.increaseRef(); ++ return ret; ++ } ++ ++ valueInMap.increaseRef(); ++ return valueInMap; ++ }); ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$releaseChunkData(final long chunkKey) { ++ return this.chunkData.compute(chunkKey, (final long keyInMap, final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData) -> { ++ return chunkData.decreaseRef() == 0 ? null : chunkData; ++ }); ++ } ++ ++ @Override ++ public boolean moonrise$areChunksLoaded(final int fromX, final int fromZ, final int toX, final int toZ) { ++ final ChunkSource chunkSource = this.getChunkSource(); ++ ++ for (int currZ = fromZ; currZ <= toZ; ++currZ) { ++ for (int currX = fromX; currX <= toX; ++currX) { ++ if (!chunkSource.hasChunk(currX, currZ)) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ @Override ++ public boolean hasChunksAt(final int minBlockX, final int minBlockZ, final int maxBlockX, final int maxBlockZ) { ++ return this.moonrise$areChunksLoaded( ++ minBlockX >> 4, minBlockZ >> 4, maxBlockX >> 4, maxBlockZ >> 4 ++ ); ++ } ++ ++ /** ++ * @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 ++ /** ++ * Route to faster lookup. ++ * See {@link EntityGetter#isUnobstructed(Entity, VoxelShape)} for expected behavior ++ * @author Spottedleaf ++ */ ++ @Override ++ public 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.getApproximateNearest(from.x - to.x, from.y - to.y, from.z - to.z), BlockPos.containing(to.x, to.y, to.z)); ++ } ++ ++ private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState(); ++ ++ 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.common.util.WorldUtil.getMinSection(level); ++ ++ 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 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 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 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 java.util.Optional<net.minecraft.core.BlockPos> findSupportingBlock(final Entity entity, final AABB aabb) { ++ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection((Level)(Object)this); ++ ++ final int minBlockX = Mth.floor(aabb.minX - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1; ++ final int maxBlockX = Mth.floor(aabb.maxX + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1; ++ ++ final int minBlockY = Math.max((minSection << 4) - 1, Mth.floor(aabb.minY - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1); ++ final int maxBlockY = Math.min((ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection((Level)(Object)this) << 4) + 16, Mth.floor(aabb.maxY + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1); ++ ++ final int minBlockZ = Mth.floor(aabb.minZ - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1; ++ final int maxBlockZ = Mth.floor(aabb.maxZ + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1; ++ ++ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); ++ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext collisionShape = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(entity); ++ BlockPos selected = null; ++ double selectedDistance = Double.MAX_VALUE; ++ final Vec3 entityPos = entity.position(); ++ ++ // special cases: ++ if (minBlockY > maxBlockY) { ++ // no point in checking ++ return java.util.Optional.empty(); ++ } ++ ++ 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 ChunkSource chunkSource = this.getChunkSource(); ++ ++ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { ++ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { ++ final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, false); ++ ++ if (chunk == null) { ++ 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.hasOnlyAir()) { ++ // empty ++ continue; ++ } ++ ++ final boolean hasSpecial = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$hasSpecialCollidingBlocks(); ++ final int sectionAdjust = !hasSpecial ? 1 : 0; ++ ++ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> blocks = section.states; ++ ++ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0; ++ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15; ++ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) + sectionAdjust : 0; ++ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) - sectionAdjust : 15; ++ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) + sectionAdjust : 0; ++ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) - sectionAdjust : 15; ++ ++ for (int currY = minYIterate; currY <= maxYIterate; ++currY) { ++ final int blockY = currY | (currChunkY << 4); ++ mutablePos.setY(blockY); ++ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { ++ final int blockZ = currZ | (currChunkZ << 4); ++ mutablePos.setZ(blockZ); ++ for (int currX = minXIterate; currX <= maxXIterate; ++currX) { ++ final int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8); ++ final int blockX = currX | (currChunkX << 4); ++ mutablePos.setX(blockX); ++ ++ final int edgeCount = hasSpecial ? ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + ++ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + ++ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0) : 0; ++ if (edgeCount == 3) { ++ continue; ++ } ++ ++ final double distance = mutablePos.distToCenterSqr(entityPos); ++ if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(mutablePos) >= 0)) { ++ continue; ++ } ++ ++ final BlockState blockData = blocks.get(localBlockIndex); ++ ++ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$emptyContextCollisionShape()) { ++ continue; ++ } ++ ++ VoxelShape blockCollision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$getConstantContextCollisionShape(); ++ ++ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) { ++ if (blockCollision == null) { ++ blockCollision = blockData.getCollisionShape((Level)(Object)this, mutablePos, collisionShape); ++ ++ if (blockCollision.isEmpty()) { ++ continue; ++ } ++ } ++ ++ // avoid VoxelShape#move by shifting the entity collision shape instead ++ final AABB shiftedAABB = aabb.move(-(double)blockX, -(double)blockY, -(double)blockZ); ++ ++ final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation(); ++ if (singleAABB != null) { ++ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(singleAABB, shiftedAABB)) { ++ continue; ++ } ++ ++ selected = mutablePos.immutable(); ++ selectedDistance = distance; ++ continue; ++ } ++ ++ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(blockCollision, shiftedAABB)) { ++ continue; ++ } ++ ++ selected = mutablePos.immutable(); ++ selectedDistance = distance; ++ continue; ++ } ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ return java.util.Optional.ofNullable(selected); ++ } ++ // Paper end - optimise collisions ++ // Paper start - getblock optimisations - cache world height/sections ++ private final int minY; ++ private final int height; ++ private final int maxY; ++ private final int minSectionY; ++ private final int maxSectionY; ++ private final int sectionsCount; ++ ++ @Override ++ public int getMinY() { ++ return this.minY; ++ } ++ ++ @Override ++ public int getHeight() { ++ return this.height; ++ } ++ ++ @Override ++ public int getMaxY() { ++ return this.maxY; ++ } ++ ++ @Override ++ public int getSectionsCount() { ++ return this.sectionsCount; ++ } ++ ++ @Override ++ public int getMinSectionY() { ++ return this.minSectionY; ++ } ++ ++ @Override ++ public int getMaxSectionY() { ++ return this.maxSectionY; ++ } ++ ++ @Override ++ public boolean isInsideBuildHeight(final int blockY) { ++ return blockY >= this.minY && blockY <= this.maxY; ++ } ++ ++ @Override ++ public boolean isOutsideBuildHeight(final BlockPos pos) { ++ return this.isOutsideBuildHeight(pos.getY()); ++ } ++ ++ @Override ++ public boolean isOutsideBuildHeight(final int blockY) { ++ return blockY < this.minY || blockY > this.maxY; ++ } ++ ++ @Override ++ public int getSectionIndex(final int blockY) { ++ return (blockY >> 4) - this.minSectionY; ++ } ++ ++ @Override ++ public int getSectionIndexFromSectionY(final int sectionY) { ++ return sectionY - this.minSectionY; ++ } ++ ++ @Override ++ public int getSectionYFromSectionIndex(final int sectionIdx) { ++ return sectionIdx + this.minSectionY; ++ } ++ // Paper end - getblock optimisations - cache world height/sections ++ // Paper start - optimise random ticking ++ @Override ++ public abstract Holder<Biome> getUncachedNoiseBiome(final int x, final int y, final int z); ++ ++ /** ++ * @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, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - create paper world config & Anti-Xray ++ // Paper start - getblock optimisations - cache world height/sections ++ final DimensionType dimType = holder.value(); ++ this.minY = dimType.minY(); ++ this.height = dimType.height(); ++ this.maxY = this.minY + this.height - 1; ++ this.minSectionY = this.minY >> 4; ++ this.maxSectionY = this.maxY >> 4; ++ this.sectionsCount = this.maxSectionY - this.minSectionY + 1; ++ // Paper end - getblock optimisations - cache world height/sections + this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot + this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config + this.generator = gen; +@@ -287,6 +920,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime); + this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime); + this.chunkPacketBlockController = this.paperConfig().anticheat.antiXray.enabled ? new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray ++ this.entityLookup = new ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl.DefaultEntityLookup(this); // Paper - rewrite chunk system + } + + // Paper start - Cancel hit for vanished players +@@ -556,7 +1190,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); + } + +@@ -819,6 +1453,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 +@@ -834,6 +1470,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 +@@ -854,12 +1495,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 + +@@ -911,7 +1560,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) { +@@ -1003,26 +1652,15 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + Profiler.get().incrementCounter("getEntities"); + List<Entity> list = Lists.newArrayList(); + +- this.getEntities().get(box, (entity1) -> { +- if (entity1 != except && predicate.test(entity1)) { +- list.add(entity1); +- } +- +- if (entity1 instanceof EnderDragon) { +- EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity1).getSubEntities(); +- int i = aentitycomplexpart.length; ++ // Paper start - rewrite chunk system ++ final List<Entity> ret = new java.util.ArrayList<>(); + +- for (int j = 0; j < i; ++j) { +- EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(except, box, ret, predicate); + +- if (entity1 != except && predicate.test(entitycomplexpart)) { +- list.add(entitycomplexpart); +- } +- } +- } ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, except, box, predicate, ret); + +- }); +- return list; ++ return ret; ++ // Paper end - rewrite chunk system + } + + @Override +@@ -1037,36 +1675,94 @@ 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) { + Profiler.get().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); ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); ++ return; ++ } else { ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(byType, boundingBox, into, predicate); ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); ++ 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); ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); ++ return; ++ } else { ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate); ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); ++ 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); ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); ++ return; ++ } else { ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate); ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); ++ 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); ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); ++ return; ++ } else { ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate); ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); ++ 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]; ++ } ++ ++ List<org.bukkit.entity.Entity> ret = new java.util.ArrayList<>(); ++ for (Entity entity : slices.getAllEntities()) { ++ org.bukkit.entity.Entity bukkit = entity.getBukkitEntity(); ++ if (bukkit != null && bukkit.isValid()) { ++ ret.add(bukkit); ++ } ++ } ++ ++ return ret.toArray(new org.bukkit.entity.Entity[0]); + } ++ // 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 5eb8982678110fabb82a93c5ec67c666b7fde017..ade435de0af4ee3566fa4a490df53cddd2f6531c 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/ServerExplosion.java b/src/main/java/net/minecraft/world/level/ServerExplosion.java +index fd1ecedfab037e377e4dded61539689bacc90f80..bbbd451ff184be8fa13bd93d53c89a9502f9951a 100644 +--- a/src/main/java/net/minecraft/world/level/ServerExplosion.java ++++ b/src/main/java/net/minecraft/world/level/ServerExplosion.java +@@ -64,6 +64,249 @@ public class ServerExplosion implements Explosion { + public float yield; + // CraftBukkit end + public boolean excludeSourceFromDamage = true; // Paper - Allow explosions to damage source ++ // Paper start - collisions optimisations ++ 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[] directMappedBlockCache; ++ private BlockPos.MutableBlockPos mutablePos; ++ ++ 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.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$emptyContextCollisionShape()) { ++ net.minecraft.world.phys.shapes.VoxelShape collision = cachedBlock.cachedCollisionShape; ++ if (collision == null) { ++ collision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$getConstantContextCollisionShape(); ++ 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 - collisions optimisations + + public ServerExplosion(ServerLevel world, @Nullable Entity entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, Vec3 pos, float power, boolean createFire, Explosion.BlockInteraction destructionType) { + this.level = world; +@@ -127,65 +370,101 @@ public class ServerExplosion implements Explosion { + } + + private List<BlockPos> calculateExplodedPositions() { +- Set<BlockPos> set = new HashSet(); +- boolean flag = true; +- +- for (int i = 0; i < 16; ++i) { +- for (int j = 0; j < 16; ++j) { +- for (int k = 0; k < 16; ++k) { +- if (i == 0 || i == 15 || j == 0 || j == 15 || k == 0 || k == 15) { +- double d0 = (double) ((float) i / 15.0F * 2.0F - 1.0F); +- double d1 = (double) ((float) j / 15.0F * 2.0F - 1.0F); +- double d2 = (double) ((float) k / 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.center.x; +- double d5 = this.center.y; +- double d6 = this.center.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 ++ final ObjectArrayList<BlockPos> ret = new ObjectArrayList<>(); + +- Optional<Float> optional = this.damageCalculator.getBlockExplosionResistance(this, this.level, blockposition, iblockdata, fluid); ++ final Vec3 center = this.center; + +- if (optional.isPresent()) { +- f -= ((Float) optional.get() + 0.3F) * 0.3F; +- } ++ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache = this.directMappedBlockCache; + +- 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())); +- } ++ // 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(center.x); ++ final int blockY = Mth.floor(center.y); ++ final int blockZ = Mth.floor(center.z); ++ ++ final long key = BlockPos.asLong(blockX, blockY, blockZ); ++ ++ initialCache = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); ++ } ++ ++ // only ~1/3rd of the loop iterations in vanilla will result in a ray, as it is iterating the perimeter of ++ // a 16x16x16 cube ++ // we can cache the rays and their normals as well, so that we eliminate the excess iterations / checks and ++ // calculations in one go ++ // additional aggressive caching of block retrieval is very significant, as at low power (i.e tnt) most ++ // block retrievals are not unique ++ for (int ray = 0, len = CACHED_RAYS.length; ray < len;) { ++ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache cachedBlock = initialCache; ++ ++ double currX = center.x; ++ double currY = center.y; ++ double currZ = center.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); ++ } ++ } ++ ++ if (cachedBlock.outOfWorld) { ++ break; ++ } ++ final BlockState iblockdata = cachedBlock.blockState; ++ ++ power -= cachedBlock.resistance; ++ ++ if (power > 0.0f && cachedBlock.shouldExplode == null) { ++ // 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 = iblockdata.isDestroyable() && this.damageCalculator.shouldBlockExplode((Explosion)(Object)this, this.level, cachedBlock.immutablePos, cachedBlock.blockState, power); // Paper - Protect Bedrock and End Portal/Frames from being destroyed ++ cachedBlock.shouldExplode = shouldExplode ? Boolean.TRUE : Boolean.FALSE; ++ if (shouldExplode) { ++ if (this.fire || !cachedBlock.blockState.isAir()) { ++ ret.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); ++ ret.add(cachedBlock.immutablePos.relative(direction.getOpposite())); // Paper - optimise collisions + } +- // Paper end - prevent headless pistons from forming + } +- +- d4 += d0 * 0.30000001192092896D; +- d5 += d1 * 0.30000001192092896D; +- d6 += d2 * 0.30000001192092896D; ++ // Paper end - prevent headless pistons from forming + } + } + } +- } ++ ++ power -= 0.22500001F; ++ currX += incX; ++ currY += incY; ++ currZ += incZ; ++ } while (power > 0.0f); + } + +- return new ObjectArrayList(set); ++ return ret; ++ // Paper end - collision optimisations + } + + private void hurtEntities() { +@@ -391,6 +670,14 @@ public class ServerExplosion implements Explosion { + return; + } + // CraftBukkit end ++ // 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]; ++ this.directMappedBlockCache = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH]; ++ this.mutablePos = new BlockPos.MutableBlockPos(); ++ // Paper end - collision optimisations + this.level.gameEvent(this.source, (Holder) GameEvent.EXPLODE, this.center); + List<BlockPos> list = this.calculateExplodedPositions(); + +@@ -406,6 +693,13 @@ public class ServerExplosion implements Explosion { + if (this.fire) { + this.createFire(list); + } ++ // Paper start - collision optimisations ++ this.blockCache = null; ++ this.chunkPosCache = null; ++ this.chunkCache = null; ++ this.directMappedBlockCache = null; ++ this.mutablePos = null; ++ // Paper end - collision optimisations + + } + +@@ -495,12 +789,12 @@ public class ServerExplosion implements Explosion { + // Paper start - Optimize explosions + private float getBlockDensity(Vec3 vec3d, Entity entity) { + if (!this.level.paperConfig().environment.optimizeExplosions) { +- return getSeenPercent(vec3d, entity); ++ return this.getSeenFraction(vec3d, entity, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations + } + 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, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations + this.level.explosionDensityCache.put(key, blockDensity); + } + +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 8590de51b572c0f73d45aee60313d466e4671da5..b725eea9d3ca81d2ef7802f5d0346d924aa1f808 100644 +--- a/src/main/java/net/minecraft/world/level/biome/Biome.java ++++ b/src/main/java/net/minecraft/world/level/biome/Biome.java +@@ -112,20 +112,7 @@ public final class Biome { + + @Deprecated + public float getTemperature(BlockPos blockPos, int seaLevel) { +- 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, seaLevel); +- if (long2FloatLinkedOpenHashMap.size() == 1024) { +- long2FloatLinkedOpenHashMap.removeFirstFloat(); +- } +- +- long2FloatLinkedOpenHashMap.put(l, g); +- return g; +- } ++ return this.getHeightAdjustedTemperature(blockPos, seaLevel); // Paper - optimise random ticking + } + + public boolean shouldFreeze(LevelReader world, BlockPos blockPos) { +diff --git a/src/main/java/net/minecraft/world/level/biome/BiomeManager.java b/src/main/java/net/minecraft/world/level/biome/BiomeManager.java +index 01352cc83b25eb0e30b7e0ff521fc7c1b3d5155b..90f8360f547ce709fd13ee34f8e67d8bfa94b498 100644 +--- a/src/main/java/net/minecraft/world/level/biome/BiomeManager.java ++++ b/src/main/java/net/minecraft/world/level/biome/BiomeManager.java +@@ -98,8 +98,7 @@ public class BiomeManager { + } + + private static double getFiddle(long l) { +- double d = (double)Math.floorMod(l >> 24, 1024) / 1024.0; +- return (d - 0.5) * 0.9; ++ return (double)(((l >> 24) & (1024 - 1)) - (1024/2)) * (0.9 / 1024.0); // Paper - avoid floorMod, fp division, and fp subtraction + } + + public interface NoiseBiomeSource { +diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java +index 0f7b73634930df02d7b0a7f44890597cc2e6deca..b6d6c2cb9b227a17fb4ce42bc75f92206fbea043 100644 +--- a/src/main/java/net/minecraft/world/level/block/Block.java ++++ b/src/main/java/net/minecraft/world/level/block/Block.java +@@ -271,7 +271,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 95d30c2db7e291d65c24feb114b0f3598d280912..99fd67a78539133adf78d65e2c520ff3dd260301 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 +@@ -793,7 +793,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 static final Direction[] DIRECTIONS = Direction.values(); + private static final VoxelShape[] EMPTY_OCCLUSION_SHAPES = (VoxelShape[]) Util.make(new VoxelShape[BlockBehaviour.BlockStateBase.DIRECTIONS.length], (avoxelshape) -> { +@@ -837,6 +837,76 @@ public abstract class BlockBehaviour implements FeatureElement { + private boolean propagatesSkylightDown; + private int lightBlock; + ++ // Paper start - rewrite chunk system ++ private boolean isConditionallyFullOpaque; ++ ++ @Override ++ public final boolean starlight$isConditionallyFullOpaque() { ++ return this.isConditionallyFullOpaque; ++ } ++ // 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 boolean emptyConstantCollisionShape; ++ private VoxelShape constantCollisionShape; ++ ++ private static void initCaches(final VoxelShape shape, final boolean neighbours) { ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$isFullBlock(); ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$occludesFullBlock(); ++ shape.toAabbs(); ++ if (!shape.isEmpty()) { ++ shape.bounds(); ++ } ++ if (neighbours) { ++ for (final Direction direction : DIRECTIONS_CACHED) { ++ initCaches(((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$getFaceShapeClamped(direction), false); ++ initCaches(shape.getFaceShape(direction), false); ++ } ++ } ++ } ++ ++ @Override ++ 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 boolean moonrise$emptyContextCollisionShape() { ++ return this.emptyConstantCollisionShape; ++ } ++ ++ @Override ++ public final int moonrise$uniqueId1() { ++ return this.id1; ++ } ++ ++ @Override ++ public final int moonrise$uniqueId2() { ++ return this.id2; ++ } ++ ++ @Override ++ public final VoxelShape moonrise$getConstantContextCollisionShape() { ++ return this.constantCollisionShape; ++ } ++ // Paper end - optimise collisions ++ + protected BlockStateBase(Block block, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<BlockState> codec) { + super(block, propertyMap, codec); + this.fluidState = Fluids.EMPTY.defaultFluidState(); +@@ -921,6 +991,41 @@ public abstract class BlockBehaviour implements FeatureElement { + + this.propagatesSkylightDown = ((Block) this.owner).propagatesSkylightDown(this.asState()); + this.lightBlock = ((Block) this.owner).getLightBlock(this.asState()); ++ // Paper start - rewrite chunk system ++ this.isConditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion; ++ // 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); ++ } catch (final Throwable throwable) { ++ this.constantCollisionShape = null; ++ } ++ this.occludesFullBlock = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)collisionShape).moonrise$occludesFullBlock(); ++ this.emptyCollisionShape = collisionShape.isEmpty(); ++ this.emptyConstantCollisionShape = this.constantCollisionShape != null && this.constantCollisionShape.isEmpty(); ++ // init caches ++ initCaches(collisionShape, true); ++ if (this.constantCollisionShape != null) { ++ initCaches(this.constantCollisionShape, true); ++ } ++ } else { ++ this.occludesFullBlock = false; ++ this.emptyCollisionShape = false; ++ this.emptyConstantCollisionShape = false; ++ this.constantCollisionShape = null; ++ } ++ ++ if (this.occlusionShape != null) { ++ initCaches(this.occlusionShape, true); ++ } ++ if (this.occlusionShapesByFace != null) { ++ for (final VoxelShape shape : this.occlusionShapesByFace) { ++ initCaches(shape, true); ++ } ++ } ++ // Paper end - optimise collisions + } + + public Block getBlock() { +diff --git a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java +index 422b364764e0df16ca250b4939d7b226e69c0840..815ee11aa5ed3448ff255e9c36d769478de477bd 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java ++++ b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java +@@ -15,7 +15,7 @@ import java.util.stream.Collectors; + import javax.annotation.Nullable; + import net.minecraft.world.level.block.state.properties.Property; + +-public abstract class StateHolder<O, S> { ++public abstract class StateHolder<O, S> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccessStateHolder { // Paper - optimise blockstate property access + public static final String NAME_TAG = "Name"; + public static final String PROPERTIES_TAG = "Properties"; + public static final Function<Entry<Property<?>, Comparable<?>>, String> PROPERTY_ENTRY_TO_STRING_FUNCTION = new Function<Entry<Property<?>, Comparable<?>>, String>() { +@@ -34,14 +34,28 @@ public abstract class StateHolder<O, S> { + } + }; + protected final O owner; +- private final Reference2ObjectArrayMap<Property<?>, Comparable<?>> values; ++ private Reference2ObjectArrayMap<Property<?>, Comparable<?>> values; // Paper - optimise blockstate property access - remove final + private Map<Property<?>, S[]> neighbours; + protected final MapCodec<S> propertiesCodec; + ++ // Paper start - optimise blockstate property access ++ protected ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<O, S> optimisedTable; ++ protected final long tableIndex; ++ ++ @Override ++ public final long moonrise$getTableIndex() { ++ return this.tableIndex; ++ } ++ // Paper end - optimise blockstate property access ++ + protected StateHolder(O owner, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<S> codec) { + this.owner = owner; + this.values = propertyMap; + this.propertiesCodec = codec; ++ // Paper start - optimise blockstate property access ++ this.optimisedTable = new ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<>(this.values.keySet()); ++ this.tableIndex = this.optimisedTable.getIndex((StateHolder<O, S>)(Object)this); ++ // Paper end - optimise blockstate property access + } + + public <T extends Comparable<T>> S cycle(Property<T> property) { +@@ -67,20 +81,21 @@ public abstract class StateHolder<O, S> { + } + + public Collection<Property<?>> getProperties() { +- return Collections.unmodifiableCollection(this.values.keySet()); ++ return this.optimisedTable.getProperties(); // Paper - optimise blockstate property access + } + + public <T extends Comparable<T>> boolean hasProperty(Property<T> property) { +- return this.values.containsKey(property); ++ return property != null && this.optimisedTable.hasProperty(property); // Paper - optimise blockstate property access + } + + public <T extends Comparable<T>> T getValue(Property<T> property) { +- Comparable<?> comparable = this.values.get(property); +- if (comparable == null) { +- throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner); +- } else { +- return property.getValueClass().cast(comparable); ++ // Paper start - optimise blockstate property access ++ final T ret = this.optimisedTable.get(this.tableIndex, property); ++ if (ret != null) { ++ return ret; + } ++ throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner); ++ // Paper end - optimise blockstate property access + } + + public <T extends Comparable<T>> Optional<T> getOptionalValue(Property<T> property) { +@@ -93,22 +108,30 @@ public abstract class StateHolder<O, S> { + + @Nullable + public <T extends Comparable<T>> T getNullableValue(Property<T> property) { +- Comparable<?> comparable = this.values.get(property); +- return comparable == null ? null : property.getValueClass().cast(comparable); ++ return property == null ? null : this.optimisedTable.get(this.tableIndex, property); // Paper - optimise blockstate property access + } + + public <T extends Comparable<T>, V extends T> S setValue(Property<T> property, V value) { +- Comparable<?> comparable = this.values.get(property); +- if (comparable == null) { +- throw new IllegalArgumentException("Cannot set property " + property + " as it does not exist in " + this.owner); +- } else { +- return this.setValueInternal(property, value, comparable); ++ // Paper start - optimise blockstate property access ++ final S ret = this.optimisedTable.set(this.tableIndex, property, value); ++ if (ret != null) { ++ return ret; + } ++ throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner); ++ // Paper end - optimise blockstate property access + } + + public <T extends Comparable<T>, V extends T> S trySetValue(Property<T> property, V value) { +- Comparable<?> comparable = this.values.get(property); +- return (S)(comparable == null ? this : this.setValueInternal(property, value, comparable)); ++ // Paper start - optimise blockstate property access ++ if (property == null) { ++ return (S)(StateHolder<O, S>)(Object)this; ++ } ++ final S ret = this.optimisedTable.trySet(this.tableIndex, property, value, (S)(StateHolder<O, S>)(Object)this); ++ if (ret != null) { ++ return ret; ++ } ++ throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner); ++ // Paper end - optimise blockstate property access + } + + private <T extends Comparable<T>, V extends T> S setValueInternal(Property<T> property, V newValue, Comparable<?> oldValue) { +@@ -125,18 +148,27 @@ public abstract class StateHolder<O, S> { + } + + public void populateNeighbours(Map<Map<Property<?>, Comparable<?>>, S> states) { +- if (this.neighbours != null) { +- throw new IllegalStateException(); +- } else { +- Map<Property<?>, S[]> map = new Reference2ObjectArrayMap<>(this.values.size()); ++ // Paper start - optimise blockstate property access ++ final Map<Map<Property<?>, Comparable<?>>, S> map = states; ++ if (this.optimisedTable.isLoaded()) { ++ return; ++ } ++ this.optimisedTable.loadInTable(map); + +- for (Entry<Property<?>, Comparable<?>> entry : this.values.entrySet()) { +- Property<?> property = entry.getKey(); +- map.put(property, property.getPossibleValues().stream().map(value -> states.get(this.makeNeighbourValues(property, value))).toArray()); +- } ++ // de-duplicate the tables ++ for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : map.entrySet()) { ++ final S value = entry.getValue(); ++ ((StateHolder<O, S>)value).optimisedTable = this.optimisedTable; ++ } + +- this.neighbours = map; ++ // remove values arrays ++ for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : map.entrySet()) { ++ final S value = entry.getValue(); ++ ((StateHolder<O, S>)value).values = null; + } ++ ++ return; ++ // Paper end optimise blockstate property access + } + + private Map<Property<?>, Comparable<?>> makeNeighbourValues(Property<?> property, Comparable<?> value) { +@@ -146,7 +178,11 @@ public abstract class StateHolder<O, S> { + } + + public Map<Property<?>, Comparable<?>> getValues() { +- return this.values; ++ // Paper start - optimise blockstate property access ++ ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<O, S> table = this.optimisedTable; ++ // We have to use this.values until the table is loaded ++ return table.isLoaded() ? table.getMapView(this.tableIndex) : this.values; ++ // Paper end - optimise blockstate property access + } + + protected static <O, S extends StateHolder<O, S>> Codec<S> codec(Codec<O> codec, Function<O, S> ownerToStateFunction) { +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java +index ea76aa490358e9e1d13350ba0ea246ec2c423894..98058505d36baf74008da08339afc196713b14a7 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java +@@ -3,13 +3,23 @@ package net.minecraft.world.level.block.state.properties; + import java.util.List; + import java.util.Optional; + +-public final class BooleanProperty extends Property<Boolean> { ++public final class BooleanProperty extends Property<Boolean> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<Boolean> { // Paper - optimise blockstate property access + private static final List<Boolean> VALUES = List.of(true, false); + private static final int TRUE_INDEX = 0; + private static final int FALSE_INDEX = 1; + ++ // Paper start - optimise blockstate property access ++ private static final Boolean[] BY_ID = new Boolean[]{ Boolean.FALSE, Boolean.TRUE }; ++ ++ @Override ++ public final int moonrise$getIdFor(final Boolean value) { ++ return value.booleanValue() ? 1 : 0; ++ } ++ // Paper end - optimise blockstate property access ++ + private BooleanProperty(String name) { + super(name, Boolean.class); ++ this.moonrise$setById(BY_ID); // Paper - optimise blockstate property access + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java +index 85a197232be9377c0313ec00e8f935551e2c60e0..30b2fce9e47ffcc3de1542b1d0f073f5640127a7 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java +@@ -10,11 +10,39 @@ import java.util.function.Predicate; + import java.util.stream.Collectors; + import net.minecraft.util.StringRepresentable; + +-public final class EnumProperty<T extends Enum<T> & StringRepresentable> extends Property<T> { ++public final class EnumProperty<T extends Enum<T> & StringRepresentable> extends Property<T> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<T> { // Paper - optimise blockstate property access + private final List<T> values; + private final Map<String, T> names; + private final int[] ordinalToIndex; + ++ // Paper start - optimise blockstate property access ++ private int[] idLookupTable; ++ ++ @Override ++ public final int moonrise$getIdFor(final T value) { ++ final Class<T> target = this.getValueClass(); ++ return ((value.getClass() != target && value.getDeclaringClass() != target)) ? -1 : this.idLookupTable[value.ordinal()]; ++ } ++ ++ private void init() { ++ final java.util.Collection<T> values = this.getPossibleValues(); ++ final Class<T> clazz = this.getValueClass(); ++ ++ int id = 0; ++ this.idLookupTable = new int[clazz.getEnumConstants().length]; ++ Arrays.fill(this.idLookupTable, -1); ++ final T[] byId = (T[])java.lang.reflect.Array.newInstance(clazz, values.size()); ++ ++ for (final T value : values) { ++ final int valueId = id++; ++ this.idLookupTable[value.ordinal()] = valueId; ++ byId[valueId] = value; ++ } ++ ++ this.moonrise$setById(byId); ++ } ++ // Paper end - optimise blockstate property access ++ + private EnumProperty(String name, Class<T> type, List<T> values) { + super(name, type); + if (values.isEmpty()) { +@@ -37,6 +65,7 @@ public final class EnumProperty<T extends Enum<T> & StringRepresentable> extends + + this.names = builder.buildOrThrow(); + } ++ this.init(); // Paper - optimise blockstate property access + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java +index 55a87592a99105dbf57b26fb6ccba695295fce24..986365acc9983331a7982ea2e1eac2b0efe1506d 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java +@@ -5,11 +5,33 @@ import java.util.List; + import java.util.Optional; + import java.util.stream.IntStream; + +-public final class IntegerProperty extends Property<Integer> { ++public final class IntegerProperty extends Property<Integer> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<Integer> { // Paper - optimise blockstate property access + private final IntImmutableList values; + public final int min; + public final int max; + ++ // Paper start - optimise blockstate property access ++ @Override ++ public final int moonrise$getIdFor(final Integer value) { ++ final int val = value.intValue(); ++ final int ret = val - this.min; ++ ++ return ret | ((this.max - ret) >> 31); ++ } ++ ++ private void init() { ++ final int min = this.min; ++ final int max = this.max; ++ ++ final Integer[] byId = new Integer[max - min + 1]; ++ for (int i = min; i <= max; ++i) { ++ byId[i - min] = Integer.valueOf(i); ++ } ++ ++ this.moonrise$setById(byId); ++ } ++ // Paper end - optimise blockstate property access ++ + private IntegerProperty(String name, int min, int max) { + super(name, Integer.class); + if (min < 0) { +@@ -21,6 +43,7 @@ public final class IntegerProperty extends Property<Integer> { + this.max = max; + this.values = IntImmutableList.toList(IntStream.range(min, max + 1)); + } ++ this.init(); // Paper - optimise blockstate property access + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java +index fcf04c5c58ff35d38c5bf0df562ae2f8dc98a0ee..0b116160924300a9d62ad5948bfaf276f0386e4d 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java +@@ -10,7 +10,7 @@ import java.util.stream.Stream; + import javax.annotation.Nullable; + import net.minecraft.world.level.block.state.StateHolder; + +-public abstract class Property<T extends Comparable<T>> { ++public abstract class Property<T extends Comparable<T>> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<T> { // Paper - optimise blockstate property access + private final Class<T> clazz; + private final String name; + @Nullable +@@ -24,9 +24,38 @@ public abstract class Property<T extends Comparable<T>> { + ); + private final Codec<Property.Value<T>> valueCodec = this.codec.xmap(this::value, Property.Value::value); + ++ // Paper start - optimise blockstate property access ++ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger(); ++ private final int id; ++ private T[] byId; ++ ++ @Override ++ public final int moonrise$getId() { ++ return this.id; ++ } ++ ++ @Override ++ public final T moonrise$getById(final int id) { ++ final T[] byId = this.byId; ++ return id < 0 || id >= byId.length ? null : this.byId[id]; ++ } ++ ++ @Override ++ public final void moonrise$setById(final T[] byId) { ++ if (this.byId != null) { ++ throw new IllegalStateException(); ++ } ++ this.byId = byId; ++ } ++ ++ @Override ++ public abstract int moonrise$getIdFor(final T value); ++ // Paper end - optimise blockstate property access ++ + protected Property(String name, Class<T> type) { + this.clazz = type; + this.name = name; ++ this.id = ID_GENERATOR.getAndIncrement(); // Paper - optimise blockstate property access + } + + public Property.Value<T> value(T value) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +index 63d7d6b93119d96d753230472df30a9dedd889dc..a402e2f774cfc062afab8d86969f3e6f38874063 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.SavedTick; + import net.minecraft.world.ticks.TickContainerAccess; + import org.slf4j.Logger; + +-public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, LightChunk, StructureAccess { ++public abstract class ChunkAccess implements 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 BiomeManager.NoiseBiomeSource, Ligh + @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 BiomeManager.NoiseBiomeSource, Ligh + 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 BiomeManager.NoiseBiomeSource, Ligh + 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 BiomeManager.NoiseBiomeSource, Ligh + this.replaceMissingSections(biomeRegistry, this.sections); // Paper - Anti-Xray - make it a non-static method + // 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 +@@ -452,22 +513,22 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh + + @Override + public Holder<Biome> getNoiseBiome(int biomeX, int biomeY, int biomeZ) { +- try { +- int l = QuartPos.fromBlock(this.getMinY()); +- 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 +@@ -524,12 +585,12 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh + } + + 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 PackedTicks(List<SavedTick<Block>> blocks, List<SavedTick<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 582065b2d4e818c0edec36b2e9847f8ed3266b10..b5114f84b2df2f4606702b892d32d484225d9dcf 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(() -> { + chunk.fillBiomesFromNoise(this.biomeSource, noiseConfig.sampler()); + return chunk; +- }, Util.backgroundExecutor().forName("init_biomes")); ++ }, Runnable::run); // Paper - rewrite chunk system + } + + public abstract void applyCarvers(WorldGenRegion chunkRegion, long seed, RandomState noiseConfig, BiomeManager biomeAccess, StructureManager structureAccessor, ChunkAccess chunk); +@@ -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/HashMapPalette.java b/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java +index 98dbeaf8bde15940e5b5d5d1f13fd4bb32f0a10d..7beea075b5a7ef738a4ac0558b99f4c5708f2c4a 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java ++++ b/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java +@@ -8,12 +8,19 @@ import net.minecraft.network.FriendlyByteBuf; + import net.minecraft.network.VarInt; + import net.minecraft.util.CrudeIncrementalIntIdentityHashBiMap; + +-public class HashMapPalette<T> implements Palette<T> { ++public class HashMapPalette<T> implements Palette<T>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads + private final IdMap<T> registry; + private final CrudeIncrementalIntIdentityHashBiMap<T> values; + private final PaletteResize<T> resizeHandler; + private final int bits; + ++ // Paper start - optimise palette reads ++ @Override ++ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> container) { ++ return ((ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T>)this.values).moonrise$getRawPalette(container); ++ } ++ // Paper end - optimise palette reads ++ + public HashMapPalette(IdMap<T> idList, int bits, PaletteResize<T> listener, List<T> entries) { + this(idList, bits, listener); + entries.forEach(this.values::add); +diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java +index f38700e5fbeeb8a913272d4464b8aa325d511dac..1eb8022f3e31603322e6c56516304afc9a11bbec 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java +@@ -30,7 +30,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; + +@@ -46,6 +46,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 5d15aed0f340a49a47e035fb0ce23413946bc124..01cfb478764b8deb38be5692390dd9f014b8999f 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -54,7 +54,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.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() { +@@ -113,6 +113,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 +@@ -123,6 +131,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()); +@@ -152,13 +193,19 @@ public class LevelChunk extends ChunkAccess { + } + } + +- this.skyLightSources = protoChunk.skyLightSources; ++ // Paper - rewrite chunk system + this.setLightCorrect(protoChunk.isLightCorrect()); + this.markUnsaved(); + 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 + } + + public void setUnsavedListener(LevelChunk.UnsavedListener unsavedListener) { +@@ -361,7 +408,7 @@ public class LevelChunk extends ChunkAccess { + ProfilerFiller gameprofilerfiller = Profiler.get(); + + 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(); +@@ -627,11 +674,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 +@@ -640,6 +688,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)); ++ org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesLoadEvent(this.level, this.chunkPos, ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.locX, this.locZ).getEntityChunk().getAllEntities()); // Paper - rewrite chunk system + + if (this.needsDecoration) { + this.needsDecoration = false; +@@ -666,13 +715,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(); ++ org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesUnloadEvent(this.level, this.chunkPos, ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.locX, this.locZ).getEntityChunk().getAllEntities()); // 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 +@@ -680,8 +731,31 @@ 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 boolean tryMarkSaved() { ++ if (!this.isUnsaved()) { ++ return false; ++ } ++ ((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.tryMarkSaved(); ++ ++ return true; + } ++ // Paper end - rewrite chunk system + // CraftBukkit end + + public boolean isEmpty() { +@@ -789,6 +863,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 3dab36d00ea48101807ba40c7a7358b7eed12747..e4ae25c83ab9dd1aaa530a5456275ef63cdb8511 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; +@@ -25,6 +25,30 @@ public class LevelChunkSection { + public final PalettedContainer<BlockState> states; + private PalettedContainer<Holder<Biome>> biomes; // CraftBukkit - read/write + ++ // Paper start - block counting ++ private static final it.unimi.dsi.fastutil.shorts.ShortArrayList FULL_LIST = new it.unimi.dsi.fastutil.shorts.ShortArrayList(16*16*16); ++ static { ++ for (short i = 0; i < (16*16*16); ++i) { ++ FULL_LIST.add(i); ++ } ++ } ++ ++ private boolean isClient; ++ private static final short CLIENT_FORCED_SPECIAL_COLLIDING_BLOCKS = (short)9999; ++ private short specialCollidingBlocks; ++ private final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = new ca.spottedleaf.moonrise.common.list.ShortList(); ++ ++ @Override ++ public final boolean moonrise$hasSpecialCollidingBlocks() { ++ return this.specialCollidingBlocks != 0; ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.common.list.ShortList moonrise$getTickingBlockList() { ++ return this.tickingBlocks; ++ } ++ // Paper end - block counting ++ + private LevelChunkSection(LevelChunkSection section) { + this.nonEmptyBlockCount = section.nonEmptyBlockCount; + this.tickingBlockCount = section.tickingBlockCount; +@@ -67,6 +91,45 @@ public class LevelChunkSection { + return this.setBlockState(x, y, z, state, true); + } + ++ // Paper start - block counting ++ private void updateBlockCallback(final int x, final int y, final int z, final BlockState newState, ++ final BlockState oldState) { ++ if (oldState == newState) { ++ return; ++ } ++ ++ if (this.isClient) { ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(newState)) { ++ this.specialCollidingBlocks = CLIENT_FORCED_SPECIAL_COLLIDING_BLOCKS; ++ } ++ return; ++ } ++ ++ final boolean isSpecialOld = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(oldState); ++ final boolean isSpecialNew = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(newState); ++ if (isSpecialOld != isSpecialNew) { ++ if (isSpecialOld) { ++ --this.specialCollidingBlocks; ++ } else { ++ ++this.specialCollidingBlocks; ++ } ++ } ++ ++ final boolean oldTicking = oldState.isRandomlyTicking(); ++ final boolean newTicking = newState.isRandomlyTicking(); ++ if (oldTicking != newTicking) { ++ final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = this.tickingBlocks; ++ final short position = (short)(x | (z << 4) | (y << (4+4))); ++ ++ if (oldTicking) { ++ tickingBlocks.remove(position); ++ } else { ++ tickingBlocks.add(position); ++ } ++ } ++ } ++ // Paper end - block counting ++ + public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) { + BlockState iblockdata1; + +@@ -86,7 +149,7 @@ public class LevelChunkSection { + } + } + +- if (!fluid.isEmpty()) { ++ if (!!fluid.isRandomlyTicking()) { // Paper - block counting + --this.tickingFluidCount; + } + +@@ -97,10 +160,12 @@ public class LevelChunkSection { + } + } + +- if (!fluid1.isEmpty()) { ++ if (!!fluid1.isRandomlyTicking()) { // Paper - block counting + ++this.tickingFluidCount; + } + ++ this.updateBlockCallback(x, y, z, state, iblockdata1); // Paper - block counting ++ + return iblockdata1; + } + +@@ -121,40 +186,70 @@ 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.shorts.ShortArrayList> 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(); ++ } ++ ++ for (final java.util.Iterator<it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<it.unimi.dsi.fastutil.shorts.ShortArrayList>> iterator = counts.int2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<it.unimi.dsi.fastutil.shorts.ShortArrayList> entry = iterator.next(); ++ final int paletteIdx = entry.getIntKey(); ++ final it.unimi.dsi.fastutil.shorts.ShortArrayList coordinates = entry.getValue(); ++ final int paletteCount = coordinates.size(); + +- public int nonEmptyBlockCount; +- public int tickingBlockCount; +- public int tickingFluidCount; ++ final BlockState state = palette.valueFor(paletteIdx); + +- a(final LevelChunkSection chunksection) {} ++ if (state.isAir()) { ++ continue; ++ } ++ ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(state)) { ++ this.specialCollidingBlocks += (short)paletteCount; ++ } ++ this.nonEmptyBlockCount += (short)paletteCount; ++ if (state.isRandomlyTicking()) { ++ this.tickingBlockCount += (short)paletteCount; ++ final short[] raw = coordinates.elements(); ++ final int rawLen = raw.length; ++ ++ final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = this.tickingBlocks; + +- public void accept(BlockState iblockdata, int i) { +- FluidState fluid = iblockdata.getFluidState(); ++ tickingBlocks.setMinCapacity(Math.min((rawLen + tickingBlocks.size()) * 3 / 2, 16*16*16)); + +- if (!iblockdata.isAir()) { +- this.nonEmptyBlockCount += i; +- if (iblockdata.isRandomlyTicking()) { +- this.tickingBlockCount += i; ++ java.util.Objects.checkFromToIndex(0, paletteCount, raw.length); ++ for (int i = 0; i < paletteCount; ++i) { ++ tickingBlocks.add(raw[i]); + } + } + ++ 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 += (short)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() { +@@ -172,6 +267,11 @@ public class LevelChunkSection { + + datapaletteblock.read(buf); + this.biomes = datapaletteblock; ++ // Paper start - block counting ++ this.isClient = true; ++ // force has special colliding blocks to be true ++ this.specialCollidingBlocks = this.nonEmptyBlockCount != (short)0 && this.maybeHas(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil::isSpecialCollidingBlock) ? CLIENT_FORCED_SPECIAL_COLLIDING_BLOCKS : (short)0; ++ // Paper end - block counting + } + + public void readBiomes(FriendlyByteBuf buf) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java b/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java +index bc4d9452bbeb05a691fd285603e49491f41d3ad2..f8d9892970c9092f7cc84434d4fbf34354ce1195 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java +@@ -7,13 +7,20 @@ import net.minecraft.network.FriendlyByteBuf; + import net.minecraft.network.VarInt; + import org.apache.commons.lang3.Validate; + +-public class LinearPalette<T> implements Palette<T> { ++public class LinearPalette<T> implements Palette<T>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads + private final IdMap<T> registry; + private final T[] values; + private final PaletteResize<T> resizeHandler; + private final int bits; + private int size; + ++ // Paper start - optimise palette reads ++ @Override ++ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> container) { ++ return this.values; ++ } ++ // Paper end - optimise palette reads ++ + private LinearPalette(IdMap<T> idList, int bits, PaletteResize<T> listener, List<T> list) { + this.registry = idList; + this.values = (T[])(new Object[1 << bits]); +diff --git a/src/main/java/net/minecraft/world/level/chunk/Palette.java b/src/main/java/net/minecraft/world/level/chunk/Palette.java +index b8922e4a13df535cdc5701e893a6e460b33ff90d..100807f8b8337f56f49cdb818ccc75be2f08ecd1 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/Palette.java ++++ b/src/main/java/net/minecraft/world/level/chunk/Palette.java +@@ -5,7 +5,7 @@ import java.util.function.Predicate; + import net.minecraft.core.IdMap; + import net.minecraft.network.FriendlyByteBuf; + +-public interface Palette<T> { ++public interface Palette<T> extends ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads + int idFor(T object); + + boolean maybeHas(Predicate<T> predicate); +diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +index 69d6f203366df658e1ade55d917f0aa2b8a49be9..8b84bf2272556ac3321cbf16361d7f48a1cc6873 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +@@ -29,7 +29,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + private final PaletteResize<T> dummyPaletteResize = (newSize, added) -> 0; + public final IdMap<T> registry; + private final T @org.jetbrains.annotations.Nullable [] presetValues; // Paper - Anti-Xray - Add preset values +- 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 + +@@ -77,6 +77,33 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + ); + } + ++ // Paper start - optimise palette reads ++ private void updateData(final PalettedContainer.Data<T> data) { ++ if (data != null) { ++ ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T>)(Object)data).moonrise$setPalette( ++ ((ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T>)data.palette).moonrise$getRawPalette((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T>)(Object)data) ++ ); ++ } ++ } ++ ++ private T readPaletteSlow(final PalettedContainer.Data<T> data, final int paletteIdx) { ++ return data.palette.valueFor(paletteIdx); ++ } ++ ++ private T readPalette(final PalettedContainer.Data<T> data, final int paletteIdx) { ++ final T[] palette = ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T>)(Object)data).moonrise$getPalette(); ++ if (palette == null) { ++ return this.readPaletteSlow(data, paletteIdx); ++ } ++ ++ final T ret = palette[paletteIdx]; ++ if (ret == null) { ++ throw new IllegalArgumentException("Palette index out of bounds"); ++ } ++ return ret; ++ } ++ // Paper end - optimise palette reads ++ + // Paper start - Anti-Xray - Add preset values + @Deprecated @io.papermc.paper.annotation.DoNotUse public PalettedContainer(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Configuration<T> dataProvider, BitStorage storage, List<T> paletteEntries) { this(idList, paletteProvider, dataProvider, storage, paletteEntries, null, null); } + public PalettedContainer( +@@ -113,6 +140,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + } + } + // Paper end ++ this.updateData(this.data); // Paper - optimise palette reads + } + + // Paper start - Anti-Xray - Add preset values +@@ -122,6 +150,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + this.registry = idList; + this.strategy = paletteProvider; + this.data = data; ++ this.updateData(this.data); // Paper - optimise palette reads + } + + private PalettedContainer(PalettedContainer<T> container, T @org.jetbrains.annotations.Nullable [] presetValues) { // Paper - Anti-Xray - Add preset values +@@ -140,6 +169,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + this.registry = idList; + this.data = this.createOrReuseData(null, 0); + this.data.palette.idFor(object); ++ this.updateData(this.data); // Paper - optimise palette reads + } + + private PalettedContainer.Data<T> createOrReuseData(@Nullable PalettedContainer.Data<T> previousData, int bits) { +@@ -166,6 +196,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + data2.copyFrom(data.palette, data.storage); + this.data = data2; + this.addPresetValues(); ++ this.updateData(this.data); // Paper - optimise palette reads + return object == null ? -1 : data2.palette.idFor(object); + // Paper end + } +@@ -198,9 +229,12 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + } + + private synchronized T getAndSet(int index, T value) { // Paper - synchronize +- int i = this.data.palette.idFor(value); +- int j = this.data.storage.getAndSet(index, i); +- return this.data.palette.valueFor(j); ++ // Paper start - optimise palette reads ++ final int paletteIdx = this.data.palette.idFor(value); ++ final PalettedContainer.Data<T> data = this.data; ++ final int prev = data.storage.getAndSet(index, paletteIdx); ++ return this.readPalette(data, prev); ++ // Paper end - optimise palette reads + } + + public void set(int x, int y, int z, T value) { +@@ -223,9 +257,11 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + return this.get(this.strategy.getIndex(x, y, z)); + } + +- protected T get(int index) { +- PalettedContainer.Data<T> data = this.data; +- return data.palette.valueFor(data.storage.get(index)); ++ public T get(int index) { // Paper - public ++ // Paper start - optimise palette reads ++ final PalettedContainer.Data<T> data = this.data; ++ return this.readPalette(data, data.storage.get(index)); ++ // Paper end - optimise palette reads + } + + @Override +@@ -246,6 +282,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + buf.readLongArray(data.storage.getRaw()); + this.data = data; + this.addPresetValues(); // Paper - Anti-Xray - Add preset values (inefficient, but this isn't used by the server) ++ this.updateData(this.data); // Paper - optimise palette reads + } finally { + this.release(); + } +@@ -394,7 +431,44 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + void accept(T object, int count); + } + +- static record Data<T>(PalettedContainer.Configuration<T> configuration, BitStorage storage, Palette<T> palette) { ++ // Paper start - optimise palette reads ++ public static final class Data<T> implements ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> { ++ ++ private final PalettedContainer.Configuration<T> configuration; ++ private final BitStorage storage; ++ private final Palette<T> palette; ++ ++ private T[] moonrise$palette; ++ ++ public Data(final PalettedContainer.Configuration<T> configuration, final BitStorage storage, final Palette<T> palette) { ++ this.configuration = configuration; ++ this.storage = storage; ++ this.palette = palette; ++ } ++ ++ public PalettedContainer.Configuration<T> configuration() { ++ return this.configuration; ++ } ++ ++ public BitStorage storage() { ++ return this.storage; ++ } ++ ++ public Palette<T> palette() { ++ return this.palette; ++ } ++ ++ @Override ++ public final T[] moonrise$getPalette() { ++ return this.moonrise$palette; ++ } ++ ++ @Override ++ public final void moonrise$setPalette(final T[] palette) { ++ this.moonrise$palette = palette; ++ } ++ // Paper end - optimise palette reads ++ + public void copyFrom(Palette<T> palette, BitStorage storage) { + for (int i = 0; i < storage.getSize(); i++) { + T object = palette.valueFor(storage.get(i)); +diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java +index 4d5704df4a7ac6e148774f1a986d46bfb7e95f95..360edef3634bc61f36e41d63d168449df5d50933 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java +@@ -149,7 +149,7 @@ public class ProtoChunk extends ChunkAccess { + } + + if (LightEngine.hasDifferentLightProperties(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/SingleValuePalette.java b/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java +index a45e6410600afc5464e5d29932c193786ce0a6fb..a1ba68c95c2cdebdc0d7782cce7895529918073c 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java ++++ b/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java +@@ -8,12 +8,24 @@ import net.minecraft.network.FriendlyByteBuf; + import net.minecraft.network.VarInt; + import org.apache.commons.lang3.Validate; + +-public class SingleValuePalette<T> implements Palette<T> { ++public class SingleValuePalette<T> implements Palette<T>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads + private final IdMap<T> registry; + @Nullable + private T value; + private final PaletteResize<T> resizeHandler; + ++ // Paper start - optimise palette reads ++ private T[] rawPalette; ++ ++ @Override ++ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> container) { ++ if (this.rawPalette != null) { ++ return this.rawPalette; ++ } ++ return this.rawPalette = (T[])new Object[] { this.value }; ++ } ++ // Paper end - optimise palette reads ++ + public SingleValuePalette(IdMap<T> idList, PaletteResize<T> listener, List<T> entries) { + this.registry = idList; + this.resizeHandler = listener; +@@ -33,6 +45,11 @@ public class SingleValuePalette<T> implements Palette<T> { + return this.resizeHandler.onResize(1, object); + } else { + this.value = object; ++ // Paper start - optimise palette reads ++ if (this.rawPalette != null) { ++ this.rawPalette[0] = object; ++ } ++ // Paper end - optimise palette reads + return 0; + } + } +@@ -58,6 +75,11 @@ public class SingleValuePalette<T> implements Palette<T> { + @Override + public void read(FriendlyByteBuf buf) { + this.value = this.registry.byIdOrThrow(buf.readVarInt()); ++ // Paper start - optimise palette reads ++ if (this.rawPalette != null) { ++ this.rawPalette[0] = this.value; ++ } ++ // Paper end - optimise palette reads + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/chunk/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 4f84ff9cdb3303251e035a12ce9d8b9a0b58f46e..d80b7d555e02d1d4b82945373d383eaedbf4b976 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 aff4c3d63a97d5bbde004a616f7e14fca59b5ab9..d8dbaecc3c8ef59b0116f0ed4b1c06369b805e1d 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 +@@ -152,7 +152,7 @@ public class ChunkStatusTasks { + chunk1 = protochunkextension.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)); + } +@@ -168,7 +168,7 @@ public class ChunkStatusTasks { + }, context.mainThreadExecutor()); + } + +- public static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities) { // Paper - public ++ public static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities, ChunkPos pos) { // Paper - public // Paper - rewrite chunk system - 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, EntitySpawnReason.LOAD).filter((entity) -> { +@@ -180,7 +180,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/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java +index 092f7b6bba4e1291f76c2c09155f33803e93eb04..46f4b6706a1ca24ff6fc28960ad01a067109819f 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, Supplier<CompoundTag> nbtSupplier) { +@@ -185,29 +203,54 @@ public class ChunkStorage implements AutoCloseable { + }; + // Paper end - guard against possible chunk pos desync + this.handleLegacyStructureIndex(chunkPos); +- return this.worker.store(chunkPos, guardedPosCheck); // Paper - guard against possible chunk pos desync ++ // Paper start - rewrite chunk system ++ try { ++ this.storage.write(chunkPos, guardedPosCheck.get()); ++ 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 a0cbccd2cf1ac785745d86c42b6f58fb8bad7ffa..16ca1c8672e5f0a27f8a30498c754a81cdec5191 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 +@@ -71,12 +71,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 cb823d342e41b5861adfc847a313c265fb702a4c..2b1ea97199d5976e5ff4bd049c1e6c8b9b8a4177 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 +@@ -30,7 +30,7 @@ public class IOWorker implements ChunkScanAccess, AutoCloseable { + private static final Logger LOGGER = LogUtils.getLogger(); + private final AtomicBoolean shutdownRequested = new AtomicBoolean(); + private final PriorityConsecutiveExecutor consecutiveExecutor; +- private final RegionFileStorage storage; ++ public final RegionFileStorage storage; // Paper - public + private final SequencedMap<ChunkPos, IOWorker.PendingStore> pendingWrites = new LinkedHashMap<>(); + 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/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +index 863960ead8deaa0553be1c98e4fa09f07fcb8ef0..057875cbbdc92ba49b429f9a129514760edb32a2 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +@@ -28,7 +28,7 @@ import net.minecraft.nbt.NbtIo; // Paper + import net.minecraft.world.level.ChunkPos; + import org.slf4j.Logger; + +-public class RegionFile implements AutoCloseable { ++public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile { // Paper - rewrite chunk system + + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int SECTOR_BYTES = 4096; +@@ -52,6 +52,21 @@ public class RegionFile implements AutoCloseable { + @VisibleForTesting + protected final RegionBitmap usedSectors; + ++ // Paper start - rewrite chunk system ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(final net.minecraft.nbt.CompoundTag data, final ChunkPos pos) throws IOException { ++ final RegionFile.ChunkBuffer buffer = ((RegionFile)(Object)this).new ChunkBuffer(pos); ++ ((ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkBuffer)buffer).moonrise$setWriteOnClose(false); ++ ++ final DataOutputStream out = new DataOutputStream(this.version.wrap(buffer)); ++ ++ return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData( ++ data, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.WRITE, ++ out, ((ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkBuffer)buffer)::moonrise$write ++ ); ++ } ++ // Paper end - rewrite chunk system ++ + public RegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync) throws IOException { + this(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync); // Paper - Configurable region compression format + } +@@ -224,6 +239,16 @@ public class RegionFile implements AutoCloseable { + + @Nullable + private DataInputStream createExternalChunkInputStream(ChunkPos pos, byte flags) throws IOException { ++ // Paper start - rewrite chunk system ++ final DataInputStream is = this.createExternalChunkInputStream0(pos, flags); ++ if (is == null) { ++ return is; ++ } ++ return new ca.spottedleaf.moonrise.patches.chunk_system.util.stream.ExternalChunkStreamMarker(is); ++ } ++ @Nullable ++ private DataInputStream createExternalChunkInputStream0(ChunkPos pos, byte flags) throws IOException { ++ // Paper end - rewrite chunk system + Path path = this.getExternalChunkPath(pos); + + if (!Files.isRegularFile(path, new LinkOption[0])) { +@@ -514,10 +539,29 @@ public class RegionFile implements AutoCloseable { + + } + // Paper end +- private class ChunkBuffer extends ByteArrayOutputStream { ++ private class ChunkBuffer extends ByteArrayOutputStream implements ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkBuffer { // Paper - rewrite chunk system + + private final ChunkPos pos; + ++ // Paper start - rewrite chunk system ++ private boolean writeOnClose = true; ++ ++ @Override ++ public final boolean moonrise$getWriteOnClose() { ++ return this.writeOnClose; ++ } ++ ++ @Override ++ public final void moonrise$setWriteOnClose(final boolean value) { ++ this.writeOnClose = value; ++ } ++ ++ @Override ++ public final void moonrise$write(final RegionFile regionFile) throws IOException { ++ regionFile.write(this.pos, ByteBuffer.wrap(this.buf, 0, this.count)); ++ } ++ // Paper end - rewrite chunk system ++ + public ChunkBuffer(final ChunkPos chunkcoordintpair) { + super(8096); + super.write(0); +@@ -534,7 +578,7 @@ public class RegionFile implements AutoCloseable { + + JvmProfiler.INSTANCE.onRegionFileWrite(RegionFile.this.info, this.pos, RegionFile.this.version, i); + bytebuffer.putInt(0, i); +- RegionFile.this.write(this.pos, bytebuffer); ++ if (this.writeOnClose) { RegionFile.this.write(this.pos, bytebuffer); } // Paper - rewrite chunk system + } + } + +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +index e6abe35d6c43b7f76cf3da129ec9552e7b82453e..fdf8e18d24442178b52397acb482ffa3306a32e3 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,219 @@ 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 * 4; ++ private final it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet nonExistingRegionFiles = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet(); ++ private static String getRegionFileName(final int chunkX, final int chunkZ) { ++ return "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT) + ".mca"; ++ } ++ ++ 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; ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite( ++ final int chunkX, final int chunkZ, final CompoundTag compound ++ ) throws IOException { ++ if (compound == null) { ++ return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData( ++ compound, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.DELETE, ++ null, null ++ ); ++ } ++ ++ final ChunkPos pos = new ChunkPos(chunkX, chunkZ); ++ final RegionFile regionFile = this.getRegionFile(pos); ++ ++ // note: not required to keep regionfile loaded after this call, as the write param takes a regionfile as input ++ // (and, the regionfile parameter is unused for writing until the write call) ++ final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData writeData = ((ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile)regionFile).moonrise$startWrite(compound, pos); ++ ++ try { ++ NbtIo.write(compound, writeData.output()); ++ } finally { ++ writeData.output().close(); ++ } ++ ++ return writeData; ++ } ++ ++ @Override ++ public final void moonrise$finishWrite( ++ final int chunkX, final int chunkZ, final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData writeData ++ ) throws IOException { ++ final ChunkPos pos = new ChunkPos(chunkX, chunkZ); ++ if (writeData.result() == ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.DELETE) { ++ final RegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); ++ if (regionFile != null) { ++ regionFile.clear(pos); ++ } // else: didn't exist ++ ++ return; ++ } ++ ++ writeData.write().run(this.getRegionFile(pos)); ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData moonrise$readData( ++ final int chunkX, final int chunkZ ++ ) throws IOException { ++ final RegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); ++ ++ final DataInputStream input = regionFile == null ? null : regionFile.getChunkDataInputStream(new ChunkPos(chunkX, chunkZ)); ++ ++ if (input == null) { ++ return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData( ++ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData.ReadResult.NO_DATA, null, null ++ ); ++ } ++ ++ final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData ret = new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData( ++ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData.ReadResult.HAS_DATA, input, null ++ ); ++ ++ if (!(input instanceof ca.spottedleaf.moonrise.patches.chunk_system.util.stream.ExternalChunkStreamMarker)) { ++ // internal stream, which is fully read ++ return ret; ++ } ++ ++ final CompoundTag syncRead = this.moonrise$finishRead(chunkX, chunkZ, ret); ++ ++ if (syncRead == null) { ++ // need to try again ++ return this.moonrise$readData(chunkX, chunkZ); ++ } ++ ++ return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData( ++ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData.ReadResult.SYNC_READ, null, syncRead ++ ); ++ } ++ ++ // if the return value is null, then the caller needs to re-try with a new call to readData() ++ @Override ++ public final CompoundTag moonrise$finishRead( ++ final int chunkX, final int chunkZ, final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData readData ++ ) throws IOException { ++ try { ++ return NbtIo.read(readData.input()); ++ } finally { ++ readData.input().close(); ++ } ++ } ++ // Paper end - rewrite chunk system ++ ++ 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); ++ // Paper start - rewrite chunk system ++ public RegionFile getRegionFile(ChunkPos chunkcoordintpair) throws IOException { ++ return this.getRegionFile(chunkcoordintpair, false); ++ } ++ // Paper end - rewrite chunk system + +- 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(); ++ 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); ++ ++ 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 + } + + // Paper start +@@ -175,8 +361,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 - rewrite chunk system - 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 + + if (nbt == null) { + regionfile.clear(pos); +@@ -206,30 +398,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 93972352cd4881dccba9b90ccc8dcced3563e340..c3beb7fcad46a917d2b61bd0a0e98e5106056728 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 +@@ -40,10 +40,10 @@ import net.minecraft.world.level.ChunkPos; + import net.minecraft.world.level.LevelHeightAccessor; + import org.slf4j.Logger; + +-public class SectionStorage<R, P> implements AutoCloseable { ++public class SectionStorage<R, P> implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage { // Paper - rewrite chunk system + 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 dirtyChunks = new LongLinkedOpenHashSet(); + private final Codec<P> codec; +@@ -57,6 +57,18 @@ public class SectionStorage<R, P> implements AutoCloseable { + private final Long2ObjectMap<CompletableFuture<Optional<SectionStorage.PackedChunk<P>>>> pendingLoads = new Long2ObjectOpenHashMap<>(); + private final Object loadLock = new Object(); + ++ // Paper start - rewrite chunk system ++ private final RegionFileStorage regionStorage; ++ ++ @Override ++ public final RegionFileStorage moonrise$getRegionStorage() { ++ return this.regionStorage; ++ } ++ ++ @Override ++ public void moonrise$close() throws IOException {} ++ // Paper end - rewrite chunk system ++ + public SectionStorage( + SimpleRegionStorage storageAccess, + Codec<P> codec, +@@ -67,7 +79,7 @@ public class SectionStorage<R, P> implements AutoCloseable { + ChunkIOErrorReporter errorHandler, + LevelHeightAccessor world + ) { +- this.simpleRegionStorage = storageAccess; ++ // Paper - rewrite chunk system + this.codec = codec; + this.packer = serializer; + this.unpacker = deserializer; +@@ -75,6 +87,7 @@ public class SectionStorage<R, P> implements AutoCloseable { + this.registryAccess = registryManager; + this.errorReporter = errorHandler; + this.levelHeightAccessor = world; ++ this.regionStorage = storageAccess.worker.storage; // Paper - rewrite chunk system + } + + protected void tick(BooleanSupplier shouldKeepTicking) { +@@ -188,60 +201,15 @@ public class SectionStorage<R, P> implements AutoCloseable { + } + + private CompletableFuture<Optional<SectionStorage.PackedChunk<P>>> tryRead(ChunkPos chunkPos) { +- RegistryOps<Tag> registryOps = this.registryAccess.createSerializationContext(NbtOps.INSTANCE); +- return this.simpleRegionStorage +- .read(chunkPos) +- .thenApplyAsync( +- chunkNbt -> chunkNbt.map( +- nbt -> SectionStorage.PackedChunk.parse(this.codec, registryOps, nbt, this.simpleRegionStorage, this.levelHeightAccessor) +- ), +- Util.backgroundExecutor().forName("parseSection") +- ) +- .exceptionally(throwable -> { +- if (throwable instanceof IOException iOException) { +- LOGGER.error("Error reading chunk {} data from disk", chunkPos, iOException); +- this.errorReporter.reportChunkLoadFailure(iOException, this.simpleRegionStorage.storageInfo(), chunkPos); +- return Optional.empty(); +- } else { +- throw new CompletionException(throwable); +- } +- }); ++ throw new IllegalStateException("Only chunk system can write state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system + } + + private void unpackChunk(ChunkPos chunkPos, @Nullable SectionStorage.PackedChunk<P> result) { +- if (result == null) { +- for (int i = this.levelHeightAccessor.getMinSectionY(); i <= this.levelHeightAccessor.getMaxSectionY(); i++) { +- this.storage.put(getKey(chunkPos, i), Optional.empty()); +- } +- } else { +- boolean bl = result.versionChanged(); +- +- for (int j = this.levelHeightAccessor.getMinSectionY(); j <= this.levelHeightAccessor.getMaxSectionY(); j++) { +- long l = getKey(chunkPos, j); +- Optional<R> optional = Optional.ofNullable(result.sectionsByY.get(j)).map(section -> this.unpacker.apply((P)section, () -> this.setDirty(l))); +- this.storage.put(l, optional); +- optional.ifPresent(object -> { +- this.onSectionLoad(l); +- if (bl) { +- this.setDirty(l); +- } +- }); +- } +- } ++ throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system + } + + private void writeChunk(ChunkPos pos) { +- RegistryOps<Tag> registryOps = this.registryAccess.createSerializationContext(NbtOps.INSTANCE); +- Dynamic<Tag> dynamic = this.writeChunk(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; +- }); +- } else { +- LOGGER.error("Expected compound tag, got {}", tag); +- } ++ throw new IllegalStateException("Only chunk system can write state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system + } + + private <T> Dynamic<T> writeChunk(ChunkPos chunkPos, DynamicOps<T> ops) { +@@ -277,7 +245,7 @@ public class SectionStorage<R, P> 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.dirtyChunks.add(ChunkPos.asLong(SectionPos.x(pos), SectionPos.z(pos))); +@@ -298,7 +266,7 @@ public class SectionStorage<R, P> implements AutoCloseable { + + @Override + public void close() throws IOException { +- this.simpleRegionStorage.close(); ++ this.moonrise$close(); // Paper - rewrite chunk system + } + + static record PackedChunk<T>(Int2ObjectMap<T> sectionsByY, boolean versionChanged) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +index 4bc7fa3324e9af3abce2acf960c7b0650aca2e36..0296f52fb2c871adbf2ce73a64d8f77fab826cd7 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +@@ -129,7 +129,7 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun + long j = nbt.getLong("InhabitedTime"); + ChunkStatus chunkstatus = ChunkStatus.byName(nbt.getString("Status")); + UpgradeData chunkconverter = nbt.contains("UpgradeData", 10) ? new UpgradeData(nbt.getCompound("UpgradeData"), world) : UpgradeData.EMPTY; +- boolean flag = nbt.getBoolean("isLightOn"); ++ boolean flag = chunkstatus.isOrAfter(ChunkStatus.LIGHT) && (nbt.get("isLightOn") != null && nbt.getInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_VERSION_TAG) == ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_LIGHT_VERSION); // Paper - starlight + DataResult dataresult; + Logger logger; + BlendingData.Packed blendingdata_d; +@@ -246,7 +246,17 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun + DataLayer nibblearray = nbttagcompound3.contains("BlockLight", 7) ? new DataLayer(nbttagcompound3.getByteArray("BlockLight")) : null; + DataLayer nibblearray1 = nbttagcompound3.contains("SkyLight", 7) ? new DataLayer(nbttagcompound3.getByteArray("SkyLight")) : null; + +- list4.add(new SerializableChunkData.SectionData(b0, chunksection, nibblearray, nibblearray1)); ++ // Paper start - starlight ++ SerializableChunkData.SectionData serializableChunkData = new SerializableChunkData.SectionData(b0, chunksection, nibblearray, nibblearray1); ++ if (sectionData.contains(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.BLOCKLIGHT_STATE_TAG, Tag.TAG_ANY_NUMERIC)) { ++ ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)serializableChunkData).starlight$setBlockLightState(sectionData.getInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.BLOCKLIGHT_STATE_TAG)); ++ } ++ ++ if (sectionData.contains(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.SKYLIGHT_STATE_TAG, Tag.TAG_ANY_NUMERIC)) { ++ ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)serializableChunkData).starlight$setSkyLightState(sectionData.getInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.SKYLIGHT_STATE_TAG)); ++ } ++ list4.add(serializableChunkData); ++ // Paper end - starlight + } + + // CraftBukkit - ChunkBukkitValues +@@ -254,6 +264,59 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun + } + } + ++ // Paper start - starlight ++ private ProtoChunk loadStarlightLightData(final ServerLevel world, final ProtoChunk ret) { ++ ++ final boolean hasSkyLight = world.dimensionType().hasSkyLight(); ++ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinLightSection(world); ++ ++ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles = ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(world); ++ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] skyNibbles = ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(world); ++ ++ if (!this.lightCorrect) { ++ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)ret).starlight$setBlockNibbles(blockNibbles); ++ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)ret).starlight$setSkyNibbles(skyNibbles); ++ return ret; ++ } ++ ++ try { ++ for (final SerializableChunkData.SectionData sectionData : this.sectionData) { ++ final int y = sectionData.y(); ++ final DataLayer blockLight = sectionData.blockLight(); ++ final DataLayer skyLight = sectionData.skyLight(); ++ ++ final int blockState = ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$getBlockLightState(); ++ final int skyState = ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$getSkyLightState(); ++ ++ if (blockState >= 0) { ++ if (blockLight != null) { ++ blockNibbles[y - minSection] = new ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray(ca.spottedleaf.moonrise.common.util.MixinWorkarounds.clone(blockLight.getData()), blockState); // clone for data safety ++ } else { ++ blockNibbles[y - minSection] = new ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray(null, blockState); ++ } ++ } ++ ++ if (skyState >= 0 && hasSkyLight) { ++ if (skyLight != null) { ++ skyNibbles[y - minSection] = new ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray(ca.spottedleaf.moonrise.common.util.MixinWorkarounds.clone(skyLight.getData()), skyState); // clone for data safety ++ } else { ++ skyNibbles[y - minSection] = new ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray(null, skyState); ++ } ++ } ++ } ++ ++ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)ret).starlight$setBlockNibbles(blockNibbles); ++ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)ret).starlight$setSkyNibbles(skyNibbles); ++ } catch (final Throwable thr) { ++ ret.setLightCorrect(false); ++ ++ LOGGER.error("Failed to parse light data for chunk " + ret.getPos() + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(world) + "'", thr); ++ } ++ ++ return ret; ++ } ++ // Paper end - starlight ++ + public ProtoChunk read(ServerLevel world, PoiManager poiStorage, RegionStorageInfo key, ChunkPos expectedPos) { + if (!Objects.equals(expectedPos, this.chunkPos)) { + SerializableChunkData.LOGGER.error("Chunk file at {} is in the wrong location; relocating. (Expected {}, got {})", new Object[]{expectedPos, expectedPos, this.chunkPos}); +@@ -275,7 +338,7 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun + + if (serializablechunkdata_b.chunkSection != null) { + achunksection[world.getSectionIndexFromSectionY(serializablechunkdata_b.y)] = serializablechunkdata_b.chunkSection; +- poiStorage.checkConsistencyWithBlocks(sectionposition, serializablechunkdata_b.chunkSection); ++ //poiStorage.checkConsistencyWithBlocks(sectionposition, serializablechunkdata_b.chunkSection); // Paper - rewrite chunk system + } + + boolean flag2 = serializablechunkdata_b.blockLight != null; +@@ -352,7 +415,7 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun + } + + if (chunktype == ChunkType.LEVELCHUNK) { +- return new ImposterProtoChunk((LevelChunk) object, false); ++ return this.loadStarlightLightData(world, new ImposterProtoChunk((LevelChunk) object, false)); // Paper - starlight + } else { + ProtoChunk protochunk1 = (ProtoChunk) object; + Iterator iterator2 = this.entities.iterator(); +@@ -382,7 +445,7 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun + protochunk1.setCarvingMask(new CarvingMask(this.carvingMask, ((ChunkAccess) object).getMinY())); + } + +- return protochunk1; ++ return this.loadStarlightLightData(world, protochunk1); // Paper - starlight + } + } + +@@ -405,24 +468,48 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun + throw new IllegalArgumentException("Chunk can't be serialized: " + String.valueOf(chunk)); + } else { + ChunkPos chunkcoordintpair = chunk.getPos(); +- List<SerializableChunkData.SectionData> list = new ArrayList(); ++ List<SerializableChunkData.SectionData> list = new ArrayList(); final List<SerializableChunkData.SectionData> sections = list; // Paper - starlight - OBFHELPER + LevelChunkSection[] achunksection = chunk.getSections(); + ThreadedLevelLightEngine lightenginethreaded = world.getChunkSource().getLightEngine(); + +- for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) { +- int j = chunk.getSectionIndexFromSectionY(i); +- boolean flag = j >= 0 && j < achunksection.length; +- DataLayer nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); +- DataLayer nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); +- DataLayer nibblearray2 = nibblearray != null && !nibblearray.isEmpty() ? nibblearray.copy() : null; +- DataLayer nibblearray3 = nibblearray1 != null && !nibblearray1.isEmpty() ? nibblearray1.copy() : null; ++ // Paper start - starlight ++ final int minLightSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinLightSection(world); ++ final int maxLightSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxLightSection(world); ++ final int minBlockSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(world); ++ ++ final LevelChunkSection[] chunkSections = chunk.getSections(); ++ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles = ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)chunk).starlight$getBlockNibbles(); ++ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] skyNibbles = ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)chunk).starlight$getSkyNibbles(); ++ ++ for (int lightSection = minLightSection; lightSection <= maxLightSection; ++lightSection) { ++ final int lightSectionIdx = lightSection - minLightSection; ++ final int blockSectionIdx = lightSection - minBlockSection; + +- if (flag || nibblearray2 != null || nibblearray3 != null) { +- LevelChunkSection chunksection = flag ? achunksection[j].copy() : null; ++ final LevelChunkSection chunkSection = (blockSectionIdx >= 0 && blockSectionIdx < chunkSections.length) ? chunkSections[blockSectionIdx].copy() : null; ++ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray.SaveState blockNibble = blockNibbles[lightSectionIdx].getSaveState(); ++ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray.SaveState skyNibble = skyNibbles[lightSectionIdx].getSaveState(); + +- list.add(new SerializableChunkData.SectionData(i, chunksection, nibblearray2, nibblearray3)); ++ if (chunkSection == null && blockNibble == null && skyNibble == null) { ++ continue; + } ++ ++ final SerializableChunkData.SectionData sectionData = new SerializableChunkData.SectionData( ++ lightSection, chunkSection, ++ blockNibble == null ? null : (blockNibble.data == null ? null : new DataLayer(blockNibble.data)), ++ skyNibble == null ? null : (skyNibble.data == null ? null : new DataLayer(skyNibble.data)) ++ ); ++ ++ if (blockNibble != null) { ++ ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$setBlockLightState(blockNibble.state); ++ } ++ ++ if (skyNibble != null) { ++ ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$setSkyLightState(skyNibble.state); ++ } ++ ++ sections.add(sectionData); + } ++ // Paper end - starlight + + List<CompoundTag> list1 = new ArrayList(chunk.getBlockEntitiesPos().size()); + Iterator iterator = chunk.getBlockEntitiesPos().iterator(); +@@ -521,8 +608,8 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun + Iterator iterator = this.sectionData.iterator(); + + while (iterator.hasNext()) { +- SerializableChunkData.SectionData serializablechunkdata_b = (SerializableChunkData.SectionData) iterator.next(); +- CompoundTag nbttagcompound1 = new CompoundTag(); ++ SerializableChunkData.SectionData serializablechunkdata_b = (SerializableChunkData.SectionData) iterator.next(); final SerializableChunkData.SectionData sectionData = serializablechunkdata_b; // Paper - starlight - OBFHELPER ++ CompoundTag nbttagcompound1 = new CompoundTag(); final CompoundTag sectionNBT = nbttagcompound1; // Paper - starlight - OBFHELPER + LevelChunkSection chunksection = serializablechunkdata_b.chunkSection; + + if (chunksection != null) { +@@ -538,6 +625,19 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun + nbttagcompound1.putByteArray("SkyLight", serializablechunkdata_b.skyLight.getData()); + } + ++ // Paper start - starlight ++ final int blockState = ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$getBlockLightState(); ++ final int skyState = ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$getSkyLightState(); ++ ++ if (blockState > 0) { ++ sectionNBT.putInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.BLOCKLIGHT_STATE_TAG, blockState); ++ } ++ ++ if (skyState > 0) { ++ sectionNBT.putInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.SKYLIGHT_STATE_TAG, skyState); ++ } ++ // Paper end - starlight ++ + if (!nbttagcompound1.isEmpty()) { + nbttagcompound1.putByte("Y", (byte) serializablechunkdata_b.y); + nbttaglist.add(nbttagcompound1); +@@ -577,6 +677,14 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun + nbttagcompound.put("ChunkBukkitValues", this.persistentDataContainer); + } + // CraftBukkit end ++ // Paper start - starlight ++ if (this.lightCorrect && !this.chunkStatus.isBefore(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) { ++ // clobber vanilla value to force vanilla to relight ++ nbttagcompound.putBoolean("isLightOn", false); ++ // store our light version ++ nbttagcompound.putInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_VERSION_TAG, ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_LIGHT_VERSION); ++ } ++ // Paper end - starlight + return nbttagcompound; + } + +@@ -763,7 +871,67 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun + return nbttaglist; + } + +- public static record SectionData(int y, @Nullable LevelChunkSection chunkSection, @Nullable DataLayer blockLight, @Nullable DataLayer skyLight) { ++ // Paper start - starlight - convert from record ++ public static final class SectionData implements ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData { // Paper - starlight - our diff ++ private final int y; ++ @javax.annotation.Nullable ++ private final net.minecraft.world.level.chunk.LevelChunkSection chunkSection; ++ @javax.annotation.Nullable ++ private final net.minecraft.world.level.chunk.DataLayer blockLight; ++ @javax.annotation.Nullable ++ private final net.minecraft.world.level.chunk.DataLayer skyLight; ++ ++ // Paper start - starlight - our diff ++ private int blockLightState = -1; ++ private int skyLightState = -1; ++ ++ @Override ++ public final int starlight$getBlockLightState() { ++ return this.blockLightState; ++ } ++ ++ @Override ++ public final void starlight$setBlockLightState(final int state) { ++ this.blockLightState = state; ++ } ++ ++ @Override ++ public final int starlight$getSkyLightState() { ++ return this.skyLightState; ++ } ++ ++ @Override ++ public final void starlight$setSkyLightState(final int state) { ++ this.skyLightState = state; ++ } ++ // Paper end - starlight - our diff ++ ++ public SectionData(int y, @javax.annotation.Nullable net.minecraft.world.level.chunk.LevelChunkSection chunkSection, @javax.annotation.Nullable net.minecraft.world.level.chunk.DataLayer blockLight, @javax.annotation.Nullable net.minecraft.world.level.chunk.DataLayer skyLight) { ++ this.y = y; ++ this.chunkSection = chunkSection; ++ this.blockLight = blockLight; ++ this.skyLight = skyLight; ++ } ++ ++ public int y() { ++ return y; ++ } ++ ++ @javax.annotation.Nullable ++ public net.minecraft.world.level.chunk.LevelChunkSection chunkSection() { ++ return chunkSection; ++ } ++ ++ @javax.annotation.Nullable ++ public net.minecraft.world.level.chunk.DataLayer blockLight() { ++ return blockLight; ++ } ++ ++ @javax.annotation.Nullable ++ public net.minecraft.world.level.chunk.DataLayer skyLight() { ++ return skyLight; ++ } ++ // Paper end - starlight - convert from record + + } + +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 578d270d5b7efb9ac8f5dde539170f6021e2b786..c5085ebf4e801837010f3750c5e89576bb0c27a5 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 1fcc2b287ed723cf51720f80e68f18f4a15cf429..3f39d6c786d9dfdd9ad591e08ff05fcbb41a1df6 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(() -> { + this.doCreateBiomes(blender, noiseConfig, structureAccessor, chunk); + return chunk; +- }, Util.backgroundExecutor().forName("init_biomes")); ++ }, 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().forName("wgen_fill_noise")); ++ }, 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 c3586281c9594769593a6027ea0a78f7c76c0262..decdb275e83fa6244aa3a24458872b42c49d04ed 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 8d90e783967280025d711c709facbcc87f611f8a..987e3397503cd07d3a2f172cede341297bc58dba 100644 +--- a/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java ++++ b/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java +@@ -9,151 +9,111 @@ 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 { // Paper - rewrite chunk system + public static final int LIGHT_SECTION_PADDING = 1; + public static final LevelLightEngine EMPTY = new LevelLightEngine(); + 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 + } + + private LevelLightEngine() { + this.levelHeightAccessor = LevelHeightAccessor.create(0, 0); +- this.blockEngine = null; +- this.skyEngine = null; ++ // Paper start - rewrite chunk system ++ this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(null, false, false, (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 lightOnInColumn(long sectionPos) { +- return this.blockEngine == null +- || this.blockEngine.storage.lightOnInColumn(sectionPos) && (this.skyEngine == null || this.skyEngine.storage.lightOnInColumn(sectionPos)); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system // Paper - not implemented on server + } + + public int getLightSectionCount() { +diff --git a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +index 261e5994d13f8bc30490b86691c80c0a21e7640a..f4fbcbb8ff6d2677af1a02a0801a323c06dce9b1 100644 +--- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java ++++ b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +@@ -55,6 +55,48 @@ public abstract class FlowingFluid extends Fluid { + }); + private final Map<FluidState, VoxelShape> shapes = Maps.newIdentityHashMap(); + ++ // Paper start - fluid method optimisations ++ private FluidState sourceFalling; ++ private FluidState sourceNotFalling; ++ ++ private static final int TOTAL_FLOWING_STATES = FALLING.getPossibleValues().size() * LEVEL.getPossibleValues().size(); ++ private static final int MIN_LEVEL = LEVEL.getPossibleValues().stream().sorted().findFirst().get().intValue(); ++ ++ // index = (falling ? 1 : 0) + level*2 ++ private FluidState[] flowingLookUp; ++ private volatile boolean init; ++ ++ private static final int COLLISION_OCCLUSION_CACHE_SIZE = 2048; ++ private static final ThreadLocal<ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey[]> COLLISION_OCCLUSION_CACHE = ThreadLocal.withInitial(() -> new ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey[COLLISION_OCCLUSION_CACHE_SIZE]); ++ ++ ++ /** ++ * Due to init order, we need to use callbacks to initialise our state ++ */ ++ private void init() { ++ synchronized (this) { ++ if (this.init) { ++ return; ++ } ++ this.flowingLookUp = new FluidState[TOTAL_FLOWING_STATES]; ++ final FluidState defaultFlowState = this.getFlowing().defaultFluidState(); ++ for (int i = 0; i < TOTAL_FLOWING_STATES; ++i) { ++ final int falling = i & 1; ++ final int level = (i >>> 1) + MIN_LEVEL; ++ ++ this.flowingLookUp[i] = defaultFlowState.setValue(FALLING, falling == 1 ? Boolean.TRUE : Boolean.FALSE) ++ .setValue(LEVEL, Integer.valueOf(level)); ++ } ++ ++ final FluidState defaultFallState = this.getSource().defaultFluidState(); ++ this.sourceFalling = defaultFallState.setValue(FALLING, Boolean.TRUE); ++ this.sourceNotFalling = defaultFallState.setValue(FALLING, Boolean.FALSE); ++ ++ this.init = true; ++ } ++ } ++ // Paper end - fluid method optimisations ++ + public FlowingFluid() {} + + @Override +@@ -246,65 +288,70 @@ public abstract class FlowingFluid extends Fluid { + } + } + +- private static boolean canPassThroughWall(Direction face, BlockGetter world, BlockPos pos, BlockState state, BlockPos fromPos, BlockState fromState) { +- VoxelShape voxelshape = fromState.getCollisionShape(world, fromPos); ++ // Paper start - fluid method optimisations ++ private static boolean canPassThroughWall(final Direction direction, final BlockGetter level, ++ final BlockPos fromPos, final BlockState fromState, ++ final BlockPos toPos, final BlockState toState) { ++ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$emptyCollisionShape() & ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$emptyCollisionShape()) { ++ // don't even try to cache simple cases ++ return true; ++ } + +- if (voxelshape == Shapes.block()) { ++ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$occludesFullBlock() | ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$occludesFullBlock()) { ++ // don't even try to cache simple cases + return false; +- } else { +- VoxelShape voxelshape1 = state.getCollisionShape(world, pos); +- +- if (voxelshape1 == Shapes.block()) { +- return false; +- } else if (voxelshape1 == Shapes.empty() && voxelshape == Shapes.empty()) { +- return true; +- } else { +- Object2ByteLinkedOpenHashMap object2bytelinkedopenhashmap; +- +- if (!state.getBlock().hasDynamicShape() && !fromState.getBlock().hasDynamicShape()) { +- object2bytelinkedopenhashmap = (Object2ByteLinkedOpenHashMap) FlowingFluid.OCCLUSION_CACHE.get(); +- } else { +- object2bytelinkedopenhashmap = null; +- } ++ } + +- FlowingFluid.BlockStatePairKey fluidtypeflowing_a; ++ final ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey[] cache = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$hasCache() & ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$hasCache() ? ++ COLLISION_OCCLUSION_CACHE.get() : null; + +- if (object2bytelinkedopenhashmap != null) { +- fluidtypeflowing_a = new FlowingFluid.BlockStatePairKey(state, fromState, face); +- byte b0 = object2bytelinkedopenhashmap.getAndMoveToFirst(fluidtypeflowing_a); ++ final int keyIndex ++ = (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$uniqueId1() ^ ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$uniqueId2() ^ ((ca.spottedleaf.moonrise.patches.collisions.util.CollisionDirection)(Object)direction).moonrise$uniqueId()) ++ & (COLLISION_OCCLUSION_CACHE_SIZE - 1); + +- if (b0 != 127) { +- return b0 != 0; +- } +- } else { +- fluidtypeflowing_a = null; +- } +- +- boolean flag = !Shapes.mergedFaceOccludes(voxelshape1, voxelshape, face); ++ if (cache != null) { ++ final ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey cached = cache[keyIndex]; ++ if (cached != null && cached.first() == fromState && cached.second() == toState && cached.direction() == direction) { ++ return cached.result(); ++ } ++ } + +- if (object2bytelinkedopenhashmap != null) { +- if (object2bytelinkedopenhashmap.size() == 200) { +- object2bytelinkedopenhashmap.removeLastByte(); +- } ++ final VoxelShape shape1 = fromState.getCollisionShape(level, fromPos); ++ final VoxelShape shape2 = toState.getCollisionShape(level, toPos); + +- object2bytelinkedopenhashmap.putAndMoveToFirst(fluidtypeflowing_a, (byte) (flag ? 1 : 0)); +- } ++ final boolean result = !Shapes.mergedFaceOccludes(shape1, shape2, direction); + +- return flag; +- } ++ if (cache != null) { ++ // we can afford to replace in-use keys more often due to the excessive caching the collision patch does in mergedFaceOccludes ++ cache[keyIndex] = new ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey(fromState, toState, direction, result); + } ++ ++ return result; + } ++ // Paper end - fluid method optimisations + + public abstract Fluid getFlowing(); + + public FluidState getFlowing(int level, boolean falling) { +- return (FluidState) ((FluidState) this.getFlowing().defaultFluidState().setValue(FlowingFluid.LEVEL, level)).setValue(FlowingFluid.FALLING, falling); ++ // Paper start - fluid method optimisations ++ final int amount = level; ++ if (!this.init) { ++ this.init(); ++ } ++ final int index = (falling ? 1 : 0) | ((amount - MIN_LEVEL) << 1); ++ return this.flowingLookUp[index]; ++ // Paper end - fluid method optimisations + } + + public abstract Fluid getSource(); + + public FluidState getSource(boolean falling) { +- return (FluidState) this.getSource().defaultFluidState().setValue(FlowingFluid.FALLING, falling); ++ // Paper start - fluid method optimisations ++ if (!this.init) { ++ this.init(); ++ } ++ return falling ? this.sourceFalling : this.sourceNotFalling; ++ // Paper end - fluid method optimisations + } + + protected abstract boolean canConvertToSource(ServerLevel world); +diff --git a/src/main/java/net/minecraft/world/level/material/FluidState.java b/src/main/java/net/minecraft/world/level/material/FluidState.java +index 87adfe152abd1b8b4d547034576883c5d1cdf134..2d50d72bf026d0cf9c546a3c6fc1859379bfd805 100644 +--- a/src/main/java/net/minecraft/world/level/material/FluidState.java ++++ b/src/main/java/net/minecraft/world/level/material/FluidState.java +@@ -22,12 +22,30 @@ import net.minecraft.world.level.block.state.properties.Property; + import net.minecraft.world.phys.Vec3; + import net.minecraft.world.phys.shapes.VoxelShape; + +-public final class FluidState extends StateHolder<Fluid, FluidState> { ++public final class FluidState extends StateHolder<Fluid, FluidState> implements ca.spottedleaf.moonrise.patches.fluid.FluidFluidState { // Paper - fluid method optimisations + public static final Codec<FluidState> CODEC = codec(BuiltInRegistries.FLUID.byNameCodec(), Fluid::defaultFluidState).stable(); + public static final int AMOUNT_MAX = 9; + public static final int AMOUNT_FULL = 8; + protected final boolean isEmpty; // Paper - Perf: moved from isEmpty() + ++ // Paper start - fluid method optimisations ++ private int amount; ++ //private boolean isEmpty; ++ private boolean isSource; ++ private float ownHeight; ++ private boolean isRandomlyTicking; ++ private BlockState legacyBlock; ++ ++ @Override ++ public final void moonrise$initCaches() { ++ this.amount = this.getType().getAmount((FluidState)(Object)this); ++ //this.isEmpty = this.getType().isEmpty(); ++ this.isSource = this.getType().isSource((FluidState)(Object)this); ++ this.ownHeight = this.getType().getOwnHeight((FluidState)(Object)this); ++ this.isRandomlyTicking = this.getType().isRandomlyTicking(); ++ } ++ // Paper end - fluid method optimisations ++ + public FluidState(Fluid fluid, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<FluidState> codec) { + super(fluid, propertyMap, codec); + this.isEmpty = fluid.isEmpty(); // Paper - Perf: moved from isEmpty() +@@ -38,11 +56,11 @@ public final class FluidState extends StateHolder<Fluid, FluidState> { + } + + public boolean isSource() { +- return this.getType().isSource(this); ++ return this.isSource; // Paper - fluid method optimisations + } + + public boolean isSourceOfType(Fluid fluid) { +- return this.owner == fluid && this.owner.isSource(this); ++ return this.isSource && this.owner == fluid; // Paper - fluid method optimisations + } + + public boolean isEmpty() { +@@ -54,11 +72,11 @@ public final class FluidState extends StateHolder<Fluid, FluidState> { + } + + public float getOwnHeight() { +- return this.getType().getOwnHeight(this); ++ return this.ownHeight; // Paper - fluid method optimisations + } + + public int getAmount() { +- return this.getType().getAmount(this); ++ return this.amount; // Paper - fluid method optimisations + } + + public boolean shouldRenderBackwardUpFace(BlockGetter world, BlockPos pos) { +@@ -84,7 +102,7 @@ public final class FluidState extends StateHolder<Fluid, FluidState> { + } + + public boolean isRandomlyTicking() { +- return this.getType().isRandomlyTicking(); ++ return this.isRandomlyTicking; // Paper - fluid method optimisations + } + + public void randomTick(ServerLevel world, BlockPos pos, RandomSource random) { +@@ -96,7 +114,12 @@ public final class FluidState extends StateHolder<Fluid, FluidState> { + } + + public BlockState createLegacyBlock() { +- return this.getType().createLegacyBlock(this); ++ // Paper start - fluid method optimisations ++ if (this.legacyBlock != null) { ++ return this.legacyBlock; ++ } ++ return this.legacyBlock = this.getType().createLegacyBlock((FluidState)(Object)this); ++ // Paper end - fluid method optimisations + } + + @Nullable +diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java +index 5dc2674b537f4a61b2e21a21bdb2e8dc090d3a3c..6cf6d4ec7b9e43c7b2b4c0e2fb080964ff588130 100644 +--- a/src/main/java/net/minecraft/world/phys/AABB.java ++++ b/src/main/java/net/minecraft/world/phys/AABB.java +@@ -331,7 +331,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 + ) { + return getDirection( +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..fbe0c4b0fdbb992b7002f6afe1e74d63cbb420f2 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 && (voxelSet[0] & 1L) != 0L; ++ ++ 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 76d7435e6fe81a3f1d24b35eae72d06232a1792b..ca3a2419252721bb3b3b719eb19afb5f175394c0 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}) ++ 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 }) + ); +- } 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) +- ); +- 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.joinUnoptimized(first, second, BooleanOp.OR); ++ } ++ } ++ size = newSize; ++ } ++ ++ return tmp[0].optimize(); ++ // 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,51 +226,116 @@ 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 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 ++ 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(); + +- if (!DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7)) { +- voxelShape2 = empty(); +- } ++ 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); + +- return !joinIsNotEmpty( +- block(), +- joinUnoptimized(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), BooleanOp.OR), +- BooleanOp.ONLY_FIRST +- ); +- } else { ++ 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 ++ ++ // 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 (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); ++ 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 bcb79462c8b3309ae8701cba4753b27a9d22eb2e..3f8e7e29c3e52211a29e6f0a32890f6b53bfd9a8 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java +@@ -15,61 +15,546 @@ 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 = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.sliceShape((VoxelShape)(Object)this, axis, this.shape.getSize(axis) - 1); ++ } else { ++ ret = Shapes.empty(); ++ } ++ } else { ++ if (DoubleMath.fuzzyEquals(this.min(axis), 0.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) { ++ ret = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.sliceShape((VoxelShape)(Object)this, axis, 0); ++ } else { ++ ret = Shapes.empty(); ++ } ++ } ++ ++ cache[direction.ordinal()] = ret; ++ ++ return ret; ++ } ++ ++ 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 double[] src, final double by) { ++ final it.unimi.dsi.fastutil.doubles.DoubleArrayList wrap = it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(src); ++ if (by == 0.0) { ++ return wrap; ++ } ++ return new OffsetDoubleList(wrap, by); ++ } ++ ++ private List<AABB> toAabbsUncached() { ++ final List<AABB> ret; ++ if (this.singleAABBRepresentation != null) { ++ ret = new java.util.ArrayList<>(1); ++ ret.add(this.singleAABBRepresentation); ++ } else { ++ ret = new java.util.ArrayList<>(); ++ final double[] coordsX = this.rootCoordinatesX; ++ final double[] coordsY = this.rootCoordinatesY; ++ final double[] coordsZ = this.rootCoordinatesZ; ++ ++ 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); ++ } ++ ++ private VoxelShape calculateFaceDirect(final Direction direction, final Direction.Axis axis, final double[] coords, final double offset) { ++ if (coords.length == 2 && ++ DoubleMath.fuzzyEquals(coords[0] + offset, 0.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) && ++ DoubleMath.fuzzyEquals(coords[1] + offset, 1.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) { ++ return (VoxelShape)(Object)this; ++ } ++ ++ final boolean positiveDir = direction.getAxisDirection() == Direction.AxisDirection.POSITIVE; ++ ++ // see findIndex ++ final int index = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor( ++ coords, (positiveDir ? (1.0 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) : (0.0 + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) - offset, ++ 0, coords.length - 1 ++ ); ++ ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.sliceShape( ++ (VoxelShape)(Object)this, axis, index ++ ); ++ } ++ // Paper end - optimise collisions ++ + protected VoxelShape(DiscreteVoxelShape voxels) { + 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() { +- return this.isEmpty() +- ? Shapes.empty() +- : Shapes.box( +- this.min(Direction.Axis.X), +- this.min(Direction.Axis.Y), +- this.min(Direction.Axis.Z), +- this.max(Direction.Axis.X), +- this.max(Direction.Axis.Y), +- this.max(Direction.Axis.Z) +- ); ++ // Paper start - optimise collisions ++ if (this.isEmpty) { ++ return Shapes.empty(); ++ } ++ return Shapes.create(this.bounds()); ++ // Paper end - optimise collisions + } + + protected double get(Direction.Axis axis, int index) { +- return this.getCoords(axis).getDouble(index); ++ // Paper start - optimise collisions ++ final int idx = index; ++ switch (axis) { ++ case X: { ++ return this.rootCoordinatesX[idx] + this.offsetX; ++ } ++ case Y: { ++ return this.rootCoordinatesY[idx] + this.offsetY; ++ } ++ case Z: { ++ return this.rootCoordinatesZ[idx] + this.offsetZ; ++ } ++ default: { ++ throw new IllegalStateException("Unknown axis: " + axis); ++ } ++ } ++ // Paper end - optimise collisions + } + + public abstract DoubleList getCoords(Direction.Axis axis); + + public boolean isEmpty() { +- return this.shape.isEmpty(); ++ return this.isEmpty; // Paper - optimise collisions + } + + public VoxelShape move(Vec3 vec3d) { +@@ -77,24 +562,96 @@ public abstract class VoxelShape { + } + + 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.rootCoordinatesX, this.offsetX + x), ++ offsetList(this.rootCoordinatesY, this.offsetY + y), ++ offsetList(this.rootCoordinatesZ, this.offsetZ + z) ++ ); ++ ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cachedToAABBs = this.cachedToAABBs; ++ 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.isEmpty()) { ++ // We are a SliceShape, which does not properly fill isEmpty for every case ++ return Shapes.empty(); ++ } ++ ++ if (aabbs.size() == 1) { ++ final AABB singleAABB = aabbs.get(0); ++ final VoxelShape ret = Shapes.create(singleAABB); ++ ++ // 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) { +@@ -131,9 +688,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,46 +727,92 @@ public abstract class VoxelShape { + } + + protected int findIndex(Direction.Axis axis, double coord) { +- return Mth.binarySearch(0, this.shape.getSize(axis) + 1, i -> coord < this.get(axis, i)) - 1; ++ // Paper start - optimise collisions ++ final double value = coord; ++ switch (axis) { ++ case X: { ++ final double[] values = this.rootCoordinatesX; ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor( ++ values, value - this.offsetX, 0, values.length - 1 ++ ); ++ } ++ case Y: { ++ final double[] values = this.rootCoordinatesY; ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor( ++ values, value - this.offsetY, 0, values.length - 1 ++ ); ++ } ++ case Z: { ++ final double[] values = this.rootCoordinatesZ; ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor( ++ values, value - this.offsetZ, 0, values.length - 1 ++ ); ++ } ++ default: { ++ throw new IllegalStateException("Unknown axis: " + axis); ++ } ++ } ++ // Paper end - optimise collisions + } + + @Nullable +- 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.getApproximateNearest(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.getApproximateNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true); + } ++ return clip(singleAABB, from, to, offset); ++ } ++ ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.strictlyContains((VoxelShape)(Object)this, fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) { ++ return new BlockHitResult(fromBehind, Direction.getApproximateNearest(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) { +@@ -216,20 +834,24 @@ public abstract class VoxelShape { + } + } + +- private VoxelShape calculateFace(Direction facing) { +- Direction.Axis axis = facing.getAxis(); +- if (this.isCubeLikeAlong(axis)) { +- return this; +- } else { +- Direction.AxisDirection axisDirection = facing.getAxisDirection(); +- int i = this.findIndex(axis, axisDirection == Direction.AxisDirection.POSITIVE ? 0.9999999 : 1.0E-7); +- SliceShape sliceShape = new SliceShape(this, axis, i); +- if (sliceShape.isEmpty()) { +- return Shapes.empty(); +- } else { +- return (VoxelShape)(sliceShape.isCubeLike() ? Shapes.block() : sliceShape); ++ private VoxelShape calculateFace(Direction direction) { ++ // Paper start - optimise collisions ++ final Direction.Axis axis = direction.getAxis(); ++ switch (axis) { ++ case X: { ++ return this.calculateFaceDirect(direction, axis, this.rootCoordinatesX, this.offsetX); ++ } ++ case Y: { ++ return this.calculateFaceDirect(direction, axis, this.rootCoordinatesY, this.offsetY); ++ } ++ case Z: { ++ return this.calculateFaceDirect(direction, axis, this.rootCoordinatesZ, this.offsetZ); ++ } ++ default: { ++ throw new IllegalStateException("Unknown axis: " + axis); + } + } ++ // Paper end - optimise collisions + } + + protected boolean isCubeLike() { +@@ -249,9 +871,30 @@ public abstract class VoxelShape { + && DoubleMath.fuzzyEquals(doubleList.getDouble(1), 1.0, 1.0E-7); + } + +- 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) { + if (this.isEmpty()) { +diff --git a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java +index 26620c06d26a2c0eb957fbadc6ac3d7a309bff46..3858c83c58e78435a6e29de84c33faa2f26d593d 100644 +--- a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java ++++ b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java +@@ -17,7 +17,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; +@@ -25,6 +25,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() { + } + +@@ -49,7 +73,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; +@@ -58,7 +82,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 + } + } + +@@ -80,7 +104,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); + } + } +@@ -110,6 +134,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon + } + + public ListTag save(long time, Function<T, String> typeToNameFunction) { ++ this.lastSaved = time; // Paper - rewrite chunk system + ListTag listTag = new ListTag(); + + for (SavedTick<T> savedTick : this.pack(time)) { +@@ -121,6 +146,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 f65cc95ab28e8a3b21eac2b16bd9ebe97e56e571..0074bc0e7147dc3a8c538e796f14ac9bf8725896 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 +- ConsecutiveExecutor 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 87477ad186b5f692a39f323a618d016b669d8051..1df118625aeb57f3353c40f9552f00d2f34fc655 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -1444,7 +1444,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(); +@@ -1489,7 +1489,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); +@@ -2527,7 +2527,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 4a5a0e33af16369f665bf39e70238e4e5a5486da..696152286a4d16fa51a23ff6e15fb297f3476007 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -488,10 +488,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 + + // Paper start - Anti-Xray bypass + final Map<Object, ClientboundLevelChunkWithLightPacket> refreshPackets = new HashMap<>(); +@@ -504,8 +508,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + })); + // Paper end - Anti-Xray bypass + } +- }); +- }); ++ // Paper - chunk system + + return true; + } +@@ -609,20 +612,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.moonrise$getChunkHolderManager().getPluginChunkTickets(x, z); // Paper - rewrite chunk system + } + + @Override +@@ -630,7 +621,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.moonrise$getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { // Paper - rewrite chunk system + long chunkKey = chunkTickets.getLongKey(); + SortedArraySet<Ticket<?>> tickets = chunkTickets.getValue(); + +@@ -1333,12 +1324,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() { +@@ -2473,17 +2464,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 5989a9af8840e1bdb5c7a25a44473e2ab597e1e5..e1f20b5b9ebc5d9870136aa2c77d887094bd4b6e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -3513,7 +3513,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 +@@ -3523,7 +3525,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 +@@ -3533,7 +3537,9 @@ 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); + } + + // Paper start - entity effect API +diff --git a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java +index 625562a38bc78feae3ae4b50b9afefbd05ff767a..e34060c21755c61228ba91e468b7c92fc4c4cf0c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java ++++ b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java +@@ -262,7 +262,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 54c4434662d057a08800918641b95708cda61207..37458e8fd5d57acbf90a6bea4e66797cb07f69fa 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java +@@ -810,6 +810,13 @@ 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, "------------------------------" ); + // |