From 6186079231d3c0cc03c484c7932b6eb9ed3015b1 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Mon, 16 Dec 2024 10:42:50 -0800 Subject: Migrate ChunkSystem class to PaperHooks --- .../0018-Moonrise-optimisation-patches.patch | 36590 +++++++++++++++++++ .../patches/features/0001-Add-PaperHooks.patch | 345 +- .../0018-Moonrise-optimisation-patches.patch | 36330 ------------------ .../0020-Rewrite-dataconverter-system.patch | 4 +- .../minecraft/server/level/ChunkHolder.java.patch | 12 +- .../net/minecraft/server/level/ChunkMap.java.patch | 22 +- .../minecraft/server/level/ServerLevel.java.patch | 2 +- .../PersistentEntitySectionManager.java.patch | 2 +- .../spottedleaf/moonrise/common/PlatformHooks.java | 5 +- .../moonrise/common/util/ChunkSystem.java | 288 - .../moonrise/common/util/ChunkSystemHooks.java | 77 + .../moonrise/common/util/ThreadUnsafeRandom.java | 2 +- .../java/org/bukkit/craftbukkit/CraftWorld.java | 8 +- .../org/bukkit/craftbukkit/entity/CraftPlayer.java | 6 +- 14 files changed, 37040 insertions(+), 36653 deletions(-) create mode 100644 feature-patches/0018-Moonrise-optimisation-patches.patch delete mode 100644 paper-server/patches/features/0018-Moonrise-optimisation-patches.patch delete mode 100644 paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java create mode 100644 paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystemHooks.java diff --git a/feature-patches/0018-Moonrise-optimisation-patches.patch b/feature-patches/0018-Moonrise-optimisation-patches.patch new file mode 100644 index 0000000000..b20c8eacca --- /dev/null +++ b/feature-patches/0018-Moonrise-optimisation-patches.patch @@ -0,0 +1,36590 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +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/ca/spottedleaf/moonrise/common/PlatformHooks.java b/ca/spottedleaf/moonrise/common/PlatformHooks.java +index 6c98d420ea84c10ef4f15d4deb3f04e610ed8548..9b879cbc037a17ffeb9a963111fd3f303a935eef 100644 +--- a/ca/spottedleaf/moonrise/common/PlatformHooks.java ++++ b/ca/spottedleaf/moonrise/common/PlatformHooks.java +@@ -1,5 +1,6 @@ + package ca.spottedleaf.moonrise.common; + ++import ca.spottedleaf.moonrise.common.util.ChunkSystemHooks; + import com.mojang.datafixers.DSL; + import com.mojang.datafixers.DataFixer; + import net.minecraft.core.BlockPos; +@@ -23,7 +24,7 @@ import java.util.List; + import java.util.ServiceLoader; + import java.util.function.Predicate; + +-public interface PlatformHooks { ++public interface PlatformHooks extends ChunkSystemHooks { + public static PlatformHooks get() { + return Holder.INSTANCE; + } +@@ -63,8 +64,6 @@ public interface PlatformHooks { + + public void entityMove(final Entity entity, final long oldSection, final long newSection); + +- public boolean screenEntity(final ServerLevel world, final Entity entity, final boolean fromDisk, final boolean event); +- + public boolean configFixMC224294(); + + public boolean configAutoConfigSendDistance(); +diff --git a/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1b8193587814225c2ef2c5d9e667436eb50ff6c5 +--- /dev/null ++++ b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java +@@ -0,0 +1,273 @@ ++package ca.spottedleaf.moonrise.common.misc; ++ ++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.MoonriseConstants; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; ++import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants; ++import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; ++import net.minecraft.core.BlockPos; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.level.ChunkPos; ++import java.util.ArrayList; ++ ++public final class NearbyPlayers { ++ ++ public static enum NearbyMapType { ++ GENERAL, ++ GENERAL_SMALL, ++ GENERAL_REALLY_SMALL, ++ TICK_VIEW_DISTANCE, ++ VIEW_DISTANCE, ++ // Moonrise start - chunk tick iteration ++ SPAWN_RANGE { ++ @Override ++ void addTo(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { ++ ((ChunkTickServerLevel)world).moonrise$addPlayerTickingRequest(chunkX, chunkZ); ++ } ++ ++ @Override ++ void removeFrom(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { ++ ((ChunkTickServerLevel)world).moonrise$removePlayerTickingRequest(chunkX, chunkZ); ++ } ++ }; ++ // Moonrise end - chunk tick iteration ++ ++ void addTo(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { ++ ++ } ++ ++ void removeFrom(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { ++ ++ } ++ } ++ ++ private static final NearbyMapType[] MAP_TYPES = NearbyMapType.values(); ++ public static final int TOTAL_MAP_TYPES = MAP_TYPES.length; ++ ++ private static final int GENERAL_AREA_VIEW_DISTANCE = MoonriseConstants.MAX_VIEW_DISTANCE + 1; ++ private static final int GENERAL_SMALL_VIEW_DISTANCE = 10; ++ private static final int GENERAL_REALLY_SMALL_VIEW_DISTANCE = 3; ++ ++ public static final int GENERAL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_AREA_VIEW_DISTANCE << 4); ++ public static final int GENERAL_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_SMALL_VIEW_DISTANCE << 4); ++ public static final int GENERAL_REALLY_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_REALLY_SMALL_VIEW_DISTANCE << 4); ++ ++ private final ServerLevel world; ++ private final Reference2ReferenceOpenHashMap players = new Reference2ReferenceOpenHashMap<>(); ++ private final Long2ReferenceOpenHashMap byChunk = new Long2ReferenceOpenHashMap<>(); ++ private final Long2ReferenceOpenHashMap>[] directByChunk = new Long2ReferenceOpenHashMap[TOTAL_MAP_TYPES]; ++ { ++ for (int i = 0; i < this.directByChunk.length; ++i) { ++ this.directByChunk[i] = new Long2ReferenceOpenHashMap<>(); ++ } ++ } ++ ++ public NearbyPlayers(final ServerLevel world) { ++ this.world = world; ++ } ++ ++ public void addPlayer(final ServerPlayer player) { ++ final TrackedPlayer[] newTrackers = new TrackedPlayer[TOTAL_MAP_TYPES]; ++ if (this.players.putIfAbsent(player, newTrackers) != null) { ++ throw new IllegalStateException("Already have player " + player); ++ } ++ ++ final ChunkPos chunk = player.chunkPosition(); ++ ++ for (int i = 0; i < TOTAL_MAP_TYPES; ++i) { ++ // use 0 for default, will be updated by tickPlayer ++ (newTrackers[i] = new TrackedPlayer(player, MAP_TYPES[i])).add(chunk.x, chunk.z, 0); ++ } ++ ++ // update view distances ++ this.tickPlayer(player); ++ } ++ ++ public void removePlayer(final ServerPlayer player) { ++ final TrackedPlayer[] players = this.players.remove(player); ++ if (players == null) { ++ return; // May be called during teleportation before the player is actually placed ++ } ++ ++ for (final TrackedPlayer tracker : players) { ++ tracker.remove(); ++ } ++ } ++ ++ public void clear() { ++ if (this.players.isEmpty()) { ++ return; ++ } ++ ++ for (final ServerPlayer player : new ArrayList<>(this.players.keySet())) { ++ this.removePlayer(player); ++ } ++ } ++ ++ public void tickPlayer(final ServerPlayer player) { ++ final TrackedPlayer[] players = this.players.get(player); ++ if (players == null) { ++ throw new IllegalStateException("Don't have player " + player); ++ } ++ ++ final ChunkPos chunk = player.chunkPosition(); ++ ++ players[NearbyMapType.GENERAL.ordinal()].update(chunk.x, chunk.z, GENERAL_AREA_VIEW_DISTANCE); ++ players[NearbyMapType.GENERAL_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_SMALL_VIEW_DISTANCE); ++ players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE); ++ players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, PlatformHooks.get().getTickViewDistance(player)); ++ players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, PlatformHooks.get().getViewDistance(player)); ++ players[NearbyMapType.SPAWN_RANGE.ordinal()].update(chunk.x, chunk.z, ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Moonrise - chunk tick iteration ++ } ++ ++ public TrackedChunk getChunk(final ChunkPos pos) { ++ return this.byChunk.get(CoordinateUtils.getChunkKey(pos)); ++ } ++ ++ public TrackedChunk getChunk(final BlockPos pos) { ++ return this.byChunk.get(CoordinateUtils.getChunkKey(pos)); ++ } ++ ++ public TrackedChunk getChunk(final int chunkX, final int chunkZ) { ++ return this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ ++ public ReferenceList getPlayers(final BlockPos pos, final NearbyMapType type) { ++ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos)); ++ } ++ ++ public ReferenceList getPlayers(final ChunkPos pos, final NearbyMapType type) { ++ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos)); ++ } ++ ++ public ReferenceList getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) { ++ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ ++ public ReferenceList getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) { ++ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4)); ++ } ++ ++ public static final class TrackedChunk { ++ ++ private static final ServerPlayer[] EMPTY_PLAYERS_ARRAY = new ServerPlayer[0]; ++ ++ private final long chunkKey; ++ private final NearbyPlayers nearbyPlayers; ++ private final ReferenceList[] players = new ReferenceList[TOTAL_MAP_TYPES]; ++ private int nonEmptyLists; ++ private long updateCount; ++ ++ public TrackedChunk(final long chunkKey, final NearbyPlayers nearbyPlayers) { ++ this.chunkKey = chunkKey; ++ this.nearbyPlayers = nearbyPlayers; ++ } ++ ++ public boolean isEmpty() { ++ return this.nonEmptyLists == 0; ++ } ++ ++ public long getUpdateCount() { ++ return this.updateCount; ++ } ++ ++ public ReferenceList getPlayers(final NearbyMapType type) { ++ return this.players[type.ordinal()]; ++ } ++ ++ public void addPlayer(final ServerPlayer player, final NearbyMapType type) { ++ ++this.updateCount; ++ ++ final int idx = type.ordinal(); ++ final ReferenceList list = this.players[idx]; ++ if (list == null) { ++ ++this.nonEmptyLists; ++ final ReferenceList players = (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY)); ++ this.nearbyPlayers.directByChunk[idx].put(this.chunkKey, players); ++ players.add(player); ++ return; ++ } ++ ++ if (!list.add(player)) { ++ throw new IllegalStateException("Already contains player " + player); ++ } ++ } ++ ++ public void removePlayer(final ServerPlayer player, final NearbyMapType type) { ++ ++this.updateCount; ++ ++ final int idx = type.ordinal(); ++ final ReferenceList list = this.players[idx]; ++ if (list == null) { ++ throw new IllegalStateException("Does not contain player " + player); ++ } ++ ++ if (!list.remove(player)) { ++ throw new IllegalStateException("Does not contain player " + player); ++ } ++ ++ if (list.size() == 0) { ++ this.players[idx] = null; ++ this.nearbyPlayers.directByChunk[idx].remove(this.chunkKey); ++ --this.nonEmptyLists; ++ } ++ } ++ } ++ ++ private final class TrackedPlayer extends SingleUserAreaMap { ++ ++ private final NearbyMapType type; ++ ++ public TrackedPlayer(final ServerPlayer player, final NearbyMapType type) { ++ super(player); ++ this.type = type; ++ } ++ ++ @Override ++ protected void addCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) { ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey); ++ final NearbyMapType type = this.type; ++ if (chunk != null) { ++ chunk.addPlayer(parameter, type); ++ type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ); ++ } else { ++ final TrackedChunk created = new TrackedChunk(chunkKey, NearbyPlayers.this); ++ NearbyPlayers.this.byChunk.put(chunkKey, created); ++ created.addPlayer(parameter, type); ++ type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ); ++ ++ ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$requestChunkData(chunkKey).nearbyPlayers = created; ++ } ++ } ++ ++ @Override ++ protected void removeCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) { ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey); ++ if (chunk == null) { ++ throw new IllegalStateException("Chunk should exist at " + new ChunkPos(chunkKey)); ++ } ++ ++ final NearbyMapType type = this.type; ++ chunk.removePlayer(parameter, type); ++ type.removeFrom(parameter, NearbyPlayers.this.world, chunkX, chunkZ); ++ ++ if (chunk.isEmpty()) { ++ NearbyPlayers.this.byChunk.remove(chunkKey); ++ final ChunkData chunkData = ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$releaseChunkData(chunkKey); ++ if (chunkData != null) { ++ chunkData.nearbyPlayers = null; ++ } ++ } ++ } ++ } ++} +diff --git a/ca/spottedleaf/moonrise/common/util/BaseChunkSystemHooks.java b/ca/spottedleaf/moonrise/common/util/BaseChunkSystemHooks.java +new file mode 100644 +index 0000000000000000000000000000000000000000..89406dbda09eea03579ed724fda0df2d42e2e504 +--- /dev/null ++++ b/ca/spottedleaf/moonrise/common/util/BaseChunkSystemHooks.java +@@ -0,0 +1,190 @@ ++package ca.spottedleaf.moonrise.common.util; ++ ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk; ++import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader; ++import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache; ++import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel; ++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.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import java.util.List; ++import java.util.function.Consumer; ++ ++public abstract class BaseChunkSystemHooks implements ChunkSystemHooks { ++ ++ @Override ++ public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) { ++ scheduleChunkTask(level, chunkX, chunkZ, run, Priority.NORMAL); ++ } ++ ++ @Override ++ public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final Priority priority) { ++ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkTask(chunkX, chunkZ, run, priority); ++ } ++ ++ @Override ++ public 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 onComplete) { ++ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete); ++ } ++ ++ @Override ++ public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, ++ final boolean addTicket, final Priority priority, final Consumer onComplete) { ++ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ } ++ ++ @Override ++ public void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, ++ final FullChunkStatus toStatus, final boolean addTicket, ++ final Priority priority, final Consumer onComplete) { ++ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ } ++ ++ @Override ++ public List getVisibleChunkHolders(final ServerLevel level) { ++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders(); ++ } ++ ++ @Override ++ public List getUpdatingChunkHolders(final ServerLevel level) { ++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders(); ++ } ++ ++ @Override ++ public int getVisibleChunkHolderCount(final ServerLevel level) { ++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size(); ++ } ++ ++ @Override ++ public int getUpdatingChunkHolderCount(final ServerLevel level) { ++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size(); ++ } ++ ++ @Override ++ public boolean hasAnyChunkHolders(final ServerLevel level) { ++ return getUpdatingChunkHolderCount(level) != 0; ++ } ++ ++ @Override ++ public void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) { ++ ++ } ++ ++ @Override ++ public void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { ++ // Update progress listener for LevelLoadingScreen ++ final ChunkProgressListener progressListener = level.getChunkSource().chunkMap.progressListener; ++ if (progressListener != null) { ++ this.scheduleChunkTask(level, holder.getPos().x, holder.getPos().z, () -> { ++ progressListener.onStatusChange(holder.getPos(), null); ++ }); ++ } ++ } ++ ++ @Override ++ public void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) { ++ ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource()) ++ .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, chunk); ++ } ++ ++ @Override ++ public void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { ++ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().add( ++ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() ++ ); ++ chunk.loadCallback(); ++ } ++ ++ @Override ++ public void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { ++ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().remove( ++ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() ++ ); ++ chunk.unloadCallback(); ++ } ++ ++ @Override ++ public void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder) { ++ ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource()) ++ .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, null); ++ } ++ ++ @Override ++ public 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 ++ } ++ ++ @Override ++ public 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 ++ } ++ ++ @Override ++ public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { ++ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().add( ++ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() ++ ); ++ } ++ ++ @Override ++ public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { ++ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().remove( ++ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() ++ ); ++ } ++ ++ @Override ++ public ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { ++ return null; ++ } ++ ++ @Override ++ public int getSendViewDistance(final ServerPlayer player) { ++ return RegionizedPlayerChunkLoader.getAPISendViewDistance(player); ++ } ++ ++ @Override ++ public int getViewDistance(final ServerPlayer player) { ++ return RegionizedPlayerChunkLoader.getAPIViewDistance(player); ++ } ++ ++ @Override ++ public int getTickViewDistance(final ServerPlayer player) { ++ return RegionizedPlayerChunkLoader.getAPITickViewDistance(player); ++ } ++ ++ @Override ++ public void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player) { ++ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().addPlayer(player); ++ } ++ ++ @Override ++ public void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player) { ++ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().removePlayer(player); ++ } ++ ++ @Override ++ public void updateMaps(final ServerLevel world, final ServerPlayer player) { ++ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().updatePlayer(player); ++ } ++} +diff --git a/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/ca/spottedleaf/moonrise/common/util/ChunkSystem.java +deleted file mode 100644 +index 58a99bc38e137431f10af36fa9e2d04fe61694aa..0000000000000000000000000000000000000000 +--- a/ca/spottedleaf/moonrise/common/util/ChunkSystem.java ++++ /dev/null +@@ -1,288 +0,0 @@ +-package ca.spottedleaf.moonrise.common.util; +- +-import ca.spottedleaf.concurrentutil.util.Priority; +-import ca.spottedleaf.moonrise.common.PlatformHooks; +-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.world.entity.Entity; +-import net.minecraft.world.level.chunk.ChunkAccess; +-import net.minecraft.world.level.chunk.LevelChunk; +-import net.minecraft.world.level.chunk.status.ChunkStatus; +-import org.slf4j.Logger; +-import java.util.List; +-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); +- } +- +- 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 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); +- } +- } +- } +- }); +- } +- +- static final net.minecraft.server.level.TicketType 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 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 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> 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 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); +- }); +- } +- +- public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, +- final FullChunkStatus toStatus, final boolean addTicket, +- final Priority priority, final Consumer 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 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> 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 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); +- }); +- } +- +- public static List getVisibleChunkHolders(final ServerLevel level) { +- return new java.util.ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values()); +- } +- +- public static List getUpdatingChunkHolders(final ServerLevel level) { +- return new java.util.ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values()); +- } +- +- public static int getVisibleChunkHolderCount(final ServerLevel level) { +- return level.chunkSource.chunkMap.visibleChunkMap.size(); +- } +- +- public static int getUpdatingChunkHolderCount(final ServerLevel level) { +- return level.chunkSource.chunkMap.updatingChunkMap.size(); +- } +- +- public static boolean hasAnyChunkHolders(final ServerLevel level) { +- return getUpdatingChunkHolderCount(level) != 0; +- } +- +- public static boolean screenEntity(final ServerLevel level, final Entity entity, final boolean fromDisk, final boolean event) { +- if (!PlatformHooks.get().screenEntity(level, entity, fromDisk, event)) { +- return false; +- } +- return true; +- } +- +- public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) { +- +- } +- +- public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { +- +- } +- +- public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { +- +- } +- +- public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { +- +- } +- +- public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { +- +- } +- +- public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { +- +- } +- +- public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { +- +- } +- +- public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { +- +- } +- +- public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { +- return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ); +- } +- +- public static int getSendViewDistance(final ServerPlayer player) { +- return getViewDistance(player); +- } +- +- public static int getViewDistance(final ServerPlayer player) { +- final ServerLevel level = player.serverLevel(); +- if (level == null) { +- return org.bukkit.Bukkit.getViewDistance(); +- } +- return level.chunkSource.chunkMap.serverViewDistance; +- } +- +- 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; +- } +- +- private ChunkSystem() {} +-} +diff --git a/ca/spottedleaf/moonrise/common/util/ChunkSystemHooks.java b/ca/spottedleaf/moonrise/common/util/ChunkSystemHooks.java +new file mode 100644 +index 0000000000000000000000000000000000000000..427079ae47b6e0e1aa42013a8760fbefa76941f2 +--- /dev/null ++++ b/ca/spottedleaf/moonrise/common/util/ChunkSystemHooks.java +@@ -0,0 +1,77 @@ ++package ca.spottedleaf.moonrise.common.util; ++ ++import ca.spottedleaf.concurrentutil.util.Priority; ++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.world.entity.Entity; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import java.util.List; ++import java.util.function.Consumer; ++ ++public interface ChunkSystemHooks { ++ ++ public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run); ++ ++ public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final Priority priority); ++ ++ public 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 onComplete); ++ ++ public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, ++ final boolean addTicket, final Priority priority, final Consumer onComplete); ++ ++ public void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, ++ final FullChunkStatus toStatus, final boolean addTicket, ++ final Priority priority, final Consumer onComplete); ++ ++ public List getVisibleChunkHolders(final ServerLevel level); ++ ++ public List getUpdatingChunkHolders(final ServerLevel level); ++ ++ public int getVisibleChunkHolderCount(final ServerLevel level); ++ ++ public int getUpdatingChunkHolderCount(final ServerLevel level); ++ ++ public boolean hasAnyChunkHolders(final ServerLevel level); ++ ++ public boolean screenEntity(final ServerLevel level, final Entity entity, final boolean fromDisk, final boolean event); ++ ++ public void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder); ++ ++ public void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder); ++ ++ public void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder); ++ ++ public void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder); ++ ++ public void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder); ++ ++ public void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder); ++ ++ public void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder); ++ ++ public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder); ++ ++ public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder); ++ ++ public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder); ++ ++ public ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ); ++ ++ public int getSendViewDistance(final ServerPlayer player); ++ ++ public int getViewDistance(final ServerPlayer player); ++ ++ public int getTickViewDistance(final ServerPlayer player); ++ ++ public void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player); ++ ++ public void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player); ++ ++ public void updateMaps(final ServerLevel world, final ServerPlayer player); ++} +diff --git a/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java b/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java +index 12eb3add0931a4d77acdf6e875c42dda9c313dc3..5239993a681d6113eec99fa627b85508656ed7ac 100644 +--- a/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java ++++ b/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/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java +index 11cfe9cc29666ce3a6a40281069fb9eb4fa0ded2..8c197c59eb35e02f163ec98b8aa0888e4ff40b1a 100644 +--- a/ca/spottedleaf/moonrise/paper/PaperHooks.java ++++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java +@@ -27,7 +27,7 @@ import net.minecraft.world.phys.AABB; + import java.util.List; + import java.util.function.Predicate; + +-public final class PaperHooks implements PlatformHooks { ++public final class PaperHooks extends ca.spottedleaf.moonrise.common.util.BaseChunkSystemHooks implements PlatformHooks { // Paper - rewrite chunk system + + @Override + public String getBrand() { +@@ -267,7 +267,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/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java +new file mode 100644 +index 0000000000000000000000000000000000000000..93bc56daec4526f373c84763b8c7ccb4a30e800b +--- /dev/null ++++ b/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 moonrise$countEntries(); ++ ++} +diff --git a/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0d1443a113c07d7655e7b927a899447f70db8fa9 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java +new file mode 100644 +index 0000000000000000000000000000000000000000..89e75b454695e174c5619104eeb15eb923a2d9a7 +--- /dev/null ++++ b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java +@@ -0,0 +1,12 @@ ++package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess; ++ ++public interface PropertyAccess { ++ ++ 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/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..01da52b9e8a786824f199a057b62ce0431ecbc43 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..866f38eb0f379ffbe2888023a7d1c290f521a231 +--- /dev/null ++++ b/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 { ++ ++ private final Int2ObjectOpenHashMap propertyToIndexer; ++ private S[] lookup; ++ private final Collection> properties; ++ ++ public ZeroCollidingReferenceStateTable(final Collection> properties) { ++ this.propertyToIndexer = new Int2ObjectOpenHashMap<>(properties.size()); ++ this.properties = new ReferenceArrayList<>(properties); ++ ++ final List> 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 > boolean hasProperty(final Property property) { ++ return this.propertyToIndexer.containsKey(((PropertyAccess)property).moonrise$getId()); ++ } ++ ++ public long getIndex(final StateHolder stateHolder) { ++ long ret = 0L; ++ ++ for (final Map.Entry, 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, Comparable>, S> universe) { ++ if (this.lookup != null) { ++ throw new IllegalStateException(); ++ } ++ ++ this.lookup = (S[])new StateHolder[universe.size()]; ++ ++ for (final Map.Entry, Comparable>, S> entry : universe.entrySet()) { ++ final S value = entry.getValue(); ++ if (value == null) { ++ continue; ++ } ++ this.lookup[(int)((PropertyAccessStateHolder)(StateHolder)value).moonrise$getTableIndex()] = value; ++ } ++ ++ for (final S value : this.lookup) { ++ if (value == null) { ++ throw new IllegalStateException(); ++ } ++ } ++ } ++ ++ public > T get(final long index, final Property property) { ++ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess)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)property).moonrise$getById((int)modded); ++ } ++ ++ public > S set(final long index, final Property property, final T with) { ++ final int newValueId = ((PropertyAccess)property).moonrise$getIdFor(with); ++ if (newValueId < 0) { ++ return null; ++ } ++ ++ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess)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 > S trySet(final long index, final Property property, final T with, final S dfl) { ++ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess)property).moonrise$getId()); ++ if (indexer == null) { ++ return dfl; ++ } ++ ++ final int newValueId = ((PropertyAccess)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> getProperties() { ++ return Collections.unmodifiableCollection(this.properties); ++ } ++ ++ public Map, 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, 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, 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, Comparable>> { ++ @Override ++ public ObjectIterator, Comparable>> iterator() { ++ final Iterator> propIterator = ZeroCollidingReferenceStateTable.this.properties.iterator(); ++ return new ObjectIterator<>() { ++ @Override ++ public boolean hasNext() { ++ return propIterator.hasNext(); ++ } ++ ++ @Override ++ public Entry, 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/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java +new file mode 100644 +index 0000000000000000000000000000000000000000..44bb25554634af2ec0b2e9b3d9231304d5dff034 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java b/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c7da23900228aab3a5673eb5adfada5091140319 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a814512fcfb85312474ae2c2c21443843bf57831 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1acea58838f057ab87efd103cbecb6f5aeaef393 +--- /dev/null ++++ b/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. ++ *

++ * Impl notes: ++ *

++ *
  • ++ * 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. ++ *
  • ++ *
  • ++ * Writes may be called concurrently, although only the "later" write will go through. ++ *
  • ++ * ++ * @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. ++ *

    ++ * Impl notes: ++ *

    ++ *
  • ++ * 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. ++ *
  • ++ *
  • ++ * Writes may be called concurrently, although only the "later" write will go through. ++ *
  • ++ * ++ * @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 consumer) -> { ++ consumer.accept(data, null); ++ }, null, type, priority ++ ); ++ } ++ ++ /** ++ * Schedules the chunk data to be written asynchronously. ++ *

    ++ * Impl notes: ++ *

    ++ *
  • ++ * 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. ++ *
  • ++ *
  • ++ * Writes may be called concurrently, although only the "later" write will go through. ++ *
  • ++ *
  • ++ * The specified write task, if not null, will have its priority controlled by the scheduler. ++ *
  • ++ * ++ * @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 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. ++ *

    ++ * Impl notes: ++ *

    ++ *
  • ++ * 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. ++ *
  • ++ *
  • ++ * Writes may be called concurrently, although only the "later" write will go through. ++ *
  • ++ *
  • ++ * The specified write task, if not null, will have its priority controlled by the scheduler. ++ *
  • ++ * ++ * @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 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> 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. ++ *

    ++ * Impl notes: ++ *

    ++ *
  • ++ * 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. ++ *
  • ++ * ++ * @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 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. ++ *

    ++ * Impl notes: ++ *

    ++ *
  • ++ * 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. ++ *
  • ++ * ++ * @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 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. ++ *

    ++ * Impl notes: ++ *

    ++ *
  • ++ * 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. ++ *
  • ++ * ++ * @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 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. ++ *

    ++ * Impl notes: ++ *

    ++ *
  • ++ * 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. ++ *
  • ++ * ++ * @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 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}. ++ *

    ++ * Impl notes: ++ *

    ++ *
  • ++ * 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. ++ *
  • ++ * ++ * @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 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}. ++ *

    ++ * Impl notes: ++ *

    ++ *
  • ++ * 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. ++ *
  • ++ * ++ * @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 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 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 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 callback; ++ private ChunkIOTask.InProgressRead read; ++ private ChunkIOTask.InProgressWrite write; ++ ++ private CancellableRead(final BiConsumer callback, ++ final ChunkIOTask.InProgressRead read, ++ final ChunkIOTask.InProgressWrite write) { ++ this.callback = callback; ++ this.read = read; ++ this.write = write; ++ } ++ ++ @Override ++ public boolean cancel() { ++ final BiConsumer 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 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> callbacks = new MultiThreadedQueue<>(); ++ ++ public boolean hasNoWaiters() { ++ return this.callbacks.isEmpty(); ++ } ++ ++ public boolean addToAsyncWaiters(final BiConsumer callback) { ++ return this.callbacks.add(callback); ++ } ++ ++ public boolean cancel(final BiConsumer 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 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> 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> scheduler) { ++ scheduler.accept((final CompoundTag data, final Throwable throwable) -> { ++ InProgressWrite.this.complete(task, data, throwable); ++ }); ++ } ++ ++ public boolean addToAsyncWaiters(final BiConsumer callback) { ++ return this.callbacks.add(callback); ++ } ++ ++ public void addToWaiters(final ChunkIOTask task, final BiConsumer consumer) { ++ if (!this.callbacks.add(consumer)) { ++ this.syncAccept(task, consumer, this.value, this.throwable); ++ } ++ } ++ ++ private void syncAccept(final ChunkIOTask task, final BiConsumer 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 consumer; ++ while ((consumer = this.callbacks.pollOrBlockAdds()) != null) { ++ this.syncAccept(task, consumer, value, throwable); ++ } ++ } ++ ++ public boolean cancel(final BiConsumer 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a36ab89f5c37f5f9ab0152f087bb4cf3560f8581 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java +new file mode 100644 +index 0000000000000000000000000000000000000000..828c868f68c2a20bf90d0f7ec253fdeb591f15f6 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bd0d782852f9cfe5bc0b5339ecf4d82c10332ec9 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..47a4d3376d08dde94a39254bec21473ff27f53e6 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5d4d650186b18eb00782429d53d861564d8e4ba9 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0b58701342d573fa43cdd06681534854a0e51d77 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c278f8ef806f0b45c28cc3040c7db052cb51e053 +--- /dev/null ++++ b/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> onLoad); ++ ++ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, ++ final ChunkStatus chunkStatus, final Priority priority, ++ final Consumer> onLoad); ++ ++ public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, ++ final Priority priority, ++ final Consumer> 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> onLoad); ++ ++ public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder(); ++ ++ public long moonrise$getLastMidTickFailure(); ++ ++ public void moonrise$setLastMidTickFailure(final long time); ++ ++ public NearbyPlayers moonrise$getNearbyPlayers(); ++ ++ public ReferenceList moonrise$getLoadedChunks(); ++ ++ public ReferenceList moonrise$getTickingChunks(); ++ ++ public ReferenceList moonrise$getEntityTickingChunks(); ++} +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8b9dc582627b46843f4b5ea6f8c3df2d8cac46fa +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7d049d750df88762566f13a9c4fc7574a2df4825 +--- /dev/null ++++ b/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 moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge); ++ ++ public LevelChunk moonrise$getFullChunk(); ++ ++} +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f4bc44bb266763345c4e6f859c89352c769a104d +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..aacd543f03b35908011d0c2891e978cc093ebcf5 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5b092bca7027e37aeee8f4b852ad896dd0d5febc +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7aea4e343581b977d11af90f9f65eac3532eade1 +--- /dev/null ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java +@@ -0,0 +1,569 @@ ++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, EntityCollectionBySection> entitiesByClass; ++ private final Reference2ObjectOpenHashMap, 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 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 entities, final ChunkPos chunkPos, final ServerLevel world) { ++ return saveEntityChunk0(entities, chunkPos, world, false); ++ } ++ ++ public static CompoundTag saveEntityChunk0(final List 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 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 getAllEntities() { ++ final int len = this.entities.size(); ++ if (len == 0) { ++ return new ArrayList<>(); ++ } ++ ++ final Entity[] rawData = this.entities.getRawData(); ++ final List 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, EntityCollectionBySection>> iterator = ++ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry, 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, EntityCollectionBySection>> iterator = ++ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry, 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 into, final Predicate predicate) { ++ this.hardCollidingEntities.getEntities(except, box, into, predicate); ++ } ++ ++ public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ this.allEntities.getEntities(except, box, into, predicate); ++ } ++ ++ ++ public boolean getEntities(final Entity except, final AABB box, final List into, final Predicate predicate, ++ final int maxCount) { ++ return this.allEntities.getEntitiesLimited(except, box, into, predicate, maxCount); ++ } ++ ++ public void getEntities(final EntityType type, final AABB box, final List into, ++ final Predicate predicate) { ++ final EntityCollectionBySection byType = this.entitiesByType.get(type); ++ ++ if (byType != null) { ++ byType.getEntities((Entity)null, box, (List)into, (Predicate) predicate); ++ } ++ } ++ ++ public boolean getEntities(final EntityType type, final AABB box, final List into, ++ final Predicate 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 clazz) { ++ final EntityCollectionBySection ret = new EntityCollectionBySection(this); ++ ++ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) { ++ final BasicEntityList 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 void getEntities(final Class clazz, final Entity except, final AABB box, final List into, ++ final Predicate predicate) { ++ EntityCollectionBySection collection = this.entitiesByClass.get(clazz); ++ if (collection != null) { ++ collection.getEntities(except, box, (List)into, (Predicate)predicate); ++ } else { ++ this.entitiesByClass.put(clazz, collection = this.initClass(clazz)); ++ collection.getEntities(except, box, (List)into, (Predicate)predicate); ++ } ++ } ++ ++ public boolean getEntities(final Class clazz, final Entity except, final AABB box, final List into, ++ final Predicate predicate, final int maxCount) { ++ EntityCollectionBySection collection = this.entitiesByClass.get(clazz); ++ if (collection != null) { ++ return collection.getEntitiesLimited(except, box, (List)into, (Predicate)predicate, maxCount); ++ } else { ++ this.entitiesByClass.put(clazz, collection = this.initClass(clazz)); ++ return collection.getEntitiesLimited(except, box, (List)into, (Predicate)predicate, maxCount); ++ } ++ } ++ ++ private static final class BasicEntityList { ++ ++ 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[] 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 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 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 into, final Predicate 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[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList 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 into, final Predicate 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[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList 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; ++ } ++ } ++} +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7554c109c35397bc1a43dd80e87764fd78645bbf +--- /dev/null ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java +@@ -0,0 +1,1002 @@ ++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 { ++ ++ 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 regions = new SWMRLong2ObjectHashTable<>(128, 0.5f); ++ ++ protected final LevelCallback worldCallback; ++ ++ protected final ConcurrentLong2ReferenceChainedHashTable entityById = new ConcurrentLong2ReferenceChainedHashTable<>(); ++ protected final ConcurrentHashMap entityByUUID = new ConcurrentHashMap<>(); ++ protected final EntityList accessibleEntities = new EntityList(); ++ ++ public EntityLookup(final Level world, final LevelCallback 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 implements Iterable { ++ ++ 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 iterator() { ++ return new ArrayIterator<>(this.array, this.off, this.length); ++ } ++ ++ protected static final class ArrayIterator implements Iterator { ++ ++ 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 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 void get(final EntityTypeTest filter, final AbortableIterationConsumer action) { ++ for (final Iterator 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 action) { ++ List entities = new ArrayList<>(); ++ this.getEntities((Entity)null, box, entities, null); ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ action.accept(entities.get(i)); ++ } ++ } ++ ++ @Override ++ public void get(final EntityTypeTest filter, final AABB box, final AbortableIterationConsumer action) { ++ List entities = new ArrayList<>(); ++ this.getEntities((Entity)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 entities, final ChunkPos forChunk) { ++ this.addEntityChunk(entities, forChunk, true); ++ } ++ ++ public void addEntityChunkEntities(final List entities, final ChunkPos forChunk) { ++ this.addEntityChunk(entities, forChunk, true); ++ } ++ ++ public void addWorldGenChunkEntities(final List 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 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 getEntities(final Entity except, final AABB box, final List into, final Predicate 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 into, final Predicate 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 void getEntities(final EntityType type, final AABB box, final List into, ++ final Predicate 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 void getEntities(final Class clazz, final Entity except, final AABB box, final List into, ++ final Predicate 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 getEntities(final Entity except, final AABB box, final List into, final Predicate 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 void getEntities(final EntityType type, final AABB box, final List into, ++ final Predicate 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 void getEntities(final Class clazz, final Entity except, final AABB box, final List into, ++ final Predicate 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/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a038215156a163b0b1cbc870ada5b4ac85ed1335 +--- /dev/null ++++ b/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 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/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2ff58cf753c60913ee73aae015182e9c5560d529 +--- /dev/null ++++ b/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 { ++ ++ @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/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java +new file mode 100644 +index 0000000000000000000000000000000000000000..26207443b1223119c03db478d7e816d9cdf8e618 +--- /dev/null ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java +@@ -0,0 +1,115 @@ ++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.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 trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker ++ ++ public ServerEntityLookup(final ServerLevel world, final LevelCallback 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 PlatformHooks.get().screenEntity(this.serverWorld, entity, fromDisk, event); ++ } ++} +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..458d1fc5e1222912512e6c59b56f6fca347d9ee9 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java +new file mode 100644 +index 0000000000000000000000000000000000000000..89b956b8fdf1a0d862a843104511005e2990a897 +--- /dev/null ++++ b/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 moonrise$asOptional(); ++ ++} +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bbf9d6c1c9525d97160806819a57be03eca290f1 +--- /dev/null ++++ b/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 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 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 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java +new file mode 100644 +index 0000000000000000000000000000000000000000..524752744e37a2db0e3ea089468bdf497129bfef +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..003a857e70ead858e8437e3c1bfaf22f4daba0df +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +new file mode 100644 +index 0000000000000000000000000000000000000000..dd2509996bfd08e8c3f9f2be042229eac6d7692d +--- /dev/null ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +@@ -0,0 +1,1092 @@ ++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.MoonriseConstants; ++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 PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo); ++ public static final TicketType 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 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) { ++ if (distance != -1 && (distance < (0) || distance > (MoonriseConstants.MAX_VIEW_DISTANCE))) { ++ throw new IllegalArgumentException(Integer.toString(distance)); ++ } ++ return new ViewDistances(distance, this.loadViewDistance, this.sendViewDistance); ++ } ++ ++ public ViewDistances setLoadViewDistance(final int distance) { ++ // note: load view distance = api view distance + 1 ++ if (distance != -1 && (distance < (2 + 1) || distance > (MoonriseConstants.MAX_VIEW_DISTANCE + 1))) { ++ throw new IllegalArgumentException(Integer.toString(distance)); ++ } ++ return new ViewDistances(this.tickViewDistance, distance, this.sendViewDistance); ++ } ++ ++ public ViewDistances setSendViewDistance(final int distance) { ++ // note: send view distance <= load view distance - 1 ++ if (distance != -1 && (distance < (0) || distance > (MoonriseConstants.MAX_VIEW_DISTANCE))) { ++ throw new IllegalArgumentException(Integer.toString(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 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> 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 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 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 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 - 1) : (playerLoadViewDistance - 1) ++ ); ++ } ++ ++ 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/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7eafc5b7cba23d8dec92ecc1050afe3fd8c9e309 +--- /dev/null ++++ b/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 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 retrieveForAllRegions() { ++ final List ret = new ArrayList<>(); ++ ++ for (final Iterator> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) { ++ final ConcurrentLong2ReferenceChainedHashTable.TableEntry 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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b5817aa8f537593f6d9fc6b612c82ccccb250ac7 +--- /dev/null ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +@@ -0,0 +1,1456 @@ ++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.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 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>> tickets = new ConcurrentLong2ReferenceChainedHashTable<>(); ++ private final ConcurrentLong2ReferenceChainedHashTable sectionToChunkToExpireCount = new ConcurrentLong2ReferenceChainedHashTable<>(); ++ final ChunkUnloadQueue unloadQueue; ++ ++ private final ConcurrentLong2ReferenceChainedHashTable chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f); ++ private final ServerLevel world; ++ private final ChunkTaskScheduler taskScheduler; ++ private long currentTick; ++ ++ private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); ++ private final ObjectRBTreeSet 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 scheduledTasks = new ArrayList<>(); ++ final List 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 scheduledTasks, ++ final List changedFullStatus) { ++ return this.ticketLevelPropagator.performUpdate( ++ sectionX, sectionZ, this.taskScheduler.schedulingLockArea, scheduledTasks, changedFullStatus ++ ); ++ } ++ ++ public List getOldChunkHolders() { ++ final List ret = new ArrayList<>(this.chunkHolders.size() + 1); ++ for (final Iterator iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) { ++ ret.add(iterator.next().vanillaChunkHolder); ++ } ++ return ret; ++ } ++ ++ public List getChunkHolders() { ++ final List ret = new ArrayList<>(this.chunkHolders.size() + 1); ++ for (final Iterator 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 getOldChunkHoldersIterable() { ++ return new Iterable() { ++ @Override ++ public Iterator iterator() { ++ final Iterator iterator = ChunkHolderManager.this.chunkHolders.valueIterator(); ++ return new Iterator() { ++ @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); ++ } ++ } ++ ++ this.taskScheduler.setShutdown(true); ++ } ++ ++ void ensureInAutosave(final NewChunkHolder holder) { ++ if (!this.autoSaveQueue.contains(holder)) { ++ holder.lastAutoSave = this.currentTick; ++ this.autoSaveQueue.add(holder); ++ } ++ } ++ ++ public void autoSave() { ++ final List 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 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 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 scheduledTasks, ++ final List changedFullStatus) { ++ final List 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> tickets = this.tickets.get(coordinate); ++ ++ return tickets != null ? tickets.first().toString() : "no_ticket"; ++ } finally { ++ if (ticketLock != null) { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ } ++ ++ public Long2ObjectOpenHashMap>> getTicketsCopy() { ++ final Long2ObjectOpenHashMap>> ret = new Long2ObjectOpenHashMap<>(); ++ final Long2ObjectOpenHashMap 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> iterator = sections.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry 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> tickets = this.tickets.get(coord); ++ if (tickets == null) { ++ // removed before we acquired lock ++ continue; ++ } ++ ret.put(coord, ((ChunkSystemSortedArraySet>)tickets).moonrise$copy()); ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ ++ return ret; ++ } ++ ++ // Paper start ++ public Collection getPluginChunkTickets(int x, int z) { ++ com.google.common.collect.ImmutableList.Builder ret; ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(x, z); ++ try { ++ final long coordinate = CoordinateUtils.getChunkKey(x, z); ++ final SortedArraySet> 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> tickets) { ++ return !tickets.isEmpty() ? tickets.first().getTicketLevel() : MAX_TICKET_LEVEL + 1; ++ } ++ ++ public boolean addTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, ++ final T identifier) { ++ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); ++ } ++ ++ public boolean addTicketAtLevel(final TicketType 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 boolean addTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) { ++ return this.addTicketAtLevel(type, chunk, level, identifier, true); ++ } ++ ++ boolean addTicketAtLevel(final TicketType 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 ticket = new Ticket<>(type, level, identifier); ++ ((ChunkSystemTicket)(Object)ticket).moonrise$setRemoveDelay(removeDelay); ++ ++ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; ++ try { ++ final SortedArraySet> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> { ++ return SortedArraySet.create(4); ++ }); ++ ++ final int levelBefore = getTicketLevelAt(ticketsAtChunk); ++ final Ticket current = (Ticket)((ChunkSystemSortedArraySet>)ticketsAtChunk).moonrise$replace(ticket); ++ final int levelAfter = getTicketLevelAt(ticketsAtChunk); ++ ++ if (current != ticket) { ++ final long oldRemoveDelay = ((ChunkSystemTicket)(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 boolean removeTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, final T identifier) { ++ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); ++ } ++ ++ public boolean removeTicketAtLevel(final TicketType type, final int chunkX, final int chunkZ, final int level, final T identifier) { ++ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); ++ } ++ ++ public boolean removeTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) { ++ return this.removeTicketAtLevel(type, chunk, level, identifier, true); ++ } ++ ++ boolean removeTicketAtLevel(final TicketType 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 probe = new Ticket<>(type, level, identifier); ++ ++ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; ++ try { ++ final SortedArraySet> ticketsAtChunk = this.tickets.get(chunk); ++ if (ticketsAtChunk == null) { ++ return false; ++ } ++ ++ final int oldLevel = getTicketLevelAt(ticketsAtChunk); ++ final Ticket ticket = (Ticket)((ChunkSystemSortedArraySet>)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 unknownTicket = new Ticket<>(TicketType.UNKNOWN, level, new ChunkPos(chunk)); ++ ((ChunkSystemTicket)(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)(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 void addAndRemoveTickets(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier, ++ final TicketType 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 boolean addIfRemovedTicket(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier, ++ final TicketType 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 void removeAllTicketsFor(final TicketType ticketType, final int ticketLevel, final T ticketIdentifier) { ++ if (ticketLevel > MAX_TICKET_LEVEL) { ++ return; ++ } ++ ++ final Long2ObjectOpenHashMap 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> iterator = sections.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry 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> 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 iterator1 = chunkToExpireCount.long2IntEntrySet().fastIterator(); iterator1.hasNext();) { ++ final Long2IntMap.Entry entry = iterator1.next(); ++ ++ final long chunkKey = entry.getLongKey(); ++ final int expireCount = entry.getIntValue(); ++ ++ final SortedArraySet> 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); ++ ++ PlatformHooks.get().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 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 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 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 changedFullStatus) { ++ if (changedFullStatus.isEmpty()) { ++ return; ++ } ++ if (!TickThread.isTickThread()) { ++ this.taskScheduler.scheduleChunkTask(() -> { ++ final ArrayDeque 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 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); ++ PlatformHooks.get().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 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 stage1 = new ArrayList<>(); ++ final List 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 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 ( ++ TicketOperationType op, long chunkCoord, ++ TicketType ticketType, int ticketLevel, T identifier, ++ TicketType ticketType2, int ticketLevel2, V identifier2 ++ ) { ++ ++ private TicketOperation(TicketOperationType op, long chunkCoord, ++ TicketType ticketType, int ticketLevel, T identifier) { ++ this(op, chunkCoord, ticketType, ticketLevel, identifier, null, 0, null); ++ } ++ ++ public static TicketOperation addOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { ++ return addOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation addOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { ++ return addOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation addOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { ++ return new TicketOperation<>(TicketOperationType.ADD, chunk, type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation removeOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { ++ return removeOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation removeOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { ++ return removeOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation removeOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { ++ return new TicketOperation<>(TicketOperationType.REMOVE, chunk, type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation addIfRemovedOp(final long chunk, ++ final TicketType addType, final int addLevel, final T addIdentifier, ++ final TicketType removeType, final int removeLevel, final V removeIdentifier) { ++ return new TicketOperation<>( ++ TicketOperationType.ADD_IF_REMOVED, chunk, addType, addLevel, addIdentifier, ++ removeType, removeLevel, removeIdentifier ++ ); ++ } ++ ++ public static TicketOperation addAndRemove(final long chunk, ++ final TicketType addType, final int addLevel, final T addIdentifier, ++ final TicketType removeType, final int removeLevel, final V removeIdentifier) { ++ return new TicketOperation<>( ++ TicketOperationType.ADD_AND_REMOVE, chunk, addType, addLevel, addIdentifier, ++ removeType, removeLevel, removeIdentifier ++ ); ++ } ++ } ++ ++ private boolean processTicketOp(TicketOperation 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> operations) { ++ for (final TicketOperation operation : operations) { ++ this.processTicketOp(operation); ++ } ++ } ++ ++ private final ThreadLocal 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> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>(); ++ ++ static List getCurrentTicketUpdateScheduling() { ++ return CURRENT_TICKET_UPDATE_SCHEDULING.get(); ++ } ++ ++ private boolean processTicketUpdates(final boolean processFullUpdates, List 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 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 pendingFullLoadUpdate = this.pendingFullLoadUpdate; ++ ++ boolean ret = false; ++ ++ final List 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>>> iterator = this.tickets.entryIterator(); ++ iterator.hasNext();) { ++ final ConcurrentLong2ReferenceChainedHashTable.TableEntry>> coordinateTickets = iterator.next(); ++ final long coordinate = coordinateTickets.getKey(); ++ final SortedArraySet> 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>)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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..67532b85073b7978254a0b04caadfe822679e61f +--- /dev/null ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java +@@ -0,0 +1,1055 @@ ++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 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 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 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 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 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 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 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; ++ } ++ ++ private volatile boolean shutdown; ++ ++ public boolean hasShutdown() { ++ return this.shutdown; ++ } ++ ++ public void setShutdown(final boolean shutdown) { ++ this.shutdown = shutdown; ++ } ++ ++ 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 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 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 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 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 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 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; ++ } ++ ++ if (this.hasShutdown()) { ++ throw new IllegalStateException( ++ "Chunk system has shut down, cannot process chunk requests in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.world) + "' at " ++ + "(" + chunkX + "," + chunkZ + ") status: " + status ++ ); ++ } ++ ++ 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 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 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 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 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 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 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 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 chunkHolderNeighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1)); ++ final StaticCache2D 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e4a5fa25ed368fc4662c30934da2963ef446d782 +--- /dev/null ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java +@@ -0,0 +1,1997 @@ ++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.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 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 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 result) { ++ final List completeWaiters; ++ ChunkLoadTask.EntityDataLoadTask entityDataLoadTask = null; ++ boolean scheduleEntityTask = false; ++ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ final List 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> 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> 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 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 result) { ++ final List completeWaiters; ++ ChunkLoadTask.PoiDataLoadTask poiDataLoadTask = null; ++ boolean schedulePoiTask = false; ++ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ final List 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> 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> 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> consumer; ++ protected final NewChunkHolder chunkHolder; ++ protected boolean completed; ++ protected GenericDataLoadTask schedule; ++ protected final AtomicBoolean scheduled = new AtomicBoolean(); ++ ++ public GenericDataLoadTaskCallback(final Consumer> 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 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 neighboursBlockingGenTask = new ReferenceLinkedOpenHashSet<>(4); ++ ++ /** ++ * map of ChunkHolder -> Required Status for this chunk ++ */ ++ private final Reference2ObjectLinkedOpenHashMap 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 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 scheduledTasks, final List 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 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 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); ++ PlatformHooks.get().onChunkPreBorder(chunk, this.vanillaChunkHolder); ++ this.scheduler.chunkHolderManager.ensureInAutosave(this); ++ this.changeEntityChunkStatus(FullChunkStatus.FULL); ++ PlatformHooks.get().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); ++ PlatformHooks.get().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); ++ PlatformHooks.get().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); ++ PlatformHooks.get().onChunkNotEntityTicking(chunk, this.vanillaChunkHolder); ++ this.updateCurrentState(FullChunkStatus.BLOCK_TICKING); ++ } ++ ++ if (current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && !pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { ++ this.changeEntityChunkStatus(FullChunkStatus.FULL); ++ PlatformHooks.get().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); ++ PlatformHooks.get().onChunkNotBorder(chunk, this.vanillaChunkHolder); ++ PlatformHooks.get().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>> statusWaiters = new Reference2ObjectOpenHashMap<>(); ++ ++ void addStatusConsumer(final ChunkStatus status, final Consumer 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> 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 consumer : consumers) { ++ try { ++ consumer.accept(chunk); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to process chunk status callback", thr); ++ } ++ } ++ }, Priority.HIGHEST); ++ } ++ ++ private final Reference2ObjectOpenHashMap>> fullStatusWaiters = new Reference2ObjectOpenHashMap<>(); ++ ++ void addFullStatusConsumer(final FullChunkStatus status, final Consumer consumer) { ++ this.fullStatusWaiters.computeIfAbsent(status, (final FullChunkStatus keyInMap) -> { ++ return new ArrayList<>(4); ++ }).add(consumer); ++ } ++ ++ private void completeFullStatusConsumers(FullChunkStatus status, final LevelChunk chunk) { ++ final List> 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 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 scheduleList, final List 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> iterator = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry 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 needsScheduling = null; ++ boolean recalculatePriority = false; ++ for (final Iterator> iterator ++ = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry 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 needsScheduling, final List 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 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 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 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6b468c621b74449a6218391f6477cf63cfc98c7c +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java +new file mode 100644 +index 0000000000000000000000000000000000000000..310a8f80debadd64c2d962ebf83b7d0505ce6e42 +--- /dev/null ++++ b/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
    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 scheduledTasks, ++ final List 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 scheduledTasks, final List 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 scheduledTasks, final List 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 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 scheduledTasks, final List 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 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 { ++ 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 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> 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> 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 scheduledTasks, List 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 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 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 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5f4b99d8c5453f8ad2e600a57ea4e7dafa2d45f8 +--- /dev/null ++++ b/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 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 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 treeFinished() { ++ this.canQueueTasks = true; ++ for (int priority = 0; priority < this.queues.length; ++priority) { ++ final DependencyTree queue = this.queues[priority]; ++ if (queue.hasWaitingTasks()) { ++ final List 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 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 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 awaiting = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); ++ ++ private final PriorityQueue infiniteRadius = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); ++ private boolean isInfiniteRadiusScheduled; ++ ++ private final Long2ReferenceOpenHashMap 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 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 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 tryPushTasks() { ++ // tasks are not queued, but only created here - we do hold the lock for the map ++ List 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 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 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 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 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 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6ab353b0d2465c3680bb3c8d0852ba0f65c00fd2 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4538ccfaea83d217ed85eaf16e82393c7f286489 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1440c9e2b106616884edcb20201113320817ed9f +--- /dev/null ++++ b/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 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 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 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> 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 extends GenericDataLoadTask { ++ ++ private TaskResult result; ++ private final MultiThreadedQueue>> 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> 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 result) { ++ if ((boolean)COMPLETED_HANDLE.getAndSet((CallbackDataLoadTask)this, (boolean)true)) { ++ throw new IllegalStateException("Already completed"); ++ } ++ this.result = result; ++ Consumer> 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 { ++ 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 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 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 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 { ++ ++ 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 completeOnMainOffMain(final PoiChunk data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ protected TaskResult 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 runOnMain(final PoiChunk data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ } ++ ++ public static final class EntityDataLoadTask extends CallbackDataLoadTask { ++ ++ 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 completeOnMainOffMain(final CompoundTag data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ protected TaskResult 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 runOnMain(final CompoundTag data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ } ++} +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..002ee365aa70d8e6a6e6bd5c95988bd17db4395a +--- /dev/null ++++ b/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> 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..25d8da4773dcee5096053e7e3788bfc224d705a7 +--- /dev/null ++++ b/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 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bdcd1879457bafcca4e76523aac0555968f37c0b +--- /dev/null ++++ b/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 { ++ ++ 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 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 runOffMain(final CompoundTag data, final Throwable throwable); ++ ++ protected abstract TaskResult runOnMain(final OnMain data, final Throwable throwable); ++ ++ protected abstract void onComplete(final TaskResult result); ++ ++ protected abstract TaskResult 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 { ++ ++ 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 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 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)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 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 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 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 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 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java b/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cb6af3712bf9f6f6b8f7a459c309c75dabe83a50 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java b/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ea759ce6f10f2a5a4e107ab7528030fe931ba223 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..51c126735ace8fdde89ad97b5cab62f244212db0 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java +new file mode 100644 +index 0000000000000000000000000000000000000000..129a35ff2db5b3bb6736810fc180796ce55e1875 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3bd1b59250dbab15097a64d515999b278636795a +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java b/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java +new file mode 100644 +index 0000000000000000000000000000000000000000..786e6ad17cd6216ef0aadaa7cf10044a0c19c933 +--- /dev/null ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.ticket; ++ ++public interface ChunkSystemTicket { ++ ++ public long moonrise$getRemoveDelay(); ++ ++ public void moonrise$setRemoveDelay(final long removeDelay); ++ ++} +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java b/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2add7fd15a2210286aeb9af5024263333340d34c +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java b/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ce3bb903c9ccb7efa0f004cf79b291dcb1cb7a23 +--- /dev/null ++++ b/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 { ++ ++ public SortedArraySet moonrise$copy(); ++ ++ public Object[] moonrise$copyBackingArray(); ++ ++ public T moonrise$replace(final T object); ++ ++ public T moonrise$removeAndGet(final T object); ++ ++} +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java b/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..93fd23027c00cef76562098306737272fda1350a +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java b/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7ef3dcca89ed7578c6c0f5565131889110063056 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ea6b6ed27b212719feb31610faac974899688839 +--- /dev/null ++++ b/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 moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate predicate); ++ ++} +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4b9e2fa963c14f65f15407c1814c543c2999ea32 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e97e7d276faf055c89207385d3820debffb06463 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f28fd0e01e2bdda0daf9d775e514a7253d32d8d0 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6af03fd7807d4c71dbf85028d18dc850978ef429 +--- /dev/null ++++ b/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 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/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e04bd54744335fb5398c6e4f7ce8b981f35bfb7d +--- /dev/null ++++ b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +@@ -0,0 +1,2183 @@ ++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 offset, 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] + offset); ++ ++ 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: 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, off_x, aabb.minX + 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, off_x, aabb.maxX - 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, off_y, aabb.minY + 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, off_y, aabb.maxY - 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, off_z, aabb.minZ + 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, off_z, aabb.maxZ - 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: 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, off_y, source.minY + 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, off_y, source.maxY - 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, off_z, source.minZ + 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, off_z, source.maxZ - 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; ++ final int ceil_max_x = findFloor( ++ coords_x, off_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] + off_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; ++ final int floor_min_x = findFloor( ++ coords_x, off_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] + off_x) - 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: 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, off_x, source.minX + 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, off_x, source.maxX - 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, off_z, source.minZ + 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, off_z, source.maxZ - 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; ++ final int ceil_max_y = findFloor( ++ coords_y, off_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] + off_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; ++ final int floor_min_y = findFloor( ++ coords_y, off_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] + off_y) - 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: 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, off_x, source.minX + 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, off_x, source.maxX - 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, off_y, source.minY + 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, off_y, source.maxY - 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; ++ final int ceil_max_z = findFloor( ++ coords_z, off_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] + off_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; ++ final int floor_min_z = findFloor( ++ coords_z, off_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] + off_z) - 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, final double x, final double y, final 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; ++ } ++ ++ 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: 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, off_x, x, 0, size_x); ++ if (index_x < 0 || index_x >= size_x) { ++ return false; ++ } ++ ++ final int index_y = findFloor(coords_y, off_y, y, 0, size_y); ++ if (index_y < 0 || index_y >= size_y) { ++ return false; ++ } ++ ++ final int index_z = findFloor(coords_z, off_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 potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ final AABB target = potentialCollisions.get(i); ++ value = collideX(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static double performAABBCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ final AABB target = potentialCollisions.get(i); ++ value = collideY(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static double performAABBCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ final AABB target = potentialCollisions.get(i); ++ value = collideZ(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static double performVoxelCollisionsX(final AABB currentBoundingBox, double value, final List potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ final VoxelShape target = potentialCollisions.get(i); ++ value = collideX(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static double performVoxelCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ final VoxelShape target = potentialCollisions.get(i); ++ value = collideY(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static double performVoxelCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ final VoxelShape target = potentialCollisions.get(i); ++ value = collideZ(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static Vec3 performVoxelCollisions(final Vec3 moveVector, AABB axisalignedbb, final List 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 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 voxels, ++ final List 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 intoVoxel, final List intoAABB, ++ final int collisionFlags, final BiPredicate 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 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 into, final int collisionFlags, final Predicate 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 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 intoVoxel, final List intoAABB, final int collisionFlags, ++ final BiPredicate blockPredicate, ++ final Predicate 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 = super.getEntity(); ++ return this.delegate == null ? this.delegate = (entity == null ? CollisionContext.empty() : CollisionContext.of(entity)) : this.delegate; ++ } ++ ++ @Override ++ public Entity getEntity() { ++ this.getDelegate(); ++ return super.getEntity(); ++ } ++ ++ @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/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java b/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java +new file mode 100644 +index 0000000000000000000000000000000000000000..35c8aaf0bfa42717f45eed1d1072e1614874de91 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java b/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a38ab583200ebf68ca68fdddf2d12077720b72b7 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5a6b16be4b8c0cc92d017bc592bc4818dba17da7 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9d33ead3a97d86b371e4d9ad9fed80d789bed844 +--- /dev/null ++++ b/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 aabbs, ++ boolean isOffset, ++ double offX, double offY, double offZ ++) { ++ ++ public CachedToAABBs removeOffset() { ++ final List toOffset = this.aabbs; ++ final double offX = this.offX; ++ final double offY = this.offY; ++ final double offZ = this.offZ; ++ ++ final List 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/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java +new file mode 100644 +index 0000000000000000000000000000000000000000..07fe5e02c2d0a27d2fe37bb45761654dc2d02e5d +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java +new file mode 100644 +index 0000000000000000000000000000000000000000..05d7b3f9d8659c259f3ed0537c57e6e43eb6e288 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java b/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java +new file mode 100644 +index 0000000000000000000000000000000000000000..44831fc18efb7534dc6e4822f3c9b5cdc4dcc33e +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java b/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f62359e5d6aa9a9cdb015441dbdb6182dc302f02 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java b/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cf9ffdeff6bf0b62a45f7a44dbfe0dd7d17dc4f4 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5f5734c00ce8245a1ff69b2d4c3036579d5392e0 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8e7472157a98de607c03769a91f64c8369fd3ea6 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java b/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4a7abd239a9c59aa98947e7993962d75e9051902 +--- /dev/null ++++ b/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.fast_palette; ++ ++public interface FastPalette { ++ ++ public default T[] moonrise$getRawPalette(final FastPaletteData src) { ++ return null; ++ } ++ ++} +diff --git a/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java b/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4503f3495846a7d7ed082b9e24636044e4fbccd1 +--- /dev/null ++++ b/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.fast_palette; ++ ++public interface FastPaletteData { ++ ++ public T[] moonrise$getPalette(); ++ ++ public void moonrise$setPalette(final T[] palette); ++ ++} +diff --git a/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java b/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java +new file mode 100644 +index 0000000000000000000000000000000000000000..107c97089354edd35f330582f5e0c8a18e792a6e +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java b/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java +new file mode 100644 +index 0000000000000000000000000000000000000000..540c14a6d2c216cd3ef2a9c4056e15712bf8cb8c +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java b/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8e6d79b7c10ef25f5478b72c53c555423d615a2f +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java b/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ed80017c8f257b981d626a37ffc5480d9b326558 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java b/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fa7b784a89626e8528c249d7889a598bd7ee3d49 +--- /dev/null ++++ b/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 positions) { ++ for (final BlockPos pos : positions) { ++ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); ++ } ++ ++ this.performLightDecrease(lightAccess); ++ } ++ ++ protected List getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) { ++ final List 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 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 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/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java b/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4ca68a903e67606fc4ef0bfa9862a73797121c8b +--- /dev/null ++++ b/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> 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/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java b/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f9aef289e9a2d6f63c98c72c56ef32b8793f57f4 +--- /dev/null ++++ b/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 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/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8aeb5fb87f94a35659347a09a638420699b52a6f +--- /dev/null ++++ b/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 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 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 chunks, ++ final Consumer 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 nibblesByChunk = new Long2ObjectOpenHashMap<>(); ++ final Long2ObjectOpenHashMap 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 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/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java +new file mode 100644 +index 0000000000000000000000000000000000000000..571db5f9bf94745a8afe2cd313e593fb15db5e37 +--- /dev/null ++++ b/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 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 cachedSkyPropagators; ++ private final ArrayDeque 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 chunks, final Consumer 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 onComplete = new MultiThreadedQueue<>(); ++ protected final Set changedPositions = new HashSet<>(); ++ protected Boolean[] changedSectionSet; ++ protected ShortOpenHashSet queuedEdgeChecksSky; ++ protected ShortOpenHashSet queuedEdgeChecksBlock; ++ protected List 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 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 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 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/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7fe59ab70557aa6a484a02db2b2007fdd9e4bbb8 +--- /dev/null ++++ b/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 chunks, ++ final Consumer chunkLightCallback, ++ final IntConsumer onComplete) throws UnsupportedOperationException { ++ throw new UnsupportedOperationException(); ++ } ++ ++} +diff --git a/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java b/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..40d004afdc6449530f5bb2d7c7638b8ee3e3a577 +--- /dev/null ++++ b/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/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java b/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..689ce367164e79e0426eeecb81dbbc521d4bc742 +--- /dev/null ++++ b/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/io/papermc/paper/FeatureHooks.java b/io/papermc/paper/FeatureHooks.java +index a6779295bff446ee79e7c9d41e405447becc2966..efc7f4071655201c59c912e9c84e35a8da66e34c 100644 +--- a/io/papermc/paper/FeatureHooks.java ++++ b/io/papermc/paper/FeatureHooks.java +@@ -1,6 +1,8 @@ + package io.papermc.paper; + + import io.papermc.paper.command.PaperSubcommand; ++import io.papermc.paper.command.subcommands.ChunkDebugCommand; ++import io.papermc.paper.command.subcommands.FixLightCommand; + import it.unimi.dsi.fastutil.longs.LongOpenHashSet; + import it.unimi.dsi.fastutil.longs.LongSet; + import it.unimi.dsi.fastutil.longs.LongSets; +@@ -29,9 +31,12 @@ import org.bukkit.World; + public final class FeatureHooks { + + public static void initChunkTaskScheduler(final boolean useParallelGen) { ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.init(useParallelGen); // Paper - Chunk system + } + + public static void registerPaperCommands(final Map, PaperSubcommand> commands) { ++ commands.put(Set.of("fixlight"), new FixLightCommand()); // Paper - rewrite chunk system ++ commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand()); // Paper - rewrite chunk system + } + + public static LevelChunkSection createSection(final Registry biomeRegistry, final Level level, final ChunkPos chunkPos, final int chunkSection) { +diff --git a/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/io/papermc/paper/command/subcommands/ChunkDebugCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd01fba0fc +--- /dev/null ++++ b/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 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 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 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 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 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/io/papermc/paper/command/subcommands/FixLightCommand.java b/io/papermc/paper/command/subcommands/FixLightCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..85950a1aa732ab8c01ad28bec9e0de140e1a172e +--- /dev/null ++++ b/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 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 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 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/io/papermc/paper/threadedregions/TickRegions.java b/io/papermc/paper/threadedregions/TickRegions.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8424cf9d4617b4732d44cc460d25b04481068989 +--- /dev/null ++++ b/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/net/minecraft/core/Direction.java b/net/minecraft/core/Direction.java +index 690e1d2394e68356c56a39ac083cc53ee0388d71..928f38fd6beb00753c92ae9f4678f7507519a39b 100644 +--- a/net/minecraft/core/Direction.java ++++ b/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/net/minecraft/core/MappedRegistry.java b/net/minecraft/core/MappedRegistry.java +index 063630c1ffcce099139c59d598fc5a210e21f640..a61153c5d99bdc26f37a10f33baf839e943e17e1 100644 +--- a/net/minecraft/core/MappedRegistry.java ++++ b/net/minecraft/core/MappedRegistry.java +@@ -50,6 +50,19 @@ public class MappedRegistry implements WritableRegistry { + 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> key, Lifecycle lifecycle) { + this(key, lifecycle, false); + } +@@ -114,6 +127,7 @@ public class MappedRegistry implements WritableRegistry { + 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/net/minecraft/server/Main.java b/net/minecraft/server/Main.java +index 731bdabd53fd4a3d17494f26781223097a5d6e16..42d46c7a7437bea5335a23cbee5708ac57131474 100644 +--- a/net/minecraft/server/Main.java ++++ b/net/minecraft/server/Main.java +@@ -322,6 +322,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/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 807d05097f7313361eadb600187421d25e294413..5e7ba47247fc9b6bc8da86d8f67c6cd923cd0b1e 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -204,7 +204,7 @@ import org.bukkit.event.server.ServerLoadEvent; + // CraftBukkit end + + +-public abstract class MinecraftServer extends ReentrantBlockableEventLoop implements ServerInfo, ChunkIOErrorReporter, CommandSource { ++public abstract class MinecraftServer extends ReentrantBlockableEventLoop 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(); +@@ -333,7 +333,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { + ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system + AtomicReference 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"); + +@@ -352,6 +352,77 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= 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 +@@ -672,7 +743,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { ++ 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; +@@ -1015,19 +1091,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + 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; +@@ -1432,6 +1511,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { ++ 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/net/minecraft/server/level/ChunkHolder.java b/net/minecraft/server/level/ChunkHolder.java +index b9ab241b930edc63a39dbbcf14cd0b5edacb9ea9..8dd9375f2ad2c65a773a3195aeff1f977e09e7e0 100644 +--- a/net/minecraft/server/level/ChunkHolder.java ++++ b/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 UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk"); + private static final CompletableFuture> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK); + private final LevelHeightAccessor levelHeightAccessor; +- private volatile CompletableFuture> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage +- private volatile CompletableFuture> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage +- private volatile CompletableFuture> 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 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 moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge) { ++ final List 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> getTickingChunkFuture() { +- return this.tickingChunkFuture; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public CompletableFuture> getEntityTickingChunkFuture() { +- return this.entityTickingChunkFuture; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public CompletableFuture> 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> chunkFuture, Executor executor, FullChunkStatus target) { +- this.pendingFullStateConfirmation.cancel(false); +- CompletableFuture 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/net/minecraft/server/level/ChunkLevel.java b/net/minecraft/server/level/ChunkLevel.java +index 11b30b6daa1d049634350e34502c701e9800add4..fae17a075d7efaf24d916877dd5968eb9652bb66 100644 +--- a/net/minecraft/server/level/ChunkLevel.java ++++ b/net/minecraft/server/level/ChunkLevel.java +@@ -7,8 +7,8 @@ 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; ++ 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(); +diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java +index e9b585387f6cbc454e7b16feb36a256e733c5488..204965b3dfa2ac9f6709e61b847e11526dfd7c2f 100644 +--- a/net/minecraft/server/level/ChunkMap.java ++++ b/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> UNLOADED_CHUNK_LIST_RESULT = ChunkResult.error("Unloaded chunks found in range"); + private static final CompletableFuture>> 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 updatingChunkMap = new Long2ObjectLinkedOpenHashMap(); +- public volatile Long2ObjectLinkedOpenHashMap visibleChunkMap; +- private final Long2ObjectLinkedOpenHashMap pendingUnloads; +- private final List pendingGenerationTasks; ++ // Paper - rewrite chunk system + public final ServerLevel level; + private final ThreadedLevelLightEngine lightEngine; + private final BlockableEventLoop 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 entityMap; + private final Long2ByteMap chunkTypeCache; +- private final Long2LongMap nextChunkSaveTime; +- private final LongSet chunksToEagerlySave; +- private final Queue 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 mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier 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>> getChunkRangeFuture(ChunkHolder centerChunk, int margin, IntFunction 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>> 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 list2 = new ArrayList(list1.size()); +- Iterator iterator = list1.iterator(); +- +- while (iterator.hasNext()) { +- ChunkResult 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> 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 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 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 scheduleChunkLoad(ChunkPos pos) { +- CompletableFuture> 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 applyStep(GenerationChunkHolder chunkHolder, ChunkStep step, StaticCache2D 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 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> prepareTickingChunk(ChunkHolder holder) { +- CompletableFuture>> completablefuture = this.getChunkRangeFuture(holder, 1, (i) -> { +- return ChunkStatus.FULL; +- }); +- CompletableFuture> 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> 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 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.PlatformHooks.get().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> read(final ChunkPos pos) { ++ final CompletableFuture> 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 write(final ChunkPos pos, final Supplier 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 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 getPlayersCloseForSpawning(ChunkPos pos) { +- long i = pos.toLong(); ++ // Paper start - chunk tick iteration optimisation ++ final ca.spottedleaf.moonrise.common.list.ReferenceList 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 builder = ImmutableList.builder(); +- Iterator iterator = this.playerMap.getAllPlayers().iterator(); ++ List 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.PlatformHooks.get().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.PlatformHooks.get().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.PlatformHooks.get().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 getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge) { +- Set set = this.playerMap.getAllPlayers(); +- Builder 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 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 list = Lists.newArrayList(); + List 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 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 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.of()) { ++ return this.scaledRange(range); ++ } + +- if (j > i) { +- i = j; +- } ++ // note: we change to List ++ final List passengers = (List)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 players) { +diff --git a/net/minecraft/server/level/DistanceManager.java b/net/minecraft/server/level/DistanceManager.java +index f7c2c03749d6be25bf33afd61e1da120770b3432..746f61661e22d22f2acbbe54a5933e57fbca45b2 100644 +--- a/net/minecraft/server/level/DistanceManager.java ++++ b/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> playersPerChunk = new Long2ObjectOpenHashMap(); +- public final Long2ObjectOpenHashMap>> 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 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 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>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); +- +- while (objectiterator.hasNext()) { +- Entry>> entry = (Entry) objectiterator.next(); +- Iterator> 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 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> 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> 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> 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 void addTicket(TicketType type, ChunkPos pos, int level, T argument) { +@@ -219,13 +128,7 @@ public abstract class DistanceManager { + } + + public boolean addRegionTicketAtDistance(TicketType tickettype, ChunkPos chunkcoordintpair, int i, T t0) { +- // CraftBukkit end +- Ticket 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 void removeRegionTicket(TicketType type, ChunkPos pos, int radius, T argument) { +@@ -234,32 +137,21 @@ public abstract class DistanceManager { + } + + public boolean removeRegionTicketAtDistance(TicketType tickettype, ChunkPos chunkcoordintpair, int i, T t0) { +- // CraftBukkit end +- Ticket 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> 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 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,93 @@ 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> 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()); +- } ++ // Paper start - rewrite chunk system ++ // note: vanilla does not clamp to 0, but we do simply because we need a min of 0 ++ final int clamped = net.minecraft.util.Mth.clamp(simulationDistance, 0, ca.spottedleaf.moonrise.common.util.MoonriseConstants.MAX_VIEW_DISTANCE); + ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getPlayerChunkLoader().setTickDistance(clamped); ++ // Paper end - 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>> 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> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve +- ObjectIterator>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); +- +- while (objectiterator.hasNext()) { +- Entry>> entry = (Entry) objectiterator.next(); +- Iterator> 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 void removeAllTicketsFor(TicketType ticketType, int ticketLevel, T ticketIdentifier) { +- Ticket target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier); +- +- for (java.util.Iterator>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) { +- Entry>> entry = iterator.next(); +- SortedArraySet> 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 +307,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 +387,7 @@ public abstract class DistanceManager { + } + } + ++ /* // Paper - rewrite chunk system + private class PlayerTicketTracker extends DistanceManager.FixedPlayerDistanceChunkTracker { + + private int viewDistance = 0; +@@ -657,5 +482,5 @@ public abstract class DistanceManager { + private boolean haveTicketFor(int distance) { + return distance <= this.viewDistance; + } +- } ++ }*/ // Paper - rewrite chunk system + } +diff --git a/net/minecraft/server/level/GenerationChunkHolder.java b/net/minecraft/server/level/GenerationChunkHolder.java +index 65206fdfa5b94eaca139e433b4865c16b16641f3..bf4463bcb5dc439ac5a3fa08dd60845a5fd7489a 100644 +--- a/net/minecraft/server/level/GenerationChunkHolder.java ++++ b/net/minecraft/server/level/GenerationChunkHolder.java +@@ -27,13 +27,7 @@ public abstract class GenerationChunkHolder { + public static final ChunkResult UNLOADED_CHUNK = ChunkResult.error("Unloaded chunk"); + public static final CompletableFuture> UNLOADED_CHUNK_FUTURE = CompletableFuture.completedFuture(UNLOADED_CHUNK); + protected final ChunkPos pos; +- @Nullable +- private volatile ChunkStatus highestAllowedStatus; +- private final AtomicReference startedWork = new AtomicReference<>(); +- private final AtomicReferenceArray>> futures = new AtomicReferenceArray<>(CHUNK_STATUSES.size()); +- private final AtomicReference task = new AtomicReference<>(); +- private final AtomicInteger generationRefCount = new AtomicInteger(); +- private volatile CompletableFuture 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> scheduleChunkGenerationTask(ChunkStatus requestedStatus, ChunkMap chunkLoadingManager) { +- if (this.isStatusDisallowed(requestedStatus)) { +- return UNLOADED_CHUNK_FUTURE; +- } else { +- CompletableFuture> 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> applyStep(ChunkStep step, GeneratingChunkMap chunkLoadingManager, StaticCache2D 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> completableFuture = CompletableFuture.completedFuture(ChunkResult.of(chunk)); +- +- for (int i = 0; i < this.futures.length() - 1; i++) { +- CompletableFuture> 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> getOrCreateFuture(ChunkStatus status) { +- if (this.isStatusDisallowed(status)) { +- return UNLOADED_CHUNK_FUTURE; +- } else { +- int i = status.getIndex(); +- CompletableFuture> completableFuture = this.futures.get(i); +- +- while (completableFuture == null) { +- CompletableFuture> 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> 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> 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 chunkResult = ChunkResult.of(chunk); +- int i = status.getIndex(); +- +- while (true) { +- CompletableFuture> 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 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> 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> 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>>> getAllFutures() { +- List>>> 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/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java +index 6a2af3cd3aebe525a5ff41a801929547d59b8fec..d7382fc1498a33db909c343d8d07c5aa7130c20f 100644 +--- a/net/minecraft/server/level/ServerChunkCache.java ++++ b/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,107 @@ 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 completable = new CompletableFuture<>(); ++ chunkTaskScheduler.scheduleChunkLoad( ++ chunkX, chunkZ, toStatus, true, ca.spottedleaf.concurrentutil.util.Priority.BLOCKING, ++ completable::complete ++ ); ++ ++ if (!completable.isDone() && chunkTaskScheduler.hasShutdown()) { ++ throw new IllegalStateException( ++ "Chunk system has shut down, cannot process chunk requests in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.level) + "' at " ++ + "(" + chunkX + "," + chunkZ + ") status: " + toStatus ++ ); ++ } ++ ++ 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 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 persistentStateManagerFactory) { + this.level = world; +@@ -112,13 +213,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 +284,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(); ++ // Paper start - rewrite chunk system ++ if (leastStatus == ChunkStatus.FULL) { ++ final LevelChunk ret = this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x, z)); + +- gameprofilerfiller.incrementCounter("getChunk"); +- long k = ChunkPos.asLong(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> 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 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 +350,59 @@ public class ServerChunkCache extends ChunkSource { + } + + private CompletableFuture> 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> ret = new CompletableFuture<>(); ++ final Consumer 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 +415,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 +437,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 +470,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 +497,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 +519,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 +531,26 @@ public class ServerChunkCache extends ChunkSource { + } + + private void collectTickingChunks(List chunks) { +- this.chunkMap.forEachSpawnCandidateChunk((playerchunk) -> { +- LevelChunk chunk = playerchunk.getTickingChunk(); ++ // Paper start - chunk tick iteration optimisation ++ final ca.spottedleaf.moonrise.common.list.ReferenceList 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 chunks) { +@@ -508,7 +591,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 +604,13 @@ public class ServerChunkCache extends ChunkSource { + } + + private void getFullChunk(long pos, Consumer 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 +704,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 +801,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/net/minecraft/server/level/ServerEntity.java b/net/minecraft/server/level/ServerEntity.java +index d5bc702f2676b1b7a32c8f3a4a349fc2710ee825..301e8d6599d200cb0f1328f0e386af2f9a619939 100644 +--- a/net/minecraft/server/level/ServerEntity.java ++++ b/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 list = this.entity.getPassengers(); + + if (!list.equals(this.lastPassengers)) { +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index 6c71ef3c7430623900a7021f853d2bb514273e4d..cf692267c6376ed8484478dc90f4f905d8325618 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -186,7 +186,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); +@@ -202,7 +202,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 entityManager; ++ // Paper - rewrite chunk system + private final GameEventDispatcher gameEventDispatcher; + public boolean noSave; + private final SleepStatus sleepStatus; +@@ -273,12 +273,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> 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; + +@@ -297,32 +292,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> onLoad) { +- List 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 loadedChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); ++ private final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); ++ private final ca.spottedleaf.moonrise.common.list.ReferenceList 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 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> 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> 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> 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> 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 ret = new ArrayList<>(requiredChunks); ++ ++ final java.util.function.Consumer 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); + } + } + } +@@ -330,22 +452,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 moonrise$getLoadedChunks() { ++ return this.loadedChunks; ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getTickingChunks() { ++ return this.tickingChunks; ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.common.list.ReferenceList 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 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 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 resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { +@@ -379,14 +616,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + DataFixer datafixer = minecraftserver.getFixerUpper(); + EntityPersistentStorage 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(); +@@ -414,6 +650,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 + } + +@@ -536,7 +786,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) { +@@ -559,13 +809,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() { +@@ -605,7 +858,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 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(); +@@ -613,7 +919,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)) { +@@ -645,7 +951,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)); + } + } +@@ -653,35 +959,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(); +@@ -954,6 +1232,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 + + } + +@@ -963,6 +1246,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 + + } + +@@ -1041,6 +1329,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) { +@@ -1054,14 +1347,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; +@@ -1201,7 +1499,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 +@@ -1232,7 +1530,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 + } + } + +@@ -1243,11 +1541,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + public boolean tryAddFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) { + // CraftBukkit end +- Stream 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 +@@ -1924,7 +2218,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())); +@@ -1973,7 +2267,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 { +@@ -1994,7 +2288,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 { +@@ -2136,7 +2430,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()); + } +@@ -2166,15 +2460,25 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + @Override + public LevelEntityGetter 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 entities) { +- this.entityManager.addLegacyChunkEntities(entities); ++ // Paper start - add chunkpos param ++ this.addLegacyChunkEntities(entities, null); ++ } ++ public void addLegacyChunkEntities(Stream entities, ChunkPos chunkPos) { ++ // Paper end - add chunkpos param ++ this.moonrise$getEntityLookup().addLegacyChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system + } + + public void addWorldGenChunkEntities(Stream entities) { +- this.entityManager.addWorldGenChunkEntities(entities); ++ // Paper start - add chunkpos param ++ this.addWorldGenChunkEntities(entities, null); ++ } ++ public void addWorldGenChunkEntities(Stream entities, ChunkPos chunkPos) { ++ // Paper end - add chunkpos param ++ this.moonrise$getEntityLookup().addWorldGenChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system + } + + public void startTickingChunk(LevelChunk chunk) { +@@ -2194,34 +2498,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 +@@ -2277,7 +2594,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/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index f6e3073e1f1ff99f6917d84974a18e3e756fa9ea..ba873bcc183f9b3f64ba39be08cb88a95ff52b0e 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -217,7 +217,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; +@@ -322,6 +322,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/net/minecraft/server/level/ThreadedLevelLightEngine.java b/net/minecraft/server/level/ThreadedLevelLightEngine.java +index 39d34f3728ae8d845d1bffc09f3ab8b64eb4d48b..3e82adf061bd0ec0100ca4d16ec9b157bddf99a7 100644 +--- a/net/minecraft/server/level/ThreadedLevelLightEngine.java ++++ b/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> 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 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 chunks0, ++ final java.util.function.Consumer chunkLightCallback, ++ final java.util.function.IntConsumer onComplete) { ++ final java.util.Set chunks = new java.util.LinkedHashSet<>(chunks0); ++ final java.util.Map ticketIds = new java.util.HashMap<>(); ++ final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld(); ++ ++ for (final java.util.Iterator 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 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 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 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 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> objectListIterator = this.lightTasks.iterator(); +- +- int j; +- for (j = 0; objectListIterator.hasNext() && j < i; j++) { +- Pair 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 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/net/minecraft/server/level/Ticket.java b/net/minecraft/server/level/Ticket.java +index eba83b085435150e5954fd5d41dda9ce1d0601ad..daf543b51d8875b374688957ae4bc466f5512bcd 100644 +--- a/net/minecraft/server/level/Ticket.java ++++ b/net/minecraft/server/level/Ticket.java +@@ -2,13 +2,25 @@ package net.minecraft.server.level; + + import java.util.Objects; + +-public final class Ticket implements Comparable> { ++public final class Ticket implements Comparable>, ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket { // Paper - rewrite chunk system + private final TicketType type; + private final int ticketLevel; + public final T key; +- private long createdTick; ++ // Paper start - rewrite chunk system ++ private long removeDelay; + +- protected Ticket(TicketType 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 type, int level, T argument) { // Paper - public + this.type = type; + this.ticketLevel = level; + this.key = argument; +@@ -41,7 +53,7 @@ public final class Ticket implements Comparable> { + + @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 getType() { +@@ -53,11 +65,10 @@ public final class Ticket implements Comparable> { + } + + 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/net/minecraft/server/level/WorldGenRegion.java b/net/minecraft/server/level/WorldGenRegion.java +index b7d29389a357f142237cecd75f8ca91cf1eb6b5b..e4b0dc3121101d54394a0c3a413dabf8103b2ea6 100644 +--- a/net/minecraft/server/level/WorldGenRegion.java ++++ b/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 chunks, ChunkStep generationStep, ChunkAccess centerPos) { + this.generatingStep = generationStep; + this.cache = chunks; +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index c68040a59fa8aa9b8b9f1e0b4fdded565ea592d9..7913c41aac1f9dd53a2b49da2a17fd894bcb6b3a 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -1426,7 +1426,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()) { +@@ -1441,7 +1441,7 @@ public abstract class PlayerList { + + public void setSimulationDistance(int simulationDistance) { + this.simulationDistance = simulationDistance; +- this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); ++ //this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); // Paper - rewrite chunk system + Iterator iterator = this.server.getAllLevels().iterator(); + + while (iterator.hasNext()) { +diff --git a/net/minecraft/util/BitStorage.java b/net/minecraft/util/BitStorage.java +index 68648c5a5e3ff079f832092af0f2f801c42d1ede..e4e153cb8899e70273aa150b8ea26907cf68b15c 100644 +--- a/net/minecraft/util/BitStorage.java ++++ b/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 moonrise$countEntries() { ++ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap 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/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java b/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java +index 61dee55417bc802e25b9ba2f271d32d8c12844a9..a8a260a3caaa8e5004069b833ecc8b17b2fc8db5 100644 +--- a/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java ++++ b/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 implements IdMap { ++public class CrudeIncrementalIntIdentityHashBiMap implements IdMap, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // 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 implements IdMap { + private int nextId; + private int size; + ++ // Paper start - optimise palette reads ++ private ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData reference; ++ ++ @Override ++ public final K[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData 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 implements IdMap { + 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 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/net/minecraft/util/SimpleBitStorage.java b/net/minecraft/util/SimpleBitStorage.java +index 9f438d9c6eb05e43d24e4af68188a3d4c46a938c..d99ec470b4653beab630999a5b2c1a6428b20c38 100644 +--- a/net/minecraft/util/SimpleBitStorage.java ++++ b/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 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 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 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/net/minecraft/util/SortedArraySet.java b/net/minecraft/util/SortedArraySet.java +index ea72dcb064a35bc6245bc5c94d592efedd8faf41..87ee8e51dfa7657ed7d83fcbceef48bf857043e1 100644 +--- a/net/minecraft/util/SortedArraySet.java ++++ b/net/minecraft/util/SortedArraySet.java +@@ -8,12 +8,89 @@ import java.util.Iterator; + import java.util.NoSuchElementException; + import javax.annotation.Nullable; + +-public class SortedArraySet extends AbstractSet { ++public class SortedArraySet extends AbstractSet implements ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet { // Paper - rewrite chunk system + private static final int DEFAULT_INITIAL_CAPACITY = 10; + private final Comparator comparator; + T[] contents; + int size; + ++ // Paper start - rewrite chunk system ++ @Override ++ public final boolean removeIf(final java.util.function.Predicate 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 moonrise$copy() { ++ final SortedArraySet 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 comparator) { + this.comparator = comparator; + if (initialCapacity < 0) { +diff --git a/net/minecraft/util/ZeroBitStorage.java b/net/minecraft/util/ZeroBitStorage.java +index 50040c497a819cd1229042ab3cb057d34a32cacc..1f9c436a632e4f110be61cf76fcfc3b7eb80334e 100644 +--- a/net/minecraft/util/ZeroBitStorage.java ++++ b/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 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 ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1); ++ ret.put(0, coordinates); ++ return ret; ++ } ++ // Paper end - block counting + } +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 766031d1482b0f49b196326b820d5ce9ae1c7c06..1f54752a4ea0788e73279cd99c7c35e3b5d9b6ce 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/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 +@@ -462,6 +418,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 voxels, final List 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 into, final List 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(); +@@ -1387,41 +1493,76 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + private Vec3 collide(Vec3 movement) { +- AABB axisalignedbb = this.getBoundingBox(); +- List 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 potentialCollisionsVoxel = new ArrayList<>(); ++ final List potentialCollisionsBB = new ArrayList<>(); + +- List 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 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 stepVoxels = new ArrayList<>(); ++ final List 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 collisions, float f, float stepHeight) { +@@ -2821,18 +2962,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 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) { +@@ -4310,14 +4543,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + public Iterable getIndirectPassengers() { +- // Paper start - Optimize indirect passenger iteration +- if (this.passengers.isEmpty()) { return ImmutableList.of(); } +- ImmutableList.Builder indirectPassengers = ImmutableList.builder(); +- for (Entity passenger : this.passengers) { +- indirectPassengers.add(passenger); +- indirectPassengers.addAll(passenger.getIndirectPassengers()); ++ // Paper start - optimise entity tracker ++ final List 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 getIndirectPassengers_old() { + // Paper end - Optimize indirect passenger iteration +@@ -4475,82 +4711,136 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return Mth.lerp(delta, this.yRotO, this.yRot); + } + +- public boolean updateFluidHeightAndDoFluidPushing(TagKey tag, double speed) { ++ // Paper start - optimise collisions ++ public boolean updateFluidHeightAndDoFluidPushing(final TagKey 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 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); +@@ -4702,6 +4992,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; + } +@@ -4831,6 +5130,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 +@@ -4842,7 +5147,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 +@@ -4874,7 +5179,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/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/net/minecraft/world/entity/ai/village/poi/PoiManager.java +index 96bc0ba60195e5e666d47b3a0b943b733986d96a..5930a430983061afddf20e3208ff2462ca1b78cd 100644 +--- a/net/minecraft/world/entity/ai/village/poi/PoiManager.java ++++ b/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 { ++public class PoiManager extends SectionStorage 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 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 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 { + world + ); + this.distanceTracker = new PoiManager.DistanceTracker(); ++ this.world = (net.minecraft.server.level.ServerLevel)world; // Paper - rewrite chunk system + } + + public void add(BlockPos pos, Holder type) { +@@ -197,8 +323,10 @@ public class PoiManager extends SectionStorage { + } + + 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 { + + @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 { + .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/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/net/minecraft/world/entity/ai/village/poi/PoiSection.java +index b9e0bc8f1e948614d986335de1f3d2df199eea81..712cbfc100e8aaf612d1d651dae64f57f892a768 100644 +--- a/net/minecraft/world/entity/ai/village/poi/PoiSection.java ++++ b/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 records = new Short2ObjectOpenHashMap<>(); + private final Map, Set> byType = Maps.newHashMap(); + private final Runnable setDirty; + private boolean isValid; + ++ // Paper start - rewrite chunk system ++ private final Optional 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 moonrise$asOptional() { ++ return this.noAllocOptional; ++ } ++ // Paper end - rewrite chunk system ++ + public PoiSection(Runnable updateListener) { + this(updateListener, true, ImmutableList.of()); + } +diff --git a/net/minecraft/world/entity/decoration/ArmorStand.java b/net/minecraft/world/entity/decoration/ArmorStand.java +index 63f02cdc67d9e88cc6998d0ae9d139c83e85b447..70b8023c3badc745f342d5b0ab54699e3923826a 100644 +--- a/net/minecraft/world/entity/decoration/ArmorStand.java ++++ b/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 list = this.level().getEntities((Entity) this, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS); ++ List list = this.level().getEntitiesOfClass(AbstractMinecart.class, this.getBoundingBox(), RIDABLE_MINECARTS); // Paper - optimise collisions + Iterator iterator = list.iterator(); + + while (iterator.hasNext()) { +diff --git a/net/minecraft/world/level/ClipContext.java b/net/minecraft/world/level/ClipContext.java +index 3fa2964b979053ecbefc946c7fe76828de86d8f1..28bf0518f7d17099d7e4990defbeda6757b4477c 100644 +--- a/net/minecraft/world/level/ClipContext.java ++++ b/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/net/minecraft/world/level/EntityGetter.java b/net/minecraft/world/level/EntityGetter.java +index e185a33b5b1f8e8e0a0e666b24ba3e9186a8a7ff..5d7a6e4b73f032db356e7ec369b150013e940ee6 100644 +--- a/net/minecraft/world/level/EntityGetter.java ++++ b/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 getEntities(@Nullable Entity except, AABB box, Predicate predicate); + + List getEntities(EntityTypeTest filter, AABB box, Predicate 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 moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate 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 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 List getEntitiesOfClass(Class entityClass, AABB box) { +@@ -52,23 +75,41 @@ public interface EntityGetter { + } + + default List 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 entities; ++ if (entity != null && ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$isHardColliding()) { ++ entities = this.getEntities(entity, box, null); + } else { +- Predicate predicate = entity == null ? EntitySelector.CAN_BE_COLLIDED_WITH : EntitySelector.NO_SPECTATORS.and(entity::canCollideWith); +- List list = this.getEntities(entity, box.inflate(1.0E-7), predicate); +- if (list.isEmpty()) { +- return List.of(); +- } else { +- Builder 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 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/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index 2a078293332efe4369f314ab021dfa16f63f7f3f..f477c5817f022ce7c4ad25e9b827401434bcfff1 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -84,6 +84,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 +@@ -105,7 +106,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> RESOURCE_KEY_CODEC = ResourceKey.codec(Registries.DIMENSION); + public static final ResourceKey OVERWORLD = ResourceKey.create(Registries.DIMENSION, ResourceLocation.withDefaultNamespace("overworld")); +@@ -131,7 +132,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(); +@@ -207,7 +208,639 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + public abstract ResourceKey getTypeKey(); + ++ // Paper start - rewrite chunk system ++ private ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup; ++ private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable 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 List getEntitiesOfClass(final Class entityClass, final AABB boundingBox, final Predicate predicate) { ++ Profiler.get().incrementCounter("getEntities"); ++ final List 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 moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate predicate) { ++ Profiler.get().incrementCounter("getEntities"); ++ final List 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 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 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 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 aabbs = new java.util.ArrayList<>(); ++ final List 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 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 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 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 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 resourcekey, RegistryAccess iregistrycustom, Holder 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 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; +@@ -288,6 +921,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 +@@ -557,7 +1191,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); + } + +@@ -820,6 +1454,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + // Iterator 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(); // Paper - Fix MC-117075; use removeAll + toRemove.add(null); // Paper - Fix MC-117075 +@@ -835,6 +1471,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 +@@ -855,12 +1496,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 + +@@ -912,7 +1561,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) { +@@ -1004,23 +1653,15 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + Profiler.get().incrementCounter("getEntities"); + List list = Lists.newArrayList(); + +- this.getEntities().get(box, (entity1) -> { +- if (entity1 != except && predicate.test(entity1)) { +- list.add(entity1); +- } +- +- }); +- Iterator iterator = this.dragonParts().iterator(); ++ // Paper start - rewrite chunk system ++ final List ret = new java.util.ArrayList<>(); + +- while (iterator.hasNext()) { +- EnderDragonPart entitycomplexpart = (EnderDragonPart) iterator.next(); ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(except, box, ret, predicate); + +- if (entitycomplexpart != except && entitycomplexpart.parentMob != except && predicate.test(entitycomplexpart) && box.intersects(entitycomplexpart.getBoundingBox())) { +- 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 +@@ -1035,36 +1676,94 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + this.getEntities(filter, box, predicate, result, Integer.MAX_VALUE); + } + +- public void getEntities(EntityTypeTest filter, AABB box, Predicate predicate, List result, int limit) { ++ // Paper start - rewrite chunk system ++ public void getEntities(final EntityTypeTest entityTypeTest, ++ final AABB boundingBox, final Predicate predicate, ++ final List 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 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 base = entityTypeTest.getBaseClass(); + +- if (t0 != null && predicate.test(t0)) { +- result.add(t0); +- if (result.size() >= limit) { +- return AbortableIterationConsumer.Continuation.ABORT; +- } +- } ++ final Predicate 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 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/net/minecraft/world/level/LevelReader.java b/net/minecraft/world/level/LevelReader.java +index 5eb8982678110fabb82a93c5ec67c666b7fde017..ade435de0af4ee3566fa4a490df53cddd2f6531c 100644 +--- a/net/minecraft/world/level/LevelReader.java ++++ b/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/net/minecraft/world/level/ServerExplosion.java b/net/minecraft/world/level/ServerExplosion.java +index b8ffe547ad29645b65c3df8bd6ccb7c20985711d..685ccfb73bf7125585ef90b6a0f51b2f81daa428 100644 +--- a/net/minecraft/world/level/ServerExplosion.java ++++ b/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 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 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 calculateExplodedPositions() { +- Set 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 ret = new ObjectArrayList<>(); + +- Optional 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 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 + + } + +@@ -499,12 +793,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/net/minecraft/world/level/biome/Biome.java b/net/minecraft/world/level/biome/Biome.java +index 9f86b69d8c93a63e0b408ea52519f1fc2e798226..78afd8e51e03cd53c12b64db8a817da457f81bef 100644 +--- a/net/minecraft/world/level/biome/Biome.java ++++ b/net/minecraft/world/level/biome/Biome.java +@@ -113,20 +113,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/net/minecraft/world/level/biome/BiomeManager.java b/net/minecraft/world/level/biome/BiomeManager.java +index 01352cc83b25eb0e30b7e0ff521fc7c1b3d5155b..90f8360f547ce709fd13ee34f8e67d8bfa94b498 100644 +--- a/net/minecraft/world/level/biome/BiomeManager.java ++++ b/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/net/minecraft/world/level/block/Block.java b/net/minecraft/world/level/block/Block.java +index 1aa69f4a7005242925124c74b8229e6fa7362717..c0b1f903962b25d8ff6c2b4fcd2be0e45de09b35 100644 +--- a/net/minecraft/world/level/block/Block.java ++++ b/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/net/minecraft/world/level/block/state/BlockBehaviour.java b/net/minecraft/world/level/block/state/BlockBehaviour.java +index b1101156b281d800f18b25208018722bbecded9f..8c0f332a1a0918f60226d969918ae7fe4fe74166 100644 +--- a/net/minecraft/world/level/block/state/BlockBehaviour.java ++++ b/net/minecraft/world/level/block/state/BlockBehaviour.java +@@ -797,7 +797,7 @@ public abstract class BlockBehaviour implements FeatureElement { + boolean test(BlockState state, BlockGetter world, BlockPos pos); + } + +- public abstract static class BlockStateBase extends StateHolder { ++ public abstract static class BlockStateBase extends StateHolder 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) -> { +@@ -841,6 +841,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, Comparable> propertyMap, MapCodec codec) { + super(block, propertyMap, codec); + this.fluidState = Fluids.EMPTY.defaultFluidState(); +@@ -925,6 +995,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; ++ if (this.isAir()) { ++ this.constantCollisionShape = Shapes.empty(); ++ } else { ++ 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/net/minecraft/world/level/block/state/StateHolder.java b/net/minecraft/world/level/block/state/StateHolder.java +index 422b364764e0df16ca250b4939d7b226e69c0840..815ee11aa5ed3448ff255e9c36d769478de477bd 100644 +--- a/net/minecraft/world/level/block/state/StateHolder.java ++++ b/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 { ++public abstract class StateHolder 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, Comparable>, String> PROPERTY_ENTRY_TO_STRING_FUNCTION = new Function, Comparable>, String>() { +@@ -34,14 +34,28 @@ public abstract class StateHolder { + } + }; + protected final O owner; +- private final Reference2ObjectArrayMap, Comparable> values; ++ private Reference2ObjectArrayMap, Comparable> values; // Paper - optimise blockstate property access - remove final + private Map, S[]> neighbours; + protected final MapCodec propertiesCodec; + ++ // Paper start - optimise blockstate property access ++ protected ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable 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, Comparable> propertyMap, MapCodec 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)(Object)this); ++ // Paper end - optimise blockstate property access + } + + public > S cycle(Property property) { +@@ -67,20 +81,21 @@ public abstract class StateHolder { + } + + public Collection> getProperties() { +- return Collections.unmodifiableCollection(this.values.keySet()); ++ return this.optimisedTable.getProperties(); // Paper - optimise blockstate property access + } + + public > boolean hasProperty(Property property) { +- return this.values.containsKey(property); ++ return property != null && this.optimisedTable.hasProperty(property); // Paper - optimise blockstate property access + } + + public > T getValue(Property 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 > Optional getOptionalValue(Property property) { +@@ -93,22 +108,30 @@ public abstract class StateHolder { + + @Nullable + public > T getNullableValue(Property 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 , V extends T> S setValue(Property 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 , V extends T> S trySetValue(Property 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)(Object)this; ++ } ++ final S ret = this.optimisedTable.trySet(this.tableIndex, property, value, (S)(StateHolder)(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 , V extends T> S setValueInternal(Property property, V newValue, Comparable oldValue) { +@@ -125,18 +148,27 @@ public abstract class StateHolder { + } + + public void populateNeighbours(Map, Comparable>, S> states) { +- if (this.neighbours != null) { +- throw new IllegalStateException(); +- } else { +- Map, S[]> map = new Reference2ObjectArrayMap<>(this.values.size()); ++ // Paper start - optimise blockstate property access ++ final Map, Comparable>, S> map = states; ++ if (this.optimisedTable.isLoaded()) { ++ return; ++ } ++ this.optimisedTable.loadInTable(map); + +- for (Entry, 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, Comparable>, S> entry : map.entrySet()) { ++ final S value = entry.getValue(); ++ ((StateHolder)value).optimisedTable = this.optimisedTable; ++ } + +- this.neighbours = map; ++ // remove values arrays ++ for (final Map.Entry, Comparable>, S> entry : map.entrySet()) { ++ final S value = entry.getValue(); ++ ((StateHolder)value).values = null; + } ++ ++ return; ++ // Paper end optimise blockstate property access + } + + private Map, Comparable> makeNeighbourValues(Property property, Comparable value) { +@@ -146,7 +178,11 @@ public abstract class StateHolder { + } + + public Map, Comparable> getValues() { +- return this.values; ++ // Paper start - optimise blockstate property access ++ ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable 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 > Codec codec(Codec codec, Function ownerToStateFunction) { +diff --git a/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/net/minecraft/world/level/block/state/properties/BooleanProperty.java +index ea76aa490358e9e1d13350ba0ea246ec2c423894..98058505d36baf74008da08339afc196713b14a7 100644 +--- a/net/minecraft/world/level/block/state/properties/BooleanProperty.java ++++ b/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 { ++public final class BooleanProperty extends Property implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess { // Paper - optimise blockstate property access + private static final List 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/net/minecraft/world/level/block/state/properties/EnumProperty.java b/net/minecraft/world/level/block/state/properties/EnumProperty.java +index 85a197232be9377c0313ec00e8f935551e2c60e0..30b2fce9e47ffcc3de1542b1d0f073f5640127a7 100644 +--- a/net/minecraft/world/level/block/state/properties/EnumProperty.java ++++ b/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 & StringRepresentable> extends Property { ++public final class EnumProperty & StringRepresentable> extends Property implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess { // Paper - optimise blockstate property access + private final List values; + private final Map 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 target = this.getValueClass(); ++ return ((value.getClass() != target && value.getDeclaringClass() != target)) ? -1 : this.idLookupTable[value.ordinal()]; ++ } ++ ++ private void init() { ++ final java.util.Collection values = this.getPossibleValues(); ++ final Class 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 type, List values) { + super(name, type); + if (values.isEmpty()) { +@@ -37,6 +65,7 @@ public final class EnumProperty & StringRepresentable> extends + + this.names = builder.buildOrThrow(); + } ++ this.init(); // Paper - optimise blockstate property access + } + + @Override +diff --git a/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/net/minecraft/world/level/block/state/properties/IntegerProperty.java +index 55a87592a99105dbf57b26fb6ccba695295fce24..986365acc9983331a7982ea2e1eac2b0efe1506d 100644 +--- a/net/minecraft/world/level/block/state/properties/IntegerProperty.java ++++ b/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 { ++public final class IntegerProperty extends Property implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess { // 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 { + this.max = max; + this.values = IntImmutableList.toList(IntStream.range(min, max + 1)); + } ++ this.init(); // Paper - optimise blockstate property access + } + + @Override +diff --git a/net/minecraft/world/level/block/state/properties/Property.java b/net/minecraft/world/level/block/state/properties/Property.java +index fcf04c5c58ff35d38c5bf0df562ae2f8dc98a0ee..0b116160924300a9d62ad5948bfaf276f0386e4d 100644 +--- a/net/minecraft/world/level/block/state/properties/Property.java ++++ b/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> { ++public abstract class Property> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess { // Paper - optimise blockstate property access + private final Class clazz; + private final String name; + @Nullable +@@ -24,9 +24,38 @@ public abstract class Property> { + ); + private final Codec> 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 type) { + this.clazz = type; + this.name = name; ++ this.id = ID_GENERATOR.getAndIncrement(); // Paper - optimise blockstate property access + } + + public Property.Value value(T value) { +diff --git a/net/minecraft/world/level/chunk/ChunkAccess.java b/net/minecraft/world/level/chunk/ChunkAccess.java +index 9d240aa87101662480cdd510839e017aa9c58fcd..f87abb22dd161b2b74401086de80dc95c9ac2dbb 100644 +--- a/net/minecraft/world/level/chunk/ChunkAccess.java ++++ b/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 heightmaps = Maps.newEnumMap(Heightmap.Types.class); +- protected ChunkSkyLightSources skyLightSources; ++ // Paper - rewrite chunk system + private final Map structureStarts = Maps.newHashMap(); + private final Map structuresRefences = Maps.newHashMap(); + protected final Map 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 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 biomeRegistry; + // CraftBukkit end +@@ -457,22 +518,22 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh + + @Override + public Holder 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 +@@ -529,12 +590,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> blocks, List> fluids) { +diff --git a/net/minecraft/world/level/chunk/ChunkGenerator.java b/net/minecraft/world/level/chunk/ChunkGenerator.java +index ca6928f959eb63ac9183ba6c95738609839a7d32..e0cb360ece042c4fc6aa0d10106923fe25288f5c 100644 +--- a/net/minecraft/world/level/chunk/ChunkGenerator.java ++++ b/net/minecraft/world/level/chunk/ChunkGenerator.java +@@ -120,7 +120,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); +@@ -315,7 +315,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/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/net/minecraft/world/level/chunk/EmptyLevelChunk.java +index dcc0acd259920463a4464213b9a5e793603852f9..ef4161884574d3d137e12591d983dc95a960cb19 100644 +--- a/net/minecraft/world/level/chunk/EmptyLevelChunk.java ++++ b/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; + + public EmptyLevelChunk(Level world, ChunkPos pos, Holder 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/net/minecraft/world/level/chunk/HashMapPalette.java b/net/minecraft/world/level/chunk/HashMapPalette.java +index 98dbeaf8bde15940e5b5d5d1f13fd4bb32f0a10d..7beea075b5a7ef738a4ac0558b99f4c5708f2c4a 100644 +--- a/net/minecraft/world/level/chunk/HashMapPalette.java ++++ b/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 implements Palette { ++public class HashMapPalette implements Palette, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // Paper - optimise palette reads + private final IdMap registry; + private final CrudeIncrementalIntIdentityHashBiMap values; + private final PaletteResize resizeHandler; + private final int bits; + ++ // Paper start - optimise palette reads ++ @Override ++ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData container) { ++ return ((ca.spottedleaf.moonrise.patches.fast_palette.FastPalette)this.values).moonrise$getRawPalette(container); ++ } ++ // Paper end - optimise palette reads ++ + public HashMapPalette(IdMap idList, int bits, PaletteResize listener, List entries) { + this(idList, bits, listener); + entries.forEach(this.values::add); +diff --git a/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/net/minecraft/world/level/chunk/ImposterProtoChunk.java +index f38700e5fbeeb8a913272d4464b8aa325d511dac..1eb8022f3e31603322e6c56516304afc9a11bbec 100644 +--- a/net/minecraft/world/level/chunk/ImposterProtoChunk.java ++++ b/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/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java +index 0ade64bbdec563e555c981cee2208e6c72afe249..134d63076f231791988e67a5bdf191005112080b 100644 +--- a/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/net/minecraft/world/level/chunk/LevelChunk.java +@@ -55,7 +55,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() { +@@ -114,6 +114,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 +@@ -124,6 +132,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()); +@@ -157,13 +198,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) { +@@ -366,7 +413,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(); +@@ -632,11 +679,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 +@@ -645,6 +693,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; +@@ -671,13 +720,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 +@@ -685,8 +736,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() { +@@ -794,6 +868,7 @@ public class LevelChunk extends ChunkAccess { + + this.pendingBlockEntities.clear(); + this.upgradeData.upgrade(this); ++ this.postProcessingDone = true; // Paper - rewrite chunk system + } + + @Nullable +diff --git a/net/minecraft/world/level/chunk/LevelChunkSection.java b/net/minecraft/world/level/chunk/LevelChunkSection.java +index 3dab36d00ea48101807ba40c7a7358b7eed12747..e4ae25c83ab9dd1aaa530a5456275ef63cdb8511 100644 +--- a/net/minecraft/world/level/chunk/LevelChunkSection.java ++++ b/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 states; + private PalettedContainer> 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 { ++ // 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 data = this.states.data; ++ final Palette palette = data.palette(); ++ final int paletteSize = palette.getSize(); ++ final net.minecraft.util.BitStorage storage = data.storage(); ++ ++ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap 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> iterator = counts.int2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry 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 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/net/minecraft/world/level/chunk/LinearPalette.java b/net/minecraft/world/level/chunk/LinearPalette.java +index bc4d9452bbeb05a691fd285603e49491f41d3ad2..f8d9892970c9092f7cc84434d4fbf34354ce1195 100644 +--- a/net/minecraft/world/level/chunk/LinearPalette.java ++++ b/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 implements Palette { ++public class LinearPalette implements Palette, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // Paper - optimise palette reads + private final IdMap registry; + private final T[] values; + private final PaletteResize 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 container) { ++ return this.values; ++ } ++ // Paper end - optimise palette reads ++ + private LinearPalette(IdMap idList, int bits, PaletteResize listener, List list) { + this.registry = idList; + this.values = (T[])(new Object[1 << bits]); +diff --git a/net/minecraft/world/level/chunk/Palette.java b/net/minecraft/world/level/chunk/Palette.java +index b8922e4a13df535cdc5701e893a6e460b33ff90d..100807f8b8337f56f49cdb818ccc75be2f08ecd1 100644 +--- a/net/minecraft/world/level/chunk/Palette.java ++++ b/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 { ++public interface Palette extends ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // Paper - optimise palette reads + int idFor(T object); + + boolean maybeHas(Predicate predicate); +diff --git a/net/minecraft/world/level/chunk/PalettedContainer.java b/net/minecraft/world/level/chunk/PalettedContainer.java +index 69d6f203366df658e1ade55d917f0aa2b8a49be9..8b84bf2272556ac3321cbf16361d7f48a1cc6873 100644 +--- a/net/minecraft/world/level/chunk/PalettedContainer.java ++++ b/net/minecraft/world/level/chunk/PalettedContainer.java +@@ -29,7 +29,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + private final PaletteResize dummyPaletteResize = (newSize, added) -> 0; + public final IdMap registry; + private final T @org.jetbrains.annotations.Nullable [] presetValues; // Paper - Anti-Xray - Add preset values +- private volatile PalettedContainer.Data data; ++ public volatile PalettedContainer.Data 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 implements PaletteResize, PalettedContainer + ); + } + ++ // Paper start - optimise palette reads ++ private void updateData(final PalettedContainer.Data data) { ++ if (data != null) { ++ ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData)(Object)data).moonrise$setPalette( ++ ((ca.spottedleaf.moonrise.patches.fast_palette.FastPalette)data.palette).moonrise$getRawPalette((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData)(Object)data) ++ ); ++ } ++ } ++ ++ private T readPaletteSlow(final PalettedContainer.Data data, final int paletteIdx) { ++ return data.palette.valueFor(paletteIdx); ++ } ++ ++ private T readPalette(final PalettedContainer.Data data, final int paletteIdx) { ++ final T[] palette = ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData)(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 idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Configuration dataProvider, BitStorage storage, List paletteEntries) { this(idList, paletteProvider, dataProvider, storage, paletteEntries, null, null); } + public PalettedContainer( +@@ -113,6 +140,7 @@ public class PalettedContainer implements PaletteResize, 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 implements PaletteResize, PalettedContainer + this.registry = idList; + this.strategy = paletteProvider; + this.data = data; ++ this.updateData(this.data); // Paper - optimise palette reads + } + + private PalettedContainer(PalettedContainer container, T @org.jetbrains.annotations.Nullable [] presetValues) { // Paper - Anti-Xray - Add preset values +@@ -140,6 +169,7 @@ public class PalettedContainer implements PaletteResize, 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 createOrReuseData(@Nullable PalettedContainer.Data previousData, int bits) { +@@ -166,6 +196,7 @@ public class PalettedContainer implements PaletteResize, 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 implements PaletteResize, 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 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 implements PaletteResize, PalettedContainer + return this.get(this.strategy.getIndex(x, y, z)); + } + +- protected T get(int index) { +- PalettedContainer.Data 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 data = this.data; ++ return this.readPalette(data, data.storage.get(index)); ++ // Paper end - optimise palette reads + } + + @Override +@@ -246,6 +282,7 @@ public class PalettedContainer implements PaletteResize, 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 implements PaletteResize, PalettedContainer + void accept(T object, int count); + } + +- static record Data(PalettedContainer.Configuration configuration, BitStorage storage, Palette palette) { ++ // Paper start - optimise palette reads ++ public static final class Data implements ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData { ++ ++ private final PalettedContainer.Configuration configuration; ++ private final BitStorage storage; ++ private final Palette palette; ++ ++ private T[] moonrise$palette; ++ ++ public Data(final PalettedContainer.Configuration configuration, final BitStorage storage, final Palette palette) { ++ this.configuration = configuration; ++ this.storage = storage; ++ this.palette = palette; ++ } ++ ++ public PalettedContainer.Configuration configuration() { ++ return this.configuration; ++ } ++ ++ public BitStorage storage() { ++ return this.storage; ++ } ++ ++ public Palette 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 palette, BitStorage storage) { + for (int i = 0; i < storage.getSize(); i++) { + T object = palette.valueFor(storage.get(i)); +diff --git a/net/minecraft/world/level/chunk/ProtoChunk.java b/net/minecraft/world/level/chunk/ProtoChunk.java +index 15e14f5d006389c823fa6baf8c1a4f22804d4aa8..759adee51bad99bd4bbee4f44247e8c8486cfbd6 100644 +--- a/net/minecraft/world/level/chunk/ProtoChunk.java ++++ b/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/net/minecraft/world/level/chunk/SingleValuePalette.java b/net/minecraft/world/level/chunk/SingleValuePalette.java +index a45e6410600afc5464e5d29932c193786ce0a6fb..a1ba68c95c2cdebdc0d7782cce7895529918073c 100644 +--- a/net/minecraft/world/level/chunk/SingleValuePalette.java ++++ b/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 implements Palette { ++public class SingleValuePalette implements Palette, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // Paper - optimise palette reads + private final IdMap registry; + @Nullable + private T value; + private final PaletteResize resizeHandler; + ++ // Paper start - optimise palette reads ++ private T[] rawPalette; ++ ++ @Override ++ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData container) { ++ if (this.rawPalette != null) { ++ return this.rawPalette; ++ } ++ return this.rawPalette = (T[])new Object[] { this.value }; ++ } ++ // Paper end - optimise palette reads ++ + public SingleValuePalette(IdMap idList, PaletteResize listener, List entries) { + this.registry = idList; + this.resizeHandler = listener; +@@ -33,6 +45,11 @@ public class SingleValuePalette implements Palette { + 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 implements Palette { + @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/net/minecraft/world/level/chunk/status/ChunkPyramid.java b/net/minecraft/world/level/chunk/status/ChunkPyramid.java +index b1058bf0dcda544a074f4d3772d7899b94f98927..b7bf82f6b6023bd628d3e7ea84d2d6755a0d931a 100644 +--- a/net/minecraft/world/level/chunk/status/ChunkPyramid.java ++++ b/net/minecraft/world/level/chunk/status/ChunkPyramid.java +@@ -54,7 +54,7 @@ public record ChunkPyramid(ImmutableList 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/net/minecraft/world/level/chunk/status/ChunkStatus.java b/net/minecraft/world/level/chunk/status/ChunkStatus.java +index 4f84ff9cdb3303251e035a12ce9d8b9a0b58f46e..d80b7d555e02d1d4b82945373d383eaedbf4b976 100644 +--- a/net/minecraft/world/level/chunk/status/ChunkStatus.java ++++ b/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 WORLDGEN_HEIGHTMAPS = EnumSet.of(Heightmap.Types.OCEAN_FLOOR_WG, Heightmap.Types.WORLD_SURFACE_WG); + public static final EnumSet 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 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/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java b/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java +index 3d8a35d8cf29447ee7ac750dbc6ffcdb0f89b81b..9a3900e970f22892d8a3da8a28f922aa9b62765f 100644 +--- a/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java ++++ b/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 entities) { // Paper - public ++ public static void postLoadProtoChunk(ServerLevel world, List 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/net/minecraft/world/level/chunk/status/ChunkStep.java b/net/minecraft/world/level/chunk/status/ChunkStep.java +index 3d37a0372cdd99e806a9651cc1cabaefa9338065..f9aad1b8c02b70e620efdc2a58cadf4fff0f3ed5 100644 +--- a/net/minecraft/world/level/chunk/status/ChunkStep.java ++++ b/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/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/net/minecraft/world/level/chunk/storage/ChunkStorage.java +index 092f7b6bba4e1291f76c2c09155f33803e93eb04..46f4b6706a1ca24ff6fc28960ad01a067109819f 100644 +--- a/net/minecraft/world/level/chunk/storage/ChunkStorage.java ++++ b/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> 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 write(ChunkPos chunkPos, Supplier 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/net/minecraft/world/level/chunk/storage/EntityStorage.java b/net/minecraft/world/level/chunk/storage/EntityStorage.java +index a0cbccd2cf1ac785745d86c42b6f58fb8bad7ffa..16ca1c8672e5f0a27f8a30498c754a81cdec5191 100644 +--- a/net/minecraft/world/level/chunk/storage/EntityStorage.java ++++ b/net/minecraft/world/level/chunk/storage/EntityStorage.java +@@ -71,12 +71,12 @@ public class EntityStorage implements EntityPersistentStorage { + } + } + +- 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/net/minecraft/world/level/chunk/storage/IOWorker.java b/net/minecraft/world/level/chunk/storage/IOWorker.java +index 1f2997cf5367200084f32c437f77040c8c6a18e6..a8a9e59a9721a76e34f78c1baa5026e5fe1d2bda 100644 +--- a/net/minecraft/world/level/chunk/storage/IOWorker.java ++++ b/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 pendingWrites = new LinkedHashMap<>(); + private final Long2ObjectLinkedOpenHashMap> regionCacheForBlender = new Long2ObjectLinkedOpenHashMap<>(); + private static final int REGION_CACHE_SIZE = 1024; +diff --git a/net/minecraft/world/level/chunk/storage/RegionFile.java b/net/minecraft/world/level/chunk/storage/RegionFile.java +index 863960ead8deaa0553be1c98e4fa09f07fcb8ef0..057875cbbdc92ba49b429f9a129514760edb32a2 100644 +--- a/net/minecraft/world/level/chunk/storage/RegionFile.java ++++ b/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/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +index e6abe35d6c43b7f76cf3da129ec9552e7b82453e..fdf8e18d24442178b52397acb482ffa3306a32e3 100644 +--- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java ++++ b/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 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 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 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/net/minecraft/world/level/chunk/storage/SectionStorage.java b/net/minecraft/world/level/chunk/storage/SectionStorage.java +index 75b2cf0e13c23a8348b7ff55e72e5ee755aa7460..c3beb7fcad46a917d2b61bd0a0e98e5106056728 100644 +--- a/net/minecraft/world/level/chunk/storage/SectionStorage.java ++++ b/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 implements AutoCloseable { ++public class SectionStorage 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> storage = new Long2ObjectOpenHashMap<>(); + private final LongLinkedOpenHashSet dirtyChunks = new LongLinkedOpenHashSet(); + private final Codec

    codec; +@@ -57,6 +57,18 @@ public class SectionStorage implements AutoCloseable { + private final Long2ObjectMap>>> 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

    codec, +@@ -67,7 +79,7 @@ public class SectionStorage 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 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,64 +201,15 @@ public class SectionStorage implements AutoCloseable { + } + + private CompletableFuture>> tryRead(ChunkPos chunkPos) { +- RegistryOps 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 CompletionException) { +- throwable = throwable.getCause(); +- } +- +- 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

    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 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 registryOps = this.registryAccess.createSerializationContext(NbtOps.INSTANCE); +- Dynamic 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 Dynamic writeChunk(ChunkPos chunkPos, DynamicOps ops) { +@@ -281,7 +245,7 @@ public class SectionStorage implements AutoCloseable { + protected void onSectionLoad(long pos) { + } + +- protected void setDirty(long pos) { ++ public void setDirty(long pos) { // Paper - public + Optional optional = this.storage.get(pos); + if (optional != null && !optional.isEmpty()) { + this.dirtyChunks.add(ChunkPos.asLong(SectionPos.x(pos), SectionPos.z(pos))); +@@ -302,7 +266,7 @@ public class SectionStorage implements AutoCloseable { + + @Override + public void close() throws IOException { +- this.simpleRegionStorage.close(); ++ this.moonrise$close(); // Paper - rewrite chunk system + } + + static record PackedChunk(Int2ObjectMap sectionsByY, boolean versionChanged) { +diff --git a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +index 4bc7fa3324e9af3abce2acf960c7b0650aca2e36..0296f52fb2c871adbf2ce73a64d8f77fab826cd7 100644 +--- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java ++++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +@@ -129,7 +129,7 @@ public record SerializableChunkData(Registry 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 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 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 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 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 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 biomeRegistry, ChunkPos chun + throw new IllegalArgumentException("Chunk can't be serialized: " + String.valueOf(chunk)); + } else { + ChunkPos chunkcoordintpair = chunk.getPos(); +- List list = new ArrayList(); ++ List list = new ArrayList(); final List 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 list1 = new ArrayList(chunk.getBlockEntitiesPos().size()); + Iterator iterator = chunk.getBlockEntitiesPos().iterator(); +@@ -521,8 +608,8 @@ public record SerializableChunkData(Registry 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 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 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 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/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java b/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java +index 578d270d5b7efb9ac8f5dde539170f6021e2b786..c5085ebf4e801837010f3750c5e89576bb0c27a5 100644 +--- a/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java ++++ b/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/net/minecraft/world/level/entity/EntityTickList.java b/net/minecraft/world/level/entity/EntityTickList.java +index 74a285b8b018a9c94ccea519f1ce8b9e2ef3cb64..d8b4196adf955f8d414688dc451caac2d9c609d9 100644 +--- a/net/minecraft/world/level/entity/EntityTickList.java ++++ b/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 active = new Int2ObjectLinkedOpenHashMap<>(); +- private Int2ObjectMap passive = new Int2ObjectLinkedOpenHashMap<>(); +- @Nullable +- private Int2ObjectMap iterated; ++ private final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet 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 entry : Int2ObjectMaps.fastIterable(this.active)) { +- this.passive.put(entry.getIntKey(), entry.getValue()); +- } +- +- Int2ObjectMap 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 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 iterator = this.entities.iterator(); ++ try { ++ while (iterator.hasNext()) { ++ action.accept(iterator.next()); + } ++ } finally { ++ iterator.finishedIterating(); + } ++ // Paper end - rewrite chunk system + } + } +diff --git a/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java +index 1fcc2b287ed723cf51720f80e68f18f4a15cf429..3f39d6c786d9dfdd9ad591e08ff05fcbb41a1df6 100644 +--- a/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java ++++ b/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/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/net/minecraft/world/level/levelgen/structure/StructureCheck.java +index c3586281c9594769593a6027ea0a78f7c76c0262..decdb275e83fa6244aa3a24458872b42c49d04ed 100644 +--- a/net/minecraft/world/level/levelgen/structure/StructureCheck.java ++++ b/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> loadedChunks = new Long2ObjectOpenHashMap<>(); +- private final Map 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> loadedChunksSafe = new ca.spottedleaf.moonrise.common.map.SynchronisedLong2ObjectMap<>(CHUNK_TOTAL_LIMIT); ++ private final java.util.concurrent.ConcurrentHashMap 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 object2IntMap = this.loadedChunks.get(l); ++ Object2IntMap 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 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 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/net/minecraft/world/level/lighting/LevelLightEngine.java b/net/minecraft/world/level/lighting/LevelLightEngine.java +index 8d90e783967280025d711c709facbcc87f611f8a..987e3397503cd07d3a2f172cede341297bc58dba 100644 +--- a/net/minecraft/world/level/lighting/LevelLightEngine.java ++++ b/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/net/minecraft/world/level/material/FlowingFluid.java b/net/minecraft/world/level/material/FlowingFluid.java +index 261e5994d13f8bc30490b86691c80c0a21e7640a..f4fbcbb8ff6d2677af1a02a0801a323c06dce9b1 100644 +--- a/net/minecraft/world/level/material/FlowingFluid.java ++++ b/net/minecraft/world/level/material/FlowingFluid.java +@@ -55,6 +55,48 @@ public abstract class FlowingFluid extends Fluid { + }); + private final Map 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 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/net/minecraft/world/level/material/FluidState.java b/net/minecraft/world/level/material/FluidState.java +index 87adfe152abd1b8b4d547034576883c5d1cdf134..2d50d72bf026d0cf9c546a3c6fc1859379bfd805 100644 +--- a/net/minecraft/world/level/material/FluidState.java ++++ b/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 { ++public final class FluidState extends StateHolder implements ca.spottedleaf.moonrise.patches.fluid.FluidFluidState { // Paper - fluid method optimisations + public static final Codec 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, Comparable> propertyMap, MapCodec codec) { + super(fluid, propertyMap, codec); + this.isEmpty = fluid.isEmpty(); // Paper - Perf: moved from isEmpty() +@@ -38,11 +56,11 @@ public final class FluidState extends StateHolder { + } + + 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 { + } + + 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 { + } + + 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 { + } + + 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/net/minecraft/world/phys/AABB.java b/net/minecraft/world/phys/AABB.java +index 5dc2674b537f4a61b2e21a21bdb2e8dc090d3a3c..6cf6d4ec7b9e43c7b2b4c0e2fb080964ff588130 100644 +--- a/net/minecraft/world/phys/AABB.java ++++ b/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/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/net/minecraft/world/phys/shapes/ArrayVoxelShape.java +index 4fee67f7214b464b9e09862778e3ef187fcb8b72..31a54af04ab072a433d6df9fe37beb12243fea80 100644 +--- a/net/minecraft/world/phys/shapes/ArrayVoxelShape.java ++++ b/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/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java b/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java +index e8f3307727e7e3da9a7629cafc6e1ee53790b75d..97ef481156ec5d821779f126ab98a8f28cbaf30b 100644 +--- a/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java ++++ b/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/net/minecraft/world/phys/shapes/CubeVoxelShape.java b/net/minecraft/world/phys/shapes/CubeVoxelShape.java +index d812949c7329ae2696b38dc792fa011ba87decb9..7743495c7ec3fc5e17947144457cef7bbe0f4b38 100644 +--- a/net/minecraft/world/phys/shapes/CubeVoxelShape.java ++++ b/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/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java b/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java +index 01693ba050b12b9debcdaefceeff9cbcd503b369..fbe0c4b0fdbb992b7002f6afe1e74d63cbb420f2 100644 +--- a/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java ++++ b/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/net/minecraft/world/phys/shapes/OffsetDoubleList.java b/net/minecraft/world/phys/shapes/OffsetDoubleList.java +index 7ec02a7849437a18860aa0df7d9ddd71b2447d4c..5e45e49ab09344cb95736f4124b1c6e002ef5b82 100644 +--- a/net/minecraft/world/phys/shapes/OffsetDoubleList.java ++++ b/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/net/minecraft/world/phys/shapes/Shapes.java b/net/minecraft/world/phys/shapes/Shapes.java +index 76d7435e6fe81a3f1d24b35eae72d06232a1792b..ca3a2419252721bb3b3b719eb19afb5f175394c0 100644 +--- a/net/minecraft/world/phys/shapes/Shapes.java ++++ b/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/net/minecraft/world/phys/shapes/SliceShape.java b/net/minecraft/world/phys/shapes/SliceShape.java +index b07f1c58e00d232e7c83e6df3499e4b677645609..b88c71f27996d24d29048e06a69a004617eb53a2 100644 +--- a/net/minecraft/world/phys/shapes/SliceShape.java ++++ b/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/net/minecraft/world/phys/shapes/VoxelShape.java b/net/minecraft/world/phys/shapes/VoxelShape.java +index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..6182f1d37c7a63479f6c6e7c37a7edc9cffc3071 100644 +--- a/net/minecraft/world/phys/shapes/VoxelShape.java ++++ b/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 toAabbsUncached() { ++ final List 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, offset, (positiveDir ? (1.0 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) : (0.0 + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)), ++ 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 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 toAabbs() { +- List 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, this.offsetX, value, 0, values.length - 1 ++ ); ++ } ++ case Y: { ++ final double[] values = this.rootCoordinatesY; ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor( ++ values, this.offsetY, value, 0, values.length - 1 ++ ); ++ } ++ case Z: { ++ final double[] values = this.rootCoordinatesZ; ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor( ++ values, this.offsetZ, value, 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 closestPointTo(Vec3 target) { +- if (this.isEmpty()) { ++ // Paper start - optimise collisions ++ public Optional 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 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/net/minecraft/world/ticks/LevelChunkTicks.java b/net/minecraft/world/ticks/LevelChunkTicks.java +index 26620c06d26a2c0eb957fbadc6ac3d7a309bff46..3858c83c58e78435a6e29de84c33faa2f26d593d 100644 +--- a/net/minecraft/world/ticks/LevelChunkTicks.java ++++ b/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 implements SerializableTickContainer, TickContainerAccess { ++public class LevelChunkTicks implements SerializableTickContainer, TickContainerAccess, ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks { // Paper - rewrite chunk system + private final Queue> tickQueue = new PriorityQueue<>(ScheduledTick.DRAIN_ORDER); + @Nullable + private List> pendingTicks; +@@ -25,6 +25,30 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon + @Nullable + private BiConsumer, ScheduledTick> 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 implements SerializableTickContainer, TickCon + public ScheduledTick poll() { + ScheduledTick 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 implements SerializableTickContainer, TickCon + @Override + public void schedule(ScheduledTick 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 implements SerializableTickContainer, TickCon + while (iterator.hasNext()) { + ScheduledTick 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 implements SerializableTickContainer, TickCon + } + + public ListTag save(long time, Function typeToNameFunction) { ++ this.lastSaved = time; // Paper - rewrite chunk system + ListTag listTag = new ListTag(); + + for (SavedTick savedTick : this.pack(time)) { +@@ -121,6 +146,7 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon + + public void unpack(long time) { + if (this.pendingTicks != null) { ++ this.lastSaved = time; // Paper - rewrite chunk system + int i = -this.pendingTicks.size(); + + for (SavedTick savedTick : this.pendingTicks) { +diff --git a/org/bukkit/craftbukkit/CraftChunk.java b/org/bukkit/craftbukkit/CraftChunk.java +index f3ab07e44e2e912ea66c6148cfdb2a4a528741b2..c2bffe3450ee9f768e00a23ec09df74d7a06d49b 100644 +--- a/org/bukkit/craftbukkit/CraftChunk.java ++++ b/org/bukkit/craftbukkit/CraftChunk.java +@@ -83,6 +83,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 +@@ -117,60 +123,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 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/org/bukkit/craftbukkit/CraftServer.java b/org/bukkit/craftbukkit/CraftServer.java +index 5b64111bc8baca45ecc7bfa384e5f8a004163a0b..97b5d6ba2b19a7c730730c74175a29157aed1840 100644 +--- a/org/bukkit/craftbukkit/CraftServer.java ++++ b/org/bukkit/craftbukkit/CraftServer.java +@@ -1448,7 +1448,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(); +@@ -1493,7 +1493,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); +@@ -2531,7 +2531,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/org/bukkit/craftbukkit/CraftWorld.java b/org/bukkit/craftbukkit/CraftWorld.java +index ca62105a0ff0aa69385cbf2018f8fe6a4bb69fd4..92d9f0ea8f7810ae20d3996f49aefa539b4bcb69 100644 +--- a/org/bukkit/craftbukkit/CraftWorld.java ++++ b/org/bukkit/craftbukkit/CraftWorld.java +@@ -507,15 +507,17 @@ 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 playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false); +- if (playersInRange.isEmpty()) return; ++ if (playersInRange.isEmpty()) return true; // Paper - chunk system + + FeatureHooks.sendChunkRefreshPackets(playersInRange, chunk); +- }); +- }); +- ++ // Paper - chunk system + return true; + } + +@@ -618,20 +620,8 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + public Collection getPluginChunkTickets(int x, int z) { + DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; +- SortedArraySet> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z)); +- +- if (tickets == null) { +- return Collections.emptyList(); +- } + +- ImmutableList.Builder 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 +@@ -639,7 +629,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + Map> ret = new HashMap<>(); + DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; + +- for (Long2ObjectMap.Entry>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) { ++ for (Long2ObjectMap.Entry>> chunkTickets : chunkDistanceManager.moonrise$getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { // Paper - rewrite chunk system + long chunkKey = chunkTickets.getLongKey(); + SortedArraySet> tickets = chunkTickets.getValue(); + +@@ -1342,12 +1332,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() { +@@ -2486,17 +2476,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/org/bukkit/craftbukkit/entity/CraftPlayer.java b/org/bukkit/craftbukkit/entity/CraftPlayer.java +index e9df37ff66700278bc94ea1e42135b92d97d03f7..6a647cab8b2e476987931486e290703b8726f2c7 100644 +--- a/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -3527,7 +3527,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 +@@ -3537,7 +3539,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 +@@ -3547,7 +3551,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/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java b/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java +index 39377ba0739f9660567b38475f101672f7b5e035..c025a4ff42257a4e84f0f9574b84f6987ef8ac11 100644 +--- a/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java ++++ b/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java +@@ -264,7 +264,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/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java +index 54c4434662d057a08800918641b95708cda61207..37458e8fd5d57acbf90a6bea4e66797cb07f69fa 100644 +--- a/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java ++++ b/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 moonrise$getHardCollidingEntities(final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB box, final java.util.function.Predicate predicate) { ++ return this.handle.moonrise$getHardCollidingEntities(entity, box, predicate); ++ } ++ // Paper end - rewrite chunk system + // Paper end + } + +diff --git a/org/spigotmc/AsyncCatcher.java b/org/spigotmc/AsyncCatcher.java +index ef2598760458833021ef1bee92137f42c9fe591f..1f23e775eba1c34e01145bd91b0ce26fed6ca9de 100644 +--- a/org/spigotmc/AsyncCatcher.java ++++ b/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/org/spigotmc/WatchdogThread.java b/org/spigotmc/WatchdogThread.java +index ad282d34919716b75acd10426cd071da9d064a51..529df2a41dd93d6e1505053bd04032dbf0cdaa31 100644 +--- a/org/spigotmc/WatchdogThread.java ++++ b/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, "------------------------------" ); + // diff --git a/paper-server/patches/features/0001-Add-PaperHooks.patch b/paper-server/patches/features/0001-Add-PaperHooks.patch index db8dd7f311..5df4d535a5 100644 --- a/paper-server/patches/features/0001-Add-PaperHooks.patch +++ b/paper-server/patches/features/0001-Add-PaperHooks.patch @@ -6,13 +6,14 @@ Subject: [PATCH] Add PaperHooks diff --git a/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java new file mode 100644 -index 0000000000000000000000000000000000000000..834c5ce238c7adb0164a6282582d709348ef96cc +index 0000000000000000000000000000000000000000..2988c418b34d6f699a9c24406cfd6949465b64f0 --- /dev/null +++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java -@@ -0,0 +1,240 @@ +@@ -0,0 +1,241 @@ +package ca.spottedleaf.moonrise.paper; + +import ca.spottedleaf.moonrise.common.PlatformHooks; ++import ca.spottedleaf.moonrise.paper.util.BaseChunkSystemHooks; +import com.mojang.datafixers.DSL; +import com.mojang.datafixers.DataFixer; +import com.mojang.serialization.Dynamic; @@ -39,7 +40,7 @@ index 0000000000000000000000000000000000000000..834c5ce238c7adb0164a6282582d7093 +import java.util.List; +import java.util.function.Predicate; + -+public final class PaperHooks implements PlatformHooks { ++public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHooks { + + @Override + public String getBrand() { @@ -250,3 +251,341 @@ index 0000000000000000000000000000000000000000..834c5ce238c7adb0164a6282582d7093 + return org.spigotmc.TrackingRange.getEntityTrackingRange(entity, currentRange); + } +} +diff --git a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java +new file mode 100644 +index 0000000000000000000000000000000000000000..34b45bc11124efb22f0f3ae5b2ad8f445c719476 +--- /dev/null ++++ b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java +@@ -0,0 +1,332 @@ ++package ca.spottedleaf.moonrise.paper.util; ++ ++import ca.spottedleaf.concurrentutil.util.Priority; ++import com.mojang.logging.LogUtils; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ChunkResult; ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.world.level.ChunkPos; ++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 org.bukkit.Bukkit; ++import org.slf4j.Logger; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.concurrent.CompletableFuture; ++import java.util.function.Consumer; ++ ++public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.common.util.ChunkSystemHooks { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ private static final ChunkStep FULL_CHUNK_STEP = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL); ++ private static final TicketType CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo); ++ ++ private long chunkLoadCounter = 0L; ++ ++ private static int getDistance(final ChunkStatus status) { ++ return FULL_CHUNK_STEP.getAccumulatedRadiusOf(status); ++ } ++ ++ @Override ++ public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) { ++ this.scheduleChunkTask(level, chunkX, chunkZ, run, Priority.NORMAL); ++ } ++ ++ @Override ++ public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final Priority priority) { ++ level.chunkSource.mainThreadProcessor.execute(run); ++ } ++ ++ @Override ++ public 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 onComplete) { ++ if (gen) { ++ this.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ return; ++ } ++ this.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)) { ++ BaseChunkSystemHooks.this.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ } else { ++ if (onComplete != null) { ++ onComplete.accept(null); ++ } ++ } ++ } ++ }); ++ } ++ ++ @Override ++ public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, ++ final boolean addTicket, final Priority priority, final Consumer onComplete) { ++ if (!Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) { ++ this.scheduleChunkTask(level, chunkX, chunkZ, () -> { ++ BaseChunkSystemHooks.this.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ }, priority); ++ return; ++ } ++ ++ final int minLevel = 33 + getDistance(toStatus); ++ final Long chunkReference = addTicket ? Long.valueOf(++this.chunkLoadCounter) : null; ++ final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); ++ ++ if (addTicket) { ++ level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); ++ } ++ level.chunkSource.runDistanceManagerUpdates(); ++ ++ final Consumer 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(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (holder == null || holder.getTicketLevel() > minLevel) { ++ loadCallback.accept(null); ++ return; ++ } ++ ++ final CompletableFuture> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap); ++ ++ if (loadFuture.isDone()) { ++ loadCallback.accept(loadFuture.join().orElse(null)); ++ return; ++ } ++ ++ loadFuture.whenCompleteAsync((final ChunkResult result, final Throwable thr) -> { ++ if (thr != null) { ++ loadCallback.accept(null); ++ return; ++ } ++ loadCallback.accept(result.orElse(null)); ++ }, (final Runnable r) -> { ++ BaseChunkSystemHooks.this.scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST); ++ }); ++ } ++ ++ @Override ++ public void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, ++ final FullChunkStatus toStatus, final boolean addTicket, ++ final Priority priority, final Consumer onComplete) { ++ // This method goes unused until the chunk system rewrite ++ if (toStatus == FullChunkStatus.INACCESSIBLE) { ++ throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); ++ } ++ ++ if (!Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) { ++ this.scheduleChunkTask(level, chunkX, chunkZ, () -> { ++ BaseChunkSystemHooks.this.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(++this.chunkLoadCounter) : null; ++ final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); ++ ++ if (addTicket) { ++ level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); ++ } ++ level.chunkSource.runDistanceManagerUpdates(); ++ ++ final Consumer 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(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); ++ level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); ++ } ++ } ++ }; ++ ++ final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (holder == null || holder.getTicketLevel() > minLevel) { ++ loadCallback.accept(null); ++ return; ++ } ++ ++ final CompletableFuture> 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 ChunkResult result, final Throwable thr) -> { ++ if (thr != null) { ++ loadCallback.accept(null); ++ return; ++ } ++ loadCallback.accept(result.orElse(null)); ++ }, (final Runnable r) -> { ++ BaseChunkSystemHooks.this.scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST); ++ }); ++ } ++ ++ @Override ++ public List getVisibleChunkHolders(final ServerLevel level) { ++ return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values()); ++ } ++ ++ @Override ++ public List getUpdatingChunkHolders(final ServerLevel level) { ++ return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values()); ++ } ++ ++ @Override ++ public int getVisibleChunkHolderCount(final ServerLevel level) { ++ return level.chunkSource.chunkMap.visibleChunkMap.size(); ++ } ++ ++ @Override ++ public int getUpdatingChunkHolderCount(final ServerLevel level) { ++ return level.chunkSource.chunkMap.updatingChunkMap.size(); ++ } ++ ++ @Override ++ public boolean hasAnyChunkHolders(final ServerLevel level) { ++ return this.getUpdatingChunkHolderCount(level) != 0; ++ } ++ ++ @Override ++ public void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) { ++ ++ } ++ ++ @Override ++ public void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { ++ ++ } ++ ++ @Override ++ public void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) { ++ ++ } ++ ++ @Override ++ public void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { ++ ++ } ++ ++ @Override ++ public void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { ++ ++ } ++ ++ @Override ++ public void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder) { ++ ++ } ++ ++ @Override ++ public void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { ++ ++ } ++ ++ @Override ++ public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { ++ ++ } ++ ++ @Override ++ public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { ++ ++ } ++ ++ @Override ++ public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { ++ ++ } ++ ++ @Override ++ public ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { ++ return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ); ++ } ++ ++ @Override ++ public int getSendViewDistance(final ServerPlayer player) { ++ return this.getViewDistance(player); ++ } ++ ++ @Override ++ public int getViewDistance(final ServerPlayer player) { ++ final ServerLevel level = player.serverLevel(); ++ if (level == null) { ++ return Bukkit.getViewDistance(); ++ } ++ return level.chunkSource.chunkMap.serverViewDistance; ++ } ++ ++ @Override ++ public int getTickViewDistance(final ServerPlayer player) { ++ final ServerLevel level = player.serverLevel(); ++ if (level == null) { ++ return Bukkit.getSimulationDistance(); ++ } ++ return level.chunkSource.chunkMap.distanceManager.simulationDistance; ++ } ++ ++ @Override ++ public void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player) { ++ ++ } ++ ++ @Override ++ public void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player) { ++ ++ } ++ ++ @Override ++ public void updateMaps(final ServerLevel world, final ServerPlayer player) { ++ ++ } ++} diff --git a/paper-server/patches/features/0018-Moonrise-optimisation-patches.patch b/paper-server/patches/features/0018-Moonrise-optimisation-patches.patch deleted file mode 100644 index dea5778bd2..0000000000 --- a/paper-server/patches/features/0018-Moonrise-optimisation-patches.patch +++ /dev/null @@ -1,36330 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -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/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7e440b4a46b040365df7317035e577d93e7d855d ---- /dev/null -+++ b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java -@@ -0,0 +1,273 @@ -+package ca.spottedleaf.moonrise.common.misc; -+ -+import ca.spottedleaf.moonrise.common.list.ReferenceList; -+import ca.spottedleaf.moonrise.common.util.CoordinateUtils; -+import ca.spottedleaf.moonrise.common.util.MoonriseConstants; -+import ca.spottedleaf.moonrise.common.util.ChunkSystem; -+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; -+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; -+import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants; -+import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel; -+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; -+import net.minecraft.core.BlockPos; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.world.level.ChunkPos; -+import java.util.ArrayList; -+ -+public final class NearbyPlayers { -+ -+ public static enum NearbyMapType { -+ GENERAL, -+ GENERAL_SMALL, -+ GENERAL_REALLY_SMALL, -+ TICK_VIEW_DISTANCE, -+ VIEW_DISTANCE, -+ // Moonrise start - chunk tick iteration -+ SPAWN_RANGE { -+ @Override -+ void addTo(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { -+ ((ChunkTickServerLevel)world).moonrise$addPlayerTickingRequest(chunkX, chunkZ); -+ } -+ -+ @Override -+ void removeFrom(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { -+ ((ChunkTickServerLevel)world).moonrise$removePlayerTickingRequest(chunkX, chunkZ); -+ } -+ }; -+ // Moonrise end - chunk tick iteration -+ -+ void addTo(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { -+ -+ } -+ -+ void removeFrom(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { -+ -+ } -+ } -+ -+ private static final NearbyMapType[] MAP_TYPES = NearbyMapType.values(); -+ public static final int TOTAL_MAP_TYPES = MAP_TYPES.length; -+ -+ private static final int GENERAL_AREA_VIEW_DISTANCE = MoonriseConstants.MAX_VIEW_DISTANCE + 1; -+ private static final int GENERAL_SMALL_VIEW_DISTANCE = 10; -+ private static final int GENERAL_REALLY_SMALL_VIEW_DISTANCE = 3; -+ -+ public static final int GENERAL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_AREA_VIEW_DISTANCE << 4); -+ public static final int GENERAL_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_SMALL_VIEW_DISTANCE << 4); -+ public static final int GENERAL_REALLY_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_REALLY_SMALL_VIEW_DISTANCE << 4); -+ -+ private final ServerLevel world; -+ private final Reference2ReferenceOpenHashMap players = new Reference2ReferenceOpenHashMap<>(); -+ private final Long2ReferenceOpenHashMap byChunk = new Long2ReferenceOpenHashMap<>(); -+ private final Long2ReferenceOpenHashMap>[] directByChunk = new Long2ReferenceOpenHashMap[TOTAL_MAP_TYPES]; -+ { -+ for (int i = 0; i < this.directByChunk.length; ++i) { -+ this.directByChunk[i] = new Long2ReferenceOpenHashMap<>(); -+ } -+ } -+ -+ public NearbyPlayers(final ServerLevel world) { -+ this.world = world; -+ } -+ -+ public void addPlayer(final ServerPlayer player) { -+ final TrackedPlayer[] newTrackers = new TrackedPlayer[TOTAL_MAP_TYPES]; -+ if (this.players.putIfAbsent(player, newTrackers) != null) { -+ throw new IllegalStateException("Already have player " + player); -+ } -+ -+ final ChunkPos chunk = player.chunkPosition(); -+ -+ for (int i = 0; i < TOTAL_MAP_TYPES; ++i) { -+ // use 0 for default, will be updated by tickPlayer -+ (newTrackers[i] = new TrackedPlayer(player, MAP_TYPES[i])).add(chunk.x, chunk.z, 0); -+ } -+ -+ // update view distances -+ this.tickPlayer(player); -+ } -+ -+ public void removePlayer(final ServerPlayer player) { -+ final TrackedPlayer[] players = this.players.remove(player); -+ if (players == null) { -+ return; // May be called during teleportation before the player is actually placed -+ } -+ -+ for (final TrackedPlayer tracker : players) { -+ tracker.remove(); -+ } -+ } -+ -+ public void clear() { -+ if (this.players.isEmpty()) { -+ return; -+ } -+ -+ for (final ServerPlayer player : new ArrayList<>(this.players.keySet())) { -+ this.removePlayer(player); -+ } -+ } -+ -+ public void tickPlayer(final ServerPlayer player) { -+ final TrackedPlayer[] players = this.players.get(player); -+ if (players == null) { -+ throw new IllegalStateException("Don't have player " + player); -+ } -+ -+ final ChunkPos chunk = player.chunkPosition(); -+ -+ players[NearbyMapType.GENERAL.ordinal()].update(chunk.x, chunk.z, GENERAL_AREA_VIEW_DISTANCE); -+ players[NearbyMapType.GENERAL_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_SMALL_VIEW_DISTANCE); -+ players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE); -+ players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getTickViewDistance(player)); -+ players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getViewDistance(player)); -+ players[NearbyMapType.SPAWN_RANGE.ordinal()].update(chunk.x, chunk.z, ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Moonrise - chunk tick iteration -+ } -+ -+ public TrackedChunk getChunk(final ChunkPos pos) { -+ return this.byChunk.get(CoordinateUtils.getChunkKey(pos)); -+ } -+ -+ public TrackedChunk getChunk(final BlockPos pos) { -+ return this.byChunk.get(CoordinateUtils.getChunkKey(pos)); -+ } -+ -+ public TrackedChunk getChunk(final int chunkX, final int chunkZ) { -+ return this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ -+ public ReferenceList getPlayers(final BlockPos pos, final NearbyMapType type) { -+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos)); -+ } -+ -+ public ReferenceList getPlayers(final ChunkPos pos, final NearbyMapType type) { -+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos)); -+ } -+ -+ public ReferenceList getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) { -+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ -+ public ReferenceList getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) { -+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4)); -+ } -+ -+ public static final class TrackedChunk { -+ -+ private static final ServerPlayer[] EMPTY_PLAYERS_ARRAY = new ServerPlayer[0]; -+ -+ private final long chunkKey; -+ private final NearbyPlayers nearbyPlayers; -+ private final ReferenceList[] players = new ReferenceList[TOTAL_MAP_TYPES]; -+ private int nonEmptyLists; -+ private long updateCount; -+ -+ public TrackedChunk(final long chunkKey, final NearbyPlayers nearbyPlayers) { -+ this.chunkKey = chunkKey; -+ this.nearbyPlayers = nearbyPlayers; -+ } -+ -+ public boolean isEmpty() { -+ return this.nonEmptyLists == 0; -+ } -+ -+ public long getUpdateCount() { -+ return this.updateCount; -+ } -+ -+ public ReferenceList getPlayers(final NearbyMapType type) { -+ return this.players[type.ordinal()]; -+ } -+ -+ public void addPlayer(final ServerPlayer player, final NearbyMapType type) { -+ ++this.updateCount; -+ -+ final int idx = type.ordinal(); -+ final ReferenceList list = this.players[idx]; -+ if (list == null) { -+ ++this.nonEmptyLists; -+ final ReferenceList players = (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY)); -+ this.nearbyPlayers.directByChunk[idx].put(this.chunkKey, players); -+ players.add(player); -+ return; -+ } -+ -+ if (!list.add(player)) { -+ throw new IllegalStateException("Already contains player " + player); -+ } -+ } -+ -+ public void removePlayer(final ServerPlayer player, final NearbyMapType type) { -+ ++this.updateCount; -+ -+ final int idx = type.ordinal(); -+ final ReferenceList list = this.players[idx]; -+ if (list == null) { -+ throw new IllegalStateException("Does not contain player " + player); -+ } -+ -+ if (!list.remove(player)) { -+ throw new IllegalStateException("Does not contain player " + player); -+ } -+ -+ if (list.size() == 0) { -+ this.players[idx] = null; -+ this.nearbyPlayers.directByChunk[idx].remove(this.chunkKey); -+ --this.nonEmptyLists; -+ } -+ } -+ } -+ -+ private final class TrackedPlayer extends SingleUserAreaMap { -+ -+ private final NearbyMapType type; -+ -+ public TrackedPlayer(final ServerPlayer player, final NearbyMapType type) { -+ super(player); -+ this.type = type; -+ } -+ -+ @Override -+ protected void addCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) { -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ -+ final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey); -+ final NearbyMapType type = this.type; -+ if (chunk != null) { -+ chunk.addPlayer(parameter, type); -+ type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ); -+ } else { -+ final TrackedChunk created = new TrackedChunk(chunkKey, NearbyPlayers.this); -+ NearbyPlayers.this.byChunk.put(chunkKey, created); -+ created.addPlayer(parameter, type); -+ type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ); -+ -+ ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$requestChunkData(chunkKey).nearbyPlayers = created; -+ } -+ } -+ -+ @Override -+ protected void removeCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) { -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ -+ final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey); -+ if (chunk == null) { -+ throw new IllegalStateException("Chunk should exist at " + new ChunkPos(chunkKey)); -+ } -+ -+ final NearbyMapType type = this.type; -+ chunk.removePlayer(parameter, type); -+ type.removeFrom(parameter, NearbyPlayers.this.world, chunkX, chunkZ); -+ -+ if (chunk.isEmpty()) { -+ NearbyPlayers.this.byChunk.remove(chunkKey); -+ final ChunkData chunkData = ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$releaseChunkData(chunkKey); -+ if (chunkData != null) { -+ chunkData.nearbyPlayers = null; -+ } -+ } -+ } -+ } -+} -diff --git a/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/ca/spottedleaf/moonrise/common/util/ChunkSystem.java -index 58a99bc38e137431f10af36fa9e2d04fe61694aa..1d288e73fd8605676c0da676e068afb5b4b8abea 100644 ---- a/ca/spottedleaf/moonrise/common/util/ChunkSystem.java -+++ b/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 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 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 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 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> 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 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 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 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> 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 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 getVisibleChunkHolders(final ServerLevel level) { -- return new java.util.ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values()); -+ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders(); - } - - public static List 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 getViewDistance(player); -+ return RegionizedPlayerChunkLoader.getAPISendViewDistance(player); - } - - public static int getViewDistance(final ServerPlayer player) { -- final ServerLevel level = player.serverLevel(); -- if (level == null) { -- return org.bukkit.Bukkit.getViewDistance(); -- } -- return level.chunkSource.chunkMap.serverViewDistance; -+ return RegionizedPlayerChunkLoader.getAPIViewDistance(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/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java b/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java -index 12eb3add0931a4d77acdf6e875c42dda9c313dc3..5239993a681d6113eec99fa627b85508656ed7ac 100644 ---- a/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java -+++ b/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/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java -index 11cfe9cc29666ce3a6a40281069fb9eb4fa0ded2..de22cfd2da4782072584d5140ce5567780d6feaa 100644 ---- a/ca/spottedleaf/moonrise/paper/PaperHooks.java -+++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java -@@ -267,7 +267,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/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java -new file mode 100644 -index 0000000000000000000000000000000000000000..93bc56daec4526f373c84763b8c7ccb4a30e800b ---- /dev/null -+++ b/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 moonrise$countEntries(); -+ -+} -diff --git a/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0d1443a113c07d7655e7b927a899447f70db8fa9 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java -new file mode 100644 -index 0000000000000000000000000000000000000000..89e75b454695e174c5619104eeb15eb923a2d9a7 ---- /dev/null -+++ b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java -@@ -0,0 +1,12 @@ -+package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess; -+ -+public interface PropertyAccess { -+ -+ 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/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java -new file mode 100644 -index 0000000000000000000000000000000000000000..01da52b9e8a786824f199a057b62ce0431ecbc43 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java -new file mode 100644 -index 0000000000000000000000000000000000000000..866f38eb0f379ffbe2888023a7d1c290f521a231 ---- /dev/null -+++ b/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 { -+ -+ private final Int2ObjectOpenHashMap propertyToIndexer; -+ private S[] lookup; -+ private final Collection> properties; -+ -+ public ZeroCollidingReferenceStateTable(final Collection> properties) { -+ this.propertyToIndexer = new Int2ObjectOpenHashMap<>(properties.size()); -+ this.properties = new ReferenceArrayList<>(properties); -+ -+ final List> 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 > boolean hasProperty(final Property property) { -+ return this.propertyToIndexer.containsKey(((PropertyAccess)property).moonrise$getId()); -+ } -+ -+ public long getIndex(final StateHolder stateHolder) { -+ long ret = 0L; -+ -+ for (final Map.Entry, 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, Comparable>, S> universe) { -+ if (this.lookup != null) { -+ throw new IllegalStateException(); -+ } -+ -+ this.lookup = (S[])new StateHolder[universe.size()]; -+ -+ for (final Map.Entry, Comparable>, S> entry : universe.entrySet()) { -+ final S value = entry.getValue(); -+ if (value == null) { -+ continue; -+ } -+ this.lookup[(int)((PropertyAccessStateHolder)(StateHolder)value).moonrise$getTableIndex()] = value; -+ } -+ -+ for (final S value : this.lookup) { -+ if (value == null) { -+ throw new IllegalStateException(); -+ } -+ } -+ } -+ -+ public > T get(final long index, final Property property) { -+ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess)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)property).moonrise$getById((int)modded); -+ } -+ -+ public > S set(final long index, final Property property, final T with) { -+ final int newValueId = ((PropertyAccess)property).moonrise$getIdFor(with); -+ if (newValueId < 0) { -+ return null; -+ } -+ -+ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess)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 > S trySet(final long index, final Property property, final T with, final S dfl) { -+ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess)property).moonrise$getId()); -+ if (indexer == null) { -+ return dfl; -+ } -+ -+ final int newValueId = ((PropertyAccess)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> getProperties() { -+ return Collections.unmodifiableCollection(this.properties); -+ } -+ -+ public Map, 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, 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, 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, Comparable>> { -+ @Override -+ public ObjectIterator, Comparable>> iterator() { -+ final Iterator> propIterator = ZeroCollidingReferenceStateTable.this.properties.iterator(); -+ return new ObjectIterator<>() { -+ @Override -+ public boolean hasNext() { -+ return propIterator.hasNext(); -+ } -+ -+ @Override -+ public Entry, 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/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java -new file mode 100644 -index 0000000000000000000000000000000000000000..44bb25554634af2ec0b2e9b3d9231304d5dff034 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java b/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c7da23900228aab3a5673eb5adfada5091140319 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a814512fcfb85312474ae2c2c21443843bf57831 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1acea58838f057ab87efd103cbecb6f5aeaef393 ---- /dev/null -+++ b/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. -+ *

    -+ * Impl notes: -+ *

    -+ *
  • -+ * 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. -+ *
  • -+ *
  • -+ * Writes may be called concurrently, although only the "later" write will go through. -+ *
  • -+ * -+ * @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. -+ *

    -+ * Impl notes: -+ *

    -+ *
  • -+ * 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. -+ *
  • -+ *
  • -+ * Writes may be called concurrently, although only the "later" write will go through. -+ *
  • -+ * -+ * @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 consumer) -> { -+ consumer.accept(data, null); -+ }, null, type, priority -+ ); -+ } -+ -+ /** -+ * Schedules the chunk data to be written asynchronously. -+ *

    -+ * Impl notes: -+ *

    -+ *
  • -+ * 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. -+ *
  • -+ *
  • -+ * Writes may be called concurrently, although only the "later" write will go through. -+ *
  • -+ *
  • -+ * The specified write task, if not null, will have its priority controlled by the scheduler. -+ *
  • -+ * -+ * @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 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. -+ *

    -+ * Impl notes: -+ *

    -+ *
  • -+ * 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. -+ *
  • -+ *
  • -+ * Writes may be called concurrently, although only the "later" write will go through. -+ *
  • -+ *
  • -+ * The specified write task, if not null, will have its priority controlled by the scheduler. -+ *
  • -+ * -+ * @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 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> 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. -+ *

    -+ * Impl notes: -+ *

    -+ *
  • -+ * 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. -+ *
  • -+ * -+ * @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 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. -+ *

    -+ * Impl notes: -+ *

    -+ *
  • -+ * 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. -+ *
  • -+ * -+ * @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 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. -+ *

    -+ * Impl notes: -+ *

    -+ *
  • -+ * 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. -+ *
  • -+ * -+ * @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 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. -+ *

    -+ * Impl notes: -+ *

    -+ *
  • -+ * 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. -+ *
  • -+ * -+ * @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 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}. -+ *

    -+ * Impl notes: -+ *

    -+ *
  • -+ * 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. -+ *
  • -+ * -+ * @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 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}. -+ *

    -+ * Impl notes: -+ *

    -+ *
  • -+ * 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. -+ *
  • -+ * -+ * @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 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 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 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 callback; -+ private ChunkIOTask.InProgressRead read; -+ private ChunkIOTask.InProgressWrite write; -+ -+ private CancellableRead(final BiConsumer callback, -+ final ChunkIOTask.InProgressRead read, -+ final ChunkIOTask.InProgressWrite write) { -+ this.callback = callback; -+ this.read = read; -+ this.write = write; -+ } -+ -+ @Override -+ public boolean cancel() { -+ final BiConsumer 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 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> callbacks = new MultiThreadedQueue<>(); -+ -+ public boolean hasNoWaiters() { -+ return this.callbacks.isEmpty(); -+ } -+ -+ public boolean addToAsyncWaiters(final BiConsumer callback) { -+ return this.callbacks.add(callback); -+ } -+ -+ public boolean cancel(final BiConsumer 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 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> 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> scheduler) { -+ scheduler.accept((final CompoundTag data, final Throwable throwable) -> { -+ InProgressWrite.this.complete(task, data, throwable); -+ }); -+ } -+ -+ public boolean addToAsyncWaiters(final BiConsumer callback) { -+ return this.callbacks.add(callback); -+ } -+ -+ public void addToWaiters(final ChunkIOTask task, final BiConsumer consumer) { -+ if (!this.callbacks.add(consumer)) { -+ this.syncAccept(task, consumer, this.value, this.throwable); -+ } -+ } -+ -+ private void syncAccept(final ChunkIOTask task, final BiConsumer 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 consumer; -+ while ((consumer = this.callbacks.pollOrBlockAdds()) != null) { -+ this.syncAccept(task, consumer, value, throwable); -+ } -+ } -+ -+ public boolean cancel(final BiConsumer 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a36ab89f5c37f5f9ab0152f087bb4cf3560f8581 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java -new file mode 100644 -index 0000000000000000000000000000000000000000..828c868f68c2a20bf90d0f7ec253fdeb591f15f6 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java -new file mode 100644 -index 0000000000000000000000000000000000000000..bd0d782852f9cfe5bc0b5339ecf4d82c10332ec9 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java -new file mode 100644 -index 0000000000000000000000000000000000000000..47a4d3376d08dde94a39254bec21473ff27f53e6 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5d4d650186b18eb00782429d53d861564d8e4ba9 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0b58701342d573fa43cdd06681534854a0e51d77 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c278f8ef806f0b45c28cc3040c7db052cb51e053 ---- /dev/null -+++ b/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> onLoad); -+ -+ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, -+ final ChunkStatus chunkStatus, final Priority priority, -+ final Consumer> onLoad); -+ -+ public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, -+ final Priority priority, -+ final Consumer> 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> onLoad); -+ -+ public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder(); -+ -+ public long moonrise$getLastMidTickFailure(); -+ -+ public void moonrise$setLastMidTickFailure(final long time); -+ -+ public NearbyPlayers moonrise$getNearbyPlayers(); -+ -+ public ReferenceList moonrise$getLoadedChunks(); -+ -+ public ReferenceList moonrise$getTickingChunks(); -+ -+ public ReferenceList moonrise$getEntityTickingChunks(); -+} -diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8b9dc582627b46843f4b5ea6f8c3df2d8cac46fa ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7d049d750df88762566f13a9c4fc7574a2df4825 ---- /dev/null -+++ b/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 moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge); -+ -+ public LevelChunk moonrise$getFullChunk(); -+ -+} -diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f4bc44bb266763345c4e6f859c89352c769a104d ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..aacd543f03b35908011d0c2891e978cc093ebcf5 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5b092bca7027e37aeee8f4b852ad896dd0d5febc ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7aea4e343581b977d11af90f9f65eac3532eade1 ---- /dev/null -+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java -@@ -0,0 +1,569 @@ -+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, EntityCollectionBySection> entitiesByClass; -+ private final Reference2ObjectOpenHashMap, 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 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 entities, final ChunkPos chunkPos, final ServerLevel world) { -+ return saveEntityChunk0(entities, chunkPos, world, false); -+ } -+ -+ public static CompoundTag saveEntityChunk0(final List 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 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 getAllEntities() { -+ final int len = this.entities.size(); -+ if (len == 0) { -+ return new ArrayList<>(); -+ } -+ -+ final Entity[] rawData = this.entities.getRawData(); -+ final List 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, EntityCollectionBySection>> iterator = -+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final Reference2ObjectMap.Entry, 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, EntityCollectionBySection>> iterator = -+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final Reference2ObjectMap.Entry, 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 into, final Predicate predicate) { -+ this.hardCollidingEntities.getEntities(except, box, into, predicate); -+ } -+ -+ public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { -+ this.allEntities.getEntities(except, box, into, predicate); -+ } -+ -+ -+ public boolean getEntities(final Entity except, final AABB box, final List into, final Predicate predicate, -+ final int maxCount) { -+ return this.allEntities.getEntitiesLimited(except, box, into, predicate, maxCount); -+ } -+ -+ public void getEntities(final EntityType type, final AABB box, final List into, -+ final Predicate predicate) { -+ final EntityCollectionBySection byType = this.entitiesByType.get(type); -+ -+ if (byType != null) { -+ byType.getEntities((Entity)null, box, (List)into, (Predicate) predicate); -+ } -+ } -+ -+ public boolean getEntities(final EntityType type, final AABB box, final List into, -+ final Predicate 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 clazz) { -+ final EntityCollectionBySection ret = new EntityCollectionBySection(this); -+ -+ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) { -+ final BasicEntityList 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 void getEntities(final Class clazz, final Entity except, final AABB box, final List into, -+ final Predicate predicate) { -+ EntityCollectionBySection collection = this.entitiesByClass.get(clazz); -+ if (collection != null) { -+ collection.getEntities(except, box, (List)into, (Predicate)predicate); -+ } else { -+ this.entitiesByClass.put(clazz, collection = this.initClass(clazz)); -+ collection.getEntities(except, box, (List)into, (Predicate)predicate); -+ } -+ } -+ -+ public boolean getEntities(final Class clazz, final Entity except, final AABB box, final List into, -+ final Predicate predicate, final int maxCount) { -+ EntityCollectionBySection collection = this.entitiesByClass.get(clazz); -+ if (collection != null) { -+ return collection.getEntitiesLimited(except, box, (List)into, (Predicate)predicate, maxCount); -+ } else { -+ this.entitiesByClass.put(clazz, collection = this.initClass(clazz)); -+ return collection.getEntitiesLimited(except, box, (List)into, (Predicate)predicate, maxCount); -+ } -+ } -+ -+ private static final class BasicEntityList { -+ -+ 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[] 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 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 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 into, final Predicate 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[] entitiesBySection = this.entitiesBySection; -+ -+ for (int section = min; section <= max; ++section) { -+ final BasicEntityList 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 into, final Predicate 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[] entitiesBySection = this.entitiesBySection; -+ -+ for (int section = min; section <= max; ++section) { -+ final BasicEntityList 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; -+ } -+ } -+} -diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7554c109c35397bc1a43dd80e87764fd78645bbf ---- /dev/null -+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java -@@ -0,0 +1,1002 @@ -+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 { -+ -+ 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 regions = new SWMRLong2ObjectHashTable<>(128, 0.5f); -+ -+ protected final LevelCallback worldCallback; -+ -+ protected final ConcurrentLong2ReferenceChainedHashTable entityById = new ConcurrentLong2ReferenceChainedHashTable<>(); -+ protected final ConcurrentHashMap entityByUUID = new ConcurrentHashMap<>(); -+ protected final EntityList accessibleEntities = new EntityList(); -+ -+ public EntityLookup(final Level world, final LevelCallback 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 implements Iterable { -+ -+ 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 iterator() { -+ return new ArrayIterator<>(this.array, this.off, this.length); -+ } -+ -+ protected static final class ArrayIterator implements Iterator { -+ -+ 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 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 void get(final EntityTypeTest filter, final AbortableIterationConsumer action) { -+ for (final Iterator 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 action) { -+ List entities = new ArrayList<>(); -+ this.getEntities((Entity)null, box, entities, null); -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ action.accept(entities.get(i)); -+ } -+ } -+ -+ @Override -+ public void get(final EntityTypeTest filter, final AABB box, final AbortableIterationConsumer action) { -+ List entities = new ArrayList<>(); -+ this.getEntities((Entity)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 entities, final ChunkPos forChunk) { -+ this.addEntityChunk(entities, forChunk, true); -+ } -+ -+ public void addEntityChunkEntities(final List entities, final ChunkPos forChunk) { -+ this.addEntityChunk(entities, forChunk, true); -+ } -+ -+ public void addWorldGenChunkEntities(final List 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 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 getEntities(final Entity except, final AABB box, final List into, final Predicate 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 into, final Predicate 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 void getEntities(final EntityType type, final AABB box, final List into, -+ final Predicate 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 void getEntities(final Class clazz, final Entity except, final AABB box, final List into, -+ final Predicate 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 getEntities(final Entity except, final AABB box, final List into, final Predicate 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 void getEntities(final EntityType type, final AABB box, final List into, -+ final Predicate 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 void getEntities(final Class clazz, final Entity except, final AABB box, final List into, -+ final Predicate 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/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a038215156a163b0b1cbc870ada5b4ac85ed1335 ---- /dev/null -+++ b/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 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/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2ff58cf753c60913ee73aae015182e9c5560d529 ---- /dev/null -+++ b/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 { -+ -+ @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/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java -new file mode 100644 -index 0000000000000000000000000000000000000000..58d9187adc188b693b6becc400f766e069bf1bf5 ---- /dev/null -+++ b/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 trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker -+ -+ public ServerEntityLookup(final ServerLevel world, final LevelCallback 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/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..458d1fc5e1222912512e6c59b56f6fca347d9ee9 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java -new file mode 100644 -index 0000000000000000000000000000000000000000..89b956b8fdf1a0d862a843104511005e2990a897 ---- /dev/null -+++ b/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 moonrise$asOptional(); -+ -+} -diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java -new file mode 100644 -index 0000000000000000000000000000000000000000..bbf9d6c1c9525d97160806819a57be03eca290f1 ---- /dev/null -+++ b/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 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 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 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java -new file mode 100644 -index 0000000000000000000000000000000000000000..524752744e37a2db0e3ea089468bdf497129bfef ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..003a857e70ead858e8437e3c1bfaf22f4daba0df ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -new file mode 100644 -index 0000000000000000000000000000000000000000..dd2509996bfd08e8c3f9f2be042229eac6d7692d ---- /dev/null -+++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -@@ -0,0 +1,1092 @@ -+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.MoonriseConstants; -+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 PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo); -+ public static final TicketType 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 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) { -+ if (distance != -1 && (distance < (0) || distance > (MoonriseConstants.MAX_VIEW_DISTANCE))) { -+ throw new IllegalArgumentException(Integer.toString(distance)); -+ } -+ return new ViewDistances(distance, this.loadViewDistance, this.sendViewDistance); -+ } -+ -+ public ViewDistances setLoadViewDistance(final int distance) { -+ // note: load view distance = api view distance + 1 -+ if (distance != -1 && (distance < (2 + 1) || distance > (MoonriseConstants.MAX_VIEW_DISTANCE + 1))) { -+ throw new IllegalArgumentException(Integer.toString(distance)); -+ } -+ return new ViewDistances(this.tickViewDistance, distance, this.sendViewDistance); -+ } -+ -+ public ViewDistances setSendViewDistance(final int distance) { -+ // note: send view distance <= load view distance - 1 -+ if (distance != -1 && (distance < (0) || distance > (MoonriseConstants.MAX_VIEW_DISTANCE))) { -+ throw new IllegalArgumentException(Integer.toString(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 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> 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 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 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 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 - 1) : (playerLoadViewDistance - 1) -+ ); -+ } -+ -+ 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/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7eafc5b7cba23d8dec92ecc1050afe3fd8c9e309 ---- /dev/null -+++ b/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 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 retrieveForAllRegions() { -+ final List ret = new ArrayList<>(); -+ -+ for (final Iterator> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) { -+ final ConcurrentLong2ReferenceChainedHashTable.TableEntry 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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3990834a41116682d6ae779a3bf24b0fd989d97d ---- /dev/null -+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java -@@ -0,0 +1,1457 @@ -+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 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>> tickets = new ConcurrentLong2ReferenceChainedHashTable<>(); -+ private final ConcurrentLong2ReferenceChainedHashTable sectionToChunkToExpireCount = new ConcurrentLong2ReferenceChainedHashTable<>(); -+ final ChunkUnloadQueue unloadQueue; -+ -+ private final ConcurrentLong2ReferenceChainedHashTable chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f); -+ private final ServerLevel world; -+ private final ChunkTaskScheduler taskScheduler; -+ private long currentTick; -+ -+ private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); -+ private final ObjectRBTreeSet 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 scheduledTasks = new ArrayList<>(); -+ final List 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 scheduledTasks, -+ final List changedFullStatus) { -+ return this.ticketLevelPropagator.performUpdate( -+ sectionX, sectionZ, this.taskScheduler.schedulingLockArea, scheduledTasks, changedFullStatus -+ ); -+ } -+ -+ public List getOldChunkHolders() { -+ final List ret = new ArrayList<>(this.chunkHolders.size() + 1); -+ for (final Iterator iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) { -+ ret.add(iterator.next().vanillaChunkHolder); -+ } -+ return ret; -+ } -+ -+ public List getChunkHolders() { -+ final List ret = new ArrayList<>(this.chunkHolders.size() + 1); -+ for (final Iterator 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 getOldChunkHoldersIterable() { -+ return new Iterable() { -+ @Override -+ public Iterator iterator() { -+ final Iterator iterator = ChunkHolderManager.this.chunkHolders.valueIterator(); -+ return new Iterator() { -+ @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); -+ } -+ } -+ -+ this.taskScheduler.setShutdown(true); -+ } -+ -+ void ensureInAutosave(final NewChunkHolder holder) { -+ if (!this.autoSaveQueue.contains(holder)) { -+ holder.lastAutoSave = this.currentTick; -+ this.autoSaveQueue.add(holder); -+ } -+ } -+ -+ public void autoSave() { -+ final List 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 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 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 scheduledTasks, -+ final List changedFullStatus) { -+ final List 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> tickets = this.tickets.get(coordinate); -+ -+ return tickets != null ? tickets.first().toString() : "no_ticket"; -+ } finally { -+ if (ticketLock != null) { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ } -+ -+ public Long2ObjectOpenHashMap>> getTicketsCopy() { -+ final Long2ObjectOpenHashMap>> ret = new Long2ObjectOpenHashMap<>(); -+ final Long2ObjectOpenHashMap 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> iterator = sections.long2ObjectEntrySet().fastIterator(); -+ iterator.hasNext();) { -+ final Long2ObjectMap.Entry 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> tickets = this.tickets.get(coord); -+ if (tickets == null) { -+ // removed before we acquired lock -+ continue; -+ } -+ ret.put(coord, ((ChunkSystemSortedArraySet>)tickets).moonrise$copy()); -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ -+ return ret; -+ } -+ -+ // Paper start -+ public Collection getPluginChunkTickets(int x, int z) { -+ com.google.common.collect.ImmutableList.Builder ret; -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(x, z); -+ try { -+ final long coordinate = CoordinateUtils.getChunkKey(x, z); -+ final SortedArraySet> 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> tickets) { -+ return !tickets.isEmpty() ? tickets.first().getTicketLevel() : MAX_TICKET_LEVEL + 1; -+ } -+ -+ public boolean addTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, -+ final T identifier) { -+ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); -+ } -+ -+ public boolean addTicketAtLevel(final TicketType 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 boolean addTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) { -+ return this.addTicketAtLevel(type, chunk, level, identifier, true); -+ } -+ -+ boolean addTicketAtLevel(final TicketType 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 ticket = new Ticket<>(type, level, identifier); -+ ((ChunkSystemTicket)(Object)ticket).moonrise$setRemoveDelay(removeDelay); -+ -+ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; -+ try { -+ final SortedArraySet> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> { -+ return SortedArraySet.create(4); -+ }); -+ -+ final int levelBefore = getTicketLevelAt(ticketsAtChunk); -+ final Ticket current = (Ticket)((ChunkSystemSortedArraySet>)ticketsAtChunk).moonrise$replace(ticket); -+ final int levelAfter = getTicketLevelAt(ticketsAtChunk); -+ -+ if (current != ticket) { -+ final long oldRemoveDelay = ((ChunkSystemTicket)(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 boolean removeTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, final T identifier) { -+ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); -+ } -+ -+ public boolean removeTicketAtLevel(final TicketType type, final int chunkX, final int chunkZ, final int level, final T identifier) { -+ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); -+ } -+ -+ public boolean removeTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) { -+ return this.removeTicketAtLevel(type, chunk, level, identifier, true); -+ } -+ -+ boolean removeTicketAtLevel(final TicketType 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 probe = new Ticket<>(type, level, identifier); -+ -+ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; -+ try { -+ final SortedArraySet> ticketsAtChunk = this.tickets.get(chunk); -+ if (ticketsAtChunk == null) { -+ return false; -+ } -+ -+ final int oldLevel = getTicketLevelAt(ticketsAtChunk); -+ final Ticket ticket = (Ticket)((ChunkSystemSortedArraySet>)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 unknownTicket = new Ticket<>(TicketType.UNKNOWN, level, new ChunkPos(chunk)); -+ ((ChunkSystemTicket)(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)(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 void addAndRemoveTickets(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier, -+ final TicketType 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 boolean addIfRemovedTicket(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier, -+ final TicketType 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 void removeAllTicketsFor(final TicketType ticketType, final int ticketLevel, final T ticketIdentifier) { -+ if (ticketLevel > MAX_TICKET_LEVEL) { -+ return; -+ } -+ -+ final Long2ObjectOpenHashMap 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> iterator = sections.long2ObjectEntrySet().fastIterator(); -+ iterator.hasNext();) { -+ final Long2ObjectMap.Entry 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> 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 iterator1 = chunkToExpireCount.long2IntEntrySet().fastIterator(); iterator1.hasNext();) { -+ final Long2IntMap.Entry entry = iterator1.next(); -+ -+ final long chunkKey = entry.getLongKey(); -+ final int expireCount = entry.getIntValue(); -+ -+ final SortedArraySet> 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 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 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 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 changedFullStatus) { -+ if (changedFullStatus.isEmpty()) { -+ return; -+ } -+ if (!TickThread.isTickThread()) { -+ this.taskScheduler.scheduleChunkTask(() -> { -+ final ArrayDeque 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 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 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 stage1 = new ArrayList<>(); -+ final List 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 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 ( -+ TicketOperationType op, long chunkCoord, -+ TicketType ticketType, int ticketLevel, T identifier, -+ TicketType ticketType2, int ticketLevel2, V identifier2 -+ ) { -+ -+ private TicketOperation(TicketOperationType op, long chunkCoord, -+ TicketType ticketType, int ticketLevel, T identifier) { -+ this(op, chunkCoord, ticketType, ticketLevel, identifier, null, 0, null); -+ } -+ -+ public static TicketOperation addOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { -+ return addOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); -+ } -+ -+ public static TicketOperation addOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { -+ return addOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); -+ } -+ -+ public static TicketOperation addOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { -+ return new TicketOperation<>(TicketOperationType.ADD, chunk, type, ticketLevel, identifier); -+ } -+ -+ public static TicketOperation removeOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { -+ return removeOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); -+ } -+ -+ public static TicketOperation removeOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { -+ return removeOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); -+ } -+ -+ public static TicketOperation removeOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { -+ return new TicketOperation<>(TicketOperationType.REMOVE, chunk, type, ticketLevel, identifier); -+ } -+ -+ public static TicketOperation addIfRemovedOp(final long chunk, -+ final TicketType addType, final int addLevel, final T addIdentifier, -+ final TicketType removeType, final int removeLevel, final V removeIdentifier) { -+ return new TicketOperation<>( -+ TicketOperationType.ADD_IF_REMOVED, chunk, addType, addLevel, addIdentifier, -+ removeType, removeLevel, removeIdentifier -+ ); -+ } -+ -+ public static TicketOperation addAndRemove(final long chunk, -+ final TicketType addType, final int addLevel, final T addIdentifier, -+ final TicketType removeType, final int removeLevel, final V removeIdentifier) { -+ return new TicketOperation<>( -+ TicketOperationType.ADD_AND_REMOVE, chunk, addType, addLevel, addIdentifier, -+ removeType, removeLevel, removeIdentifier -+ ); -+ } -+ } -+ -+ private boolean processTicketOp(TicketOperation 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> operations) { -+ for (final TicketOperation operation : operations) { -+ this.processTicketOp(operation); -+ } -+ } -+ -+ private final ThreadLocal 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> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>(); -+ -+ static List getCurrentTicketUpdateScheduling() { -+ return CURRENT_TICKET_UPDATE_SCHEDULING.get(); -+ } -+ -+ private boolean processTicketUpdates(final boolean processFullUpdates, List 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 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 pendingFullLoadUpdate = this.pendingFullLoadUpdate; -+ -+ boolean ret = false; -+ -+ final List 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>>> iterator = this.tickets.entryIterator(); -+ iterator.hasNext();) { -+ final ConcurrentLong2ReferenceChainedHashTable.TableEntry>> coordinateTickets = iterator.next(); -+ final long coordinate = coordinateTickets.getKey(); -+ final SortedArraySet> 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>)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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java -new file mode 100644 -index 0000000000000000000000000000000000000000..67532b85073b7978254a0b04caadfe822679e61f ---- /dev/null -+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java -@@ -0,0 +1,1055 @@ -+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 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 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 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 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 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 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 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; -+ } -+ -+ private volatile boolean shutdown; -+ -+ public boolean hasShutdown() { -+ return this.shutdown; -+ } -+ -+ public void setShutdown(final boolean shutdown) { -+ this.shutdown = shutdown; -+ } -+ -+ 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 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 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 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 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 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 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; -+ } -+ -+ if (this.hasShutdown()) { -+ throw new IllegalStateException( -+ "Chunk system has shut down, cannot process chunk requests in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.world) + "' at " -+ + "(" + chunkX + "," + chunkZ + ") status: " + status -+ ); -+ } -+ -+ 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 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 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 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 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 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 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 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 chunkHolderNeighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1)); -+ final StaticCache2D 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java -new file mode 100644 -index 0000000000000000000000000000000000000000..eafa4e6d55cd0f9314ac0f2b96a7f48fbb5e1a4c ---- /dev/null -+++ b/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 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 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 result) { -+ final List completeWaiters; -+ ChunkLoadTask.EntityDataLoadTask entityDataLoadTask = null; -+ boolean scheduleEntityTask = false; -+ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ final List 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> 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> 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 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 result) { -+ final List completeWaiters; -+ ChunkLoadTask.PoiDataLoadTask poiDataLoadTask = null; -+ boolean schedulePoiTask = false; -+ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ final List 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> 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> 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> consumer; -+ protected final NewChunkHolder chunkHolder; -+ protected boolean completed; -+ protected GenericDataLoadTask schedule; -+ protected final AtomicBoolean scheduled = new AtomicBoolean(); -+ -+ public GenericDataLoadTaskCallback(final Consumer> 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 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 neighboursBlockingGenTask = new ReferenceLinkedOpenHashSet<>(4); -+ -+ /** -+ * map of ChunkHolder -> Required Status for this chunk -+ */ -+ private final Reference2ObjectLinkedOpenHashMap 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 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 scheduledTasks, final List 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 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 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>> statusWaiters = new Reference2ObjectOpenHashMap<>(); -+ -+ void addStatusConsumer(final ChunkStatus status, final Consumer 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> 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 consumer : consumers) { -+ try { -+ consumer.accept(chunk); -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to process chunk status callback", thr); -+ } -+ } -+ }, Priority.HIGHEST); -+ } -+ -+ private final Reference2ObjectOpenHashMap>> fullStatusWaiters = new Reference2ObjectOpenHashMap<>(); -+ -+ void addFullStatusConsumer(final FullChunkStatus status, final Consumer consumer) { -+ this.fullStatusWaiters.computeIfAbsent(status, (final FullChunkStatus keyInMap) -> { -+ return new ArrayList<>(4); -+ }).add(consumer); -+ } -+ -+ private void completeFullStatusConsumers(FullChunkStatus status, final LevelChunk chunk) { -+ final List> 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 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 scheduleList, final List 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> iterator = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final Reference2ObjectMap.Entry 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 needsScheduling = null; -+ boolean recalculatePriority = false; -+ for (final Iterator> iterator -+ = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final Reference2ObjectMap.Entry 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 needsScheduling, final List 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 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 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 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6b468c621b74449a6218391f6477cf63cfc98c7c ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java -new file mode 100644 -index 0000000000000000000000000000000000000000..310a8f80debadd64c2d962ebf83b7d0505ce6e42 ---- /dev/null -+++ b/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
    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 scheduledTasks, -+ final List 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 scheduledTasks, final List 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 scheduledTasks, final List 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 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 scheduledTasks, final List 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 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 { -+ 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 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> 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> 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 scheduledTasks, List 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 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 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 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5f4b99d8c5453f8ad2e600a57ea4e7dafa2d45f8 ---- /dev/null -+++ b/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 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 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 treeFinished() { -+ this.canQueueTasks = true; -+ for (int priority = 0; priority < this.queues.length; ++priority) { -+ final DependencyTree queue = this.queues[priority]; -+ if (queue.hasWaitingTasks()) { -+ final List 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 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 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 awaiting = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); -+ -+ private final PriorityQueue infiniteRadius = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); -+ private boolean isInfiniteRadiusScheduled; -+ -+ private final Long2ReferenceOpenHashMap 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 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 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 tryPushTasks() { -+ // tasks are not queued, but only created here - we do hold the lock for the map -+ List 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 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 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 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 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 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6ab353b0d2465c3680bb3c8d0852ba0f65c00fd2 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4538ccfaea83d217ed85eaf16e82393c7f286489 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1440c9e2b106616884edcb20201113320817ed9f ---- /dev/null -+++ b/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 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 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 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> 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 extends GenericDataLoadTask { -+ -+ private TaskResult result; -+ private final MultiThreadedQueue>> 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> 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 result) { -+ if ((boolean)COMPLETED_HANDLE.getAndSet((CallbackDataLoadTask)this, (boolean)true)) { -+ throw new IllegalStateException("Already completed"); -+ } -+ this.result = result; -+ Consumer> 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 { -+ 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 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 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 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 { -+ -+ 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 completeOnMainOffMain(final PoiChunk data, final Throwable throwable) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ protected TaskResult 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 runOnMain(final PoiChunk data, final Throwable throwable) { -+ throw new UnsupportedOperationException(); -+ } -+ } -+ -+ public static final class EntityDataLoadTask extends CallbackDataLoadTask { -+ -+ 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 completeOnMainOffMain(final CompoundTag data, final Throwable throwable) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ protected TaskResult 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 runOnMain(final CompoundTag data, final Throwable throwable) { -+ throw new UnsupportedOperationException(); -+ } -+ } -+} -diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java -new file mode 100644 -index 0000000000000000000000000000000000000000..002ee365aa70d8e6a6e6bd5c95988bd17db4395a ---- /dev/null -+++ b/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> 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java -new file mode 100644 -index 0000000000000000000000000000000000000000..25d8da4773dcee5096053e7e3788bfc224d705a7 ---- /dev/null -+++ b/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 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java -new file mode 100644 -index 0000000000000000000000000000000000000000..bdcd1879457bafcca4e76523aac0555968f37c0b ---- /dev/null -+++ b/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 { -+ -+ 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 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 runOffMain(final CompoundTag data, final Throwable throwable); -+ -+ protected abstract TaskResult runOnMain(final OnMain data, final Throwable throwable); -+ -+ protected abstract void onComplete(final TaskResult result); -+ -+ protected abstract TaskResult 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 { -+ -+ 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 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 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)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 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 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 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 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 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 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 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/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java b/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..cb6af3712bf9f6f6b8f7a459c309c75dabe83a50 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java b/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ea759ce6f10f2a5a4e107ab7528030fe931ba223 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..51c126735ace8fdde89ad97b5cab62f244212db0 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java -new file mode 100644 -index 0000000000000000000000000000000000000000..129a35ff2db5b3bb6736810fc180796ce55e1875 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3bd1b59250dbab15097a64d515999b278636795a ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java b/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java -new file mode 100644 -index 0000000000000000000000000000000000000000..786e6ad17cd6216ef0aadaa7cf10044a0c19c933 ---- /dev/null -+++ b/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java -@@ -0,0 +1,9 @@ -+package ca.spottedleaf.moonrise.patches.chunk_system.ticket; -+ -+public interface ChunkSystemTicket { -+ -+ public long moonrise$getRemoveDelay(); -+ -+ public void moonrise$setRemoveDelay(final long removeDelay); -+ -+} -diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java b/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2add7fd15a2210286aeb9af5024263333340d34c ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java b/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ce3bb903c9ccb7efa0f004cf79b291dcb1cb7a23 ---- /dev/null -+++ b/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 { -+ -+ public SortedArraySet moonrise$copy(); -+ -+ public Object[] moonrise$copyBackingArray(); -+ -+ public T moonrise$replace(final T object); -+ -+ public T moonrise$removeAndGet(final T object); -+ -+} -diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java b/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java -new file mode 100644 -index 0000000000000000000000000000000000000000..93fd23027c00cef76562098306737272fda1350a ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java b/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7ef3dcca89ed7578c6c0f5565131889110063056 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ea6b6ed27b212719feb31610faac974899688839 ---- /dev/null -+++ b/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 moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate predicate); -+ -+} -diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4b9e2fa963c14f65f15407c1814c543c2999ea32 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e97e7d276faf055c89207385d3820debffb06463 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f28fd0e01e2bdda0daf9d775e514a7253d32d8d0 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6af03fd7807d4c71dbf85028d18dc850978ef429 ---- /dev/null -+++ b/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 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/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e04bd54744335fb5398c6e4f7ce8b981f35bfb7d ---- /dev/null -+++ b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java -@@ -0,0 +1,2183 @@ -+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 offset, 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] + offset); -+ -+ 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: 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, off_x, aabb.minX + 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, off_x, aabb.maxX - 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, off_y, aabb.minY + 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, off_y, aabb.maxY - 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, off_z, aabb.minZ + 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, off_z, aabb.maxZ - 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: 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, off_y, source.minY + 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, off_y, source.maxY - 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, off_z, source.minZ + 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, off_z, source.maxZ - 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; -+ final int ceil_max_x = findFloor( -+ coords_x, off_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] + off_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; -+ final int floor_min_x = findFloor( -+ coords_x, off_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] + off_x) - 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: 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, off_x, source.minX + 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, off_x, source.maxX - 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, off_z, source.minZ + 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, off_z, source.maxZ - 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; -+ final int ceil_max_y = findFloor( -+ coords_y, off_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] + off_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; -+ final int floor_min_y = findFloor( -+ coords_y, off_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] + off_y) - 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: 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, off_x, source.minX + 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, off_x, source.maxX - 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, off_y, source.minY + 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, off_y, source.maxY - 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; -+ final int ceil_max_z = findFloor( -+ coords_z, off_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] + off_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; -+ final int floor_min_z = findFloor( -+ coords_z, off_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] + off_z) - 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, final double x, final double y, final 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; -+ } -+ -+ 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: 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, off_x, x, 0, size_x); -+ if (index_x < 0 || index_x >= size_x) { -+ return false; -+ } -+ -+ final int index_y = findFloor(coords_y, off_y, y, 0, size_y); -+ if (index_y < 0 || index_y >= size_y) { -+ return false; -+ } -+ -+ final int index_z = findFloor(coords_z, off_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 potentialCollisions) { -+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { -+ final AABB target = potentialCollisions.get(i); -+ value = collideX(target, currentBoundingBox, value); -+ } -+ -+ return value; -+ } -+ -+ public static double performAABBCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) { -+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { -+ final AABB target = potentialCollisions.get(i); -+ value = collideY(target, currentBoundingBox, value); -+ } -+ -+ return value; -+ } -+ -+ public static double performAABBCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) { -+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { -+ final AABB target = potentialCollisions.get(i); -+ value = collideZ(target, currentBoundingBox, value); -+ } -+ -+ return value; -+ } -+ -+ public static double performVoxelCollisionsX(final AABB currentBoundingBox, double value, final List potentialCollisions) { -+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { -+ final VoxelShape target = potentialCollisions.get(i); -+ value = collideX(target, currentBoundingBox, value); -+ } -+ -+ return value; -+ } -+ -+ public static double performVoxelCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) { -+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { -+ final VoxelShape target = potentialCollisions.get(i); -+ value = collideY(target, currentBoundingBox, value); -+ } -+ -+ return value; -+ } -+ -+ public static double performVoxelCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) { -+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { -+ final VoxelShape target = potentialCollisions.get(i); -+ value = collideZ(target, currentBoundingBox, value); -+ } -+ -+ return value; -+ } -+ -+ public static Vec3 performVoxelCollisions(final Vec3 moveVector, AABB axisalignedbb, final List 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 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 voxels, -+ final List 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 intoVoxel, final List intoAABB, -+ final int collisionFlags, final BiPredicate 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 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 into, final int collisionFlags, final Predicate 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 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 intoVoxel, final List intoAABB, final int collisionFlags, -+ final BiPredicate blockPredicate, -+ final Predicate 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 = super.getEntity(); -+ return this.delegate == null ? this.delegate = (entity == null ? CollisionContext.empty() : CollisionContext.of(entity)) : this.delegate; -+ } -+ -+ @Override -+ public Entity getEntity() { -+ this.getDelegate(); -+ return super.getEntity(); -+ } -+ -+ @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/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java b/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java -new file mode 100644 -index 0000000000000000000000000000000000000000..35c8aaf0bfa42717f45eed1d1072e1614874de91 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java b/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a38ab583200ebf68ca68fdddf2d12077720b72b7 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5a6b16be4b8c0cc92d017bc592bc4818dba17da7 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9d33ead3a97d86b371e4d9ad9fed80d789bed844 ---- /dev/null -+++ b/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 aabbs, -+ boolean isOffset, -+ double offX, double offY, double offZ -+) { -+ -+ public CachedToAABBs removeOffset() { -+ final List toOffset = this.aabbs; -+ final double offX = this.offX; -+ final double offY = this.offY; -+ final double offZ = this.offZ; -+ -+ final List 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/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java -new file mode 100644 -index 0000000000000000000000000000000000000000..07fe5e02c2d0a27d2fe37bb45761654dc2d02e5d ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java -new file mode 100644 -index 0000000000000000000000000000000000000000..05d7b3f9d8659c259f3ed0537c57e6e43eb6e288 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java b/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java -new file mode 100644 -index 0000000000000000000000000000000000000000..44831fc18efb7534dc6e4822f3c9b5cdc4dcc33e ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java b/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f62359e5d6aa9a9cdb015441dbdb6182dc302f02 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java b/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java -new file mode 100644 -index 0000000000000000000000000000000000000000..cf9ffdeff6bf0b62a45f7a44dbfe0dd7d17dc4f4 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5f5734c00ce8245a1ff69b2d4c3036579d5392e0 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8e7472157a98de607c03769a91f64c8369fd3ea6 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java b/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4a7abd239a9c59aa98947e7993962d75e9051902 ---- /dev/null -+++ b/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java -@@ -0,0 +1,9 @@ -+package ca.spottedleaf.moonrise.patches.fast_palette; -+ -+public interface FastPalette { -+ -+ public default T[] moonrise$getRawPalette(final FastPaletteData src) { -+ return null; -+ } -+ -+} -diff --git a/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java b/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4503f3495846a7d7ed082b9e24636044e4fbccd1 ---- /dev/null -+++ b/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java -@@ -0,0 +1,9 @@ -+package ca.spottedleaf.moonrise.patches.fast_palette; -+ -+public interface FastPaletteData { -+ -+ public T[] moonrise$getPalette(); -+ -+ public void moonrise$setPalette(final T[] palette); -+ -+} -diff --git a/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java b/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java -new file mode 100644 -index 0000000000000000000000000000000000000000..107c97089354edd35f330582f5e0c8a18e792a6e ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java b/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java -new file mode 100644 -index 0000000000000000000000000000000000000000..540c14a6d2c216cd3ef2a9c4056e15712bf8cb8c ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java b/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8e6d79b7c10ef25f5478b72c53c555423d615a2f ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java b/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ed80017c8f257b981d626a37ffc5480d9b326558 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java b/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java -new file mode 100644 -index 0000000000000000000000000000000000000000..fa7b784a89626e8528c249d7889a598bd7ee3d49 ---- /dev/null -+++ b/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 positions) { -+ for (final BlockPos pos : positions) { -+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); -+ } -+ -+ this.performLightDecrease(lightAccess); -+ } -+ -+ protected List getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) { -+ final List 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 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 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/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java b/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4ca68a903e67606fc4ef0bfa9862a73797121c8b ---- /dev/null -+++ b/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> 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/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java b/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f9aef289e9a2d6f63c98c72c56ef32b8793f57f4 ---- /dev/null -+++ b/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 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/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8aeb5fb87f94a35659347a09a638420699b52a6f ---- /dev/null -+++ b/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 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 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 chunks, -+ final Consumer 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 nibblesByChunk = new Long2ObjectOpenHashMap<>(); -+ final Long2ObjectOpenHashMap 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 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/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java -new file mode 100644 -index 0000000000000000000000000000000000000000..571db5f9bf94745a8afe2cd313e593fb15db5e37 ---- /dev/null -+++ b/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 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 cachedSkyPropagators; -+ private final ArrayDeque 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 chunks, final Consumer 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 onComplete = new MultiThreadedQueue<>(); -+ protected final Set changedPositions = new HashSet<>(); -+ protected Boolean[] changedSectionSet; -+ protected ShortOpenHashSet queuedEdgeChecksSky; -+ protected ShortOpenHashSet queuedEdgeChecksBlock; -+ protected List 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 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 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 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/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7fe59ab70557aa6a484a02db2b2007fdd9e4bbb8 ---- /dev/null -+++ b/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 chunks, -+ final Consumer chunkLightCallback, -+ final IntConsumer onComplete) throws UnsupportedOperationException { -+ throw new UnsupportedOperationException(); -+ } -+ -+} -diff --git a/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java b/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java -new file mode 100644 -index 0000000000000000000000000000000000000000..40d004afdc6449530f5bb2d7c7638b8ee3e3a577 ---- /dev/null -+++ b/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/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java b/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..689ce367164e79e0426eeecb81dbbc521d4bc742 ---- /dev/null -+++ b/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/io/papermc/paper/FeatureHooks.java b/io/papermc/paper/FeatureHooks.java -index a6779295bff446ee79e7c9d41e405447becc2966..efc7f4071655201c59c912e9c84e35a8da66e34c 100644 ---- a/io/papermc/paper/FeatureHooks.java -+++ b/io/papermc/paper/FeatureHooks.java -@@ -1,6 +1,8 @@ - package io.papermc.paper; - - import io.papermc.paper.command.PaperSubcommand; -+import io.papermc.paper.command.subcommands.ChunkDebugCommand; -+import io.papermc.paper.command.subcommands.FixLightCommand; - import it.unimi.dsi.fastutil.longs.LongOpenHashSet; - import it.unimi.dsi.fastutil.longs.LongSet; - import it.unimi.dsi.fastutil.longs.LongSets; -@@ -29,9 +31,12 @@ import org.bukkit.World; - public final class FeatureHooks { - - public static void initChunkTaskScheduler(final boolean useParallelGen) { -+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.init(useParallelGen); // Paper - Chunk system - } - - public static void registerPaperCommands(final Map, PaperSubcommand> commands) { -+ commands.put(Set.of("fixlight"), new FixLightCommand()); // Paper - rewrite chunk system -+ commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand()); // Paper - rewrite chunk system - } - - public static LevelChunkSection createSection(final Registry biomeRegistry, final Level level, final ChunkPos chunkPos, final int chunkSection) { -diff --git a/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/io/papermc/paper/command/subcommands/ChunkDebugCommand.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd01fba0fc ---- /dev/null -+++ b/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 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 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 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 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 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/io/papermc/paper/command/subcommands/FixLightCommand.java b/io/papermc/paper/command/subcommands/FixLightCommand.java -new file mode 100644 -index 0000000000000000000000000000000000000000..85950a1aa732ab8c01ad28bec9e0de140e1a172e ---- /dev/null -+++ b/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 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 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 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/io/papermc/paper/threadedregions/TickRegions.java b/io/papermc/paper/threadedregions/TickRegions.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8424cf9d4617b4732d44cc460d25b04481068989 ---- /dev/null -+++ b/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/net/minecraft/core/Direction.java b/net/minecraft/core/Direction.java -index 690e1d2394e68356c56a39ac083cc53ee0388d71..928f38fd6beb00753c92ae9f4678f7507519a39b 100644 ---- a/net/minecraft/core/Direction.java -+++ b/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/net/minecraft/core/MappedRegistry.java b/net/minecraft/core/MappedRegistry.java -index 063630c1ffcce099139c59d598fc5a210e21f640..a61153c5d99bdc26f37a10f33baf839e943e17e1 100644 ---- a/net/minecraft/core/MappedRegistry.java -+++ b/net/minecraft/core/MappedRegistry.java -@@ -50,6 +50,19 @@ public class MappedRegistry implements WritableRegistry { - 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> key, Lifecycle lifecycle) { - this(key, lifecycle, false); - } -@@ -114,6 +127,7 @@ public class MappedRegistry implements WritableRegistry { - 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/net/minecraft/server/Main.java b/net/minecraft/server/Main.java -index 731bdabd53fd4a3d17494f26781223097a5d6e16..42d46c7a7437bea5335a23cbee5708ac57131474 100644 ---- a/net/minecraft/server/Main.java -+++ b/net/minecraft/server/Main.java -@@ -322,6 +322,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/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index 807d05097f7313361eadb600187421d25e294413..5e7ba47247fc9b6bc8da86d8f67c6cd923cd0b1e 100644 ---- a/net/minecraft/server/MinecraftServer.java -+++ b/net/minecraft/server/MinecraftServer.java -@@ -204,7 +204,7 @@ import org.bukkit.event.server.ServerLoadEvent; - // CraftBukkit end - - --public abstract class MinecraftServer extends ReentrantBlockableEventLoop implements ServerInfo, ChunkIOErrorReporter, CommandSource { -+public abstract class MinecraftServer extends ReentrantBlockableEventLoop 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(); -@@ -333,7 +333,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { - ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system - AtomicReference 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"); - -@@ -352,6 +352,77 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= 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 -@@ -672,7 +743,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { -+ 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; -@@ -1015,19 +1091,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { - 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; -@@ -1432,6 +1511,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { -+ 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/net/minecraft/server/level/ChunkHolder.java b/net/minecraft/server/level/ChunkHolder.java -index b9ab241b930edc63a39dbbcf14cd0b5edacb9ea9..8dd9375f2ad2c65a773a3195aeff1f977e09e7e0 100644 ---- a/net/minecraft/server/level/ChunkHolder.java -+++ b/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 UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk"); - private static final CompletableFuture> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK); - private final LevelHeightAccessor levelHeightAccessor; -- private volatile CompletableFuture> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage -- private volatile CompletableFuture> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage -- private volatile CompletableFuture> 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 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 moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge) { -+ final List 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> getTickingChunkFuture() { -- return this.tickingChunkFuture; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public CompletableFuture> getEntityTickingChunkFuture() { -- return this.entityTickingChunkFuture; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public CompletableFuture> 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> chunkFuture, Executor executor, FullChunkStatus target) { -- this.pendingFullStateConfirmation.cancel(false); -- CompletableFuture 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/net/minecraft/server/level/ChunkLevel.java b/net/minecraft/server/level/ChunkLevel.java -index 11b30b6daa1d049634350e34502c701e9800add4..fae17a075d7efaf24d916877dd5968eb9652bb66 100644 ---- a/net/minecraft/server/level/ChunkLevel.java -+++ b/net/minecraft/server/level/ChunkLevel.java -@@ -7,8 +7,8 @@ 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; -+ 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(); -diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java -index e9b585387f6cbc454e7b16feb36a256e733c5488..67cfc3236a39008cfcf3acffefafda1a604b8573 100644 ---- a/net/minecraft/server/level/ChunkMap.java -+++ b/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> UNLOADED_CHUNK_LIST_RESULT = ChunkResult.error("Unloaded chunks found in range"); - private static final CompletableFuture>> 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 updatingChunkMap = new Long2ObjectLinkedOpenHashMap(); -- public volatile Long2ObjectLinkedOpenHashMap visibleChunkMap; -- private final Long2ObjectLinkedOpenHashMap pendingUnloads; -- private final List pendingGenerationTasks; -+ // Paper - rewrite chunk system - public final ServerLevel level; - private final ThreadedLevelLightEngine lightEngine; - private final BlockableEventLoop 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 entityMap; - private final Long2ByteMap chunkTypeCache; -- private final Long2LongMap nextChunkSaveTime; -- private final LongSet chunksToEagerlySave; -- private final Queue 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 mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier 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>> getChunkRangeFuture(ChunkHolder centerChunk, int margin, IntFunction 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>> 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 list2 = new ArrayList(list1.size()); -- Iterator iterator = list1.iterator(); -- -- while (iterator.hasNext()) { -- ChunkResult 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> 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 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 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 scheduleChunkLoad(ChunkPos pos) { -- CompletableFuture> 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 applyStep(GenerationChunkHolder chunkHolder, ChunkStep step, StaticCache2D 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 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> prepareTickingChunk(ChunkHolder holder) { -- CompletableFuture>> completablefuture = this.getChunkRangeFuture(holder, 1, (i) -> { -- return ChunkStatus.FULL; -- }); -- CompletableFuture> 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> 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 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> read(final ChunkPos pos) { -+ final CompletableFuture> 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 write(final ChunkPos pos, final Supplier 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 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 getPlayersCloseForSpawning(ChunkPos pos) { -- long i = pos.toLong(); -+ // Paper start - chunk tick iteration optimisation -+ final ca.spottedleaf.moonrise.common.list.ReferenceList 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 builder = ImmutableList.builder(); -- Iterator iterator = this.playerMap.getAllPlayers().iterator(); -+ List 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 getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge) { -- Set set = this.playerMap.getAllPlayers(); -- Builder 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 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 list = Lists.newArrayList(); - List 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 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 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.of()) { -+ return this.scaledRange(range); -+ } - -- if (j > i) { -- i = j; -- } -+ // note: we change to List -+ final List passengers = (List)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 players) { -diff --git a/net/minecraft/server/level/DistanceManager.java b/net/minecraft/server/level/DistanceManager.java -index f7c2c03749d6be25bf33afd61e1da120770b3432..746f61661e22d22f2acbbe54a5933e57fbca45b2 100644 ---- a/net/minecraft/server/level/DistanceManager.java -+++ b/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> playersPerChunk = new Long2ObjectOpenHashMap(); -- public final Long2ObjectOpenHashMap>> 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 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 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>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); -- -- while (objectiterator.hasNext()) { -- Entry>> entry = (Entry) objectiterator.next(); -- Iterator> 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 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> 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> 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> 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 void addTicket(TicketType type, ChunkPos pos, int level, T argument) { -@@ -219,13 +128,7 @@ public abstract class DistanceManager { - } - - public boolean addRegionTicketAtDistance(TicketType tickettype, ChunkPos chunkcoordintpair, int i, T t0) { -- // CraftBukkit end -- Ticket 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 void removeRegionTicket(TicketType type, ChunkPos pos, int radius, T argument) { -@@ -234,32 +137,21 @@ public abstract class DistanceManager { - } - - public boolean removeRegionTicketAtDistance(TicketType tickettype, ChunkPos chunkcoordintpair, int i, T t0) { -- // CraftBukkit end -- Ticket 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> 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 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,93 @@ 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> 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()); -- } -+ // Paper start - rewrite chunk system -+ // note: vanilla does not clamp to 0, but we do simply because we need a min of 0 -+ final int clamped = net.minecraft.util.Mth.clamp(simulationDistance, 0, ca.spottedleaf.moonrise.common.util.MoonriseConstants.MAX_VIEW_DISTANCE); - -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getPlayerChunkLoader().setTickDistance(clamped); -+ // Paper end - 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>> 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> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve -- ObjectIterator>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); -- -- while (objectiterator.hasNext()) { -- Entry>> entry = (Entry) objectiterator.next(); -- Iterator> 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 void removeAllTicketsFor(TicketType ticketType, int ticketLevel, T ticketIdentifier) { -- Ticket target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier); -- -- for (java.util.Iterator>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -- Entry>> entry = iterator.next(); -- SortedArraySet> 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 +307,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 +387,7 @@ public abstract class DistanceManager { - } - } - -+ /* // Paper - rewrite chunk system - private class PlayerTicketTracker extends DistanceManager.FixedPlayerDistanceChunkTracker { - - private int viewDistance = 0; -@@ -657,5 +482,5 @@ public abstract class DistanceManager { - private boolean haveTicketFor(int distance) { - return distance <= this.viewDistance; - } -- } -+ }*/ // Paper - rewrite chunk system - } -diff --git a/net/minecraft/server/level/GenerationChunkHolder.java b/net/minecraft/server/level/GenerationChunkHolder.java -index 65206fdfa5b94eaca139e433b4865c16b16641f3..bf4463bcb5dc439ac5a3fa08dd60845a5fd7489a 100644 ---- a/net/minecraft/server/level/GenerationChunkHolder.java -+++ b/net/minecraft/server/level/GenerationChunkHolder.java -@@ -27,13 +27,7 @@ public abstract class GenerationChunkHolder { - public static final ChunkResult UNLOADED_CHUNK = ChunkResult.error("Unloaded chunk"); - public static final CompletableFuture> UNLOADED_CHUNK_FUTURE = CompletableFuture.completedFuture(UNLOADED_CHUNK); - protected final ChunkPos pos; -- @Nullable -- private volatile ChunkStatus highestAllowedStatus; -- private final AtomicReference startedWork = new AtomicReference<>(); -- private final AtomicReferenceArray>> futures = new AtomicReferenceArray<>(CHUNK_STATUSES.size()); -- private final AtomicReference task = new AtomicReference<>(); -- private final AtomicInteger generationRefCount = new AtomicInteger(); -- private volatile CompletableFuture 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> scheduleChunkGenerationTask(ChunkStatus requestedStatus, ChunkMap chunkLoadingManager) { -- if (this.isStatusDisallowed(requestedStatus)) { -- return UNLOADED_CHUNK_FUTURE; -- } else { -- CompletableFuture> 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> applyStep(ChunkStep step, GeneratingChunkMap chunkLoadingManager, StaticCache2D 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> completableFuture = CompletableFuture.completedFuture(ChunkResult.of(chunk)); -- -- for (int i = 0; i < this.futures.length() - 1; i++) { -- CompletableFuture> 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> getOrCreateFuture(ChunkStatus status) { -- if (this.isStatusDisallowed(status)) { -- return UNLOADED_CHUNK_FUTURE; -- } else { -- int i = status.getIndex(); -- CompletableFuture> completableFuture = this.futures.get(i); -- -- while (completableFuture == null) { -- CompletableFuture> 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> 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> 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 chunkResult = ChunkResult.of(chunk); -- int i = status.getIndex(); -- -- while (true) { -- CompletableFuture> 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 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> 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> 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>>> getAllFutures() { -- List>>> 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/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java -index 6a2af3cd3aebe525a5ff41a801929547d59b8fec..d7382fc1498a33db909c343d8d07c5aa7130c20f 100644 ---- a/net/minecraft/server/level/ServerChunkCache.java -+++ b/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,107 @@ 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 completable = new CompletableFuture<>(); -+ chunkTaskScheduler.scheduleChunkLoad( -+ chunkX, chunkZ, toStatus, true, ca.spottedleaf.concurrentutil.util.Priority.BLOCKING, -+ completable::complete -+ ); -+ -+ if (!completable.isDone() && chunkTaskScheduler.hasShutdown()) { -+ throw new IllegalStateException( -+ "Chunk system has shut down, cannot process chunk requests in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.level) + "' at " -+ + "(" + chunkX + "," + chunkZ + ") status: " + toStatus -+ ); -+ } -+ -+ 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 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 persistentStateManagerFactory) { - this.level = world; -@@ -112,13 +213,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 +284,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(); -+ // Paper start - rewrite chunk system -+ if (leastStatus == ChunkStatus.FULL) { -+ final LevelChunk ret = this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x, z)); - -- gameprofilerfiller.incrementCounter("getChunk"); -- long k = ChunkPos.asLong(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> 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 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 +350,59 @@ public class ServerChunkCache extends ChunkSource { - } - - private CompletableFuture> 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> ret = new CompletableFuture<>(); -+ final Consumer 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 +415,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 +437,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 +470,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 +497,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 +519,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 +531,26 @@ public class ServerChunkCache extends ChunkSource { - } - - private void collectTickingChunks(List chunks) { -- this.chunkMap.forEachSpawnCandidateChunk((playerchunk) -> { -- LevelChunk chunk = playerchunk.getTickingChunk(); -+ // Paper start - chunk tick iteration optimisation -+ final ca.spottedleaf.moonrise.common.list.ReferenceList 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 chunks) { -@@ -508,7 +591,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 +604,13 @@ public class ServerChunkCache extends ChunkSource { - } - - private void getFullChunk(long pos, Consumer 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 +704,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 +801,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/net/minecraft/server/level/ServerEntity.java b/net/minecraft/server/level/ServerEntity.java -index d5bc702f2676b1b7a32c8f3a4a349fc2710ee825..301e8d6599d200cb0f1328f0e386af2f9a619939 100644 ---- a/net/minecraft/server/level/ServerEntity.java -+++ b/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 list = this.entity.getPassengers(); - - if (!list.equals(this.lastPassengers)) { -diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index 6c71ef3c7430623900a7021f853d2bb514273e4d..cf692267c6376ed8484478dc90f4f905d8325618 100644 ---- a/net/minecraft/server/level/ServerLevel.java -+++ b/net/minecraft/server/level/ServerLevel.java -@@ -186,7 +186,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); -@@ -202,7 +202,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 entityManager; -+ // Paper - rewrite chunk system - private final GameEventDispatcher gameEventDispatcher; - public boolean noSave; - private final SleepStatus sleepStatus; -@@ -273,12 +273,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> 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; - -@@ -297,32 +292,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> onLoad) { -- List 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 loadedChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); -+ private final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); -+ private final ca.spottedleaf.moonrise.common.list.ReferenceList 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 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> 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> 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> 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> 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 ret = new ArrayList<>(requiredChunks); -+ -+ final java.util.function.Consumer 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); - } - } - } -@@ -330,22 +452,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 moonrise$getLoadedChunks() { -+ return this.loadedChunks; -+ } -+ -+ @Override -+ public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getTickingChunks() { -+ return this.tickingChunks; -+ } -+ -+ @Override -+ public final ca.spottedleaf.moonrise.common.list.ReferenceList 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 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 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 resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { -@@ -379,14 +616,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - DataFixer datafixer = minecraftserver.getFixerUpper(); - EntityPersistentStorage 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(); -@@ -414,6 +650,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 - } - -@@ -536,7 +786,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) { -@@ -559,13 +809,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() { -@@ -605,7 +858,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 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(); -@@ -613,7 +919,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)) { -@@ -645,7 +951,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)); - } - } -@@ -653,35 +959,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(); -@@ -954,6 +1232,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 - - } - -@@ -963,6 +1246,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 - - } - -@@ -1041,6 +1329,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) { -@@ -1054,14 +1347,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; -@@ -1201,7 +1499,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 -@@ -1232,7 +1530,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 - } - } - -@@ -1243,11 +1541,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - public boolean tryAddFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) { - // CraftBukkit end -- Stream 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 -@@ -1924,7 +2218,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())); -@@ -1973,7 +2267,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 { -@@ -1994,7 +2288,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 { -@@ -2136,7 +2430,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()); - } -@@ -2166,15 +2460,25 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - @Override - public LevelEntityGetter 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 entities) { -- this.entityManager.addLegacyChunkEntities(entities); -+ // Paper start - add chunkpos param -+ this.addLegacyChunkEntities(entities, null); -+ } -+ public void addLegacyChunkEntities(Stream entities, ChunkPos chunkPos) { -+ // Paper end - add chunkpos param -+ this.moonrise$getEntityLookup().addLegacyChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system - } - - public void addWorldGenChunkEntities(Stream entities) { -- this.entityManager.addWorldGenChunkEntities(entities); -+ // Paper start - add chunkpos param -+ this.addWorldGenChunkEntities(entities, null); -+ } -+ public void addWorldGenChunkEntities(Stream entities, ChunkPos chunkPos) { -+ // Paper end - add chunkpos param -+ this.moonrise$getEntityLookup().addWorldGenChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system - } - - public void startTickingChunk(LevelChunk chunk) { -@@ -2194,34 +2498,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 -@@ -2277,7 +2594,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/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java -index f6e3073e1f1ff99f6917d84974a18e3e756fa9ea..ba873bcc183f9b3f64ba39be08cb88a95ff52b0e 100644 ---- a/net/minecraft/server/level/ServerPlayer.java -+++ b/net/minecraft/server/level/ServerPlayer.java -@@ -217,7 +217,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; -@@ -322,6 +322,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/net/minecraft/server/level/ThreadedLevelLightEngine.java b/net/minecraft/server/level/ThreadedLevelLightEngine.java -index 39d34f3728ae8d845d1bffc09f3ab8b64eb4d48b..3e82adf061bd0ec0100ca4d16ec9b157bddf99a7 100644 ---- a/net/minecraft/server/level/ThreadedLevelLightEngine.java -+++ b/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> 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 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 chunks0, -+ final java.util.function.Consumer chunkLightCallback, -+ final java.util.function.IntConsumer onComplete) { -+ final java.util.Set chunks = new java.util.LinkedHashSet<>(chunks0); -+ final java.util.Map ticketIds = new java.util.HashMap<>(); -+ final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld(); -+ -+ for (final java.util.Iterator 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 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 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 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 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> objectListIterator = this.lightTasks.iterator(); -- -- int j; -- for (j = 0; objectListIterator.hasNext() && j < i; j++) { -- Pair 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 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/net/minecraft/server/level/Ticket.java b/net/minecraft/server/level/Ticket.java -index eba83b085435150e5954fd5d41dda9ce1d0601ad..daf543b51d8875b374688957ae4bc466f5512bcd 100644 ---- a/net/minecraft/server/level/Ticket.java -+++ b/net/minecraft/server/level/Ticket.java -@@ -2,13 +2,25 @@ package net.minecraft.server.level; - - import java.util.Objects; - --public final class Ticket implements Comparable> { -+public final class Ticket implements Comparable>, ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket { // Paper - rewrite chunk system - private final TicketType type; - private final int ticketLevel; - public final T key; -- private long createdTick; -+ // Paper start - rewrite chunk system -+ private long removeDelay; - -- protected Ticket(TicketType 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 type, int level, T argument) { // Paper - public - this.type = type; - this.ticketLevel = level; - this.key = argument; -@@ -41,7 +53,7 @@ public final class Ticket implements Comparable> { - - @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 getType() { -@@ -53,11 +65,10 @@ public final class Ticket implements Comparable> { - } - - 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/net/minecraft/server/level/WorldGenRegion.java b/net/minecraft/server/level/WorldGenRegion.java -index b7d29389a357f142237cecd75f8ca91cf1eb6b5b..e4b0dc3121101d54394a0c3a413dabf8103b2ea6 100644 ---- a/net/minecraft/server/level/WorldGenRegion.java -+++ b/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 chunks, ChunkStep generationStep, ChunkAccess centerPos) { - this.generatingStep = generationStep; - this.cache = chunks; -diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java -index c68040a59fa8aa9b8b9f1e0b4fdded565ea592d9..7913c41aac1f9dd53a2b49da2a17fd894bcb6b3a 100644 ---- a/net/minecraft/server/players/PlayerList.java -+++ b/net/minecraft/server/players/PlayerList.java -@@ -1426,7 +1426,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()) { -@@ -1441,7 +1441,7 @@ public abstract class PlayerList { - - public void setSimulationDistance(int simulationDistance) { - this.simulationDistance = simulationDistance; -- this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); -+ //this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); // Paper - rewrite chunk system - Iterator iterator = this.server.getAllLevels().iterator(); - - while (iterator.hasNext()) { -diff --git a/net/minecraft/util/BitStorage.java b/net/minecraft/util/BitStorage.java -index 68648c5a5e3ff079f832092af0f2f801c42d1ede..e4e153cb8899e70273aa150b8ea26907cf68b15c 100644 ---- a/net/minecraft/util/BitStorage.java -+++ b/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 moonrise$countEntries() { -+ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap 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/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java b/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java -index 61dee55417bc802e25b9ba2f271d32d8c12844a9..a8a260a3caaa8e5004069b833ecc8b17b2fc8db5 100644 ---- a/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java -+++ b/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 implements IdMap { -+public class CrudeIncrementalIntIdentityHashBiMap implements IdMap, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // 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 implements IdMap { - private int nextId; - private int size; - -+ // Paper start - optimise palette reads -+ private ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData reference; -+ -+ @Override -+ public final K[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData 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 implements IdMap { - 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 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/net/minecraft/util/SimpleBitStorage.java b/net/minecraft/util/SimpleBitStorage.java -index 9f438d9c6eb05e43d24e4af68188a3d4c46a938c..d99ec470b4653beab630999a5b2c1a6428b20c38 100644 ---- a/net/minecraft/util/SimpleBitStorage.java -+++ b/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 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 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 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/net/minecraft/util/SortedArraySet.java b/net/minecraft/util/SortedArraySet.java -index ea72dcb064a35bc6245bc5c94d592efedd8faf41..87ee8e51dfa7657ed7d83fcbceef48bf857043e1 100644 ---- a/net/minecraft/util/SortedArraySet.java -+++ b/net/minecraft/util/SortedArraySet.java -@@ -8,12 +8,89 @@ import java.util.Iterator; - import java.util.NoSuchElementException; - import javax.annotation.Nullable; - --public class SortedArraySet extends AbstractSet { -+public class SortedArraySet extends AbstractSet implements ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet { // Paper - rewrite chunk system - private static final int DEFAULT_INITIAL_CAPACITY = 10; - private final Comparator comparator; - T[] contents; - int size; - -+ // Paper start - rewrite chunk system -+ @Override -+ public final boolean removeIf(final java.util.function.Predicate 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 moonrise$copy() { -+ final SortedArraySet 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 comparator) { - this.comparator = comparator; - if (initialCapacity < 0) { -diff --git a/net/minecraft/util/ZeroBitStorage.java b/net/minecraft/util/ZeroBitStorage.java -index 50040c497a819cd1229042ab3cb057d34a32cacc..1f9c436a632e4f110be61cf76fcfc3b7eb80334e 100644 ---- a/net/minecraft/util/ZeroBitStorage.java -+++ b/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 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 ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1); -+ ret.put(0, coordinates); -+ return ret; -+ } -+ // Paper end - block counting - } -diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java -index 766031d1482b0f49b196326b820d5ce9ae1c7c06..1f54752a4ea0788e73279cd99c7c35e3b5d9b6ce 100644 ---- a/net/minecraft/world/entity/Entity.java -+++ b/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 -@@ -462,6 +418,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 voxels, final List 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 into, final List 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(); -@@ -1387,41 +1493,76 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - - private Vec3 collide(Vec3 movement) { -- AABB axisalignedbb = this.getBoundingBox(); -- List 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 potentialCollisionsVoxel = new ArrayList<>(); -+ final List potentialCollisionsBB = new ArrayList<>(); - -- List 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 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 stepVoxels = new ArrayList<>(); -+ final List 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 collisions, float f, float stepHeight) { -@@ -2821,18 +2962,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 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) { -@@ -4310,14 +4543,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - - public Iterable getIndirectPassengers() { -- // Paper start - Optimize indirect passenger iteration -- if (this.passengers.isEmpty()) { return ImmutableList.of(); } -- ImmutableList.Builder indirectPassengers = ImmutableList.builder(); -- for (Entity passenger : this.passengers) { -- indirectPassengers.add(passenger); -- indirectPassengers.addAll(passenger.getIndirectPassengers()); -+ // Paper start - optimise entity tracker -+ final List 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 getIndirectPassengers_old() { - // Paper end - Optimize indirect passenger iteration -@@ -4475,82 +4711,136 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - return Mth.lerp(delta, this.yRotO, this.yRot); - } - -- public boolean updateFluidHeightAndDoFluidPushing(TagKey tag, double speed) { -+ // Paper start - optimise collisions -+ public boolean updateFluidHeightAndDoFluidPushing(final TagKey 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 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); -@@ -4702,6 +4992,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; - } -@@ -4831,6 +5130,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 -@@ -4842,7 +5147,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 -@@ -4874,7 +5179,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/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/net/minecraft/world/entity/ai/village/poi/PoiManager.java -index 96bc0ba60195e5e666d47b3a0b943b733986d96a..5930a430983061afddf20e3208ff2462ca1b78cd 100644 ---- a/net/minecraft/world/entity/ai/village/poi/PoiManager.java -+++ b/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 { -+public class PoiManager extends SectionStorage 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 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 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 { - world - ); - this.distanceTracker = new PoiManager.DistanceTracker(); -+ this.world = (net.minecraft.server.level.ServerLevel)world; // Paper - rewrite chunk system - } - - public void add(BlockPos pos, Holder type) { -@@ -197,8 +323,10 @@ public class PoiManager extends SectionStorage { - } - - 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 { - - @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 { - .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/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/net/minecraft/world/entity/ai/village/poi/PoiSection.java -index b9e0bc8f1e948614d986335de1f3d2df199eea81..712cbfc100e8aaf612d1d651dae64f57f892a768 100644 ---- a/net/minecraft/world/entity/ai/village/poi/PoiSection.java -+++ b/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 records = new Short2ObjectOpenHashMap<>(); - private final Map, Set> byType = Maps.newHashMap(); - private final Runnable setDirty; - private boolean isValid; - -+ // Paper start - rewrite chunk system -+ private final Optional 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 moonrise$asOptional() { -+ return this.noAllocOptional; -+ } -+ // Paper end - rewrite chunk system -+ - public PoiSection(Runnable updateListener) { - this(updateListener, true, ImmutableList.of()); - } -diff --git a/net/minecraft/world/entity/decoration/ArmorStand.java b/net/minecraft/world/entity/decoration/ArmorStand.java -index 63f02cdc67d9e88cc6998d0ae9d139c83e85b447..70b8023c3badc745f342d5b0ab54699e3923826a 100644 ---- a/net/minecraft/world/entity/decoration/ArmorStand.java -+++ b/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 list = this.level().getEntities((Entity) this, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS); -+ List list = this.level().getEntitiesOfClass(AbstractMinecart.class, this.getBoundingBox(), RIDABLE_MINECARTS); // Paper - optimise collisions - Iterator iterator = list.iterator(); - - while (iterator.hasNext()) { -diff --git a/net/minecraft/world/level/ClipContext.java b/net/minecraft/world/level/ClipContext.java -index 3fa2964b979053ecbefc946c7fe76828de86d8f1..28bf0518f7d17099d7e4990defbeda6757b4477c 100644 ---- a/net/minecraft/world/level/ClipContext.java -+++ b/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/net/minecraft/world/level/EntityGetter.java b/net/minecraft/world/level/EntityGetter.java -index e185a33b5b1f8e8e0a0e666b24ba3e9186a8a7ff..5d7a6e4b73f032db356e7ec369b150013e940ee6 100644 ---- a/net/minecraft/world/level/EntityGetter.java -+++ b/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 getEntities(@Nullable Entity except, AABB box, Predicate predicate); - - List getEntities(EntityTypeTest filter, AABB box, Predicate 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 moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate 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 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 List getEntitiesOfClass(Class entityClass, AABB box) { -@@ -52,23 +75,41 @@ public interface EntityGetter { - } - - default List 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 entities; -+ if (entity != null && ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$isHardColliding()) { -+ entities = this.getEntities(entity, box, null); - } else { -- Predicate predicate = entity == null ? EntitySelector.CAN_BE_COLLIDED_WITH : EntitySelector.NO_SPECTATORS.and(entity::canCollideWith); -- List list = this.getEntities(entity, box.inflate(1.0E-7), predicate); -- if (list.isEmpty()) { -- return List.of(); -- } else { -- Builder 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 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/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java -index 2a078293332efe4369f314ab021dfa16f63f7f3f..f477c5817f022ce7c4ad25e9b827401434bcfff1 100644 ---- a/net/minecraft/world/level/Level.java -+++ b/net/minecraft/world/level/Level.java -@@ -84,6 +84,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 -@@ -105,7 +106,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> RESOURCE_KEY_CODEC = ResourceKey.codec(Registries.DIMENSION); - public static final ResourceKey OVERWORLD = ResourceKey.create(Registries.DIMENSION, ResourceLocation.withDefaultNamespace("overworld")); -@@ -131,7 +132,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(); -@@ -207,7 +208,639 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - - public abstract ResourceKey getTypeKey(); - -+ // Paper start - rewrite chunk system -+ private ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup; -+ private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable 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 List getEntitiesOfClass(final Class entityClass, final AABB boundingBox, final Predicate predicate) { -+ Profiler.get().incrementCounter("getEntities"); -+ final List 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 moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate predicate) { -+ Profiler.get().incrementCounter("getEntities"); -+ final List 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 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 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 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 aabbs = new java.util.ArrayList<>(); -+ final List 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 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 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 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 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 resourcekey, RegistryAccess iregistrycustom, Holder 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 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; -@@ -288,6 +921,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 -@@ -557,7 +1191,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); - } - -@@ -820,6 +1454,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - // Iterator 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(); // Paper - Fix MC-117075; use removeAll - toRemove.add(null); // Paper - Fix MC-117075 -@@ -835,6 +1471,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 -@@ -855,12 +1496,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 - -@@ -912,7 +1561,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) { -@@ -1004,23 +1653,15 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - Profiler.get().incrementCounter("getEntities"); - List list = Lists.newArrayList(); - -- this.getEntities().get(box, (entity1) -> { -- if (entity1 != except && predicate.test(entity1)) { -- list.add(entity1); -- } -- -- }); -- Iterator iterator = this.dragonParts().iterator(); -+ // Paper start - rewrite chunk system -+ final List ret = new java.util.ArrayList<>(); - -- while (iterator.hasNext()) { -- EnderDragonPart entitycomplexpart = (EnderDragonPart) iterator.next(); -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(except, box, ret, predicate); - -- if (entitycomplexpart != except && entitycomplexpart.parentMob != except && predicate.test(entitycomplexpart) && box.intersects(entitycomplexpart.getBoundingBox())) { -- 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 -@@ -1035,36 +1676,94 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - this.getEntities(filter, box, predicate, result, Integer.MAX_VALUE); - } - -- public void getEntities(EntityTypeTest filter, AABB box, Predicate predicate, List result, int limit) { -+ // Paper start - rewrite chunk system -+ public void getEntities(final EntityTypeTest entityTypeTest, -+ final AABB boundingBox, final Predicate predicate, -+ final List 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 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 base = entityTypeTest.getBaseClass(); - -- if (t0 != null && predicate.test(t0)) { -- result.add(t0); -- if (result.size() >= limit) { -- return AbortableIterationConsumer.Continuation.ABORT; -- } -- } -+ final Predicate 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 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/net/minecraft/world/level/LevelReader.java b/net/minecraft/world/level/LevelReader.java -index 5eb8982678110fabb82a93c5ec67c666b7fde017..ade435de0af4ee3566fa4a490df53cddd2f6531c 100644 ---- a/net/minecraft/world/level/LevelReader.java -+++ b/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/net/minecraft/world/level/ServerExplosion.java b/net/minecraft/world/level/ServerExplosion.java -index b8ffe547ad29645b65c3df8bd6ccb7c20985711d..685ccfb73bf7125585ef90b6a0f51b2f81daa428 100644 ---- a/net/minecraft/world/level/ServerExplosion.java -+++ b/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 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 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 calculateExplodedPositions() { -- Set 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 ret = new ObjectArrayList<>(); - -- Optional 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 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 - - } - -@@ -499,12 +793,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/net/minecraft/world/level/biome/Biome.java b/net/minecraft/world/level/biome/Biome.java -index 9f86b69d8c93a63e0b408ea52519f1fc2e798226..78afd8e51e03cd53c12b64db8a817da457f81bef 100644 ---- a/net/minecraft/world/level/biome/Biome.java -+++ b/net/minecraft/world/level/biome/Biome.java -@@ -113,20 +113,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/net/minecraft/world/level/biome/BiomeManager.java b/net/minecraft/world/level/biome/BiomeManager.java -index 01352cc83b25eb0e30b7e0ff521fc7c1b3d5155b..90f8360f547ce709fd13ee34f8e67d8bfa94b498 100644 ---- a/net/minecraft/world/level/biome/BiomeManager.java -+++ b/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/net/minecraft/world/level/block/Block.java b/net/minecraft/world/level/block/Block.java -index 1aa69f4a7005242925124c74b8229e6fa7362717..c0b1f903962b25d8ff6c2b4fcd2be0e45de09b35 100644 ---- a/net/minecraft/world/level/block/Block.java -+++ b/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/net/minecraft/world/level/block/state/BlockBehaviour.java b/net/minecraft/world/level/block/state/BlockBehaviour.java -index b1101156b281d800f18b25208018722bbecded9f..8c0f332a1a0918f60226d969918ae7fe4fe74166 100644 ---- a/net/minecraft/world/level/block/state/BlockBehaviour.java -+++ b/net/minecraft/world/level/block/state/BlockBehaviour.java -@@ -797,7 +797,7 @@ public abstract class BlockBehaviour implements FeatureElement { - boolean test(BlockState state, BlockGetter world, BlockPos pos); - } - -- public abstract static class BlockStateBase extends StateHolder { -+ public abstract static class BlockStateBase extends StateHolder 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) -> { -@@ -841,6 +841,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, Comparable> propertyMap, MapCodec codec) { - super(block, propertyMap, codec); - this.fluidState = Fluids.EMPTY.defaultFluidState(); -@@ -925,6 +995,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; -+ if (this.isAir()) { -+ this.constantCollisionShape = Shapes.empty(); -+ } else { -+ 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/net/minecraft/world/level/block/state/StateHolder.java b/net/minecraft/world/level/block/state/StateHolder.java -index 422b364764e0df16ca250b4939d7b226e69c0840..815ee11aa5ed3448ff255e9c36d769478de477bd 100644 ---- a/net/minecraft/world/level/block/state/StateHolder.java -+++ b/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 { -+public abstract class StateHolder 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, Comparable>, String> PROPERTY_ENTRY_TO_STRING_FUNCTION = new Function, Comparable>, String>() { -@@ -34,14 +34,28 @@ public abstract class StateHolder { - } - }; - protected final O owner; -- private final Reference2ObjectArrayMap, Comparable> values; -+ private Reference2ObjectArrayMap, Comparable> values; // Paper - optimise blockstate property access - remove final - private Map, S[]> neighbours; - protected final MapCodec propertiesCodec; - -+ // Paper start - optimise blockstate property access -+ protected ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable 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, Comparable> propertyMap, MapCodec 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)(Object)this); -+ // Paper end - optimise blockstate property access - } - - public > S cycle(Property property) { -@@ -67,20 +81,21 @@ public abstract class StateHolder { - } - - public Collection> getProperties() { -- return Collections.unmodifiableCollection(this.values.keySet()); -+ return this.optimisedTable.getProperties(); // Paper - optimise blockstate property access - } - - public > boolean hasProperty(Property property) { -- return this.values.containsKey(property); -+ return property != null && this.optimisedTable.hasProperty(property); // Paper - optimise blockstate property access - } - - public > T getValue(Property 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 > Optional getOptionalValue(Property property) { -@@ -93,22 +108,30 @@ public abstract class StateHolder { - - @Nullable - public > T getNullableValue(Property 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 , V extends T> S setValue(Property 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 , V extends T> S trySetValue(Property 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)(Object)this; -+ } -+ final S ret = this.optimisedTable.trySet(this.tableIndex, property, value, (S)(StateHolder)(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 , V extends T> S setValueInternal(Property property, V newValue, Comparable oldValue) { -@@ -125,18 +148,27 @@ public abstract class StateHolder { - } - - public void populateNeighbours(Map, Comparable>, S> states) { -- if (this.neighbours != null) { -- throw new IllegalStateException(); -- } else { -- Map, S[]> map = new Reference2ObjectArrayMap<>(this.values.size()); -+ // Paper start - optimise blockstate property access -+ final Map, Comparable>, S> map = states; -+ if (this.optimisedTable.isLoaded()) { -+ return; -+ } -+ this.optimisedTable.loadInTable(map); - -- for (Entry, 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, Comparable>, S> entry : map.entrySet()) { -+ final S value = entry.getValue(); -+ ((StateHolder)value).optimisedTable = this.optimisedTable; -+ } - -- this.neighbours = map; -+ // remove values arrays -+ for (final Map.Entry, Comparable>, S> entry : map.entrySet()) { -+ final S value = entry.getValue(); -+ ((StateHolder)value).values = null; - } -+ -+ return; -+ // Paper end optimise blockstate property access - } - - private Map, Comparable> makeNeighbourValues(Property property, Comparable value) { -@@ -146,7 +178,11 @@ public abstract class StateHolder { - } - - public Map, Comparable> getValues() { -- return this.values; -+ // Paper start - optimise blockstate property access -+ ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable 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 > Codec codec(Codec codec, Function ownerToStateFunction) { -diff --git a/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/net/minecraft/world/level/block/state/properties/BooleanProperty.java -index ea76aa490358e9e1d13350ba0ea246ec2c423894..98058505d36baf74008da08339afc196713b14a7 100644 ---- a/net/minecraft/world/level/block/state/properties/BooleanProperty.java -+++ b/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 { -+public final class BooleanProperty extends Property implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess { // Paper - optimise blockstate property access - private static final List 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/net/minecraft/world/level/block/state/properties/EnumProperty.java b/net/minecraft/world/level/block/state/properties/EnumProperty.java -index 85a197232be9377c0313ec00e8f935551e2c60e0..30b2fce9e47ffcc3de1542b1d0f073f5640127a7 100644 ---- a/net/minecraft/world/level/block/state/properties/EnumProperty.java -+++ b/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 & StringRepresentable> extends Property { -+public final class EnumProperty & StringRepresentable> extends Property implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess { // Paper - optimise blockstate property access - private final List values; - private final Map 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 target = this.getValueClass(); -+ return ((value.getClass() != target && value.getDeclaringClass() != target)) ? -1 : this.idLookupTable[value.ordinal()]; -+ } -+ -+ private void init() { -+ final java.util.Collection values = this.getPossibleValues(); -+ final Class 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 type, List values) { - super(name, type); - if (values.isEmpty()) { -@@ -37,6 +65,7 @@ public final class EnumProperty & StringRepresentable> extends - - this.names = builder.buildOrThrow(); - } -+ this.init(); // Paper - optimise blockstate property access - } - - @Override -diff --git a/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/net/minecraft/world/level/block/state/properties/IntegerProperty.java -index 55a87592a99105dbf57b26fb6ccba695295fce24..986365acc9983331a7982ea2e1eac2b0efe1506d 100644 ---- a/net/minecraft/world/level/block/state/properties/IntegerProperty.java -+++ b/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 { -+public final class IntegerProperty extends Property implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess { // 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 { - this.max = max; - this.values = IntImmutableList.toList(IntStream.range(min, max + 1)); - } -+ this.init(); // Paper - optimise blockstate property access - } - - @Override -diff --git a/net/minecraft/world/level/block/state/properties/Property.java b/net/minecraft/world/level/block/state/properties/Property.java -index fcf04c5c58ff35d38c5bf0df562ae2f8dc98a0ee..0b116160924300a9d62ad5948bfaf276f0386e4d 100644 ---- a/net/minecraft/world/level/block/state/properties/Property.java -+++ b/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> { -+public abstract class Property> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess { // Paper - optimise blockstate property access - private final Class clazz; - private final String name; - @Nullable -@@ -24,9 +24,38 @@ public abstract class Property> { - ); - private final Codec> 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 type) { - this.clazz = type; - this.name = name; -+ this.id = ID_GENERATOR.getAndIncrement(); // Paper - optimise blockstate property access - } - - public Property.Value value(T value) { -diff --git a/net/minecraft/world/level/chunk/ChunkAccess.java b/net/minecraft/world/level/chunk/ChunkAccess.java -index 9d240aa87101662480cdd510839e017aa9c58fcd..f87abb22dd161b2b74401086de80dc95c9ac2dbb 100644 ---- a/net/minecraft/world/level/chunk/ChunkAccess.java -+++ b/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 heightmaps = Maps.newEnumMap(Heightmap.Types.class); -- protected ChunkSkyLightSources skyLightSources; -+ // Paper - rewrite chunk system - private final Map structureStarts = Maps.newHashMap(); - private final Map structuresRefences = Maps.newHashMap(); - protected final Map 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 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 biomeRegistry; - // CraftBukkit end -@@ -457,22 +518,22 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh - - @Override - public Holder 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 -@@ -529,12 +590,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> blocks, List> fluids) { -diff --git a/net/minecraft/world/level/chunk/ChunkGenerator.java b/net/minecraft/world/level/chunk/ChunkGenerator.java -index ca6928f959eb63ac9183ba6c95738609839a7d32..e0cb360ece042c4fc6aa0d10106923fe25288f5c 100644 ---- a/net/minecraft/world/level/chunk/ChunkGenerator.java -+++ b/net/minecraft/world/level/chunk/ChunkGenerator.java -@@ -120,7 +120,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); -@@ -315,7 +315,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/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/net/minecraft/world/level/chunk/EmptyLevelChunk.java -index dcc0acd259920463a4464213b9a5e793603852f9..ef4161884574d3d137e12591d983dc95a960cb19 100644 ---- a/net/minecraft/world/level/chunk/EmptyLevelChunk.java -+++ b/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; - - public EmptyLevelChunk(Level world, ChunkPos pos, Holder 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/net/minecraft/world/level/chunk/HashMapPalette.java b/net/minecraft/world/level/chunk/HashMapPalette.java -index 98dbeaf8bde15940e5b5d5d1f13fd4bb32f0a10d..7beea075b5a7ef738a4ac0558b99f4c5708f2c4a 100644 ---- a/net/minecraft/world/level/chunk/HashMapPalette.java -+++ b/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 implements Palette { -+public class HashMapPalette implements Palette, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // Paper - optimise palette reads - private final IdMap registry; - private final CrudeIncrementalIntIdentityHashBiMap values; - private final PaletteResize resizeHandler; - private final int bits; - -+ // Paper start - optimise palette reads -+ @Override -+ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData container) { -+ return ((ca.spottedleaf.moonrise.patches.fast_palette.FastPalette)this.values).moonrise$getRawPalette(container); -+ } -+ // Paper end - optimise palette reads -+ - public HashMapPalette(IdMap idList, int bits, PaletteResize listener, List entries) { - this(idList, bits, listener); - entries.forEach(this.values::add); -diff --git a/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/net/minecraft/world/level/chunk/ImposterProtoChunk.java -index f38700e5fbeeb8a913272d4464b8aa325d511dac..1eb8022f3e31603322e6c56516304afc9a11bbec 100644 ---- a/net/minecraft/world/level/chunk/ImposterProtoChunk.java -+++ b/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/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java -index 0ade64bbdec563e555c981cee2208e6c72afe249..134d63076f231791988e67a5bdf191005112080b 100644 ---- a/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/net/minecraft/world/level/chunk/LevelChunk.java -@@ -55,7 +55,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() { -@@ -114,6 +114,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 -@@ -124,6 +132,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()); -@@ -157,13 +198,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) { -@@ -366,7 +413,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(); -@@ -632,11 +679,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 -@@ -645,6 +693,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; -@@ -671,13 +720,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 -@@ -685,8 +736,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() { -@@ -794,6 +868,7 @@ public class LevelChunk extends ChunkAccess { - - this.pendingBlockEntities.clear(); - this.upgradeData.upgrade(this); -+ this.postProcessingDone = true; // Paper - rewrite chunk system - } - - @Nullable -diff --git a/net/minecraft/world/level/chunk/LevelChunkSection.java b/net/minecraft/world/level/chunk/LevelChunkSection.java -index 3dab36d00ea48101807ba40c7a7358b7eed12747..e4ae25c83ab9dd1aaa530a5456275ef63cdb8511 100644 ---- a/net/minecraft/world/level/chunk/LevelChunkSection.java -+++ b/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 states; - private PalettedContainer> 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 { -+ // 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 data = this.states.data; -+ final Palette palette = data.palette(); -+ final int paletteSize = palette.getSize(); -+ final net.minecraft.util.BitStorage storage = data.storage(); -+ -+ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap 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> iterator = counts.int2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry 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 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/net/minecraft/world/level/chunk/LinearPalette.java b/net/minecraft/world/level/chunk/LinearPalette.java -index bc4d9452bbeb05a691fd285603e49491f41d3ad2..f8d9892970c9092f7cc84434d4fbf34354ce1195 100644 ---- a/net/minecraft/world/level/chunk/LinearPalette.java -+++ b/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 implements Palette { -+public class LinearPalette implements Palette, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // Paper - optimise palette reads - private final IdMap registry; - private final T[] values; - private final PaletteResize 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 container) { -+ return this.values; -+ } -+ // Paper end - optimise palette reads -+ - private LinearPalette(IdMap idList, int bits, PaletteResize listener, List list) { - this.registry = idList; - this.values = (T[])(new Object[1 << bits]); -diff --git a/net/minecraft/world/level/chunk/Palette.java b/net/minecraft/world/level/chunk/Palette.java -index b8922e4a13df535cdc5701e893a6e460b33ff90d..100807f8b8337f56f49cdb818ccc75be2f08ecd1 100644 ---- a/net/minecraft/world/level/chunk/Palette.java -+++ b/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 { -+public interface Palette extends ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // Paper - optimise palette reads - int idFor(T object); - - boolean maybeHas(Predicate predicate); -diff --git a/net/minecraft/world/level/chunk/PalettedContainer.java b/net/minecraft/world/level/chunk/PalettedContainer.java -index 69d6f203366df658e1ade55d917f0aa2b8a49be9..8b84bf2272556ac3321cbf16361d7f48a1cc6873 100644 ---- a/net/minecraft/world/level/chunk/PalettedContainer.java -+++ b/net/minecraft/world/level/chunk/PalettedContainer.java -@@ -29,7 +29,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer - private final PaletteResize dummyPaletteResize = (newSize, added) -> 0; - public final IdMap registry; - private final T @org.jetbrains.annotations.Nullable [] presetValues; // Paper - Anti-Xray - Add preset values -- private volatile PalettedContainer.Data data; -+ public volatile PalettedContainer.Data 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 implements PaletteResize, PalettedContainer - ); - } - -+ // Paper start - optimise palette reads -+ private void updateData(final PalettedContainer.Data data) { -+ if (data != null) { -+ ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData)(Object)data).moonrise$setPalette( -+ ((ca.spottedleaf.moonrise.patches.fast_palette.FastPalette)data.palette).moonrise$getRawPalette((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData)(Object)data) -+ ); -+ } -+ } -+ -+ private T readPaletteSlow(final PalettedContainer.Data data, final int paletteIdx) { -+ return data.palette.valueFor(paletteIdx); -+ } -+ -+ private T readPalette(final PalettedContainer.Data data, final int paletteIdx) { -+ final T[] palette = ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData)(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 idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Configuration dataProvider, BitStorage storage, List paletteEntries) { this(idList, paletteProvider, dataProvider, storage, paletteEntries, null, null); } - public PalettedContainer( -@@ -113,6 +140,7 @@ public class PalettedContainer implements PaletteResize, 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 implements PaletteResize, PalettedContainer - this.registry = idList; - this.strategy = paletteProvider; - this.data = data; -+ this.updateData(this.data); // Paper - optimise palette reads - } - - private PalettedContainer(PalettedContainer container, T @org.jetbrains.annotations.Nullable [] presetValues) { // Paper - Anti-Xray - Add preset values -@@ -140,6 +169,7 @@ public class PalettedContainer implements PaletteResize, 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 createOrReuseData(@Nullable PalettedContainer.Data previousData, int bits) { -@@ -166,6 +196,7 @@ public class PalettedContainer implements PaletteResize, 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 implements PaletteResize, 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 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 implements PaletteResize, PalettedContainer - return this.get(this.strategy.getIndex(x, y, z)); - } - -- protected T get(int index) { -- PalettedContainer.Data 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 data = this.data; -+ return this.readPalette(data, data.storage.get(index)); -+ // Paper end - optimise palette reads - } - - @Override -@@ -246,6 +282,7 @@ public class PalettedContainer implements PaletteResize, 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 implements PaletteResize, PalettedContainer - void accept(T object, int count); - } - -- static record Data(PalettedContainer.Configuration configuration, BitStorage storage, Palette palette) { -+ // Paper start - optimise palette reads -+ public static final class Data implements ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData { -+ -+ private final PalettedContainer.Configuration configuration; -+ private final BitStorage storage; -+ private final Palette palette; -+ -+ private T[] moonrise$palette; -+ -+ public Data(final PalettedContainer.Configuration configuration, final BitStorage storage, final Palette palette) { -+ this.configuration = configuration; -+ this.storage = storage; -+ this.palette = palette; -+ } -+ -+ public PalettedContainer.Configuration configuration() { -+ return this.configuration; -+ } -+ -+ public BitStorage storage() { -+ return this.storage; -+ } -+ -+ public Palette 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 palette, BitStorage storage) { - for (int i = 0; i < storage.getSize(); i++) { - T object = palette.valueFor(storage.get(i)); -diff --git a/net/minecraft/world/level/chunk/ProtoChunk.java b/net/minecraft/world/level/chunk/ProtoChunk.java -index 15e14f5d006389c823fa6baf8c1a4f22804d4aa8..759adee51bad99bd4bbee4f44247e8c8486cfbd6 100644 ---- a/net/minecraft/world/level/chunk/ProtoChunk.java -+++ b/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/net/minecraft/world/level/chunk/SingleValuePalette.java b/net/minecraft/world/level/chunk/SingleValuePalette.java -index a45e6410600afc5464e5d29932c193786ce0a6fb..a1ba68c95c2cdebdc0d7782cce7895529918073c 100644 ---- a/net/minecraft/world/level/chunk/SingleValuePalette.java -+++ b/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 implements Palette { -+public class SingleValuePalette implements Palette, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // Paper - optimise palette reads - private final IdMap registry; - @Nullable - private T value; - private final PaletteResize resizeHandler; - -+ // Paper start - optimise palette reads -+ private T[] rawPalette; -+ -+ @Override -+ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData container) { -+ if (this.rawPalette != null) { -+ return this.rawPalette; -+ } -+ return this.rawPalette = (T[])new Object[] { this.value }; -+ } -+ // Paper end - optimise palette reads -+ - public SingleValuePalette(IdMap idList, PaletteResize listener, List entries) { - this.registry = idList; - this.resizeHandler = listener; -@@ -33,6 +45,11 @@ public class SingleValuePalette implements Palette { - 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 implements Palette { - @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/net/minecraft/world/level/chunk/status/ChunkPyramid.java b/net/minecraft/world/level/chunk/status/ChunkPyramid.java -index b1058bf0dcda544a074f4d3772d7899b94f98927..b7bf82f6b6023bd628d3e7ea84d2d6755a0d931a 100644 ---- a/net/minecraft/world/level/chunk/status/ChunkPyramid.java -+++ b/net/minecraft/world/level/chunk/status/ChunkPyramid.java -@@ -54,7 +54,7 @@ public record ChunkPyramid(ImmutableList 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/net/minecraft/world/level/chunk/status/ChunkStatus.java b/net/minecraft/world/level/chunk/status/ChunkStatus.java -index 4f84ff9cdb3303251e035a12ce9d8b9a0b58f46e..d80b7d555e02d1d4b82945373d383eaedbf4b976 100644 ---- a/net/minecraft/world/level/chunk/status/ChunkStatus.java -+++ b/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 WORLDGEN_HEIGHTMAPS = EnumSet.of(Heightmap.Types.OCEAN_FLOOR_WG, Heightmap.Types.WORLD_SURFACE_WG); - public static final EnumSet 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 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/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java b/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java -index 3d8a35d8cf29447ee7ac750dbc6ffcdb0f89b81b..9a3900e970f22892d8a3da8a28f922aa9b62765f 100644 ---- a/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java -+++ b/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 entities) { // Paper - public -+ public static void postLoadProtoChunk(ServerLevel world, List 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/net/minecraft/world/level/chunk/status/ChunkStep.java b/net/minecraft/world/level/chunk/status/ChunkStep.java -index 3d37a0372cdd99e806a9651cc1cabaefa9338065..f9aad1b8c02b70e620efdc2a58cadf4fff0f3ed5 100644 ---- a/net/minecraft/world/level/chunk/status/ChunkStep.java -+++ b/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/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/net/minecraft/world/level/chunk/storage/ChunkStorage.java -index 092f7b6bba4e1291f76c2c09155f33803e93eb04..46f4b6706a1ca24ff6fc28960ad01a067109819f 100644 ---- a/net/minecraft/world/level/chunk/storage/ChunkStorage.java -+++ b/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> 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 write(ChunkPos chunkPos, Supplier 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/net/minecraft/world/level/chunk/storage/EntityStorage.java b/net/minecraft/world/level/chunk/storage/EntityStorage.java -index a0cbccd2cf1ac785745d86c42b6f58fb8bad7ffa..16ca1c8672e5f0a27f8a30498c754a81cdec5191 100644 ---- a/net/minecraft/world/level/chunk/storage/EntityStorage.java -+++ b/net/minecraft/world/level/chunk/storage/EntityStorage.java -@@ -71,12 +71,12 @@ public class EntityStorage implements EntityPersistentStorage { - } - } - -- 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/net/minecraft/world/level/chunk/storage/IOWorker.java b/net/minecraft/world/level/chunk/storage/IOWorker.java -index 1f2997cf5367200084f32c437f77040c8c6a18e6..a8a9e59a9721a76e34f78c1baa5026e5fe1d2bda 100644 ---- a/net/minecraft/world/level/chunk/storage/IOWorker.java -+++ b/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 pendingWrites = new LinkedHashMap<>(); - private final Long2ObjectLinkedOpenHashMap> regionCacheForBlender = new Long2ObjectLinkedOpenHashMap<>(); - private static final int REGION_CACHE_SIZE = 1024; -diff --git a/net/minecraft/world/level/chunk/storage/RegionFile.java b/net/minecraft/world/level/chunk/storage/RegionFile.java -index 863960ead8deaa0553be1c98e4fa09f07fcb8ef0..057875cbbdc92ba49b429f9a129514760edb32a2 100644 ---- a/net/minecraft/world/level/chunk/storage/RegionFile.java -+++ b/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/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -index e6abe35d6c43b7f76cf3da129ec9552e7b82453e..fdf8e18d24442178b52397acb482ffa3306a32e3 100644 ---- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -+++ b/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 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 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 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/net/minecraft/world/level/chunk/storage/SectionStorage.java b/net/minecraft/world/level/chunk/storage/SectionStorage.java -index 75b2cf0e13c23a8348b7ff55e72e5ee755aa7460..c3beb7fcad46a917d2b61bd0a0e98e5106056728 100644 ---- a/net/minecraft/world/level/chunk/storage/SectionStorage.java -+++ b/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 implements AutoCloseable { -+public class SectionStorage 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> storage = new Long2ObjectOpenHashMap<>(); - private final LongLinkedOpenHashSet dirtyChunks = new LongLinkedOpenHashSet(); - private final Codec

    codec; -@@ -57,6 +57,18 @@ public class SectionStorage implements AutoCloseable { - private final Long2ObjectMap>>> 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

    codec, -@@ -67,7 +79,7 @@ public class SectionStorage 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 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,64 +201,15 @@ public class SectionStorage implements AutoCloseable { - } - - private CompletableFuture>> tryRead(ChunkPos chunkPos) { -- RegistryOps 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 CompletionException) { -- throwable = throwable.getCause(); -- } -- -- 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

    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 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 registryOps = this.registryAccess.createSerializationContext(NbtOps.INSTANCE); -- Dynamic 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 Dynamic writeChunk(ChunkPos chunkPos, DynamicOps ops) { -@@ -281,7 +245,7 @@ public class SectionStorage implements AutoCloseable { - protected void onSectionLoad(long pos) { - } - -- protected void setDirty(long pos) { -+ public void setDirty(long pos) { // Paper - public - Optional optional = this.storage.get(pos); - if (optional != null && !optional.isEmpty()) { - this.dirtyChunks.add(ChunkPos.asLong(SectionPos.x(pos), SectionPos.z(pos))); -@@ -302,7 +266,7 @@ public class SectionStorage implements AutoCloseable { - - @Override - public void close() throws IOException { -- this.simpleRegionStorage.close(); -+ this.moonrise$close(); // Paper - rewrite chunk system - } - - static record PackedChunk(Int2ObjectMap sectionsByY, boolean versionChanged) { -diff --git a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java -index 4bc7fa3324e9af3abce2acf960c7b0650aca2e36..0296f52fb2c871adbf2ce73a64d8f77fab826cd7 100644 ---- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java -+++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java -@@ -129,7 +129,7 @@ public record SerializableChunkData(Registry 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 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 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 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 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 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 biomeRegistry, ChunkPos chun - throw new IllegalArgumentException("Chunk can't be serialized: " + String.valueOf(chunk)); - } else { - ChunkPos chunkcoordintpair = chunk.getPos(); -- List list = new ArrayList(); -+ List list = new ArrayList(); final List 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 list1 = new ArrayList(chunk.getBlockEntitiesPos().size()); - Iterator iterator = chunk.getBlockEntitiesPos().iterator(); -@@ -521,8 +608,8 @@ public record SerializableChunkData(Registry 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 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 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 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/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java b/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java -index 578d270d5b7efb9ac8f5dde539170f6021e2b786..c5085ebf4e801837010f3750c5e89576bb0c27a5 100644 ---- a/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java -+++ b/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/net/minecraft/world/level/entity/EntityTickList.java b/net/minecraft/world/level/entity/EntityTickList.java -index 74a285b8b018a9c94ccea519f1ce8b9e2ef3cb64..d8b4196adf955f8d414688dc451caac2d9c609d9 100644 ---- a/net/minecraft/world/level/entity/EntityTickList.java -+++ b/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 active = new Int2ObjectLinkedOpenHashMap<>(); -- private Int2ObjectMap passive = new Int2ObjectLinkedOpenHashMap<>(); -- @Nullable -- private Int2ObjectMap iterated; -+ private final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet 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 entry : Int2ObjectMaps.fastIterable(this.active)) { -- this.passive.put(entry.getIntKey(), entry.getValue()); -- } -- -- Int2ObjectMap 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 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 iterator = this.entities.iterator(); -+ try { -+ while (iterator.hasNext()) { -+ action.accept(iterator.next()); - } -+ } finally { -+ iterator.finishedIterating(); - } -+ // Paper end - rewrite chunk system - } - } -diff --git a/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java -index 1fcc2b287ed723cf51720f80e68f18f4a15cf429..3f39d6c786d9dfdd9ad591e08ff05fcbb41a1df6 100644 ---- a/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java -+++ b/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/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/net/minecraft/world/level/levelgen/structure/StructureCheck.java -index c3586281c9594769593a6027ea0a78f7c76c0262..decdb275e83fa6244aa3a24458872b42c49d04ed 100644 ---- a/net/minecraft/world/level/levelgen/structure/StructureCheck.java -+++ b/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> loadedChunks = new Long2ObjectOpenHashMap<>(); -- private final Map 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> loadedChunksSafe = new ca.spottedleaf.moonrise.common.map.SynchronisedLong2ObjectMap<>(CHUNK_TOTAL_LIMIT); -+ private final java.util.concurrent.ConcurrentHashMap 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 object2IntMap = this.loadedChunks.get(l); -+ Object2IntMap 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 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 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/net/minecraft/world/level/lighting/LevelLightEngine.java b/net/minecraft/world/level/lighting/LevelLightEngine.java -index 8d90e783967280025d711c709facbcc87f611f8a..987e3397503cd07d3a2f172cede341297bc58dba 100644 ---- a/net/minecraft/world/level/lighting/LevelLightEngine.java -+++ b/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/net/minecraft/world/level/material/FlowingFluid.java b/net/minecraft/world/level/material/FlowingFluid.java -index 261e5994d13f8bc30490b86691c80c0a21e7640a..f4fbcbb8ff6d2677af1a02a0801a323c06dce9b1 100644 ---- a/net/minecraft/world/level/material/FlowingFluid.java -+++ b/net/minecraft/world/level/material/FlowingFluid.java -@@ -55,6 +55,48 @@ public abstract class FlowingFluid extends Fluid { - }); - private final Map 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 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/net/minecraft/world/level/material/FluidState.java b/net/minecraft/world/level/material/FluidState.java -index 87adfe152abd1b8b4d547034576883c5d1cdf134..2d50d72bf026d0cf9c546a3c6fc1859379bfd805 100644 ---- a/net/minecraft/world/level/material/FluidState.java -+++ b/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 { -+public final class FluidState extends StateHolder implements ca.spottedleaf.moonrise.patches.fluid.FluidFluidState { // Paper - fluid method optimisations - public static final Codec 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, Comparable> propertyMap, MapCodec codec) { - super(fluid, propertyMap, codec); - this.isEmpty = fluid.isEmpty(); // Paper - Perf: moved from isEmpty() -@@ -38,11 +56,11 @@ public final class FluidState extends StateHolder { - } - - 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 { - } - - 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 { - } - - 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 { - } - - 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/net/minecraft/world/phys/AABB.java b/net/minecraft/world/phys/AABB.java -index 5dc2674b537f4a61b2e21a21bdb2e8dc090d3a3c..6cf6d4ec7b9e43c7b2b4c0e2fb080964ff588130 100644 ---- a/net/minecraft/world/phys/AABB.java -+++ b/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/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/net/minecraft/world/phys/shapes/ArrayVoxelShape.java -index 4fee67f7214b464b9e09862778e3ef187fcb8b72..31a54af04ab072a433d6df9fe37beb12243fea80 100644 ---- a/net/minecraft/world/phys/shapes/ArrayVoxelShape.java -+++ b/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/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java b/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java -index e8f3307727e7e3da9a7629cafc6e1ee53790b75d..97ef481156ec5d821779f126ab98a8f28cbaf30b 100644 ---- a/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java -+++ b/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/net/minecraft/world/phys/shapes/CubeVoxelShape.java b/net/minecraft/world/phys/shapes/CubeVoxelShape.java -index d812949c7329ae2696b38dc792fa011ba87decb9..7743495c7ec3fc5e17947144457cef7bbe0f4b38 100644 ---- a/net/minecraft/world/phys/shapes/CubeVoxelShape.java -+++ b/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/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java b/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java -index 01693ba050b12b9debcdaefceeff9cbcd503b369..fbe0c4b0fdbb992b7002f6afe1e74d63cbb420f2 100644 ---- a/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java -+++ b/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/net/minecraft/world/phys/shapes/OffsetDoubleList.java b/net/minecraft/world/phys/shapes/OffsetDoubleList.java -index 7ec02a7849437a18860aa0df7d9ddd71b2447d4c..5e45e49ab09344cb95736f4124b1c6e002ef5b82 100644 ---- a/net/minecraft/world/phys/shapes/OffsetDoubleList.java -+++ b/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/net/minecraft/world/phys/shapes/Shapes.java b/net/minecraft/world/phys/shapes/Shapes.java -index 76d7435e6fe81a3f1d24b35eae72d06232a1792b..ca3a2419252721bb3b3b719eb19afb5f175394c0 100644 ---- a/net/minecraft/world/phys/shapes/Shapes.java -+++ b/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/net/minecraft/world/phys/shapes/SliceShape.java b/net/minecraft/world/phys/shapes/SliceShape.java -index b07f1c58e00d232e7c83e6df3499e4b677645609..b88c71f27996d24d29048e06a69a004617eb53a2 100644 ---- a/net/minecraft/world/phys/shapes/SliceShape.java -+++ b/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/net/minecraft/world/phys/shapes/VoxelShape.java b/net/minecraft/world/phys/shapes/VoxelShape.java -index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..6182f1d37c7a63479f6c6e7c37a7edc9cffc3071 100644 ---- a/net/minecraft/world/phys/shapes/VoxelShape.java -+++ b/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 toAabbsUncached() { -+ final List 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, offset, (positiveDir ? (1.0 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) : (0.0 + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)), -+ 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 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 toAabbs() { -- List 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, this.offsetX, value, 0, values.length - 1 -+ ); -+ } -+ case Y: { -+ final double[] values = this.rootCoordinatesY; -+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor( -+ values, this.offsetY, value, 0, values.length - 1 -+ ); -+ } -+ case Z: { -+ final double[] values = this.rootCoordinatesZ; -+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor( -+ values, this.offsetZ, value, 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 closestPointTo(Vec3 target) { -- if (this.isEmpty()) { -+ // Paper start - optimise collisions -+ public Optional 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 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/net/minecraft/world/ticks/LevelChunkTicks.java b/net/minecraft/world/ticks/LevelChunkTicks.java -index 26620c06d26a2c0eb957fbadc6ac3d7a309bff46..3858c83c58e78435a6e29de84c33faa2f26d593d 100644 ---- a/net/minecraft/world/ticks/LevelChunkTicks.java -+++ b/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 implements SerializableTickContainer, TickContainerAccess { -+public class LevelChunkTicks implements SerializableTickContainer, TickContainerAccess, ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks { // Paper - rewrite chunk system - private final Queue> tickQueue = new PriorityQueue<>(ScheduledTick.DRAIN_ORDER); - @Nullable - private List> pendingTicks; -@@ -25,6 +25,30 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon - @Nullable - private BiConsumer, ScheduledTick> 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 implements SerializableTickContainer, TickCon - public ScheduledTick poll() { - ScheduledTick 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 implements SerializableTickContainer, TickCon - @Override - public void schedule(ScheduledTick 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 implements SerializableTickContainer, TickCon - while (iterator.hasNext()) { - ScheduledTick 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 implements SerializableTickContainer, TickCon - } - - public ListTag save(long time, Function typeToNameFunction) { -+ this.lastSaved = time; // Paper - rewrite chunk system - ListTag listTag = new ListTag(); - - for (SavedTick savedTick : this.pack(time)) { -@@ -121,6 +146,7 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon - - public void unpack(long time) { - if (this.pendingTicks != null) { -+ this.lastSaved = time; // Paper - rewrite chunk system - int i = -this.pendingTicks.size(); - - for (SavedTick savedTick : this.pendingTicks) { -diff --git a/org/bukkit/craftbukkit/CraftChunk.java b/org/bukkit/craftbukkit/CraftChunk.java -index f3ab07e44e2e912ea66c6148cfdb2a4a528741b2..c2bffe3450ee9f768e00a23ec09df74d7a06d49b 100644 ---- a/org/bukkit/craftbukkit/CraftChunk.java -+++ b/org/bukkit/craftbukkit/CraftChunk.java -@@ -83,6 +83,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 -@@ -117,60 +123,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 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/org/bukkit/craftbukkit/CraftServer.java b/org/bukkit/craftbukkit/CraftServer.java -index 5b64111bc8baca45ecc7bfa384e5f8a004163a0b..97b5d6ba2b19a7c730730c74175a29157aed1840 100644 ---- a/org/bukkit/craftbukkit/CraftServer.java -+++ b/org/bukkit/craftbukkit/CraftServer.java -@@ -1448,7 +1448,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(); -@@ -1493,7 +1493,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); -@@ -2531,7 +2531,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/org/bukkit/craftbukkit/CraftWorld.java b/org/bukkit/craftbukkit/CraftWorld.java -index ca62105a0ff0aa69385cbf2018f8fe6a4bb69fd4..92d9f0ea8f7810ae20d3996f49aefa539b4bcb69 100644 ---- a/org/bukkit/craftbukkit/CraftWorld.java -+++ b/org/bukkit/craftbukkit/CraftWorld.java -@@ -507,15 +507,17 @@ 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 playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false); -- if (playersInRange.isEmpty()) return; -+ if (playersInRange.isEmpty()) return true; // Paper - chunk system - - FeatureHooks.sendChunkRefreshPackets(playersInRange, chunk); -- }); -- }); -- -+ // Paper - chunk system - return true; - } - -@@ -618,20 +620,8 @@ public class CraftWorld extends CraftRegionAccessor implements World { - @Override - public Collection getPluginChunkTickets(int x, int z) { - DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; -- SortedArraySet> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z)); -- -- if (tickets == null) { -- return Collections.emptyList(); -- } - -- ImmutableList.Builder 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 -@@ -639,7 +629,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { - Map> ret = new HashMap<>(); - DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; - -- for (Long2ObjectMap.Entry>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) { -+ for (Long2ObjectMap.Entry>> chunkTickets : chunkDistanceManager.moonrise$getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { // Paper - rewrite chunk system - long chunkKey = chunkTickets.getLongKey(); - SortedArraySet> tickets = chunkTickets.getValue(); - -@@ -1342,12 +1332,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() { -@@ -2486,17 +2476,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/org/bukkit/craftbukkit/entity/CraftPlayer.java b/org/bukkit/craftbukkit/entity/CraftPlayer.java -index e9df37ff66700278bc94ea1e42135b92d97d03f7..6a647cab8b2e476987931486e290703b8726f2c7 100644 ---- a/org/bukkit/craftbukkit/entity/CraftPlayer.java -+++ b/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -3527,7 +3527,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 -@@ -3537,7 +3539,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 -@@ -3547,7 +3551,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/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java b/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java -index 39377ba0739f9660567b38475f101672f7b5e035..c025a4ff42257a4e84f0f9574b84f6987ef8ac11 100644 ---- a/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java -+++ b/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java -@@ -264,7 +264,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/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java -index 54c4434662d057a08800918641b95708cda61207..37458e8fd5d57acbf90a6bea4e66797cb07f69fa 100644 ---- a/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java -+++ b/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 moonrise$getHardCollidingEntities(final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB box, final java.util.function.Predicate predicate) { -+ return this.handle.moonrise$getHardCollidingEntities(entity, box, predicate); -+ } -+ // Paper end - rewrite chunk system - // Paper end - } - -diff --git a/org/spigotmc/AsyncCatcher.java b/org/spigotmc/AsyncCatcher.java -index ef2598760458833021ef1bee92137f42c9fe591f..1f23e775eba1c34e01145bd91b0ce26fed6ca9de 100644 ---- a/org/spigotmc/AsyncCatcher.java -+++ b/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/org/spigotmc/WatchdogThread.java b/org/spigotmc/WatchdogThread.java -index ad282d34919716b75acd10426cd071da9d064a51..529df2a41dd93d6e1505053bd04032dbf0cdaa31 100644 ---- a/org/spigotmc/WatchdogThread.java -+++ b/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, "------------------------------" ); - // diff --git a/paper-server/patches/features/0020-Rewrite-dataconverter-system.patch b/paper-server/patches/features/0020-Rewrite-dataconverter-system.patch index 4e1356ce74..6168b4106d 100644 --- a/paper-server/patches/features/0020-Rewrite-dataconverter-system.patch +++ b/paper-server/patches/features/0020-Rewrite-dataconverter-system.patch @@ -30560,10 +30560,10 @@ index 0000000000000000000000000000000000000000..5a6536377c9c1e1753e930ff2a6bb98e + } +} diff --git a/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java -index 834c5ce238c7adb0164a6282582d709348ef96cc..11cfe9cc29666ce3a6a40281069fb9eb4fa0ded2 100644 +index 0e21efc60e7dd7d348fd024d713772069951ccd4..504a5f8626b42817f04088e2539a6941cd9c6d9d 100644 --- a/ca/spottedleaf/moonrise/paper/PaperHooks.java +++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java -@@ -203,6 +203,43 @@ public final class PaperHooks implements PlatformHooks { +@@ -204,6 +204,43 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo @Override public CompoundTag convertNBT(final DSL.TypeReference type, final DataFixer dataFixer, final CompoundTag nbt, final int fromVersion, final int toVersion) { diff --git a/paper-server/patches/sources/net/minecraft/server/level/ChunkHolder.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ChunkHolder.java.patch index 8d1373ef67..b3f729b8b5 100644 --- a/paper-server/patches/sources/net/minecraft/server/level/ChunkHolder.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/level/ChunkHolder.java.patch @@ -100,7 +100,7 @@ + chunkResult.ifSuccess(chunk -> { + if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) { + ChunkHolder.this.isFullChunkReady = true; -+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkBorder(chunk, this); ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkBorder(chunk, this); + } + }); + }); @@ -111,7 +111,7 @@ if (isOrAfter && !isOrAfter1) { + // Paper start + if (this.isFullChunkReady) { -+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper + } + // Paper end this.fullChunkFuture.complete(UNLOADED_LEVEL_CHUNK); @@ -126,7 +126,7 @@ + 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); ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkTicking(chunk, this); + }); + }); + // Paper end @@ -137,7 +137,7 @@ - this.tickingChunkFuture.complete(UNLOADED_LEVEL_CHUNK); + // Paper start + if (this.isTickingReady) { -+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().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 @@ -152,7 +152,7 @@ + this.entityTickingChunkFuture.thenAccept(chunkResult -> { + chunkResult.ifSuccess(chunk -> { + ChunkHolder.this.isEntityTickingReady = true; -+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkEntityTicking(chunk, this); ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkEntityTicking(chunk, this); + }); + }); + // Paper end @@ -163,7 +163,7 @@ - this.entityTickingChunkFuture.complete(UNLOADED_LEVEL_CHUNK); + // Paper start + if (this.isEntityTickingReady) { -+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().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 diff --git a/paper-server/patches/sources/net/minecraft/server/level/ChunkMap.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ChunkMap.java.patch index 6c090dc5e0..06c477be85 100644 --- a/paper-server/patches/sources/net/minecraft/server/level/ChunkMap.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/level/ChunkMap.java.patch @@ -74,10 +74,10 @@ ); stringBuilder.append("Updating:").append(System.lineSeparator()); - this.updatingChunkMap.values().forEach(consumer); -+ ca.spottedleaf.moonrise.common.util.ChunkSystem.getUpdatingChunkHolders(this.level).forEach(consumer); // Paper ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().getUpdatingChunkHolders(this.level).forEach(consumer); // Paper stringBuilder.append("Visible:").append(System.lineSeparator()); - this.visibleChunkMap.values().forEach(consumer); -+ ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).forEach(consumer); // Paper ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level).forEach(consumer); // Paper CrashReport crashReport = CrashReport.forThrowable(exception, "Chunk loading"); CrashReportCategory crashReportCategory = crashReport.addCategory("Chunk loading"); crashReportCategory.setDetail("Details", details); @@ -86,7 +86,7 @@ } else { holder = new ChunkHolder(new ChunkPos(chunkPos), newLevel, this.level, this.lightEngine, this::onLevelChange, this); + // Paper start -+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderCreate(this.level, holder); ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderCreate(this.level, holder); + // Paper end } @@ -97,7 +97,7 @@ if (flush) { - List list = this.visibleChunkMap - .values() -+ List list = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level) // Paper - moonrise ++ List list = ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level) // Paper - moonrise + //.values() // Paper - moonrise .stream() .filter(ChunkHolder::wasAccessibleSinceLastSave) @@ -107,7 +107,7 @@ long millis = Util.getMillis(); - for (ChunkHolder chunkHolder : this.visibleChunkMap.values()) { -+ for (ChunkHolder chunkHolder : ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level)) { // Paper ++ for (ChunkHolder chunkHolder : ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level)) { // Paper this.saveChunkIfNeeded(chunkHolder, millis); } } @@ -115,7 +115,7 @@ public boolean hasWork() { return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() -+ || ca.spottedleaf.moonrise.common.util.ChunkSystem.hasAnyChunkHolders(this.level) // Paper - moonrise ++ || ca.spottedleaf.moonrise.common.PlatformHooks.get().hasAnyChunkHolders(this.level) // Paper - moonrise || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() @@ -127,7 +127,7 @@ + // Paper start + boolean removed; + if ((removed = this.pendingUnloads.remove(chunkPos, chunkHolder)) && latestChunk != null) { -+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunkHolder); ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderDelete(this.level, chunkHolder); + // Paper end if (latestChunk instanceof LevelChunk levelChunk) { levelChunk.setLoaded(false); @@ -138,7 +138,7 @@ this.nextChunkSaveTime.remove(latestChunk.getPos().toLong()); - } + } else if (removed) { // Paper start -+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunkHolder); ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderDelete(this.level, chunkHolder); + } // Paper end } }, this.unloadQueue::add).whenComplete((_void, error) -> { @@ -148,7 +148,7 @@ public int size() { - return this.visibleChunkMap.size(); -+ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolderCount(this.level); // Paper ++ return ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolderCount(this.level); // Paper } public net.minecraft.server.level.DistanceManager getDistanceManager() { @@ -157,7 +157,7 @@ protected Iterable getChunks() { - return Iterables.unmodifiableIterable(this.visibleChunkMap.values()); -+ return Iterables.unmodifiableIterable(ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level)); // Paper ++ return Iterables.unmodifiableIterable(ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level)); // Paper } void dumpChunks(Writer writer) throws IOException { @@ -167,7 +167,7 @@ - for (Entry entry : this.visibleChunkMap.long2ObjectEntrySet()) { - long longKey = entry.getLongKey(); -+ for (ChunkHolder entry : ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level)) { // Paper - Moonrise ++ for (ChunkHolder entry : ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level)) { // Paper - Moonrise + long longKey = entry.pos.toLong(); // Paper - Moonrise ChunkPos chunkPos = new ChunkPos(longKey); - ChunkHolder chunkHolder = entry.getValue(); diff --git a/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch index 99d27344d3..b8d9ca742e 100644 --- a/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch @@ -117,7 +117,7 @@ + + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { + for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { -+ ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad( ++ ca.spottedleaf.moonrise.common.PlatformHooks.get().scheduleChunkLoad( + this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer + ); + } diff --git a/paper-server/patches/sources/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch b/paper-server/patches/sources/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch index 0d57d512ea..384e9f3c58 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch @@ -34,7 +34,7 @@ + // I don't want to know why this is a generic type. + Entity entityCasted = (Entity)entity; + boolean wasRemoved = entityCasted.isRemoved(); -+ boolean screened = ca.spottedleaf.moonrise.common.util.ChunkSystem.screenEntity((net.minecraft.server.level.ServerLevel)entityCasted.level(), entityCasted, worldGenSpawned, true); ++ boolean screened = ca.spottedleaf.moonrise.common.PlatformHooks.get().screenEntity((net.minecraft.server.level.ServerLevel)entityCasted.level(), entityCasted, worldGenSpawned, true); + if ((!wasRemoved && entityCasted.isRemoved()) || !screened) { + // removed by callback + return false; diff --git a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java index 6c98d420ea..9b879cbc03 100644 --- a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java +++ b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java @@ -1,5 +1,6 @@ package ca.spottedleaf.moonrise.common; +import ca.spottedleaf.moonrise.common.util.ChunkSystemHooks; import com.mojang.datafixers.DSL; import com.mojang.datafixers.DataFixer; import net.minecraft.core.BlockPos; @@ -23,7 +24,7 @@ import java.util.List; import java.util.ServiceLoader; import java.util.function.Predicate; -public interface PlatformHooks { +public interface PlatformHooks extends ChunkSystemHooks { public static PlatformHooks get() { return Holder.INSTANCE; } @@ -63,8 +64,6 @@ public interface PlatformHooks { public void entityMove(final Entity entity, final long oldSection, final long newSection); - public boolean screenEntity(final ServerLevel world, final Entity entity, final boolean fromDisk, final boolean event); - public boolean configFixMC224294(); public boolean configAutoConfigSendDistance(); diff --git a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java deleted file mode 100644 index 58a99bc38e..0000000000 --- a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java +++ /dev/null @@ -1,288 +0,0 @@ -package ca.spottedleaf.moonrise.common.util; - -import ca.spottedleaf.concurrentutil.util.Priority; -import ca.spottedleaf.moonrise.common.PlatformHooks; -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.world.entity.Entity; -import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.LevelChunk; -import net.minecraft.world.level.chunk.status.ChunkStatus; -import org.slf4j.Logger; -import java.util.List; -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); - } - - 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 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); - } - } - } - }); - } - - static final net.minecraft.server.level.TicketType 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 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 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> 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 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); - }); - } - - public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, - final FullChunkStatus toStatus, final boolean addTicket, - final Priority priority, final Consumer 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 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> 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 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); - }); - } - - public static List getVisibleChunkHolders(final ServerLevel level) { - return new java.util.ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values()); - } - - public static List getUpdatingChunkHolders(final ServerLevel level) { - return new java.util.ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values()); - } - - public static int getVisibleChunkHolderCount(final ServerLevel level) { - return level.chunkSource.chunkMap.visibleChunkMap.size(); - } - - public static int getUpdatingChunkHolderCount(final ServerLevel level) { - return level.chunkSource.chunkMap.updatingChunkMap.size(); - } - - public static boolean hasAnyChunkHolders(final ServerLevel level) { - return getUpdatingChunkHolderCount(level) != 0; - } - - public static boolean screenEntity(final ServerLevel level, final Entity entity, final boolean fromDisk, final boolean event) { - if (!PlatformHooks.get().screenEntity(level, entity, fromDisk, event)) { - return false; - } - return true; - } - - public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) { - - } - - public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { - - } - - public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { - - } - - public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { - - } - - public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { - - } - - public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { - - } - - public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { - - } - - public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { - - } - - public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { - return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ); - } - - public static int getSendViewDistance(final ServerPlayer player) { - return getViewDistance(player); - } - - public static int getViewDistance(final ServerPlayer player) { - final ServerLevel level = player.serverLevel(); - if (level == null) { - return org.bukkit.Bukkit.getViewDistance(); - } - return level.chunkSource.chunkMap.serverViewDistance; - } - - 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; - } - - private ChunkSystem() {} -} diff --git a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystemHooks.java b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystemHooks.java new file mode 100644 index 0000000000..427079ae47 --- /dev/null +++ b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystemHooks.java @@ -0,0 +1,77 @@ +package ca.spottedleaf.moonrise.common.util; + +import ca.spottedleaf.concurrentutil.util.Priority; +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.world.entity.Entity; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import java.util.List; +import java.util.function.Consumer; + +public interface ChunkSystemHooks { + + public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run); + + public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final Priority priority); + + public 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 onComplete); + + public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, + final boolean addTicket, final Priority priority, final Consumer onComplete); + + public void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, + final FullChunkStatus toStatus, final boolean addTicket, + final Priority priority, final Consumer onComplete); + + public List getVisibleChunkHolders(final ServerLevel level); + + public List getUpdatingChunkHolders(final ServerLevel level); + + public int getVisibleChunkHolderCount(final ServerLevel level); + + public int getUpdatingChunkHolderCount(final ServerLevel level); + + public boolean hasAnyChunkHolders(final ServerLevel level); + + public boolean screenEntity(final ServerLevel level, final Entity entity, final boolean fromDisk, final boolean event); + + public void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder); + + public void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder); + + public void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder); + + public void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder); + + public void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder); + + public void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder); + + public void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder); + + public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder); + + public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder); + + public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder); + + public ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ); + + public int getSendViewDistance(final ServerPlayer player); + + public int getViewDistance(final ServerPlayer player); + + public int getTickViewDistance(final ServerPlayer player); + + public void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player); + + public void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player); + + public void updateMaps(final ServerLevel world, final ServerPlayer player); +} diff --git a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java index 12eb3add09..5239993a68 100644 --- a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java +++ b/paper-server/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/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index 61eac5fbbe..9649f41a95 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -216,7 +216,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { public int getTileEntityCount() { // We don't use the full world tile entity list, so we must iterate chunks int size = 0; - for (ChunkHolder playerchunk : ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.world)) { + for (ChunkHolder playerchunk : ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.world)) { net.minecraft.world.level.chunk.LevelChunk chunk = playerchunk.getTickingChunk(); if (chunk == null) { continue; @@ -405,7 +405,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { return chunk instanceof ImposterProtoChunk || chunk instanceof net.minecraft.world.level.chunk.LevelChunk; } final java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); - ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad( + ca.spottedleaf.moonrise.common.PlatformHooks.get().scheduleChunkLoad( this.world, x, z, false, ChunkStatus.EMPTY, true, ca.spottedleaf.concurrentutil.util.Priority.NORMAL, future::complete ); world.getChunkSource().mainThreadProcessor.managedBlock(future::isDone); @@ -420,7 +420,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public Chunk[] getLoadedChunks() { - List chunks = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.world); // Paper + List chunks = ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.world); // Paper return chunks.stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(CraftChunk::new).toArray(Chunk[]::new); } @@ -2447,7 +2447,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void getChunkAtAsync(int x, int z, boolean gen, boolean urgent, @NotNull Consumer cb) { warnUnsafeChunk("getting a faraway chunk async", x, z); // Paper - ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad( + ca.spottedleaf.moonrise.common.PlatformHooks.get().scheduleChunkLoad( this.getHandle(), x, z, gen, ChunkStatus.FULL, true, urgent ? ca.spottedleaf.concurrentutil.util.Priority.HIGHER : ca.spottedleaf.concurrentutil.util.Priority.NORMAL, (ChunkAccess chunk) -> { diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java index 1bdad8088d..039e17ad5d 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -3522,7 +3522,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { @Override public int getViewDistance() { - return ca.spottedleaf.moonrise.common.util.ChunkSystem.getViewDistance(this.getHandle()); + return ca.spottedleaf.moonrise.common.PlatformHooks.get().getViewDistance(this.getHandle()); } @Override @@ -3532,7 +3532,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { @Override public int getSimulationDistance() { - return ca.spottedleaf.moonrise.common.util.ChunkSystem.getTickViewDistance(this.getHandle()); + return ca.spottedleaf.moonrise.common.PlatformHooks.get().getTickViewDistance(this.getHandle()); } @Override @@ -3542,7 +3542,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { @Override public int getSendViewDistance() { - return ca.spottedleaf.moonrise.common.util.ChunkSystem.getSendViewDistance(this.getHandle()); + return ca.spottedleaf.moonrise.common.PlatformHooks.get().getSendViewDistance(this.getHandle()); } @Override -- cgit v1.2.3