diff options
Diffstat (limited to 'paper-server')
13 files changed, 450 insertions, 36653 deletions
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<Long> 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<ChunkAccess> 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<ChunkAccess> 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<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> { ++ try { ++ if (onComplete != null) { ++ onComplete.accept(chunk); ++ } ++ } catch (final Throwable thr) { ++ LOGGER.error("Exception handling chunk load callback", thr); ++ com.destroystokyo.paper.util.SneakyThrow.sneaky(thr); ++ } finally { ++ if (addTicket) { ++ level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); ++ level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); ++ } ++ } ++ }; ++ ++ final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (holder == null || holder.getTicketLevel() > minLevel) { ++ loadCallback.accept(null); ++ return; ++ } ++ ++ final CompletableFuture<ChunkResult<ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap); ++ ++ if (loadFuture.isDone()) { ++ loadCallback.accept(loadFuture.join().orElse(null)); ++ return; ++ } ++ ++ loadFuture.whenCompleteAsync((final ChunkResult<ChunkAccess> 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<LevelChunk> 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<LevelChunk> loadCallback = (final LevelChunk chunk) -> { ++ try { ++ if (onComplete != null) { ++ onComplete.accept(chunk); ++ } ++ } catch (final Throwable thr) { ++ LOGGER.error("Exception handling chunk load callback", thr); ++ com.destroystokyo.paper.util.SneakyThrow.sneaky(thr); ++ } finally { ++ if (addTicket) { ++ level.chunkSource.addTicketAtLevel(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<ChunkResult<LevelChunk>> tickingState; ++ switch (toStatus) { ++ case FULL: { ++ tickingState = holder.getFullChunkFuture(); ++ break; ++ } ++ case BLOCK_TICKING: { ++ tickingState = holder.getTickingChunkFuture(); ++ break; ++ } ++ case ENTITY_TICKING: { ++ tickingState = holder.getEntityTickingChunkFuture(); ++ break; ++ } ++ default: { ++ throw new IllegalStateException("Cannot reach here"); ++ } ++ } ++ ++ if (tickingState.isDone()) { ++ loadCallback.accept(tickingState.join().orElse(null)); ++ return; ++ } ++ ++ tickingState.whenCompleteAsync((final ChunkResult<LevelChunk> 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<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) { ++ return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values()); ++ } ++ ++ @Override ++ public List<ChunkHolder> 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 <[email protected]> -Date: Fri, 14 Jun 2024 11:57:26 -0700 -Subject: [PATCH] Moonrise optimisation patches - -Currently includes: - - Starlight + Chunk System - - Entity tracker optimisations - - Collision optimisations - - Random block ticking optimisations - - Chunk tick iteration optimisations - - Bitstorage optimisations - - Block/Biome Palette read optimisations - - StateHolder (BlockState/FluidState) property access optimisations - - Basic Fluid property read optimisations - - Entity/Level random replacement - -See https://github.com/Tuinity/Moonrise - -diff --git a/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<ServerPlayer, TrackedPlayer[]> players = new Reference2ReferenceOpenHashMap<>(); -+ private final Long2ReferenceOpenHashMap<TrackedChunk> byChunk = new Long2ReferenceOpenHashMap<>(); -+ private final Long2ReferenceOpenHashMap<ReferenceList<ServerPlayer>>[] directByChunk = new Long2ReferenceOpenHashMap[TOTAL_MAP_TYPES]; -+ { -+ for (int i = 0; i < this.directByChunk.length; ++i) { -+ this.directByChunk[i] = new Long2ReferenceOpenHashMap<>(); -+ } -+ } -+ -+ public NearbyPlayers(final ServerLevel world) { -+ this.world = world; -+ } -+ -+ 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<ServerPlayer> getPlayers(final BlockPos pos, final NearbyMapType type) { -+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos)); -+ } -+ -+ public ReferenceList<ServerPlayer> getPlayers(final ChunkPos pos, final NearbyMapType type) { -+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos)); -+ } -+ -+ public ReferenceList<ServerPlayer> getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) { -+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ -+ public ReferenceList<ServerPlayer> 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<ServerPlayer>[] players = new ReferenceList[TOTAL_MAP_TYPES]; -+ private int nonEmptyLists; -+ private long updateCount; -+ -+ public TrackedChunk(final long chunkKey, final NearbyPlayers nearbyPlayers) { -+ this.chunkKey = chunkKey; -+ this.nearbyPlayers = nearbyPlayers; -+ } -+ -+ public boolean isEmpty() { -+ return this.nonEmptyLists == 0; -+ } -+ -+ public long getUpdateCount() { -+ return this.updateCount; -+ } -+ -+ public ReferenceList<ServerPlayer> 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<ServerPlayer> list = this.players[idx]; -+ if (list == null) { -+ ++this.nonEmptyLists; -+ final ReferenceList<ServerPlayer> 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<ServerPlayer> 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<ServerPlayer> { -+ -+ 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<ChunkAccess> onComplete) { -- if (gen) { -- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -- return; -- } -- scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { -- if (chunk == null) { -- if (onComplete != null) { -- onComplete.accept(null); -- } -- } else { -- if (chunk.getPersistedStatus().isOrAfter(toStatus)) { -- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -- } else { -- if (onComplete != null) { -- onComplete.accept(null); -- } -- } -- } -- }); -+ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete); - } - -- static final net.minecraft.server.level.TicketType<Long> CHUNK_LOAD = net.minecraft.server.level.TicketType.create("chunk_load", Long::compareTo); -- -- private static long chunkLoadCounter = 0L; - public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, - final boolean addTicket, final Priority priority, final Consumer<ChunkAccess> onComplete) { -- if (!org.bukkit.Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) { -- scheduleChunkTask(level, chunkX, chunkZ, () -> { -- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -- }, priority); -- return; -- } -- -- final int minLevel = 33 + getDistance(toStatus); -- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; -- final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ); -- -- if (addTicket) { -- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); -- } -- level.chunkSource.runDistanceManagerUpdates(); -- -- final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> { -- try { -- if (onComplete != null) { -- onComplete.accept(chunk); -- } -- } catch (final Throwable thr) { -- LOGGER.error("Exception handling chunk load callback", thr); -- com.destroystokyo.paper.util.SneakyThrow.sneaky(thr); -- } finally { -- if (addTicket) { -- level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); -- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); -- } -- } -- }; -- -- final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -- -- if (holder == null || holder.getTicketLevel() > minLevel) { -- loadCallback.accept(null); -- return; -- } -- -- final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap); -- -- if (loadFuture.isDone()) { -- loadCallback.accept(loadFuture.join().orElse(null)); -- return; -- } -- -- loadFuture.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess> result, final Throwable thr) -> { -- if (thr != null) { -- loadCallback.accept(null); -- return; -- } -- loadCallback.accept(result.orElse(null)); -- }, (final Runnable r) -> { -- scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST); -- }); -+ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); - } - - public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, - final FullChunkStatus toStatus, final boolean addTicket, - final Priority priority, final Consumer<LevelChunk> onComplete) { -- // This method goes unused until the chunk system rewrite -- if (toStatus == FullChunkStatus.INACCESSIBLE) { -- throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); -- } -- -- if (!org.bukkit.Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) { -- scheduleChunkTask(level, chunkX, chunkZ, () -> { -- scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -- }, priority); -- return; -- } -- -- final int minLevel = 33 - (toStatus.ordinal() - 1); -- final int radius = toStatus.ordinal() - 1; -- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; -- final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ); -- -- if (addTicket) { -- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); -- } -- level.chunkSource.runDistanceManagerUpdates(); -- -- final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> { -- try { -- if (onComplete != null) { -- onComplete.accept(chunk); -- } -- } catch (final Throwable thr) { -- LOGGER.error("Exception handling chunk load callback", thr); -- com.destroystokyo.paper.util.SneakyThrow.sneaky(thr); -- } finally { -- if (addTicket) { -- level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); -- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); -- } -- } -- }; -- -- final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -- -- if (holder == null || holder.getTicketLevel() > minLevel) { -- loadCallback.accept(null); -- return; -- } -- -- final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk>> tickingState; -- switch (toStatus) { -- case FULL: { -- tickingState = holder.getFullChunkFuture(); -- break; -- } -- case BLOCK_TICKING: { -- tickingState = holder.getTickingChunkFuture(); -- break; -- } -- case ENTITY_TICKING: { -- tickingState = holder.getEntityTickingChunkFuture(); -- break; -- } -- default: { -- throw new IllegalStateException("Cannot reach here"); -- } -- } -- -- if (tickingState.isDone()) { -- loadCallback.accept(tickingState.join().orElse(null)); -- return; -- } -- -- tickingState.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk> result, final Throwable thr) -> { -- if (thr != null) { -- loadCallback.accept(null); -- return; -- } -- loadCallback.accept(result.orElse(null)); -- }, (final Runnable r) -> { -- scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST); -- }); -+ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); - } - - public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) { -- return new java.util.ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values()); -+ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders(); - } - - public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) { -- return new java.util.ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values()); -+ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders(); - } - - public static int getVisibleChunkHolderCount(final ServerLevel level) { -- return level.chunkSource.chunkMap.visibleChunkMap.size(); -+ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size(); - } - - public static int getUpdatingChunkHolderCount(final ServerLevel level) { -- return level.chunkSource.chunkMap.updatingChunkMap.size(); -+ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size(); - } - - public static boolean hasAnyChunkHolders(final ServerLevel level) { -@@ -233,55 +82,96 @@ public final class ChunkSystem { - } - - public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { -+ // Update progress listener for LevelLoadingScreen -+ final ChunkProgressListener progressListener = level.getChunkSource().chunkMap.progressListener; -+ if (progressListener != null) { -+ ChunkSystem.scheduleChunkTask(level, holder.getPos().x, holder.getPos().z, () -> { -+ progressListener.onStatusChange(holder.getPos(), null); -+ }); -+ } -+ } - -+ public static void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) { -+ ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource()) -+ .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, chunk); - } - - public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { -- -+ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().add( -+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() -+ ); -+ chunk.loadCallback(); - } - - public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { -+ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().remove( -+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() -+ ); -+ chunk.unloadCallback(); -+ } - -+ public static void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder) { -+ ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource()) -+ .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, null); - } - - public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { -- -+ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().add( -+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() -+ ); -+ if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) { -+ chunk.postProcessGeneration((ServerLevel)chunk.getLevel()); -+ } -+ ((ServerLevel)chunk.getLevel()).startTickingChunk(chunk); -+ ((ServerLevel)chunk.getLevel()).getChunkSource().chunkMap.tickingGenerated.incrementAndGet(); -+ ((ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$markChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration - } - - public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { -- -+ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove( -+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() -+ ); -+ ((ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$removeChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration - } - - public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { -- -+ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().add( -+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() -+ ); - } - - public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { -- -+ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().remove( -+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() -+ ); - } - - public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { -- return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ); -+ return null; - } - - public static int getSendViewDistance(final ServerPlayer player) { -- return 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<ShortArrayList> 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<T> { -+ -+ public int moonrise$getId(); -+ -+ public int moonrise$getIdFor(final T value); -+ -+ public T moonrise$getById(final int id); -+ -+ public void moonrise$setById(final T[] values); -+} -diff --git a/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<O, S> { -+ -+ private final Int2ObjectOpenHashMap<Indexer> propertyToIndexer; -+ private S[] lookup; -+ private final Collection<Property<?>> properties; -+ -+ public ZeroCollidingReferenceStateTable(final Collection<Property<?>> properties) { -+ this.propertyToIndexer = new Int2ObjectOpenHashMap<>(properties.size()); -+ this.properties = new ReferenceArrayList<>(properties); -+ -+ final List<Property<?>> sortedProperties = new ArrayList<>(properties); -+ -+ // important that each table sees the same property order given the same _set_ of properties, -+ // as each table will calculate the index for the block state -+ sortedProperties.sort((final Property<?> p1, final Property<?> p2) -> { -+ return Integer.compare( -+ ((PropertyAccess<?>)p1).moonrise$getId(), -+ ((PropertyAccess<?>)p2).moonrise$getId() -+ ); -+ }); -+ -+ int currentMultiple = 1; -+ for (final Property<?> property : sortedProperties) { -+ final int totalValues = property.getPossibleValues().size(); -+ -+ this.propertyToIndexer.put( -+ ((PropertyAccess<?>)property).moonrise$getId(), -+ new Indexer( -+ totalValues, -+ currentMultiple, -+ IntegerUtil.getUnsignedDivisorMagic((long)currentMultiple, 32), -+ IntegerUtil.getUnsignedDivisorMagic((long)totalValues, 32) -+ ) -+ ); -+ -+ currentMultiple *= totalValues; -+ } -+ } -+ -+ public <T extends Comparable<T>> boolean hasProperty(final Property<T> property) { -+ return this.propertyToIndexer.containsKey(((PropertyAccess<T>)property).moonrise$getId()); -+ } -+ -+ public long getIndex(final StateHolder<O, S> stateHolder) { -+ long ret = 0L; -+ -+ for (final Map.Entry<Property<?>, Comparable<?>> entry : stateHolder.getValues().entrySet()) { -+ final Property<?> property = entry.getKey(); -+ final Comparable<?> value = entry.getValue(); -+ -+ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<?>)property).moonrise$getId()); -+ -+ ret += (((PropertyAccess)property).moonrise$getIdFor(value)) * indexer.multiple; -+ } -+ -+ return ret; -+ } -+ -+ public boolean isLoaded() { -+ return this.lookup != null; -+ } -+ -+ public void loadInTable(final Map<Map<Property<?>, Comparable<?>>, S> universe) { -+ if (this.lookup != null) { -+ throw new IllegalStateException(); -+ } -+ -+ this.lookup = (S[])new StateHolder[universe.size()]; -+ -+ for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : universe.entrySet()) { -+ final S value = entry.getValue(); -+ if (value == null) { -+ continue; -+ } -+ this.lookup[(int)((PropertyAccessStateHolder)(StateHolder<O, S>)value).moonrise$getTableIndex()] = value; -+ } -+ -+ for (final S value : this.lookup) { -+ if (value == null) { -+ throw new IllegalStateException(); -+ } -+ } -+ } -+ -+ public <T extends Comparable<T>> T get(final long index, final Property<T> property) { -+ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<T>)property).moonrise$getId()); -+ if (indexer == null) { -+ return null; -+ } -+ -+ final long divided = (index * indexer.multipleDivMagic) >>> 32; -+ final long modded = (((divided * indexer.modMagic) & 0xFFFFFFFFL) * indexer.totalValues) >>> 32; -+ // equiv to: divided = index / multiple -+ // modded = divided % totalValues -+ -+ return ((PropertyAccess<T>)property).moonrise$getById((int)modded); -+ } -+ -+ public <T extends Comparable<T>> S set(final long index, final Property<T> property, final T with) { -+ final int newValueId = ((PropertyAccess<T>)property).moonrise$getIdFor(with); -+ if (newValueId < 0) { -+ return null; -+ } -+ -+ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<T>)property).moonrise$getId()); -+ if (indexer == null) { -+ return null; -+ } -+ -+ final long divided = (index * indexer.multipleDivMagic) >>> 32; -+ final long modded = (((divided * indexer.modMagic) & 0xFFFFFFFFL) * indexer.totalValues) >>> 32; -+ // equiv to: divided = index / multiple -+ // modded = divided % totalValues -+ -+ // subtract out the old value, add in the new -+ final long newIndex = (((long)newValueId - modded) * indexer.multiple) + index; -+ -+ return this.lookup[(int)newIndex]; -+ } -+ -+ public <T extends Comparable<T>> S trySet(final long index, final Property<T> property, final T with, final S dfl) { -+ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<T>)property).moonrise$getId()); -+ if (indexer == null) { -+ return dfl; -+ } -+ -+ final int newValueId = ((PropertyAccess<T>)property).moonrise$getIdFor(with); -+ if (newValueId < 0) { -+ return null; -+ } -+ -+ final long divided = (index * indexer.multipleDivMagic) >>> 32; -+ final long modded = (((divided * indexer.modMagic) & 0xFFFFFFFFL) * indexer.totalValues) >>> 32; -+ // equiv to: divided = index / multiple -+ // modded = divided % totalValues -+ -+ // subtract out the old value, add in the new -+ final long newIndex = (((long)newValueId - modded) * indexer.multiple) + index; -+ -+ return this.lookup[(int)newIndex]; -+ } -+ -+ public Collection<Property<?>> getProperties() { -+ return Collections.unmodifiableCollection(this.properties); -+ } -+ -+ public Map<Property<?>, Comparable<?>> getMapView(final long stateIndex) { -+ return new MapView(stateIndex); -+ } -+ -+ private static final record Indexer( -+ int totalValues, int multiple, long multipleDivMagic, long modMagic -+ ) {} -+ -+ private class MapView extends AbstractReference2ObjectMap<Property<?>, Comparable<?>> { -+ private final long stateIndex; -+ private EntrySet entrySet; -+ -+ MapView(final long stateIndex) { -+ this.stateIndex = stateIndex; -+ } -+ -+ @Override -+ public boolean containsKey(final Object key) { -+ return key instanceof Property<?> prop && ZeroCollidingReferenceStateTable.this.hasProperty(prop); -+ } -+ -+ @Override -+ public int size() { -+ return ZeroCollidingReferenceStateTable.this.properties.size(); -+ } -+ -+ @Override -+ public ObjectSet<Entry<Property<?>, Comparable<?>>> reference2ObjectEntrySet() { -+ if (this.entrySet == null) -+ this.entrySet = new EntrySet(); -+ return this.entrySet; -+ } -+ -+ @Override -+ public Comparable<?> get(final Object key) { -+ return key instanceof Property<?> prop ? ZeroCollidingReferenceStateTable.this.get(this.stateIndex, prop) : null; -+ } -+ -+ class EntrySet extends AbstractObjectSet<Entry<Property<?>, Comparable<?>>> { -+ @Override -+ public ObjectIterator<Reference2ObjectMap.Entry<Property<?>, Comparable<?>>> iterator() { -+ final Iterator<Property<?>> propIterator = ZeroCollidingReferenceStateTable.this.properties.iterator(); -+ return new ObjectIterator<>() { -+ @Override -+ public boolean hasNext() { -+ return propIterator.hasNext(); -+ } -+ -+ @Override -+ public Entry<Property<?>, Comparable<?>> next() { -+ Property<?> prop = propIterator.next(); -+ return new AbstractReference2ObjectMap.BasicEntry<>(prop, ZeroCollidingReferenceStateTable.this.get(MapView.this.stateIndex, prop)); -+ } -+ }; -+ } -+ -+ @Override -+ public int size() { -+ return ZeroCollidingReferenceStateTable.this.properties.size(); -+ } -+ } -+ } -+} -diff --git a/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. -+ * <p> -+ * Impl notes: -+ * </p> -+ * <li> -+ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means -+ * saves must be scheduled before a chunk is unloaded. -+ * </li> -+ * <li> -+ * Writes may be called concurrently, although only the "later" write will go through. -+ * </li> -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param data Chunk's data -+ * @param type The regionfile type to write to. -+ * -+ * @throws IllegalStateException If the file io thread has shutdown. -+ */ -+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, -+ final RegionFileType type) { -+ MoonriseRegionFileIO.scheduleSave(world, chunkX, chunkZ, data, type, Priority.NORMAL); -+ } -+ -+ /** -+ * Schedules the chunk data to be written asynchronously. -+ * <p> -+ * Impl notes: -+ * </p> -+ * <li> -+ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means -+ * saves must be scheduled before a chunk is unloaded. -+ * </li> -+ * <li> -+ * Writes may be called concurrently, although only the "later" write will go through. -+ * </li> -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param data Chunk's data -+ * @param type The regionfile type to write to. -+ * @param priority The minimum priority to schedule at. -+ * -+ * @throws IllegalStateException If the file io thread has shutdown. -+ */ -+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, -+ final RegionFileType type, final Priority priority) { -+ scheduleSave( -+ world, chunkX, chunkZ, -+ (final BiConsumer<CompoundTag, Throwable> consumer) -> { -+ consumer.accept(data, null); -+ }, null, type, priority -+ ); -+ } -+ -+ /** -+ * Schedules the chunk data to be written asynchronously. -+ * <p> -+ * Impl notes: -+ * </p> -+ * <li> -+ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means -+ * saves must be scheduled before a chunk is unloaded. -+ * </li> -+ * <li> -+ * Writes may be called concurrently, although only the "later" write will go through. -+ * </li> -+ * <li> -+ * The specified write task, if not null, will have its priority controlled by the scheduler. -+ * </li> -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param completable Chunk's pending data -+ * @param writeTask The task responsible for completing the pending chunk data -+ * @param type The regionfile type to write to. -+ * @param priority The minimum priority to schedule at. -+ * -+ * @throws IllegalStateException If the file io thread has shutdown. -+ */ -+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CallbackCompletable<CompoundTag> completable, -+ final PrioritisedExecutor.PrioritisedTask writeTask, final RegionFileType type, final Priority priority) { -+ scheduleSave(world, chunkX, chunkZ, completable::addWaiter, writeTask, type, priority); -+ } -+ -+ /** -+ * Schedules the chunk data to be written asynchronously. -+ * <p> -+ * Impl notes: -+ * </p> -+ * <li> -+ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means -+ * saves must be scheduled before a chunk is unloaded. -+ * </li> -+ * <li> -+ * Writes may be called concurrently, although only the "later" write will go through. -+ * </li> -+ * <li> -+ * The specified write task, if not null, will have its priority controlled by the scheduler. -+ * </li> -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param completable Chunk's pending data -+ * @param writeTask The task responsible for completing the pending chunk data -+ * @param type The regionfile type to write to. -+ * @param priority The minimum priority to schedule at. -+ * -+ * @throws IllegalStateException If the file io thread has shutdown. -+ */ -+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final Completable<CompoundTag> completable, -+ final PrioritisedExecutor.PrioritisedTask writeTask, final RegionFileType type, final Priority priority) { -+ scheduleSave(world, chunkX, chunkZ, completable::whenComplete, writeTask, type, priority); -+ } -+ -+ private static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final Consumer<BiConsumer<CompoundTag, Throwable>> scheduler, -+ final PrioritisedExecutor.PrioritisedTask writeTask, final RegionFileType type, final Priority priority) { -+ final RegionDataController taskController = getControllerFor(world, type); -+ -+ final boolean[] created = new boolean[1]; -+ final ChunkIOTask.InProgressWrite write = new ChunkIOTask.InProgressWrite(writeTask); -+ final ChunkIOTask task = taskController.chunkTasks.compute(CoordinateUtils.getChunkKey(chunkX, chunkZ), -+ (final long keyInMap, final ChunkIOTask taskRunning) -> { -+ if (taskRunning == null || taskRunning.failedWrite) { -+ // no task is scheduled or the previous write failed - meaning we need to overwrite it -+ -+ // create task -+ final ChunkIOTask newTask = new ChunkIOTask( -+ world, taskController, chunkX, chunkZ, priority, new ChunkIOTask.InProgressRead() -+ ); -+ -+ newTask.pushPendingWrite(write); -+ -+ created[0] = true; -+ -+ return newTask; -+ } -+ -+ taskRunning.pushPendingWrite(write); -+ -+ return taskRunning; -+ } -+ ); -+ -+ write.schedule(task, scheduler); -+ -+ if (created[0]) { -+ taskController.startTask(task); -+ task.scheduleWriteCompress(); -+ } else { -+ task.raisePriority(priority); -+ } -+ } -+ -+ /** -+ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call -+ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} -+ * for single load. -+ * <p> -+ * Impl notes: -+ * </p> -+ * <li> -+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may -+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of -+ * data is undefined behaviour, and can cause deadlock. -+ * </li> -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param onComplete Consumer to execute once this task has completed -+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost -+ * of this call. -+ * -+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. -+ * -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) -+ */ -+ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock) { -+ return MoonriseRegionFileIO.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL); -+ } -+ -+ /** -+ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call -+ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} -+ * for single load. -+ * <p> -+ * Impl notes: -+ * </p> -+ * <li> -+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may -+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of -+ * data is undefined behaviour, and can cause deadlock. -+ * </li> -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param onComplete Consumer to execute once this task has completed -+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost -+ * of this call. -+ * @param priority The minimum priority to load the data at. -+ * -+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. -+ * -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) -+ */ -+ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock, -+ final Priority priority) { -+ return MoonriseRegionFileIO.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES); -+ } -+ -+ /** -+ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and -+ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} -+ * for single load. -+ * <p> -+ * Impl notes: -+ * </p> -+ * <li> -+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may -+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of -+ * data is undefined behaviour, and can cause deadlock. -+ * </li> -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param onComplete Consumer to execute once this task has completed -+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost -+ * of this call. -+ * @param types The regionfile type(s) to load. -+ * -+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. -+ * -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) -+ */ -+ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock, -+ final RegionFileType... types) { -+ return MoonriseRegionFileIO.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL, types); -+ } -+ -+ /** -+ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and -+ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} -+ * for single load. -+ * <p> -+ * Impl notes: -+ * </p> -+ * <li> -+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may -+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of -+ * data is undefined behaviour, and can cause deadlock. -+ * </li> -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param onComplete Consumer to execute once this task has completed -+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost -+ * of this call. -+ * @param types The regionfile type(s) to load. -+ * @param priority The minimum priority to load the data at. -+ * -+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. -+ * -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) -+ */ -+ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock, -+ final Priority priority, final RegionFileType... types) { -+ if (types == null) { -+ throw new NullPointerException("Types cannot be null"); -+ } -+ if (types.length == 0) { -+ throw new IllegalArgumentException("Types cannot be empty"); -+ } -+ -+ final RegionFileData ret = new RegionFileData(); -+ -+ final Cancellable[] reads = new CancellableRead[types.length]; -+ final AtomicInteger completions = new AtomicInteger(); -+ final int expectedCompletions = types.length; -+ -+ for (int i = 0; i < expectedCompletions; ++i) { -+ final RegionFileType type = types[i]; -+ reads[i] = MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, -+ (final CompoundTag data, final Throwable throwable) -> { -+ if (throwable != null) { -+ ret.setThrowable(type, throwable); -+ } else { -+ ret.setData(type, data); -+ } -+ -+ if (completions.incrementAndGet() == expectedCompletions) { -+ onComplete.accept(ret); -+ } -+ }, intendingToBlock, priority); -+ } -+ -+ return new CancellableReads(reads); -+ } -+ -+ /** -+ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call -+ * {@code onComplete}. -+ * <p> -+ * Impl notes: -+ * </p> -+ * <li> -+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may -+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of -+ * data is undefined behaviour, and can cause deadlock. -+ * </li> -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param onComplete Consumer to execute once this task has completed -+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost -+ * of this call. -+ * -+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. -+ * -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) -+ */ -+ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, -+ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete, -+ final boolean intendingToBlock) { -+ return MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, Priority.NORMAL); -+ } -+ -+ /** -+ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call -+ * {@code onComplete}. -+ * <p> -+ * Impl notes: -+ * </p> -+ * <li> -+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may -+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of -+ * data is undefined behaviour, and can cause deadlock. -+ * </li> -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param onComplete Consumer to execute once this task has completed -+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost -+ * of this call. -+ * @param priority Minimum priority to load the data at. -+ * -+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. -+ * -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) -+ */ -+ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, -+ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete, -+ final boolean intendingToBlock, final Priority priority) { -+ final RegionDataController taskController = getControllerFor(world, type); -+ -+ final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion(); -+ -+ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ final BiLong1Function<ChunkIOTask, ChunkIOTask> compute = (final long keyInMap, final ChunkIOTask running) -> { -+ if (running == null) { -+ // not scheduled -+ -+ // set up task -+ final ChunkIOTask newTask = new ChunkIOTask( -+ world, taskController, chunkX, chunkZ, priority, new ChunkIOTask.InProgressRead() -+ ); -+ newTask.inProgressRead.addToAsyncWaiters(onComplete); -+ -+ callbackInfo.tasksNeedReadScheduling = true; -+ return newTask; -+ } -+ -+ final ChunkIOTask.InProgressWrite pendingWrite = running.inProgressWrite; -+ -+ if (pendingWrite == null) { -+ // need to add to waiters here, because the regionfile thread will use compute() to lock and check for cancellations -+ if (!running.inProgressRead.addToAsyncWaiters(onComplete)) { -+ callbackInfo.data = running.inProgressRead.value; -+ callbackInfo.throwable = running.inProgressRead.throwable; -+ callbackInfo.completeNow = true; -+ return running; -+ } -+ -+ callbackInfo.read = running.inProgressRead; -+ -+ return running; -+ } -+ -+ // at this stage we have to use the in progress write's data to avoid an order issue -+ -+ if (!pendingWrite.addToAsyncWaiters(onComplete)) { -+ // data is ready now -+ callbackInfo.data = pendingWrite.value; -+ callbackInfo.throwable = pendingWrite.throwable; -+ callbackInfo.completeNow = true; -+ return running; -+ } -+ -+ callbackInfo.write = pendingWrite; -+ -+ return running; -+ }; -+ -+ final ChunkIOTask ret = taskController.chunkTasks.compute(key, compute); -+ -+ // needs to be scheduled -+ if (callbackInfo.tasksNeedReadScheduling) { -+ taskController.startTask(ret); -+ ret.scheduleReadIO(); -+ } else if (callbackInfo.completeNow) { -+ try { -+ onComplete.accept(callbackInfo.data == null ? null : callbackInfo.data.copy(), callbackInfo.throwable); -+ } catch (final Throwable thr) { -+ LOGGER.error("Callback " + ConcurrentUtil.genericToString(onComplete) + " synchronously failed to handle chunk data for task " + ret.toString(), thr); -+ } -+ } else { -+ // we're waiting on a task we didn't schedule, so raise its priority to what we want -+ ret.raisePriority(priority); -+ } -+ -+ return new CancellableRead(onComplete, callbackInfo.read, callbackInfo.write); -+ } -+ -+ private static final class ImmediateCallbackCompletion { -+ -+ private CompoundTag data; -+ private Throwable throwable; -+ private boolean completeNow; -+ private boolean tasksNeedReadScheduling; -+ private ChunkIOTask.InProgressRead read; -+ private ChunkIOTask.InProgressWrite write; -+ -+ } -+ -+ /** -+ * Schedules a load task to be executed asynchronously, and blocks on that task. -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param type Regionfile type -+ * @param priority Minimum priority to load the data at. -+ * -+ * @return The chunk data for the chunk. Note that a {@code null} result means the chunk or regionfile does not exist on disk. -+ * -+ * @throws IOException If the load fails for any reason -+ */ -+ public static CompoundTag loadData(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, -+ final Priority priority) throws IOException { -+ final CompletableFuture<CompoundTag> ret = new CompletableFuture<>(); -+ -+ MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> { -+ if (thr != null) { -+ ret.completeExceptionally(thr); -+ } else { -+ ret.complete(compound); -+ } -+ }, true, priority); -+ -+ try { -+ return ret.join(); -+ } catch (final CompletionException ex) { -+ throw new IOException(ex); -+ } -+ } -+ -+ private static final class CancellableRead implements Cancellable { -+ -+ private BiConsumer<CompoundTag, Throwable> callback; -+ private ChunkIOTask.InProgressRead read; -+ private ChunkIOTask.InProgressWrite write; -+ -+ private CancellableRead(final BiConsumer<CompoundTag, Throwable> callback, -+ final ChunkIOTask.InProgressRead read, -+ final ChunkIOTask.InProgressWrite write) { -+ this.callback = callback; -+ this.read = read; -+ this.write = write; -+ } -+ -+ @Override -+ public boolean cancel() { -+ final BiConsumer<CompoundTag, Throwable> callback = this.callback; -+ final ChunkIOTask.InProgressRead read = this.read; -+ final ChunkIOTask.InProgressWrite write = this.write; -+ -+ if (callback == null || (read == null && write == null)) { -+ return false; -+ } -+ -+ this.callback = null; -+ this.read = null; -+ this.write = null; -+ -+ if (read != null) { -+ return read.cancel(callback); -+ } -+ if (write != null) { -+ return write.cancel(callback); -+ } -+ -+ // unreachable -+ throw new InternalError(); -+ } -+ } -+ -+ private static final class CancellableReads implements Cancellable { -+ -+ private Cancellable[] reads; -+ private static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class); -+ -+ private CancellableReads(final Cancellable[] reads) { -+ this.reads = reads; -+ } -+ -+ @Override -+ public boolean cancel() { -+ final Cancellable[] reads = (Cancellable[])READS_HANDLE.getAndSet((CancellableReads)this, (Cancellable[])null); -+ -+ if (reads == null) { -+ return false; -+ } -+ -+ boolean ret = false; -+ -+ for (final Cancellable read : reads) { -+ ret |= read.cancel(); -+ } -+ -+ return ret; -+ } -+ } -+ -+ private static final class ChunkIOTask { -+ -+ private final ServerLevel world; -+ private final RegionDataController regionDataController; -+ private final int chunkX; -+ private final int chunkZ; -+ private Priority priority; -+ private PrioritisedExecutor.PrioritisedTask currentTask; -+ -+ private final InProgressRead inProgressRead; -+ private volatile InProgressWrite inProgressWrite; -+ private final ReferenceOpenHashSet<InProgressWrite> allPendingWrites = new ReferenceOpenHashSet<>(); -+ -+ private RegionDataController.ReadData readData; -+ private RegionDataController.WriteData writeData; -+ private boolean failedWrite; -+ -+ public ChunkIOTask(final ServerLevel world, final RegionDataController regionDataController, -+ final int chunkX, final int chunkZ, final Priority priority, final InProgressRead inProgressRead) { -+ this.world = world; -+ this.regionDataController = regionDataController; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.priority = priority; -+ this.inProgressRead = inProgressRead; -+ } -+ -+ public Priority getPriority() { -+ synchronized (this) { -+ return this.priority; -+ } -+ } -+ -+ // must hold lock on this object -+ private void updatePriority(final Priority priority) { -+ this.priority = priority; -+ if (this.currentTask != null) { -+ this.currentTask.setPriority(priority); -+ } -+ for (final InProgressWrite write : this.allPendingWrites) { -+ if (write.writeTask != null) { -+ write.writeTask.setPriority(priority); -+ } -+ } -+ } -+ -+ public boolean setPriority(final Priority priority) { -+ synchronized (this) { -+ if (this.priority == priority) { -+ return false; -+ } -+ -+ this.updatePriority(priority); -+ -+ return true; -+ } -+ } -+ -+ public boolean raisePriority(final Priority priority) { -+ synchronized (this) { -+ if (this.priority.isHigherOrEqualPriority(priority)) { -+ return false; -+ } -+ -+ this.updatePriority(priority); -+ -+ return true; -+ } -+ } -+ -+ public boolean lowerPriority(final Priority priority) { -+ synchronized (this) { -+ if (this.priority.isLowerOrEqualPriority(priority)) { -+ return false; -+ } -+ -+ this.updatePriority(priority); -+ -+ return true; -+ } -+ } -+ -+ private void pushPendingWrite(final InProgressWrite write) { -+ this.inProgressWrite = write; -+ synchronized (this) { -+ this.allPendingWrites.add(write); -+ if (write.writeTask != null) { -+ write.writeTask.setPriority(this.priority); -+ } -+ } -+ } -+ -+ private void pendingWriteComplete(final InProgressWrite write) { -+ synchronized (this) { -+ this.allPendingWrites.remove(write); -+ } -+ } -+ -+ public void scheduleReadIO() { -+ final PrioritisedExecutor.PrioritisedTask task; -+ synchronized (this) { -+ task = this.regionDataController.ioScheduler.createTask(this.chunkX, this.chunkZ, this::performReadIO, this.priority); -+ this.currentTask = task; -+ } -+ task.queue(); -+ } -+ -+ private void performReadIO() { -+ final InProgressRead read = this.inProgressRead; -+ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ); -+ -+ final boolean[] canRead = new boolean[] { true }; -+ -+ if (read.hasNoWaiters()) { -+ // cancelled read? go to task controller to confirm -+ final ChunkIOTask inMap = this.regionDataController.chunkTasks.compute(chunkKey, (final long keyInMap, final ChunkIOTask valueInMap) -> { -+ if (valueInMap == null) { -+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkIOTask.this.toString() + ", report this!"); -+ } -+ if (valueInMap != ChunkIOTask.this) { -+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkIOTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); -+ } -+ -+ if (!read.hasNoWaiters()) { -+ return valueInMap; -+ } else { -+ canRead[0] = false; -+ } -+ -+ if (valueInMap.inProgressWrite != null) { -+ return valueInMap; -+ } -+ -+ return null; -+ }); -+ -+ if (inMap == null) { -+ this.regionDataController.endTask(this); -+ // read is cancelled - and no write pending, so we're done -+ return; -+ } -+ // if there is a write in progress, we don't actually have to worry about waiters gaining new entries - -+ // the readers will just use the in progress write, so the value in canRead is good to use without -+ // further synchronisation. -+ } -+ -+ if (canRead[0]) { -+ RegionDataController.ReadData readData = null; -+ Throwable throwable = null; -+ -+ try { -+ readData = this.regionDataController.readData(this.chunkX, this.chunkZ); -+ } catch (final Throwable thr) { -+ throwable = thr; -+ LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr); -+ } -+ -+ if (throwable != null) { -+ this.finishRead(null, throwable); -+ } else { -+ switch (readData.result()) { -+ case NO_DATA: -+ case SYNC_READ: { -+ this.finishRead(readData.syncRead(), null); -+ break; -+ } -+ case HAS_DATA: { -+ this.readData = readData; -+ this.scheduleReadDecompress(); -+ // read will handle write scheduling -+ return; -+ } -+ default: { -+ throw new IllegalStateException("Unknown state: " + readData.result()); -+ } -+ } -+ } -+ } -+ -+ if (!this.tryAbortWrite()) { -+ this.scheduleWriteCompress(); -+ } -+ } -+ -+ private void scheduleReadDecompress() { -+ final PrioritisedExecutor.PrioritisedTask task; -+ synchronized (this) { -+ task = this.regionDataController.compressionExecutor.createTask(this::performReadDecompress, this.priority); -+ this.currentTask = task; -+ } -+ task.queue(); -+ } -+ -+ private void performReadDecompress() { -+ final RegionDataController.ReadData readData = this.readData; -+ this.readData = null; -+ -+ CompoundTag compoundTag = null; -+ Throwable throwable = null; -+ -+ try { -+ compoundTag = this.regionDataController.finishRead(this.chunkX, this.chunkZ, readData); -+ } catch (final Throwable thr) { -+ throwable = thr; -+ LOGGER.error("Failed to decompress chunk data for task: " + this.toString(), thr); -+ } -+ -+ if (compoundTag == null) { -+ // need to re-try from the start -+ this.scheduleReadIO(); -+ return; -+ } -+ -+ this.finishRead(compoundTag, throwable); -+ if (!this.tryAbortWrite()) { -+ this.scheduleWriteCompress(); -+ } -+ } -+ -+ private void finishRead(final CompoundTag compoundTag, final Throwable throwable) { -+ this.inProgressRead.complete(this, compoundTag, throwable); -+ } -+ -+ public void scheduleWriteCompress() { -+ final InProgressWrite inProgressWrite = this.inProgressWrite; -+ -+ final PrioritisedExecutor.PrioritisedTask task; -+ synchronized (this) { -+ task = this.regionDataController.compressionExecutor.createTask(() -> { -+ ChunkIOTask.this.performWriteCompress(inProgressWrite); -+ }, this.priority); -+ this.currentTask = task; -+ } -+ -+ inProgressWrite.addToWaiters(this, (final CompoundTag data, final Throwable throwable) -> { -+ task.queue(); -+ }); -+ } -+ -+ private boolean tryAbortWrite() { -+ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ); -+ if (this.inProgressWrite == null) { -+ final ChunkIOTask inMap = this.regionDataController.chunkTasks.compute(chunkKey, (final long keyInMap, final ChunkIOTask valueInMap) -> { -+ if (valueInMap == null) { -+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkIOTask.this.toString() + ", report this!"); -+ } -+ if (valueInMap != ChunkIOTask.this) { -+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkIOTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); -+ } -+ -+ if (valueInMap.inProgressWrite != null) { -+ return valueInMap; -+ } -+ -+ return null; -+ }); -+ -+ if (inMap == null) { -+ this.regionDataController.endTask(this); -+ return true; // set the task value to null, indicating we're done -+ } // else: inProgressWrite changed, so now we have something to write -+ } -+ -+ return false; -+ } -+ -+ private void performWriteCompress(final InProgressWrite inProgressWrite) { -+ final CompoundTag write = inProgressWrite.value; -+ if (!inProgressWrite.isComplete()) { -+ throw new IllegalStateException("Should be writable"); -+ } -+ -+ RegionDataController.WriteData writeData = null; -+ boolean failedWrite = false; -+ -+ try { -+ writeData = this.regionDataController.startWrite(this.chunkX, this.chunkZ, write); -+ } catch (final Throwable thr) { -+ failedWrite = thr instanceof IOException; -+ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr); -+ } -+ -+ if (writeData == null) { -+ // null if a throwable was encountered -+ -+ // we cannot continue to the I/O stage here, so try to complete -+ -+ if (this.tryCompleteWrite(inProgressWrite, failedWrite)) { -+ return; -+ } else { -+ // fetch new data and try again -+ this.scheduleWriteCompress(); -+ return; -+ } -+ } else { -+ // writeData != null && !failedWrite -+ // we can continue to I/O stage -+ this.writeData = writeData; -+ this.scheduleWriteIO(inProgressWrite); -+ return; -+ } -+ } -+ -+ private void scheduleWriteIO(final InProgressWrite inProgressWrite) { -+ final PrioritisedExecutor.PrioritisedTask task; -+ synchronized (this) { -+ task = this.regionDataController.ioScheduler.createTask(this.chunkX, this.chunkZ, () -> { -+ ChunkIOTask.this.runWriteIO(inProgressWrite); -+ }, this.priority); -+ this.currentTask = task; -+ } -+ task.queue(); -+ } -+ -+ private void runWriteIO(final InProgressWrite inProgressWrite) { -+ RegionDataController.WriteData writeData = this.writeData; -+ this.writeData = null; -+ -+ boolean failedWrite = false; -+ -+ try { -+ this.regionDataController.finishWrite(this.chunkX, this.chunkZ, writeData); -+ } catch (final Throwable thr) { -+ failedWrite = thr instanceof IOException; -+ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr); -+ } -+ -+ if (!this.tryCompleteWrite(inProgressWrite, failedWrite)) { -+ // fetch new data and try again -+ this.scheduleWriteCompress(); -+ } -+ return; -+ } -+ -+ private boolean tryCompleteWrite(final InProgressWrite written, final boolean failedWrite) { -+ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ); -+ -+ final boolean[] done = new boolean[] { false }; -+ -+ this.regionDataController.chunkTasks.compute(chunkKey, (final long keyInMap, final ChunkIOTask valueInMap) -> { -+ if (valueInMap == null) { -+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkIOTask.this.toString() + ", report this!"); -+ } -+ if (valueInMap != ChunkIOTask.this) { -+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkIOTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); -+ } -+ if (valueInMap.inProgressWrite == written) { -+ valueInMap.failedWrite = failedWrite; -+ done[0] = true; -+ // keep the data in map if we failed the write so we can try to prevent data loss -+ return failedWrite ? valueInMap : null; -+ } -+ // different data than expected, means we need to retry write -+ return valueInMap; -+ }); -+ -+ if (done[0]) { -+ this.regionDataController.endTask(this); -+ return true; -+ } -+ return false; -+ } -+ -+ @Override -+ public String toString() { -+ return "Task for world: '" + WorldUtil.getWorldName(this.world) + "' at (" + this.chunkX + "," -+ + this.chunkZ + ") type: " + this.regionDataController.type.name() + ", hash: " + this.hashCode(); -+ } -+ -+ private static final class InProgressRead { -+ -+ private static final Logger LOGGER = LoggerFactory.getLogger(InProgressRead.class); -+ -+ private CompoundTag value; -+ private Throwable throwable; -+ private final MultiThreadedQueue<BiConsumer<CompoundTag, Throwable>> callbacks = new MultiThreadedQueue<>(); -+ -+ public boolean hasNoWaiters() { -+ return this.callbacks.isEmpty(); -+ } -+ -+ public boolean addToAsyncWaiters(final BiConsumer<CompoundTag, Throwable> callback) { -+ return this.callbacks.add(callback); -+ } -+ -+ public boolean cancel(final BiConsumer<CompoundTag, Throwable> callback) { -+ return this.callbacks.remove(callback); -+ } -+ -+ public void complete(final ChunkIOTask task, final CompoundTag value, final Throwable throwable) { -+ this.value = value; -+ this.throwable = throwable; -+ -+ BiConsumer<CompoundTag, Throwable> consumer; -+ while ((consumer = this.callbacks.pollOrBlockAdds()) != null) { -+ try { -+ consumer.accept(value == null ? null : value.copy(), throwable); -+ } catch (final Throwable thr) { -+ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data (read) for task " + task.toString(), thr); -+ } -+ } -+ } -+ } -+ -+ private static final class InProgressWrite { -+ -+ private static final Logger LOGGER = LoggerFactory.getLogger(InProgressWrite.class); -+ -+ private CompoundTag value; -+ private Throwable throwable; -+ private volatile boolean complete; -+ private final MultiThreadedQueue<BiConsumer<CompoundTag, Throwable>> callbacks = new MultiThreadedQueue<>(); -+ -+ private final PrioritisedExecutor.PrioritisedTask writeTask; -+ -+ public InProgressWrite(final PrioritisedExecutor.PrioritisedTask writeTask) { -+ this.writeTask = writeTask; -+ } -+ -+ public boolean isComplete() { -+ return this.complete; -+ } -+ -+ public void schedule(final ChunkIOTask task, final Consumer<BiConsumer<CompoundTag, Throwable>> scheduler) { -+ scheduler.accept((final CompoundTag data, final Throwable throwable) -> { -+ InProgressWrite.this.complete(task, data, throwable); -+ }); -+ } -+ -+ public boolean addToAsyncWaiters(final BiConsumer<CompoundTag, Throwable> callback) { -+ return this.callbacks.add(callback); -+ } -+ -+ public void addToWaiters(final ChunkIOTask task, final BiConsumer<CompoundTag, Throwable> consumer) { -+ if (!this.callbacks.add(consumer)) { -+ this.syncAccept(task, consumer, this.value, this.throwable); -+ } -+ } -+ -+ private void syncAccept(final ChunkIOTask task, final BiConsumer<CompoundTag, Throwable> consumer, final CompoundTag value, final Throwable throwable) { -+ try { -+ consumer.accept(value == null ? null : value.copy(), throwable); -+ } catch (final Throwable thr) { -+ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data (write) for task " + task.toString(), thr); -+ } -+ } -+ -+ public void complete(final ChunkIOTask task, final CompoundTag value, final Throwable throwable) { -+ this.value = value; -+ this.throwable = throwable; -+ this.complete = true; -+ -+ task.pendingWriteComplete(this); -+ -+ BiConsumer<CompoundTag, Throwable> consumer; -+ while ((consumer = this.callbacks.pollOrBlockAdds()) != null) { -+ this.syncAccept(task, consumer, value, throwable); -+ } -+ } -+ -+ public boolean cancel(final BiConsumer<CompoundTag, Throwable> callback) { -+ return this.callbacks.remove(callback); -+ } -+ } -+ } -+ -+ public static abstract class RegionDataController { -+ -+ public final RegionFileType type; -+ private final PrioritisedExecutor compressionExecutor; -+ private final IOScheduler ioScheduler; -+ private final ConcurrentLong2ReferenceChainedHashTable<ChunkIOTask> chunkTasks = new ConcurrentLong2ReferenceChainedHashTable<>(); -+ -+ private final AtomicLong inProgressTasks = new AtomicLong(); -+ -+ public RegionDataController(final RegionFileType type, final PrioritisedExecutor ioExecutor, -+ final PrioritisedExecutor compressionExecutor) { -+ this.type = type; -+ this.compressionExecutor = compressionExecutor; -+ this.ioScheduler = new IOScheduler(ioExecutor); -+ } -+ -+ final void startTask(final ChunkIOTask task) { -+ this.inProgressTasks.getAndIncrement(); -+ } -+ -+ final void endTask(final ChunkIOTask task) { -+ this.inProgressTasks.getAndDecrement(); -+ } -+ -+ public boolean hasTasks() { -+ return this.inProgressTasks.get() != 0L; -+ } -+ -+ public long getTotalWorkingTasks() { -+ return this.inProgressTasks.get(); -+ } -+ -+ public abstract RegionFileStorage getCache(); -+ -+ public static record WriteData(CompoundTag input, WriteResult result, DataOutputStream output, IORunnable write) { -+ public static enum WriteResult { -+ WRITE, -+ DELETE; -+ } -+ } -+ -+ public abstract WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException; -+ -+ public abstract void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException; -+ -+ public static record ReadData(ReadResult result, DataInputStream input, CompoundTag syncRead) { -+ public static enum ReadResult { -+ NO_DATA, -+ HAS_DATA, -+ SYNC_READ; -+ } -+ } -+ -+ public abstract ReadData readData(final int chunkX, final int chunkZ) throws IOException; -+ -+ // if the return value is null, then the caller needs to re-try with a new call to readData() -+ public abstract CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException; -+ -+ public static interface IORunnable { -+ -+ public void run(final RegionFile regionFile) throws IOException; -+ -+ } -+ } -+ -+ private static final class IOScheduler { -+ -+ private final ConcurrentLong2ReferenceChainedHashTable<RegionIOTasks> regionTasks = new ConcurrentLong2ReferenceChainedHashTable<>(); -+ private final PrioritisedExecutor executor; -+ -+ public IOScheduler(final PrioritisedExecutor executor) { -+ this.executor = executor; -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, -+ final Runnable run, final Priority priority) { -+ final PrioritisedExecutor.PrioritisedTask[] ret = new PrioritisedExecutor.PrioritisedTask[1]; -+ final long subOrder = this.executor.generateNextSubOrder(); -+ this.regionTasks.compute(CoordinateUtils.getChunkKey(chunkX >> REGION_FILE_SHIFT, chunkZ >> REGION_FILE_SHIFT), -+ (final long regionKey, final RegionIOTasks existing) -> { -+ final RegionIOTasks res; -+ if (existing != null) { -+ res = existing; -+ } else { -+ res = new RegionIOTasks(regionKey, IOScheduler.this); -+ } -+ -+ ret[0] = res.createTask(run, priority, subOrder); -+ -+ return res; -+ }); -+ -+ return ret[0]; -+ } -+ } -+ -+ private static final class RegionIOTasks implements Runnable { -+ -+ private static final Logger LOGGER = LoggerFactory.getLogger(RegionIOTasks.class); -+ -+ private final PrioritisedTaskQueue queue = new PrioritisedTaskQueue(); -+ private final long regionKey; -+ private final IOScheduler ioScheduler; -+ private long createdTasks; -+ private long executedTasks; -+ -+ private PrioritisedExecutor.PrioritisedTask task; -+ -+ public RegionIOTasks(final long regionKey, final IOScheduler ioScheduler) { -+ this.regionKey = regionKey; -+ this.ioScheduler = ioScheduler; -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createTask(final Runnable run, final Priority priority, -+ final long subOrder) { -+ ++this.createdTasks; -+ return new WrappedTask(this.queue.createTask(run, priority, subOrder)); -+ } -+ -+ private void adjustTaskPriority() { -+ final PrioritisedTaskQueue.PrioritySubOrderPair priority = this.queue.getHighestPrioritySubOrder(); -+ if (this.task == null) { -+ if (priority == null) { -+ return; -+ } -+ this.task = this.ioScheduler.executor.createTask(this, priority.priority(), priority.subOrder()); -+ this.task.queue(); -+ } else { -+ if (priority == null) { -+ throw new IllegalStateException(); -+ } else { -+ this.task.setPriorityAndSubOrder(priority.priority(), priority.subOrder()); -+ } -+ } -+ } -+ -+ @Override -+ public void run() { -+ final Runnable run; -+ synchronized (this) { -+ run = this.queue.pollTask(); -+ } -+ -+ try { -+ run.run(); -+ } finally { -+ synchronized (this) { -+ this.task = null; -+ this.adjustTaskPriority(); -+ } -+ this.ioScheduler.regionTasks.compute(this.regionKey, (final long keyInMap, final RegionIOTasks tasks) -> { -+ if (tasks != RegionIOTasks.this) { -+ throw new IllegalStateException("Region task mismatch"); -+ } -+ ++tasks.executedTasks; -+ if (tasks.createdTasks != tasks.executedTasks) { -+ return tasks; -+ } -+ -+ if (tasks.task != null) { -+ throw new IllegalStateException("Task may not be null when created==executed"); -+ } -+ -+ return null; -+ }); -+ } -+ } -+ -+ private final class WrappedTask implements PrioritisedExecutor.PrioritisedTask { -+ -+ private final PrioritisedExecutor.PrioritisedTask wrapped; -+ -+ public WrappedTask(final PrioritisedExecutor.PrioritisedTask wrap) { -+ this.wrapped = wrap; -+ } -+ -+ @Override -+ public PrioritisedExecutor getExecutor() { -+ return RegionIOTasks.this.ioScheduler.executor; -+ } -+ -+ @Override -+ public boolean queue() { -+ synchronized (RegionIOTasks.this) { -+ if (this.wrapped.queue()) { -+ RegionIOTasks.this.adjustTaskPriority(); -+ return true; -+ } -+ return false; -+ } -+ } -+ -+ @Override -+ public boolean isQueued() { -+ return this.wrapped.isQueued(); -+ } -+ -+ @Override -+ public boolean cancel() { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public boolean execute() { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public Priority getPriority() { -+ return this.wrapped.getPriority(); -+ } -+ -+ @Override -+ public boolean setPriority(final Priority priority) { -+ synchronized (RegionIOTasks.this) { -+ if (this.wrapped.setPriority(priority) && this.wrapped.isQueued()) { -+ RegionIOTasks.this.adjustTaskPriority(); -+ return true; -+ } -+ return false; -+ } -+ } -+ -+ @Override -+ public boolean raisePriority(final Priority priority) { -+ synchronized (RegionIOTasks.this) { -+ if (this.wrapped.raisePriority(priority) && this.wrapped.isQueued()) { -+ RegionIOTasks.this.adjustTaskPriority(); -+ return true; -+ } -+ return false; -+ } -+ } -+ -+ @Override -+ public boolean lowerPriority(final Priority priority) { -+ synchronized (RegionIOTasks.this) { -+ if (this.wrapped.lowerPriority(priority) && this.wrapped.isQueued()) { -+ RegionIOTasks.this.adjustTaskPriority(); -+ return true; -+ } -+ return false; -+ } -+ } -+ -+ @Override -+ public long getSubOrder() { -+ return this.wrapped.getSubOrder(); -+ } -+ -+ @Override -+ public boolean setSubOrder(final long subOrder) { -+ synchronized (RegionIOTasks.this) { -+ if (this.wrapped.setSubOrder(subOrder) && this.wrapped.isQueued()) { -+ RegionIOTasks.this.adjustTaskPriority(); -+ return true; -+ } -+ return false; -+ } -+ } -+ -+ @Override -+ public boolean raiseSubOrder(final long subOrder) { -+ synchronized (RegionIOTasks.this) { -+ if (this.wrapped.raiseSubOrder(subOrder) && this.wrapped.isQueued()) { -+ RegionIOTasks.this.adjustTaskPriority(); -+ return true; -+ } -+ return false; -+ } -+ } -+ -+ @Override -+ public boolean lowerSubOrder(final long subOrder) { -+ synchronized (RegionIOTasks.this) { -+ if (this.wrapped.lowerSubOrder(subOrder) && this.wrapped.isQueued()) { -+ RegionIOTasks.this.adjustTaskPriority(); -+ return true; -+ } -+ return false; -+ } -+ } -+ -+ @Override -+ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) { -+ synchronized (RegionIOTasks.this) { -+ if (this.wrapped.setPriorityAndSubOrder(priority, subOrder) && this.wrapped.isQueued()) { -+ RegionIOTasks.this.adjustTaskPriority(); -+ return true; -+ } -+ return false; -+ } -+ } -+ } -+ } -+} -diff --git a/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<List<ChunkAccess>> onLoad); -+ -+ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, -+ final ChunkStatus chunkStatus, final Priority priority, -+ final Consumer<List<ChunkAccess>> onLoad); -+ -+ public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, -+ final Priority priority, -+ final Consumer<List<ChunkAccess>> onLoad); -+ -+ public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, -+ final ChunkStatus chunkStatus, final Priority priority, -+ final Consumer<List<ChunkAccess>> onLoad); -+ -+ public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder(); -+ -+ public long moonrise$getLastMidTickFailure(); -+ -+ public void moonrise$setLastMidTickFailure(final long time); -+ -+ public NearbyPlayers moonrise$getNearbyPlayers(); -+ -+ public ReferenceList<ServerChunkCache.ChunkAndHolder> moonrise$getLoadedChunks(); -+ -+ public ReferenceList<ServerChunkCache.ChunkAndHolder> moonrise$getTickingChunks(); -+ -+ public ReferenceList<ServerChunkCache.ChunkAndHolder> moonrise$getEntityTickingChunks(); -+} -diff --git a/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<ServerPlayer> 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<Class<? extends Entity>, EntityCollectionBySection> entitiesByClass; -+ private final Reference2ObjectOpenHashMap<EntityType<?>, EntityCollectionBySection> entitiesByType; -+ private final EntityList entities = new EntityList(); -+ -+ public FullChunkStatus status; -+ public final ChunkData chunkData; -+ -+ private boolean isTransient; -+ -+ public boolean isTransient() { -+ return this.isTransient; -+ } -+ -+ public void setTransient(final boolean value) { -+ this.isTransient = value; -+ } -+ -+ public ChunkEntitySlices(final Level world, final int chunkX, final int chunkZ, final FullChunkStatus status, -+ final ChunkData chunkData, final int minSection, final int maxSection) { // inclusive, inclusive -+ this.minSection = minSection; -+ this.maxSection = maxSection; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.world = world; -+ -+ this.allEntities = new EntityCollectionBySection(this); -+ this.hardCollidingEntities = new EntityCollectionBySection(this); -+ this.entitiesByClass = new Reference2ObjectOpenHashMap<>(); -+ this.entitiesByType = new Reference2ObjectOpenHashMap<>(); -+ -+ this.status = status; -+ this.chunkData = chunkData; -+ } -+ -+ public static List<Entity> readEntities(final ServerLevel world, final CompoundTag compoundTag) { -+ // TODO check this and below on update for format changes -+ return EntityType.loadEntitiesRecursive(compoundTag.getList("Entities", 10), world, EntitySpawnReason.LOAD).collect(ImmutableList.toImmutableList()); -+ } -+ -+ // Paper start - rewrite chunk system -+ public static void copyEntities(final CompoundTag from, final CompoundTag into) { -+ if (from == null) { -+ return; -+ } -+ final ListTag entitiesFrom = from.getList("Entities", Tag.TAG_COMPOUND); -+ if (entitiesFrom == null || entitiesFrom.isEmpty()) { -+ return; -+ } -+ -+ final ListTag entitiesInto = into.getList("Entities", Tag.TAG_COMPOUND); -+ into.put("Entities", entitiesInto); // this is in case into doesn't have any entities -+ entitiesInto.addAll(0, entitiesFrom); -+ } -+ -+ public static CompoundTag saveEntityChunk(final List<Entity> entities, final ChunkPos chunkPos, final ServerLevel world) { -+ return saveEntityChunk0(entities, chunkPos, world, false); -+ } -+ -+ public static CompoundTag saveEntityChunk0(final List<Entity> entities, final ChunkPos chunkPos, final ServerLevel world, final boolean force) { -+ if (!force && entities.isEmpty()) { -+ return null; -+ } -+ -+ final ListTag entitiesTag = new ListTag(); -+ for (final Entity entity : PlatformHooks.get().modifySavedEntities(world, chunkPos.x, chunkPos.z, entities)) { -+ CompoundTag compoundTag = new CompoundTag(); -+ if (entity.save(compoundTag)) { -+ entitiesTag.add(compoundTag); -+ } -+ } -+ final CompoundTag ret = NbtUtils.addCurrentDataVersion(new CompoundTag()); -+ ret.put("Entities", entitiesTag); -+ EntityStorage.writeChunkPos(ret, chunkPos); -+ -+ return !force && entitiesTag.isEmpty() ? null : ret; -+ } -+ -+ public CompoundTag save() { -+ final int len = this.entities.size(); -+ if (len == 0) { -+ return null; -+ } -+ -+ final Entity[] rawData = this.entities.getRawData(); -+ final List<Entity> collectedEntities = new ArrayList<>(len); -+ for (int i = 0; i < len; ++i) { -+ final Entity entity = rawData[i]; -+ if (entity.shouldBeSaved()) { -+ collectedEntities.add(entity); -+ } -+ } -+ -+ if (collectedEntities.isEmpty()) { -+ return null; -+ } -+ -+ return saveEntityChunk(collectedEntities, new ChunkPos(this.chunkX, this.chunkZ), (ServerLevel)this.world); -+ } -+ -+ // returns true if this chunk has transient entities remaining -+ public boolean unload() { -+ final int len = this.entities.size(); -+ final Entity[] collectedEntities = Arrays.copyOf(this.entities.getRawData(), len); -+ -+ for (int i = 0; i < len; ++i) { -+ final Entity entity = collectedEntities[i]; -+ if (entity.isRemoved()) { -+ // removed by us below -+ continue; -+ } -+ if (entity.shouldBeSaved()) { -+ PlatformHooks.get().unloadEntity(entity); -+ if (entity.isVehicle()) { -+ // we cannot assume that these entities are contained within this chunk, because entities can -+ // desync - so we need to remove them all -+ for (final Entity passenger : entity.getIndirectPassengers()) { -+ PlatformHooks.get().unloadEntity(passenger); -+ } -+ } -+ } -+ } -+ -+ return this.entities.size() != 0; -+ } -+ -+ public List<Entity> getAllEntities() { -+ final int len = this.entities.size(); -+ if (len == 0) { -+ return new ArrayList<>(); -+ } -+ -+ final Entity[] rawData = this.entities.getRawData(); -+ final List<Entity> collectedEntities = new ArrayList<>(len); -+ for (int i = 0; i < len; ++i) { -+ collectedEntities.add(rawData[i]); -+ } -+ -+ return collectedEntities; -+ } -+ -+ public boolean isEmpty() { -+ return this.entities.size() == 0; -+ } -+ -+ public void mergeInto(final ChunkEntitySlices slices) { -+ final Entity[] entities = this.entities.getRawData(); -+ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { -+ final Entity entity = entities[i]; -+ slices.addEntity(entity, ((ChunkSystemEntity)entity).moonrise$getSectionY()); -+ } -+ } -+ -+ private boolean preventStatusUpdates; -+ public boolean startPreventingStatusUpdates() { -+ final boolean ret = this.preventStatusUpdates; -+ this.preventStatusUpdates = true; -+ return ret; -+ } -+ -+ public boolean isPreventingStatusUpdates() { -+ return this.preventStatusUpdates; -+ } -+ -+ public void stopPreventingStatusUpdates(final boolean prev) { -+ this.preventStatusUpdates = prev; -+ } -+ -+ public void updateStatus(final FullChunkStatus status, final EntityLookup lookup) { -+ this.status = status; -+ -+ final Entity[] entities = this.entities.getRawData(); -+ -+ for (int i = 0, size = this.entities.size(); i < size; ++i) { -+ final Entity entity = entities[i]; -+ -+ final Visibility oldVisibility = EntityLookup.getEntityStatus(entity); -+ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(status); -+ final Visibility newVisibility = EntityLookup.getEntityStatus(entity); -+ -+ lookup.entityStatusChange(entity, this, oldVisibility, newVisibility, false, false, false); -+ } -+ } -+ -+ public boolean addEntity(final Entity entity, final int chunkSection) { -+ if (!this.entities.add(entity)) { -+ return false; -+ } -+ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(this.status); -+ ((ChunkSystemEntity)entity).moonrise$setChunkData(this.chunkData); -+ final int sectionIndex = chunkSection - this.minSection; -+ -+ this.allEntities.addEntity(entity, sectionIndex); -+ -+ if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) { -+ this.hardCollidingEntities.addEntity(entity, sectionIndex); -+ } -+ -+ for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator = -+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next(); -+ -+ if (entry.getKey().isInstance(entity)) { -+ entry.getValue().addEntity(entity, sectionIndex); -+ } -+ } -+ -+ EntityCollectionBySection byType = this.entitiesByType.get(entity.getType()); -+ if (byType != null) { -+ byType.addEntity(entity, sectionIndex); -+ } else { -+ this.entitiesByType.put(entity.getType(), byType = new EntityCollectionBySection(this)); -+ byType.addEntity(entity, sectionIndex); -+ } -+ -+ return true; -+ } -+ -+ public boolean removeEntity(final Entity entity, final int chunkSection) { -+ if (!this.entities.remove(entity)) { -+ return false; -+ } -+ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(null); -+ ((ChunkSystemEntity)entity).moonrise$setChunkData(null); -+ final int sectionIndex = chunkSection - this.minSection; -+ -+ this.allEntities.removeEntity(entity, sectionIndex); -+ -+ if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) { -+ this.hardCollidingEntities.removeEntity(entity, sectionIndex); -+ } -+ -+ for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator = -+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next(); -+ -+ if (entry.getKey().isInstance(entity)) { -+ entry.getValue().removeEntity(entity, sectionIndex); -+ } -+ } -+ -+ final EntityCollectionBySection byType = this.entitiesByType.get(entity.getType()); -+ byType.removeEntity(entity, sectionIndex); -+ -+ return true; -+ } -+ -+ public void getHardCollidingEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) { -+ this.hardCollidingEntities.getEntities(except, box, into, predicate); -+ } -+ -+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) { -+ this.allEntities.getEntities(except, box, into, predicate); -+ } -+ -+ -+ public boolean getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate, -+ final int maxCount) { -+ return this.allEntities.getEntitiesLimited(except, box, into, predicate, maxCount); -+ } -+ -+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into, -+ final Predicate<? super T> predicate) { -+ final EntityCollectionBySection byType = this.entitiesByType.get(type); -+ -+ if (byType != null) { -+ byType.getEntities((Entity)null, box, (List)into, (Predicate) predicate); -+ } -+ } -+ -+ public <T extends Entity> boolean getEntities(final EntityType<?> type, final AABB box, final List<? super T> into, -+ final Predicate<? super T> predicate, final int maxCount) { -+ final EntityCollectionBySection byType = this.entitiesByType.get(type); -+ -+ if (byType != null) { -+ return byType.getEntitiesLimited((Entity)null, box, (List)into, (Predicate)predicate, maxCount); -+ } -+ -+ return false; -+ } -+ -+ protected EntityCollectionBySection initClass(final Class<? extends Entity> clazz) { -+ final EntityCollectionBySection ret = new EntityCollectionBySection(this); -+ -+ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) { -+ final BasicEntityList<Entity> sectionEntities = this.allEntities.entitiesBySection[sectionIndex]; -+ if (sectionEntities == null) { -+ continue; -+ } -+ -+ final Entity[] storage = sectionEntities.storage; -+ -+ for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) { -+ final Entity entity = storage[i]; -+ -+ if (clazz.isInstance(entity)) { -+ ret.addEntity(entity, sectionIndex); -+ } -+ } -+ } -+ -+ return ret; -+ } -+ -+ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into, -+ final Predicate<? super T> predicate) { -+ EntityCollectionBySection collection = this.entitiesByClass.get(clazz); -+ if (collection != null) { -+ collection.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 <T extends Entity> boolean getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into, -+ final Predicate<? super T> predicate, final int maxCount) { -+ EntityCollectionBySection collection = this.entitiesByClass.get(clazz); -+ if (collection != null) { -+ return collection.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<E extends Entity> { -+ -+ private static final Entity[] EMPTY = new Entity[0]; -+ private static final int DEFAULT_CAPACITY = 4; -+ -+ private E[] storage; -+ private int size; -+ -+ public BasicEntityList() { -+ this(0); -+ } -+ -+ public BasicEntityList(final int cap) { -+ this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]); -+ } -+ -+ public boolean isEmpty() { -+ return this.size == 0; -+ } -+ -+ public int size() { -+ return this.size; -+ } -+ -+ private void resize() { -+ if (this.storage == EMPTY) { -+ this.storage = (E[])new Entity[DEFAULT_CAPACITY]; -+ } else { -+ this.storage = Arrays.copyOf(this.storage, this.storage.length * 2); -+ } -+ } -+ -+ public void add(final E entity) { -+ final int idx = this.size++; -+ if (idx >= this.storage.length) { -+ this.resize(); -+ this.storage[idx] = entity; -+ } else { -+ this.storage[idx] = entity; -+ } -+ } -+ -+ public int indexOf(final E entity) { -+ final E[] storage = this.storage; -+ -+ for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) { -+ if (storage[i] == entity) { -+ return i; -+ } -+ } -+ -+ return -1; -+ } -+ -+ public boolean remove(final E entity) { -+ final int idx = this.indexOf(entity); -+ if (idx == -1) { -+ return false; -+ } -+ -+ final int size = --this.size; -+ final E[] storage = this.storage; -+ if (idx != size) { -+ System.arraycopy(storage, idx + 1, storage, idx, size - idx); -+ } -+ -+ storage[size] = null; -+ -+ return true; -+ } -+ -+ public boolean has(final E entity) { -+ return this.indexOf(entity) != -1; -+ } -+ } -+ -+ private static final class EntityCollectionBySection { -+ -+ private final ChunkEntitySlices slices; -+ private final BasicEntityList<Entity>[] entitiesBySection; -+ private int count; -+ -+ public EntityCollectionBySection(final ChunkEntitySlices slices) { -+ this.slices = slices; -+ -+ final int sectionCount = slices.maxSection - slices.minSection + 1; -+ -+ this.entitiesBySection = new BasicEntityList[sectionCount]; -+ } -+ -+ public void addEntity(final Entity entity, final int sectionIndex) { -+ BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex]; -+ -+ if (list != null && list.has(entity)) { -+ return; -+ } -+ -+ if (list == null) { -+ this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>(); -+ } -+ -+ list.add(entity); -+ ++this.count; -+ } -+ -+ public void removeEntity(final Entity entity, final int sectionIndex) { -+ final BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex]; -+ -+ if (list == null || !list.remove(entity)) { -+ return; -+ } -+ -+ --this.count; -+ -+ if (list.isEmpty()) { -+ this.entitiesBySection[sectionIndex] = null; -+ } -+ } -+ -+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) { -+ if (this.count == 0) { -+ return; -+ } -+ -+ final int minSection = this.slices.minSection; -+ final int maxSection = this.slices.maxSection; -+ -+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); -+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); -+ -+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection; -+ -+ for (int section = min; section <= max; ++section) { -+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection]; -+ -+ if (list == null) { -+ continue; -+ } -+ -+ final Entity[] storage = list.storage; -+ -+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { -+ final Entity entity = storage[i]; -+ -+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { -+ continue; -+ } -+ -+ if (predicate != null && !predicate.test(entity)) { -+ continue; -+ } -+ -+ into.add(entity); -+ } -+ } -+ } -+ -+ public boolean getEntitiesLimited(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate, -+ final int maxCount) { -+ if (this.count == 0) { -+ return false; -+ } -+ -+ final int minSection = this.slices.minSection; -+ final int maxSection = this.slices.maxSection; -+ -+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); -+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); -+ -+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection; -+ -+ for (int section = min; section <= max; ++section) { -+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection]; -+ -+ if (list == null) { -+ continue; -+ } -+ -+ final Entity[] storage = list.storage; -+ -+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { -+ final Entity entity = storage[i]; -+ -+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { -+ continue; -+ } -+ -+ if (predicate != null && !predicate.test(entity)) { -+ continue; -+ } -+ -+ into.add(entity); -+ if (into.size() >= maxCount) { -+ return true; -+ } -+ } -+ } -+ -+ return false; -+ } -+ } -+} -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<Entity> { -+ -+ private static final Logger LOGGER = LoggerFactory.getLogger(EntityLookup.class); -+ -+ protected static final int REGION_SHIFT = 5; -+ protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1; -+ protected static final int REGION_SIZE = 1 << REGION_SHIFT; -+ -+ public final Level world; -+ -+ protected final SWMRLong2ObjectHashTable<ChunkSlicesRegion> regions = new SWMRLong2ObjectHashTable<>(128, 0.5f); -+ -+ protected final LevelCallback<Entity> worldCallback; -+ -+ protected final ConcurrentLong2ReferenceChainedHashTable<Entity> entityById = new ConcurrentLong2ReferenceChainedHashTable<>(); -+ protected final ConcurrentHashMap<UUID, Entity> entityByUUID = new ConcurrentHashMap<>(); -+ protected final EntityList accessibleEntities = new EntityList(); -+ -+ public EntityLookup(final Level world, final LevelCallback<Entity> worldCallback) { -+ this.world = world; -+ this.worldCallback = worldCallback; -+ } -+ -+ protected abstract Boolean blockTicketUpdates(); -+ -+ protected abstract void setBlockTicketUpdates(final Boolean value); -+ -+ protected abstract void checkThread(final int chunkX, final int chunkZ, final String reason); -+ -+ protected abstract void checkThread(final Entity entity, final String reason); -+ -+ protected abstract ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk); -+ -+ protected abstract void onEmptySlices(final int chunkX, final int chunkZ); -+ -+ protected abstract void entitySectionChangeCallback( -+ final Entity entity, -+ final int oldSectionX, final int oldSectionY, final int oldSectionZ, -+ final int newSectionX, final int newSectionY, final int newSectionZ -+ ); -+ -+ protected abstract void addEntityCallback(final Entity entity); -+ -+ protected abstract void removeEntityCallback(final Entity entity); -+ -+ protected abstract void entityStartLoaded(final Entity entity); -+ -+ protected abstract void entityEndLoaded(final Entity entity); -+ -+ protected abstract void entityStartTicking(final Entity entity); -+ -+ protected abstract void entityEndTicking(final Entity entity); -+ -+ protected abstract boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event); -+ -+ private static Entity maskNonAccessible(final Entity entity) { -+ if (entity == null) { -+ return null; -+ } -+ final Visibility visibility = EntityLookup.getEntityStatus(entity); -+ return visibility.isAccessible() ? entity : null; -+ } -+ -+ @Override -+ public Entity get(final int id) { -+ return maskNonAccessible(this.entityById.get((long)id)); -+ } -+ -+ @Override -+ public Entity get(final UUID id) { -+ return maskNonAccessible(id == null ? null : this.entityByUUID.get(id)); -+ } -+ -+ public boolean hasEntity(final UUID uuid) { -+ return this.get(uuid) != null; -+ } -+ -+ public String getDebugInfo() { -+ return "count_id:" + this.entityById.size() + ",count_uuid:" + this.entityByUUID.size() + ",count_accessible:" + this.getEntityCount() + ",region_count:" + this.regions.size(); -+ } -+ -+ protected static final class ArrayIterable<T> implements Iterable<T> { -+ -+ private final T[] array; -+ private final int off; -+ private final int length; -+ -+ public ArrayIterable(final T[] array, final int off, final int length) { -+ this.array = array; -+ this.off = off; -+ this.length = length; -+ if (length > array.length) { -+ throw new IllegalArgumentException("Length must be no greater-than the array length"); -+ } -+ } -+ -+ @Override -+ public Iterator<T> iterator() { -+ return new ArrayIterator<>(this.array, this.off, this.length); -+ } -+ -+ protected static final class ArrayIterator<T> implements Iterator<T> { -+ -+ private final T[] array; -+ private int off; -+ private final int length; -+ -+ public ArrayIterator(final T[] array, final int off, final int length) { -+ this.array = array; -+ this.off = off; -+ this.length = length; -+ } -+ -+ @Override -+ public boolean hasNext() { -+ return this.off < this.length; -+ } -+ -+ @Override -+ public T next() { -+ if (this.off >= this.length) { -+ throw new NoSuchElementException(); -+ } -+ return this.array[this.off++]; -+ } -+ -+ @Override -+ public void remove() { -+ throw new UnsupportedOperationException(); -+ } -+ } -+ } -+ -+ @Override -+ public Iterable<Entity> getAll() { -+ synchronized (this.accessibleEntities) { -+ final int len = this.accessibleEntities.size(); -+ final Entity[] cpy = Arrays.copyOf(this.accessibleEntities.getRawData(), len, Entity[].class); -+ -+ Objects.checkFromToIndex(0, len, cpy.length); -+ -+ return new ArrayIterable<>(cpy, 0, len); -+ } -+ } -+ -+ public int getEntityCount() { -+ synchronized (this.accessibleEntities) { -+ return this.accessibleEntities.size(); -+ } -+ } -+ -+ public Entity[] getAllCopy() { -+ synchronized (this.accessibleEntities) { -+ return Arrays.copyOf(this.accessibleEntities.getRawData(), this.accessibleEntities.size(), Entity[].class); -+ } -+ } -+ -+ @Override -+ public <U extends Entity> void get(final EntityTypeTest<Entity, U> filter, final AbortableIterationConsumer<U> action) { -+ for (final Iterator<Entity> iterator = this.entityById.valueIterator(); iterator.hasNext();) { -+ final Entity entity = iterator.next(); -+ final Visibility visibility = EntityLookup.getEntityStatus(entity); -+ if (!visibility.isAccessible()) { -+ continue; -+ } -+ final U casted = filter.tryCast(entity); -+ if (casted != null && action.accept(casted).shouldAbort()) { -+ break; -+ } -+ } -+ } -+ -+ @Override -+ public void get(final AABB box, final Consumer<Entity> action) { -+ List<Entity> entities = new ArrayList<>(); -+ this.getEntities((Entity)null, box, entities, null); -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ action.accept(entities.get(i)); -+ } -+ } -+ -+ @Override -+ public <U extends Entity> void get(final EntityTypeTest<Entity, U> filter, final AABB box, final AbortableIterationConsumer<U> action) { -+ List<Entity> entities = new ArrayList<>(); -+ this.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<Entity> entities, final ChunkPos forChunk) { -+ this.addEntityChunk(entities, forChunk, true); -+ } -+ -+ public void addEntityChunkEntities(final List<Entity> entities, final ChunkPos forChunk) { -+ this.addEntityChunk(entities, forChunk, true); -+ } -+ -+ public void addWorldGenChunkEntities(final List<Entity> entities, final ChunkPos forChunk) { -+ this.addEntityChunk(entities, forChunk, false); -+ } -+ -+ protected void addRecursivelySafe(final Entity root, final boolean fromDisk) { -+ if (!this.addEntity(root, fromDisk, true)) { -+ // possible we are a passenger, and so should dismount from any valid entity in the world -+ root.stopRiding(); -+ return; -+ } -+ for (final Entity passenger : root.getPassengers()) { -+ this.addRecursivelySafe(passenger, fromDisk); -+ } -+ } -+ -+ protected void addEntityChunk(final List<Entity> entities, final ChunkPos forChunk, final boolean fromDisk) { -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ final Entity entity = entities.get(i); -+ if (entity.isPassenger()) { -+ continue; -+ } -+ -+ if (forChunk != null && !entity.chunkPosition().equals(forChunk)) { -+ LOGGER.warn("Root entity " + entity + " is outside of serialized chunk " + forChunk); -+ // can't set removed here, as we may not own the chunk position -+ // skip the entity -+ continue; -+ } -+ -+ final Vec3 rootPosition = entity.position(); -+ -+ // always adjust positions before adding passengers in case plugins access the entity, and so that -+ // they are added to the right entity chunk -+ for (final Entity passenger : entity.getIndirectPassengers()) { -+ if (forChunk != null && !passenger.chunkPosition().equals(forChunk)) { -+ passenger.setPosRaw(rootPosition.x, rootPosition.y, rootPosition.z); -+ } -+ } -+ -+ this.addRecursivelySafe(entity, fromDisk); -+ } -+ } -+ -+ public boolean addNewEntity(final Entity entity) { -+ return this.addNewEntity(entity, true); -+ } -+ -+ public boolean addNewEntity(final Entity entity, final boolean event) { -+ return this.addEntity(entity, false, event); -+ } -+ -+ public static Visibility getEntityStatus(final Entity entity) { -+ if (entity.isAlwaysTicking()) { -+ return Visibility.TICKING; -+ } -+ final FullChunkStatus entityStatus = ((ChunkSystemEntity)entity).moonrise$getChunkStatus(); -+ return Visibility.fromFullChunkStatus(entityStatus == null ? FullChunkStatus.INACCESSIBLE : entityStatus); -+ } -+ -+ protected boolean addEntity(final Entity entity, final boolean fromDisk, final boolean event) { -+ final BlockPos pos = entity.blockPosition(); -+ final int sectionX = pos.getX() >> 4; -+ final int sectionY = Mth.clamp(pos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); -+ final int sectionZ = pos.getZ() >> 4; -+ this.checkThread(sectionX, sectionZ, "Cannot add entity off-main thread"); -+ -+ if (entity.isRemoved()) { -+ LOGGER.warn("Refusing to add removed entity: " + entity); -+ return false; -+ } -+ -+ if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) { -+ LOGGER.warn("Entity " + entity + " is currently prevented from being added/removed to world since it is processing section status updates", new Throwable()); -+ return false; -+ } -+ -+ if (!this.screenEntity(entity, fromDisk, event)) { -+ return false; -+ } -+ -+ Entity currentlyMapped = this.entityById.putIfAbsent((long)entity.getId(), entity); -+ if (currentlyMapped != null) { -+ LOGGER.warn("Entity id already exists: " + entity.getId() + ", mapped to " + currentlyMapped + ", can't add " + entity); -+ return false; -+ } -+ -+ currentlyMapped = this.entityByUUID.putIfAbsent(entity.getUUID(), entity); -+ if (currentlyMapped != null) { -+ // need to remove mapping for id -+ this.entityById.remove((long)entity.getId(), entity); -+ LOGGER.warn("Entity uuid already exists: " + entity.getUUID() + ", mapped to " + currentlyMapped + ", can't add " + entity); -+ return false; -+ } -+ -+ ((ChunkSystemEntity)entity).moonrise$setSectionX(sectionX); -+ ((ChunkSystemEntity)entity).moonrise$setSectionY(sectionY); -+ ((ChunkSystemEntity)entity).moonrise$setSectionZ(sectionZ); -+ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ); -+ if (!slices.addEntity(entity, sectionY)) { -+ LOGGER.warn("Entity " + entity + " added to world '" + WorldUtil.getWorldName(this.world) + "', but was already contained in entity chunk (" + sectionX + "," + sectionZ + ")"); -+ } -+ -+ entity.setLevelCallback(new EntityCallback(entity)); -+ -+ this.addEntityCallback(entity); -+ -+ this.entityStatusChange(entity, slices, Visibility.HIDDEN, getEntityStatus(entity), false, !fromDisk, false); -+ -+ return true; -+ } -+ -+ public boolean canRemoveEntity(final Entity entity) { -+ if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) { -+ return false; -+ } -+ -+ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX(); -+ final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ(); -+ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ); -+ return slices == null || !slices.isPreventingStatusUpdates(); -+ } -+ -+ protected void removeEntity(final Entity entity) { -+ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX(); -+ final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY(); -+ final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ(); -+ this.checkThread(sectionX, sectionZ, "Cannot remove entity off-main"); -+ if (!entity.isRemoved()) { -+ throw new IllegalStateException("Only call Entity#setRemoved to remove an entity"); -+ } -+ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ); -+ // all entities should be in a chunk -+ if (slices == null) { -+ LOGGER.warn("Cannot remove entity " + entity + " from null entity slices (" + sectionX + "," + sectionZ + ")"); -+ } else { -+ if (slices.isPreventingStatusUpdates()) { -+ throw new IllegalStateException("Attempting to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ") that is receiving status updates"); -+ } -+ if (!slices.removeEntity(entity, sectionY)) { -+ LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")"); -+ } -+ } -+ ((ChunkSystemEntity)entity).moonrise$setSectionX(Integer.MIN_VALUE); -+ ((ChunkSystemEntity)entity).moonrise$setSectionY(Integer.MIN_VALUE); -+ ((ChunkSystemEntity)entity).moonrise$setSectionZ(Integer.MIN_VALUE); -+ -+ -+ Entity currentlyMapped; -+ if ((currentlyMapped = this.entityById.remove(entity.getId(), entity)) != entity) { -+ LOGGER.warn("Failed to remove entity " + entity + " by id, current entity mapped: " + currentlyMapped); -+ } -+ -+ Entity[] currentlyMappedArr = new Entity[1]; -+ -+ // need reference equality -+ this.entityByUUID.compute(entity.getUUID(), (final UUID keyInMap, final Entity valueInMap) -> { -+ currentlyMappedArr[0] = valueInMap; -+ if (valueInMap != entity) { -+ return valueInMap; -+ } -+ return null; -+ }); -+ -+ if (currentlyMappedArr[0] != entity) { -+ LOGGER.warn("Failed to remove entity " + entity + " by uuid, current entity mapped: " + currentlyMappedArr[0]); -+ } -+ -+ if (slices != null && slices.isEmpty()) { -+ this.onEmptySlices(sectionX, sectionZ); -+ } -+ } -+ -+ protected ChunkEntitySlices moveEntity(final Entity entity) { -+ // ensure we own the entity -+ this.checkThread(entity, "Cannot move entity off-main"); -+ -+ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX(); -+ final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY(); -+ final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ(); -+ final BlockPos newPos = entity.blockPosition(); -+ final int newSectionX = newPos.getX() >> 4; -+ final int newSectionY = Mth.clamp(newPos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); -+ final int newSectionZ = newPos.getZ() >> 4; -+ -+ if (newSectionX == sectionX && newSectionY == sectionY && newSectionZ == sectionZ) { -+ return null; -+ } -+ -+ // ensure the new section is owned by this tick thread -+ this.checkThread(newSectionX, newSectionZ, "Cannot move entity off-main"); -+ -+ // ensure the old section is owned by this tick thread -+ this.checkThread(sectionX, sectionZ, "Cannot move entity off-main"); -+ -+ final ChunkEntitySlices old = this.getChunk(sectionX, sectionZ); -+ final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ); -+ -+ if (!old.removeEntity(entity, sectionY)) { -+ LOGGER.warn("Could not remove entity " + entity + " from its old chunk section (" + sectionX + "," + sectionY + "," + sectionZ + ") since it was not contained in the section"); -+ } -+ -+ if (!slices.addEntity(entity, newSectionY)) { -+ LOGGER.warn("Could not add entity " + entity + " to its new chunk section (" + newSectionX + "," + newSectionY + "," + newSectionZ + ") as it is already contained in the section"); -+ } -+ -+ ((ChunkSystemEntity)entity).moonrise$setSectionX(newSectionX); -+ ((ChunkSystemEntity)entity).moonrise$setSectionY(newSectionY); -+ ((ChunkSystemEntity)entity).moonrise$setSectionZ(newSectionZ); -+ -+ if (old.isEmpty()) { -+ this.onEmptySlices(sectionX, sectionZ); -+ } -+ -+ this.entitySectionChangeCallback( -+ entity, -+ sectionX, sectionY, sectionZ, -+ newSectionX, newSectionY, newSectionZ -+ ); -+ -+ return slices; -+ } -+ -+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { -+ continue; -+ } -+ -+ chunk.getEntities(except, box, into, predicate); -+ } -+ } -+ } -+ } -+ } -+ -+ public void getHardCollidingEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { -+ continue; -+ } -+ -+ chunk.getHardCollidingEntities(except, box, into, predicate); -+ } -+ } -+ } -+ } -+ } -+ -+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into, -+ final Predicate<? super T> predicate) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { -+ continue; -+ } -+ -+ chunk.getEntities(type, box, (List)into, (Predicate)predicate); -+ } -+ } -+ } -+ } -+ } -+ -+ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into, -+ final Predicate<? super T> predicate) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { -+ continue; -+ } -+ -+ chunk.getEntities(clazz, except, box, into, predicate); -+ } -+ } -+ } -+ } -+ } -+ -+ //////// Limited //////// -+ -+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate, -+ final int maxCount) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { -+ continue; -+ } -+ -+ if (chunk.getEntities(except, box, into, predicate, maxCount)) { -+ return; -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into, -+ final Predicate<? super T> predicate, final int maxCount) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { -+ continue; -+ } -+ -+ if (chunk.getEntities(type, box, (List)into, (Predicate)predicate, maxCount)) { -+ return; -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into, -+ final Predicate<? super T> predicate, final int maxCount) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { -+ continue; -+ } -+ -+ if (chunk.getEntities(clazz, except, box, into, predicate, maxCount)) { -+ return; -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ public void entitySectionLoad(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { -+ this.checkThread(chunkX, chunkZ, "Cannot load in entity section off-main"); -+ synchronized (this) { -+ final ChunkEntitySlices curr = this.getChunk(chunkX, chunkZ); -+ if (curr != null) { -+ this.removeChunk(chunkX, chunkZ); -+ -+ curr.mergeInto(slices); -+ -+ this.addChunk(chunkX, chunkZ, slices); -+ } else { -+ this.addChunk(chunkX, chunkZ, slices); -+ } -+ } -+ } -+ -+ public void entitySectionUnload(final int chunkX, final int chunkZ) { -+ this.checkThread(chunkX, chunkZ, "Cannot unload entity section off-main"); -+ this.removeChunk(chunkX, chunkZ); -+ } -+ -+ public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) { -+ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); -+ if (region == null) { -+ return null; -+ } -+ -+ return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT)); -+ } -+ -+ public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) { -+ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); -+ final ChunkEntitySlices ret; -+ if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) { -+ return this.createEntityChunk(chunkX, chunkZ, true); -+ } -+ -+ return ret; -+ } -+ -+ public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) { -+ final long key = CoordinateUtils.getChunkKey(regionX, regionZ); -+ -+ return this.regions.get(key); -+ } -+ -+ protected synchronized void removeChunk(final int chunkX, final int chunkZ) { -+ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); -+ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); -+ -+ final ChunkSlicesRegion region = this.regions.get(key); -+ final int remaining = region.remove(relIndex); -+ -+ if (remaining == 0) { -+ this.regions.remove(key); -+ } -+ } -+ -+ public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { -+ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); -+ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); -+ -+ ChunkSlicesRegion region = this.regions.get(key); -+ if (region != null) { -+ region.add(relIndex, slices); -+ } else { -+ region = new ChunkSlicesRegion(); -+ region.add(relIndex, slices); -+ this.regions.put(key, region); -+ } -+ } -+ -+ public static final class ChunkSlicesRegion { -+ -+ private final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE]; -+ private int sliceCount; -+ -+ public ChunkEntitySlices get(final int index) { -+ return this.slices[index]; -+ } -+ -+ public int remove(final int index) { -+ final ChunkEntitySlices slices = this.slices[index]; -+ if (slices == null) { -+ throw new IllegalStateException(); -+ } -+ -+ this.slices[index] = null; -+ -+ return --this.sliceCount; -+ } -+ -+ public void add(final int index, final ChunkEntitySlices slices) { -+ final ChunkEntitySlices curr = this.slices[index]; -+ if (curr != null) { -+ throw new IllegalStateException(); -+ } -+ -+ this.slices[index] = slices; -+ -+ ++this.sliceCount; -+ } -+ } -+ -+ protected final class EntityCallback implements EntityInLevelCallback { -+ -+ public final Entity entity; -+ -+ public EntityCallback(final Entity entity) { -+ this.entity = entity; -+ } -+ -+ @Override -+ public void onMove() { -+ final Entity entity = this.entity; -+ final Visibility oldVisibility = getEntityStatus(entity); -+ final ChunkEntitySlices newSlices = EntityLookup.this.moveEntity(this.entity); -+ if (newSlices == null) { -+ // no new section, so didn't change sections -+ return; -+ } -+ -+ final Visibility newVisibility = getEntityStatus(entity); -+ -+ EntityLookup.this.entityStatusChange(entity, newSlices, oldVisibility, newVisibility, true, false, false); -+ } -+ -+ @Override -+ public void onRemove(final Entity.RemovalReason reason) { -+ final Entity entity = this.entity; -+ EntityLookup.this.checkThread(entity, "Cannot remove entity off-main"); -+ final Visibility tickingState = EntityLookup.getEntityStatus(entity); -+ -+ EntityLookup.this.removeEntity(entity); -+ -+ EntityLookup.this.entityStatusChange(entity, null, tickingState, Visibility.HIDDEN, false, false, reason.shouldDestroy()); -+ -+ EntityLookup.this.removeEntityCallback(entity); -+ -+ this.entity.setLevelCallback(NoOpCallback.INSTANCE); -+ } -+ } -+ -+ protected static final class NoOpCallback implements EntityInLevelCallback { -+ -+ public static final NoOpCallback INSTANCE = new NoOpCallback(); -+ -+ @Override -+ public void onMove() {} -+ -+ @Override -+ public void onRemove(final Entity.RemovalReason reason) {} -+ } -+} -diff --git a/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<Entity> worldCallback) { -+ super(world, worldCallback); -+ } -+ -+ @Override -+ protected Boolean blockTicketUpdates() { -+ // not present on client -+ return null; -+ } -+ -+ @Override -+ protected void setBlockTicketUpdates(Boolean value) { -+ // not present on client -+ } -+ -+ @Override -+ protected void checkThread(final int chunkX, final int chunkZ, final String reason) { -+ // TODO implement? -+ } -+ -+ @Override -+ protected void checkThread(final Entity entity, final String reason) { -+ // TODO implement? -+ } -+ -+ @Override -+ protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { -+ final boolean ticking = this.tickingChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ -+ final ChunkEntitySlices ret = new ChunkEntitySlices( -+ this.world, chunkX, chunkZ, -+ ticking ? FullChunkStatus.ENTITY_TICKING : FullChunkStatus.FULL, null, -+ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) -+ ); -+ -+ // note: not handled by superclass -+ this.addChunk(chunkX, chunkZ, ret); -+ -+ return ret; -+ } -+ -+ @Override -+ protected void onEmptySlices(final int chunkX, final int chunkZ) { -+ this.removeChunk(chunkX, chunkZ); -+ } -+ -+ @Override -+ protected void entitySectionChangeCallback(final Entity entity, -+ final int oldSectionX, final int oldSectionY, final int oldSectionZ, -+ final int newSectionX, final int newSectionY, final int newSectionZ) { -+ PlatformHooks.get().entityMove( -+ entity, -+ CoordinateUtils.getChunkSectionKey(oldSectionX, oldSectionY, oldSectionZ), -+ CoordinateUtils.getChunkSectionKey(newSectionX, newSectionY, newSectionZ) -+ ); -+ } -+ -+ @Override -+ protected void addEntityCallback(final Entity entity) { -+ -+ } -+ -+ @Override -+ protected void removeEntityCallback(final Entity entity) { -+ -+ } -+ -+ @Override -+ protected void entityStartLoaded(final Entity entity) { -+ -+ } -+ -+ @Override -+ protected void entityEndLoaded(final Entity entity) { -+ -+ } -+ -+ @Override -+ protected void entityStartTicking(final Entity entity) { -+ -+ } -+ -+ @Override -+ protected void entityEndTicking(final Entity entity) { -+ -+ } -+ -+ @Override -+ protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) { -+ return true; -+ } -+ -+ public void markTicking(final long pos) { -+ if (this.tickingChunks.add(pos)) { -+ final int chunkX = CoordinateUtils.getChunkX(pos); -+ final int chunkZ = CoordinateUtils.getChunkZ(pos); -+ if (this.getChunk(chunkX, chunkZ) != null) { -+ this.chunkStatusChange(chunkX, chunkZ, FullChunkStatus.ENTITY_TICKING); -+ } -+ } -+ } -+ -+ public void markNonTicking(final long pos) { -+ if (this.tickingChunks.remove(pos)) { -+ final int chunkX = CoordinateUtils.getChunkX(pos); -+ final int chunkZ = CoordinateUtils.getChunkZ(pos); -+ if (this.getChunk(chunkX, chunkZ) != null) { -+ this.chunkStatusChange(chunkX, chunkZ, FullChunkStatus.FULL); -+ } -+ } -+ } -+} -diff --git a/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<Entity> { -+ -+ @Override -+ public void onCreated(final Entity entity) {} -+ -+ @Override -+ public void onDestroyed(final Entity entity) {} -+ -+ @Override -+ public void onTickingStart(final Entity entity) {} -+ -+ @Override -+ public void onTickingEnd(final Entity entity) {} -+ -+ @Override -+ public void onTrackingStart(final Entity entity) {} -+ -+ @Override -+ public void onTrackingEnd(final Entity entity) {} -+ -+ @Override -+ public void onSectionChange(final Entity entity) {} -+ } -+} -diff --git a/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<Entity> trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker -+ -+ public ServerEntityLookup(final ServerLevel world, final LevelCallback<Entity> worldCallback) { -+ super(world, worldCallback); -+ this.serverWorld = world; -+ } -+ -+ @Override -+ protected Boolean blockTicketUpdates() { -+ return ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager.blockTicketUpdates(); -+ } -+ -+ @Override -+ protected void setBlockTicketUpdates(final Boolean value) { -+ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager.unblockTicketUpdates(value); -+ } -+ -+ @Override -+ protected void checkThread(final int chunkX, final int chunkZ, final String reason) { -+ TickThread.ensureTickThread(this.serverWorld, chunkX, chunkZ, reason); -+ } -+ -+ @Override -+ protected void checkThread(final Entity entity, final String reason) { -+ TickThread.ensureTickThread(entity, reason); -+ } -+ -+ @Override -+ protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { -+ // loadInEntityChunk will call addChunk for us -+ return ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager -+ .getOrCreateEntityChunk(chunkX, chunkZ, transientChunk); -+ } -+ -+ @Override -+ protected void onEmptySlices(final int chunkX, final int chunkZ) { -+ // entity slices unloading is managed by ticket levels in chunk system -+ } -+ -+ @Override -+ protected void entitySectionChangeCallback(final Entity entity, -+ final int oldSectionX, final int oldSectionY, final int oldSectionZ, -+ final int newSectionX, final int newSectionY, final int newSectionZ) { -+ if (entity instanceof ServerPlayer player) { -+ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().tickPlayer(player); -+ } -+ PlatformHooks.get().entityMove( -+ entity, -+ CoordinateUtils.getChunkSectionKey(oldSectionX, oldSectionY, oldSectionZ), -+ CoordinateUtils.getChunkSectionKey(newSectionX, newSectionY, newSectionZ) -+ ); -+ } -+ -+ @Override -+ protected void addEntityCallback(final Entity entity) { -+ if (entity instanceof ServerPlayer player) { -+ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().addPlayer(player); -+ } -+ } -+ -+ @Override -+ protected void removeEntityCallback(final Entity entity) { -+ if (entity instanceof ServerPlayer player) { -+ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().removePlayer(player); -+ } -+ } -+ -+ @Override -+ protected void entityStartLoaded(final Entity entity) { -+ // Moonrise start - entity tracker -+ this.trackerEntities.add(entity); -+ // Moonrise end - entity tracker -+ } -+ -+ @Override -+ protected void entityEndLoaded(final Entity entity) { -+ // Moonrise start - entity tracker -+ this.trackerEntities.remove(entity); -+ // Moonrise end - entity tracker -+ } -+ -+ @Override -+ protected void entityStartTicking(final Entity entity) { -+ -+ } -+ -+ @Override -+ protected void entityEndTicking(final Entity entity) { -+ -+ } -+ -+ @Override -+ protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) { -+ return ChunkSystem.screenEntity(this.serverWorld, entity, fromDisk, event); -+ } -+} -diff --git a/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<PoiSection> 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<PoiSection> getSectionForVanilla(final int chunkY) { -+ if (chunkY >= this.minSection && chunkY <= this.maxSection) { -+ final PoiSection ret = this.sections[chunkY - this.minSection]; -+ return ret == null ? Optional.empty() : ((ChunkSystemPoiSection)ret).moonrise$asOptional(); -+ } -+ return Optional.empty(); -+ } -+ -+ public boolean isDirty() { -+ return this.isDirty; -+ } -+ -+ public void setDirty(final boolean dirty) { -+ this.isDirty = dirty; -+ } -+ -+ // returns null if empty -+ public CompoundTag save() { -+ final RegistryOps<Tag> registryOps = RegistryOps.create(NbtOps.INSTANCE, this.world.registryAccess()); -+ -+ final CompoundTag ret = new CompoundTag(); -+ final CompoundTag sections = new CompoundTag(); -+ ret.put("Sections", sections); -+ -+ ret.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion()); -+ -+ final ServerLevel world = this.world; -+ final int chunkX = this.chunkX; -+ final int chunkZ = this.chunkZ; -+ -+ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { -+ final PoiSection section = this.sections[sectionY - this.minSection]; -+ if (section == null || ((ChunkSystemPoiSection)section).moonrise$isEmpty()) { -+ continue; -+ } -+ -+ // I do not believe asynchronously converting to CompoundTag is worth the scheduling. -+ final DataResult<Tag> serializedResult = PoiSection.Packed.CODEC.encodeStart(registryOps, section.pack()); -+ final int finalSectionY = sectionY; -+ final Tag serialized = serializedResult.resultOrPartial((final String description) -> { -+ LOGGER.error("Failed to serialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); -+ }).orElse(null); -+ if (serialized == null) { -+ // failed, should be logged from the resultOrPartial -+ continue; -+ } -+ -+ sections.put(Integer.toString(sectionY), serialized); -+ } -+ -+ return sections.isEmpty() ? null : ret; -+ } -+ -+ public static PoiChunk empty(final ServerLevel world, final int chunkX, final int chunkZ) { -+ final PoiChunk ret = new PoiChunk(world, chunkX, chunkZ, WorldUtil.getMinSection(world), WorldUtil.getMaxSection(world)); -+ ret.loaded = true; -+ return ret; -+ } -+ -+ public static PoiChunk parse(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data) { -+ final PoiChunk ret = empty(world, chunkX, chunkZ); -+ -+ final RegistryOps<Tag> registryOps = RegistryOps.create(NbtOps.INSTANCE, world.registryAccess()); -+ -+ final CompoundTag sections = data.getCompound("Sections"); -+ -+ if (sections.isEmpty()) { -+ // nothing to parse -+ return ret; -+ } -+ -+ final PoiManager poiManager = world.getPoiManager(); -+ -+ boolean readAnything = false; -+ -+ for (int sectionY = ret.minSection; sectionY <= ret.maxSection; ++sectionY) { -+ final String key = Integer.toString(sectionY); -+ if (!sections.contains(key)) { -+ continue; -+ } -+ -+ final CompoundTag section = sections.getCompound(key); -+ final DataResult<PoiSection.Packed> deserializeResult = PoiSection.Packed.CODEC.parse(registryOps, section); -+ final int finalSectionY = sectionY; -+ final PoiSection.Packed packed = deserializeResult.resultOrPartial((final String description) -> { -+ LOGGER.error("Failed to deserialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); -+ }).orElse(null); -+ -+ final long coordinateKey = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ); -+ final PoiSection deserialized = packed == null ? null : packed.unpack(() -> { -+ poiManager.setDirty(coordinateKey); -+ }); -+ -+ if (deserialized == null || ((ChunkSystemPoiSection)deserialized).moonrise$isEmpty()) { -+ // completely empty, no point in storing this -+ continue; -+ } -+ -+ readAnything = true; -+ ret.sections[sectionY - ret.minSection] = deserialized; -+ } -+ -+ ret.loaded = !readAnything; // Set loaded to false if we read anything to ensure proper callbacks to PoiManager are made on #load -+ -+ return ret; -+ } -+} -diff --git a/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<Long> PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo); -+ public static final TicketType<Long> PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 5 * 20); -+ -+ public static final int MIN_VIEW_DISTANCE = 2; -+ public static final int MAX_VIEW_DISTANCE = 32; -+ -+ public static final int GENERATED_TICKET_LEVEL = ChunkHolderManager.FULL_LOADED_TICKET_LEVEL; -+ public static final int LOADED_TICKET_LEVEL = ChunkTaskScheduler.getTicketLevel(ChunkStatus.EMPTY); -+ public static final int TICK_TICKET_LEVEL = ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL; -+ -+ public static final class ViewDistanceHolder { -+ -+ private volatile ViewDistances viewDistances; -+ private static final VarHandle VIEW_DISTANCES_HANDLE = ConcurrentUtil.getVarHandle(ViewDistanceHolder.class, "viewDistances", ViewDistances.class); -+ -+ public ViewDistanceHolder() { -+ VIEW_DISTANCES_HANDLE.setVolatile(this, new ViewDistances(-1, -1, -1)); -+ } -+ -+ public ViewDistances getViewDistances() { -+ return (ViewDistances)VIEW_DISTANCES_HANDLE.getVolatile(this); -+ } -+ -+ public ViewDistances compareAndExchangeViewDistance(final ViewDistances expect, final ViewDistances update) { -+ return (ViewDistances)VIEW_DISTANCES_HANDLE.compareAndExchange(this, expect, update); -+ } -+ -+ public void updateViewDistance(final Function<ViewDistances, ViewDistances> update) { -+ int failures = 0; -+ for (ViewDistances curr = this.getViewDistances();;) { -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ -+ if (curr == (curr = this.compareAndExchangeViewDistance(curr, update.apply(curr)))) { -+ return; -+ } -+ ++failures; -+ } -+ } -+ -+ public void setTickViewDistance(final int distance) { -+ this.updateViewDistance((final ViewDistances param) -> { -+ return param.setTickViewDistance(distance); -+ }); -+ } -+ -+ public void setLoadViewDistance(final int distance) { -+ this.updateViewDistance((final ViewDistances param) -> { -+ return param.setLoadViewDistance(distance); -+ }); -+ } -+ -+ public void setSendViewDistance(final int distance) { -+ this.updateViewDistance((final ViewDistances param) -> { -+ return param.setSendViewDistance(distance); -+ }); -+ } -+ -+ public JsonObject toJson() { -+ return this.getViewDistances().toJson(); -+ } -+ } -+ -+ public static final record ViewDistances( -+ int tickViewDistance, -+ int loadViewDistance, -+ int sendViewDistance -+ ) { -+ public ViewDistances setTickViewDistance(final int distance) { -+ 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<ChunkHolderManager.TicketOperation<?, ?>> delayedTicketOps = new ArrayDeque<>(); -+ private final LongOpenHashSet sentChunks = new LongOpenHashSet(); -+ -+ private static final byte CHUNK_TICKET_STAGE_NONE = 0; -+ private static final byte CHUNK_TICKET_STAGE_LOADING = 1; -+ private static final byte CHUNK_TICKET_STAGE_LOADED = 2; -+ private static final byte CHUNK_TICKET_STAGE_GENERATING = 3; -+ private static final byte CHUNK_TICKET_STAGE_GENERATED = 4; -+ private static final byte CHUNK_TICKET_STAGE_TICK = 5; -+ private static final int[] TICKET_STAGE_TO_LEVEL = new int[] { -+ ChunkHolderManager.MAX_TICKET_LEVEL + 1, -+ LOADED_TICKET_LEVEL, -+ LOADED_TICKET_LEVEL, -+ GENERATED_TICKET_LEVEL, -+ GENERATED_TICKET_LEVEL, -+ TICK_TICKET_LEVEL -+ }; -+ private final Long2ByteOpenHashMap chunkTicketStage = new Long2ByteOpenHashMap(); -+ { -+ this.chunkTicketStage.defaultReturnValue(CHUNK_TICKET_STAGE_NONE); -+ } -+ -+ // rate limiting -+ private static final long ALLOCATION_GRANULARITY = TimeUnit.SECONDS.toNanos(1L); -+ private final AllocatingRateLimiter chunkSendLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY); -+ private final AllocatingRateLimiter chunkLoadTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY); -+ private final AllocatingRateLimiter chunkGenerateTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY); -+ -+ // queues -+ private final LongComparator CLOSEST_MANHATTAN_DIST = (final long c1, final long c2) -> { -+ final int c1x = CoordinateUtils.getChunkX(c1); -+ final int c1z = CoordinateUtils.getChunkZ(c1); -+ -+ final int c2x = CoordinateUtils.getChunkX(c2); -+ final int c2z = CoordinateUtils.getChunkZ(c2); -+ -+ final int centerX = PlayerChunkLoaderData.this.lastChunkX; -+ final int centerZ = PlayerChunkLoaderData.this.lastChunkZ; -+ -+ return Integer.compare( -+ Math.abs(c1x - centerX) + Math.abs(c1z - centerZ), -+ Math.abs(c2x - centerX) + Math.abs(c2z - centerZ) -+ ); -+ }; -+ private final LongHeapPriorityQueue sendQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); -+ private final LongHeapPriorityQueue tickingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); -+ private final LongHeapPriorityQueue generatingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); -+ private final LongHeapPriorityQueue genQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); -+ private final LongHeapPriorityQueue loadingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); -+ private final LongHeapPriorityQueue loadQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); -+ -+ private volatile boolean removed; -+ -+ public PlayerChunkLoaderData(final ServerLevel world, final ServerPlayer player) { -+ this.world = world; -+ this.player = player; -+ } -+ -+ private void flushDelayedTicketOps() { -+ if (this.delayedTicketOps.isEmpty()) { -+ return; -+ } -+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.performTicketUpdates(this.delayedTicketOps); -+ this.delayedTicketOps.clear(); -+ } -+ -+ private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation<?, ?> op) { -+ this.delayedTicketOps.addLast(op); -+ } -+ -+ private void sendChunk(final int chunkX, final int chunkZ) { -+ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { -+ ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager -+ .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$addReceivedChunk(this.player); -+ -+ final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ); -+ -+ PlatformHooks.get().onChunkWatch(this.world, chunk, this.player); -+ PlayerChunkSender.sendChunk(this.player.connection, this.world, chunk); -+ return; -+ } -+ throw new IllegalStateException(); -+ } -+ -+ private void sendUnloadChunk(final int chunkX, final int chunkZ) { -+ if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { -+ return; -+ } -+ this.sendUnloadChunkRaw(chunkX, chunkZ); -+ } -+ -+ private void sendUnloadChunkRaw(final int chunkX, final int chunkZ) { -+ PlatformHooks.get().onChunkUnWatch(this.world, new ChunkPos(chunkX, chunkZ), this.player); -+ // Note: Check PlayerChunkSender#dropChunk for other logic -+ // Note: drop isAlive() check so that chunks properly unload client-side when the player dies -+ ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager -+ .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$removeReceivedChunk(this.player); -+ this.player.connection.send(new ClientboundForgetLevelChunkPacket(new ChunkPos(chunkX, chunkZ))); -+ // Paper start - PlayerChunkUnloadEvent -+ if (io.papermc.paper.event.packet.PlayerChunkUnloadEvent.getHandlerList().getRegisteredListeners().length > 0) { -+ new io.papermc.paper.event.packet.PlayerChunkUnloadEvent(player.getBukkitEntity().getWorld().getChunkAt(new ChunkPos(chunkX, chunkZ).longKey), player.getBukkitEntity()).callEvent(); -+ } -+ // Paper end - PlayerChunkUnloadEvent -+ } -+ -+ private final SingleUserAreaMap<PlayerChunkLoaderData> broadcastMap = new SingleUserAreaMap<>(this) { -+ @Override -+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { -+ // do nothing, we only care about remove -+ } -+ -+ @Override -+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { -+ parameter.sendUnloadChunk(chunkX, chunkZ); -+ } -+ }; -+ private final SingleUserAreaMap<PlayerChunkLoaderData> loadTicketCleanup = new SingleUserAreaMap<>(this) { -+ @Override -+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { -+ // do nothing, we only care about remove -+ } -+ -+ @Override -+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { -+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ final byte ticketStage = parameter.chunkTicketStage.remove(chunk); -+ final int level = TICKET_STAGE_TO_LEVEL[ticketStage]; -+ if (level > ChunkHolderManager.MAX_TICKET_LEVEL) { -+ return; -+ } -+ -+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove( -+ chunk, -+ PLAYER_TICKET_DELAYED, level, parameter.idBoxed, -+ PLAYER_TICKET, level, parameter.idBoxed -+ )); -+ } -+ }; -+ private final SingleUserAreaMap<PlayerChunkLoaderData> tickMap = new SingleUserAreaMap<>(this) { -+ @Override -+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { -+ // do nothing, we will detect ticking chunks when we try to load them -+ } -+ -+ @Override -+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { -+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ // note: by the time this is called, the tick cleanup should have ran - so, if the chunk is at -+ // the tick stage it was deemed in range for loading. Thus, we need to move it to generated -+ if (!parameter.chunkTicketStage.replace(chunk, CHUNK_TICKET_STAGE_TICK, CHUNK_TICKET_STAGE_GENERATED)) { -+ return; -+ } -+ -+ // Since we are possibly downgrading the ticket level, we add the delayed unload ticket so that -+ // the level is kept for a short period of time -+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove( -+ chunk, -+ PLAYER_TICKET_DELAYED, TICK_TICKET_LEVEL, parameter.idBoxed, -+ PLAYER_TICKET, TICK_TICKET_LEVEL, parameter.idBoxed -+ )); -+ // keep chunk at new generated level -+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addOp( -+ chunk, PLAYER_TICKET, GENERATED_TICKET_LEVEL, parameter.idBoxed -+ )); -+ } -+ }; -+ -+ private static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ, -+ final int sendRadius) { -+ // expect sendRadius to be = 1 + target viewable radius -+ return ChunkTrackingView.isWithinDistance(centerX, centerZ, sendRadius, chunkX, chunkZ, true); -+ } -+ -+ private static int getClientViewDistance(final ServerPlayer player) { -+ final Integer vd = player.requestedViewDistance(); -+ return vd == null ? -1 : Math.max(0, vd.intValue()); -+ } -+ -+ private static int getTickDistance(final int playerTickViewDistance, final int worldTickViewDistance, -+ final int playerLoadViewDistance, final int worldLoadViewDistance) { -+ return Math.min( -+ playerTickViewDistance < 0 ? worldTickViewDistance : playerTickViewDistance, -+ playerLoadViewDistance < 0 ? (worldLoadViewDistance - 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<UnloadSection> unloadSections = new ConcurrentLong2ReferenceChainedHashTable<>(); -+ -+ /* -+ * Note: write operations do not occur in parallel for any given section. -+ * Note: coordinateShift <= region shift in order for retrieveForCurrentRegion() to function correctly -+ */ -+ -+ public ChunkUnloadQueue(final int coordinateShift) { -+ this.coordinateShift = coordinateShift; -+ } -+ -+ public static record SectionToUnload(int sectionX, int sectionZ, long order, int count) {} -+ -+ public List<SectionToUnload> retrieveForAllRegions() { -+ final List<SectionToUnload> ret = new ArrayList<>(); -+ -+ for (final Iterator<ConcurrentLong2ReferenceChainedHashTable.TableEntry<UnloadSection>> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) { -+ final ConcurrentLong2ReferenceChainedHashTable.TableEntry<UnloadSection> entry = iterator.next(); -+ final long key = entry.getKey(); -+ final UnloadSection section = entry.getValue(); -+ final int sectionX = CoordinateUtils.getChunkX(key); -+ final int sectionZ = CoordinateUtils.getChunkZ(key); -+ -+ ret.add(new SectionToUnload(sectionX, sectionZ, section.order, section.chunks.size())); -+ } -+ -+ ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> { -+ return Long.compare(s1.order, s2.order); -+ }); -+ -+ return ret; -+ } -+ -+ public UnloadSection getSectionUnsynchronized(final int sectionX, final int sectionZ) { -+ return this.unloadSections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ)); -+ } -+ -+ public UnloadSection removeSection(final int sectionX, final int sectionZ) { -+ return this.unloadSections.remove(CoordinateUtils.getChunkKey(sectionX, sectionZ)); -+ } -+ -+ // write operation -+ public boolean addChunk(final int chunkX, final int chunkZ) { -+ // write operations do not occur in parallel for a given section -+ final int shift = this.coordinateShift; -+ final int sectionX = chunkX >> shift; -+ final int sectionZ = chunkZ >> shift; -+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ -+ UnloadSection section = this.unloadSections.get(sectionKey); -+ if (section == null) { -+ section = new UnloadSection(this.orderGenerator.getAndIncrement()); -+ this.unloadSections.put(sectionKey, section); -+ } -+ -+ return section.chunks.add(chunkKey); -+ } -+ -+ // write operation -+ public boolean removeChunk(final int chunkX, final int chunkZ) { -+ // write operations do not occur in parallel for a given section -+ final int shift = this.coordinateShift; -+ final int sectionX = chunkX >> shift; -+ final int sectionZ = chunkZ >> shift; -+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ -+ final UnloadSection section = this.unloadSections.get(sectionKey); -+ -+ if (section == null) { -+ return false; -+ } -+ -+ if (!section.chunks.remove(chunkKey)) { -+ return false; -+ } -+ -+ if (section.chunks.isEmpty()) { -+ this.unloadSections.remove(sectionKey); -+ } -+ -+ return true; -+ } -+ -+ public JsonElement toDebugJson() { -+ final JsonArray ret = new JsonArray(); -+ -+ for (final SectionToUnload section : this.retrieveForAllRegions()) { -+ final JsonObject sectionJson = new JsonObject(); -+ ret.add(sectionJson); -+ -+ sectionJson.addProperty("sectionX", section.sectionX()); -+ sectionJson.addProperty("sectionZ", section.sectionX()); -+ sectionJson.addProperty("order", section.order()); -+ -+ final JsonArray coordinates = new JsonArray(); -+ sectionJson.add("coordinates", coordinates); -+ -+ final UnloadSection actualSection = this.getSectionUnsynchronized(section.sectionX(), section.sectionZ()); -+ if (actualSection != null) { -+ for (final LongIterator iterator = actualSection.chunks.clone().iterator(); iterator.hasNext(); ) { -+ final long coordinate = iterator.nextLong(); -+ -+ final JsonObject coordinateJson = new JsonObject(); -+ coordinates.add(coordinateJson); -+ -+ coordinateJson.addProperty("chunkX", Integer.valueOf(CoordinateUtils.getChunkX(coordinate))); -+ coordinateJson.addProperty("chunkZ", Integer.valueOf(CoordinateUtils.getChunkZ(coordinate))); -+ } -+ } -+ } -+ -+ return ret; -+ } -+ -+ public static final class UnloadSection { -+ -+ public final long order; -+ public final LongLinkedOpenHashSet chunks = new LongLinkedOpenHashSet(); -+ -+ public UnloadSection(final long order) { -+ this.order = order; -+ } -+ } -+} -\ No newline at end of file -diff --git a/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<Unit> UNLOAD_COOLDOWN = TicketType.create("unload_cooldown", (u1, u2) -> 0, 5 * 20); -+ -+ private static final long NO_TIMEOUT_MARKER = Long.MIN_VALUE; -+ private static final long PROBE_MARKER = Long.MIN_VALUE + 1; -+ public final ReentrantAreaLock ticketLockArea; -+ -+ private final ConcurrentLong2ReferenceChainedHashTable<SortedArraySet<Ticket<?>>> tickets = new ConcurrentLong2ReferenceChainedHashTable<>(); -+ private final ConcurrentLong2ReferenceChainedHashTable<Long2IntOpenHashMap> sectionToChunkToExpireCount = new ConcurrentLong2ReferenceChainedHashTable<>(); -+ final ChunkUnloadQueue unloadQueue; -+ -+ private final ConcurrentLong2ReferenceChainedHashTable<NewChunkHolder> chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f); -+ private final ServerLevel world; -+ private final ChunkTaskScheduler taskScheduler; -+ private long currentTick; -+ -+ private final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = new ArrayDeque<>(); -+ private final ObjectRBTreeSet<NewChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { -+ if (c1 == c2) { -+ return 0; -+ } -+ -+ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); -+ -+ if (saveTickCompare != 0) { -+ return saveTickCompare; -+ } -+ -+ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); -+ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); -+ -+ if (coord1 == coord2) { -+ throw new IllegalStateException("Duplicate chunkholder in auto save queue"); -+ } -+ -+ return Long.compare(coord1, coord2); -+ }); -+ -+ public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { -+ this.world = world; -+ this.taskScheduler = taskScheduler; -+ this.ticketLockArea = new ReentrantAreaLock(taskScheduler.getChunkSystemLockShift()); -+ this.unloadQueue = new ChunkUnloadQueue(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift()); -+ } -+ -+ public boolean processTicketUpdates(final int posX, final int posZ) { -+ final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT; -+ final int ticketMask = (1 << ticketShift) - 1; -+ final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>(); -+ final List<NewChunkHolder> changedFullStatus = new ArrayList<>(); -+ final boolean ret; -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( -+ ((posX >> ticketShift) - 1) << ticketShift, -+ ((posZ >> ticketShift) - 1) << ticketShift, -+ (((posX >> ticketShift) + 1) << ticketShift) | ticketMask, -+ (((posZ >> ticketShift) + 1) << ticketShift) | ticketMask -+ ); -+ try { -+ ret = this.processTicketUpdatesNoLock(posX >> ticketShift, posZ >> ticketShift, scheduledTasks, changedFullStatus); -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ -+ this.addChangedStatuses(changedFullStatus); -+ -+ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) { -+ scheduledTasks.get(i).schedule(); -+ } -+ -+ return ret; -+ } -+ -+ private boolean processTicketUpdatesNoLock(final int sectionX, final int sectionZ, final List<ChunkProgressionTask> scheduledTasks, -+ final List<NewChunkHolder> changedFullStatus) { -+ return this.ticketLevelPropagator.performUpdate( -+ sectionX, sectionZ, this.taskScheduler.schedulingLockArea, scheduledTasks, changedFullStatus -+ ); -+ } -+ -+ public List<ChunkHolder> getOldChunkHolders() { -+ final List<ChunkHolder> ret = new ArrayList<>(this.chunkHolders.size() + 1); -+ for (final Iterator<NewChunkHolder> iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) { -+ ret.add(iterator.next().vanillaChunkHolder); -+ } -+ return ret; -+ } -+ -+ public List<NewChunkHolder> getChunkHolders() { -+ final List<NewChunkHolder> ret = new ArrayList<>(this.chunkHolders.size() + 1); -+ for (final Iterator<NewChunkHolder> iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) { -+ ret.add(iterator.next()); -+ } -+ return ret; -+ } -+ -+ public int size() { -+ return this.chunkHolders.size(); -+ } -+ -+ // TODO replace the need for this, specifically: optimise ServerChunkCache#tickChunks -+ public Iterable<ChunkHolder> getOldChunkHoldersIterable() { -+ return new Iterable<ChunkHolder>() { -+ @Override -+ public Iterator<ChunkHolder> iterator() { -+ final Iterator<NewChunkHolder> iterator = ChunkHolderManager.this.chunkHolders.valueIterator(); -+ return new Iterator<ChunkHolder>() { -+ @Override -+ public boolean hasNext() { -+ return iterator.hasNext(); -+ } -+ -+ @Override -+ public ChunkHolder next() { -+ return iterator.next().vanillaChunkHolder; -+ } -+ }; -+ } -+ }; -+ } -+ -+ public void close(final boolean save, final boolean halt) { -+ TickThread.ensureTickThread("Closing world off-main"); -+ if (halt) { -+ LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'"); -+ if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) { -+ LOGGER.warn("Failed to halt generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'"); -+ } else { -+ LOGGER.info("Halted chunk system for world '" + WorldUtil.getWorldName(this.world) + "'"); -+ } -+ } -+ -+ if (save) { -+ this.saveAllChunks(true, true, true); -+ } -+ -+ MoonriseRegionFileIO.flush(this.world); -+ -+ if (halt) { -+ LOGGER.info("Waiting 60s for chunk I/O to halt for world '" + WorldUtil.getWorldName(this.world) + "'"); -+ if (!this.taskScheduler.haltIO(true, TimeUnit.SECONDS.toNanos(60L))) { -+ LOGGER.warn("Failed to halt I/O tasks for world '" + WorldUtil.getWorldName(this.world) + "'"); -+ } else { -+ LOGGER.info("Halted I/O scheduler for world '" + WorldUtil.getWorldName(this.world) + "'"); -+ } -+ } -+ -+ // kill regionfile cache -+ for (final MoonriseRegionFileIO.RegionFileType type : MoonriseRegionFileIO.RegionFileType.values()) { -+ try { -+ MoonriseRegionFileIO.getControllerFor(this.world, type).getCache().close(); -+ } catch (final IOException ex) { -+ LOGGER.error("Failed to close '" + type.name() + "' regionfile cache for world '" + WorldUtil.getWorldName(this.world) + "'", ex); -+ } -+ } -+ -+ 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<NewChunkHolder> reschedule = new ArrayList<>(); -+ final long currentTick = this.currentTick; -+ final long maxSaveTime = currentTick - Math.max(1L, PlatformHooks.get().configAutoSaveInterval(this.world)); -+ final int maxToSave = PlatformHooks.get().configMaxAutoSavePerTick(this.world); -+ for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) { -+ final NewChunkHolder holder = this.autoSaveQueue.first(); -+ -+ if (holder.lastAutoSave > maxSaveTime) { -+ break; -+ } -+ -+ this.autoSaveQueue.remove(holder); -+ -+ holder.lastAutoSave = currentTick; -+ if (holder.save(false) != null) { -+ ++autoSaved; -+ } -+ -+ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) { -+ reschedule.add(holder); -+ } -+ } -+ -+ for (final NewChunkHolder holder : reschedule) { -+ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) { -+ this.autoSaveQueue.add(holder); -+ } -+ } -+ } -+ -+ public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) { -+ final List<NewChunkHolder> holders = this.getChunkHolders(); -+ -+ if (logProgress) { -+ LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'"); -+ } -+ -+ final DecimalFormat format = new DecimalFormat("#0.00"); -+ -+ int saved = 0; -+ -+ long start = System.nanoTime(); -+ long lastLog = start; -+ final int flushInterval = 200; -+ int lastFlush = 0; -+ -+ int savedChunk = 0; -+ int savedEntity = 0; -+ int savedPoi = 0; -+ -+ if (shutdown) { -+ // Normal unload process does not occur during shutdown: fire event manually -+ // for mods that expect ChunkEvent.Unload to fire on shutdown (before LevelEvent.Unload) -+ for (int i = 0, len = holders.size(); i < len; ++i) { -+ final NewChunkHolder holder = holders.get(i); -+ if (holder.getCurrentChunk() instanceof LevelChunk levelChunk) { -+ PlatformHooks.get().chunkUnloadFromWorld(levelChunk); -+ } -+ } -+ } -+ for (int i = 0, len = holders.size(); i < len; ++i) { -+ final NewChunkHolder holder = holders.get(i); -+ try { -+ final NewChunkHolder.SaveStat saveStat = holder.save(shutdown); -+ if (saveStat != null) { -+ if (saveStat.savedChunk()) { -+ ++savedChunk; -+ ++saved; -+ } -+ if (saveStat.savedEntityChunk()) { -+ ++savedEntity; -+ ++saved; -+ } -+ if (saveStat.savedPoiChunk()) { -+ ++savedPoi; -+ ++saved; -+ } -+ } -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to save chunk (" + holder.chunkX + "," + holder.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr); -+ } -+ if (flush && (saved - lastFlush) > (flushInterval / 2)) { -+ lastFlush = saved; -+ MoonriseRegionFileIO.partialFlush(this.world, flushInterval / 2); -+ } -+ if (logProgress) { -+ final long currTime = System.nanoTime(); -+ if ((currTime - lastLog) > TimeUnit.SECONDS.toNanos(10L)) { -+ lastLog = currTime; -+ LOGGER.info( -+ "Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi -+ + " poi chunks in world '" + WorldUtil.getWorldName(this.world) + "', progress: " -+ + format.format((double)(i+1)/(double)len * 100.0) -+ ); -+ } -+ } -+ } -+ if (flush) { -+ MoonriseRegionFileIO.flush(this.world); -+ try { -+ MoonriseRegionFileIO.flushRegionStorages(this.world); -+ } catch (final IOException ex) { -+ LOGGER.error("Exception when flushing regions in world '" + WorldUtil.getWorldName(this.world) + "'", ex); -+ } -+ } -+ if (logProgress) { -+ LOGGER.info( -+ "Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi -+ + " poi chunks in world '" + WorldUtil.getWorldName(this.world) + "' in " -+ + format.format(1.0E-9 * (System.nanoTime() - start)) + "s" -+ ); -+ } -+ } -+ -+ private final ThreadedTicketLevelPropagator ticketLevelPropagator = new ThreadedTicketLevelPropagator() { -+ @Override -+ protected void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates) { -+ // first the necessary chunkholders must be created, so just update the ticket levels -+ for (final Iterator<Long2ByteMap.Entry> iterator = updates.long2ByteEntrySet().fastIterator(); iterator.hasNext();) { -+ final Long2ByteMap.Entry entry = iterator.next(); -+ final long key = entry.getLongKey(); -+ final int newLevel = convertBetweenTicketLevels((int)entry.getByteValue()); -+ -+ NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key); -+ if (current == null && newLevel > MAX_TICKET_LEVEL) { -+ // not loaded and it shouldn't be loaded! -+ iterator.remove(); -+ continue; -+ } -+ -+ final int currentLevel = current == null ? MAX_TICKET_LEVEL + 1 : current.getCurrentTicketLevel(); -+ if (currentLevel == newLevel) { -+ // nothing to do -+ iterator.remove(); -+ continue; -+ } -+ -+ if (current == null) { -+ // must create -+ current = ChunkHolderManager.this.createChunkHolder(key); -+ ChunkHolderManager.this.chunkHolders.put(key, current); -+ current.updateTicketLevel(newLevel); -+ } else { -+ current.updateTicketLevel(newLevel); -+ } -+ } -+ } -+ -+ @Override -+ protected void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List<ChunkProgressionTask> scheduledTasks, -+ final List<NewChunkHolder> changedFullStatus) { -+ final List<ChunkProgressionTask> prev = CURRENT_TICKET_UPDATE_SCHEDULING.get(); -+ CURRENT_TICKET_UPDATE_SCHEDULING.set(scheduledTasks); -+ try { -+ for (final LongIterator iterator = updates.keySet().iterator(); iterator.hasNext();) { -+ final long key = iterator.nextLong(); -+ final NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key); -+ -+ if (current == null) { -+ throw new IllegalStateException("Expected chunk holder to be created"); -+ } -+ -+ current.processTicketLevelUpdate(scheduledTasks, changedFullStatus); -+ } -+ } finally { -+ CURRENT_TICKET_UPDATE_SCHEDULING.set(prev); -+ } -+ } -+ }; -+ // function for converting between ticket levels and propagator levels and vice versa -+ // the problem is the ticket level propagator will propagate from a set source down to zero, whereas mojang expects -+ // levels to propagate from a set value up to a maximum value. so we need to convert the levels we put into the propagator -+ // and the levels we get out of the propagator -+ -+ public static int convertBetweenTicketLevels(final int level) { -+ return ChunkLevel.MAX_LEVEL - level + 1; -+ } -+ -+ public String getTicketDebugString(final long coordinate) { -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate)); -+ try { -+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coordinate); -+ -+ return tickets != null ? tickets.first().toString() : "no_ticket"; -+ } finally { -+ if (ticketLock != null) { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ } -+ -+ public Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> getTicketsCopy() { -+ final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> ret = new Long2ObjectOpenHashMap<>(); -+ final Long2ObjectOpenHashMap<LongArrayList> sections = new Long2ObjectOpenHashMap<>(); -+ final int sectionShift = this.taskScheduler.getChunkSystemLockShift(); -+ for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) { -+ final long coord = iterator.nextLong(); -+ sections.computeIfAbsent( -+ CoordinateUtils.getChunkKey( -+ CoordinateUtils.getChunkX(coord) >> sectionShift, -+ CoordinateUtils.getChunkZ(coord) >> sectionShift -+ ), -+ (final long keyInMap) -> { -+ return new LongArrayList(); -+ } -+ ).add(coord); -+ } -+ -+ for (final Iterator<Long2ObjectMap.Entry<LongArrayList>> iterator = sections.long2ObjectEntrySet().fastIterator(); -+ iterator.hasNext();) { -+ final Long2ObjectMap.Entry<LongArrayList> entry = iterator.next(); -+ final long sectionKey = entry.getLongKey(); -+ final LongArrayList coordinates = entry.getValue(); -+ -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( -+ CoordinateUtils.getChunkX(sectionKey) << sectionShift, -+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift -+ ); -+ try { -+ for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) { -+ final long coord = iterator2.nextLong(); -+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coord); -+ if (tickets == null) { -+ // removed before we acquired lock -+ continue; -+ } -+ ret.put(coord, ((ChunkSystemSortedArraySet<Ticket<?>>)tickets).moonrise$copy()); -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ -+ return ret; -+ } -+ -+ // Paper start -+ public Collection<org.bukkit.plugin.Plugin> getPluginChunkTickets(int x, int z) { -+ com.google.common.collect.ImmutableList.Builder<org.bukkit.plugin.Plugin> ret; -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(x, z); -+ try { -+ final long coordinate = CoordinateUtils.getChunkKey(x, z); -+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coordinate); -+ -+ if (tickets == null) { -+ return java.util.Collections.emptyList(); -+ } -+ -+ ret = com.google.common.collect.ImmutableList.builder(); -+ for (Ticket<?> ticket : tickets) { -+ if (ticket.getType() == TicketType.PLUGIN_TICKET) { -+ ret.add((org.bukkit.plugin.Plugin)ticket.key); -+ } -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ -+ return ret.build(); -+ } -+ // Paper end -+ -+ protected final void updateTicketLevel(final long coordinate, final int ticketLevel) { -+ if (ticketLevel > ChunkLevel.MAX_LEVEL) { -+ this.ticketLevelPropagator.removeSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate)); -+ } else { -+ this.ticketLevelPropagator.setSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate), convertBetweenTicketLevels(ticketLevel)); -+ } -+ } -+ -+ private static int getTicketLevelAt(SortedArraySet<Ticket<?>> tickets) { -+ return !tickets.isEmpty() ? tickets.first().getTicketLevel() : MAX_TICKET_LEVEL + 1; -+ } -+ -+ public <T> boolean addTicketAtLevel(final TicketType<T> type, final ChunkPos chunkPos, final int level, -+ final T identifier) { -+ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); -+ } -+ -+ public <T> boolean addTicketAtLevel(final TicketType<T> type, final int chunkX, final int chunkZ, final int level, -+ final T identifier) { -+ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); -+ } -+ -+ private void addExpireCount(final int chunkX, final int chunkZ) { -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ -+ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); -+ final long sectionKey = CoordinateUtils.getChunkKey( -+ chunkX >> sectionShift, -+ chunkZ >> sectionShift -+ ); -+ -+ this.sectionToChunkToExpireCount.computeIfAbsent(sectionKey, (final long keyInMap) -> { -+ return new Long2IntOpenHashMap(); -+ }).addTo(chunkKey, 1); -+ } -+ -+ private void removeExpireCount(final int chunkX, final int chunkZ) { -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ -+ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); -+ final long sectionKey = CoordinateUtils.getChunkKey( -+ chunkX >> sectionShift, -+ chunkZ >> sectionShift -+ ); -+ -+ final Long2IntOpenHashMap removeCounts = this.sectionToChunkToExpireCount.get(sectionKey); -+ final int prevCount = removeCounts.addTo(chunkKey, -1); -+ -+ if (prevCount == 1) { -+ removeCounts.remove(chunkKey); -+ if (removeCounts.isEmpty()) { -+ this.sectionToChunkToExpireCount.remove(sectionKey); -+ } -+ } -+ } -+ -+ // supposed to return true if the ticket was added and did not replace another -+ // but, we always return false if the ticket cannot be added -+ public <T> boolean addTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier) { -+ return this.addTicketAtLevel(type, chunk, level, identifier, true); -+ } -+ -+ <T> boolean addTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier, final boolean lock) { -+ final long removeDelay = type.timeout <= 0 ? NO_TIMEOUT_MARKER : type.timeout; -+ if (level > MAX_TICKET_LEVEL) { -+ return false; -+ } -+ -+ final int chunkX = CoordinateUtils.getChunkX(chunk); -+ final int chunkZ = CoordinateUtils.getChunkZ(chunk); -+ final Ticket<T> ticket = new Ticket<>(type, level, identifier); -+ ((ChunkSystemTicket<T>)(Object)ticket).moonrise$setRemoveDelay(removeDelay); -+ -+ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; -+ try { -+ final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> { -+ return SortedArraySet.create(4); -+ }); -+ -+ final int levelBefore = getTicketLevelAt(ticketsAtChunk); -+ final Ticket<T> current = (Ticket<T>)((ChunkSystemSortedArraySet<Ticket<?>>)ticketsAtChunk).moonrise$replace(ticket); -+ final int levelAfter = getTicketLevelAt(ticketsAtChunk); -+ -+ if (current != ticket) { -+ final long oldRemoveDelay = ((ChunkSystemTicket<T>)(Object)current).moonrise$getRemoveDelay(); -+ if (removeDelay != oldRemoveDelay) { -+ if (oldRemoveDelay != NO_TIMEOUT_MARKER && removeDelay == NO_TIMEOUT_MARKER) { -+ this.removeExpireCount(chunkX, chunkZ); -+ } else if (oldRemoveDelay == NO_TIMEOUT_MARKER) { -+ // since old != new, we have that NO_TIMEOUT_MARKER != new -+ this.addExpireCount(chunkX, chunkZ); -+ } -+ } -+ } else { -+ if (removeDelay != NO_TIMEOUT_MARKER) { -+ this.addExpireCount(chunkX, chunkZ); -+ } -+ } -+ -+ if (levelBefore != levelAfter) { -+ this.updateTicketLevel(chunk, levelAfter); -+ } -+ -+ return current == ticket; -+ } finally { -+ if (ticketLock != null) { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ } -+ -+ public <T> boolean removeTicketAtLevel(final TicketType<T> type, final ChunkPos chunkPos, final int level, final T identifier) { -+ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); -+ } -+ -+ public <T> boolean removeTicketAtLevel(final TicketType<T> type, final int chunkX, final int chunkZ, final int level, final T identifier) { -+ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); -+ } -+ -+ public <T> boolean removeTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier) { -+ return this.removeTicketAtLevel(type, chunk, level, identifier, true); -+ } -+ -+ <T> boolean removeTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier, final boolean lock) { -+ if (level > MAX_TICKET_LEVEL) { -+ return false; -+ } -+ -+ final int chunkX = CoordinateUtils.getChunkX(chunk); -+ final int chunkZ = CoordinateUtils.getChunkZ(chunk); -+ final Ticket<T> probe = new Ticket<>(type, level, identifier); -+ -+ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; -+ try { -+ final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.get(chunk); -+ if (ticketsAtChunk == null) { -+ return false; -+ } -+ -+ final int oldLevel = getTicketLevelAt(ticketsAtChunk); -+ final Ticket<T> ticket = (Ticket<T>)((ChunkSystemSortedArraySet<Ticket<?>>)ticketsAtChunk).moonrise$removeAndGet(probe); -+ -+ if (ticket == null) { -+ return false; -+ } -+ -+ final int newLevel = getTicketLevelAt(ticketsAtChunk); -+ // we should not change the ticket levels while the target region may be ticking -+ if (oldLevel != newLevel) { -+ final Ticket<ChunkPos> unknownTicket = new Ticket<>(TicketType.UNKNOWN, level, new ChunkPos(chunk)); -+ ((ChunkSystemTicket<ChunkPos>)(Object)unknownTicket).moonrise$setRemoveDelay(Math.max(1, TicketType.UNKNOWN.timeout)); -+ if (ticketsAtChunk.add(unknownTicket)) { -+ this.addExpireCount(chunkX, chunkZ); -+ } else { -+ throw new IllegalStateException("Should have been able to add " + unknownTicket + " to " + ticketsAtChunk); -+ } -+ } -+ -+ final long removeDelay = ((ChunkSystemTicket<T>)(Object)ticket).moonrise$getRemoveDelay(); -+ if (removeDelay != NO_TIMEOUT_MARKER) { -+ this.removeExpireCount(chunkX, chunkZ); -+ } -+ -+ return true; -+ } finally { -+ if (ticketLock != null) { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ } -+ -+ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk -+ public <T, V> void addAndRemoveTickets(final long chunk, final TicketType<T> addType, final int addLevel, final T addIdentifier, -+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) { -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)); -+ try { -+ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false); -+ this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false); -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ -+ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk -+ public <T, V> boolean addIfRemovedTicket(final long chunk, final TicketType<T> addType, final int addLevel, final T addIdentifier, -+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) { -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)); -+ try { -+ if (this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false)) { -+ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false); -+ return true; -+ } -+ return false; -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ -+ public <T> void removeAllTicketsFor(final TicketType<T> ticketType, final int ticketLevel, final T ticketIdentifier) { -+ if (ticketLevel > MAX_TICKET_LEVEL) { -+ return; -+ } -+ -+ final Long2ObjectOpenHashMap<LongArrayList> sections = new Long2ObjectOpenHashMap<>(); -+ final int sectionShift = this.taskScheduler.getChunkSystemLockShift(); -+ for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) { -+ final long coord = iterator.nextLong(); -+ sections.computeIfAbsent( -+ CoordinateUtils.getChunkKey( -+ CoordinateUtils.getChunkX(coord) >> sectionShift, -+ CoordinateUtils.getChunkZ(coord) >> sectionShift -+ ), -+ (final long keyInMap) -> { -+ return new LongArrayList(); -+ } -+ ).add(coord); -+ } -+ -+ for (final Iterator<Long2ObjectMap.Entry<LongArrayList>> iterator = sections.long2ObjectEntrySet().fastIterator(); -+ iterator.hasNext();) { -+ final Long2ObjectMap.Entry<LongArrayList> entry = iterator.next(); -+ final long sectionKey = entry.getLongKey(); -+ final LongArrayList coordinates = entry.getValue(); -+ -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( -+ CoordinateUtils.getChunkX(sectionKey) << sectionShift, -+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift -+ ); -+ try { -+ for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) { -+ final long coord = iterator2.nextLong(); -+ this.removeTicketAtLevel(ticketType, coord, ticketLevel, ticketIdentifier, false); -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ } -+ -+ public void tick() { -+ ++this.currentTick; -+ -+ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); -+ -+ final Predicate<Ticket<?>> expireNow = (final Ticket<?> ticket) -> { -+ long removeDelay = ((ChunkSystemTicket<?>)(Object)ticket).moonrise$getRemoveDelay(); -+ if (removeDelay == NO_TIMEOUT_MARKER) { -+ return false; -+ } -+ --removeDelay; -+ ((ChunkSystemTicket<?>)(Object)ticket).moonrise$setRemoveDelay(removeDelay); -+ return removeDelay <= 0L; -+ }; -+ -+ for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) { -+ final long sectionKey = iterator.nextLong(); -+ -+ if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) { -+ // removed concurrently -+ continue; -+ } -+ -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( -+ CoordinateUtils.getChunkX(sectionKey) << sectionShift, -+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift -+ ); -+ -+ try { -+ final Long2IntOpenHashMap chunkToExpireCount = this.sectionToChunkToExpireCount.get(sectionKey); -+ if (chunkToExpireCount == null) { -+ // lost to some race -+ continue; -+ } -+ -+ for (final Iterator<Long2IntMap.Entry> iterator1 = chunkToExpireCount.long2IntEntrySet().fastIterator(); iterator1.hasNext();) { -+ final Long2IntMap.Entry entry = iterator1.next(); -+ -+ final long chunkKey = entry.getLongKey(); -+ final int expireCount = entry.getIntValue(); -+ -+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(chunkKey); -+ final int levelBefore = getTicketLevelAt(tickets); -+ -+ final int sizeBefore = tickets.size(); -+ tickets.removeIf(expireNow); -+ final int sizeAfter = tickets.size(); -+ final int levelAfter = getTicketLevelAt(tickets); -+ -+ if (tickets.isEmpty()) { -+ this.tickets.remove(chunkKey); -+ } -+ if (levelBefore != levelAfter) { -+ this.updateTicketLevel(chunkKey, levelAfter); -+ } -+ -+ final int newExpireCount = expireCount - (sizeBefore - sizeAfter); -+ -+ if (newExpireCount == expireCount) { -+ continue; -+ } -+ -+ if (newExpireCount != 0) { -+ entry.setValue(newExpireCount); -+ } else { -+ iterator1.remove(); -+ } -+ } -+ -+ if (chunkToExpireCount.isEmpty()) { -+ this.sectionToChunkToExpireCount.remove(sectionKey); -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ -+ this.processTicketUpdates(); -+ } -+ -+ public NewChunkHolder getChunkHolder(final int chunkX, final int chunkZ) { -+ return this.chunkHolders.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ -+ public NewChunkHolder getChunkHolder(final long position) { -+ return this.chunkHolders.get(position); -+ } -+ -+ public void raisePriority(final int x, final int z, final Priority priority) { -+ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); -+ if (chunkHolder != null) { -+ chunkHolder.raisePriority(priority); -+ } -+ } -+ -+ public void setPriority(final int x, final int z, final Priority priority) { -+ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); -+ if (chunkHolder != null) { -+ chunkHolder.setPriority(priority); -+ } -+ } -+ -+ public void lowerPriority(final int x, final int z, final Priority priority) { -+ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); -+ if (chunkHolder != null) { -+ chunkHolder.lowerPriority(priority); -+ } -+ } -+ -+ private NewChunkHolder createChunkHolder(final long position) { -+ final NewChunkHolder ret = new NewChunkHolder(this.world, CoordinateUtils.getChunkX(position), CoordinateUtils.getChunkZ(position), this.taskScheduler); -+ -+ ChunkSystem.onChunkHolderCreate(this.world, ret.vanillaChunkHolder); -+ -+ return ret; -+ } -+ -+ // because this function creates the chunk holder without a ticket, it is the caller's responsibility to ensure -+ // the chunk holder eventually unloads. this should only be used to avoid using processTicketUpdates to create chunkholders, -+ // as processTicketUpdates may call plugin logic; in every other case a ticket is appropriate -+ private NewChunkHolder getOrCreateChunkHolder(final int chunkX, final int chunkZ) { -+ return this.getOrCreateChunkHolder(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ -+ private NewChunkHolder getOrCreateChunkHolder(final long position) { -+ final int chunkX = CoordinateUtils.getChunkX(position); -+ final int chunkZ = CoordinateUtils.getChunkZ(position); -+ -+ if (!this.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ)) { -+ throw new IllegalStateException("Must hold ticket level update lock!"); -+ } -+ if (!this.taskScheduler.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ)) { -+ throw new IllegalStateException("Must hold scheduler lock!!"); -+ } -+ -+ // we could just acquire these locks, but... -+ // must own the locks because the caller needs to ensure that no unload can occur AFTER this function returns -+ -+ NewChunkHolder current = this.chunkHolders.get(position); -+ if (current != null) { -+ return current; -+ } -+ -+ current = this.createChunkHolder(position); -+ this.chunkHolders.put(position, current); -+ -+ -+ return current; -+ } -+ -+ public ChunkEntitySlices getOrCreateEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { -+ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create entity chunk off-main"); -+ ChunkEntitySlices ret; -+ -+ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ); -+ if (current != null && (ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) { -+ return ret; -+ } -+ -+ final AtomicBoolean isCompleted = new AtomicBoolean(); -+ final Thread waiter = Thread.currentThread(); -+ final Long entityLoadId = ChunkTaskScheduler.getNextEntityLoadId(); -+ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null; -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ); -+ try { -+ this.addTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); -+ final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ); -+ try { -+ current = this.getOrCreateChunkHolder(chunkX, chunkZ); -+ if ((ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) { -+ this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); -+ return ret; -+ } -+ -+ if (!transientChunk) { -+ if (current.isEntityChunkNBTLoaded()) { -+ isCompleted.setPlain(true); -+ } else { -+ loadTask = current.getOrLoadEntityData((final GenericDataLoadTask.TaskResult<CompoundTag, Throwable> result) -> { -+ isCompleted.set(true); -+ LockSupport.unpark(waiter); -+ }); -+ final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask(); -+ -+ if (entityLoad != null) { -+ entityLoad.raisePriority(Priority.BLOCKING); -+ } -+ } -+ } -+ } finally { -+ this.taskScheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ -+ if (loadTask != null) { -+ loadTask.schedule(); -+ } -+ -+ if (!transientChunk) { -+ // Note: no need to busy wait on the chunk queue, entity load will complete off-main -+ boolean interrupted = false; -+ while (!isCompleted.get()) { -+ interrupted |= Thread.interrupted(); -+ LockSupport.park(); -+ } -+ -+ if (interrupted) { -+ Thread.currentThread().interrupt(); -+ } -+ } -+ -+ // now that the entity data is loaded, we can load it into the world -+ -+ ret = current.loadInEntityChunk(transientChunk); -+ -+ this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); -+ -+ return ret; -+ } -+ -+ public PoiChunk getPoiChunkIfLoaded(final int chunkX, final int chunkZ, final boolean checkLoadInCallback) { -+ final NewChunkHolder holder = this.getChunkHolder(chunkX, chunkZ); -+ if (holder != null) { -+ final PoiChunk ret = holder.getPoiChunk(); -+ return ret == null || (checkLoadInCallback && !ret.isLoaded()) ? null : ret; -+ } -+ return null; -+ } -+ -+ public PoiChunk loadPoiChunk(final int chunkX, final int chunkZ) { -+ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create poi chunk off-main"); -+ PoiChunk ret; -+ -+ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ); -+ if (current != null && (ret = current.getPoiChunk()) != null) { -+ ret.load(); -+ return ret; -+ } -+ -+ final AtomicReference<PoiChunk> completed = new AtomicReference<>(); -+ final AtomicBoolean isCompleted = new AtomicBoolean(); -+ final Thread waiter = Thread.currentThread(); -+ final Long poiLoadId = ChunkTaskScheduler.getNextPoiLoadId(); -+ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null; -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ); -+ try { -+ this.addTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId); -+ final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ); -+ try { -+ current = this.getOrCreateChunkHolder(chunkX, chunkZ); -+ if (null == (ret = current.getPoiChunk())) { -+ loadTask = current.getOrLoadPoiData((final GenericDataLoadTask.TaskResult<PoiChunk, Throwable> result) -> { -+ completed.setPlain(result.left()); -+ isCompleted.set(true); -+ LockSupport.unpark(waiter); -+ }); -+ final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask(); -+ -+ if (poiLoad != null) { -+ poiLoad.raisePriority(Priority.BLOCKING); -+ } -+ } -+ } finally { -+ this.taskScheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ -+ if (loadTask != null) { -+ loadTask.schedule(); -+ -+ // Note: no need to busy wait on the chunk queue, poi load will complete off-main -+ -+ boolean interrupted = false; -+ while (!isCompleted.get()) { -+ interrupted |= Thread.interrupted(); -+ LockSupport.park(); -+ } -+ -+ if (interrupted) { -+ Thread.currentThread().interrupt(); -+ } -+ -+ ret = completed.getPlain(); -+ } // else: became loaded during the scheduling attempt, need to ensure load() is invoked -+ -+ ret.load(); -+ -+ this.removeTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId); -+ -+ return ret; -+ } -+ -+ void addChangedStatuses(final List<NewChunkHolder> changedFullStatus) { -+ if (changedFullStatus.isEmpty()) { -+ return; -+ } -+ if (!TickThread.isTickThread()) { -+ this.taskScheduler.scheduleChunkTask(() -> { -+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate; -+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { -+ pendingFullLoadUpdate.add(changedFullStatus.get(i)); -+ } -+ -+ ChunkHolderManager.this.processPendingFullUpdate(); -+ }, Priority.HIGHEST); -+ } else { -+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate; -+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { -+ pendingFullLoadUpdate.add(changedFullStatus.get(i)); -+ } -+ } -+ } -+ -+ private void removeChunkHolder(final NewChunkHolder holder) { -+ holder.onUnload(); -+ this.autoSaveQueue.remove(holder); -+ ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder); -+ this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ)); -+ } -+ -+ // note: never call while inside the chunk system, this will absolutely break everything -+ public void processUnloads() { -+ TickThread.ensureTickThread("Cannot unload chunks off-main"); -+ -+ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { -+ throw new IllegalStateException("Cannot unload chunks recursively"); -+ } -+ final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift -+ final List<ChunkUnloadQueue.SectionToUnload> unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions(); -+ int unloadCountTentative = 0; -+ for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) { -+ final ChunkUnloadQueue.UnloadSection section -+ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ()); -+ -+ if (section == null) { -+ // removed concurrently -+ continue; -+ } -+ -+ // technically reading the size field is unsafe, and it may be incorrect. -+ // We assume that the error here cumulatively goes away over many ticks. If it did not, then it is possible -+ // for chunks to never unload or not unload fast enough. -+ unloadCountTentative += section.chunks.size(); -+ } -+ -+ if (unloadCountTentative <= 0) { -+ // no work to do -+ return; -+ } -+ -+ // We do need to process updates here so that any addTicket that is synchronised before this call does not go missed. -+ this.processTicketUpdates(); -+ -+ final int toUnloadCount = Math.max(50, (int)(unloadCountTentative * 0.05)); -+ int processedCount = 0; -+ -+ for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) { -+ final List<NewChunkHolder> stage1 = new ArrayList<>(); -+ final List<NewChunkHolder.UnloadState> stage2 = new ArrayList<>(); -+ -+ final int sectionLowerX = sectionRef.sectionX() << sectionShift; -+ final int sectionLowerZ = sectionRef.sectionZ() << sectionShift; -+ -+ // stage 1: set up for stage 2 while holding critical locks -+ ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ); -+ try { -+ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ); -+ try { -+ final ChunkUnloadQueue.UnloadSection section -+ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ()); -+ -+ if (section == null) { -+ // removed concurrently -+ continue; -+ } -+ -+ // collect the holders to run stage 1 on -+ final int sectionCount = section.chunks.size(); -+ -+ if ((sectionCount + processedCount) <= toUnloadCount) { -+ // we can just drain the entire section -+ -+ for (final LongIterator iterator = section.chunks.iterator(); iterator.hasNext();) { -+ final NewChunkHolder holder = this.chunkHolders.get(iterator.nextLong()); -+ if (holder == null) { -+ throw new IllegalStateException(); -+ } -+ stage1.add(holder); -+ } -+ -+ // remove section -+ this.unloadQueue.removeSection(sectionRef.sectionX(), sectionRef.sectionZ()); -+ } else { -+ // processedCount + len = toUnloadCount -+ // we cannot drain the entire section -+ for (int i = 0, len = toUnloadCount - processedCount; i < len; ++i) { -+ final NewChunkHolder holder = this.chunkHolders.get(section.chunks.removeFirstLong()); -+ if (holder == null) { -+ throw new IllegalStateException(); -+ } -+ stage1.add(holder); -+ } -+ } -+ -+ // run stage 1 -+ for (int i = 0, len = stage1.size(); i < len; ++i) { -+ final NewChunkHolder chunkHolder = stage1.get(i); -+ chunkHolder.removeFromUnloadQueue(); -+ if (chunkHolder.isSafeToUnload() != null) { -+ LOGGER.error("Chunkholder " + chunkHolder + " is not safe to unload but is inside the unload queue?"); -+ continue; -+ } -+ final NewChunkHolder.UnloadState state = chunkHolder.unloadStage1(); -+ if (state == null) { -+ // can unload immediately -+ this.removeChunkHolder(chunkHolder); -+ continue; -+ } -+ stage2.add(state); -+ } -+ } finally { -+ this.taskScheduler.schedulingLockArea.unlock(scheduleLock); -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ -+ // stage 2: invoke expensive unload logic, designed to run without locks thanks to stage 1 -+ final List<NewChunkHolder> stage3 = new ArrayList<>(stage2.size()); -+ -+ final Boolean before = this.blockTicketUpdates(); -+ try { -+ for (int i = 0, len = stage2.size(); i < len; ++i) { -+ final NewChunkHolder.UnloadState state = stage2.get(i); -+ final NewChunkHolder holder = state.holder(); -+ -+ holder.unloadStage2(state); -+ stage3.add(holder); -+ } -+ } finally { -+ this.unblockTicketUpdates(before); -+ } -+ -+ // stage 3: actually attempt to remove the chunk holders -+ ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ); -+ try { -+ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ); -+ try { -+ for (int i = 0, len = stage3.size(); i < len; ++i) { -+ final NewChunkHolder holder = stage3.get(i); -+ -+ if (holder.unloadStage3()) { -+ this.removeChunkHolder(holder); -+ } else { -+ // add cooldown so the next unload check is not immediately next tick -+ this.addTicketAtLevel(UNLOAD_COOLDOWN, CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ), MAX_TICKET_LEVEL, Unit.INSTANCE, false); -+ } -+ } -+ } finally { -+ this.taskScheduler.schedulingLockArea.unlock(scheduleLock); -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ -+ processedCount += stage1.size(); -+ -+ if (processedCount >= toUnloadCount) { -+ break; -+ } -+ } -+ } -+ -+ public enum TicketOperationType { -+ ADD, REMOVE, ADD_IF_REMOVED, ADD_AND_REMOVE -+ } -+ -+ public static record TicketOperation<T, V> ( -+ TicketOperationType op, long chunkCoord, -+ TicketType<T> ticketType, int ticketLevel, T identifier, -+ TicketType<V> ticketType2, int ticketLevel2, V identifier2 -+ ) { -+ -+ private TicketOperation(TicketOperationType op, long chunkCoord, -+ TicketType<T> ticketType, int ticketLevel, T identifier) { -+ this(op, chunkCoord, ticketType, ticketLevel, identifier, null, 0, null); -+ } -+ -+ public static <T> TicketOperation<T, T> addOp(final ChunkPos chunk, final TicketType<T> type, final int ticketLevel, final T identifier) { -+ return addOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); -+ } -+ -+ public static <T> TicketOperation<T, T> addOp(final int chunkX, final int chunkZ, final TicketType<T> type, final int ticketLevel, final T identifier) { -+ return addOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); -+ } -+ -+ public static <T> TicketOperation<T, T> addOp(final long chunk, final TicketType<T> type, final int ticketLevel, final T identifier) { -+ return new TicketOperation<>(TicketOperationType.ADD, chunk, type, ticketLevel, identifier); -+ } -+ -+ public static <T> TicketOperation<T, T> removeOp(final ChunkPos chunk, final TicketType<T> type, final int ticketLevel, final T identifier) { -+ return removeOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); -+ } -+ -+ public static <T> TicketOperation<T, T> removeOp(final int chunkX, final int chunkZ, final TicketType<T> type, final int ticketLevel, final T identifier) { -+ return removeOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); -+ } -+ -+ public static <T> TicketOperation<T, T> removeOp(final long chunk, final TicketType<T> type, final int ticketLevel, final T identifier) { -+ return new TicketOperation<>(TicketOperationType.REMOVE, chunk, type, ticketLevel, identifier); -+ } -+ -+ public static <T, V> TicketOperation<T, V> addIfRemovedOp(final long chunk, -+ final TicketType<T> addType, final int addLevel, final T addIdentifier, -+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) { -+ return new TicketOperation<>( -+ TicketOperationType.ADD_IF_REMOVED, chunk, addType, addLevel, addIdentifier, -+ removeType, removeLevel, removeIdentifier -+ ); -+ } -+ -+ public static <T, V> TicketOperation<T, V> addAndRemove(final long chunk, -+ final TicketType<T> addType, final int addLevel, final T addIdentifier, -+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) { -+ return new TicketOperation<>( -+ TicketOperationType.ADD_AND_REMOVE, chunk, addType, addLevel, addIdentifier, -+ removeType, removeLevel, removeIdentifier -+ ); -+ } -+ } -+ -+ private <T, V> boolean processTicketOp(TicketOperation<T, V> operation) { -+ boolean ret = false; -+ switch (operation.op) { -+ case ADD: { -+ ret |= this.addTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier); -+ break; -+ } -+ case REMOVE: { -+ ret |= this.removeTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier); -+ break; -+ } -+ case ADD_IF_REMOVED: { -+ ret |= this.addIfRemovedTicket( -+ operation.chunkCoord, -+ operation.ticketType, operation.ticketLevel, operation.identifier, -+ operation.ticketType2, operation.ticketLevel2, operation.identifier2 -+ ); -+ break; -+ } -+ case ADD_AND_REMOVE: { -+ ret = true; -+ this.addAndRemoveTickets( -+ operation.chunkCoord, -+ operation.ticketType, operation.ticketLevel, operation.identifier, -+ operation.ticketType2, operation.ticketLevel2, operation.identifier2 -+ ); -+ break; -+ } -+ } -+ -+ return ret; -+ } -+ -+ public void performTicketUpdates(final Collection<TicketOperation<?, ?>> operations) { -+ for (final TicketOperation<?, ?> operation : operations) { -+ this.processTicketOp(operation); -+ } -+ } -+ -+ private final ThreadLocal<Boolean> BLOCK_TICKET_UPDATES = ThreadLocal.withInitial(() -> { -+ return Boolean.FALSE; -+ }); -+ -+ public Boolean blockTicketUpdates() { -+ final Boolean ret = BLOCK_TICKET_UPDATES.get(); -+ BLOCK_TICKET_UPDATES.set(Boolean.TRUE); -+ return ret; -+ } -+ -+ public void unblockTicketUpdates(final Boolean before) { -+ BLOCK_TICKET_UPDATES.set(before); -+ } -+ -+ public boolean processTicketUpdates() { -+ return this.processTicketUpdates(true, null); -+ } -+ -+ private static final ThreadLocal<List<ChunkProgressionTask>> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>(); -+ -+ static List<ChunkProgressionTask> getCurrentTicketUpdateScheduling() { -+ return CURRENT_TICKET_UPDATE_SCHEDULING.get(); -+ } -+ -+ private boolean processTicketUpdates(final boolean processFullUpdates, List<ChunkProgressionTask> scheduledTasks) { -+ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { -+ throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager"); -+ } -+ if (!PlatformHooks.get().allowAsyncTicketUpdates() && !TickThread.isTickThread()) { -+ TickThread.ensureTickThread("Cannot asynchronously process ticket updates"); -+ } -+ -+ List<NewChunkHolder> changedFullStatus = null; -+ -+ final boolean isTickThread = TickThread.isTickThread(); -+ -+ boolean ret = false; -+ final boolean canProcessFullUpdates = processFullUpdates & isTickThread; -+ final boolean canProcessScheduling = scheduledTasks == null; -+ -+ if (this.ticketLevelPropagator.hasPendingUpdates()) { -+ if (scheduledTasks == null) { -+ scheduledTasks = new ArrayList<>(); -+ } -+ changedFullStatus = new ArrayList<>(); -+ -+ this.blockTicketUpdates(); -+ try { -+ ret |= this.ticketLevelPropagator.performUpdates( -+ this.ticketLockArea, this.taskScheduler.schedulingLockArea, -+ scheduledTasks, changedFullStatus -+ ); -+ } finally { -+ this.unblockTicketUpdates(Boolean.FALSE); -+ } -+ } -+ -+ if (changedFullStatus != null) { -+ this.addChangedStatuses(changedFullStatus); -+ } -+ -+ if (canProcessScheduling && scheduledTasks != null) { -+ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) { -+ scheduledTasks.get(i).schedule(); -+ } -+ } -+ -+ if (canProcessFullUpdates) { -+ ret |= this.processPendingFullUpdate(); -+ } -+ -+ return ret; -+ } -+ -+ // only call on tick thread -+ private boolean processPendingFullUpdate() { -+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate; -+ -+ boolean ret = false; -+ -+ final List<NewChunkHolder> changedFullStatus = new ArrayList<>(); -+ -+ NewChunkHolder holder; -+ while ((holder = pendingFullLoadUpdate.poll()) != null) { -+ ret |= holder.handleFullStatusChange(changedFullStatus); -+ -+ if (!changedFullStatus.isEmpty()) { -+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { -+ pendingFullLoadUpdate.add(changedFullStatus.get(i)); -+ } -+ changedFullStatus.clear(); -+ } -+ } -+ -+ return ret; -+ } -+ -+ public JsonObject getDebugJson() { -+ final JsonObject ret = new JsonObject(); -+ -+ ret.add("unload_queue", this.unloadQueue.toDebugJson()); -+ -+ final JsonArray holders = new JsonArray(); -+ ret.add("chunkholders", holders); -+ -+ for (final NewChunkHolder holder : this.getChunkHolders()) { -+ holders.add(holder.getDebugJson()); -+ } -+ -+ final JsonArray allTicketsJson = new JsonArray(); -+ ret.add("tickets", allTicketsJson); -+ -+ for (final Iterator<ConcurrentLong2ReferenceChainedHashTable.TableEntry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.entryIterator(); -+ iterator.hasNext();) { -+ final ConcurrentLong2ReferenceChainedHashTable.TableEntry<SortedArraySet<Ticket<?>>> coordinateTickets = iterator.next(); -+ final long coordinate = coordinateTickets.getKey(); -+ final SortedArraySet<Ticket<?>> tickets = coordinateTickets.getValue(); -+ -+ final JsonObject coordinateJson = new JsonObject(); -+ allTicketsJson.add(coordinateJson); -+ -+ coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); -+ coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); -+ -+ final JsonArray ticketsSerialized = new JsonArray(); -+ coordinateJson.add("tickets", ticketsSerialized); -+ -+ // note: by using a copy of the backing array, we can avoid explicit exceptions we may trip when iterating -+ // directly over the set using the iterator -+ // however, it also means we need to null-check the values, and there is a possibility that we _miss_ an -+ // entry OR iterate over an entry multiple times -+ for (final Object ticketUncasted : ((ChunkSystemSortedArraySet<Ticket<?>>)tickets).moonrise$copyBackingArray()) { -+ if (ticketUncasted == null) { -+ continue; -+ } -+ final Ticket<?> ticket = (Ticket<?>)ticketUncasted; -+ final JsonObject ticketSerialized = new JsonObject(); -+ ticketsSerialized.add(ticketSerialized); -+ -+ ticketSerialized.addProperty("type", ticket.getType().toString()); -+ ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel())); -+ ticketSerialized.addProperty("identifier", Objects.toString(ticket.key)); -+ ticketSerialized.addProperty("remove_tick", Long.valueOf(((ChunkSystemTicket<?>)(Object)ticket).moonrise$getRemoveDelay())); -+ } -+ } -+ -+ return ret; -+ } -+} -diff --git a/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<Long> CHUNK_LOAD = TicketType.create("chunk_system:chunk_load", Long::compareTo); -+ private static final AtomicLong CHUNK_LOAD_IDS = new AtomicLong(); -+ -+ public static Long getNextChunkLoadId() { -+ return Long.valueOf(CHUNK_LOAD_IDS.getAndIncrement()); -+ } -+ -+ public static final TicketType<Long> NON_FULL_CHUNK_LOAD = TicketType.create("chunk_system:non_full_load", Long::compareTo); -+ private static final AtomicLong NON_FULL_CHUNK_LOAD_IDS = new AtomicLong(); -+ -+ public static Long getNextNonFullLoadId() { -+ return Long.valueOf(NON_FULL_CHUNK_LOAD_IDS.getAndIncrement()); -+ } -+ -+ public static final TicketType<Long> ENTITY_LOAD = TicketType.create("chunk_system:entity_load", Long::compareTo); -+ private static final AtomicLong ENTITY_LOAD_IDS = new AtomicLong(); -+ -+ public static Long getNextEntityLoadId() { -+ return Long.valueOf(ENTITY_LOAD_IDS.getAndIncrement()); -+ } -+ -+ public static final TicketType<Long> POI_LOAD = TicketType.create("chunk_system:poi_load", Long::compareTo); -+ private static final AtomicLong POI_LOAD_IDS = new AtomicLong(); -+ -+ public static Long getNextPoiLoadId() { -+ return Long.valueOf(POI_LOAD_IDS.getAndIncrement()); -+ } -+ -+ public static final TicketType<Long> CHUNK_RELIGHT = TicketType.create("starlight:chunk_relight", Long::compareTo); -+ private static final AtomicLong CHUNK_RELIGHT_IDS = new AtomicLong(); -+ -+ public static Long getNextChunkRelightId() { -+ return Long.valueOf(CHUNK_RELIGHT_IDS.getAndIncrement()); -+ } -+ -+ -+ public static int getTicketLevel(final ChunkStatus status) { -+ return ChunkLevel.byStatus(status); -+ } -+ -+ public final ServerLevel world; -+ public final RadiusAwarePrioritisedExecutor radiusAwareScheduler; -+ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor parallelGenExecutor; -+ private final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor radiusAwareGenExecutor; -+ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor loadExecutor; -+ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor ioExecutor; -+ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor compressionExecutor; -+ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor saveExecutor; -+ -+ private final PrioritisedTaskQueue mainThreadExecutor = new PrioritisedTaskQueue(); -+ -+ public final ChunkHolderManager chunkHolderManager; -+ -+ static { -+ ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setWriteRadius(0); -+ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_STARTS).moonrise$setWriteRadius(0); -+ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setWriteRadius(0); -+ ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setWriteRadius(0); -+ ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setWriteRadius(0); -+ ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setWriteRadius(0); -+ ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setWriteRadius(0); -+ ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setWriteRadius(1); -+ ((ChunkSystemChunkStatus)ChunkStatus.INITIALIZE_LIGHT).moonrise$setWriteRadius(0); -+ ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$setWriteRadius(2); -+ ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setWriteRadius(0); -+ ((ChunkSystemChunkStatus)ChunkStatus.FULL).moonrise$setWriteRadius(0); -+ -+ ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setEmptyLoadStatus(true); -+ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setEmptyLoadStatus(true); -+ ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setEmptyLoadStatus(true); -+ ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setEmptyLoadStatus(true); -+ ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setEmptyLoadStatus(true); -+ ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setEmptyLoadStatus(true); -+ ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setEmptyLoadStatus(true); -+ ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setEmptyLoadStatus(true); -+ -+ /* -+ It's important that the neighbour read radius is taken into account. If _any_ later status is using some chunk as -+ a neighbour, it must be also safe if that neighbour is being generated. i.e for any status later than FEATURES, -+ for a status to be parallel safe it must not read the block data from its neighbours. -+ */ -+ final List<ChunkStatus> parallelCapableStatus = Arrays.asList( -+ // No-op executor. -+ ChunkStatus.EMPTY, -+ -+ // This is parallel capable, as CB has fixed the concurrency issue with stronghold generations. -+ // Does not touch neighbour chunks. -+ ChunkStatus.STRUCTURE_STARTS, -+ -+ // Surprisingly this is parallel capable. It is simply reading the already-created structure starts -+ // into the structure references for the chunk. So while it reads from it neighbours, its neighbours -+ // will not change, even if executed in parallel. -+ ChunkStatus.STRUCTURE_REFERENCES, -+ -+ // Safe. Mojang runs it in parallel as well. -+ ChunkStatus.BIOMES, -+ -+ // Safe. Mojang runs it in parallel as well. -+ ChunkStatus.NOISE, -+ -+ // Parallel safe. Only touches the target chunk. Biome retrieval is now noise based, which is -+ // completely thread-safe. -+ ChunkStatus.SURFACE, -+ -+ // No global state is modified in the carvers. It only touches the specified chunk. So it is parallel safe. -+ ChunkStatus.CARVERS, -+ -+ // FEATURES is not parallel safe. It writes to neighbours. -+ -+ // no-op executor -+ ChunkStatus.INITIALIZE_LIGHT -+ -+ // LIGHT is not parallel safe. It also doesn't run on the generation executor, so no point. -+ -+ // Only writes to the specified chunk. State is not read by later statuses. Parallel safe. -+ // Note: it may look unsafe because it writes to a worldgenregion, but the region size is always 0 - -+ // see the task margin. -+ // However, if the neighbouring FEATURES chunk is unloaded, but then fails to load in again (for whatever -+ // reason), then it would write to this chunk - and since this status reads blocks from itself, it's not -+ // safe to execute this in parallel. -+ // SPAWN -+ -+ // FULL is executed on main. -+ ); -+ -+ for (final ChunkStatus status : parallelCapableStatus) { -+ ((ChunkSystemChunkStatus)status).moonrise$setParallelCapable(true); -+ } -+ } -+ -+ private static final int[] ACCESS_RADIUS_TABLE_LOAD = new int[ChunkStatus.getStatusList().size()]; -+ private static final int[] ACCESS_RADIUS_TABLE_GEN = new int[ChunkStatus.getStatusList().size()]; -+ private static final int[] ACCESS_RADIUS_TABLE = new int[ChunkStatus.getStatusList().size()]; -+ static { -+ Arrays.fill(ACCESS_RADIUS_TABLE_LOAD, -1); -+ Arrays.fill(ACCESS_RADIUS_TABLE_GEN, -1); -+ Arrays.fill(ACCESS_RADIUS_TABLE, -1); -+ } -+ -+ private static int getAccessRadius0(final ChunkStatus toStatus, final ChunkPyramid pyramid) { -+ if (toStatus == ChunkStatus.EMPTY) { -+ return 0; -+ } -+ -+ final ChunkStep chunkStep = pyramid.getStepTo(toStatus); -+ -+ final int radius = chunkStep.getAccumulatedRadiusOf(ChunkStatus.EMPTY); -+ int maxRange = radius; -+ -+ for (int dist = 0; dist <= radius; ++dist) { -+ final ChunkStatus requiredNeighbourStatus = ((ChunkSystemChunkStep)(Object)chunkStep).moonrise$getRequiredStatusAtRadius(dist); -+ final int rad = ACCESS_RADIUS_TABLE[requiredNeighbourStatus.getIndex()]; -+ if (rad == -1) { -+ throw new IllegalStateException(); -+ } -+ -+ maxRange = Math.max(maxRange, dist + rad); -+ } -+ -+ return maxRange; -+ } -+ -+ private static final int MAX_ACCESS_RADIUS; -+ -+ static { -+ final List<ChunkStatus> statuses = ChunkStatus.getStatusList(); -+ for (int i = 0, len = statuses.size(); i < len; ++i) { -+ final ChunkStatus status = statuses.get(i); -+ ACCESS_RADIUS_TABLE_LOAD[i] = getAccessRadius0(status, ChunkPyramid.LOADING_PYRAMID); -+ ACCESS_RADIUS_TABLE_GEN[i] = getAccessRadius0(status, ChunkPyramid.GENERATION_PYRAMID); -+ ACCESS_RADIUS_TABLE[i] = Math.max( -+ ACCESS_RADIUS_TABLE_LOAD[i], -+ ACCESS_RADIUS_TABLE_GEN[i] -+ ); -+ } -+ MAX_ACCESS_RADIUS = ACCESS_RADIUS_TABLE[ACCESS_RADIUS_TABLE.length - 1]; -+ } -+ -+ public static int getMaxAccessRadius() { -+ return MAX_ACCESS_RADIUS; -+ } -+ -+ public static int getAccessRadius(final ChunkStatus genStatus) { -+ return ACCESS_RADIUS_TABLE[genStatus.getIndex()]; -+ } -+ -+ public static int getAccessRadius(final FullChunkStatus status) { -+ return (status.ordinal() - 1) + getAccessRadius(ChunkStatus.FULL); -+ } -+ -+ -+ public final ReentrantAreaLock schedulingLockArea; -+ private final int lockShift; -+ -+ public final int getChunkSystemLockShift() { -+ return this.lockShift; -+ } -+ -+ 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<String, Object> objectsOfInterest, final Throwable thr) { -+ final NewChunkHolder holder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ); -+ LOGGER.error("Chunk system error at chunk (" + chunkX + "," + chunkZ + "), holder: " + holder + ", exception:", new Throwable(thr)); -+ -+ if (this.failedChunkSystem.getAndSet(true)) { -+ return; -+ } -+ -+ final ReportedException reportedException = thr instanceof ReportedException ? (ReportedException)thr : new ReportedException(new CrashReport("Chunk system error", thr)); -+ -+ CrashReportCategory crashReportCategory = reportedException.getReport().addCategory("Chunk system details"); -+ crashReportCategory.setDetail("Chunk coordinate", new ChunkPos(chunkX, chunkZ).toString()); -+ crashReportCategory.setDetail("ChunkHolder", Objects.toString(holder)); -+ crashReportCategory.setDetail("unrecoverableChunkSystemFailure caller thread", Thread.currentThread().getName()); -+ -+ crashReportCategory = reportedException.getReport().addCategory("Chunk System Objects of Interest"); -+ for (final Map.Entry<String, Object> entry : objectsOfInterest.entrySet()) { -+ if (entry.getValue() instanceof Throwable thrObject) { -+ crashReportCategory.setDetailError(Objects.toString(entry.getKey()), thrObject); -+ } else { -+ crashReportCategory.setDetail(Objects.toString(entry.getKey()), Objects.toString(entry.getValue())); -+ } -+ } -+ -+ final Runnable crash = () -> { -+ throw new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException); -+ }; -+ -+ // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions -+ this.scheduleChunkTask(chunkX, chunkZ, crash, Priority.BLOCKING); -+ // so, make the main thread pick it up -+ ((ChunkSystemMinecraftServer)this.world.getServer()).moonrise$setChunkSystemCrash(new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException)); -+ } -+ -+ public boolean executeMainThreadTask() { -+ TickThread.ensureTickThread("Cannot execute main thread task off-main"); -+ return this.mainThreadExecutor.executeTask(); -+ } -+ -+ public void raisePriority(final int x, final int z, final Priority priority) { -+ this.chunkHolderManager.raisePriority(x, z, priority); -+ } -+ -+ public void setPriority(final int x, final int z, final Priority priority) { -+ this.chunkHolderManager.setPriority(x, z, priority); -+ } -+ -+ public void lowerPriority(final int x, final int z, final Priority priority) { -+ this.chunkHolderManager.lowerPriority(x, z, priority); -+ } -+ -+ public void scheduleTickingState(final int chunkX, final int chunkZ, final FullChunkStatus toStatus, -+ final boolean addTicket, final Priority priority, -+ final Consumer<LevelChunk> onComplete) { -+ final int radius = toStatus.ordinal() - 1; // 0 -> BORDER, 1 -> TICKING, 2 -> ENTITY_TICKING -+ -+ if (!TickThread.isTickThreadFor(this.world, chunkX, chunkZ, Math.max(0, radius))) { -+ this.scheduleChunkTask(chunkX, chunkZ, () -> { -+ ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -+ }, priority); -+ return; -+ } -+ final int accessRadius = getAccessRadius(toStatus); -+ if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { -+ throw new IllegalStateException("Cannot schedule chunk load during ticket level update"); -+ } -+ if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { -+ throw new IllegalStateException("Cannot schedule chunk loading recursively"); -+ } -+ -+ if (toStatus == FullChunkStatus.INACCESSIBLE) { -+ throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); -+ } -+ -+ final int minLevel = 33 - (toStatus.ordinal() - 1); -+ final Long chunkReference = addTicket ? getNextChunkLoadId() : null; -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ -+ if (addTicket) { -+ this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); -+ this.chunkHolderManager.processTicketUpdates(); -+ } -+ -+ final Consumer<LevelChunk> loadCallback = onComplete == null && !addTicket ? null : (final LevelChunk chunk) -> { -+ try { -+ if (onComplete != null) { -+ onComplete.accept(chunk); -+ } -+ } finally { -+ if (addTicket) { -+ ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); -+ } -+ } -+ }; -+ -+ final boolean scheduled; -+ final LevelChunk chunk; -+ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); -+ try { -+ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); -+ try { -+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); -+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { -+ scheduled = false; -+ chunk = null; -+ } else { -+ final FullChunkStatus currStatus = chunkHolder.getChunkStatus(); -+ if (currStatus.isOrAfter(toStatus)) { -+ scheduled = false; -+ chunk = (LevelChunk)chunkHolder.getCurrentChunk(); -+ } else { -+ scheduled = true; -+ chunk = null; -+ -+ for (int dz = -radius; dz <= radius; ++dz) { -+ for (int dx = -radius; dx <= radius; ++dx) { -+ final NewChunkHolder neighbour = -+ (dx | dz) == 0 ? chunkHolder : this.chunkHolderManager.getChunkHolder(dx + chunkX, dz + chunkZ); -+ if (neighbour != null) { -+ neighbour.raisePriority(priority); -+ } -+ } -+ } -+ -+ // ticket level should schedule for us -+ if (loadCallback != null) { -+ chunkHolder.addFullStatusConsumer(toStatus, loadCallback); -+ } -+ } -+ } -+ } finally { -+ this.schedulingLockArea.unlock(schedulingLock); -+ } -+ } finally { -+ this.chunkHolderManager.ticketLockArea.unlock(ticketLock); -+ } -+ -+ if (loadCallback != null && !scheduled) { -+ // couldn't schedule -+ try { -+ loadCallback.accept(chunk); -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to process chunk full status callback", thr); -+ } -+ } -+ } -+ -+ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final boolean gen, final ChunkStatus toStatus, final boolean addTicket, -+ final Priority priority, final Consumer<ChunkAccess> onComplete) { -+ if (gen) { -+ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -+ return; -+ } -+ this.scheduleChunkLoad(chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { -+ if (chunk == null) { -+ if (onComplete != null) { -+ onComplete.accept(null); -+ } -+ } else { -+ if (chunk.getPersistedStatus().isOrAfter(toStatus)) { -+ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -+ } else { -+ if (onComplete != null) { -+ onComplete.accept(null); -+ } -+ } -+ } -+ }); -+ } -+ -+ // only appropriate to use with syncLoadNonFull -+ public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus, -+ final Priority priority) { -+ final int accessRadius = getAccessRadius(toStatus); -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus); -+ final List<ChunkProgressionTask> tasks = new ArrayList<>(); -+ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention -+ try { -+ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention -+ try { -+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); -+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { -+ return false; -+ } else { -+ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus(); -+ if (genStatus != null && genStatus.isOrAfter(toStatus)) { -+ return true; -+ } else { -+ chunkHolder.raisePriority(priority); -+ -+ if (!chunkHolder.upgradeGenTarget(toStatus)) { -+ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); -+ } -+ } -+ } -+ } finally { -+ this.schedulingLockArea.unlock(schedulingLock); -+ } -+ } finally { -+ this.chunkHolderManager.ticketLockArea.unlock(ticketLock); -+ } -+ -+ for (int i = 0, len = tasks.size(); i < len; ++i) { -+ tasks.get(i).schedule(); -+ } -+ -+ return true; -+ } -+ -+ // Note: on Moonrise the non-full sync load requires blocking on managedBlock, but this is fine since there is only -+ // one main thread. On Folia, it is required that the non-full load can occur completely asynchronously to avoid deadlock -+ // between regions -+ public ChunkAccess syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status) { -+ if (status == null || status.isOrAfter(ChunkStatus.FULL)) { -+ throw new IllegalArgumentException("Status: " + status); -+ } -+ -+ if (!TickThread.isTickThread()) { -+ return this.world.getChunkSource().getChunk(chunkX, chunkZ, status, true); -+ } -+ -+ ChunkAccess loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status); -+ if (loaded != null) { -+ return loaded; -+ } -+ -+ 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<ChunkAccess> onComplete) { -+ if (!TickThread.isTickThreadFor(this.world, chunkX, chunkZ)) { -+ this.scheduleChunkTask(chunkX, chunkZ, () -> { -+ ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -+ }, priority); -+ return; -+ } -+ final int accessRadius = getAccessRadius(toStatus); -+ if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { -+ throw new IllegalStateException("Cannot schedule chunk load during ticket level update"); -+ } -+ if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { -+ throw new IllegalStateException("Cannot schedule chunk loading recursively"); -+ } -+ -+ if (toStatus == ChunkStatus.FULL) { -+ this.scheduleTickingState(chunkX, chunkZ, FullChunkStatus.FULL, addTicket, priority, (Consumer)onComplete); -+ return; -+ } -+ -+ final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus); -+ final Long chunkReference = addTicket ? getNextChunkLoadId() : null; -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ -+ if (addTicket) { -+ this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); -+ this.chunkHolderManager.processTicketUpdates(); -+ } -+ -+ final Consumer<ChunkAccess> loadCallback = onComplete == null && !addTicket ? null : (final ChunkAccess chunk) -> { -+ try { -+ if (onComplete != null) { -+ onComplete.accept(chunk); -+ } -+ } finally { -+ if (addTicket) { -+ ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); -+ } -+ } -+ }; -+ -+ final List<ChunkProgressionTask> tasks = new ArrayList<>(); -+ -+ final boolean scheduled; -+ final ChunkAccess chunk; -+ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); -+ try { -+ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); -+ try { -+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); -+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { -+ scheduled = false; -+ chunk = null; -+ } else { -+ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus(); -+ if (genStatus != null && genStatus.isOrAfter(toStatus)) { -+ scheduled = false; -+ chunk = chunkHolder.getCurrentChunk(); -+ } else { -+ scheduled = true; -+ chunk = null; -+ chunkHolder.raisePriority(priority); -+ -+ if (!chunkHolder.upgradeGenTarget(toStatus)) { -+ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); -+ } -+ if (loadCallback != null) { -+ chunkHolder.addStatusConsumer(toStatus, loadCallback); -+ } -+ } -+ } -+ } finally { -+ this.schedulingLockArea.unlock(schedulingLock); -+ } -+ } finally { -+ this.chunkHolderManager.ticketLockArea.unlock(ticketLock); -+ } -+ -+ for (int i = 0, len = tasks.size(); i < len; ++i) { -+ tasks.get(i).schedule(); -+ } -+ -+ if (loadCallback != null && !scheduled) { -+ // couldn't schedule -+ try { -+ loadCallback.accept(chunk); -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to process chunk status callback", thr); -+ } -+ } -+ } -+ -+ private ChunkProgressionTask createTask(final int chunkX, final int chunkZ, final ChunkAccess chunk, -+ final NewChunkHolder chunkHolder, final StaticCache2D<GenerationChunkHolder> neighbours, -+ final ChunkStatus toStatus, final Priority initialPriority) { -+ if (toStatus == ChunkStatus.EMPTY) { -+ return new ChunkLoadTask(this, this.world, chunkX, chunkZ, chunkHolder, initialPriority); -+ } -+ if (toStatus == ChunkStatus.LIGHT) { -+ return new ChunkLightTask(this, this.world, chunkX, chunkZ, chunk, initialPriority); -+ } -+ if (toStatus == ChunkStatus.FULL) { -+ return new ChunkFullTask(this, this.world, chunkX, chunkZ, chunkHolder, chunk, initialPriority); -+ } -+ -+ return new ChunkUpgradeGenericStatusTask(this, this.world, chunkX, chunkZ, chunk, neighbours, toStatus, initialPriority); -+ } -+ -+ ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, final NewChunkHolder chunkHolder, -+ final List<ChunkProgressionTask> allTasks) { -+ return this.schedule(chunkX, chunkZ, targetStatus, chunkHolder, allTasks, chunkHolder.getEffectivePriority(Priority.NORMAL)); -+ } -+ -+ // rets new task scheduled for the _specified_ chunk -+ // note: this must hold the scheduling lock -+ // minPriority is only used to pass the priority through to neighbours, as priority calculation has not yet been done -+ // schedule will ignore the generation target, so it should be checked by the caller to ensure the target is not regressed! -+ private ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, -+ final NewChunkHolder chunkHolder, final List<ChunkProgressionTask> allTasks, -+ final Priority minPriority) { -+ if (!this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, getAccessRadius(targetStatus))) { -+ throw new IllegalStateException("Not holding scheduling lock"); -+ } -+ -+ if (chunkHolder.hasGenerationTask()) { -+ chunkHolder.upgradeGenTarget(targetStatus); -+ return null; -+ } -+ -+ final Priority requestedPriority = Priority.max( -+ minPriority, chunkHolder.getEffectivePriority(Priority.NORMAL) -+ ); -+ final ChunkStatus currentGenStatus = chunkHolder.getCurrentGenStatus(); -+ final ChunkAccess chunk = chunkHolder.getCurrentChunk(); -+ -+ if (currentGenStatus == null) { -+ // not yet loaded -+ final ChunkProgressionTask task = this.createTask( -+ chunkX, chunkZ, chunk, chunkHolder, null, ChunkStatus.EMPTY, requestedPriority -+ ); -+ -+ allTasks.add(task); -+ -+ final List<NewChunkHolder> chunkHolderNeighbours = new ArrayList<>(1); -+ chunkHolderNeighbours.add(chunkHolder); -+ -+ chunkHolder.setGenerationTarget(targetStatus); -+ chunkHolder.setGenerationTask(task, ChunkStatus.EMPTY, chunkHolderNeighbours); -+ -+ return task; -+ } -+ -+ if (currentGenStatus.isOrAfter(targetStatus)) { -+ // nothing to do -+ return null; -+ } -+ -+ // we know for sure now that we want to schedule _something_, so set the target -+ chunkHolder.setGenerationTarget(targetStatus); -+ -+ final ChunkStatus chunkRealStatus = chunk.getPersistedStatus(); -+ final ChunkStatus toStatus = ((ChunkSystemChunkStatus)currentGenStatus).moonrise$getNextStatus(); -+ final ChunkPyramid chunkPyramid = chunkRealStatus.isOrAfter(toStatus) ? ChunkPyramid.LOADING_PYRAMID : ChunkPyramid.GENERATION_PYRAMID; -+ final ChunkStep chunkStep = chunkPyramid.getStepTo(toStatus); -+ -+ final int neighbourReadRadius = Math.max( -+ 0, -+ chunkStep.getAccumulatedRadiusOf(ChunkStatus.EMPTY) -+ ); -+ -+ boolean unGeneratedNeighbours = false; -+ -+ if (neighbourReadRadius > 0) { -+ final ChunkMap chunkMap = this.world.getChunkSource().chunkMap; -+ for (final long pos : ParallelSearchRadiusIteration.getSearchIteration(neighbourReadRadius)) { -+ final int x = CoordinateUtils.getChunkX(pos); -+ final int z = CoordinateUtils.getChunkZ(pos); -+ final int radius = Math.max(Math.abs(x), Math.abs(z)); -+ final ChunkStatus requiredNeighbourStatus = ((ChunkSystemChunkStep)(Object)chunkStep).moonrise$getRequiredStatusAtRadius(radius); -+ -+ unGeneratedNeighbours |= this.checkNeighbour( -+ chunkX + x, chunkZ + z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority -+ ); -+ } -+ } -+ -+ if (unGeneratedNeighbours) { -+ // can't schedule, but neighbour completion will schedule for us when they're ALL done -+ -+ // propagate our priority to neighbours -+ chunkHolder.recalculateNeighbourPriorities(); -+ return null; -+ } -+ -+ // need to gather neighbours -+ -+ final List<NewChunkHolder> chunkHolderNeighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1)); -+ final StaticCache2D<GenerationChunkHolder> neighbours = StaticCache2D -+ .create(chunkX, chunkZ, neighbourReadRadius, (final int nx, final int nz) -> { -+ final NewChunkHolder holder = nx == chunkX && nz == chunkZ ? chunkHolder : this.chunkHolderManager.getChunkHolder(nx, nz); -+ chunkHolderNeighbours.add(holder); -+ -+ return holder.vanillaChunkHolder; -+ }); -+ -+ final ChunkProgressionTask task = this.createTask( -+ chunkX, chunkZ, chunk, chunkHolder, neighbours, toStatus, -+ chunkHolder.getEffectivePriority(Priority.NORMAL) -+ ); -+ allTasks.add(task); -+ -+ chunkHolder.setGenerationTask(task, toStatus, chunkHolderNeighbours); -+ -+ return task; -+ } -+ -+ // rets true if the neighbour is not at the required status, false otherwise -+ private boolean checkNeighbour(final int chunkX, final int chunkZ, final ChunkStatus requiredStatus, final NewChunkHolder center, -+ final List<ChunkProgressionTask> tasks, final Priority minPriority) { -+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ); -+ -+ if (chunkHolder == null) { -+ throw new IllegalStateException("Missing chunkholder when required"); -+ } -+ -+ final ChunkStatus holderStatus = chunkHolder.getCurrentGenStatus(); -+ if (holderStatus != null && holderStatus.isOrAfter(requiredStatus)) { -+ return false; -+ } -+ -+ if (chunkHolder.hasFailedGeneration()) { -+ return true; -+ } -+ -+ center.addGenerationBlockingNeighbour(chunkHolder); -+ chunkHolder.addWaitingNeighbour(center, requiredStatus); -+ -+ if (chunkHolder.upgradeGenTarget(requiredStatus)) { -+ return true; -+ } -+ -+ // not at status required, so we need to schedule its generation -+ this.schedule( -+ chunkX, chunkZ, requiredStatus, chunkHolder, tasks, minPriority -+ ); -+ -+ return true; -+ } -+ -+ /** -+ * @deprecated Chunk tasks must be tied to coordinates in the future -+ */ -+ @Deprecated -+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) { -+ return this.scheduleChunkTask(run, Priority.NORMAL); -+ } -+ -+ /** -+ * @deprecated Chunk tasks must be tied to coordinates in the future -+ */ -+ @Deprecated -+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final Priority priority) { -+ return this.mainThreadExecutor.queueTask(run, priority); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) { -+ return this.createChunkTask(chunkX, chunkZ, run, Priority.NORMAL); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run, -+ final Priority priority) { -+ return this.mainThreadExecutor.createTask(run, priority); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) { -+ return this.scheduleChunkTask(chunkX, chunkZ, run, Priority.NORMAL); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run, -+ final Priority priority) { -+ return this.mainThreadExecutor.queueTask(run, priority); -+ } -+ -+ public boolean halt(final boolean sync, final long maxWaitNS) { -+ this.radiusAwareGenExecutor.halt(); -+ this.parallelGenExecutor.halt(); -+ this.loadExecutor.halt(); -+ if (sync) { -+ final long time = System.nanoTime(); -+ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { -+ if ( -+ !this.radiusAwareGenExecutor.isActive() && -+ !this.parallelGenExecutor.isActive() && -+ !this.loadExecutor.isActive() -+ ) { -+ return true; -+ } -+ if ((System.nanoTime() - time) >= maxWaitNS) { -+ return false; -+ } -+ } -+ } -+ -+ return true; -+ } -+ -+ public boolean haltIO(final boolean sync, final long maxWaitNS) { -+ this.ioExecutor.halt(); -+ this.saveExecutor.halt(); -+ this.compressionExecutor.halt(); -+ if (sync) { -+ final long time = System.nanoTime(); -+ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { -+ if ( -+ !this.ioExecutor.isActive() && -+ !this.saveExecutor.isActive() && -+ !this.compressionExecutor.isActive() -+ ) { -+ return true; -+ } -+ if ((System.nanoTime() - time) >= maxWaitNS) { -+ return false; -+ } -+ } -+ } -+ -+ return true; -+ } -+ -+ public static final ArrayDeque<ChunkInfo> WAITING_CHUNKS = new ArrayDeque<>(); // stack -+ -+ public static final class ChunkInfo { -+ -+ public final int chunkX; -+ public final int chunkZ; -+ public final ServerLevel world; -+ -+ public ChunkInfo(final int chunkX, final int chunkZ, final ServerLevel world) { -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.world = world; -+ } -+ -+ public JsonObject toJson() { -+ final JsonObject ret = new JsonObject(); -+ -+ ret.addProperty("chunk-x", Integer.valueOf(this.chunkX)); -+ ret.addProperty("chunk-z", Integer.valueOf(this.chunkZ)); -+ ret.addProperty("world-name", WorldUtil.getWorldName(this.world)); -+ -+ return ret; -+ } -+ -+ @Override -+ public String toString() { -+ return "[( " + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "']"; -+ } -+ } -+ -+ public static void pushChunkWait(final ServerLevel world, final int chunkX, final int chunkZ) { -+ synchronized (WAITING_CHUNKS) { -+ WAITING_CHUNKS.push(new ChunkInfo(chunkX, chunkZ, world)); -+ } -+ } -+ -+ public static void popChunkWait() { -+ synchronized (WAITING_CHUNKS) { -+ WAITING_CHUNKS.pop(); -+ } -+ } -+ -+ public static ChunkInfo[] getChunkInfos() { -+ synchronized (WAITING_CHUNKS) { -+ return WAITING_CHUNKS.toArray(new ChunkInfo[0]); -+ } -+ } -+ -+ private static JsonObject debugPlayer(final ServerPlayer player) { -+ final Level world = player.level(); -+ -+ final JsonObject ret = new JsonObject(); -+ -+ ret.addProperty("name", player.getScoreboardName()); -+ ret.addProperty("uuid", player.getUUID().toString()); -+ ret.addProperty("real", ((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()); -+ -+ ret.addProperty("world-name", WorldUtil.getWorldName(world)); -+ -+ final Vec3 pos = player.position(); -+ -+ ret.addProperty("x", pos.x); -+ ret.addProperty("y", pos.y); -+ ret.addProperty("z", pos.z); -+ -+ final Entity.RemovalReason removalReason = player.getRemovalReason(); -+ -+ ret.addProperty("removal-reason", removalReason == null ? "null" : removalReason.name()); -+ -+ ret.add("view-distances", ((ChunkSystemServerPlayer)player).moonrise$getViewDistanceHolder().toJson()); -+ -+ return ret; -+ } -+ -+ public JsonObject getDebugJson() { -+ final JsonObject ret = new JsonObject(); -+ -+ ret.addProperty("lock_shift", Integer.valueOf(this.getChunkSystemLockShift())); -+ ret.addProperty("ticket_shift", Integer.valueOf(ThreadedTicketLevelPropagator.SECTION_SHIFT)); -+ ret.addProperty("region_shift", Integer.valueOf(((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift())); -+ -+ ret.addProperty("name", WorldUtil.getWorldName(this.world)); -+ ret.addProperty("view-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPIViewDistance()); -+ ret.addProperty("tick-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPITickDistance()); -+ ret.addProperty("send-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPISendViewDistance()); -+ -+ final JsonArray players = new JsonArray(); -+ ret.add("players", players); -+ -+ for (final ServerPlayer player : this.world.players()) { -+ players.add(debugPlayer(player)); -+ } -+ -+ ret.add("chunk-holder-manager", this.chunkHolderManager.getDebugJson()); -+ -+ return ret; -+ } -+ -+ public static JsonObject debugAllWorlds(final MinecraftServer server) { -+ final JsonObject ret = new JsonObject(); -+ -+ ret.addProperty("data-version", 2); -+ -+ final JsonArray allPlayers = new JsonArray(); -+ ret.add("all-players", allPlayers); -+ -+ for (final ServerPlayer player : server.getPlayerList().getPlayers()) { -+ allPlayers.add(debugPlayer(player)); -+ } -+ -+ final JsonArray chunkWaitInfos = new JsonArray(); -+ ret.add("chunk-wait-infos", chunkWaitInfos); -+ -+ for (final ChunkTaskScheduler.ChunkInfo info : getChunkInfos()) { -+ chunkWaitInfos.add(info.toJson()); -+ } -+ -+ final JsonArray worlds = new JsonArray(); -+ ret.add("worlds", worlds); -+ -+ for (final ServerLevel world : server.getAllLevels()) { -+ worlds.add(((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().getDebugJson()); -+ } -+ -+ return ret; -+ } -+ -+ public static File getChunkDebugFile() { -+ return new File( -+ new File(new File("."), "debug"), -+ "chunks-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt" -+ ); -+ } -+ -+ public static void dumpAllChunkLoadInfo(final MinecraftServer server, final boolean writeDebugInfo) { -+ final ChunkInfo[] chunkInfos = getChunkInfos(); -+ if (chunkInfos.length > 0) { -+ LOGGER.error("Chunk wait task info below: "); -+ for (final ChunkInfo chunkInfo : chunkInfos) { -+ final NewChunkHolder holder = ((ChunkSystemServerLevel)chunkInfo.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkInfo.chunkX, chunkInfo.chunkZ); -+ LOGGER.error("Chunk wait: " + chunkInfo); -+ LOGGER.error("Chunk holder: " + holder); -+ } -+ -+ if (writeDebugInfo) { -+ final File file = getChunkDebugFile(); -+ LOGGER.error("Writing chunk information dump to " + file); -+ try { -+ JsonUtil.writeJson(ChunkTaskScheduler.debugAllWorlds(server), file); -+ LOGGER.error("Successfully written chunk information!"); -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to dump chunk information to file " + file.toString(), thr); -+ } -+ } -+ } -+ } -+} -diff --git a/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<Entity> entities = ChunkEntitySlices.readEntities(this.world, entityChunk); -+ -+ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().addEntityChunkEntities(entities, new ChunkPos(this.chunkX, this.chunkZ)); -+ } -+ } -+ -+ return ret; -+ } -+ -+ // needed to distinguish whether the entity chunk has been read from disk but is empty or whether it has _not_ -+ // been read from disk -+ private static final CompoundTag EMPTY_ENTITY_CHUNK = new CompoundTag(); -+ -+ private ChunkLoadTask.EntityDataLoadTask entityDataLoadTask; -+ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0, -+ // then the task is rescheduled -+ private List<GenericDataLoadTaskCallback> entityDataLoadTaskWaiters; -+ -+ public ChunkLoadTask.EntityDataLoadTask getEntityDataLoadTask() { -+ return this.entityDataLoadTask; -+ } -+ -+ // must hold schedule lock for the two below functions -+ -+ // returns only if the data has been loaded from disk, DOES NOT relate to whether it has been deserialized -+ // or added into the world (or even into entityChunk) -+ public boolean isEntityChunkNBTLoaded() { -+ return (this.entityChunk != null && !this.entityChunk.isTransient()) || this.pendingEntityChunk != null; -+ } -+ -+ private void completeEntityLoad(final GenericDataLoadTask.TaskResult<CompoundTag, Throwable> result) { -+ final List<GenericDataLoadTaskCallback> completeWaiters; -+ ChunkLoadTask.EntityDataLoadTask entityDataLoadTask = null; -+ boolean scheduleEntityTask = false; -+ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ final List<GenericDataLoadTaskCallback> waiters = this.entityDataLoadTaskWaiters; -+ this.entityDataLoadTask = null; -+ if (result != null) { -+ this.entityDataLoadTaskWaiters = null; -+ this.pendingEntityChunk = result.left() == null ? EMPTY_ENTITY_CHUNK : result.left(); -+ if (result.right() != null) { -+ LOGGER.error("Unhandled entity data load exception, data data will be lost: ", result.right()); -+ } -+ -+ for (final GenericDataLoadTaskCallback callback : waiters) { -+ callback.markCompleted(); -+ } -+ -+ completeWaiters = waiters; -+ } else { -+ // cancelled -+ completeWaiters = null; -+ -+ // need to re-schedule? -+ if (waiters.isEmpty()) { -+ this.entityDataLoadTaskWaiters = null; -+ // no tasks to schedule _for_ -+ } else { -+ entityDataLoadTask = this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( -+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL) -+ ); -+ entityDataLoadTask.addCallback(this::completeEntityLoad); -+ // need one schedule() per waiter -+ for (final GenericDataLoadTaskCallback callback : waiters) { -+ scheduleEntityTask |= entityDataLoadTask.schedule(true); -+ } -+ } -+ } -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ -+ if (scheduleEntityTask) { -+ entityDataLoadTask.scheduleNow(); -+ } -+ -+ // avoid holding the scheduling lock while completing -+ if (completeWaiters != null) { -+ for (final GenericDataLoadTaskCallback callback : completeWaiters) { -+ callback.acceptCompleted(result); -+ } -+ } -+ -+ schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ this.checkUnload(); -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } -+ -+ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held -+ // however, when the consumer is invoked, it will hold the schedule lock -+ public GenericDataLoadTaskCallback getOrLoadEntityData(final Consumer<GenericDataLoadTask.TaskResult<CompoundTag, Throwable>> consumer) { -+ if (this.isEntityChunkNBTLoaded()) { -+ throw new IllegalStateException("Cannot load entity data, it is already loaded"); -+ } -+ // why not just acquire the lock? because the caller NEEDS to call isEntityChunkNBTLoaded before this! -+ if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) { -+ throw new IllegalStateException("Must hold scheduling lock"); -+ } -+ -+ final GenericDataLoadTaskCallback ret = new EntityDataLoadTaskCallback((Consumer)consumer, this); -+ -+ if (this.entityDataLoadTask == null) { -+ this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( -+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL) -+ ); -+ this.entityDataLoadTask.addCallback(this::completeEntityLoad); -+ this.entityDataLoadTaskWaiters = new ArrayList<>(); -+ } -+ this.entityDataLoadTaskWaiters.add(ret); -+ if (this.entityDataLoadTask.schedule(true)) { -+ ret.schedule = this.entityDataLoadTask; -+ } -+ this.checkUnload(); -+ -+ return ret; -+ } -+ -+ private static final class EntityDataLoadTaskCallback extends GenericDataLoadTaskCallback { -+ -+ public EntityDataLoadTaskCallback(final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer, final NewChunkHolder chunkHolder) { -+ super(consumer, chunkHolder); -+ } -+ -+ @Override -+ void internalCancel() { -+ this.chunkHolder.entityDataLoadTaskWaiters.remove(this); -+ this.chunkHolder.entityDataLoadTask.cancel(); -+ } -+ } -+ -+ private PoiChunk poiChunk; -+ -+ private ChunkLoadTask.PoiDataLoadTask poiDataLoadTask; -+ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0, -+ // then the task is rescheduled -+ private List<GenericDataLoadTaskCallback> poiDataLoadTaskWaiters; -+ -+ public ChunkLoadTask.PoiDataLoadTask getPoiDataLoadTask() { -+ return this.poiDataLoadTask; -+ } -+ -+ // must hold schedule lock for the two below functions -+ -+ public boolean isPoiChunkLoaded() { -+ return this.poiChunk != null; -+ } -+ -+ private void completePoiLoad(final GenericDataLoadTask.TaskResult<PoiChunk, Throwable> result) { -+ final List<GenericDataLoadTaskCallback> completeWaiters; -+ ChunkLoadTask.PoiDataLoadTask poiDataLoadTask = null; -+ boolean schedulePoiTask = false; -+ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ final List<GenericDataLoadTaskCallback> waiters = this.poiDataLoadTaskWaiters; -+ this.poiDataLoadTask = null; -+ if (result != null) { -+ this.poiDataLoadTaskWaiters = null; -+ this.poiChunk = result.left(); -+ if (result.right() != null) { -+ LOGGER.error("Unhandled poi load exception, poi data will be lost: ", result.right()); -+ } -+ -+ for (final GenericDataLoadTaskCallback callback : waiters) { -+ callback.markCompleted(); -+ } -+ -+ completeWaiters = waiters; -+ } else { -+ // cancelled -+ completeWaiters = null; -+ -+ // need to re-schedule? -+ if (waiters.isEmpty()) { -+ this.poiDataLoadTaskWaiters = null; -+ // no tasks to schedule _for_ -+ } else { -+ poiDataLoadTask = this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( -+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL) -+ ); -+ poiDataLoadTask.addCallback(this::completePoiLoad); -+ // need one schedule() per waiter -+ for (final GenericDataLoadTaskCallback callback : waiters) { -+ schedulePoiTask |= poiDataLoadTask.schedule(true); -+ } -+ } -+ } -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ -+ if (schedulePoiTask) { -+ poiDataLoadTask.scheduleNow(); -+ } -+ -+ // avoid holding the scheduling lock while completing -+ if (completeWaiters != null) { -+ for (final GenericDataLoadTaskCallback callback : completeWaiters) { -+ callback.acceptCompleted(result); -+ } -+ } -+ schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ this.checkUnload(); -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } -+ -+ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held -+ // however, when the consumer is invoked, it will hold the schedule lock -+ public GenericDataLoadTaskCallback getOrLoadPoiData(final Consumer<GenericDataLoadTask.TaskResult<PoiChunk, Throwable>> consumer) { -+ if (this.isPoiChunkLoaded()) { -+ throw new IllegalStateException("Cannot load poi data, it is already loaded"); -+ } -+ // why not just acquire the lock? because the caller NEEDS to call isPoiChunkLoaded before this! -+ if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) { -+ throw new IllegalStateException("Must hold scheduling lock"); -+ } -+ -+ final GenericDataLoadTaskCallback ret = new PoiDataLoadTaskCallback((Consumer)consumer, this); -+ -+ if (this.poiDataLoadTask == null) { -+ this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( -+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL) -+ ); -+ this.poiDataLoadTask.addCallback(this::completePoiLoad); -+ this.poiDataLoadTaskWaiters = new ArrayList<>(); -+ } -+ this.poiDataLoadTaskWaiters.add(ret); -+ if (this.poiDataLoadTask.schedule(true)) { -+ ret.schedule = this.poiDataLoadTask; -+ } -+ this.checkUnload(); -+ -+ return ret; -+ } -+ -+ private static final class PoiDataLoadTaskCallback extends GenericDataLoadTaskCallback { -+ -+ public PoiDataLoadTaskCallback(final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer, final NewChunkHolder chunkHolder) { -+ super(consumer, chunkHolder); -+ } -+ -+ @Override -+ void internalCancel() { -+ this.chunkHolder.poiDataLoadTaskWaiters.remove(this); -+ this.chunkHolder.poiDataLoadTask.cancel(); -+ } -+ } -+ -+ public static abstract class GenericDataLoadTaskCallback implements Cancellable { -+ -+ protected final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer; -+ protected final NewChunkHolder chunkHolder; -+ protected boolean completed; -+ protected GenericDataLoadTask<?, ?> schedule; -+ protected final AtomicBoolean scheduled = new AtomicBoolean(); -+ -+ public GenericDataLoadTaskCallback(final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer, -+ final NewChunkHolder chunkHolder) { -+ this.consumer = consumer; -+ this.chunkHolder = chunkHolder; -+ } -+ -+ public void schedule() { -+ if (this.scheduled.getAndSet(true)) { -+ throw new IllegalStateException("Double calling schedule()"); -+ } -+ if (this.schedule != null) { -+ this.schedule.scheduleNow(); -+ this.schedule = null; -+ } -+ } -+ -+ boolean isCompleted() { -+ return this.completed; -+ } -+ -+ // must hold scheduling lock -+ private boolean setCompleted() { -+ if (this.completed) { -+ return false; -+ } -+ return this.completed = true; -+ } -+ -+ // must hold scheduling lock -+ void markCompleted() { -+ if (this.completed) { -+ throw new IllegalStateException("May not be completed here"); -+ } -+ this.completed = true; -+ } -+ -+ void acceptCompleted(final GenericDataLoadTask.TaskResult<?, Throwable> result) { -+ if (result != null) { -+ if (this.completed) { -+ this.consumer.accept(result); -+ } else { -+ throw new IllegalStateException("Cannot be uncompleted at this point"); -+ } -+ } else { -+ throw new NullPointerException("Result cannot be null (cancelled)"); -+ } -+ } -+ -+ // holds scheduling lock -+ abstract void internalCancel(); -+ -+ @Override -+ public boolean cancel() { -+ final NewChunkHolder holder = this.chunkHolder; -+ final ReentrantAreaLock.Node schedulingLock = holder.scheduler.schedulingLockArea.lock(holder.chunkX, holder.chunkZ); -+ try { -+ if (!this.completed) { -+ this.completed = true; -+ this.internalCancel(); -+ return true; -+ } -+ return false; -+ } finally { -+ holder.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } -+ } -+ -+ private ChunkAccess currentChunk; -+ -+ // generation status state -+ -+ /** -+ * Current status the chunk has been brought up to by the chunk system. null indicates no work at all -+ */ -+ private ChunkStatus currentGenStatus; -+ -+ // This allows lockless access to the chunk and last gen status -+ private static final ChunkStatus[] ALL_STATUSES = ChunkStatus.getStatusList().toArray(new ChunkStatus[0]); -+ -+ public static final record ChunkCompletion(ChunkAccess chunk, ChunkStatus genStatus) {}; -+ private static final VarHandle CHUNK_COMPLETION_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(ChunkCompletion[].class); -+ private final ChunkCompletion[] chunkCompletions = new ChunkCompletion[ALL_STATUSES.length]; -+ -+ private volatile ChunkCompletion lastChunkCompletion; -+ -+ public ChunkCompletion getLastChunkCompletion() { -+ return this.lastChunkCompletion; -+ } -+ -+ public ChunkAccess getChunkIfPresentUnchecked(final ChunkStatus status) { -+ final ChunkCompletion completion = (ChunkCompletion)CHUNK_COMPLETION_ARRAY_HANDLE.getVolatile(this.chunkCompletions, status.getIndex()); -+ return completion == null ? null : completion.chunk; -+ } -+ -+ public ChunkAccess getChunkIfPresent(final ChunkStatus status) { -+ final ChunkStatus maxStatus = ChunkLevel.generationStatus(this.getTicketLevel()); -+ -+ if (maxStatus == null || status.isAfter(maxStatus)) { -+ return null; -+ } -+ -+ return this.getChunkIfPresentUnchecked(status); -+ } -+ -+ public void replaceProtoChunk(final ImposterProtoChunk imposterProtoChunk) { -+ for (int i = 0, max = ChunkStatus.FULL.getIndex(); i < max; ++i) { -+ CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, i, new ChunkCompletion(imposterProtoChunk, ALL_STATUSES[i])); -+ } -+ } -+ -+ /** -+ * The target final chunk status the chunk system will bring the chunk to. -+ */ -+ private ChunkStatus requestedGenStatus; -+ -+ private ChunkProgressionTask generationTask; -+ private ChunkStatus generationTaskStatus; -+ -+ /** -+ * contains the neighbours that this chunk generation is blocking on -+ */ -+ private final ReferenceLinkedOpenHashSet<NewChunkHolder> neighboursBlockingGenTask = new ReferenceLinkedOpenHashSet<>(4); -+ -+ /** -+ * map of ChunkHolder -> Required Status for this chunk -+ */ -+ private final Reference2ObjectLinkedOpenHashMap<NewChunkHolder, ChunkStatus> neighboursWaitingForUs = new Reference2ObjectLinkedOpenHashMap<>(); -+ -+ public void addGenerationBlockingNeighbour(final NewChunkHolder neighbour) { -+ this.neighboursBlockingGenTask.add(neighbour); -+ } -+ -+ public void addWaitingNeighbour(final NewChunkHolder neighbour, final ChunkStatus requiredStatus) { -+ final boolean wasEmpty = this.neighboursWaitingForUs.isEmpty(); -+ this.neighboursWaitingForUs.put(neighbour, requiredStatus); -+ if (wasEmpty) { -+ this.checkUnload(); -+ } -+ } -+ -+ // priority state -+ -+ // the target priority for this chunk to generate at -+ private Priority priority = null; -+ private boolean priorityLocked; -+ -+ // the priority neighbouring chunks have requested this chunk generate at -+ private Priority neighbourRequestedPriority = null; -+ -+ public Priority getEffectivePriority(final Priority dfl) { -+ final Priority neighbour = this.neighbourRequestedPriority; -+ final Priority us = this.priority; -+ -+ if (neighbour == null) { -+ return us == null ? dfl : us; -+ } -+ if (us == null) { -+ return neighbour; -+ } -+ -+ return Priority.max(us, neighbour); -+ } -+ -+ private void recalculateNeighbourRequestedPriority() { -+ if (this.neighboursWaitingForUs.isEmpty()) { -+ this.neighbourRequestedPriority = null; -+ return; -+ } -+ -+ Priority max = null; -+ -+ for (final NewChunkHolder holder : this.neighboursWaitingForUs.keySet()) { -+ final Priority neighbourPriority = holder.getEffectivePriority(null); -+ if (neighbourPriority != null && (max == null || neighbourPriority.isHigherPriority(max))) { -+ max = neighbourPriority; -+ } -+ } -+ -+ final Priority current = this.getEffectivePriority(Priority.NORMAL); -+ this.neighbourRequestedPriority = max; -+ final Priority next = this.getEffectivePriority(Priority.NORMAL); -+ -+ if (current == next) { -+ return; -+ } -+ -+ // our effective priority has changed, so change our task -+ if (this.generationTask != null) { -+ this.generationTask.setPriority(next); -+ } -+ -+ // now propagate this to our neighbours -+ this.recalculateNeighbourPriorities(); -+ } -+ -+ public void recalculateNeighbourPriorities() { -+ for (final NewChunkHolder holder : this.neighboursBlockingGenTask) { -+ holder.recalculateNeighbourRequestedPriority(); -+ } -+ } -+ -+ // must hold scheduling lock -+ public void raisePriority(final Priority priority) { -+ if (this.priority != null && this.priority.isHigherOrEqualPriority(priority)) { -+ return; -+ } -+ this.setPriority(priority); -+ } -+ -+ private void lockPriority() { -+ this.priority = null; -+ this.priorityLocked = true; -+ } -+ -+ // must hold scheduling lock -+ public void setPriority(final Priority priority) { -+ if (this.priorityLocked) { -+ return; -+ } -+ final Priority old = this.getEffectivePriority(null); -+ this.priority = priority; -+ final Priority newPriority = this.getEffectivePriority(Priority.NORMAL); -+ -+ if (old != newPriority) { -+ if (this.generationTask != null) { -+ this.generationTask.setPriority(newPriority); -+ } -+ } -+ -+ this.recalculateNeighbourPriorities(); -+ } -+ -+ // must hold scheduling lock -+ public void lowerPriority(final Priority priority) { -+ if (this.priority != null && this.priority.isLowerOrEqualPriority(priority)) { -+ return; -+ } -+ this.setPriority(priority); -+ } -+ -+ // error handling state -+ private ChunkStatus failedGenStatus; -+ private Throwable genTaskException; -+ private Thread genTaskFailedThread; -+ -+ private boolean failedLightUpdate; -+ -+ public void failedLightUpdate() { -+ this.failedLightUpdate = true; -+ } -+ -+ public boolean hasFailedGeneration() { -+ return this.genTaskException != null; -+ } -+ -+ // ticket level state -+ private int oldTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1; -+ private int currentTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1; -+ -+ public int getTicketLevel() { -+ return this.currentTicketLevel; -+ } -+ -+ public final ChunkHolder vanillaChunkHolder; -+ -+ public NewChunkHolder(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkTaskScheduler scheduler) { -+ this.world = world; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.scheduler = scheduler; -+ this.vanillaChunkHolder = new ChunkHolder( -+ new ChunkPos(chunkX, chunkZ), ChunkHolderManager.MAX_TICKET_LEVEL, world, -+ world.getLightEngine(), null, world.getChunkSource().chunkMap -+ ); -+ ((ChunkSystemChunkHolder)this.vanillaChunkHolder).moonrise$setRealChunkHolder(this); -+ this.holderData = ((ChunkSystemLevel)this.world).moonrise$requestChunkData(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ -+ public ChunkAccess getCurrentChunk() { -+ return this.currentChunk; -+ } -+ -+ int getCurrentTicketLevel() { -+ return this.currentTicketLevel; -+ } -+ -+ void updateTicketLevel(final int toLevel) { -+ this.currentTicketLevel = toLevel; -+ } -+ -+ private int totalNeighboursUsingThisChunk = 0; -+ -+ // holds schedule lock -+ public void addNeighbourUsingChunk() { -+ final int now = ++this.totalNeighboursUsingThisChunk; -+ -+ if (now == 1) { -+ this.checkUnload(); -+ } -+ } -+ -+ // holds schedule lock -+ public void removeNeighbourUsingChunk() { -+ final int now = --this.totalNeighboursUsingThisChunk; -+ -+ if (now == 0) { -+ this.checkUnload(); -+ } -+ -+ if (now < 0) { -+ throw new IllegalStateException("Neighbours using this chunk cannot be negative"); -+ } -+ } -+ -+ // must hold scheduling lock -+ // returns string reason for why chunk should remain loaded, null otherwise -+ public final String isSafeToUnload() { -+ // is ticket level below threshold? -+ if (this.oldTicketLevel <= ChunkHolderManager.MAX_TICKET_LEVEL) { -+ return "ticket_level"; -+ } -+ -+ // are we being used by another chunk for generation? -+ if (this.totalNeighboursUsingThisChunk != 0) { -+ return "neighbours_generating"; -+ } -+ -+ // are we going to be used by another chunk for generation? -+ if (!this.neighboursWaitingForUs.isEmpty()) { -+ return "neighbours_waiting"; -+ } -+ -+ // chunk must be marked inaccessible (i.e. unloaded to plugins) -+ if (this.getChunkStatus() != FullChunkStatus.INACCESSIBLE) { -+ return "fullchunkstatus"; -+ } -+ -+ // are we currently generating anything, or have requested generation? -+ if (this.generationTask != null) { -+ return "generating"; -+ } -+ if (this.requestedGenStatus != null) { -+ return "requested_generation"; -+ } -+ -+ // entity data requested? -+ if (this.entityDataLoadTask != null) { -+ return "entity_data_requested"; -+ } -+ -+ // poi data requested? -+ if (this.poiDataLoadTask != null) { -+ return "poi_data_requested"; -+ } -+ -+ // are we pending serialization? -+ if (this.entityDataUnload != null) { -+ return "entity_serialization"; -+ } -+ if (this.poiDataUnload != null) { -+ return "poi_serialization"; -+ } -+ if (this.chunkDataUnload != null) { -+ return "chunk_serialization"; -+ } -+ -+ // Note: light tasks do not need a check, as they add a ticket. -+ -+ // nothing is using this chunk, so it should be unloaded -+ return null; -+ } -+ -+ /** Unloaded from chunk map */ -+ private boolean unloaded; -+ -+ void onUnload() { -+ this.unloaded = true; -+ ((ChunkSystemLevel)this.world).moonrise$releaseChunkData(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ)); -+ } -+ -+ private boolean inUnloadQueue = false; -+ -+ void removeFromUnloadQueue() { -+ this.inUnloadQueue = false; -+ } -+ -+ // must hold scheduling lock -+ private void checkUnload() { -+ if (this.unloaded) { -+ return; -+ } -+ if (this.isSafeToUnload() == null) { -+ // ensure in unload queue -+ if (!this.inUnloadQueue) { -+ this.inUnloadQueue = true; -+ this.scheduler.chunkHolderManager.unloadQueue.addChunk(this.chunkX, this.chunkZ); -+ } -+ } else { -+ // ensure not in unload queue -+ if (this.inUnloadQueue) { -+ this.inUnloadQueue = false; -+ this.scheduler.chunkHolderManager.unloadQueue.removeChunk(this.chunkX, this.chunkZ); -+ } -+ } -+ } -+ -+ static final record UnloadState(NewChunkHolder holder, ChunkAccess chunk, ChunkEntitySlices entityChunk, PoiChunk poiChunk) {}; -+ -+ // note: these are completed with null to indicate that no write occurred -+ // they are also completed with null to indicate a null write occurred -+ private UnloadTask chunkDataUnload; -+ private UnloadTask entityDataUnload; -+ private UnloadTask poiDataUnload; -+ -+ public static final record UnloadTask(CallbackCompletable<CompoundTag> completable, PrioritisedExecutor.PrioritisedTask task, -+ LazyRunnable toRun) {} -+ -+ public UnloadTask getUnloadTask(final MoonriseRegionFileIO.RegionFileType type) { -+ switch (type) { -+ case CHUNK_DATA: -+ return this.chunkDataUnload; -+ case ENTITY_DATA: -+ return this.entityDataUnload; -+ case POI_DATA: -+ return this.poiDataUnload; -+ default: -+ throw new IllegalStateException("Unknown regionfile type " + type); -+ } -+ } -+ -+ private void removeUnloadTask(final MoonriseRegionFileIO.RegionFileType type) { -+ switch (type) { -+ case CHUNK_DATA: { -+ this.chunkDataUnload = null; -+ return; -+ } -+ case ENTITY_DATA: { -+ this.entityDataUnload = null; -+ return; -+ } -+ case POI_DATA: { -+ this.poiDataUnload = null; -+ return; -+ } -+ default: -+ throw new IllegalStateException("Unknown regionfile type " + type); -+ } -+ } -+ -+ private UnloadState unloadState; -+ -+ // holds schedule lock -+ UnloadState unloadStage1() { -+ // because we hold the scheduling lock, we cannot actually unload anything -+ // so, what we do here instead is to null this chunk's state and setup the unload tasks -+ // the unload tasks will ensure that any loads that take place after stage1 (i.e during stage2, in which -+ // we do not hold the lock) c -+ final ChunkAccess chunk = this.currentChunk; -+ final ChunkEntitySlices entityChunk = this.entityChunk; -+ final PoiChunk poiChunk = this.poiChunk; -+ // chunk state -+ this.currentChunk = null; -+ this.currentGenStatus = null; -+ for (int i = 0; i < this.chunkCompletions.length; ++i) { -+ CHUNK_COMPLETION_ARRAY_HANDLE.setRelease(this.chunkCompletions, i, (ChunkCompletion)null); -+ } -+ this.lastChunkCompletion = null; -+ // entity chunk state -+ this.entityChunk = null; -+ this.pendingEntityChunk = null; -+ -+ // poi chunk state -+ this.poiChunk = null; -+ -+ // priority state -+ this.priorityLocked = false; -+ -+ if (chunk != null) { -+ final LazyRunnable toRun = new LazyRunnable(); -+ this.chunkDataUnload = new UnloadTask(new CallbackCompletable<>(), this.scheduler.saveExecutor.createTask(toRun), toRun); -+ } -+ if (poiChunk != null) { -+ this.poiDataUnload = new UnloadTask(new CallbackCompletable<>(), null, null); -+ } -+ if (entityChunk != null) { -+ this.entityDataUnload = new UnloadTask(new CallbackCompletable<>(), null, null); -+ } -+ -+ return this.unloadState = (chunk != null || entityChunk != null || poiChunk != null) ? new UnloadState(this, chunk, entityChunk, poiChunk) : null; -+ } -+ -+ // data is null if failed or does not need to be saved -+ void completeAsyncUnloadDataSave(final MoonriseRegionFileIO.RegionFileType type, final CompoundTag data) { -+ if (data != null) { -+ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, data, type); -+ } -+ -+ this.getUnloadTask(type).completable().complete(data); -+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ // can only write to these fields while holding the schedule lock -+ this.removeUnloadTask(type); -+ this.checkUnload(); -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } -+ -+ void unloadStage2(final UnloadState state) { -+ this.unloadState = null; -+ final ChunkAccess chunk = state.chunk(); -+ final ChunkEntitySlices entityChunk = state.entityChunk(); -+ final PoiChunk poiChunk = state.poiChunk(); -+ -+ final boolean shouldLevelChunkNotSave = PlatformHooks.get().forceNoSave(chunk); -+ -+ // unload chunk data -+ if (chunk != null) { -+ if (chunk instanceof LevelChunk levelChunk) { -+ levelChunk.setLoaded(false); -+ PlatformHooks.get().chunkUnloadFromWorld(levelChunk); -+ } -+ -+ if (!shouldLevelChunkNotSave) { -+ this.saveChunk(chunk, true); -+ } else { -+ this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, null); -+ } -+ -+ if (chunk instanceof LevelChunk levelChunk) { -+ this.world.unload(levelChunk); -+ } -+ } -+ -+ // unload entity data -+ if (entityChunk != null) { -+ this.saveEntities(entityChunk, true); -+ // yes this is a hack to pass the compound tag through... -+ final CompoundTag lastEntityUnload = this.lastEntityUnload; -+ this.lastEntityUnload = null; -+ -+ if (entityChunk.unload()) { -+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ entityChunk.setTransient(true); -+ this.entityChunk = entityChunk; -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } else { -+ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().entitySectionUnload(this.chunkX, this.chunkZ); -+ } -+ // we need to delay the callback until after determining transience, otherwise a potential loader could -+ // set entityChunk before we do -+ this.entityDataUnload.completable().complete(lastEntityUnload); -+ } -+ -+ // unload poi data -+ if (poiChunk != null) { -+ if (poiChunk.isDirty() && !shouldLevelChunkNotSave) { -+ this.savePOI(poiChunk, true); -+ } else { -+ this.poiDataUnload.completable().complete(null); -+ } -+ -+ if (poiChunk.isLoaded()) { -+ ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$onUnload(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ)); -+ } -+ } -+ } -+ -+ boolean unloadStage3() { -+ // can only write to these while holding the schedule lock, and we instantly complete them in stage2 -+ this.poiDataUnload = null; -+ this.entityDataUnload = null; -+ -+ // we need to check if anything has been loaded in the meantime (or if we have transient entities) -+ if (this.entityChunk != null || this.poiChunk != null || this.currentChunk != null) { -+ return false; -+ } -+ -+ return this.isSafeToUnload() == null; -+ } -+ -+ private void cancelGenTask() { -+ if (this.generationTask != null) { -+ this.generationTask.cancel(); -+ } else { -+ // otherwise, we are blocking on neighbours, so remove them -+ if (!this.neighboursBlockingGenTask.isEmpty()) { -+ for (final NewChunkHolder neighbour : this.neighboursBlockingGenTask) { -+ if (neighbour.neighboursWaitingForUs.remove(this) == null) { -+ throw new IllegalStateException("Corrupt state"); -+ } -+ if (neighbour.neighboursWaitingForUs.isEmpty()) { -+ neighbour.checkUnload(); -+ } -+ } -+ this.neighboursBlockingGenTask.clear(); -+ this.checkUnload(); -+ } -+ } -+ } -+ -+ // holds: ticket level update lock -+ // holds: schedule lock -+ public void processTicketLevelUpdate(final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedLoadStatus) { -+ final int oldLevel = this.oldTicketLevel; -+ final int newLevel = this.currentTicketLevel; -+ -+ if (oldLevel == newLevel) { -+ return; -+ } -+ -+ this.oldTicketLevel = newLevel; -+ -+ final FullChunkStatus oldState = ChunkLevel.fullStatus(oldLevel); -+ final FullChunkStatus newState = ChunkLevel.fullStatus(newLevel); -+ final boolean oldUnloaded = oldLevel > ChunkHolderManager.MAX_TICKET_LEVEL; -+ final boolean newUnloaded = newLevel > ChunkHolderManager.MAX_TICKET_LEVEL; -+ -+ final ChunkStatus maxGenerationStatusOld = ChunkLevel.generationStatus(oldLevel); -+ final ChunkStatus maxGenerationStatusNew = ChunkLevel.generationStatus(newLevel); -+ -+ // check for cancellations from downgrading ticket level -+ if (this.requestedGenStatus != null && !newState.isOrAfter(FullChunkStatus.FULL) && newLevel > oldLevel) { -+ // note: cancel() may invoke onChunkGenComplete synchronously here -+ if (newUnloaded) { -+ // need to cancel all tasks -+ // note: requested status must be set to null here before cancellation, to indicate to the -+ // completion logic that we do not want rescheduling to occur -+ this.requestedGenStatus = null; -+ this.cancelGenTask(); -+ } else { -+ final ChunkStatus toCancel = ((ChunkSystemChunkStatus)maxGenerationStatusNew).moonrise$getNextStatus(); -+ final ChunkStatus currentRequestedStatus = this.requestedGenStatus; -+ -+ if (currentRequestedStatus.isOrAfter(toCancel)) { -+ // we do have to cancel something here -+ // clamp requested status to the maximum -+ if (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(maxGenerationStatusNew)) { -+ // already generated to status, so we must cancel -+ this.requestedGenStatus = null; -+ this.cancelGenTask(); -+ } else { -+ // not generated to status, so we may have to cancel -+ // note: gen task is always 1 status above current gen status if not null -+ this.requestedGenStatus = maxGenerationStatusNew; -+ if (this.generationTaskStatus != null && this.generationTaskStatus.isOrAfter(toCancel)) { -+ // TOOD is this even possible? i don't think so -+ throw new IllegalStateException("?????"); -+ } -+ } -+ } -+ } -+ } -+ -+ if (oldState != newState) { -+ if (newState.isOrAfter(oldState)) { -+ // status upgrade -+ if (!oldState.isOrAfter(FullChunkStatus.FULL) && newState.isOrAfter(FullChunkStatus.FULL)) { -+ // may need to schedule full load -+ if (this.currentGenStatus != ChunkStatus.FULL) { -+ if (this.requestedGenStatus != null) { -+ this.requestedGenStatus = ChunkStatus.FULL; -+ } else { -+ this.scheduler.schedule( -+ this.chunkX, this.chunkZ, ChunkStatus.FULL, this, scheduledTasks -+ ); -+ } -+ } -+ } -+ } else { -+ // status downgrade -+ if (!newState.isOrAfter(FullChunkStatus.ENTITY_TICKING) && oldState.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { -+ this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, null); -+ } -+ -+ if (!newState.isOrAfter(FullChunkStatus.BLOCK_TICKING) && oldState.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { -+ this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, null); -+ } -+ -+ if (!newState.isOrAfter(FullChunkStatus.FULL) && oldState.isOrAfter(FullChunkStatus.FULL)) { -+ this.completeFullStatusConsumers(FullChunkStatus.FULL, null); -+ } -+ } -+ -+ if (this.updatePendingStatus()) { -+ changedLoadStatus.add(this); -+ } -+ } -+ -+ if (oldUnloaded != newUnloaded) { -+ this.checkUnload(); -+ } -+ -+ // Don't really have a choice but to place this hook here -+ PlatformHooks.get().onChunkHolderTicketChange(this.world, this.vanillaChunkHolder, oldLevel, newLevel); -+ } -+ -+ static final int NEIGHBOUR_RADIUS = 2; -+ private long fullNeighbourChunksLoadedBitset; -+ -+ private static int getFullNeighbourIndex(final int relativeX, final int relativeZ) { -+ // index = (relativeX + NEIGHBOUR_CACHE_RADIUS) + (relativeZ + NEIGHBOUR_CACHE_RADIUS) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1) -+ // optimised variant of the above by moving some of the ops to compile time -+ return relativeX + (relativeZ * (NEIGHBOUR_RADIUS * 2 + 1)) + (NEIGHBOUR_RADIUS + NEIGHBOUR_RADIUS * ((NEIGHBOUR_RADIUS * 2 + 1))); -+ } -+ public final boolean isNeighbourFullLoaded(final int relativeX, final int relativeZ) { -+ return (this.fullNeighbourChunksLoadedBitset & (1L << getFullNeighbourIndex(relativeX, relativeZ))) != 0; -+ } -+ -+ // returns true if this chunk changed pending full status -+ // must hold scheduling lock -+ public final boolean setNeighbourFullLoaded(final int relativeX, final int relativeZ) { -+ final int index = getFullNeighbourIndex(relativeX, relativeZ); -+ this.fullNeighbourChunksLoadedBitset |= (1L << index); -+ return this.updatePendingStatus(); -+ } -+ -+ // returns true if this chunk changed pending full status -+ // must hold scheduling lock -+ public final boolean setNeighbourFullUnloaded(final int relativeX, final int relativeZ) { -+ final int index = getFullNeighbourIndex(relativeX, relativeZ); -+ this.fullNeighbourChunksLoadedBitset &= ~(1L << index); -+ return this.updatePendingStatus(); -+ } -+ -+ private static long getLoadedMask(final int radius) { -+ long mask = 0L; -+ for (int dx = -radius; dx <= radius; ++dx) { -+ for (int dz = -radius; dz <= radius; ++dz) { -+ mask |= (1L << getFullNeighbourIndex(dx, dz)); -+ } -+ } -+ -+ return mask; -+ } -+ -+ private static final long CHUNK_LOADED_MASK_RAD0 = getLoadedMask(0); -+ private static final long CHUNK_LOADED_MASK_RAD1 = getLoadedMask(1); -+ private static final long CHUNK_LOADED_MASK_RAD2 = getLoadedMask(2); -+ -+ // only updated while holding scheduling lock -+ private FullChunkStatus pendingFullChunkStatus = FullChunkStatus.INACCESSIBLE; -+ // updated while holding no locks, but adds a ticket before to prevent pending status from dropping -+ // so, current will never update to a value higher than pending -+ private FullChunkStatus currentFullChunkStatus = FullChunkStatus.INACCESSIBLE; -+ -+ public FullChunkStatus getChunkStatus() { -+ // no volatile access, access off-main is considered racey anyways -+ return this.currentFullChunkStatus; -+ } -+ -+ public boolean isEntityTickingReady() { -+ return this.getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING); -+ } -+ -+ public boolean isTickingReady() { -+ return this.getChunkStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING); -+ } -+ -+ public boolean isFullChunkReady() { -+ return this.getChunkStatus().isOrAfter(FullChunkStatus.FULL); -+ } -+ -+ private static FullChunkStatus getStatusForBitset(final long bitset) { -+ if ((bitset & CHUNK_LOADED_MASK_RAD2) == CHUNK_LOADED_MASK_RAD2) { -+ return FullChunkStatus.ENTITY_TICKING; -+ } else if ((bitset & CHUNK_LOADED_MASK_RAD1) == CHUNK_LOADED_MASK_RAD1) { -+ return FullChunkStatus.BLOCK_TICKING; -+ } else if ((bitset & CHUNK_LOADED_MASK_RAD0) == CHUNK_LOADED_MASK_RAD0) { -+ return FullChunkStatus.FULL; -+ } else { -+ return FullChunkStatus.INACCESSIBLE; -+ } -+ } -+ -+ // must hold scheduling lock -+ // returns whether the pending status was changed -+ private boolean updatePendingStatus() { -+ final FullChunkStatus byTicketLevel = ChunkLevel.fullStatus(this.oldTicketLevel); // oldTicketLevel is controlled by scheduling lock -+ -+ FullChunkStatus pending = getStatusForBitset(this.fullNeighbourChunksLoadedBitset); -+ if (pending == FullChunkStatus.INACCESSIBLE && byTicketLevel.isOrAfter(FullChunkStatus.FULL) && this.currentGenStatus == ChunkStatus.FULL) { -+ // the bitset is only for chunks that have gone through the status updater -+ // but here we are ready to go to FULL -+ pending = FullChunkStatus.FULL; -+ } -+ -+ if (pending.isOrAfter(byTicketLevel)) { // pending >= byTicketLevel -+ // cannot set above ticket level -+ pending = byTicketLevel; -+ } -+ -+ if (this.pendingFullChunkStatus == pending) { -+ return false; -+ } -+ -+ this.pendingFullChunkStatus = pending; -+ -+ return true; -+ } -+ -+ private void onFullChunkLoadChange(final boolean loaded, final List<NewChunkHolder> changedFullStatus) { -+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, NEIGHBOUR_RADIUS); -+ try { -+ for (int dz = -NEIGHBOUR_RADIUS; dz <= NEIGHBOUR_RADIUS; ++dz) { -+ for (int dx = -NEIGHBOUR_RADIUS; dx <= NEIGHBOUR_RADIUS; ++dx) { -+ final NewChunkHolder holder = (dx | dz) == 0 ? this : this.scheduler.chunkHolderManager.getChunkHolder(dx + this.chunkX, dz + this.chunkZ); -+ if (loaded) { -+ if (holder.setNeighbourFullLoaded(-dx, -dz)) { -+ changedFullStatus.add(holder); -+ } -+ } else { -+ if (holder != null && holder.setNeighbourFullUnloaded(-dx, -dz)) { -+ changedFullStatus.add(holder); -+ } -+ } -+ } -+ } -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } -+ -+ private void changeEntityChunkStatus(final FullChunkStatus toStatus) { -+ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().chunkStatusChange(this.chunkX, this.chunkZ, toStatus); -+ } -+ -+ private boolean processingFullStatus = false; -+ -+ private void updateCurrentState(final FullChunkStatus to) { -+ this.currentFullChunkStatus = to; -+ } -+ -+ // only to be called on the main thread, no locks need to be held -+ public boolean handleFullStatusChange(final List<NewChunkHolder> changedFullStatus) { -+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot update full status thread off-main"); -+ -+ boolean ret = false; -+ -+ if (this.processingFullStatus) { -+ // we cannot process updates recursively, as we may be in the middle of logic to upgrade/downgrade status -+ return ret; -+ } -+ -+ this.processingFullStatus = true; -+ try { -+ for (;;) { -+ // check if we have any remaining work to do -+ -+ // we do not need to hold the scheduling lock to read pending, as changes to pending -+ // will queue a status update -+ -+ final FullChunkStatus pending = this.pendingFullChunkStatus; -+ FullChunkStatus current = this.currentFullChunkStatus; -+ -+ if (pending == current) { -+ if (pending == FullChunkStatus.INACCESSIBLE) { -+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ this.checkUnload(); -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } -+ return ret; -+ } -+ -+ ret = true; -+ -+ // note: because the chunk system delays any ticket downgrade to the chunk holder manager tick, we -+ // do not need to consider cases where the ticket level may decrease during this call by asynchronous -+ // ticket changes -+ -+ // chunks cannot downgrade state while status is pending a change -+ // note: currentChunk must be LevelChunk, as current != pending which means that at least one is not ACCESSIBLE -+ final LevelChunk chunk = (LevelChunk)this.currentChunk; -+ -+ // Note: we assume that only load/unload contain plugin logic -+ // plugin logic is anything stupid enough to possibly change the chunk status while it is already -+ // being changed (i.e during load it is possible it will try to set to full ticking) -+ // in order to allow this change, we also need this plugin logic to be contained strictly after all -+ // of the chunk system load callbacks are invoked -+ if (pending.isOrAfter(current)) { -+ // state upgrade -+ if (!current.isOrAfter(FullChunkStatus.FULL) && pending.isOrAfter(FullChunkStatus.FULL)) { -+ this.updateCurrentState(FullChunkStatus.FULL); -+ ChunkSystem.onChunkPreBorder(chunk, this.vanillaChunkHolder); -+ this.scheduler.chunkHolderManager.ensureInAutosave(this); -+ this.changeEntityChunkStatus(FullChunkStatus.FULL); -+ ChunkSystem.onChunkBorder(chunk, this.vanillaChunkHolder); -+ this.onFullChunkLoadChange(true, changedFullStatus); -+ this.completeFullStatusConsumers(FullChunkStatus.FULL, chunk); -+ } -+ -+ if (!current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { -+ this.updateCurrentState(FullChunkStatus.BLOCK_TICKING); -+ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING); -+ ChunkSystem.onChunkTicking(chunk, this.vanillaChunkHolder); -+ this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, chunk); -+ } -+ -+ if (!current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { -+ this.updateCurrentState(FullChunkStatus.ENTITY_TICKING); -+ this.changeEntityChunkStatus(FullChunkStatus.ENTITY_TICKING); -+ ChunkSystem.onChunkEntityTicking(chunk, this.vanillaChunkHolder); -+ this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, chunk); -+ } -+ } else { -+ if (current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && !pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { -+ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING); -+ ChunkSystem.onChunkNotEntityTicking(chunk, this.vanillaChunkHolder); -+ this.updateCurrentState(FullChunkStatus.BLOCK_TICKING); -+ } -+ -+ if (current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && !pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { -+ this.changeEntityChunkStatus(FullChunkStatus.FULL); -+ ChunkSystem.onChunkNotTicking(chunk, this.vanillaChunkHolder); -+ this.updateCurrentState(FullChunkStatus.FULL); -+ } -+ -+ if (current.isOrAfter(FullChunkStatus.FULL) && !pending.isOrAfter(FullChunkStatus.FULL)) { -+ this.onFullChunkLoadChange(false, changedFullStatus); -+ this.changeEntityChunkStatus(FullChunkStatus.INACCESSIBLE); -+ ChunkSystem.onChunkNotBorder(chunk, this.vanillaChunkHolder); -+ ChunkSystem.onChunkPostNotBorder(chunk, this.vanillaChunkHolder); -+ this.updateCurrentState(FullChunkStatus.INACCESSIBLE); -+ } -+ } -+ } -+ } finally { -+ this.processingFullStatus = false; -+ } -+ } -+ -+ // note: must hold scheduling lock -+ // rets true if the current requested gen status is not null (effectively, whether further scheduling is not needed) -+ boolean upgradeGenTarget(final ChunkStatus toStatus) { -+ if (toStatus == null) { -+ throw new NullPointerException("toStatus cannot be null"); -+ } -+ if (this.requestedGenStatus == null && this.generationTask == null) { -+ return false; -+ } -+ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(toStatus)) { -+ this.requestedGenStatus = toStatus; -+ } -+ return true; -+ } -+ -+ public void setGenerationTarget(final ChunkStatus toStatus) { -+ this.requestedGenStatus = toStatus; -+ } -+ -+ public boolean hasGenerationTask() { -+ return this.generationTask != null; -+ } -+ -+ public ChunkStatus getCurrentGenStatus() { -+ return this.currentGenStatus; -+ } -+ -+ public ChunkStatus getRequestedGenStatus() { -+ return this.requestedGenStatus; -+ } -+ -+ private final Reference2ObjectOpenHashMap<ChunkStatus, List<Consumer<ChunkAccess>>> statusWaiters = new Reference2ObjectOpenHashMap<>(); -+ -+ void addStatusConsumer(final ChunkStatus status, final Consumer<ChunkAccess> consumer) { -+ this.statusWaiters.computeIfAbsent(status, (final ChunkStatus keyInMap) -> { -+ return new ArrayList<>(4); -+ }).add(consumer); -+ } -+ -+ private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) { -+ // Update progress listener for LevelLoadingScreen -+ if (chunk != null) { -+ final ChunkProgressListener progressListener = this.world.getChunkSource().chunkMap.progressListener; -+ if (progressListener != null) { -+ final ChunkStatus finalStatus = status; -+ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { -+ progressListener.onStatusChange(this.vanillaChunkHolder.getPos(), finalStatus); -+ }); -+ } -+ } -+ -+ // need to tell future statuses to complete if cancelled -+ do { -+ this.completeStatusConsumers0(status, chunk); -+ } while (chunk == null && status != (status = ((ChunkSystemChunkStatus)status).moonrise$getNextStatus())); -+ } -+ -+ private void completeStatusConsumers0(final ChunkStatus status, final ChunkAccess chunk) { -+ final List<Consumer<ChunkAccess>> consumers; -+ consumers = this.statusWaiters.remove(status); -+ -+ if (consumers == null) { -+ return; -+ } -+ -+ // must be scheduled to main, we do not trust the callback to not do anything stupid -+ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { -+ for (final Consumer<ChunkAccess> consumer : consumers) { -+ try { -+ consumer.accept(chunk); -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to process chunk status callback", thr); -+ } -+ } -+ }, Priority.HIGHEST); -+ } -+ -+ private final Reference2ObjectOpenHashMap<FullChunkStatus, List<Consumer<LevelChunk>>> fullStatusWaiters = new Reference2ObjectOpenHashMap<>(); -+ -+ void addFullStatusConsumer(final FullChunkStatus status, final Consumer<LevelChunk> consumer) { -+ this.fullStatusWaiters.computeIfAbsent(status, (final FullChunkStatus keyInMap) -> { -+ return new ArrayList<>(4); -+ }).add(consumer); -+ } -+ -+ private void completeFullStatusConsumers(FullChunkStatus status, final LevelChunk chunk) { -+ final List<Consumer<LevelChunk>> consumers; -+ consumers = this.fullStatusWaiters.remove(status); -+ -+ if (consumers == null) { -+ return; -+ } -+ -+ // must be scheduled to main, we do not trust the callback to not do anything stupid -+ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { -+ for (final Consumer<LevelChunk> consumer : consumers) { -+ try { -+ consumer.accept(chunk); -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to process chunk status callback", thr); -+ } -+ } -+ }, Priority.HIGHEST); -+ } -+ -+ // note: must hold scheduling lock -+ private void onChunkGenComplete(final ChunkAccess newChunk, final ChunkStatus newStatus, -+ final List<ChunkProgressionTask> scheduleList, final List<NewChunkHolder> changedLoadStatus) { -+ if (!this.neighboursBlockingGenTask.isEmpty()) { -+ throw new IllegalStateException("Cannot have neighbours blocking this gen task"); -+ } -+ if (newChunk != null || (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(newStatus))) { -+ this.completeStatusConsumers(newStatus, newChunk); -+ } -+ // done now, clear state (must be done before scheduling new tasks) -+ this.generationTask = null; -+ this.generationTaskStatus = null; -+ if (newChunk == null) { -+ // task was cancelled -+ // should be careful as this could be called while holding the schedule lock and/or inside the -+ // ticket level update -+ // while a task may be cancelled, it is possible for it to be later re-scheduled -+ // however, because generationTask is only set to null on _completion_, the scheduler leaves -+ // the rescheduling logic to us here -+ final ChunkStatus requestedGenStatus = this.requestedGenStatus; -+ this.requestedGenStatus = null; -+ if (requestedGenStatus != null) { -+ // it looks like it has been requested, so we must reschedule -+ if (!this.neighboursWaitingForUs.isEmpty()) { -+ for (final Iterator<Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus>> iterator = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus> entry = iterator.next(); -+ -+ final NewChunkHolder chunkHolder = entry.getKey(); -+ final ChunkStatus toStatus = entry.getValue(); -+ -+ if (!requestedGenStatus.isOrAfter(toStatus)) { -+ // if we were cancelled, we are responsible for removing the waiter -+ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) { -+ throw new IllegalStateException("Corrupt state"); -+ } -+ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) { -+ chunkHolder.checkUnload(); -+ } -+ iterator.remove(); -+ continue; -+ } -+ } -+ } -+ -+ // note: only after generationTask -> null, generationTaskStatus -> null, and requestedGenStatus -> null -+ this.scheduler.schedule( -+ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList -+ ); -+ -+ // return, can't do anything further -+ return; -+ } -+ -+ if (!this.neighboursWaitingForUs.isEmpty()) { -+ for (final NewChunkHolder chunkHolder : this.neighboursWaitingForUs.keySet()) { -+ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) { -+ throw new IllegalStateException("Corrupt state"); -+ } -+ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) { -+ chunkHolder.checkUnload(); -+ } -+ } -+ this.neighboursWaitingForUs.clear(); -+ } -+ // reset priority, we have nothing left to generate to -+ this.setPriority(null); -+ this.checkUnload(); -+ return; -+ } -+ -+ this.currentChunk = newChunk; -+ this.currentGenStatus = newStatus; -+ final ChunkCompletion completion = new ChunkCompletion(newChunk, newStatus); -+ CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, newStatus.getIndex(), completion); -+ this.lastChunkCompletion = completion; -+ -+ final ChunkStatus requestedGenStatus = this.requestedGenStatus; -+ -+ List<NewChunkHolder> needsScheduling = null; -+ boolean recalculatePriority = false; -+ for (final Iterator<Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus>> iterator -+ = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus> entry = iterator.next(); -+ final NewChunkHolder neighbour = entry.getKey(); -+ final ChunkStatus requiredStatus = entry.getValue(); -+ -+ if (!newStatus.isOrAfter(requiredStatus)) { -+ if (requestedGenStatus == null || !requestedGenStatus.isOrAfter(requiredStatus)) { -+ // if we're cancelled, still need to clear this map -+ if (!neighbour.neighboursBlockingGenTask.remove(this)) { -+ throw new IllegalStateException("Neighbour is not waiting for us?"); -+ } -+ if (neighbour.neighboursBlockingGenTask.isEmpty()) { -+ neighbour.checkUnload(); -+ } -+ -+ iterator.remove(); -+ } -+ continue; -+ } -+ -+ // doesn't matter what isCancelled is here, we need to schedule if we can -+ -+ recalculatePriority = true; -+ if (!neighbour.neighboursBlockingGenTask.remove(this)) { -+ throw new IllegalStateException("Neighbour is not waiting for us?"); -+ } -+ -+ if (neighbour.neighboursBlockingGenTask.isEmpty()) { -+ if (neighbour.requestedGenStatus != null) { -+ if (needsScheduling == null) { -+ needsScheduling = new ArrayList<>(); -+ } -+ needsScheduling.add(neighbour); -+ } else { -+ neighbour.checkUnload(); -+ } -+ } -+ -+ // remove last; access to entry will throw if removed -+ iterator.remove(); -+ } -+ -+ if (newStatus == ChunkStatus.FULL) { -+ this.lockPriority(); -+ // try to push pending to FULL -+ if (this.updatePendingStatus()) { -+ changedLoadStatus.add(this); -+ } -+ } -+ -+ if (recalculatePriority) { -+ this.recalculateNeighbourRequestedPriority(); -+ } -+ -+ if (requestedGenStatus != null && !newStatus.isOrAfter(requestedGenStatus)) { -+ this.scheduleNeighbours(needsScheduling, scheduleList); -+ -+ // we need to schedule more tasks now -+ this.scheduler.schedule( -+ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList -+ ); -+ } else { -+ // we're done now -+ if (requestedGenStatus != null) { -+ this.requestedGenStatus = null; -+ } -+ // reached final stage, so stop scheduling now -+ this.setPriority(null); -+ this.checkUnload(); -+ -+ this.scheduleNeighbours(needsScheduling, scheduleList); -+ } -+ } -+ -+ private void scheduleNeighbours(final List<NewChunkHolder> needsScheduling, final List<ChunkProgressionTask> scheduleList) { -+ if (needsScheduling != null) { -+ for (int i = 0, len = needsScheduling.size(); i < len; ++i) { -+ final NewChunkHolder neighbour = needsScheduling.get(i); -+ -+ this.scheduler.schedule( -+ neighbour.chunkX, neighbour.chunkZ, neighbour.requestedGenStatus, neighbour, scheduleList -+ ); -+ } -+ } -+ } -+ -+ public void setGenerationTask(final ChunkProgressionTask generationTask, final ChunkStatus taskStatus, -+ final List<NewChunkHolder> neighbours) { -+ if (this.generationTask != null || (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(taskStatus))) { -+ throw new IllegalStateException("Currently generating or provided task is trying to generate to a level we are already at!"); -+ } -+ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(taskStatus)) { -+ throw new IllegalStateException("Cannot schedule generation task when not requested"); -+ } -+ this.generationTask = generationTask; -+ this.generationTaskStatus = taskStatus; -+ -+ for (int i = 0, len = neighbours.size(); i < len; ++i) { -+ neighbours.get(i).addNeighbourUsingChunk(); -+ } -+ -+ this.checkUnload(); -+ -+ generationTask.onComplete((final ChunkAccess access, final Throwable thr) -> { -+ if (generationTask != this.generationTask) { -+ throw new IllegalStateException( -+ "Cannot complete generation task '" + generationTask + "' because we are waiting on '" + this.generationTask + "' instead!" -+ ); -+ } -+ if (thr != null) { -+ if (this.genTaskException != null) { -+ LOGGER.warn("Ignoring exception for " + this.toString(), thr); -+ return; -+ } -+ // don't set generation task to null, so that scheduling will not attempt to create another task and it -+ // will automatically block any further scheduling usage of this chunk as it will wait forever for a failed -+ // task to complete -+ this.genTaskException = thr; -+ this.failedGenStatus = taskStatus; -+ this.genTaskFailedThread = Thread.currentThread(); -+ -+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( -+ "Generation task", ChunkTaskScheduler.stringIfNull(generationTask), -+ "Task to status", ChunkTaskScheduler.stringIfNull(taskStatus) -+ ), thr); -+ return; -+ } -+ -+ final boolean scheduleTasks; -+ List<ChunkProgressionTask> tasks = ChunkHolderManager.getCurrentTicketUpdateScheduling(); -+ if (tasks == null) { -+ scheduleTasks = true; -+ tasks = new ArrayList<>(); -+ } else { -+ scheduleTasks = false; -+ // we are currently updating ticket levels, so we already hold the schedule lock -+ // this means we have to leave the ticket level update to handle the scheduling -+ } -+ final List<NewChunkHolder> changedLoadStatus = new ArrayList<>(); -+ // theoretically, we could schedule a chunk at the max radius which performs another max radius access. So we need to double the radius. -+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, 2 * ChunkTaskScheduler.getMaxAccessRadius()); -+ try { -+ for (int i = 0, len = neighbours.size(); i < len; ++i) { -+ neighbours.get(i).removeNeighbourUsingChunk(); -+ } -+ this.onChunkGenComplete(access, taskStatus, tasks, changedLoadStatus); -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ this.scheduler.chunkHolderManager.addChangedStatuses(changedLoadStatus); -+ -+ if (scheduleTasks) { -+ // can't hold the lock while scheduling, so we have to build the tasks and then schedule after -+ for (int i = 0, len = tasks.size(); i < len; ++i) { -+ tasks.get(i).schedule(); -+ } -+ } -+ }); -+ } -+ -+ public PoiChunk getPoiChunk() { -+ return this.poiChunk; -+ } -+ -+ public ChunkEntitySlices getEntityChunk() { -+ return this.entityChunk; -+ } -+ -+ public long lastAutoSave; -+ -+ public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {} -+ -+ private static final MoonriseRegionFileIO.RegionFileType[] REGION_FILE_TYPES = MoonriseRegionFileIO.RegionFileType.values(); -+ -+ public SaveStat save(final boolean shutdown) { -+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main"); -+ -+ ChunkAccess chunk = this.getCurrentChunk(); -+ PoiChunk poi = this.getPoiChunk(); -+ ChunkEntitySlices entities = this.getEntityChunk(); -+ boolean executedUnloadTask = false; -+ final boolean[] executedUnloadTasks = new boolean[REGION_FILE_TYPES.length]; -+ -+ if (shutdown) { -+ // make sure that the async unloads complete -+ if (this.unloadState != null) { -+ // must have errored during unload -+ chunk = this.unloadState.chunk(); -+ poi = this.unloadState.poiChunk(); -+ entities = this.unloadState.entityChunk(); -+ } -+ for (final MoonriseRegionFileIO.RegionFileType regionFileType : REGION_FILE_TYPES) { -+ final UnloadTask unloadTask = this.getUnloadTask(regionFileType); -+ if (unloadTask == null) { -+ continue; -+ } -+ -+ final PrioritisedExecutor.PrioritisedTask task = unloadTask.task(); -+ if (task != null && task.isQueued()) { -+ final boolean executed = task.execute(); -+ executedUnloadTask |= executed; -+ executedUnloadTasks[regionFileType.ordinal()] = executed; -+ } -+ } -+ } -+ -+ final boolean forceNoSaveChunk = PlatformHooks.get().forceNoSave(chunk); -+ -+ // can only synchronously save worldgen chunks during shutdown -+ boolean canSaveChunk = !forceNoSaveChunk && (chunk != null && ((shutdown || chunk instanceof LevelChunk) && chunk.isUnsaved())); -+ boolean canSavePOI = !forceNoSaveChunk && (poi != null && poi.isDirty()); -+ boolean canSaveEntities = entities != null; -+ -+ if (canSaveChunk) { -+ canSaveChunk = this.saveChunk(chunk, false); -+ } -+ if (canSavePOI) { -+ canSavePOI = this.savePOI(poi, false); -+ } -+ if (canSaveEntities) { -+ // on shutdown, we need to force transient entity chunks to save -+ canSaveEntities = this.saveEntities(entities, shutdown); -+ if (shutdown) { -+ this.lastEntityUnload = null; -+ } -+ } -+ -+ return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI ? -+ new SaveStat( -+ canSaveChunk | executedUnloadTasks[MoonriseRegionFileIO.RegionFileType.CHUNK_DATA.ordinal()], -+ canSaveEntities | executedUnloadTasks[MoonriseRegionFileIO.RegionFileType.ENTITY_DATA.ordinal()], -+ canSavePOI | executedUnloadTasks[MoonriseRegionFileIO.RegionFileType.POI_DATA.ordinal()] -+ ) -+ : null; -+ } -+ -+ private boolean saveChunk(final ChunkAccess chunk, final boolean unloading) { -+ if (!chunk.isUnsaved()) { -+ if (unloading) { -+ this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, null); -+ } -+ return false; -+ } -+ try { -+ final SerializableChunkData chunkData = SerializableChunkData.copyOf(this.world, chunk); -+ PlatformHooks.get().chunkSyncSave(this.world, chunk, chunkData); -+ -+ chunk.tryMarkSaved(); -+ -+ final CallbackCompletable<CompoundTag> completable = new CallbackCompletable<>(); -+ -+ final Runnable run = () -> { -+ final CompoundTag data = chunkData.write(); -+ -+ completable.complete(data); -+ -+ if (unloading) { -+ NewChunkHolder.this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, data); -+ } -+ }; -+ -+ final PrioritisedExecutor.PrioritisedTask task; -+ if (unloading) { -+ this.chunkDataUnload.toRun().setRunnable(run); -+ task = this.chunkDataUnload.task(); -+ } else { -+ task = this.scheduler.saveExecutor.createTask(run); -+ } -+ -+ task.queue(); -+ -+ MoonriseRegionFileIO.scheduleSave( -+ this.world, this.chunkX, this.chunkZ, completable, task, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, Priority.NORMAL -+ ); -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr); -+ } -+ -+ return true; -+ } -+ -+ private boolean lastEntitySaveNull; -+ private CompoundTag lastEntityUnload; -+ private boolean saveEntities(final ChunkEntitySlices entities, final boolean unloading) { -+ try { -+ CompoundTag mergeFrom = null; -+ if (entities.isTransient()) { -+ if (!unloading) { -+ // if we're a transient chunk, we cannot save until unloading because otherwise a double save will -+ // result in double adding the entities -+ return false; -+ } -+ try { -+ mergeFrom = MoonriseRegionFileIO.loadData(this.world, this.chunkX, this.chunkZ, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, Priority.BLOCKING); -+ } catch (final Exception ex) { -+ LOGGER.error("Cannot merge transient entities for chunk (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', data on disk will be replaced", ex); -+ } -+ } -+ -+ final CompoundTag save = entities.save(); -+ if (mergeFrom != null) { -+ if (save == null) { -+ // don't override the data on disk with nothing -+ return false; -+ } else { -+ ChunkEntitySlices.copyEntities(mergeFrom, save); -+ } -+ } -+ if (save == null && this.lastEntitySaveNull) { -+ return false; -+ } -+ -+ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, save, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA); -+ this.lastEntitySaveNull = save == null; -+ if (unloading) { -+ this.lastEntityUnload = save; -+ } -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr); -+ } -+ -+ return true; -+ } -+ -+ private boolean lastPoiSaveNull; -+ private boolean savePOI(final PoiChunk poi, final boolean unloading) { -+ try { -+ final CompoundTag save = poi.save(); -+ poi.setDirty(false); -+ if (save == null && this.lastPoiSaveNull) { -+ if (unloading) { -+ this.poiDataUnload.completable().complete(null); -+ } -+ return false; -+ } -+ -+ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, save, MoonriseRegionFileIO.RegionFileType.POI_DATA); -+ this.lastPoiSaveNull = save == null; -+ if (unloading) { -+ this.poiDataUnload.completable().complete(save); -+ } -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr); -+ } -+ -+ return true; -+ } -+ -+ @Override -+ public String toString() { -+ final ChunkCompletion lastCompletion = this.lastChunkCompletion; -+ final ChunkEntitySlices entityChunk = this.entityChunk; -+ final FullChunkStatus pendingFullStatus = this.pendingFullChunkStatus; -+ final FullChunkStatus currentFullStatus = this.currentFullChunkStatus; -+ return "NewChunkHolder{" + -+ "world=" + WorldUtil.getWorldName(this.world) + -+ ", chunkX=" + this.chunkX + -+ ", chunkZ=" + this.chunkZ + -+ ", entityChunkFromDisk=" + (entityChunk != null && !entityChunk.isTransient()) + -+ ", lastChunkCompletion={chunk_class=" + (lastCompletion == null || lastCompletion.chunk() == null ? "null" : lastCompletion.chunk().getClass().getName()) + ",status=" + (lastCompletion == null ? "null" : lastCompletion.genStatus()) + "}" + -+ ", currentGenStatus=" + this.currentGenStatus + -+ ", requestedGenStatus=" + this.requestedGenStatus + -+ ", generationTask=" + this.generationTask + -+ ", generationTaskStatus=" + this.generationTaskStatus + -+ ", priority=" + this.priority + -+ ", priorityLocked=" + this.priorityLocked + -+ ", neighbourRequestedPriority=" + this.neighbourRequestedPriority + -+ ", effective_priority=" + this.getEffectivePriority(null) + -+ ", oldTicketLevel=" + this.oldTicketLevel + -+ ", currentTicketLevel=" + this.currentTicketLevel + -+ ", totalNeighboursUsingThisChunk=" + this.totalNeighboursUsingThisChunk + -+ ", fullNeighbourChunksLoadedBitset=" + this.fullNeighbourChunksLoadedBitset + -+ ", currentChunkStatus=" + currentFullStatus + -+ ", pendingChunkStatus=" + pendingFullStatus + -+ ", is_unload_safe=" + this.isSafeToUnload() + -+ ", killed=" + this.unloaded + -+ '}'; -+ } -+ -+ private static JsonElement serializeStacktraceElement(final StackTraceElement element) { -+ return element == null ? JsonNull.INSTANCE : new JsonPrimitive(element.toString()); -+ } -+ -+ private static JsonObject serializeCompletable(final CallbackCompletable<?> completable) { -+ final JsonObject ret = new JsonObject(); -+ -+ if (completable == null) { -+ return ret; -+ } -+ -+ ret.addProperty("valid", Boolean.TRUE); -+ -+ final boolean isCompleted = completable.isCompleted(); -+ ret.addProperty("completed", Boolean.valueOf(isCompleted)); -+ -+ if (isCompleted) { -+ final Throwable throwable = completable.getThrowable(); -+ if (throwable != null) { -+ final JsonArray throwableJson = new JsonArray(); -+ ret.add("throwable", throwableJson); -+ -+ for (final StackTraceElement element : throwable.getStackTrace()) { -+ throwableJson.add(serializeStacktraceElement(element)); -+ } -+ } else { -+ final Object result = completable.getResult(); -+ ret.add("result_class", result == null ? JsonNull.INSTANCE : new JsonPrimitive(result.getClass().getName())); -+ } -+ } -+ -+ return ret; -+ } -+ -+ // (probably) holds ticket and scheduling lock -+ public JsonObject getDebugJson() { -+ final JsonObject ret = new JsonObject(); -+ -+ final ChunkCompletion lastCompletion = this.lastChunkCompletion; -+ final ChunkEntitySlices slices = this.entityChunk; -+ final PoiChunk poiChunk = this.poiChunk; -+ -+ ret.addProperty("chunkX", Integer.valueOf(this.chunkX)); -+ ret.addProperty("chunkZ", Integer.valueOf(this.chunkZ)); -+ ret.addProperty("entity_chunk", slices == null ? "null" : "transient=" + slices.isTransient()); -+ ret.addProperty("poi_chunk", "null=" + (poiChunk == null)); -+ ret.addProperty("completed_chunk_class", lastCompletion == null ? "null" : lastCompletion.chunk().getClass().getName()); -+ ret.addProperty("completed_gen_status", lastCompletion == null ? "null" : lastCompletion.genStatus().toString()); -+ ret.addProperty("priority", Objects.toString(this.priority)); -+ ret.addProperty("neighbour_requested_priority", Objects.toString(this.neighbourRequestedPriority)); -+ ret.addProperty("generation_task", Objects.toString(this.generationTask)); -+ ret.addProperty("is_safe_unload", Objects.toString(this.isSafeToUnload())); -+ ret.addProperty("old_ticket_level", Integer.valueOf(this.oldTicketLevel)); -+ ret.addProperty("current_ticket_level", Integer.valueOf(this.currentTicketLevel)); -+ ret.addProperty("neighbours_using_chunk", Integer.valueOf(this.totalNeighboursUsingThisChunk)); -+ -+ final JsonObject neighbourWaitState = new JsonObject(); -+ ret.add("neighbour_state", neighbourWaitState); -+ -+ final JsonArray blockingGenNeighbours = new JsonArray(); -+ neighbourWaitState.add("blocking_gen_task", blockingGenNeighbours); -+ for (final NewChunkHolder blockingGenNeighbour : this.neighboursBlockingGenTask) { -+ final JsonObject neighbour = new JsonObject(); -+ blockingGenNeighbours.add(neighbour); -+ -+ neighbour.addProperty("chunkX", Integer.valueOf(blockingGenNeighbour.chunkX)); -+ neighbour.addProperty("chunkZ", Integer.valueOf(blockingGenNeighbour.chunkZ)); -+ } -+ -+ final JsonArray neighboursWaitingForUs = new JsonArray(); -+ neighbourWaitState.add("neighbours_waiting_on_us", neighboursWaitingForUs); -+ for (final Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus> entry : this.neighboursWaitingForUs.reference2ObjectEntrySet()) { -+ final NewChunkHolder holder = entry.getKey(); -+ final ChunkStatus status = entry.getValue(); -+ -+ final JsonObject neighbour = new JsonObject(); -+ neighboursWaitingForUs.add(neighbour); -+ -+ -+ neighbour.addProperty("chunkX", Integer.valueOf(holder.chunkX)); -+ neighbour.addProperty("chunkZ", Integer.valueOf(holder.chunkZ)); -+ neighbour.addProperty("waiting_for", Objects.toString(status)); -+ } -+ -+ ret.addProperty("pending_chunk_full_status", Objects.toString(this.pendingFullChunkStatus)); -+ ret.addProperty("current_chunk_full_status", Objects.toString(this.currentFullChunkStatus)); -+ ret.addProperty("generation_task", Objects.toString(this.generationTask)); -+ ret.addProperty("requested_generation", Objects.toString(this.requestedGenStatus)); -+ ret.addProperty("has_entity_load_task", Boolean.valueOf(this.entityDataLoadTask != null)); -+ ret.addProperty("has_poi_load_task", Boolean.valueOf(this.poiDataLoadTask != null)); -+ -+ final UnloadTask entityDataUnload = this.entityDataUnload; -+ final UnloadTask poiDataUnload = this.poiDataUnload; -+ final UnloadTask chunkDataUnload = this.chunkDataUnload; -+ -+ ret.add("entity_unload_completable", serializeCompletable(entityDataUnload == null ? null : entityDataUnload.completable())); -+ ret.add("poi_unload_completable", serializeCompletable(poiDataUnload == null ? null : poiDataUnload.completable())); -+ ret.add("chunk_unload_completable", serializeCompletable(chunkDataUnload == null ? null : chunkDataUnload.completable())); -+ -+ final PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task(); -+ if (unloadTask == null) { -+ ret.addProperty("unload_task_priority", "null"); -+ ret.addProperty("unload_task_suborder", Long.valueOf(0L)); -+ } else { -+ ret.addProperty("unload_task_priority", Objects.toString(unloadTask.getPriority())); -+ ret.addProperty("unload_task_suborder", Long.valueOf(unloadTask.getSubOrder())); -+ } -+ -+ ret.addProperty("killed", Boolean.valueOf(this.unloaded)); -+ -+ return ret; -+ } -+} -diff --git a/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<Section> sections; -+ -+ public ThreadedTicketLevelPropagator() { -+ this.updateQueue = new UpdateQueue(); -+ this.sections = new ConcurrentLong2ReferenceChainedHashTable<>(); -+ } -+ -+ // must hold ticket lock for: -+ // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1)) -+ public void setSource(final int posX, final int posZ, final int to) { -+ if (to < 1 || to > MAX_SOURCE_LEVEL) { -+ throw new IllegalArgumentException("Source: " + to); -+ } -+ -+ final int sectionX = posX >> SECTION_SHIFT; -+ final int sectionZ = posZ >> SECTION_SHIFT; -+ -+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); -+ Section section = this.sections.get(coordinate); -+ if (section == null) { -+ if (null != this.sections.putIfAbsent(coordinate, section = new Section(sectionX, sectionZ))) { -+ throw new IllegalStateException("Race condition while creating new section"); -+ } -+ } -+ -+ final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); -+ final short sLocalIdx = (short)localIdx; -+ -+ final short sourceAndLevel = section.levels[localIdx]; -+ final int currentSource = (sourceAndLevel >>> 8) & 0xFF; -+ -+ if (currentSource == to) { -+ // nothing to do -+ // make sure to kill the current update, if any -+ section.queuedSources.replace(sLocalIdx, (byte)to); -+ return; -+ } -+ -+ if (section.queuedSources.put(sLocalIdx, (byte)to) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) { -+ this.queueSectionUpdate(section); -+ } -+ } -+ -+ // must hold ticket lock for: -+ // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1)) -+ public void removeSource(final int posX, final int posZ) { -+ final int sectionX = posX >> SECTION_SHIFT; -+ final int sectionZ = posZ >> SECTION_SHIFT; -+ -+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); -+ final Section section = this.sections.get(coordinate); -+ -+ if (section == null) { -+ return; -+ } -+ -+ final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); -+ final short sLocalIdx = (short)localIdx; -+ -+ final int currentSource = (section.levels[localIdx] >>> 8) & 0xFF; -+ -+ if (currentSource == 0) { -+ // we use replace here so that we do not possibly multi-queue a section for an update -+ section.queuedSources.replace(sLocalIdx, (byte)0); -+ return; -+ } -+ -+ if (section.queuedSources.put(sLocalIdx, (byte)0) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) { -+ this.queueSectionUpdate(section); -+ } -+ } -+ -+ private void queueSectionUpdate(final Section section) { -+ this.updateQueue.append(new UpdateQueue.UpdateQueueNode(section, null)); -+ } -+ -+ public boolean hasPendingUpdates() { -+ return !this.updateQueue.isEmpty(); -+ } -+ -+ // holds ticket lock for every chunk section represented by any position in the key set -+ // updates is modifiable and passed to processSchedulingUpdates after this call -+ protected abstract void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates); -+ -+ // holds ticket lock for every chunk section represented by any position in the key set -+ // holds scheduling lock in max access radius for every position held by the ticket lock -+ // updates is cleared after this call -+ protected abstract void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List<ChunkProgressionTask> scheduledTasks, -+ final List<NewChunkHolder> changedFullStatus); -+ -+ // must hold ticket lock for every position in the sections in one radius around sectionX,sectionZ -+ public boolean performUpdate(final int sectionX, final int sectionZ, final ReentrantAreaLock schedulingLock, -+ final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedFullStatus) { -+ if (!this.hasPendingUpdates()) { -+ return false; -+ } -+ -+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); -+ final Section section = this.sections.get(coordinate); -+ -+ if (section == null || section.queuedSources.isEmpty()) { -+ // no section or no updates -+ return false; -+ } -+ -+ final Propagator propagator = Propagator.acquirePropagator(); -+ final boolean ret = this.performUpdate(section, null, propagator, -+ null, schedulingLock, scheduledTasks, changedFullStatus -+ ); -+ Propagator.returnPropagator(propagator); -+ return ret; -+ } -+ -+ private boolean performUpdate(final Section section, final UpdateQueue.UpdateQueueNode node, final Propagator propagator, -+ final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock, -+ final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedFullStatus) { -+ final int sectionX = section.sectionX; -+ final int sectionZ = section.sectionZ; -+ -+ final int rad1MinX = (sectionX - 1) << SECTION_SHIFT; -+ final int rad1MinZ = (sectionZ - 1) << SECTION_SHIFT; -+ final int rad1MaxX = ((sectionX + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1); -+ final int rad1MaxZ = ((sectionZ + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1); -+ -+ // set up encode offset first as we need to queue level changes _before_ -+ propagator.setupEncodeOffset(sectionX, sectionZ); -+ -+ final int coordinateOffset = propagator.coordinateOffset; -+ -+ final ReentrantAreaLock.Node ticketNode = ticketLock == null ? null : ticketLock.lock(rad1MinX, rad1MinZ, rad1MaxX, rad1MaxZ); -+ final boolean ret; -+ try { -+ // first, check if this update was stolen -+ if (section != this.sections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ))) { -+ // occurs when a stolen update deletes this section -+ // it is possible that another update is scheduled, but that one will have the correct section -+ if (node != null) { -+ this.updateQueue.remove(node); -+ } -+ return false; -+ } -+ -+ final int oldSourceSize = section.sources.size(); -+ -+ // process pending sources -+ for (final Iterator<Short2ByteMap.Entry> iterator = section.queuedSources.short2ByteEntrySet().fastIterator(); iterator.hasNext();) { -+ final Short2ByteMap.Entry entry = iterator.next(); -+ final int pos = (int)entry.getShortKey(); -+ final int posX = (pos & (SECTION_SIZE - 1)) | (sectionX << SECTION_SHIFT); -+ final int posZ = ((pos >> SECTION_SHIFT) & (SECTION_SIZE - 1)) | (sectionZ << SECTION_SHIFT); -+ final int newSource = (int)entry.getByteValue(); -+ -+ final short currentEncoded = section.levels[pos]; -+ final int currLevel = currentEncoded & 0xFF; -+ final int prevSource = (currentEncoded >>> 8) & 0xFF; -+ -+ if (prevSource == newSource) { -+ // nothing changed -+ continue; -+ } -+ -+ if ((prevSource < currLevel && newSource <= currLevel) || newSource == currLevel) { -+ // just update the source, don't need to propagate change -+ section.levels[pos] = (short)(currLevel | (newSource << 8)); -+ // level is unchanged, don't add to changed positions -+ } else { -+ // set current level and current source to new source -+ section.levels[pos] = (short)(newSource | (newSource << 8)); -+ // must add to updated positions in case this is final -+ propagator.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)newSource); -+ if (newSource != 0) { -+ // queue increase with new source level -+ propagator.appendToIncreaseQueue( -+ ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) | -+ ((newSource & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) | -+ (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS)) -+ ); -+ } -+ // queue decrease with previous level -+ if (newSource < currLevel) { -+ propagator.appendToDecreaseQueue( -+ ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) | -+ ((currLevel & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) | -+ (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS)) -+ ); -+ } -+ } -+ -+ if (newSource == 0) { -+ // prevSource != newSource, so we are removing this source -+ section.sources.remove((short)pos); -+ } else if (prevSource == 0) { -+ // prevSource != newSource, so we are adding this source -+ section.sources.add((short)pos); -+ } -+ } -+ -+ section.queuedSources.clear(); -+ -+ final int newSourceSize = section.sources.size(); -+ -+ if (oldSourceSize == 0 && newSourceSize != 0) { -+ // need to make sure the sections in 1 radius are initialised -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ if ((dx | dz) == 0) { -+ continue; -+ } -+ final int offX = dx + sectionX; -+ final int offZ = dz + sectionZ; -+ final long coordinate = CoordinateUtils.getChunkKey(offX, offZ); -+ final Section neighbour = this.sections.computeIfAbsent(coordinate, (final long keyInMap) -> { -+ return new Section(CoordinateUtils.getChunkX(keyInMap), CoordinateUtils.getChunkZ(keyInMap)); -+ }); -+ -+ // increase ref count -+ ++neighbour.oneRadNeighboursWithSources; -+ if (neighbour.oneRadNeighboursWithSources <= 0 || neighbour.oneRadNeighboursWithSources > 8) { -+ throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources)); -+ } -+ } -+ } -+ } -+ -+ if (propagator.hasUpdates()) { -+ propagator.setupCaches(this, sectionX, sectionZ, 1); -+ propagator.performDecrease(); -+ // don't need try-finally, as any exception will cause the propagator to not be returned -+ propagator.destroyCaches(); -+ } -+ -+ if (newSourceSize == 0) { -+ final boolean decrementRef = oldSourceSize != 0; -+ // check for section de-init -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ final int offX = dx + sectionX; -+ final int offZ = dz + sectionZ; -+ final long coordinate = CoordinateUtils.getChunkKey(offX, offZ); -+ final Section neighbour = this.sections.get(coordinate); -+ -+ if (neighbour == null) { -+ if (oldSourceSize == 0 && (dx | dz) != 0) { -+ // since we don't have sources, this section is allowed to be null -+ continue; -+ } -+ throw new IllegalStateException("??"); -+ } -+ -+ if (decrementRef && (dx | dz) != 0) { -+ // decrease ref count, but only for neighbours -+ --neighbour.oneRadNeighboursWithSources; -+ } -+ -+ // we need to check the current section for de-init as well -+ if (neighbour.oneRadNeighboursWithSources == 0) { -+ if (neighbour.queuedSources.isEmpty() && neighbour.sources.isEmpty()) { -+ // need to de-init -+ this.sections.remove(coordinate); -+ } // else: neighbour is queued for an update, and it will de-init itself -+ } else if (neighbour.oneRadNeighboursWithSources < 0 || neighbour.oneRadNeighboursWithSources > 8) { -+ throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources)); -+ } -+ } -+ } -+ } -+ -+ -+ ret = !propagator.updatedPositions.isEmpty(); -+ -+ if (ret) { -+ this.processLevelUpdates(propagator.updatedPositions); -+ -+ if (!propagator.updatedPositions.isEmpty()) { -+ // now we can actually update the ticket levels in the chunk holders -+ final int maxScheduleRadius = getMaxSchedulingRadius(); -+ -+ // allow the chunkholders to process ticket level updates without needing to acquire the schedule lock every time -+ final ReentrantAreaLock.Node schedulingNode = schedulingLock.lock( -+ rad1MinX - maxScheduleRadius, rad1MinZ - maxScheduleRadius, -+ rad1MaxX + maxScheduleRadius, rad1MaxZ + maxScheduleRadius -+ ); -+ try { -+ this.processSchedulingUpdates(propagator.updatedPositions, scheduledTasks, changedFullStatus); -+ } finally { -+ schedulingLock.unlock(schedulingNode); -+ } -+ } -+ -+ propagator.updatedPositions.clear(); -+ } -+ } finally { -+ if (ticketLock != null) { -+ ticketLock.unlock(ticketNode); -+ } -+ } -+ -+ // finished -+ if (node != null) { -+ this.updateQueue.remove(node); -+ } -+ -+ return ret; -+ } -+ -+ public boolean performUpdates(final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock, -+ final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedFullStatus) { -+ if (this.updateQueue.isEmpty()) { -+ return false; -+ } -+ -+ final long maxOrder = this.updateQueue.getLastOrder(); -+ -+ boolean updated = false; -+ Propagator propagator = null; -+ -+ for (;;) { -+ final UpdateQueue.UpdateQueueNode toUpdate = this.updateQueue.acquireNextOrWait(maxOrder); -+ if (toUpdate == null) { -+ if (!this.updateQueue.hasRemainingUpdates(maxOrder)) { -+ if (propagator != null) { -+ Propagator.returnPropagator(propagator); -+ } -+ return updated; -+ } -+ -+ continue; -+ } -+ -+ if (propagator == null) { -+ propagator = Propagator.acquirePropagator(); -+ } -+ -+ updated |= this.performUpdate(toUpdate.section, toUpdate, propagator, ticketLock, schedulingLock, scheduledTasks, changedFullStatus); -+ } -+ } -+ -+ // Similar implementation of concurrent FIFO queue (See MTQ in ConcurrentUtil) which has an additional node pointer -+ // for the last update node being handled -+ private static final class UpdateQueue { -+ -+ private volatile UpdateQueueNode head; -+ private volatile UpdateQueueNode tail; -+ -+ private static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "head", UpdateQueueNode.class); -+ private static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "tail", UpdateQueueNode.class); -+ -+ /* head */ -+ -+ private final void setHeadPlain(final UpdateQueueNode newHead) { -+ HEAD_HANDLE.set(this, newHead); -+ } -+ -+ private final void setHeadOpaque(final UpdateQueueNode newHead) { -+ HEAD_HANDLE.setOpaque(this, newHead); -+ } -+ -+ private final UpdateQueueNode getHeadPlain() { -+ return (UpdateQueueNode)HEAD_HANDLE.get(this); -+ } -+ -+ private final UpdateQueueNode getHeadOpaque() { -+ return (UpdateQueueNode)HEAD_HANDLE.getOpaque(this); -+ } -+ -+ private final UpdateQueueNode getHeadAcquire() { -+ return (UpdateQueueNode)HEAD_HANDLE.getAcquire(this); -+ } -+ -+ /* tail */ -+ -+ private final void setTailPlain(final UpdateQueueNode newTail) { -+ TAIL_HANDLE.set(this, newTail); -+ } -+ -+ private final void setTailOpaque(final UpdateQueueNode newTail) { -+ TAIL_HANDLE.setOpaque(this, newTail); -+ } -+ -+ private final UpdateQueueNode getTailPlain() { -+ return (UpdateQueueNode)TAIL_HANDLE.get(this); -+ } -+ -+ private final UpdateQueueNode getTailOpaque() { -+ return (UpdateQueueNode)TAIL_HANDLE.getOpaque(this); -+ } -+ -+ public UpdateQueue() { -+ final UpdateQueueNode dummy = new UpdateQueueNode(null, null); -+ dummy.order = -1L; -+ dummy.preventAdds(); -+ -+ this.setHeadPlain(dummy); -+ this.setTailPlain(dummy); -+ } -+ -+ public boolean isEmpty() { -+ return this.peek() == null; -+ } -+ -+ public boolean hasRemainingUpdates(final long maxUpdate) { -+ final UpdateQueueNode node = this.peek(); -+ return node != null && node.order <= maxUpdate; -+ } -+ -+ public long getLastOrder() { -+ for (UpdateQueueNode tail = this.getTailOpaque(), curr = tail;;) { -+ final UpdateQueueNode next = curr.getNextVolatile(); -+ if (next == null) { -+ // try to update stale tail -+ if (this.getTailOpaque() == tail && curr != tail) { -+ this.setTailOpaque(curr); -+ } -+ return curr.order; -+ } -+ curr = next; -+ } -+ } -+ -+ private static void await(final UpdateQueueNode node) { -+ final Thread currThread = Thread.currentThread(); -+ // we do not use add-blocking because we use the nullability of the section to block -+ // remove() does not begin to poll from the wait queue until the section is null'd, -+ // and so provided we check the nullability before parking there is no ordering of these operations -+ // such that remove() finishes polling from the wait queue while section is not null -+ node.add(currThread); -+ -+ // wait until completed -+ while (node.getSectionVolatile() != null) { -+ LockSupport.park(); -+ } -+ } -+ -+ public UpdateQueueNode acquireNextOrWait(final long maxOrder) { -+ final List<UpdateQueueNode> blocking = new ArrayList<>(); -+ -+ node_search: -+ for (UpdateQueueNode curr = this.peek(); curr != null && curr.order <= maxOrder; curr = curr.getNextVolatile()) { -+ if (curr.getSectionVolatile() == null) { -+ continue; -+ } -+ -+ if (curr.getUpdatingVolatile()) { -+ blocking.add(curr); -+ continue; -+ } -+ -+ for (int i = 0, len = blocking.size(); i < len; ++i) { -+ final UpdateQueueNode node = blocking.get(i); -+ -+ if (node.intersects(curr)) { -+ continue node_search; -+ } -+ } -+ -+ if (curr.getAndSetUpdatingVolatile(true)) { -+ blocking.add(curr); -+ continue; -+ } -+ -+ return curr; -+ } -+ -+ if (!blocking.isEmpty()) { -+ await(blocking.get(0)); -+ } -+ -+ return null; -+ } -+ -+ public UpdateQueueNode peek() { -+ for (UpdateQueueNode head = this.getHeadOpaque(), curr = head;;) { -+ final UpdateQueueNode next = curr.getNextVolatile(); -+ final Section element = curr.getSectionVolatile(); /* Likely in sync */ -+ -+ if (element != null) { -+ if (this.getHeadOpaque() == head && curr != head) { -+ this.setHeadOpaque(curr); -+ } -+ return curr; -+ } -+ -+ if (next == null) { -+ if (this.getHeadOpaque() == head && curr != head) { -+ this.setHeadOpaque(curr); -+ } -+ return null; -+ } -+ curr = next; -+ } -+ } -+ -+ public void remove(final UpdateQueueNode node) { -+ // mark as removed -+ node.setSectionVolatile(null); -+ -+ // use peek to advance head -+ this.peek(); -+ -+ // unpark any waiters / block the wait queue -+ Thread unpark; -+ while ((unpark = node.poll()) != null) { -+ LockSupport.unpark(unpark); -+ } -+ } -+ -+ public void append(final UpdateQueueNode node) { -+ int failures = 0; -+ -+ for (UpdateQueueNode currTail = this.getTailOpaque(), curr = currTail;;) { -+ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */ -+ /* It is likely due to a cache miss caused by another write to the next field */ -+ final UpdateQueueNode next = curr.getNextVolatile(); -+ -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ -+ if (next == null) { -+ node.order = curr.order + 1L; -+ final UpdateQueueNode compared = curr.compareExchangeNextVolatile(null, node); -+ -+ if (compared == null) { -+ /* Added */ -+ /* Avoid CASing on tail more than we need to */ -+ /* CAS to avoid setting an out-of-date tail */ -+ if (this.getTailOpaque() == currTail) { -+ this.setTailOpaque(node); -+ } -+ return; -+ } -+ -+ ++failures; -+ curr = compared; -+ continue; -+ } -+ -+ if (curr == currTail) { -+ /* Tail is likely not up-to-date */ -+ curr = next; -+ } else { -+ /* Try to update to tail */ -+ if (currTail == (currTail = this.getTailOpaque())) { -+ curr = next; -+ } else { -+ curr = currTail; -+ } -+ } -+ } -+ } -+ -+ // each node also represents a set of waiters, represented by the MTQ -+ // if the queue is add-blocked, then the update is complete -+ private static final class UpdateQueueNode extends MultiThreadedQueue<Thread> { -+ private final int sectionX; -+ private final int sectionZ; -+ -+ private long order; -+ private volatile Section section; -+ private volatile UpdateQueueNode next; -+ private volatile boolean updating; -+ -+ private static final VarHandle SECTION_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "section", Section.class); -+ private static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "next", UpdateQueueNode.class); -+ private static final VarHandle UPDATING_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "updating", boolean.class); -+ -+ public UpdateQueueNode(final Section section, final UpdateQueueNode next) { -+ if (section == null) { -+ this.sectionX = this.sectionZ = 0; -+ } else { -+ this.sectionX = section.sectionX; -+ this.sectionZ = section.sectionZ; -+ } -+ -+ SECTION_HANDLE.set(this, section); -+ NEXT_HANDLE.set(this, next); -+ } -+ -+ public boolean intersects(final UpdateQueueNode other) { -+ final int dist = Math.max(Math.abs(this.sectionX - other.sectionX), Math.abs(this.sectionZ - other.sectionZ)); -+ -+ // intersection radius is ticket update radius (1) + scheduling radius -+ return dist <= (1 + ((getMaxSchedulingRadius() + (SECTION_SIZE - 1)) >> SECTION_SHIFT)); -+ } -+ -+ /* section */ -+ -+ private final Section getSectionPlain() { -+ return (Section)SECTION_HANDLE.get(this); -+ } -+ -+ private final Section getSectionVolatile() { -+ return (Section)SECTION_HANDLE.getVolatile(this); -+ } -+ -+ private final void setSectionPlain(final Section update) { -+ SECTION_HANDLE.set(this, update); -+ } -+ -+ private final void setSectionOpaque(final Section update) { -+ SECTION_HANDLE.setOpaque(this, update); -+ } -+ -+ private final void setSectionVolatile(final Section update) { -+ SECTION_HANDLE.setVolatile(this, update); -+ } -+ -+ private final Section getAndSetSectionVolatile(final Section update) { -+ return (Section)SECTION_HANDLE.getAndSet(this, update); -+ } -+ -+ private final Section compareExchangeSectionVolatile(final Section expect, final Section update) { -+ return (Section)SECTION_HANDLE.compareAndExchange(this, expect, update); -+ } -+ -+ /* next */ -+ -+ private final UpdateQueueNode getNextPlain() { -+ return (UpdateQueueNode)NEXT_HANDLE.get(this); -+ } -+ -+ private final UpdateQueueNode getNextOpaque() { -+ return (UpdateQueueNode)NEXT_HANDLE.getOpaque(this); -+ } -+ -+ private final UpdateQueueNode getNextAcquire() { -+ return (UpdateQueueNode)NEXT_HANDLE.getAcquire(this); -+ } -+ -+ private final UpdateQueueNode getNextVolatile() { -+ return (UpdateQueueNode)NEXT_HANDLE.getVolatile(this); -+ } -+ -+ private final void setNextPlain(final UpdateQueueNode next) { -+ NEXT_HANDLE.set(this, next); -+ } -+ -+ private final void setNextVolatile(final UpdateQueueNode next) { -+ NEXT_HANDLE.setVolatile(this, next); -+ } -+ -+ private final UpdateQueueNode compareExchangeNextVolatile(final UpdateQueueNode expect, final UpdateQueueNode set) { -+ return (UpdateQueueNode)NEXT_HANDLE.compareAndExchange(this, expect, set); -+ } -+ -+ /* updating */ -+ -+ private final boolean getUpdatingVolatile() { -+ return (boolean)UPDATING_HANDLE.getVolatile(this); -+ } -+ -+ private final boolean getAndSetUpdatingVolatile(final boolean value) { -+ return (boolean)UPDATING_HANDLE.getAndSet(this, value); -+ } -+ } -+ } -+ -+ private static final class Section { -+ -+ // upper 8 bits: sources, lower 8 bits: level -+ // if we REALLY wanted to get crazy, we could make the increase propagator use MethodHandles#byteArrayViewVarHandle -+ // to read and write the lower 8 bits of this array directly rather than reading, updating the bits, then writing back. -+ private final short[] levels = new short[SECTION_SIZE * SECTION_SIZE]; -+ // set of local positions that represent sources -+ private final ShortOpenHashSet sources = new ShortOpenHashSet(); -+ // map of local index to new source level -+ // the source level _cannot_ be updated in the backing storage immediately since the update -+ private static final byte NO_QUEUED_UPDATE = (byte)-1; -+ private final Short2ByteLinkedOpenHashMap queuedSources = new Short2ByteLinkedOpenHashMap(); -+ { -+ this.queuedSources.defaultReturnValue(NO_QUEUED_UPDATE); -+ } -+ private int oneRadNeighboursWithSources = 0; -+ -+ public final int sectionX; -+ public final int sectionZ; -+ -+ public Section(final int sectionX, final int sectionZ) { -+ this.sectionX = sectionX; -+ this.sectionZ = sectionZ; -+ } -+ -+ public boolean isZero() { -+ for (final short val : this.levels) { -+ if (val != 0) { -+ return false; -+ } -+ } -+ return true; -+ } -+ -+ @Override -+ public String toString() { -+ final StringBuilder ret = new StringBuilder(); -+ -+ for (int x = 0; x < SECTION_SIZE; ++x) { -+ ret.append("levels x=").append(x).append("\n"); -+ for (int z = 0; z < SECTION_SIZE; ++z) { -+ final short v = this.levels[x | (z << SECTION_SHIFT)]; -+ ret.append(v & 0xFF).append("."); -+ } -+ ret.append("\n"); -+ ret.append("sources x=").append(x).append("\n"); -+ for (int z = 0; z < SECTION_SIZE; ++z) { -+ final short v = this.levels[x | (z << SECTION_SHIFT)]; -+ ret.append((v >>> 8) & 0xFF).append("."); -+ } -+ ret.append("\n\n"); -+ } -+ -+ return ret.toString(); -+ } -+ } -+ -+ -+ private static final class Propagator { -+ -+ private static final ArrayDeque<Propagator> CACHED_PROPAGATORS = new ArrayDeque<>(); -+ private static final int MAX_PROPAGATORS = Runtime.getRuntime().availableProcessors() * 2; -+ -+ private static Propagator acquirePropagator() { -+ synchronized (CACHED_PROPAGATORS) { -+ final Propagator ret = CACHED_PROPAGATORS.pollFirst(); -+ if (ret != null) { -+ return ret; -+ } -+ } -+ return new Propagator(); -+ } -+ -+ private static void returnPropagator(final Propagator propagator) { -+ synchronized (CACHED_PROPAGATORS) { -+ if (CACHED_PROPAGATORS.size() < MAX_PROPAGATORS) { -+ CACHED_PROPAGATORS.add(propagator); -+ } -+ } -+ } -+ -+ private static final int SECTION_RADIUS = 2; -+ private static final int SECTION_CACHE_WIDTH = 2 * SECTION_RADIUS + 1; -+ // minimum number of bits to represent [0, SECTION_SIZE * SECTION_CACHE_WIDTH) -+ private static final int COORDINATE_BITS = 9; -+ private static final int COORDINATE_SIZE = 1 << COORDINATE_BITS; -+ static { -+ if ((SECTION_SIZE * SECTION_CACHE_WIDTH) > (1 << COORDINATE_BITS)) { -+ throw new IllegalStateException("Adjust COORDINATE_BITS"); -+ } -+ } -+ // index = x + (z * SECTION_CACHE_WIDTH) -+ // (this requires x >= 0 and z >= 0) -+ private final Section[] sections = new Section[SECTION_CACHE_WIDTH * SECTION_CACHE_WIDTH]; -+ -+ private int encodeOffsetX; -+ private int encodeOffsetZ; -+ -+ private int coordinateOffset; -+ -+ private int encodeSectionOffsetX; -+ private int encodeSectionOffsetZ; -+ -+ private int sectionIndexOffset; -+ -+ public final boolean hasUpdates() { -+ return this.decreaseQueueInitialLength != 0 || this.increaseQueueInitialLength != 0; -+ } -+ -+ private final void setupEncodeOffset(final int centerSectionX, final int centerSectionZ) { -+ final int maxCoordinate = (SECTION_RADIUS * SECTION_SIZE - 1); -+ // must have that encoded >= 0 -+ // coordinates can range from [-maxCoordinate + centerSection*SECTION_SIZE, maxCoordinate + centerSection*SECTION_SIZE] -+ // we want a range of [0, maxCoordinate*2] -+ // so, 0 = -maxCoordinate + centerSection*SECTION_SIZE + offset -+ this.encodeOffsetX = maxCoordinate - (centerSectionX << SECTION_SHIFT); -+ this.encodeOffsetZ = maxCoordinate - (centerSectionZ << SECTION_SHIFT); -+ -+ // encoded coordinates range from [0, SECTION_SIZE * SECTION_CACHE_WIDTH) -+ // coordinate index = (x + encodeOffsetX) + ((z + encodeOffsetZ) << COORDINATE_BITS) -+ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << COORDINATE_BITS); -+ -+ // need encoded values to be >= 0 -+ // so, 0 = (-SECTION_RADIUS + centerSectionX) + encodeOffset -+ this.encodeSectionOffsetX = SECTION_RADIUS - centerSectionX; -+ this.encodeSectionOffsetZ = SECTION_RADIUS - centerSectionZ; -+ -+ // section index = (secX + encodeSectionOffsetX) + ((secZ + encodeSectionOffsetZ) * SECTION_CACHE_WIDTH) -+ this.sectionIndexOffset = this.encodeSectionOffsetX + (this.encodeSectionOffsetZ * SECTION_CACHE_WIDTH); -+ } -+ -+ // must hold ticket lock for (centerSectionX,centerSectionZ) in radius rad -+ // must call setupEncodeOffset -+ private final void setupCaches(final ThreadedTicketLevelPropagator propagator, -+ final int centerSectionX, final int centerSectionZ, -+ final int rad) { -+ for (int dz = -rad; dz <= rad; ++dz) { -+ for (int dx = -rad; dx <= rad; ++dx) { -+ final int sectionX = centerSectionX + dx; -+ final int sectionZ = centerSectionZ + dz; -+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); -+ final Section section = propagator.sections.get(coordinate); -+ -+ if (section == null) { -+ throw new IllegalStateException("Section at " + coordinate + " should not be null"); -+ } -+ -+ this.setSectionInCache(sectionX, sectionZ, section); -+ } -+ } -+ } -+ -+ private final void setSectionInCache(final int sectionX, final int sectionZ, final Section section) { -+ this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset] = section; -+ } -+ -+ private final Section getSection(final int sectionX, final int sectionZ) { -+ return this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset]; -+ } -+ -+ private final int getLevel(final int posX, final int posZ) { -+ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset]; -+ if (section != null) { -+ return (int)section.levels[(posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT)] & 0xFF; -+ } -+ -+ return 0; -+ } -+ -+ private final void setLevel(final int posX, final int posZ, final int to) { -+ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset]; -+ if (section != null) { -+ final int index = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); -+ final short level = section.levels[index]; -+ section.levels[index] = (short)((level & ~0xFF) | (to & 0xFF)); -+ this.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)to); -+ } -+ } -+ -+ private final void destroyCaches() { -+ Arrays.fill(this.sections, null); -+ } -+ -+ // contains: -+ // lower (COORDINATE_BITS(9) + COORDINATE_BITS(9) = 18) bits encoded position: (x | (z << COORDINATE_BITS)) -+ // next LEVEL_BITS (6) bits: propagated level [0, 63] -+ // propagation directions bitset (16 bits): -+ private static final long ALL_DIRECTIONS_BITSET = ( -+ // z = -1 -+ (1L << ((1 - 1) | ((1 - 1) << 2))) | -+ (1L << ((1 + 0) | ((1 - 1) << 2))) | -+ (1L << ((1 + 1) | ((1 - 1) << 2))) | -+ -+ // z = 0 -+ (1L << ((1 - 1) | ((1 + 0) << 2))) | -+ //(1L << ((1 + 0) | ((1 + 0) << 2))) | // exclude (0,0) -+ (1L << ((1 + 1) | ((1 + 0) << 2))) | -+ -+ // z = 1 -+ (1L << ((1 - 1) | ((1 + 1) << 2))) | -+ (1L << ((1 + 0) | ((1 + 1) << 2))) | -+ (1L << ((1 + 1) | ((1 + 1) << 2))) -+ ); -+ -+ private void ex(int bitset) { -+ for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) { -+ final int set = Integer.numberOfTrailingZeros(bitset); -+ final int tailingBit = (-bitset) & bitset; -+ // XOR to remove the trailing bit -+ bitset ^= tailingBit; -+ -+ // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits -+ // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the -+ // index of the set bit is the encoded value -+ // the encoded coordinate has 3 valid states: -+ // 0b00 (0) -> -1 -+ // 0b01 (1) -> 0 -+ // 0b10 (2) -> 1 -+ // the decode operation then is val - 1, and the encode operation is val + 1 -+ final int xOff = (set & 3) - 1; -+ final int zOff = ((set >>> 2) & 3) - 1; -+ System.out.println("Encoded: (" + xOff + "," + zOff + ")"); -+ } -+ } -+ -+ private void ch(long bs, int shift) { -+ int bitset = (int)(bs >>> shift); -+ for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) { -+ final int set = Integer.numberOfTrailingZeros(bitset); -+ final int tailingBit = (-bitset) & bitset; -+ // XOR to remove the trailing bit -+ bitset ^= tailingBit; -+ -+ // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits -+ // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the -+ // index of the set bit is the encoded value -+ // the encoded coordinate has 3 valid states: -+ // 0b00 (0) -> -1 -+ // 0b01 (1) -> 0 -+ // 0b10 (2) -> 1 -+ // the decode operation then is val - 1, and the encode operation is val + 1 -+ final int xOff = (set & 3) - 1; -+ final int zOff = ((set >>> 2) & 3) - 1; -+ if (Math.abs(xOff) > 1 || Math.abs(zOff) > 1 || (xOff | zOff) == 0) { -+ throw new IllegalStateException(); -+ } -+ } -+ } -+ -+ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading -+ // updates for sources -+ private static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 1; -+ // whether the propagation needs to check if its current level is equal to the expected level -+ // used only in increase propagation -+ private static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 0; -+ -+ private long[] increaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2]; -+ private int increaseQueueInitialLength; -+ private long[] decreaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2]; -+ private int decreaseQueueInitialLength; -+ -+ private final Long2ByteLinkedOpenHashMap updatedPositions = new Long2ByteLinkedOpenHashMap(); -+ -+ private final long[] resizeIncreaseQueue() { -+ return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2); -+ } -+ -+ private final long[] resizeDecreaseQueue() { -+ return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2); -+ } -+ -+ private final void appendToIncreaseQueue(final long value) { -+ final int idx = this.increaseQueueInitialLength++; -+ long[] queue = this.increaseQueue; -+ if (idx >= queue.length) { -+ queue = this.resizeIncreaseQueue(); -+ queue[idx] = value; -+ return; -+ } else { -+ queue[idx] = value; -+ return; -+ } -+ } -+ -+ private final void appendToDecreaseQueue(final long value) { -+ final int idx = this.decreaseQueueInitialLength++; -+ long[] queue = this.decreaseQueue; -+ if (idx >= queue.length) { -+ queue = this.resizeDecreaseQueue(); -+ queue[idx] = value; -+ return; -+ } else { -+ queue[idx] = value; -+ return; -+ } -+ } -+ -+ private final void performIncrease() { -+ long[] queue = this.increaseQueue; -+ int queueReadIndex = 0; -+ int queueLength = this.increaseQueueInitialLength; -+ this.increaseQueueInitialLength = 0; -+ final int decodeOffsetX = -this.encodeOffsetX; -+ final int decodeOffsetZ = -this.encodeOffsetZ; -+ final int encodeOffset = this.coordinateOffset; -+ final int sectionOffset = this.sectionIndexOffset; -+ -+ final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions; -+ -+ while (queueReadIndex < queueLength) { -+ final long queueValue = queue[queueReadIndex++]; -+ -+ final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX; -+ final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ; -+ final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1); -+ // note: the above code requires coordinate bits * 2 < 32 -+ // bitset is 16 bits -+ int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1); -+ -+ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) { -+ if (this.getLevel(posX, posZ) != propagatedLevel) { -+ // not at the level we expect, so something changed. -+ continue; -+ } -+ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) { -+ // these are used to restore sources after a propagation decrease -+ this.setLevel(posX, posZ, propagatedLevel); -+ } -+ -+ // this bitset represents the values that we have not propagated to -+ // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases -+ // significantly reducing the total number of ops -+ // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need -+ // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead -+ // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits) -+ // to make things easy, we use positions [0, 4] in the bitset, with current position being 2 -+ // index = x | (z << 3) -+ -+ // to start, we eliminate everything 1 radius from the current position as the previous propagator -+ // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius -+ // but the rest not propagated are already handled -+ long currentPropagation = ~( -+ // z = -1 -+ (1L << ((2 - 1) | ((2 - 1) << 3))) | -+ (1L << ((2 + 0) | ((2 - 1) << 3))) | -+ (1L << ((2 + 1) | ((2 - 1) << 3))) | -+ -+ // z = 0 -+ (1L << ((2 - 1) | ((2 + 0) << 3))) | -+ (1L << ((2 + 0) | ((2 + 0) << 3))) | -+ (1L << ((2 + 1) | ((2 + 0) << 3))) | -+ -+ // z = 1 -+ (1L << ((2 - 1) | ((2 + 1) << 3))) | -+ (1L << ((2 + 0) | ((2 + 1) << 3))) | -+ (1L << ((2 + 1) | ((2 + 1) << 3))) -+ ); -+ -+ final int toPropagate = propagatedLevel - 1; -+ -+ // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting -+ // the bits, the cpu loop predictor should perfectly predict the loop. -+ for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) { -+ final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset); -+ final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset; -+ propagateDirectionBitset ^= tailingBit; -+ -+ // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset -+ // it has been split to save some cycles via parallelism -+ final int pDecodeX = (set & 3); -+ final int pDecodeZ = ((set >>> 2) & 3); -+ -+ // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX -+ final int offX = (posX - 1) + pDecodeX; -+ final int offZ = (posZ - 1) + pDecodeZ; -+ -+ final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset; -+ final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); -+ -+ // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset -+ // bitset idx = x | (z << 3) -+ -+ // read three bits, so we need 7L -+ // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1 -+ // nstartidx1 = x rel -1 for z rel -1 -+ // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3) -+ // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3) -+ // = pDecodeX | (pDecodeZ << 3) = start -+ final int start = pDecodeX | (pDecodeZ << 3); -+ final long bitsetLine1 = currentPropagation & (7L << (start)); -+ -+ // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset) -+ final long bitsetLine2 = currentPropagation & (7L << (start + 8)); -+ -+ // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset) -+ final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8))); -+ -+ // remove ("take") lines from bitset -+ currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3); -+ -+ // now try to propagate -+ final Section section = this.sections[sectionIndex]; -+ -+ // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag -+ final short currentStoredLevel = section.levels[localIndex]; -+ final int currentLevel = currentStoredLevel & 0xFF; -+ -+ if (currentLevel >= toPropagate) { -+ continue; // already at the level we want -+ } -+ -+ // update level -+ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF) | (toPropagate & 0xFF)); -+ updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)toPropagate); -+ -+ // queue next -+ if (toPropagate > 1) { -+ // now combine into one bitset to pass to child -+ // the child bitset is 4x4, so we just shift each line by 4 -+ // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value -+ final long childPropagation = -+ ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1 -+ ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0 -+ ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1 -+ -+ // don't queue update if toPropagate cannot propagate anything to neighbours -+ // (for increase, propagating 0 to neighbours is useless) -+ if (queueLength >= queue.length) { -+ queue = this.resizeIncreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | -+ ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | -+ childPropagation; //(ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); -+ continue; -+ } -+ continue; -+ } -+ } -+ } -+ -+ private final void performDecrease() { -+ long[] queue = this.decreaseQueue; -+ long[] increaseQueue = this.increaseQueue; -+ int queueReadIndex = 0; -+ int queueLength = this.decreaseQueueInitialLength; -+ this.decreaseQueueInitialLength = 0; -+ int increaseQueueLength = this.increaseQueueInitialLength; -+ final int decodeOffsetX = -this.encodeOffsetX; -+ final int decodeOffsetZ = -this.encodeOffsetZ; -+ final int encodeOffset = this.coordinateOffset; -+ final int sectionOffset = this.sectionIndexOffset; -+ -+ final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions; -+ -+ while (queueReadIndex < queueLength) { -+ final long queueValue = queue[queueReadIndex++]; -+ -+ final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX; -+ final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ; -+ final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1); -+ // note: the above code requires coordinate bits * 2 < 32 -+ // bitset is 16 bits -+ int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1); -+ -+ // this bitset represents the values that we have not propagated to -+ // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases -+ // significantly reducing the total number of ops -+ // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need -+ // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead -+ // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits) -+ // to make things easy, we use positions [0, 4] in the bitset, with current position being 2 -+ // index = x | (z << 3) -+ -+ // to start, we eliminate everything 1 radius from the current position as the previous propagator -+ // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius -+ // but the rest not propagated are already handled -+ long currentPropagation = ~( -+ // z = -1 -+ (1L << ((2 - 1) | ((2 - 1) << 3))) | -+ (1L << ((2 + 0) | ((2 - 1) << 3))) | -+ (1L << ((2 + 1) | ((2 - 1) << 3))) | -+ -+ // z = 0 -+ (1L << ((2 - 1) | ((2 + 0) << 3))) | -+ (1L << ((2 + 0) | ((2 + 0) << 3))) | -+ (1L << ((2 + 1) | ((2 + 0) << 3))) | -+ -+ // z = 1 -+ (1L << ((2 - 1) | ((2 + 1) << 3))) | -+ (1L << ((2 + 0) | ((2 + 1) << 3))) | -+ (1L << ((2 + 1) | ((2 + 1) << 3))) -+ ); -+ -+ final int toPropagate = propagatedLevel - 1; -+ -+ // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting -+ // the bits, the cpu loop predictor should perfectly predict the loop. -+ for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) { -+ final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset); -+ final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset; -+ propagateDirectionBitset ^= tailingBit; -+ -+ -+ // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset -+ // it has been split to save some cycles via parallelism -+ final int pDecodeX = (set & 3); -+ final int pDecodeZ = ((set >>> 2) & 3); -+ -+ // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX -+ final int offX = (posX - 1) + pDecodeX; -+ final int offZ = (posZ - 1) + pDecodeZ; -+ -+ final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset; -+ final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); -+ -+ // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset -+ // bitset idx = x | (z << 3) -+ -+ // read three bits, so we need 7L -+ // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1 -+ // nstartidx1 = x rel -1 for z rel -1 -+ // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3) -+ // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3) -+ // = pDecodeX | (pDecodeZ << 3) = start -+ final int start = pDecodeX | (pDecodeZ << 3); -+ final long bitsetLine1 = currentPropagation & (7L << (start)); -+ -+ // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset) -+ final long bitsetLine2 = currentPropagation & (7L << (start + 8)); -+ -+ // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset) -+ final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8))); -+ -+ // now try to propagate -+ final Section section = this.sections[sectionIndex]; -+ -+ // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag -+ final short currentStoredLevel = section.levels[localIndex]; -+ final int currentLevel = currentStoredLevel & 0xFF; -+ final int sourceLevel = (currentStoredLevel >>> 8) & 0xFF; -+ -+ if (currentLevel == 0) { -+ continue; // already at the level we want -+ } -+ -+ if (currentLevel > toPropagate) { -+ // it looks like another source propagated here, so re-propagate it -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | -+ ((currentLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | -+ (FLAG_RECHECK_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS))); -+ continue; -+ } -+ -+ // remove ("take") lines from bitset -+ // can't do this during decrease, TODO WHY? -+ //currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3); -+ -+ // update level -+ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF)); -+ updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)0); -+ -+ if (sourceLevel != 0) { -+ // re-propagate source -+ // note: do not set recheck level, or else the propagation will fail -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | -+ ((sourceLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | -+ (FLAG_WRITE_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS))); -+ } -+ -+ // queue next -+ // note: targetLevel > 0 here, since toPropagate >= currentLevel and currentLevel > 0 -+ // now combine into one bitset to pass to child -+ // the child bitset is 4x4, so we just shift each line by 4 -+ // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value -+ final long childPropagation = -+ ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1 -+ ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0 -+ ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1 -+ -+ // don't queue update if toPropagate cannot propagate anything to neighbours -+ // (for increase, propagating 0 to neighbours is useless) -+ if (queueLength >= queue.length) { -+ queue = this.resizeDecreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | -+ ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | -+ (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); //childPropagation; -+ continue; -+ } -+ } -+ -+ // propagate sources we clobbered -+ this.increaseQueueInitialLength = increaseQueueLength; -+ this.performIncrease(); -+ } -+ } -+ -+ /* -+ private static final java.util.Random random = new java.util.Random(4L); -+ private static final List<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void>> walkers = -+ new java.util.ArrayList<>(); -+ static final int PLAYERS = 0; -+ static final int RAD_BLOCKS = 10000; -+ static final int RAD = RAD_BLOCKS >> 4; -+ static final int RAD_BIG_BLOCKS = 100_000; -+ static final int RAD_BIG = RAD_BIG_BLOCKS >> 4; -+ static final int VD = 4; -+ static final int BIG_PLAYERS = 50; -+ static final double WALK_CHANCE = 0.10; -+ static final double TP_CHANCE = 0.01; -+ static final int TP_BACK_PLAYERS = 200; -+ static final double TP_BACK_CHANCE = 0.25; -+ static final double TP_STEAL_CHANCE = 0.25; -+ private static final List<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void>> tpBack = -+ new java.util.ArrayList<>(); -+ -+ public static void main(final String[] args) { -+ final ReentrantAreaLock ticketLock = new ReentrantAreaLock(SECTION_SHIFT); -+ final ReentrantAreaLock schedulingLock = new ReentrantAreaLock(SECTION_SHIFT); -+ final Long2ByteLinkedOpenHashMap levelMap = new Long2ByteLinkedOpenHashMap(); -+ final Long2ByteLinkedOpenHashMap refMap = new Long2ByteLinkedOpenHashMap(); -+ final io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D ref = new io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D((final long coordinate, final byte oldLevel, final byte newLevel) -> { -+ if (newLevel == 0) { -+ refMap.remove(coordinate); -+ } else { -+ refMap.put(coordinate, newLevel); -+ } -+ }); -+ final ThreadedTicketLevelPropagator propagator = new ThreadedTicketLevelPropagator() { -+ @Override -+ protected void processLevelUpdates(Long2ByteLinkedOpenHashMap updates) { -+ for (final long key : updates.keySet()) { -+ final byte val = updates.get(key); -+ if (val == 0) { -+ levelMap.remove(key); -+ } else { -+ levelMap.put(key, val); -+ } -+ } -+ } -+ -+ @Override -+ protected void processSchedulingUpdates(Long2ByteLinkedOpenHashMap updates, List<ChunkProgressionTask> scheduledTasks, List<NewChunkHolder> changedFullStatus) {} -+ }; -+ -+ for (;;) { -+ if (walkers.isEmpty() && tpBack.isEmpty()) { -+ for (int i = 0; i < PLAYERS; ++i) { -+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; -+ int posX = random.nextInt(-rad, rad + 1); -+ int posZ = random.nextInt(-rad, rad + 1); -+ -+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { -+ @Override -+ protected void addCallback(Void parameter, int chunkX, int chunkZ) { -+ int src = 45 - 31 + 1; -+ ref.setSource(chunkX, chunkZ, src); -+ propagator.setSource(chunkX, chunkZ, src); -+ } -+ -+ @Override -+ protected void removeCallback(Void parameter, int chunkX, int chunkZ) { -+ ref.removeSource(chunkX, chunkZ); -+ propagator.removeSource(chunkX, chunkZ); -+ } -+ }; -+ -+ map.add(posX, posZ, VD); -+ -+ walkers.add(map); -+ } -+ for (int i = 0; i < TP_BACK_PLAYERS; ++i) { -+ int rad = RAD_BIG; -+ int posX = random.nextInt(-rad, rad + 1); -+ int posZ = random.nextInt(-rad, rad + 1); -+ -+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { -+ @Override -+ protected void addCallback(Void parameter, int chunkX, int chunkZ) { -+ int src = 45 - 31 + 1; -+ ref.setSource(chunkX, chunkZ, src); -+ propagator.setSource(chunkX, chunkZ, src); -+ } -+ -+ @Override -+ protected void removeCallback(Void parameter, int chunkX, int chunkZ) { -+ ref.removeSource(chunkX, chunkZ); -+ propagator.removeSource(chunkX, chunkZ); -+ } -+ }; -+ -+ map.add(posX, posZ, random.nextInt(1, 63)); -+ -+ tpBack.add(map); -+ } -+ } else { -+ for (int i = 0; i < PLAYERS; ++i) { -+ if (random.nextDouble() > WALK_CHANCE) { -+ continue; -+ } -+ -+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = walkers.get(i); -+ -+ int updateX = random.nextInt(-1, 2); -+ int updateZ = random.nextInt(-1, 2); -+ -+ map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD); -+ } -+ -+ for (int i = 0; i < PLAYERS; ++i) { -+ if (random.nextDouble() > TP_CHANCE) { -+ continue; -+ } -+ -+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; -+ int posX = random.nextInt(-rad, rad + 1); -+ int posZ = random.nextInt(-rad, rad + 1); -+ -+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = walkers.get(i); -+ -+ map.update(posX, posZ, VD); -+ } -+ -+ for (int i = 0; i < TP_BACK_PLAYERS; ++i) { -+ if (random.nextDouble() > TP_BACK_CHANCE) { -+ continue; -+ } -+ -+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = tpBack.get(i); -+ -+ map.update(-map.lastChunkX, -map.lastChunkZ, random.nextInt(1, 63)); -+ -+ if (random.nextDouble() > TP_STEAL_CHANCE) { -+ propagator.performUpdate( -+ map.lastChunkX >> SECTION_SHIFT, map.lastChunkZ >> SECTION_SHIFT, schedulingLock, null, null -+ ); -+ propagator.performUpdate( -+ (-map.lastChunkX >> SECTION_SHIFT), (-map.lastChunkZ >> SECTION_SHIFT), schedulingLock, null, null -+ ); -+ } -+ } -+ } -+ -+ ref.propagateUpdates(); -+ propagator.performUpdates(ticketLock, schedulingLock, null, null); -+ -+ if (!refMap.equals(levelMap)) { -+ throw new IllegalStateException("Error!"); -+ } -+ } -+ } -+ */ -+} -diff --git a/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<DependencyNode> DEPENDENCY_NODE_COMPARATOR = (final DependencyNode t1, final DependencyNode t2) -> { -+ return Long.compare(t1.id, t2.id); -+ }; -+ -+ private final PrioritisedExecutor executor; -+ private final DependencyTree[] queues = new DependencyTree[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; -+ private static final int NO_TASKS_QUEUED = -1; -+ private int selectedQueue = NO_TASKS_QUEUED; -+ private boolean canQueueTasks = true; -+ -+ public RadiusAwarePrioritisedExecutor(final PrioritisedExecutor executor, final int maxToSchedule) { -+ this.executor = executor; -+ -+ for (int i = 0; i < this.queues.length; ++i) { -+ this.queues[i] = new DependencyTree(this, executor, maxToSchedule); -+ } -+ } -+ -+ public void setMaxToSchedule(final int maxToSchedule) { -+ final List<PrioritisedExecutor.PrioritisedTask> tasks; -+ -+ synchronized (this) { -+ for (final DependencyTree dependencyTree : this.queues) { -+ dependencyTree.maxToSchedule = maxToSchedule; -+ } -+ -+ if (this.selectedQueue == NO_TASKS_QUEUED || !this.canQueueTasks) { -+ return; -+ } -+ -+ tasks = this.queues[this.selectedQueue].tryPushTasks(); -+ } -+ -+ scheduleTasks(tasks); -+ } -+ -+ private boolean canQueueTasks() { -+ return this.canQueueTasks; -+ } -+ -+ private List<PrioritisedExecutor.PrioritisedTask> treeFinished() { -+ this.canQueueTasks = true; -+ for (int priority = 0; priority < this.queues.length; ++priority) { -+ final DependencyTree queue = this.queues[priority]; -+ if (queue.hasWaitingTasks()) { -+ final List<PrioritisedExecutor.PrioritisedTask> ret = queue.tryPushTasks(); -+ -+ if (ret == null || ret.isEmpty()) { -+ // this happens when the tasks in the wait queue were purged -+ // in this case, the queue was actually empty, we just had to purge it -+ // if we set the selected queue without scheduling any tasks, the queue will never be unselected -+ // as that requires a scheduled task completing... -+ continue; -+ } -+ -+ this.selectedQueue = priority; -+ return ret; -+ } -+ } -+ -+ this.selectedQueue = NO_TASKS_QUEUED; -+ -+ return null; -+ } -+ -+ private List<PrioritisedExecutor.PrioritisedTask> queue(final Task task, final Priority priority) { -+ final int priorityId = priority.priority; -+ final DependencyTree queue = this.queues[priorityId]; -+ -+ final DependencyNode node = new DependencyNode(task, queue); -+ -+ if (task.dependencyNode != null) { -+ throw new IllegalStateException(); -+ } -+ task.dependencyNode = node; -+ -+ queue.pushNode(node); -+ -+ if (this.selectedQueue == NO_TASKS_QUEUED) { -+ this.canQueueTasks = true; -+ this.selectedQueue = priorityId; -+ return queue.tryPushTasks(); -+ } -+ -+ if (!this.canQueueTasks) { -+ return null; -+ } -+ -+ if (Priority.isHigherPriority(priorityId, this.selectedQueue)) { -+ // prevent the lower priority tree from queueing more tasks -+ this.canQueueTasks = false; -+ return null; -+ } -+ -+ // priorityId != selectedQueue: lower priority, don't care - treeFinished will pick it up -+ return priorityId == this.selectedQueue ? queue.tryPushTasks() : null; -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius, -+ final Runnable run, final Priority priority) { -+ if (radius < 0) { -+ throw new IllegalArgumentException("Radius must be > 0: " + radius); -+ } -+ return new Task(this, chunkX, chunkZ, radius, run, priority); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius, -+ final Runnable run) { -+ return this.createTask(chunkX, chunkZ, radius, run, Priority.NORMAL); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius, -+ final Runnable run, final Priority priority) { -+ final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run, priority); -+ -+ ret.queue(); -+ -+ return ret; -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius, -+ final Runnable run) { -+ final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run); -+ -+ ret.queue(); -+ -+ return ret; -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run, final Priority priority) { -+ return new Task(this, 0, 0, -1, run, priority); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run) { -+ return this.createInfiniteRadiusTask(run, Priority.NORMAL); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final Priority priority) { -+ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, priority); -+ -+ ret.queue(); -+ -+ return ret; -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run) { -+ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, Priority.NORMAL); -+ -+ ret.queue(); -+ -+ return ret; -+ } -+ -+ private static void scheduleTasks(final List<PrioritisedExecutor.PrioritisedTask> toSchedule) { -+ if (toSchedule != null) { -+ for (int i = 0, len = toSchedule.size(); i < len; ++i) { -+ toSchedule.get(i).queue(); -+ } -+ } -+ } -+ -+ // all accesses must be synchronised by the radius aware object -+ private static final class DependencyTree { -+ -+ private final RadiusAwarePrioritisedExecutor scheduler; -+ private final PrioritisedExecutor executor; -+ private int maxToSchedule; -+ -+ private int currentlyExecuting; -+ private long idGenerator; -+ -+ private final PriorityQueue<DependencyNode> awaiting = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); -+ -+ private final PriorityQueue<DependencyNode> infiniteRadius = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); -+ private boolean isInfiniteRadiusScheduled; -+ -+ private final Long2ReferenceOpenHashMap<DependencyNode> nodeByPosition = new Long2ReferenceOpenHashMap<>(); -+ -+ public DependencyTree(final RadiusAwarePrioritisedExecutor scheduler, final PrioritisedExecutor executor, -+ final int maxToSchedule) { -+ this.scheduler = scheduler; -+ this.executor = executor; -+ this.maxToSchedule = maxToSchedule; -+ } -+ -+ public boolean hasWaitingTasks() { -+ return !this.awaiting.isEmpty() || !this.infiniteRadius.isEmpty(); -+ } -+ -+ private long nextId() { -+ return this.idGenerator++; -+ } -+ -+ private boolean isExecutingAnyTasks() { -+ return this.currentlyExecuting != 0; -+ } -+ -+ private void pushNode(final DependencyNode node) { -+ if (!node.task.isFiniteRadius()) { -+ this.infiniteRadius.add(node); -+ return; -+ } -+ -+ // set up dependency for node -+ final Task task = node.task; -+ -+ final int centerX = task.chunkX; -+ final int centerZ = task.chunkZ; -+ final int radius = task.radius; -+ -+ final int minX = centerX - radius; -+ final int maxX = centerX + radius; -+ -+ final int minZ = centerZ - radius; -+ final int maxZ = centerZ + radius; -+ -+ ReferenceOpenHashSet<DependencyNode> parents = null; -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final DependencyNode dependency = this.nodeByPosition.put(CoordinateUtils.getChunkKey(currX, currZ), node); -+ if (dependency != null) { -+ if (parents == null) { -+ parents = new ReferenceOpenHashSet<>(); -+ } -+ if (parents.add(dependency)) { -+ // added a dependency, so we need to add as a child to the dependency -+ if (dependency.children == null) { -+ dependency.children = new ArrayList<>(); -+ } -+ dependency.children.add(node); -+ } -+ } -+ } -+ } -+ -+ if (parents == null) { -+ // no dependencies, add straight to awaiting -+ this.awaiting.add(node); -+ } else { -+ node.parents = parents.size(); -+ // we will be added to awaiting once we have no parents -+ } -+ } -+ -+ // called only when a node is returned after being executed -+ private List<PrioritisedExecutor.PrioritisedTask> returnNode(final DependencyNode node) { -+ final Task task = node.task; -+ -+ // now that the task is completed, we can push its children to the awaiting queue -+ this.pushChildren(node); -+ -+ if (task.isFiniteRadius()) { -+ // remove from dependency map -+ this.removeNodeFromMap(node); -+ } else { -+ // mark as no longer executing infinite radius -+ if (!this.isInfiniteRadiusScheduled) { -+ throw new IllegalStateException(); -+ } -+ this.isInfiniteRadiusScheduled = false; -+ } -+ -+ // decrement executing count, we are done executing this task -+ --this.currentlyExecuting; -+ -+ if (this.currentlyExecuting == 0) { -+ return this.scheduler.treeFinished(); -+ } -+ -+ return this.scheduler.canQueueTasks() ? this.tryPushTasks() : null; -+ } -+ -+ private List<PrioritisedExecutor.PrioritisedTask> tryPushTasks() { -+ // tasks are not queued, but only created here - we do hold the lock for the map -+ List<PrioritisedExecutor.PrioritisedTask> ret = null; -+ PrioritisedExecutor.PrioritisedTask pushedTask; -+ while ((pushedTask = this.tryPushTask()) != null) { -+ if (ret == null) { -+ ret = new ArrayList<>(); -+ } -+ ret.add(pushedTask); -+ } -+ -+ return ret; -+ } -+ -+ private void removeNodeFromMap(final DependencyNode node) { -+ final Task task = node.task; -+ -+ final int centerX = task.chunkX; -+ final int centerZ = task.chunkZ; -+ final int radius = task.radius; -+ -+ final int minX = centerX - radius; -+ final int maxX = centerX + radius; -+ -+ final int minZ = centerZ - radius; -+ final int maxZ = centerZ + radius; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ this.nodeByPosition.remove(CoordinateUtils.getChunkKey(currX, currZ), node); -+ } -+ } -+ } -+ -+ private void pushChildren(final DependencyNode node) { -+ // add all the children that we can into awaiting -+ final List<DependencyNode> children = node.children; -+ if (children != null) { -+ for (int i = 0, len = children.size(); i < len; ++i) { -+ final DependencyNode child = children.get(i); -+ int newParents = --child.parents; -+ if (newParents == 0) { -+ // no more dependents, we can push to awaiting -+ // even if the child is purged, we need to push it so that its children will be pushed -+ this.awaiting.add(child); -+ } else if (newParents < 0) { -+ throw new IllegalStateException(); -+ } -+ } -+ } -+ } -+ -+ private DependencyNode pollAwaiting() { -+ final DependencyNode ret = this.awaiting.poll(); -+ if (ret == null) { -+ return ret; -+ } -+ -+ if (ret.parents != 0) { -+ throw new IllegalStateException(); -+ } -+ -+ if (ret.purged) { -+ // need to manually remove from state here -+ this.pushChildren(ret); -+ this.removeNodeFromMap(ret); -+ } // else: delay children push until the task has finished -+ -+ return ret; -+ } -+ -+ private DependencyNode pollInfinite() { -+ return this.infiniteRadius.poll(); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask tryPushTask() { -+ if (this.currentlyExecuting >= this.maxToSchedule || this.isInfiniteRadiusScheduled) { -+ return null; -+ } -+ -+ DependencyNode firstInfinite; -+ while ((firstInfinite = this.infiniteRadius.peek()) != null && firstInfinite.purged) { -+ this.pollInfinite(); -+ } -+ -+ DependencyNode firstAwaiting; -+ while ((firstAwaiting = this.awaiting.peek()) != null && firstAwaiting.purged) { -+ this.pollAwaiting(); -+ } -+ -+ if (firstInfinite == null && firstAwaiting == null) { -+ return null; -+ } -+ -+ // firstAwaiting compared to firstInfinite -+ final int compare; -+ -+ if (firstAwaiting == null) { -+ // we choose first infinite, or infinite < awaiting -+ compare = 1; -+ } else if (firstInfinite == null) { -+ // we choose first awaiting, or awaiting < infinite -+ compare = -1; -+ } else { -+ compare = DEPENDENCY_NODE_COMPARATOR.compare(firstAwaiting, firstInfinite); -+ } -+ -+ if (compare >= 0) { -+ if (this.currentlyExecuting != 0) { -+ // don't queue infinite task while other tasks are executing in parallel -+ return null; -+ } -+ ++this.currentlyExecuting; -+ this.pollInfinite(); -+ this.isInfiniteRadiusScheduled = true; -+ return firstInfinite.task.pushTask(this.executor); -+ } else { -+ ++this.currentlyExecuting; -+ this.pollAwaiting(); -+ return firstAwaiting.task.pushTask(this.executor); -+ } -+ } -+ } -+ -+ private static final class DependencyNode { -+ -+ private final Task task; -+ private final DependencyTree tree; -+ -+ // dependency tree fields -+ // (must hold lock on the scheduler to use) -+ // null is the same as empty, we just use it so that we don't allocate the set unless we need to -+ private List<DependencyNode> children; -+ // 0 indicates that this task is considered "awaiting" -+ private int parents; -+ // false -> scheduled and not cancelled -+ // true -> scheduled but cancelled -+ private boolean purged; -+ private final long id; -+ -+ public DependencyNode(final Task task, final DependencyTree tree) { -+ this.task = task; -+ this.id = tree.nextId(); -+ this.tree = tree; -+ } -+ } -+ -+ private static final class Task implements PrioritisedExecutor.PrioritisedTask, Runnable { -+ -+ // task specific fields -+ private final RadiusAwarePrioritisedExecutor scheduler; -+ private final int chunkX; -+ private final int chunkZ; -+ private final int radius; -+ private Runnable run; -+ private Priority priority; -+ -+ private DependencyNode dependencyNode; -+ private PrioritisedExecutor.PrioritisedTask queuedTask; -+ -+ private Task(final RadiusAwarePrioritisedExecutor scheduler, final int chunkX, final int chunkZ, final int radius, -+ final Runnable run, final Priority priority) { -+ this.scheduler = scheduler; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.radius = radius; -+ this.run = run; -+ this.priority = priority; -+ } -+ -+ private boolean isFiniteRadius() { -+ return this.radius >= 0; -+ } -+ -+ private PrioritisedExecutor.PrioritisedTask pushTask(final PrioritisedExecutor executor) { -+ return this.queuedTask = executor.createTask(this, this.priority); -+ } -+ -+ private void executeTask() { -+ final Runnable run = this.run; -+ this.run = null; -+ run.run(); -+ } -+ -+ private void returnNode() { -+ final List<PrioritisedExecutor.PrioritisedTask> toSchedule; -+ synchronized (this.scheduler) { -+ final DependencyNode node = this.dependencyNode; -+ this.dependencyNode = null; -+ toSchedule = node.tree.returnNode(node); -+ } -+ -+ scheduleTasks(toSchedule); -+ } -+ -+ @Override -+ public PrioritisedExecutor getExecutor() { -+ return this.scheduler.executor; -+ } -+ -+ @Override -+ public void run() { -+ final Runnable run = this.run; -+ this.run = null; -+ try { -+ run.run(); -+ } finally { -+ this.returnNode(); -+ } -+ } -+ -+ @Override -+ public boolean queue() { -+ final List<PrioritisedExecutor.PrioritisedTask> toSchedule; -+ synchronized (this.scheduler) { -+ if (this.queuedTask != null || this.dependencyNode != null || this.priority == Priority.COMPLETING) { -+ return false; -+ } -+ -+ toSchedule = this.scheduler.queue(this, this.priority); -+ } -+ -+ scheduleTasks(toSchedule); -+ return true; -+ } -+ -+ @Override -+ public boolean isQueued() { -+ synchronized (this.scheduler) { -+ return (this.queuedTask != null || this.dependencyNode != null) && this.priority != Priority.COMPLETING; -+ } -+ } -+ -+ @Override -+ public boolean cancel() { -+ final PrioritisedExecutor.PrioritisedTask task; -+ synchronized (this.scheduler) { -+ if ((task = this.queuedTask) == null) { -+ if (this.priority == Priority.COMPLETING) { -+ return false; -+ } -+ -+ this.priority = Priority.COMPLETING; -+ if (this.dependencyNode != null) { -+ this.dependencyNode.purged = true; -+ this.dependencyNode = null; -+ } -+ -+ return true; -+ } -+ } -+ -+ if (task.cancel()) { -+ // must manually return the node -+ this.run = null; -+ this.returnNode(); -+ return true; -+ } -+ return false; -+ } -+ -+ @Override -+ public boolean execute() { -+ final PrioritisedExecutor.PrioritisedTask task; -+ synchronized (this.scheduler) { -+ if ((task = this.queuedTask) == null) { -+ if (this.priority == Priority.COMPLETING) { -+ return false; -+ } -+ -+ this.priority = Priority.COMPLETING; -+ if (this.dependencyNode != null) { -+ this.dependencyNode.purged = true; -+ this.dependencyNode = null; -+ } -+ // fall through to execution logic -+ } -+ } -+ -+ if (task != null) { -+ // will run the return node logic automatically -+ return task.execute(); -+ } else { -+ // don't run node removal/insertion logic, we aren't actually removed from the dependency tree -+ this.executeTask(); -+ return true; -+ } -+ } -+ -+ @Override -+ public Priority getPriority() { -+ final PrioritisedExecutor.PrioritisedTask task; -+ synchronized (this.scheduler) { -+ if ((task = this.queuedTask) == null) { -+ return this.priority; -+ } -+ } -+ -+ return task.getPriority(); -+ } -+ -+ @Override -+ public boolean setPriority(final Priority priority) { -+ if (!Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ -+ final PrioritisedExecutor.PrioritisedTask task; -+ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null; -+ synchronized (this.scheduler) { -+ if ((task = this.queuedTask) == null) { -+ if (this.priority == Priority.COMPLETING) { -+ return false; -+ } -+ -+ if (this.priority == priority) { -+ return true; -+ } -+ -+ this.priority = priority; -+ if (this.dependencyNode != null) { -+ // need to re-insert node -+ this.dependencyNode.purged = true; -+ this.dependencyNode = null; -+ toSchedule = this.scheduler.queue(this, priority); -+ } -+ } -+ } -+ -+ if (task != null) { -+ return task.setPriority(priority); -+ } -+ -+ scheduleTasks(toSchedule); -+ -+ return true; -+ } -+ -+ @Override -+ public boolean raisePriority(final Priority priority) { -+ if (!Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ -+ final PrioritisedExecutor.PrioritisedTask task; -+ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null; -+ synchronized (this.scheduler) { -+ if ((task = this.queuedTask) == null) { -+ if (this.priority == Priority.COMPLETING) { -+ return false; -+ } -+ -+ if (this.priority.isHigherOrEqualPriority(priority)) { -+ return true; -+ } -+ -+ this.priority = priority; -+ if (this.dependencyNode != null) { -+ // need to re-insert node -+ this.dependencyNode.purged = true; -+ this.dependencyNode = null; -+ toSchedule = this.scheduler.queue(this, priority); -+ } -+ } -+ } -+ -+ if (task != null) { -+ return task.raisePriority(priority); -+ } -+ -+ scheduleTasks(toSchedule); -+ -+ return true; -+ } -+ -+ @Override -+ public boolean lowerPriority(final Priority priority) { -+ if (!Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ -+ final PrioritisedExecutor.PrioritisedTask task; -+ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null; -+ synchronized (this.scheduler) { -+ if ((task = this.queuedTask) == null) { -+ if (this.priority == Priority.COMPLETING) { -+ return false; -+ } -+ -+ if (this.priority.isLowerOrEqualPriority(priority)) { -+ return true; -+ } -+ -+ this.priority = priority; -+ if (this.dependencyNode != null) { -+ // need to re-insert node -+ this.dependencyNode.purged = true; -+ this.dependencyNode = null; -+ toSchedule = this.scheduler.queue(this, priority); -+ } -+ } -+ } -+ -+ if (task != null) { -+ return task.lowerPriority(priority); -+ } -+ -+ scheduleTasks(toSchedule); -+ -+ return true; -+ } -+ -+ @Override -+ public long getSubOrder() { -+ // TODO implement -+ return 0; -+ } -+ -+ @Override -+ public boolean setSubOrder(final long subOrder) { -+ // TODO implement -+ return false; -+ } -+ -+ @Override -+ public boolean raiseSubOrder(final long subOrder) { -+ // TODO implement -+ return false; -+ } -+ -+ @Override -+ public boolean lowerSubOrder(final long subOrder) { -+ // TODO implement -+ return false; -+ } -+ -+ @Override -+ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) { -+ // TODO implement -+ return this.setPriority(priority); -+ } -+ } -+} -diff --git a/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<ChunkAccess, Throwable> loadResult; -+ private final AtomicInteger taskCountToComplete = new AtomicInteger(3); // one for poi, one for entity, and one for chunk data -+ -+ public ChunkLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, -+ final NewChunkHolder chunkHolder, final Priority priority) { -+ super(scheduler, world, chunkX, chunkZ); -+ this.chunkHolder = chunkHolder; -+ this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority); -+ this.loadTask.addCallback((final GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> result) -> { -+ ChunkLoadTask.this.loadResult = result; // must be before getAndDecrement -+ ChunkLoadTask.this.tryCompleteLoad(); -+ }); -+ } -+ -+ private void tryCompleteLoad() { -+ final int count = this.taskCountToComplete.decrementAndGet(); -+ if (count == 0) { -+ final GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> result = this.cancelled ? null : this.loadResult; // only after the getAndDecrement -+ ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right()); -+ } else if (count < 0) { -+ throw new IllegalStateException("Called tryCompleteLoad() too many times"); -+ } -+ } -+ -+ @Override -+ public ChunkStatus getTargetStatus() { -+ return ChunkStatus.EMPTY; -+ } -+ -+ private boolean scheduled; -+ -+ @Override -+ public boolean isScheduled() { -+ return this.scheduled; -+ } -+ -+ @Override -+ public void schedule() { -+ final NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; -+ final NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask; -+ -+ final Consumer<GenericDataLoadTask.TaskResult<?, ?>> scheduleLoadTask = (final GenericDataLoadTask.TaskResult<?, ?> result) -> { -+ ChunkLoadTask.this.tryCompleteLoad(); -+ }; -+ -+ // NOTE: it is IMPOSSIBLE for getOrLoadEntityData/getOrLoadPoiData to complete synchronously, because -+ // they must schedule a task to off main or to on main to complete -+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ if (this.scheduled) { -+ throw new IllegalStateException("schedule() called twice"); -+ } -+ this.scheduled = true; -+ if (this.cancelled) { -+ return; -+ } -+ if (!this.chunkHolder.isEntityChunkNBTLoaded()) { -+ entityLoadTask = this.chunkHolder.getOrLoadEntityData((Consumer)scheduleLoadTask); -+ } else { -+ entityLoadTask = null; -+ this.tryCompleteLoad(); -+ } -+ -+ if (!this.chunkHolder.isPoiChunkLoaded()) { -+ poiLoadTask = this.chunkHolder.getOrLoadPoiData((Consumer)scheduleLoadTask); -+ } else { -+ poiLoadTask = null; -+ this.tryCompleteLoad(); -+ } -+ -+ this.entityLoadTask = entityLoadTask; -+ this.poiLoadTask = poiLoadTask; -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ -+ if (entityLoadTask != null) { -+ entityLoadTask.schedule(); -+ } -+ -+ if (poiLoadTask != null) { -+ poiLoadTask.schedule(); -+ } -+ -+ this.loadTask.schedule(false); -+ } -+ -+ @Override -+ public void cancel() { -+ // must be before load task access, so we can synchronise with the writes to the fields -+ final boolean scheduled; -+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ // must read field here, as it may be written later conucrrently - -+ // we need to know if we scheduled _before_ cancellation -+ scheduled = this.scheduled; -+ this.cancelled = true; -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ -+ /* -+ Note: The entityLoadTask/poiLoadTask do not complete when cancelled, -+ so we need to manually try to complete in those cases -+ It is also important to note that we set the cancelled field first, just in case -+ the chunk load task attempts to complete with a non-null value -+ */ -+ -+ if (scheduled) { -+ // since we scheduled, we need to cancel the tasks -+ if (this.entityLoadTask != null) { -+ if (this.entityLoadTask.cancel()) { -+ this.tryCompleteLoad(); -+ } -+ } -+ if (this.poiLoadTask != null) { -+ if (this.poiLoadTask.cancel()) { -+ this.tryCompleteLoad(); -+ } -+ } -+ } else { -+ // since nothing was scheduled, we need to decrement the task count here ourselves -+ -+ // for entity load task -+ this.tryCompleteLoad(); -+ -+ // for poi load task -+ this.tryCompleteLoad(); -+ } -+ this.loadTask.cancel(); -+ } -+ -+ @Override -+ public Priority getPriority() { -+ return this.loadTask.getPriority(); -+ } -+ -+ @Override -+ public void lowerPriority(final Priority priority) { -+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); -+ if (entityLoad != null) { -+ entityLoad.lowerPriority(priority); -+ } -+ -+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); -+ -+ if (poiLoad != null) { -+ poiLoad.lowerPriority(priority); -+ } -+ -+ this.loadTask.lowerPriority(priority); -+ } -+ -+ @Override -+ public void setPriority(final Priority priority) { -+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); -+ if (entityLoad != null) { -+ entityLoad.setPriority(priority); -+ } -+ -+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); -+ -+ if (poiLoad != null) { -+ poiLoad.setPriority(priority); -+ } -+ -+ this.loadTask.setPriority(priority); -+ } -+ -+ @Override -+ public void raisePriority(final Priority priority) { -+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); -+ if (entityLoad != null) { -+ entityLoad.raisePriority(priority); -+ } -+ -+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); -+ -+ if (poiLoad != null) { -+ poiLoad.raisePriority(priority); -+ } -+ -+ this.loadTask.raisePriority(priority); -+ } -+ -+ protected static abstract class CallbackDataLoadTask<OnMain,FinalCompletion> extends GenericDataLoadTask<OnMain,FinalCompletion> { -+ -+ private TaskResult<FinalCompletion, Throwable> result; -+ private final MultiThreadedQueue<Consumer<TaskResult<FinalCompletion, Throwable>>> waiters = new MultiThreadedQueue<>(); -+ -+ protected volatile boolean completed; -+ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(CallbackDataLoadTask.class, "completed", boolean.class); -+ -+ protected CallbackDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, -+ final int chunkZ, final MoonriseRegionFileIO.RegionFileType type, -+ final Priority priority) { -+ super(scheduler, world, chunkX, chunkZ, type, priority); -+ } -+ -+ public void addCallback(final Consumer<TaskResult<FinalCompletion, Throwable>> consumer) { -+ if (!this.waiters.add(consumer)) { -+ try { -+ consumer.accept(this.result); -+ } catch (final Throwable throwable) { -+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( -+ "Consumer", ChunkTaskScheduler.stringIfNull(consumer), -+ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.result.right()), -+ "CallbackDataLoadTask impl", this.getClass().getName() -+ ), throwable); -+ } -+ } -+ } -+ -+ @Override -+ protected void onComplete(final TaskResult<FinalCompletion, Throwable> result) { -+ if ((boolean)COMPLETED_HANDLE.getAndSet((CallbackDataLoadTask)this, (boolean)true)) { -+ throw new IllegalStateException("Already completed"); -+ } -+ this.result = result; -+ Consumer<TaskResult<FinalCompletion, Throwable>> consumer; -+ while ((consumer = this.waiters.pollOrBlockAdds()) != null) { -+ try { -+ consumer.accept(result); -+ } catch (final Throwable throwable) { -+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( -+ "Consumer", ChunkTaskScheduler.stringIfNull(consumer), -+ "Completed throwable", ChunkTaskScheduler.stringIfNull(result.right()), -+ "CallbackDataLoadTask impl", this.getClass().getName() -+ ), throwable); -+ return; -+ } -+ } -+ } -+ } -+ -+ -+ private static record ReadChunk(ProtoChunk protoChunk, SerializableChunkData chunkData) {} -+ -+ private static final class ChunkDataLoadTask extends CallbackDataLoadTask<ReadChunk, ChunkAccess> { -+ private ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, -+ final int chunkZ, final Priority priority) { -+ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, priority); -+ } -+ -+ @Override -+ protected boolean hasOffMain() { -+ return true; -+ } -+ -+ @Override -+ protected boolean hasOnMain() { -+ return true; -+ } -+ -+ @Override -+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority) { -+ return this.scheduler.loadExecutor.createTask(run, priority); -+ } -+ -+ @Override -+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) { -+ return this.scheduler.createChunkTask(this.chunkX, this.chunkZ, run, priority); -+ } -+ -+ @Override -+ protected TaskResult<ChunkAccess, Throwable> completeOnMainOffMain(final ReadChunk data, final Throwable throwable) { -+ if (throwable != null) { -+ return new TaskResult<>(null, throwable); -+ } -+ -+ if (data == null || data.protoChunk() == null) { -+ return new TaskResult<>(this.getEmptyChunk(), null); -+ } -+ -+ if (!PlatformHooks.get().hasMainChunkLoadHook()) { -+ return new TaskResult<>(data.protoChunk(), null); -+ } -+ -+ // need to invoke the callback for loading on the main thread -+ return null; -+ } -+ -+ private ProtoChunk getEmptyChunk() { -+ return new ProtoChunk( -+ new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, this.world, -+ this.world.registryAccess().lookupOrThrow(Registries.BIOME), (BlendingData)null -+ ); -+ } -+ -+ @Override -+ protected TaskResult<ReadChunk, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) { -+ if (throwable != null) { -+ LOGGER.error("Failed to load chunk data for task: " + this.toString() + ", chunk data will be lost", throwable); -+ return new TaskResult<>(null, null); -+ } -+ -+ if (data == null) { -+ return new TaskResult<>(null, null); -+ } -+ -+ try { -+ // run converters -+ final CompoundTag converted = this.world.getChunkSource().chunkMap.upgradeChunkTag(data, new ChunkPos(this.chunkX, this.chunkZ)); // Paper -+ -+ // unpack the data -+ final SerializableChunkData chunkData = SerializableChunkData.parse( -+ this.world, this.world.registryAccess(), converted -+ ); -+ -+ if (chunkData == null) { -+ LOGGER.error("Deserialized chunk for task: " + this.toString() + " produced null, chunk data will be lost?"); -+ } -+ -+ // read into ProtoChunk -+ final ProtoChunk chunk = chunkData == null ? null : chunkData.read( -+ this.world, this.world.getPoiManager(), this.world.getChunkSource().chunkMap.storageInfo(), -+ new ChunkPos(this.chunkX, this.chunkZ) -+ ); -+ -+ return new TaskResult<>(new ReadChunk(chunk, chunkData), null); -+ } catch (final Throwable thr2) { -+ LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2); -+ return new TaskResult<>(null, null); -+ } -+ } -+ -+ @Override -+ protected TaskResult<ChunkAccess, Throwable> runOnMain(final ReadChunk data, final Throwable throwable) { -+ PlatformHooks.get().mainChunkLoad(data.protoChunk(), data.chunkData()); -+ -+ return new TaskResult<>(data.protoChunk(), null); -+ } -+ } -+ -+ public static final class PoiDataLoadTask extends CallbackDataLoadTask<PoiChunk, PoiChunk> { -+ -+ public PoiDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, -+ final int chunkZ, final Priority priority) { -+ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.POI_DATA, priority); -+ } -+ -+ @Override -+ protected boolean hasOffMain() { -+ return true; -+ } -+ -+ @Override -+ protected boolean hasOnMain() { -+ return false; -+ } -+ -+ @Override -+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority) { -+ return this.scheduler.loadExecutor.createTask(run, priority); -+ } -+ -+ @Override -+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ protected TaskResult<PoiChunk, Throwable> completeOnMainOffMain(final PoiChunk data, final Throwable throwable) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ protected TaskResult<PoiChunk, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) { -+ if (throwable != null) { -+ LOGGER.error("Failed to load poi data for task: " + this.toString() + ", poi data will be lost", throwable); -+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); -+ } -+ -+ if (data == null || data.isEmpty()) { -+ // nothing to do -+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); -+ } -+ -+ try { -+ // run converters -+ final CompoundTag converted = ChunkSystemConverters.convertPoiCompoundTag(data, this.world); -+ -+ // now we need to parse it -+ return new TaskResult<>(PoiChunk.parse(this.world, this.chunkX, this.chunkZ, converted), null); -+ } catch (final Throwable thr2) { -+ LOGGER.error("Failed to run parse poi data for task: " + this.toString() + ", poi data will be lost", thr2); -+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); -+ } -+ } -+ -+ @Override -+ protected TaskResult<PoiChunk, Throwable> runOnMain(final PoiChunk data, final Throwable throwable) { -+ throw new UnsupportedOperationException(); -+ } -+ } -+ -+ public static final class EntityDataLoadTask extends CallbackDataLoadTask<CompoundTag, CompoundTag> { -+ -+ public EntityDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, -+ final int chunkZ, final Priority priority) { -+ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, priority); -+ } -+ -+ @Override -+ protected boolean hasOffMain() { -+ return true; -+ } -+ -+ @Override -+ protected boolean hasOnMain() { -+ return false; -+ } -+ -+ @Override -+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority) { -+ return this.scheduler.loadExecutor.createTask(run, priority); -+ } -+ -+ @Override -+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ protected TaskResult<CompoundTag, Throwable> completeOnMainOffMain(final CompoundTag data, final Throwable throwable) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ protected TaskResult<CompoundTag, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) { -+ if (throwable != null) { -+ LOGGER.error("Failed to load entity data for task: " + this.toString() + ", entity data will be lost", throwable); -+ return new TaskResult<>(null, null); -+ } -+ -+ if (data == null || data.isEmpty()) { -+ // nothing to do -+ return new TaskResult<>(null, null); -+ } -+ -+ try { -+ return new TaskResult<>(ChunkSystemConverters.convertEntityChunkCompoundTag(data, this.world), null); -+ } catch (final Throwable thr2) { -+ LOGGER.error("Failed to run converters for entity data for task: " + this.toString() + ", entity data will be lost", thr2); -+ return new TaskResult<>(null, thr2); -+ } -+ } -+ -+ @Override -+ protected TaskResult<CompoundTag, Throwable> runOnMain(final CompoundTag data, final Throwable throwable) { -+ throw new UnsupportedOperationException(); -+ } -+ } -+} -diff --git a/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<BiConsumer<ChunkAccess, Throwable>> waiters = new MultiThreadedQueue<>(); -+ private ChunkAccess completedChunk; -+ private Throwable completedThrowable; -+ -+ protected final ChunkTaskScheduler scheduler; -+ protected final ServerLevel world; -+ protected final int chunkX; -+ protected final int chunkZ; -+ -+ protected volatile boolean completed; -+ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(ChunkProgressionTask.class, "completed", boolean.class); -+ -+ protected ChunkProgressionTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ) { -+ this.scheduler = scheduler; -+ this.world = world; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ } -+ -+ // Used only for debug json -+ public abstract boolean isScheduled(); -+ -+ // Note: It is the responsibility of the task to set the chunk's status once it has completed -+ public abstract ChunkStatus getTargetStatus(); -+ -+ /* Only executed once */ -+ /* Implementations must be prepared to handle cases where cancel() is called before schedule() */ -+ public abstract void schedule(); -+ -+ /* May be called multiple times */ -+ public abstract void cancel(); -+ -+ public abstract Priority getPriority(); -+ -+ /* Schedule lock is always held for the priority update calls */ -+ -+ public abstract void lowerPriority(final Priority priority); -+ -+ public abstract void setPriority(final Priority priority); -+ -+ public abstract void raisePriority(final Priority priority); -+ -+ public final void onComplete(final BiConsumer<ChunkAccess, Throwable> onComplete) { -+ if (!this.waiters.add(onComplete)) { -+ try { -+ onComplete.accept(this.completedChunk, this.completedThrowable); -+ } catch (final Throwable throwable) { -+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( -+ "Consumer", ChunkTaskScheduler.stringIfNull(onComplete), -+ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.completedThrowable) -+ ), throwable); -+ } -+ } -+ } -+ -+ protected final void complete(final ChunkAccess chunk, final Throwable throwable) { -+ try { -+ this.complete0(chunk, throwable); -+ } catch (final Throwable thr2) { -+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( -+ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable) -+ ), thr2); -+ } -+ } -+ -+ private void complete0(final ChunkAccess chunk, final Throwable throwable) { -+ if ((boolean)COMPLETED_HANDLE.getAndSet((ChunkProgressionTask)this, (boolean)true)) { -+ throw new IllegalStateException("Already completed"); -+ } -+ this.completedChunk = chunk; -+ this.completedThrowable = throwable; -+ -+ BiConsumer<ChunkAccess, Throwable> consumer; -+ while ((consumer = this.waiters.pollOrBlockAdds()) != null) { -+ consumer.accept(chunk, throwable); -+ } -+ } -+ -+ @Override -+ public String toString() { -+ return "ChunkProgressionTask{class: " + this.getClass().getName() + ", for world: " + WorldUtil.getWorldName(this.world) + -+ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() + -+ ", status: " + this.getTargetStatus().toString() + ", scheduled: " + this.isScheduled() + "}"; -+ } -+} -\ No newline at end of file -diff --git a/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<GenerationChunkHolder> neighbours; -+ -+ private final PrioritisedExecutor.PrioritisedTask generateTask; -+ -+ public ChunkUpgradeGenericStatusTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, -+ final int chunkZ, final ChunkAccess chunk, final StaticCache2D<GenerationChunkHolder> neighbours, -+ final ChunkStatus toStatus, final Priority priority) { -+ super(scheduler, world, chunkX, chunkZ); -+ if (!Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ this.fromChunk = chunk; -+ this.fromStatus = chunk.getPersistedStatus(); -+ this.toStatus = toStatus; -+ this.neighbours = neighbours; -+ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isParallelCapable()) { -+ this.generateTask = this.scheduler.parallelGenExecutor.createTask(this, priority); -+ } else { -+ final int writeRadius = ((ChunkSystemChunkStatus)this.toStatus).moonrise$getWriteRadius(); -+ if (writeRadius < 0) { -+ this.generateTask = this.scheduler.radiusAwareScheduler.createInfiniteRadiusTask(this, priority); -+ } else { -+ this.generateTask = this.scheduler.radiusAwareScheduler.createTask(chunkX, chunkZ, writeRadius, this, priority); -+ } -+ } -+ } -+ -+ @Override -+ public ChunkStatus getTargetStatus() { -+ return this.toStatus; -+ } -+ -+ private boolean isEmptyTask() { -+ // must use fromStatus here to avoid any race condition with run() overwriting the status -+ final boolean generation = !this.fromStatus.isOrAfter(this.toStatus); -+ return (generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) || (!generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus()); -+ } -+ -+ @Override -+ public void run() { -+ final ChunkAccess chunk = this.fromChunk; -+ -+ final ServerChunkCache serverChunkCache = this.world.getChunkSource(); -+ final ChunkMap chunkMap = serverChunkCache.chunkMap; -+ -+ final CompletableFuture<ChunkAccess> completeFuture; -+ -+ final boolean generation; -+ boolean completing = false; -+ -+ // note: should optimise the case where the chunk does not need to execute the status, because -+ // schedule() calls this synchronously if it will run through that path -+ -+ final WorldGenContext ctx = chunkMap.worldGenContext; -+ try { -+ generation = !chunk.getPersistedStatus().isOrAfter(this.toStatus); -+ if (generation) { -+ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) { -+ if (chunk instanceof ProtoChunk) { -+ ((ProtoChunk)chunk).setPersistedStatus(this.toStatus); -+ } -+ completing = true; -+ this.complete(chunk, null); -+ return; -+ } -+ completeFuture = ChunkPyramid.GENERATION_PYRAMID.getStepTo(this.toStatus).apply(ctx, this.neighbours, this.fromChunk) -+ .whenComplete((final ChunkAccess either, final Throwable throwable) -> { -+ if (either instanceof ProtoChunk proto) { -+ proto.setPersistedStatus(ChunkUpgradeGenericStatusTask.this.toStatus); -+ } -+ } -+ ); -+ } else { -+ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus()) { -+ completing = true; -+ this.complete(chunk, null); -+ return; -+ } -+ completeFuture = ChunkPyramid.LOADING_PYRAMID.getStepTo(this.toStatus).apply(ctx, this.neighbours, this.fromChunk); -+ } -+ } catch (final Throwable throwable) { -+ if (!completing) { -+ this.complete(null, throwable); -+ return; -+ } -+ -+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( -+ "Target status", ChunkTaskScheduler.stringIfNull(this.toStatus), -+ "From status", ChunkTaskScheduler.stringIfNull(this.fromStatus), -+ "Generation task", this -+ ), throwable); -+ -+ LOGGER.error( -+ "Failed to complete status for chunk: status:" + this.toStatus + ", chunk: (" + this.chunkX + -+ "," + this.chunkZ + "), world: " + WorldUtil.getWorldName(this.world), -+ throwable -+ ); -+ -+ return; -+ } -+ -+ if (!completeFuture.isDone() && !((ChunkSystemChunkStatus)this.toStatus).moonrise$getWarnedAboutNoImmediateComplete().getAndSet(true)) { -+ LOGGER.warn("Future status not complete after scheduling: " + this.toStatus.toString() + ", generate: " + generation); -+ } -+ -+ final ChunkAccess newChunk; -+ -+ try { -+ newChunk = completeFuture.join(); -+ } catch (final Throwable throwable) { -+ this.complete(null, throwable); -+ return; -+ } -+ -+ if (newChunk == null) { -+ this.complete(null, -+ new IllegalStateException( -+ "Chunk for status: " + ChunkUpgradeGenericStatusTask.this.toStatus.toString() -+ + ", generation: " + generation + " should not be null! Future: " + completeFuture -+ ).fillInStackTrace() -+ ); -+ return; -+ } -+ -+ this.complete(newChunk, null); -+ } -+ -+ private volatile boolean scheduled; -+ private static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkUpgradeGenericStatusTask.class, "scheduled", boolean.class); -+ -+ @Override -+ public boolean isScheduled() { -+ return this.scheduled; -+ } -+ -+ @Override -+ public void schedule() { -+ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkUpgradeGenericStatusTask)this, true)) { -+ throw new IllegalStateException("Cannot double call schedule()"); -+ } -+ if (this.isEmptyTask()) { -+ if (this.generateTask.cancel()) { -+ this.run(); -+ } -+ } else { -+ this.generateTask.queue(); -+ } -+ } -+ -+ @Override -+ public void cancel() { -+ if (this.generateTask.cancel()) { -+ this.complete(null, null); -+ } -+ } -+ -+ @Override -+ public Priority getPriority() { -+ return this.generateTask.getPriority(); -+ } -+ -+ @Override -+ public void lowerPriority(final Priority priority) { -+ if (!Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ this.generateTask.lowerPriority(priority); -+ } -+ -+ @Override -+ public void setPriority(final Priority priority) { -+ if (!Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ this.generateTask.setPriority(priority); -+ } -+ -+ @Override -+ public void raisePriority(final Priority priority) { -+ if (!Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ this.generateTask.raisePriority(priority); -+ } -+} -diff --git a/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<OnMain,FinalCompletion> { -+ -+ private static final Logger LOGGER = LoggerFactory.getLogger(GenericDataLoadTask.class); -+ -+ protected static final CompoundTag CANCELLED_DATA = new CompoundTag(); -+ -+ // reference count is the upper 32 bits -+ protected final AtomicLong stageAndReferenceCount = new AtomicLong(STAGE_NOT_STARTED); -+ -+ protected static final long STAGE_MASK = 0xFFFFFFFFL; -+ protected static final long STAGE_CANCELLED = 0xFFFFFFFFL; -+ protected static final long STAGE_NOT_STARTED = 0L; -+ protected static final long STAGE_LOADING = 1L; -+ protected static final long STAGE_PROCESSING = 2L; -+ protected static final long STAGE_COMPLETED = 3L; -+ -+ // for loading data off disk -+ protected final LoadDataFromDiskTask loadDataFromDiskTask; -+ // processing off-main -+ protected final PrioritisedExecutor.PrioritisedTask processOffMain; -+ // processing on-main -+ protected final PrioritisedExecutor.PrioritisedTask processOnMain; -+ -+ protected final ChunkTaskScheduler scheduler; -+ protected final ServerLevel world; -+ protected final int chunkX; -+ protected final int chunkZ; -+ protected final MoonriseRegionFileIO.RegionFileType type; -+ -+ public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, -+ final int chunkZ, final MoonriseRegionFileIO.RegionFileType type, -+ final Priority priority) { -+ this.scheduler = scheduler; -+ this.world = world; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.type = type; -+ -+ final ProcessOnMainTask mainTask; -+ if (this.hasOnMain()) { -+ mainTask = new ProcessOnMainTask(); -+ this.processOnMain = this.createOnMain(mainTask, priority); -+ } else { -+ mainTask = null; -+ this.processOnMain = null; -+ } -+ -+ final ProcessOffMainTask offMainTask; -+ if (this.hasOffMain()) { -+ offMainTask = new ProcessOffMainTask(mainTask); -+ this.processOffMain = this.createOffMain(offMainTask, priority); -+ } else { -+ offMainTask = null; -+ this.processOffMain = null; -+ } -+ -+ if (this.processOffMain == null && this.processOnMain == null) { -+ throw new IllegalStateException("Illegal class implementation: " + this.getClass().getName() + ", should be able to schedule at least one task!"); -+ } -+ -+ this.loadDataFromDiskTask = new LoadDataFromDiskTask(world, chunkX, chunkZ, type, new DataLoadCallback(offMainTask, mainTask), priority); -+ } -+ -+ public static final record TaskResult<L, R>(L left, R right) {} -+ -+ protected abstract boolean hasOffMain(); -+ -+ protected abstract boolean hasOnMain(); -+ -+ protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority); -+ -+ protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority); -+ -+ protected abstract TaskResult<OnMain, Throwable> runOffMain(final CompoundTag data, final Throwable throwable); -+ -+ protected abstract TaskResult<FinalCompletion, Throwable> runOnMain(final OnMain data, final Throwable throwable); -+ -+ protected abstract void onComplete(final TaskResult<FinalCompletion,Throwable> result); -+ -+ protected abstract TaskResult<FinalCompletion, Throwable> completeOnMainOffMain(final OnMain data, final Throwable throwable); -+ -+ @Override -+ public String toString() { -+ return "GenericDataLoadTask{class: " + this.getClass().getName() + ", world: " + WorldUtil.getWorldName(this.world) + -+ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() + -+ ", type: " + this.type.toString() + "}"; -+ } -+ -+ public Priority getPriority() { -+ if (this.processOnMain != null) { -+ return this.processOnMain.getPriority(); -+ } else { -+ return this.processOffMain.getPriority(); -+ } -+ } -+ -+ public void lowerPriority(final Priority priority) { -+ // can't lower I/O tasks, we don't know what they affect -+ if (this.processOffMain != null) { -+ this.processOffMain.lowerPriority(priority); -+ } -+ if (this.processOnMain != null) { -+ this.processOnMain.lowerPriority(priority); -+ } -+ } -+ -+ public void setPriority(final Priority priority) { -+ // can't lower I/O tasks, we don't know what they affect -+ this.loadDataFromDiskTask.raisePriority(priority); -+ if (this.processOffMain != null) { -+ this.processOffMain.setPriority(priority); -+ } -+ if (this.processOnMain != null) { -+ this.processOnMain.setPriority(priority); -+ } -+ } -+ -+ public void raisePriority(final Priority priority) { -+ // can't lower I/O tasks, we don't know what they affect -+ this.loadDataFromDiskTask.raisePriority(priority); -+ if (this.processOffMain != null) { -+ this.processOffMain.raisePriority(priority); -+ } -+ if (this.processOnMain != null) { -+ this.processOnMain.raisePriority(priority); -+ } -+ } -+ -+ // returns whether scheduleNow() needs to be called -+ public boolean schedule(final boolean delay) { -+ if (this.stageAndReferenceCount.get() != STAGE_NOT_STARTED || -+ !this.stageAndReferenceCount.compareAndSet(STAGE_NOT_STARTED, (1L << 32) | STAGE_LOADING)) { -+ // try and increment reference count -+ int failures = 0; -+ for (long curr = this.stageAndReferenceCount.get();;) { -+ if ((curr & STAGE_MASK) == STAGE_CANCELLED || (curr & STAGE_MASK) == STAGE_COMPLETED) { -+ // cancelled or completed, nothing to do here -+ return false; -+ } -+ -+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, curr + (1L << 32)))) { -+ // successful -+ return false; -+ } -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ if (!delay) { -+ this.scheduleNow(); -+ return false; -+ } -+ return true; -+ } -+ -+ public void scheduleNow() { -+ this.loadDataFromDiskTask.schedule(); // will schedule the rest -+ } -+ -+ // assumes the current stage cannot be completed -+ // returns false if cancelled, returns true if can proceed -+ private boolean advanceStage(final long expect, final long to) { -+ int failures = 0; -+ for (long curr = this.stageAndReferenceCount.get();;) { -+ if ((curr & STAGE_MASK) != expect) { -+ // must be cancelled -+ return false; -+ } -+ -+ final long newVal = (curr & ~STAGE_MASK) | to; -+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { -+ return true; -+ } -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ public boolean cancel() { -+ int failures = 0; -+ for (long curr = this.stageAndReferenceCount.get();;) { -+ if ((curr & STAGE_MASK) == STAGE_COMPLETED || (curr & STAGE_MASK) == STAGE_CANCELLED) { -+ return false; -+ } -+ -+ if ((curr & STAGE_MASK) == STAGE_NOT_STARTED || (curr & ~STAGE_MASK) == (1L << 32)) { -+ // no other references, so we can cancel -+ final long newVal = STAGE_CANCELLED; -+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { -+ this.loadDataFromDiskTask.cancel(); -+ if (this.processOffMain != null) { -+ this.processOffMain.cancel(); -+ } -+ if (this.processOnMain != null) { -+ this.processOnMain.cancel(); -+ } -+ this.onComplete(null); -+ return true; -+ } -+ } else { -+ if ((curr & ~STAGE_MASK) == (0L << 32)) { -+ throw new IllegalStateException("Reference count cannot be zero here"); -+ } -+ // just decrease the reference count -+ final long newVal = curr - (1L << 32); -+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { -+ return false; -+ } -+ } -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ private final class DataLoadCallback implements BiConsumer<CompoundTag, Throwable> { -+ -+ private final ProcessOffMainTask offMainTask; -+ private final ProcessOnMainTask onMainTask; -+ -+ public DataLoadCallback(final ProcessOffMainTask offMainTask, final ProcessOnMainTask onMainTask) { -+ this.offMainTask = offMainTask; -+ this.onMainTask = onMainTask; -+ } -+ -+ @Override -+ public void accept(final CompoundTag compoundTag, final Throwable throwable) { -+ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) { -+ // don't try to schedule further -+ return; -+ } -+ -+ try { -+ if (compoundTag == CANCELLED_DATA) { -+ // cancelled, except this isn't possible -+ LOGGER.error("Data callback says cancelled, but stage does not?"); -+ return; -+ } -+ -+ // get off of the regionfile callback ASAP, no clue what locks are held right now... -+ if (GenericDataLoadTask.this.processOffMain != null) { -+ this.offMainTask.data = compoundTag; -+ this.offMainTask.throwable = throwable; -+ GenericDataLoadTask.this.processOffMain.queue(); -+ return; -+ } else { -+ // no off-main task, so go straight to main -+ this.onMainTask.data = (OnMain)compoundTag; -+ this.onMainTask.throwable = throwable; -+ GenericDataLoadTask.this.processOnMain.queue(); -+ } -+ } catch (final Throwable thr2) { -+ LOGGER.error("Failed I/O callback for task: " + GenericDataLoadTask.this.toString(), thr2); -+ GenericDataLoadTask.this.scheduler.unrecoverableChunkSystemFailure( -+ GenericDataLoadTask.this.chunkX, GenericDataLoadTask.this.chunkZ, Map.of( -+ "Callback throwable", ChunkTaskScheduler.stringIfNull(throwable) -+ ), thr2 -+ ); -+ } -+ } -+ } -+ -+ private final class ProcessOffMainTask implements Runnable { -+ -+ private CompoundTag data; -+ private Throwable throwable; -+ private final ProcessOnMainTask schedule; -+ -+ public ProcessOffMainTask(final ProcessOnMainTask schedule) { -+ this.schedule = schedule; -+ } -+ -+ @Override -+ public void run() { -+ if (!GenericDataLoadTask.this.advanceStage(STAGE_LOADING, this.schedule == null ? STAGE_COMPLETED : STAGE_PROCESSING)) { -+ // cancelled -+ return; -+ } -+ final TaskResult<OnMain, Throwable> newData = GenericDataLoadTask.this.runOffMain(this.data, this.throwable); -+ -+ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) { -+ // don't try to schedule further -+ return; -+ } -+ -+ if (this.schedule != null) { -+ final TaskResult<FinalCompletion, Throwable> syncComplete = GenericDataLoadTask.this.completeOnMainOffMain(newData.left, newData.right); -+ -+ if (syncComplete != null) { -+ if (GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) { -+ GenericDataLoadTask.this.onComplete(syncComplete); -+ } // else: cancelled -+ return; -+ } -+ -+ this.schedule.data = newData.left; -+ this.schedule.throwable = newData.right; -+ -+ GenericDataLoadTask.this.processOnMain.queue(); -+ } else { -+ GenericDataLoadTask.this.onComplete((TaskResult<FinalCompletion, Throwable>)newData); -+ } -+ } -+ } -+ -+ private final class ProcessOnMainTask implements Runnable { -+ -+ private OnMain data; -+ private Throwable throwable; -+ -+ @Override -+ public void run() { -+ if (!GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) { -+ // cancelled -+ return; -+ } -+ final TaskResult<FinalCompletion, Throwable> result = GenericDataLoadTask.this.runOnMain(this.data, this.throwable); -+ -+ GenericDataLoadTask.this.onComplete(result); -+ } -+ } -+ -+ protected static final class LoadDataFromDiskTask { -+ -+ private volatile int priority; -+ private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(LoadDataFromDiskTask.class, "priority", int.class); -+ -+ private static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 0; -+ private static final int PRIORITY_LOAD_SCHEDULED = Integer.MIN_VALUE >>> 1; -+ private static final int PRIORITY_UNLOAD_SCHEDULED = Integer.MIN_VALUE >>> 2; -+ -+ private static final int PRIORITY_FLAGS = ~Character.MAX_VALUE; -+ -+ private final int getPriorityVolatile() { -+ return (int)PRIORITY_HANDLE.getVolatile((LoadDataFromDiskTask)this); -+ } -+ -+ private final int compareAndExchangePriorityVolatile(final int expect, final int update) { -+ return (int)PRIORITY_HANDLE.compareAndExchange((LoadDataFromDiskTask)this, (int)expect, (int)update); -+ } -+ -+ private final int getAndOrPriorityVolatile(final int val) { -+ return (int)PRIORITY_HANDLE.getAndBitwiseOr((LoadDataFromDiskTask)this, (int)val); -+ } -+ -+ private final void setPriorityPlain(final int val) { -+ PRIORITY_HANDLE.set((LoadDataFromDiskTask)this, (int)val); -+ } -+ -+ private final ServerLevel world; -+ private final int chunkX; -+ private final int chunkZ; -+ -+ private final MoonriseRegionFileIO.RegionFileType type; -+ private Cancellable dataLoadTask; -+ private Cancellable dataUnloadCancellable; -+ private PrioritisedExecutor.PrioritisedTask dataUnloadTask; -+ -+ private final BiConsumer<CompoundTag, Throwable> onComplete; -+ private final AtomicBoolean scheduled = new AtomicBoolean(); -+ -+ // onComplete should be caller sensitive, it may complete synchronously with schedule() - which does -+ // hold a priority lock. -+ public LoadDataFromDiskTask(final ServerLevel world, final int chunkX, final int chunkZ, -+ final MoonriseRegionFileIO.RegionFileType type, -+ final BiConsumer<CompoundTag, Throwable> onComplete, -+ final Priority priority) { -+ if (!Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ this.world = world; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.type = type; -+ this.onComplete = onComplete; -+ this.setPriorityPlain(priority.priority); -+ } -+ -+ private void complete(final CompoundTag data, final Throwable throwable) { -+ try { -+ this.onComplete.accept(data, throwable); -+ } catch (final Throwable thr2) { -+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( -+ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable), -+ "Regionfile type", ChunkTaskScheduler.stringIfNull(this.type) -+ ), thr2); -+ } -+ } -+ -+ private boolean markExecuting() { -+ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0; -+ } -+ -+ private boolean isMarkedExecuted() { -+ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0; -+ } -+ -+ public void lowerPriority(final Priority priority) { -+ if (!Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ -+ int failures = 0; -+ for (int curr = this.getPriorityVolatile();;) { -+ if ((curr & PRIORITY_EXECUTED) != 0) { -+ // cancelled or executed -+ return; -+ } -+ -+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { -+ MoonriseRegionFileIO.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); -+ return; -+ } -+ -+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { -+ if (this.dataUnloadTask != null) { -+ this.dataUnloadTask.lowerPriority(priority); -+ } -+ // no return - we need to propagate priority -+ } -+ -+ if (!priority.isHigherPriority(curr & ~PRIORITY_FLAGS)) { -+ return; -+ } -+ -+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { -+ return; -+ } -+ -+ // failed, retry -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ public void setPriority(final Priority priority) { -+ if (!Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ -+ int failures = 0; -+ for (int curr = this.getPriorityVolatile();;) { -+ if ((curr & PRIORITY_EXECUTED) != 0) { -+ // cancelled or executed -+ return; -+ } -+ -+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { -+ MoonriseRegionFileIO.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); -+ return; -+ } -+ -+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { -+ if (this.dataUnloadTask != null) { -+ this.dataUnloadTask.setPriority(priority); -+ } -+ // no return - we need to propagate priority -+ } -+ -+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { -+ return; -+ } -+ -+ // failed, retry -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ public void raisePriority(final Priority priority) { -+ if (!Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ -+ int failures = 0; -+ for (int curr = this.getPriorityVolatile();;) { -+ if ((curr & PRIORITY_EXECUTED) != 0) { -+ // cancelled or executed -+ return; -+ } -+ -+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { -+ MoonriseRegionFileIO.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority); -+ return; -+ } -+ -+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { -+ if (this.dataUnloadTask != null) { -+ this.dataUnloadTask.raisePriority(priority); -+ } -+ // no return - we need to propagate priority -+ } -+ -+ if (!priority.isLowerPriority(curr & ~PRIORITY_FLAGS)) { -+ return; -+ } -+ -+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { -+ return; -+ } -+ -+ // failed, retry -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ public void cancel() { -+ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) { -+ // cancelled or executed already -+ return; -+ } -+ -+ // OK if we miss the field read, the task cannot complete if the cancelled bit is set and -+ // the write to dataLoadTask will check for the cancelled bit -+ if (this.dataUnloadCancellable != null) { -+ this.dataUnloadCancellable.cancel(); -+ } -+ -+ if (this.dataLoadTask != null) { -+ this.dataLoadTask.cancel(); -+ } -+ -+ this.complete(CANCELLED_DATA, null); -+ } -+ -+ public void schedule() { -+ if (this.scheduled.getAndSet(true)) { -+ throw new IllegalStateException("schedule() called twice"); -+ } -+ int priority = this.getPriorityVolatile(); -+ -+ if ((priority & PRIORITY_EXECUTED) != 0) { -+ // cancelled -+ return; -+ } -+ -+ final BiConsumer<CompoundTag, Throwable> consumer = (final CompoundTag data, final Throwable thr) -> { -+ // because cancelScheduled() cannot actually stop this task from executing in every case, we need -+ // to mark complete here to ensure we do not double complete -+ if (LoadDataFromDiskTask.this.markExecuting()) { -+ LoadDataFromDiskTask.this.complete(data, thr); -+ } // else: cancelled -+ }; -+ -+ final Priority initialPriority = Priority.getPriority(priority); -+ boolean scheduledUnload = false; -+ -+ final NewChunkHolder holder = ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ); -+ if (holder != null) { -+ final BiConsumer<CompoundTag, Throwable> unloadConsumer = (final CompoundTag data, final Throwable thr) -> { -+ if (data != null) { -+ consumer.accept(data, null); -+ } else { -+ // need to schedule task -+ LoadDataFromDiskTask.this.schedule(false, consumer, Priority.getPriority(LoadDataFromDiskTask.this.getPriorityVolatile() & ~PRIORITY_FLAGS)); -+ } -+ }; -+ Cancellable unloadCancellable = null; -+ CompoundTag syncComplete = null; -+ final NewChunkHolder.UnloadTask unloadTask = holder.getUnloadTask(this.type); // can be null if no task exists -+ final CallbackCompletable<CompoundTag> unloadCompletable = unloadTask == null ? null : unloadTask.completable(); -+ if (unloadCompletable != null) { -+ unloadCancellable = unloadCompletable.addAsynchronousWaiter(unloadConsumer); -+ if (unloadCancellable == null) { -+ syncComplete = unloadCompletable.getResult(); -+ } -+ } -+ -+ if (syncComplete != null) { -+ consumer.accept(syncComplete, null); -+ return; -+ } -+ -+ if (unloadCancellable != null) { -+ scheduledUnload = true; -+ this.dataUnloadCancellable = unloadCancellable; -+ this.dataUnloadTask = unloadTask.task(); -+ } -+ } -+ -+ this.schedule(scheduledUnload, consumer, initialPriority); -+ } -+ -+ private void schedule(final boolean scheduledUnload, final BiConsumer<CompoundTag, Throwable> consumer, final Priority initialPriority) { -+ int priority = this.getPriorityVolatile(); -+ -+ if ((priority & PRIORITY_EXECUTED) != 0) { -+ // cancelled -+ return; -+ } -+ -+ if (!scheduledUnload) { -+ this.dataLoadTask = MoonriseRegionFileIO.loadDataAsync( -+ this.world, this.chunkX, this.chunkZ, this.type, consumer, -+ initialPriority.isHigherPriority(Priority.NORMAL), initialPriority -+ ); -+ } -+ -+ int failures = 0; -+ for (;;) { -+ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | (scheduledUnload ? PRIORITY_UNLOAD_SCHEDULED : PRIORITY_LOAD_SCHEDULED)))) { -+ return; -+ } -+ -+ if ((priority & PRIORITY_EXECUTED) != 0) { -+ // cancelled or executed -+ if (this.dataUnloadCancellable != null) { -+ this.dataUnloadCancellable.cancel(); -+ } -+ -+ if (this.dataLoadTask != null) { -+ this.dataLoadTask.cancel(); -+ } -+ return; -+ } -+ -+ if (scheduledUnload) { -+ if (this.dataUnloadTask != null) { -+ this.dataUnloadTask.setPriority(Priority.getPriority(priority & ~PRIORITY_FLAGS)); -+ } -+ } else { -+ MoonriseRegionFileIO.setPriority(this.world, this.chunkX, this.chunkZ, this.type, Priority.getPriority(priority & ~PRIORITY_FLAGS)); -+ } -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ } -+} -diff --git a/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<T> { -+ -+ 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<T> { -+ -+ public SortedArraySet<T> moonrise$copy(); -+ -+ public Object[] moonrise$copyBackingArray(); -+ -+ public T moonrise$replace(final T object); -+ -+ public T moonrise$removeAndGet(final T object); -+ -+} -diff --git a/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<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> 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<ServerChunkCache.ChunkAndHolder> moonrise$getPlayerTickingChunks(); -+ -+ public void moonrise$markChunkForPlayerTicking(final LevelChunk chunk); -+ -+ public void moonrise$removeChunkForPlayerTicking(final LevelChunk chunk); -+ -+ public void moonrise$addPlayerTickingRequest(final int chunkX, final int chunkZ); -+ -+ public void moonrise$removePlayerTickingRequest(final int chunkX, final int chunkZ); -+ -+} -diff --git a/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<AABB> 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<AABB> 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<AABB> 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<VoxelShape> 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<VoxelShape> 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<VoxelShape> 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<VoxelShape> potentialCollisions) { -+ double x = moveVector.x; -+ double y = moveVector.y; -+ double z = moveVector.z; -+ -+ if (y != 0.0) { -+ y = performVoxelCollisionsY(axisalignedbb, y, potentialCollisions); -+ if (y != 0.0) { -+ axisalignedbb = offsetY(axisalignedbb, y); -+ } -+ } -+ -+ final boolean xSmaller = Math.abs(x) < Math.abs(z); -+ -+ if (xSmaller && z != 0.0) { -+ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions); -+ if (z != 0.0) { -+ axisalignedbb = offsetZ(axisalignedbb, z); -+ } -+ } -+ -+ if (x != 0.0) { -+ x = performVoxelCollisionsX(axisalignedbb, x, potentialCollisions); -+ if (!xSmaller && x != 0.0) { -+ axisalignedbb = offsetX(axisalignedbb, x); -+ } -+ } -+ -+ if (!xSmaller && z != 0.0) { -+ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions); -+ } -+ -+ return new Vec3(x, y, z); -+ } -+ -+ public static Vec3 performAABBCollisions(final Vec3 moveVector, AABB axisalignedbb, final List<AABB> potentialCollisions) { -+ double x = moveVector.x; -+ double y = moveVector.y; -+ double z = moveVector.z; -+ -+ if (y != 0.0) { -+ y = performAABBCollisionsY(axisalignedbb, y, potentialCollisions); -+ if (y != 0.0) { -+ axisalignedbb = offsetY(axisalignedbb, y); -+ } -+ } -+ -+ final boolean xSmaller = Math.abs(x) < Math.abs(z); -+ -+ if (xSmaller && z != 0.0) { -+ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions); -+ if (z != 0.0) { -+ axisalignedbb = offsetZ(axisalignedbb, z); -+ } -+ } -+ -+ if (x != 0.0) { -+ x = performAABBCollisionsX(axisalignedbb, x, potentialCollisions); -+ if (!xSmaller && x != 0.0) { -+ axisalignedbb = offsetX(axisalignedbb, x); -+ } -+ } -+ -+ if (!xSmaller && z != 0.0) { -+ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions); -+ } -+ -+ return new Vec3(x, y, z); -+ } -+ -+ public static Vec3 performCollisions(final Vec3 moveVector, AABB axisalignedbb, -+ final List<VoxelShape> voxels, -+ final List<AABB> aabbs) { -+ if (voxels.isEmpty()) { -+ // fast track only AABBs -+ return performAABBCollisions(moveVector, axisalignedbb, aabbs); -+ } -+ -+ double x = moveVector.x; -+ double y = moveVector.y; -+ double z = moveVector.z; -+ -+ if (y != 0.0) { -+ y = performAABBCollisionsY(axisalignedbb, y, aabbs); -+ y = performVoxelCollisionsY(axisalignedbb, y, voxels); -+ if (y != 0.0) { -+ axisalignedbb = offsetY(axisalignedbb, y); -+ } -+ } -+ -+ final boolean xSmaller = Math.abs(x) < Math.abs(z); -+ -+ if (xSmaller && z != 0.0) { -+ z = performAABBCollisionsZ(axisalignedbb, z, aabbs); -+ z = performVoxelCollisionsZ(axisalignedbb, z, voxels); -+ if (z != 0.0) { -+ axisalignedbb = offsetZ(axisalignedbb, z); -+ } -+ } -+ -+ if (x != 0.0) { -+ x = performAABBCollisionsX(axisalignedbb, x, aabbs); -+ x = performVoxelCollisionsX(axisalignedbb, x, voxels); -+ if (!xSmaller && x != 0.0) { -+ axisalignedbb = offsetX(axisalignedbb, x); -+ } -+ } -+ -+ if (!xSmaller && z != 0.0) { -+ z = performAABBCollisionsZ(axisalignedbb, z, aabbs); -+ z = performVoxelCollisionsZ(axisalignedbb, z, voxels); -+ } -+ -+ return new Vec3(x, y, z); -+ } -+ -+ public static boolean isCollidingWithBorder(final WorldBorder worldborder, final AABB boundingBox) { -+ return isCollidingWithBorder(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); -+ } -+ -+ public static boolean isCollidingWithBorder(final WorldBorder worldborder, -+ final double boxMinX, final double boxMaxX, -+ final double boxMinZ, final double boxMaxZ) { -+ final double borderMinX = Math.floor(worldborder.getMinX()); // -X -+ final double borderMaxX = Math.ceil(worldborder.getMaxX()); // +X -+ -+ final double borderMinZ = Math.floor(worldborder.getMinZ()); // -Z -+ final double borderMaxZ = Math.ceil(worldborder.getMaxZ()); // +Z -+ -+ // inverted check for world border enclosing the specified box expanded by -EPSILON -+ return (borderMinX - boxMinX) > CollisionUtil.COLLISION_EPSILON || (borderMaxX - boxMaxX) < -CollisionUtil.COLLISION_EPSILON || -+ (borderMinZ - boxMinZ) > CollisionUtil.COLLISION_EPSILON || (borderMaxZ - boxMaxZ) < -CollisionUtil.COLLISION_EPSILON; -+ } -+ -+ /* Math.max/min specify that any NaN argument results in a NaN return, unlike these functions */ -+ private static double min(final double x, final double y) { -+ return x < y ? x : y; -+ } -+ -+ private static double max(final double x, final double y) { -+ return x > y ? x : y; -+ } -+ -+ public static final int COLLISION_FLAG_LOAD_CHUNKS = 1 << 0; -+ public static final int COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS = 1 << 1; -+ public static final int COLLISION_FLAG_CHECK_BORDER = 1 << 2; -+ public static final int COLLISION_FLAG_CHECK_ONLY = 1 << 3; -+ -+ public static boolean getCollisionsForBlocksOrWorldBorder(final Level world, final Entity entity, final AABB aabb, -+ final List<VoxelShape> intoVoxel, final List<AABB> intoAABB, -+ final int collisionFlags, final BiPredicate<BlockState, BlockPos> predicate) { -+ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; -+ boolean ret = false; -+ -+ if ((collisionFlags & COLLISION_FLAG_CHECK_BORDER) != 0) { -+ final WorldBorder worldBorder = world.getWorldBorder(); -+ if (CollisionUtil.isCollidingWithBorder(worldBorder, aabb) && entity != null && worldBorder.isInsideCloseToBorder(entity, aabb)) { -+ if (checkOnly) { -+ return true; -+ } else { -+ final VoxelShape borderShape = worldBorder.getCollisionShape(); -+ intoVoxel.add(borderShape); -+ ret = true; -+ } -+ } -+ } -+ -+ final int minSection = WorldUtil.getMinSection(world); -+ -+ final int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1; -+ final int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1; -+ -+ final int minBlockY = Math.max((minSection << 4) - 1, Mth.floor(aabb.minY - COLLISION_EPSILON) - 1); -+ final int maxBlockY = Math.min((WorldUtil.getMaxSection(world) << 4) + 16, Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1); -+ -+ final int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1; -+ final int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1; -+ -+ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); -+ final CollisionContext collisionShape = new LazyEntityCollisionContext(entity); -+ final boolean useEntityCollisionShape = LazyEntityCollisionContext.useEntityCollisionShape(world, entity); -+ -+ // special cases: -+ if (minBlockY > maxBlockY) { -+ // no point in checking -+ return ret; -+ } -+ -+ final int minChunkX = minBlockX >> 4; -+ final int maxChunkX = maxBlockX >> 4; -+ -+ final int minChunkY = minBlockY >> 4; -+ final int maxChunkY = maxBlockY >> 4; -+ -+ final int minChunkZ = minBlockZ >> 4; -+ final int maxChunkZ = maxBlockZ >> 4; -+ -+ final boolean loadChunks = (collisionFlags & COLLISION_FLAG_LOAD_CHUNKS) != 0; -+ final ChunkSource chunkSource = world.getChunkSource(); -+ -+ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { -+ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { -+ final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks); -+ -+ if (chunk == null) { -+ if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) { -+ if (checkOnly) { -+ return true; -+ } else { -+ intoAABB.add(getBoxForChunk(currChunkX, currChunkZ)); -+ ret = true; -+ } -+ } -+ continue; -+ } -+ -+ final LevelChunkSection[] sections = chunk.getSections(); -+ -+ // bound y -+ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { -+ final int sectionIdx = currChunkY - minSection; -+ if (sectionIdx < 0 || sectionIdx >= sections.length) { -+ continue; -+ } -+ final LevelChunkSection section = sections[sectionIdx]; -+ if (section.hasOnlyAir()) { -+ // empty -+ continue; -+ } -+ -+ final boolean hasSpecial = ((BlockCountingChunkSection)section).moonrise$hasSpecialCollidingBlocks(); -+ final int sectionAdjust = !hasSpecial ? 1 : 0; -+ -+ final PalettedContainer<BlockState> blocks = section.states; -+ -+ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0; -+ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15; -+ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) + sectionAdjust : 0; -+ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) - sectionAdjust : 15; -+ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) + sectionAdjust : 0; -+ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) - sectionAdjust : 15; -+ -+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) { -+ final int blockY = currY | (currChunkY << 4); -+ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { -+ final int blockZ = currZ | (currChunkZ << 4); -+ for (int currX = minXIterate; currX <= maxXIterate; ++currX) { -+ final int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8); -+ final int blockX = currX | (currChunkX << 4); -+ -+ final int edgeCount = hasSpecial ? ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + -+ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + -+ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0) : 0; -+ if (edgeCount == 3) { -+ continue; -+ } -+ -+ final BlockState blockData = blocks.get(localBlockIndex); -+ -+ if (((CollisionBlockState)blockData).moonrise$emptyContextCollisionShape()) { -+ continue; -+ } -+ -+ VoxelShape blockCollision = ((CollisionBlockState)blockData).moonrise$getConstantContextCollisionShape(); -+ -+ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) { -+ if (useEntityCollisionShape) { -+ mutablePos.set(blockX, blockY, blockZ); -+ blockCollision = collisionShape.getCollisionShape(blockData, world, mutablePos); -+ } else if (blockCollision == null) { -+ mutablePos.set(blockX, blockY, blockZ); -+ blockCollision = blockData.getCollisionShape(world, mutablePos, collisionShape); -+ } -+ -+ AABB singleAABB = ((CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation(); -+ if (singleAABB != null) { -+ singleAABB = singleAABB.move((double)blockX, (double)blockY, (double)blockZ); -+ if (!voxelShapeIntersect(aabb, singleAABB)) { -+ continue; -+ } -+ -+ if (predicate != null) { -+ mutablePos.set(blockX, blockY, blockZ); -+ if (!predicate.test(blockData, mutablePos)) { -+ continue; -+ } -+ } -+ -+ if (checkOnly) { -+ return true; -+ } else { -+ ret = true; -+ intoAABB.add(singleAABB); -+ continue; -+ } -+ } -+ -+ if (blockCollision.isEmpty()) { -+ continue; -+ } -+ -+ final VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ); -+ -+ if (!voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) { -+ continue; -+ } -+ -+ if (predicate != null) { -+ mutablePos.set(blockX, blockY, blockZ); -+ if (!predicate.test(blockData, mutablePos)) { -+ continue; -+ } -+ } -+ -+ if (checkOnly) { -+ return true; -+ } else { -+ ret = true; -+ intoVoxel.add(blockCollisionOffset); -+ continue; -+ } -+ } -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ return ret; -+ } -+ -+ public static boolean getEntityHardCollisions(final Level world, final Entity entity, AABB aabb, -+ final List<AABB> into, final int collisionFlags, final Predicate<Entity> predicate) { -+ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; -+ -+ boolean ret = false; -+ -+ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with. -+ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems -+ // specifically with boat collisions. -+ aabb = aabb.inflate(-COLLISION_EPSILON, -COLLISION_EPSILON, -COLLISION_EPSILON); -+ final List<Entity> entities; -+ if (entity != null && ((ChunkSystemEntity)entity).moonrise$isHardColliding()) { -+ entities = world.getEntities(entity, aabb, predicate); -+ } else { -+ entities = ((ChunkSystemEntityGetter)world).moonrise$getHardCollidingEntities(entity, aabb, predicate); -+ } -+ -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ final Entity otherEntity = entities.get(i); -+ -+ if (otherEntity.isSpectator()) { -+ continue; -+ } -+ -+ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) { -+ if (checkOnly) { -+ return true; -+ } else { -+ into.add(otherEntity.getBoundingBox()); -+ ret = true; -+ } -+ } -+ } -+ -+ return ret; -+ } -+ -+ public static boolean getCollisions(final Level world, final Entity entity, final AABB aabb, -+ final List<VoxelShape> intoVoxel, final List<AABB> intoAABB, final int collisionFlags, -+ final BiPredicate<BlockState, BlockPos> blockPredicate, -+ final Predicate<Entity> entityPredicate) { -+ if ((collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0) { -+ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate) -+ || getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate); -+ } else { -+ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate) -+ | getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate); -+ } -+ } -+ -+ public static final class LazyEntityCollisionContext extends EntityCollisionContext { -+ -+ private CollisionContext delegate; -+ private boolean delegated; -+ -+ public LazyEntityCollisionContext(final Entity entity) { -+ super(false, 0.0, null, null, entity); -+ } -+ -+ public static boolean useEntityCollisionShape(final Level world, final Entity entity) { -+ return entity instanceof AbstractMinecart && AbstractMinecart.useExperimentalMovement(world); -+ } -+ -+ public boolean isDelegated() { -+ final boolean delegated = this.delegated; -+ this.delegated = false; -+ return delegated; -+ } -+ -+ public CollisionContext getDelegate() { -+ this.delegated = true; -+ final Entity entity = 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<AABB> aabbs, -+ boolean isOffset, -+ double offX, double offY, double offZ -+) { -+ -+ public CachedToAABBs removeOffset() { -+ final List<AABB> toOffset = this.aabbs; -+ final double offX = this.offX; -+ final double offY = this.offY; -+ final double offZ = this.offZ; -+ -+ final List<AABB> ret = new ArrayList<>(toOffset.size()); -+ -+ for (int i = 0, len = toOffset.size(); i < len; ++i) { -+ ret.add(toOffset.get(i).move(offX, offY, offZ)); -+ } -+ -+ return new CachedToAABBs(ret, false, 0.0, 0.0, 0.0); -+ } -+ -+ public static CachedToAABBs offset(final CachedToAABBs cache, final double offX, final double offY, final double offZ) { -+ if (offX == 0.0 && offY == 0.0 && offZ == 0.0) { -+ return cache; -+ } -+ -+ final double resX = cache.offX + offX; -+ final double resY = cache.offY + offY; -+ final double resZ = cache.offZ + offZ; -+ -+ return new CachedToAABBs(cache.aabbs, true, resX, resY, resZ); -+ } -+} -diff --git a/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<T> { -+ -+ public default T[] moonrise$getRawPalette(final FastPaletteData<T> 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<T> { -+ -+ 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<BlockPos> positions) { -+ for (final BlockPos pos : positions) { -+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); -+ } -+ -+ this.performLightDecrease(lightAccess); -+ } -+ -+ protected List<BlockPos> getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) { -+ final List<BlockPos> sources = new ArrayList<>(); -+ -+ final int offX = chunk.getPos().x << 4; -+ final int offZ = chunk.getPos().z << 4; -+ -+ final PlatformHooks platformHooks = PlatformHooks.get(); -+ -+ final BlockGetter world = lightAccess.getLevel(); -+ final LevelChunkSection[] sections = chunk.getSections(); -+ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { -+ final LevelChunkSection section = sections[sectionY - this.minSection]; -+ if (section.hasOnlyAir()) { -+ // no sources in empty sections -+ continue; -+ } -+ if (!section.maybeHas(platformHooks.maybeHasLightEmission())) { -+ // no light sources in palette -+ continue; -+ } -+ final PalettedContainer<BlockState> states = section.states; -+ final int offY = sectionY << 4; -+ -+ final BlockPos.MutableBlockPos mutablePos = this.lightEmissionPos; -+ for (int index = 0; index < (16 * 16 * 16); ++index) { -+ final BlockState state = states.get(index); -+ mutablePos.set(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15)); -+ -+ if ((platformHooks.getLightEmission(state, world, mutablePos)) == 0) { -+ continue; -+ } -+ -+ // index = x | (z << 4) | (y << 8) -+ sources.add(mutablePos.immutable()); -+ } -+ } -+ -+ return sources; -+ } -+ -+ @Override -+ public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { -+ // setup sources -+ final BlockGetter world = lightAccess.getLevel(); -+ final PlatformHooks platformHooks = PlatformHooks.get(); -+ -+ final int emittedMask = this.emittedLightMask; -+ final List<BlockPos> positions = this.getSources(lightAccess, chunk); -+ for (int i = 0, len = positions.size(); i < len; ++i) { -+ final BlockPos pos = positions.get(i); -+ final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ()); -+ final int emittedLight = platformHooks.getLightEmission(blockState, world, pos) & emittedMask; -+ -+ if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) { -+ // some other source is brighter -+ continue; -+ } -+ -+ this.appendToIncreaseQueue( -+ ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | (emittedLight & 0xFL) << (6 + 6 + 16) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) -+ ); -+ -+ -+ // propagation wont set this for us -+ this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight); -+ } -+ -+ if (needsEdgeChecks) { -+ // not required to propagate here, but this will reduce the hit of the edge checks -+ this.performLightIncrease(lightAccess); -+ -+ // verify neighbour edges -+ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); -+ } else { -+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection); -+ -+ this.performLightIncrease(lightAccess); -+ } -+ } -+} -diff --git a/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<ArrayDeque<byte[]>> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new); -+ -+ private static byte[] allocateBytes() { -+ final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst(); -+ if (inPool != null) { -+ return inPool; -+ } -+ -+ return new byte[ARRAY_SIZE]; -+ } -+ -+ private static void freeBytes(final byte[] bytes) { -+ WORKING_BYTES_POOL.get().addFirst(bytes); -+ } -+ -+ public static SWMRNibbleArray fromVanilla(final DataLayer nibble) { -+ if (nibble == null) { -+ return new SWMRNibbleArray(null, true); -+ } else if (nibble.isEmpty()) { -+ return new SWMRNibbleArray(); -+ } else { -+ return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later -+ } -+ } -+ -+ protected int stateUpdating; -+ protected volatile int stateVisible; -+ -+ protected byte[] storageUpdating; -+ protected boolean updatingDirty; // only returns whether storageUpdating is dirty -+ protected volatile byte[] storageVisible; -+ -+ public SWMRNibbleArray() { -+ this(null, false); // lazy init -+ } -+ -+ public SWMRNibbleArray(final byte[] bytes) { -+ this(bytes, false); -+ } -+ -+ public SWMRNibbleArray(final byte[] bytes, final boolean isNullNibble) { -+ if (bytes != null && bytes.length != ARRAY_SIZE) { -+ throw new IllegalArgumentException("Data of wrong length: " + bytes.length); -+ } -+ this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT; -+ this.storageUpdating = this.storageVisible = bytes; -+ } -+ -+ public SWMRNibbleArray(final byte[] bytes, final int state) { -+ if (bytes != null && bytes.length != ARRAY_SIZE) { -+ throw new IllegalArgumentException("Data of wrong length: " + bytes.length); -+ } -+ if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) { -+ throw new IllegalArgumentException("Data cannot be null and have state be initialised"); -+ } -+ this.stateUpdating = this.stateVisible = state; -+ this.storageUpdating = this.storageVisible = bytes; -+ } -+ -+ @Override -+ public String toString() { -+ StringBuilder stringBuilder = new StringBuilder(); -+ stringBuilder.append("State: "); -+ switch (this.stateVisible) { -+ case INIT_STATE_NULL: -+ stringBuilder.append("null"); -+ break; -+ case INIT_STATE_UNINIT: -+ stringBuilder.append("uninitialised"); -+ break; -+ case INIT_STATE_INIT: -+ stringBuilder.append("initialised"); -+ break; -+ case INIT_STATE_HIDDEN: -+ stringBuilder.append("hidden"); -+ break; -+ default: -+ stringBuilder.append("unknown"); -+ break; -+ } -+ stringBuilder.append("\nData:\n"); -+ -+ final byte[] data = this.storageVisible; -+ if (data != null) { -+ for (int i = 0; i < 4096; ++i) { -+ // Copied from NibbleArray#toString -+ final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF); -+ -+ stringBuilder.append(Integer.toHexString(level)); -+ if ((i & 15) == 15) { -+ stringBuilder.append("\n"); -+ } -+ -+ if ((i & 255) == 255) { -+ stringBuilder.append("\n"); -+ } -+ } -+ } else { -+ stringBuilder.append("null"); -+ } -+ -+ return stringBuilder.toString(); -+ } -+ -+ public SaveState getSaveState() { -+ synchronized (this) { -+ final int state = this.stateVisible; -+ final byte[] data = this.storageVisible; -+ if (state == INIT_STATE_NULL) { -+ return null; -+ } -+ if (state == INIT_STATE_UNINIT) { -+ return new SaveState(null, state); -+ } -+ final boolean zero = isAllZero(data); -+ if (zero) { -+ return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null; -+ } else { -+ return new SaveState(data.clone(), state); -+ } -+ } -+ } -+ -+ protected static boolean isAllZero(final byte[] data) { -+ for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) { -+ byte whole = data[i << 4]; -+ -+ for (int k = 1; k < (1 << 4); ++k) { -+ whole |= data[(i << 4) | k]; -+ } -+ -+ if (whole != 0) { -+ return false; -+ } -+ } -+ -+ return true; -+ } -+ -+ // operation type: updating on src, updating on other -+ public void extrudeLower(final SWMRNibbleArray other) { -+ if (other.stateUpdating == INIT_STATE_NULL) { -+ throw new IllegalArgumentException(); -+ } -+ -+ if (other.storageUpdating == null) { -+ this.setUninitialised(); -+ return; -+ } -+ -+ final byte[] src = other.storageUpdating; -+ final byte[] into; -+ -+ if (!this.updatingDirty) { -+ if (this.storageUpdating != null) { -+ into = this.storageUpdating = allocateBytes(); -+ } else { -+ this.storageUpdating = into = allocateBytes(); -+ this.stateUpdating = INIT_STATE_INIT; -+ } -+ this.updatingDirty = true; -+ } else { -+ into = this.storageUpdating; -+ } -+ -+ final int start = 0; -+ final int end = (15 | (15 << 4)) >>> 1; -+ -+ /* x | (z << 4) | (y << 8) */ -+ for (int y = 0; y <= 15; ++y) { -+ System.arraycopy(src, start, into, y << (8 - 1), end - start + 1); -+ } -+ } -+ -+ // operation type: updating -+ public void setFull() { -+ if (this.stateUpdating != INIT_STATE_HIDDEN) { -+ this.stateUpdating = INIT_STATE_INIT; -+ } -+ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1); -+ this.updatingDirty = true; -+ } -+ -+ // operation type: updating -+ public void setZero() { -+ if (this.stateUpdating != INIT_STATE_HIDDEN) { -+ this.stateUpdating = INIT_STATE_INIT; -+ } -+ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0); -+ this.updatingDirty = true; -+ } -+ -+ // operation type: updating -+ public void setNonNull() { -+ if (this.stateUpdating == INIT_STATE_HIDDEN) { -+ this.stateUpdating = INIT_STATE_INIT; -+ return; -+ } -+ if (this.stateUpdating != INIT_STATE_NULL) { -+ return; -+ } -+ this.stateUpdating = INIT_STATE_UNINIT; -+ } -+ -+ // operation type: updating -+ public void setNull() { -+ this.stateUpdating = INIT_STATE_NULL; -+ if (this.updatingDirty && this.storageUpdating != null) { -+ freeBytes(this.storageUpdating); -+ } -+ this.storageUpdating = null; -+ this.updatingDirty = false; -+ } -+ -+ // operation type: updating -+ public void setUninitialised() { -+ this.stateUpdating = INIT_STATE_UNINIT; -+ if (this.storageUpdating != null && this.updatingDirty) { -+ freeBytes(this.storageUpdating); -+ } -+ this.storageUpdating = null; -+ this.updatingDirty = false; -+ } -+ -+ // operation type: updating -+ public void setHidden() { -+ if (this.stateUpdating == INIT_STATE_HIDDEN) { -+ return; -+ } -+ if (this.stateUpdating != INIT_STATE_INIT) { -+ this.setNull(); -+ } else { -+ this.stateUpdating = INIT_STATE_HIDDEN; -+ } -+ } -+ -+ // operation type: updating -+ public boolean isDirty() { -+ return this.stateUpdating != this.stateVisible || this.updatingDirty; -+ } -+ -+ // operation type: updating -+ public boolean isNullNibbleUpdating() { -+ return this.stateUpdating == INIT_STATE_NULL; -+ } -+ -+ // operation type: visible -+ public boolean isNullNibbleVisible() { -+ return this.stateVisible == INIT_STATE_NULL; -+ } -+ -+ // opeartion type: updating -+ public boolean isUninitialisedUpdating() { -+ return this.stateUpdating == INIT_STATE_UNINIT; -+ } -+ -+ // operation type: visible -+ public boolean isUninitialisedVisible() { -+ return this.stateVisible == INIT_STATE_UNINIT; -+ } -+ -+ // operation type: updating -+ public boolean isInitialisedUpdating() { -+ return this.stateUpdating == INIT_STATE_INIT; -+ } -+ -+ // operation type: visible -+ public boolean isInitialisedVisible() { -+ return this.stateVisible == INIT_STATE_INIT; -+ } -+ -+ // operation type: updating -+ public boolean isHiddenUpdating() { -+ return this.stateUpdating == INIT_STATE_HIDDEN; -+ } -+ -+ // operation type: updating -+ public boolean isHiddenVisible() { -+ return this.stateVisible == INIT_STATE_HIDDEN; -+ } -+ -+ // operation type: updating -+ protected void swapUpdatingAndMarkDirty() { -+ if (this.updatingDirty) { -+ return; -+ } -+ -+ if (this.storageUpdating == null) { -+ this.storageUpdating = allocateBytes(); -+ Arrays.fill(this.storageUpdating, (byte)0); -+ } else { -+ System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE); -+ } -+ -+ if (this.stateUpdating != INIT_STATE_HIDDEN) { -+ this.stateUpdating = INIT_STATE_INIT; -+ } -+ this.updatingDirty = true; -+ } -+ -+ // operation type: updating -+ public boolean updateVisible() { -+ if (!this.isDirty()) { -+ return false; -+ } -+ -+ synchronized (this) { -+ if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) { -+ this.storageVisible = null; -+ } else { -+ if (this.storageVisible == null) { -+ this.storageVisible = this.storageUpdating.clone(); -+ } else { -+ if (this.storageUpdating != this.storageVisible) { -+ System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE); -+ } -+ } -+ -+ if (this.storageUpdating != this.storageVisible) { -+ freeBytes(this.storageUpdating); -+ } -+ this.storageUpdating = this.storageVisible; -+ } -+ this.updatingDirty = false; -+ this.stateVisible = this.stateUpdating; -+ } -+ -+ return true; -+ } -+ -+ // operation type: visible -+ public DataLayer toVanillaNibble() { -+ synchronized (this) { -+ switch (this.stateVisible) { -+ case INIT_STATE_HIDDEN: -+ case INIT_STATE_NULL: -+ return null; -+ case INIT_STATE_UNINIT: -+ return new DataLayer(); -+ case INIT_STATE_INIT: -+ return new DataLayer(this.storageVisible.clone()); -+ default: -+ throw new IllegalStateException(); -+ } -+ } -+ } -+ -+ /* x | (z << 4) | (y << 8) */ -+ -+ // operation type: updating -+ public int getUpdating(final int x, final int y, final int z) { -+ return this.getUpdating((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); -+ } -+ -+ // operation type: updating -+ public int getUpdating(final int index) { -+ // indices range from 0 -> 4096 -+ final byte[] bytes = this.storageUpdating; -+ if (bytes == null) { -+ return 0; -+ } -+ final byte value = bytes[index >>> 1]; -+ -+ // if we are an even index, we want lower 4 bits -+ // if we are an odd index, we want upper 4 bits -+ return ((value >>> ((index & 1) << 2)) & 0xF); -+ } -+ -+ // operation type: visible -+ public int getVisible(final int x, final int y, final int z) { -+ return this.getVisible((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); -+ } -+ -+ // operation type: visible -+ public int getVisible(final int index) { -+ // indices range from 0 -> 4096 -+ final byte[] visibleBytes = this.storageVisible; -+ if (visibleBytes == null) { -+ return 0; -+ } -+ final byte value = visibleBytes[index >>> 1]; -+ -+ // if we are an even index, we want lower 4 bits -+ // if we are an odd index, we want upper 4 bits -+ return ((value >>> ((index & 1) << 2)) & 0xF); -+ } -+ -+ // operation type: updating -+ public void set(final int x, final int y, final int z, final int value) { -+ this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value); -+ } -+ -+ // operation type: updating -+ public void set(final int index, final int value) { -+ if (!this.updatingDirty) { -+ this.swapUpdatingAndMarkDirty(); -+ } -+ final int shift = (index & 1) << 2; -+ final int i = index >>> 1; -+ -+ this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift)); -+ } -+ -+ public static final class SaveState { -+ -+ public final byte[] data; -+ public final int state; -+ -+ public SaveState(final byte[] data, final int state) { -+ this.data = data; -+ this.state = state; -+ } -+ } -+} -diff --git a/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<BlockPos> positions) { -+ this.rewriteNibbleCacheForSkylight(atChunk); -+ Arrays.fill(this.nullPropagationCheckCache, false); -+ -+ final BlockGetter world = lightAccess.getLevel(); -+ final int chunkX = atChunk.getPos().x; -+ final int chunkZ = atChunk.getPos().z; -+ final int heightMapOffset = chunkX * -16 + (chunkZ * (-16 * 16)); -+ -+ // setup heightmap for changes -+ for (final BlockPos pos : positions) { -+ final int index = pos.getX() + (pos.getZ() << 4) + heightMapOffset; -+ final int curr = this.heightMapBlockChange[index]; -+ if (pos.getY() > curr) { -+ this.heightMapBlockChange[index] = pos.getY(); -+ } -+ } -+ -+ // note: light sets are delayed while processing skylight source changes due to how -+ // nibbles are initialised, as we want to avoid clobbering nibble values so what when -+ // below nibbles are initialised they aren't reading from partially modified nibbles -+ -+ // now we can recalculate the sources for the changed columns -+ for (int index = 0; index < (16 * 16); ++index) { -+ final int maxY = this.heightMapBlockChange[index]; -+ if (maxY == Integer.MIN_VALUE) { -+ // not changed -+ continue; -+ } -+ this.heightMapBlockChange[index] = Integer.MIN_VALUE; // restore default for next caller -+ -+ final int columnX = (index & 15) | (chunkX << 4); -+ final int columnZ = (index >>> 4) | (chunkZ << 4); -+ -+ // try and propagate from the above y -+ // delay light set until after processing all sources to setup -+ final int maxPropagationY = this.tryPropagateSkylight(world, columnX, maxY, columnZ, true, true); -+ -+ // maxPropagationY is now the highest block that could not be propagated to -+ -+ // remove all sources below that are 15 -+ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; -+ final int encodeOffset = this.coordinateOffset; -+ -+ if (this.getLightLevelExtruded(columnX, maxPropagationY, columnZ) == 15) { -+ // ensure section is checked -+ this.checkNullSection(columnX >> 4, maxPropagationY >> 4, columnZ >> 4, true); -+ -+ for (int currY = maxPropagationY; currY >= (this.minLightSection << 4); --currY) { -+ if ((currY & 15) == 15) { -+ // ensure section is checked -+ this.checkNullSection(columnX >> 4, (currY >> 4), columnZ >> 4, true); -+ } -+ -+ // ensure section below is always checked -+ final SWMRNibbleArray nibble = this.getNibbleFromCache(columnX >> 4, currY >> 4, columnZ >> 4); -+ if (nibble == null) { -+ // advance currY to the the top of the section below -+ currY = (currY) & (~15); -+ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually -+ // end up there -+ continue; -+ } -+ -+ if (nibble.getUpdating(columnX, currY, columnZ) != 15) { -+ break; -+ } -+ -+ // delay light set until after processing all sources to setup -+ this.appendToDecreaseQueue( -+ ((columnX + (columnZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | (15L << (6 + 6 + 16)) -+ | (propagateDirection << (6 + 6 + 16 + 4)) -+ // do not set transparent blocks for the same reason we don't in the checkBlock method -+ ); -+ } -+ } -+ } -+ -+ // delayed light sets are processed here, and must be processed before checkBlock as checkBlock reads -+ // immediate light value -+ this.processDelayedIncreases(); -+ this.processDelayedDecreases(); -+ -+ for (final BlockPos pos : positions) { -+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); -+ } -+ -+ this.performLightDecrease(lightAccess); -+ } -+ -+ protected final int[] heightMapGen = new int[32 * 32]; -+ -+ @Override -+ protected void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { -+ this.rewriteNibbleCacheForSkylight(chunk); -+ Arrays.fill(this.nullPropagationCheckCache, false); -+ -+ final BlockGetter world = lightAccess.getLevel(); -+ final ChunkPos chunkPos = chunk.getPos(); -+ final int chunkX = chunkPos.x; -+ final int chunkZ = chunkPos.z; -+ -+ final LevelChunkSection[] sections = chunk.getSections(); -+ -+ int highestNonEmptySection = this.maxSection; -+ while (highestNonEmptySection == (this.minSection - 1) || -+ sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].hasOnlyAir()) { -+ this.checkNullSection(chunkX, highestNonEmptySection, chunkZ, false); -+ // try propagate FULL to neighbours -+ -+ // check neighbours to see if we need to propagate into them -+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { -+ final int neighbourX = chunkX + direction.x; -+ final int neighbourZ = chunkZ + direction.z; -+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(neighbourX, highestNonEmptySection, neighbourZ); -+ if (neighbourNibble == null) { -+ // unloaded neighbour -+ // most of the time we fall here -+ continue; -+ } -+ -+ // it looks like we need to propagate into the neighbour -+ -+ final int incX; -+ final int incZ; -+ final int startX; -+ final int startZ; -+ -+ if (direction.x != 0) { -+ // x direction -+ incX = 0; -+ incZ = 1; -+ -+ if (direction.x < 0) { -+ // negative -+ startX = chunkX << 4; -+ } else { -+ startX = chunkX << 4 | 15; -+ } -+ startZ = chunkZ << 4; -+ } else { -+ // z direction -+ incX = 1; -+ incZ = 0; -+ -+ if (direction.z < 0) { -+ // negative -+ startZ = chunkZ << 4; -+ } else { -+ startZ = chunkZ << 4 | 15; -+ } -+ startX = chunkX << 4; -+ } -+ -+ final int encodeOffset = this.coordinateOffset; -+ final long propagateDirection = 1L << direction.ordinal(); // we only want to check in this direction -+ -+ for (int currY = highestNonEmptySection << 4, maxY = currY | 15; currY <= maxY; ++currY) { -+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { -+ this.appendToIncreaseQueue( -+ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | (15L << (6 + 6 + 16)) // we know we're at full lit here -+ | (propagateDirection << (6 + 6 + 16 + 4)) -+ // no transparent flag, we know for a fact there are no blocks here that could be directionally transparent (as the section is EMPTY) -+ ); -+ } -+ } -+ } -+ -+ if (highestNonEmptySection-- == (this.minSection - 1)) { -+ break; -+ } -+ } -+ -+ if (highestNonEmptySection >= this.minSection) { -+ // fill out our other sources -+ final int minX = chunkPos.x << 4; -+ final int maxX = chunkPos.x << 4 | 15; -+ final int minZ = chunkPos.z << 4; -+ final int maxZ = chunkPos.z << 4 | 15; -+ final int startY = highestNonEmptySection << 4 | 15; -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ this.tryPropagateSkylight(world, currX, startY + 1, currZ, false, false); -+ } -+ } -+ } // else: apparently the chunk is empty -+ -+ if (needsEdgeChecks) { -+ // not required to propagate here, but this will reduce the hit of the edge checks -+ this.performLightIncrease(lightAccess); -+ -+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { -+ this.checkNullSection(chunkX, y, chunkZ, false); -+ } -+ // no need to rewrite the nibble cache again -+ super.checkChunkEdges(lightAccess, chunk, this.minLightSection, highestNonEmptySection); -+ } else { -+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { -+ this.checkNullSection(chunkX, y, chunkZ, false); -+ } -+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, highestNonEmptySection); -+ -+ this.performLightIncrease(lightAccess); -+ } -+ } -+ -+ protected final void processDelayedIncreases() { -+ // copied from performLightIncrease -+ final long[] queue = this.increaseQueue; -+ final int decodeOffsetX = -this.encodeOffsetX; -+ final int decodeOffsetY = -this.encodeOffsetY; -+ final int decodeOffsetZ = -this.encodeOffsetZ; -+ -+ for (int i = 0, len = this.increaseQueueInitialLength; i < len; ++i) { -+ final long queueValue = queue[i]; -+ -+ final int posX = ((int)queueValue & 63) + decodeOffsetX; -+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; -+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; -+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); -+ -+ this.setLightLevel(posX, posY, posZ, propagatedLightLevel); -+ } -+ } -+ -+ protected final void processDelayedDecreases() { -+ // copied from performLightDecrease -+ final long[] queue = this.decreaseQueue; -+ final int decodeOffsetX = -this.encodeOffsetX; -+ final int decodeOffsetY = -this.encodeOffsetY; -+ final int decodeOffsetZ = -this.encodeOffsetZ; -+ -+ for (int i = 0, len = this.decreaseQueueInitialLength; i < len; ++i) { -+ final long queueValue = queue[i]; -+ -+ final int posX = ((int)queueValue & 63) + decodeOffsetX; -+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; -+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; -+ -+ this.setLightLevel(posX, posY, posZ, 0); -+ } -+ } -+ -+ // delaying the light set is useful for block changes since they need to worry about initialising nibblearrays -+ // while also queueing light at the same time (initialising nibblearrays might depend on nibbles above, so -+ // clobbering the light values will result in broken propagation) -+ protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ, -+ final boolean extrudeInitialised, final boolean delayLightSet) { -+ final int encodeOffset = this.coordinateOffset; -+ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards. -+ -+ if (this.getLightLevelExtruded(worldX, startY + 1, worldZ) != 15) { -+ return startY; -+ } -+ -+ // ensure this section is always checked -+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised); -+ -+ BlockState above = this.getBlockState(worldX, startY + 1, worldZ); -+ -+ for (;startY >= (this.minLightSection << 4); --startY) { -+ if ((startY & 15) == 15) { -+ // ensure this section is always checked -+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised); -+ } -+ final BlockState current = this.getBlockState(worldX, startY, worldZ); -+ -+ final VoxelShape fromShape; -+ if (((StarlightAbstractBlockState)above).starlight$isConditionallyFullOpaque()) { -+ fromShape = above.getFaceOcclusionShape(AxisDirection.NEGATIVE_Y.nms); -+ if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { -+ // above wont let us propagate -+ break; -+ } -+ } else { -+ fromShape = Shapes.empty(); -+ } -+ -+ // does light propagate from the top down? -+ long flags = 0L; -+ if (((StarlightAbstractBlockState)current).starlight$isConditionallyFullOpaque()) { -+ final VoxelShape cullingFace = current.getFaceOcclusionShape(AxisDirection.POSITIVE_Y.nms); -+ -+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { -+ // can't propagate here, we're done on this column. -+ break; -+ } -+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; -+ } -+ -+ final int opacity = current.getLightBlock(); -+ if (opacity > 0) { -+ // let the queued value (if any) handle it from here. -+ break; -+ } -+ -+ // light set delayed until we determine if this nibble section is null -+ this.appendToIncreaseQueue( -+ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | (15L << (6 + 6 + 16)) // we know we're at full lit here -+ | (propagateDirection << (6 + 6 + 16 + 4)) -+ | flags -+ ); -+ -+ above = current; -+ -+ if (this.getNibbleFromCache(worldX >> 4, startY >> 4, worldZ >> 4) == null) { -+ // we skip empty sections here, as this is just an easy way of making sure the above block -+ // can propagate through air. -+ -+ // nothing can propagate in null sections, remove the queue entry for it -+ --this.increaseQueueInitialLength; -+ -+ // advance currY to the the top of the section below -+ startY = (startY) & (~15); -+ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually -+ // end up there -+ -+ // make sure this is marked as AIR -+ above = AIR_BLOCK_STATE; -+ } else if (!delayLightSet) { -+ this.setLightLevel(worldX, startY, worldZ, 15); -+ } -+ } -+ -+ return startY; -+ } -+} -diff --git a/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<BlockPos> positions, final Boolean[] changedSections) { -+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); -+ try { -+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); -+ if (chunk == null) { -+ return; -+ } -+ if (changedSections != null) { -+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, changedSections, false); -+ if (ret != null) { -+ this.setEmptinessMap(chunk, ret); -+ } -+ } -+ if (!positions.isEmpty()) { -+ this.propagateBlockChanges(lightAccess, chunk, positions); -+ } -+ this.updateVisible(lightAccess); -+ } finally { -+ this.destroyCaches(); -+ } -+ } -+ -+ // subclasses should not initialise caches, as this will always be done by the super call -+ // subclasses should not invoke updateVisible, as this will always be done by the super call -+ protected abstract void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions); -+ -+ protected abstract void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ); -+ -+ // if ret > expect, then the real value is at least ret (early returns if ret > expect, rather than calculating actual) -+ // if ret == expect, then expect is the correct light value for pos -+ // if ret < expect, then ret is the real light value -+ protected abstract int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, -+ final int expect); -+ -+ protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16]; -+ protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16]; -+ -+ protected void checkChunkEdge(final LightChunkGetter lightAccess, final ChunkAccess chunk, -+ final int chunkX, final int chunkY, final int chunkZ) { -+ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); -+ if (currNibble == null) { -+ return; -+ } -+ -+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { -+ final int neighbourOffX = direction.x; -+ final int neighbourOffZ = direction.z; -+ -+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX, -+ chunkY, chunkZ + neighbourOffZ); -+ -+ if (neighbourNibble == null) { -+ continue; -+ } -+ -+ if (!currNibble.isInitialisedUpdating() && !neighbourNibble.isInitialisedUpdating()) { -+ // both are zero, nothing to check. -+ continue; -+ } -+ -+ // this chunk -+ final int incX; -+ final int incZ; -+ final int startX; -+ final int startZ; -+ -+ if (neighbourOffX != 0) { -+ // x direction -+ incX = 0; -+ incZ = 1; -+ -+ if (direction.x < 0) { -+ // negative -+ startX = chunkX << 4; -+ } else { -+ startX = chunkX << 4 | 15; -+ } -+ startZ = chunkZ << 4; -+ } else { -+ // z direction -+ incX = 1; -+ incZ = 0; -+ -+ if (neighbourOffZ < 0) { -+ // negative -+ startZ = chunkZ << 4; -+ } else { -+ startZ = chunkZ << 4 | 15; -+ } -+ startX = chunkX << 4; -+ } -+ -+ int centerDelayedChecks = 0; -+ int neighbourDelayedChecks = 0; -+ for (int currY = chunkY << 4, maxY = currY | 15; currY <= maxY; ++currY) { -+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { -+ final int neighbourX = currX + neighbourOffX; -+ final int neighbourZ = currZ + neighbourOffZ; -+ -+ final int currentIndex = (currX & 15) | -+ ((currZ & 15)) << 4 | -+ ((currY & 15) << 8); -+ final int currentLevel = currNibble.getUpdating(currentIndex); -+ -+ final int neighbourIndex = -+ (neighbourX & 15) | -+ ((neighbourZ & 15)) << 4 | -+ ((currY & 15) << 8); -+ final int neighbourLevel = neighbourNibble.getUpdating(neighbourIndex); -+ -+ // the checks are delayed because the checkBlock method clobbers light values - which then -+ // affect later calculate light value operations. While they don't affect it in a behaviourly significant -+ // way, they do have a negative performance impact due to simply queueing more values -+ -+ if (this.calculateLightValue(lightAccess, currX, currY, currZ, currentLevel) != currentLevel) { -+ this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex; -+ } -+ -+ if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel) != neighbourLevel) { -+ this.chunkCheckDelayedUpdatesNeighbour[neighbourDelayedChecks++] = neighbourIndex; -+ } -+ } -+ } -+ -+ final int currentChunkOffX = chunkX << 4; -+ final int currentChunkOffZ = chunkZ << 4; -+ final int neighbourChunkOffX = (chunkX + direction.x) << 4; -+ final int neighbourChunkOffZ = (chunkZ + direction.z) << 4; -+ final int chunkOffY = chunkY << 4; -+ for (int i = 0, len = Math.max(centerDelayedChecks, neighbourDelayedChecks); i < len; ++i) { -+ // try to queue neighbouring data together -+ // index = x | (z << 4) | (y << 8) -+ if (i < centerDelayedChecks) { -+ final int value = this.chunkCheckDelayedUpdatesCenter[i]; -+ this.checkBlock(lightAccess, currentChunkOffX | (value & 15), -+ chunkOffY | (value >>> 8), -+ currentChunkOffZ | ((value >>> 4) & 0xF)); -+ } -+ if (i < neighbourDelayedChecks) { -+ final int value = this.chunkCheckDelayedUpdatesNeighbour[i]; -+ this.checkBlock(lightAccess, neighbourChunkOffX | (value & 15), -+ chunkOffY | (value >>> 8), -+ neighbourChunkOffZ | ((value >>> 4) & 0xF)); -+ } -+ } -+ } -+ } -+ -+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) { -+ final ChunkPos chunkPos = chunk.getPos(); -+ final int chunkX = chunkPos.x; -+ final int chunkZ = chunkPos.z; -+ -+ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) { -+ this.checkChunkEdge(lightAccess, chunk, chunkX, iterator.nextShort(), chunkZ); -+ } -+ -+ this.performLightDecrease(lightAccess); -+ } -+ -+ // subclasses should not initialise caches, as this will always be done by the super call -+ // subclasses should not invoke updateVisible, as this will always be done by the super call -+ // verifies that light levels on this chunks edges are consistent with this chunk's neighbours -+ // edges. if they are not, they are decreased (effectively performing the logic in checkBlock). -+ // This does not resolve skylight source problems. -+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { -+ final ChunkPos chunkPos = chunk.getPos(); -+ final int chunkX = chunkPos.x; -+ final int chunkZ = chunkPos.z; -+ -+ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) { -+ this.checkChunkEdge(lightAccess, chunk, chunkX, currSectionY, chunkZ); -+ } -+ -+ this.performLightDecrease(lightAccess); -+ } -+ -+ // pulls light from neighbours, and adds them into the increase queue. does not actually propagate. -+ protected final void propagateNeighbourLevels(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { -+ final ChunkPos chunkPos = chunk.getPos(); -+ final int chunkX = chunkPos.x; -+ final int chunkZ = chunkPos.z; -+ -+ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) { -+ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, currSectionY, chunkZ); -+ if (currNibble == null) { -+ continue; -+ } -+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { -+ final int neighbourOffX = direction.x; -+ final int neighbourOffZ = direction.z; -+ -+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX, -+ currSectionY, chunkZ + neighbourOffZ); -+ -+ if (neighbourNibble == null || !neighbourNibble.isInitialisedUpdating()) { -+ // can't pull from 0 -+ continue; -+ } -+ -+ // neighbour chunk -+ final int incX; -+ final int incZ; -+ final int startX; -+ final int startZ; -+ -+ if (neighbourOffX != 0) { -+ // x direction -+ incX = 0; -+ incZ = 1; -+ -+ if (direction.x < 0) { -+ // negative -+ startX = (chunkX << 4) - 1; -+ } else { -+ startX = (chunkX << 4) + 16; -+ } -+ startZ = chunkZ << 4; -+ } else { -+ // z direction -+ incX = 1; -+ incZ = 0; -+ -+ if (neighbourOffZ < 0) { -+ // negative -+ startZ = (chunkZ << 4) - 1; -+ } else { -+ startZ = (chunkZ << 4) + 16; -+ } -+ startX = chunkX << 4; -+ } -+ -+ final long propagateDirection = 1L << direction.getOpposite().ordinal(); // we only want to check in this direction towards this chunk -+ final int encodeOffset = this.coordinateOffset; -+ -+ for (int currY = currSectionY << 4, maxY = currY | 15; currY <= maxY; ++currY) { -+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { -+ final int level = neighbourNibble.getUpdating( -+ (currX & 15) -+ | ((currZ & 15) << 4) -+ | ((currY & 15) << 8) -+ ); -+ -+ if (level <= 1) { -+ // nothing to propagate -+ continue; -+ } -+ -+ this.appendToIncreaseQueue( -+ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((level & 0xFL) << (6 + 6 + 16)) -+ | (propagateDirection << (6 + 6 + 16 + 4)) -+ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the current block is transparent, must check. -+ ); -+ } -+ } -+ } -+ } -+ } -+ -+ public static Boolean[] getEmptySectionsForChunk(final ChunkAccess chunk) { -+ final LevelChunkSection[] sections = chunk.getSections(); -+ final Boolean[] ret = new Boolean[sections.length]; -+ -+ for (int i = 0; i < sections.length; ++i) { -+ if (sections[i] == null || sections[i].hasOnlyAir()) { -+ ret[i] = Boolean.TRUE; -+ } else { -+ ret[i] = Boolean.FALSE; -+ } -+ } -+ -+ return ret; -+ } -+ -+ public final void forceHandleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptinessChanges) { -+ final int chunkX = chunk.getPos().x; -+ final int chunkZ = chunk.getPos().z; -+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); -+ try { -+ // force current chunk into cache -+ this.setChunkInCache(chunkX, chunkZ, chunk); -+ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections()); -+ this.setNibblesForChunkInCache(chunkX, chunkZ, this.getNibblesOnChunk(chunk)); -+ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk)); -+ -+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false); -+ if (ret != null) { -+ this.setEmptinessMap(chunk, ret); -+ } -+ this.updateVisible(lightAccess); -+ } finally { -+ this.destroyCaches(); -+ } -+ } -+ -+ public final void handleEmptySectionChanges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, -+ final Boolean[] emptinessChanges) { -+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); -+ try { -+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); -+ if (chunk == null) { -+ return; -+ } -+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false); -+ if (ret != null) { -+ this.setEmptinessMap(chunk, ret); -+ } -+ this.updateVisible(lightAccess); -+ } finally { -+ this.destroyCaches(); -+ } -+ } -+ -+ protected abstract void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles); -+ -+ protected abstract void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ); -+ -+ // subclasses should not initialise caches, as this will always be done by the super call -+ // subclasses should not invoke updateVisible, as this will always be done by the super call -+ // subclasses are guaranteed that this is always called before a changed block set -+ // newChunk specifies whether the changes describe a "first load" of a chunk or changes to existing, already loaded chunks -+ // rets non-null when the emptiness map changed and needs to be updated -+ protected final boolean[] handleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, -+ final Boolean[] emptinessChanges, final boolean unlit) { -+ final Level world = (Level)lightAccess.getLevel(); -+ final int chunkX = chunk.getPos().x; -+ final int chunkZ = chunk.getPos().z; -+ -+ boolean[] chunkEmptinessMap = this.getEmptinessMap(chunkX, chunkZ); -+ boolean[] ret = null; -+ final boolean needsInit = unlit || chunkEmptinessMap == null; -+ if (needsInit) { -+ this.setEmptinessMapCache(chunkX, chunkZ, ret = chunkEmptinessMap = new boolean[WorldUtil.getTotalSections(world)]); -+ } -+ -+ // update emptiness map -+ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) { -+ Boolean valueBoxed = emptinessChanges[sectionIndex]; -+ if (valueBoxed == null) { -+ if (!needsInit) { -+ continue; -+ } -+ final LevelChunkSection section = this.getChunkSection(chunkX, sectionIndex + this.minSection, chunkZ); -+ emptinessChanges[sectionIndex] = valueBoxed = section == null || section.hasOnlyAir() ? Boolean.TRUE : Boolean.FALSE; -+ } -+ chunkEmptinessMap[sectionIndex] = valueBoxed.booleanValue(); -+ } -+ -+ // now init neighbour nibbles -+ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) { -+ final Boolean valueBoxed = emptinessChanges[sectionIndex]; -+ final int sectionY = sectionIndex + this.minSection; -+ if (valueBoxed == null) { -+ continue; -+ } -+ -+ final boolean empty = valueBoxed.booleanValue(); -+ -+ if (empty) { -+ continue; -+ } -+ -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ // if we're not empty, we also need to initialise nibbles -+ // note: if we're unlit, we absolutely do not want to extrude, as light data isn't set up -+ final boolean extrude = (dx | dz) != 0 || !unlit; -+ for (int dy = 1; dy >= -1; --dy) { -+ this.initNibble(dx + chunkX, dy + sectionY, dz + chunkZ, extrude, false); -+ } -+ } -+ } -+ } -+ -+ // check for de-init and lazy-init -+ // lazy init is when chunks are being lit, so at the time they weren't loaded when their neighbours were running -+ // init checks. -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ // does this neighbour have 1 radius loaded? -+ boolean neighboursLoaded = true; -+ neighbour_loaded_search: -+ for (int dz2 = -1; dz2 <= 1; ++dz2) { -+ for (int dx2 = -1; dx2 <= 1; ++dx2) { -+ if (this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ) == null) { -+ neighboursLoaded = false; -+ break neighbour_loaded_search; -+ } -+ } -+ } -+ -+ for (int sectionY = this.maxLightSection; sectionY >= this.minLightSection; --sectionY) { -+ // check neighbours to see if we need to de-init this one -+ boolean allEmpty = true; -+ neighbour_search: -+ for (int dy2 = -1; dy2 <= 1; ++dy2) { -+ for (int dz2 = -1; dz2 <= 1; ++dz2) { -+ for (int dx2 = -1; dx2 <= 1; ++dx2) { -+ final int y = sectionY + dy2; -+ if (y < this.minSection || y > this.maxSection) { -+ // empty -+ continue; -+ } -+ final boolean[] emptinessMap = this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ); -+ if (emptinessMap != null) { -+ if (!emptinessMap[y - this.minSection]) { -+ allEmpty = false; -+ break neighbour_search; -+ } -+ } else { -+ final LevelChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ); -+ if (section != null && !section.hasOnlyAir()) { -+ allEmpty = false; -+ break neighbour_search; -+ } -+ } -+ } -+ } -+ } -+ -+ if (allEmpty & neighboursLoaded) { -+ // can only de-init when neighbours are loaded -+ // de-init is fine to delay, as de-init is just an optimisation - it's not required for lighting -+ // to be correct -+ -+ // all were empty, so de-init -+ this.setNibbleNull(dx + chunkX, sectionY, dz + chunkZ); -+ } else if (!allEmpty) { -+ // must init -+ final boolean extrude = (dx | dz) != 0 || !unlit; -+ this.initNibble(dx + chunkX, sectionY, dz + chunkZ, extrude, false); -+ } -+ } -+ } -+ } -+ -+ return ret; -+ } -+ -+ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ) { -+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); -+ try { -+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); -+ if (chunk == null) { -+ return; -+ } -+ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); -+ this.updateVisible(lightAccess); -+ } finally { -+ this.destroyCaches(); -+ } -+ } -+ -+ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) { -+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); -+ try { -+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); -+ if (chunk == null) { -+ return; -+ } -+ this.checkChunkEdges(lightAccess, chunk, sections); -+ this.updateVisible(lightAccess); -+ } finally { -+ this.destroyCaches(); -+ } -+ } -+ -+ // subclasses should not initialise caches, as this will always be done by the super call -+ // subclasses should not invoke updateVisible, as this will always be done by the super call -+ // needsEdgeChecks applies when possibly loading vanilla data, which means we need to validate the current -+ // chunks light values with respect to neighbours -+ // subclasses should note that the emptiness changes are propagated BEFORE this is called, so this function -+ // does not need to detect empty chunks itself (and it should do no handling for them either!) -+ protected abstract void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks); -+ -+ public final void light(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptySections) { -+ final int chunkX = chunk.getPos().x; -+ final int chunkZ = chunk.getPos().z; -+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); -+ -+ try { -+ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.maxLightSection - this.minLightSection + 1); -+ // force current chunk into cache -+ this.setChunkInCache(chunkX, chunkZ, chunk); -+ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections()); -+ this.setNibblesForChunkInCache(chunkX, chunkZ, nibbles); -+ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk)); -+ -+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptySections, true); -+ if (ret != null) { -+ this.setEmptinessMap(chunk, ret); -+ } -+ this.lightChunk(lightAccess, chunk, true); -+ this.setNibbles(chunk, nibbles); -+ this.updateVisible(lightAccess); -+ } finally { -+ this.destroyCaches(); -+ } -+ } -+ -+ public final void relightChunks(final LightChunkGetter lightAccess, final Set<ChunkPos> chunks, -+ final Consumer<ChunkPos> chunkLightCallback, final IntConsumer onComplete) { -+ // it's recommended for maximum performance that the set is ordered according to a BFS from the center of -+ // the region of chunks to relight -+ // it's required that tickets are added for each chunk to keep them loaded -+ final Long2ObjectOpenHashMap<SWMRNibbleArray[]> nibblesByChunk = new Long2ObjectOpenHashMap<>(); -+ final Long2ObjectOpenHashMap<boolean[]> emptinessMapByChunk = new Long2ObjectOpenHashMap<>(); -+ -+ final int[] neighbourLightOrder = new int[] { -+ // d = 0 -+ 0, 0, -+ // d = 1 -+ -1, 0, -+ 0, -1, -+ 1, 0, -+ 0, 1, -+ // d = 2 -+ -1, 1, -+ 1, 1, -+ -1, -1, -+ 1, -1, -+ }; -+ -+ int lightCalls = 0; -+ -+ for (final ChunkPos chunkPos : chunks) { -+ final int chunkX = chunkPos.x; -+ final int chunkZ = chunkPos.z; -+ final ChunkAccess chunk = (ChunkAccess)lightAccess.getChunkForLighting(chunkX, chunkZ); -+ if (chunk == null || !this.canUseChunk(chunk)) { -+ throw new IllegalStateException(); -+ } -+ -+ for (int i = 0, len = neighbourLightOrder.length; i < len; i += 2) { -+ final int dx = neighbourLightOrder[i]; -+ final int dz = neighbourLightOrder[i + 1]; -+ final int neighbourX = dx + chunkX; -+ final int neighbourZ = dz + chunkZ; -+ -+ final ChunkAccess neighbour = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX, neighbourZ); -+ if (neighbour == null || !this.canUseChunk(neighbour)) { -+ continue; -+ } -+ -+ if (nibblesByChunk.get(CoordinateUtils.getChunkKey(neighbourX, neighbourZ)) != null) { -+ // lit already called for neighbour, no need to light it now -+ continue; -+ } -+ -+ // light neighbour chunk -+ this.setupEncodeOffset(neighbourX * 16 + 7, 128, neighbourZ * 16 + 7); -+ try { -+ // insert all neighbouring chunks for this neighbour that we have data for -+ for (int dz2 = -1; dz2 <= 1; ++dz2) { -+ for (int dx2 = -1; dx2 <= 1; ++dx2) { -+ final int neighbourX2 = neighbourX + dx2; -+ final int neighbourZ2 = neighbourZ + dz2; -+ final long key = CoordinateUtils.getChunkKey(neighbourX2, neighbourZ2); -+ final ChunkAccess neighbour2 = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX2, neighbourZ2); -+ if (neighbour2 == null || !this.canUseChunk(neighbour2)) { -+ continue; -+ } -+ -+ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(key); -+ if (nibbles == null) { -+ // we haven't lit this chunk -+ continue; -+ } -+ -+ this.setChunkInCache(neighbourX2, neighbourZ2, neighbour2); -+ this.setBlocksForChunkInCache(neighbourX2, neighbourZ2, neighbour2.getSections()); -+ this.setNibblesForChunkInCache(neighbourX2, neighbourZ2, nibbles); -+ this.setEmptinessMapCache(neighbourX2, neighbourZ2, emptinessMapByChunk.get(key)); -+ } -+ } -+ -+ final long key = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); -+ -+ // now insert the neighbour chunk and light it -+ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.world); -+ nibblesByChunk.put(key, nibbles); -+ -+ this.setChunkInCache(neighbourX, neighbourZ, neighbour); -+ this.setBlocksForChunkInCache(neighbourX, neighbourZ, neighbour.getSections()); -+ this.setNibblesForChunkInCache(neighbourX, neighbourZ, nibbles); -+ -+ final boolean[] neighbourEmptiness = this.handleEmptySectionChanges(lightAccess, neighbour, getEmptySectionsForChunk(neighbour), true); -+ emptinessMapByChunk.put(key, neighbourEmptiness); -+ if (chunks.contains(new ChunkPos(neighbourX, neighbourZ))) { -+ this.setEmptinessMap(neighbour, neighbourEmptiness); -+ } -+ -+ this.lightChunk(lightAccess, neighbour, false); -+ } finally { -+ this.destroyCaches(); -+ } -+ } -+ -+ // done lighting all neighbours, so the chunk is now fully lit -+ -+ // make sure nibbles are fully updated before calling back -+ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ for (final SWMRNibbleArray nibble : nibbles) { -+ nibble.updateVisible(); -+ } -+ -+ this.setNibbles(chunk, nibbles); -+ -+ for (int y = this.minLightSection; y <= this.maxLightSection; ++y) { -+ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkZ)); -+ } -+ -+ // now do callback -+ if (chunkLightCallback != null) { -+ chunkLightCallback.accept(chunkPos); -+ } -+ ++lightCalls; -+ } -+ -+ if (onComplete != null) { -+ onComplete.accept(lightCalls); -+ } -+ } -+ -+ // contains: -+ // lower (6 + 6 + 16) = 28 bits: encoded coordinate position (x | (z << 6) | (y << (6 + 6)))) -+ // next 4 bits: propagated light level (0, 15] -+ // next 6 bits: propagation direction bitset -+ // next 24 bits: unused -+ // last 3 bits: state flags -+ // state flags: -+ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading light -+ // updates for block sources -+ protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 2; -+ // whether the propagation needs to check if its current level is equal to the expected level -+ // used only in increase propagation -+ protected static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 1; -+ // whether the propagation needs to consider if its block is conditionally transparent -+ protected static final long FLAG_HAS_SIDED_TRANSPARENT_BLOCKS = Long.MIN_VALUE; -+ -+ protected long[] increaseQueue = new long[16 * 16 * 16]; -+ protected int increaseQueueInitialLength; -+ protected long[] decreaseQueue = new long[16 * 16 * 16]; -+ protected int decreaseQueueInitialLength; -+ -+ protected final long[] resizeIncreaseQueue() { -+ return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2); -+ } -+ -+ protected final long[] resizeDecreaseQueue() { -+ return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2); -+ } -+ -+ protected final void appendToIncreaseQueue(final long value) { -+ final int idx = this.increaseQueueInitialLength++; -+ long[] queue = this.increaseQueue; -+ if (idx >= queue.length) { -+ queue = this.resizeIncreaseQueue(); -+ queue[idx] = value; -+ } else { -+ queue[idx] = value; -+ } -+ } -+ -+ protected final void appendToDecreaseQueue(final long value) { -+ final int idx = this.decreaseQueueInitialLength++; -+ long[] queue = this.decreaseQueue; -+ if (idx >= queue.length) { -+ queue = this.resizeDecreaseQueue(); -+ queue[idx] = value; -+ } else { -+ queue[idx] = value; -+ } -+ } -+ -+ protected static final AxisDirection[][] OLD_CHECK_DIRECTIONS = new AxisDirection[1 << 6][]; -+ protected static final int ALL_DIRECTIONS_BITSET = (1 << 6) - 1; -+ static { -+ for (int i = 0; i < OLD_CHECK_DIRECTIONS.length; ++i) { -+ final List<AxisDirection> directions = new ArrayList<>(); -+ for (int bitset = i, len = Integer.bitCount(i), index = 0; index < len; ++index, bitset ^= IntegerUtil.getTrailingBit(bitset)) { -+ directions.add(AXIS_DIRECTIONS[IntegerUtil.trailingZeros(bitset)]); -+ } -+ OLD_CHECK_DIRECTIONS[i] = directions.toArray(new AxisDirection[0]); -+ } -+ } -+ -+ protected final void performLightIncrease(final LightChunkGetter lightAccess) { -+ final BlockGetter world = lightAccess.getLevel(); -+ long[] queue = this.increaseQueue; -+ int queueReadIndex = 0; -+ int queueLength = this.increaseQueueInitialLength; -+ this.increaseQueueInitialLength = 0; -+ final int decodeOffsetX = -this.encodeOffsetX; -+ final int decodeOffsetY = -this.encodeOffsetY; -+ final int decodeOffsetZ = -this.encodeOffsetZ; -+ final int encodeOffset = this.coordinateOffset; -+ final int sectionOffset = this.chunkSectionIndexOffset; -+ -+ while (queueReadIndex < queueLength) { -+ final long queueValue = queue[queueReadIndex++]; -+ -+ final int posX = ((int)queueValue & 63) + decodeOffsetX; -+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; -+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; -+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xFL); -+ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63L)]; -+ -+ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) { -+ if (this.getLightLevel(posX, posY, posZ) != propagatedLightLevel) { -+ // not at the level we expect, so something changed. -+ continue; -+ } -+ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) { -+ // these are used to restore block sources after a propagation decrease -+ this.setLightLevel(posX, posY, posZ, propagatedLightLevel); -+ } -+ -+ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) { -+ // we don't need to worry about our state here. -+ for (final AxisDirection propagate : checkDirections) { -+ final int offX = posX + propagate.x; -+ final int offY = posY + propagate.y; -+ final int offZ = posZ + propagate.z; -+ -+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; -+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); -+ -+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; -+ final int currentLevel; -+ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) { -+ continue; // already at the level we want or unloaded -+ } -+ -+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex); -+ if (blockState == null) { -+ continue; -+ } -+ long flags = 0; -+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { -+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); -+ -+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { -+ continue; -+ } -+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; -+ } -+ -+ final int opacity = blockState.getLightBlock(); -+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity); -+ if (targetLevel <= currentLevel) { -+ continue; -+ } -+ -+ currentNibble.set(localIndex, targetLevel); -+ this.postLightUpdate(offX, offY, offZ); -+ -+ if (targetLevel > 1) { -+ if (queueLength >= queue.length) { -+ queue = this.resizeIncreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((targetLevel & 0xFL) << (6 + 6 + 16)) -+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) -+ | (flags); -+ } -+ continue; -+ } -+ } else { -+ // we actually need to worry about our state here -+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ); -+ for (final AxisDirection propagate : checkDirections) { -+ final int offX = posX + propagate.x; -+ final int offY = posY + propagate.y; -+ final int offZ = posZ + propagate.z; -+ -+ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(propagate.nms) : Shapes.empty(); -+ -+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { -+ continue; -+ } -+ -+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; -+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); -+ -+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; -+ final int currentLevel; -+ -+ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) { -+ continue; // already at the level we want -+ } -+ -+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex); -+ if (blockState == null) { -+ continue; -+ } -+ long flags = 0; -+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { -+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); -+ -+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { -+ continue; -+ } -+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; -+ } -+ -+ final int opacity = blockState.getLightBlock(); -+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity); -+ if (targetLevel <= currentLevel) { -+ continue; -+ } -+ -+ currentNibble.set(localIndex, targetLevel); -+ this.postLightUpdate(offX, offY, offZ); -+ -+ if (targetLevel > 1) { -+ if (queueLength >= queue.length) { -+ queue = this.resizeIncreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((targetLevel & 0xFL) << (6 + 6 + 16)) -+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) -+ | (flags); -+ } -+ continue; -+ } -+ } -+ } -+ } -+ -+ protected final void performLightDecrease(final LightChunkGetter lightAccess) { -+ final BlockGetter world = lightAccess.getLevel(); -+ long[] queue = this.decreaseQueue; -+ long[] increaseQueue = this.increaseQueue; -+ int queueReadIndex = 0; -+ int queueLength = this.decreaseQueueInitialLength; -+ this.decreaseQueueInitialLength = 0; -+ int increaseQueueLength = this.increaseQueueInitialLength; -+ final int decodeOffsetX = -this.encodeOffsetX; -+ final int decodeOffsetY = -this.encodeOffsetY; -+ final int decodeOffsetZ = -this.encodeOffsetZ; -+ final int encodeOffset = this.coordinateOffset; -+ final int sectionOffset = this.chunkSectionIndexOffset; -+ final int emittedMask = this.emittedLightMask; -+ -+ final PlatformHooks platformHooks = PlatformHooks.get(); -+ -+ while (queueReadIndex < queueLength) { -+ final long queueValue = queue[queueReadIndex++]; -+ -+ final int posX = ((int)queueValue & 63) + decodeOffsetX; -+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; -+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; -+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); -+ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63)]; -+ -+ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) { -+ // we don't need to worry about our state here. -+ for (final AxisDirection propagate : checkDirections) { -+ final int offX = posX + propagate.x; -+ final int offY = posY + propagate.y; -+ final int offZ = posZ + propagate.z; -+ -+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; -+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); -+ -+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; -+ final int lightLevel; -+ -+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { -+ // already at lowest (or unloaded), nothing we can do -+ continue; -+ } -+ -+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex); -+ if (blockState == null) { -+ continue; -+ } -+ this.lightEmissionPos.set(offX, offY, offZ); -+ long flags = 0; -+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { -+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); -+ -+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { -+ continue; -+ } -+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; -+ } -+ -+ final int opacity = blockState.getLightBlock(); -+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); -+ if (lightLevel > targetLevel) { -+ // it looks like another source propagated here, so re-propagate it -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((lightLevel & 0xFL) << (6 + 6 + 16)) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | (FLAG_RECHECK_LEVEL | flags); -+ continue; -+ } -+ final int emittedLight = (platformHooks.getLightEmission(blockState, world, this.lightEmissionPos)) & emittedMask; -+ if (emittedLight != 0) { -+ // re-propagate source -+ // note: do not set recheck level, or else the propagation will fail -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((emittedLight & 0xFL) << (6 + 6 + 16)) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | (flags | FLAG_WRITE_LEVEL); -+ } -+ -+ currentNibble.set(localIndex, 0); -+ this.postLightUpdate(offX, offY, offZ); -+ -+ if (targetLevel > 0) { -+ if (queueLength >= queue.length) { -+ queue = this.resizeDecreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((targetLevel & 0xFL) << (6 + 6 + 16)) -+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) -+ | flags; -+ } -+ continue; -+ } -+ } else { -+ // we actually need to worry about our state here -+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ); -+ for (final AxisDirection propagate : checkDirections) { -+ final int offX = posX + propagate.x; -+ final int offY = posY + propagate.y; -+ final int offZ = posZ + propagate.z; -+ -+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; -+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); -+ -+ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(propagate.nms) : Shapes.empty(); -+ -+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { -+ continue; -+ } -+ -+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; -+ final int lightLevel; -+ -+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { -+ // already at lowest (or unloaded), nothing we can do -+ continue; -+ } -+ -+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex); -+ if (blockState == null) { -+ continue; -+ } -+ this.lightEmissionPos.set(offX, offY, offZ); -+ long flags = 0; -+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { -+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); -+ -+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { -+ continue; -+ } -+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; -+ } -+ -+ final int opacity = blockState.getLightBlock(); -+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); -+ if (lightLevel > targetLevel) { -+ // it looks like another source propagated here, so re-propagate it -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((lightLevel & 0xFL) << (6 + 6 + 16)) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | (FLAG_RECHECK_LEVEL | flags); -+ continue; -+ } -+ final int emittedLight = (platformHooks.getLightEmission(blockState, world, this.lightEmissionPos)) & emittedMask; -+ if (emittedLight != 0) { -+ // re-propagate source -+ // note: do not set recheck level, or else the propagation will fail -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((emittedLight & 0xFL) << (6 + 6 + 16)) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | (flags | FLAG_WRITE_LEVEL); -+ } -+ -+ currentNibble.set(localIndex, 0); -+ this.postLightUpdate(offX, offY, offZ); -+ -+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... -+ if (queueLength >= queue.length) { -+ queue = this.resizeDecreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((targetLevel & 0xFL) << (6 + 6 + 16)) -+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) -+ | flags; -+ } -+ continue; -+ } -+ } -+ } -+ -+ // propagate sources we clobbered -+ this.increaseQueueInitialLength = increaseQueueLength; -+ this.performLightIncrease(lightAccess); -+ } -+} -diff --git a/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<Long> CHUNK_WORK_TICKET = TicketType.create("starlight:chunk_work_ticket", Long::compareTo); -+ public static final int LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(ChunkStatus.LIGHT); -+ // ticket level = ChunkLevel.byStatus(FullChunkStatus.FULL) - input -+ public static final int REGION_LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.FULL) - LIGHT_TICKET_LEVEL; -+ -+ /** -+ * Can be {@code null}, indicating the light is all empty. -+ */ -+ public final Level world; -+ public final LightChunkGetter lightAccess; -+ -+ private final ArrayDeque<SkyStarLightEngine> cachedSkyPropagators; -+ private final ArrayDeque<BlockStarLightEngine> cachedBlockPropagators; -+ -+ private final LightQueue lightQueue; -+ -+ private final LayerLightEventListener skyReader; -+ private final LayerLightEventListener blockReader; -+ private final boolean isClientSide; -+ -+ public final int minSection; -+ public final int maxSection; -+ public final int minLightSection; -+ public final int maxLightSection; -+ -+ public final LevelLightEngine lightEngine; -+ -+ private final boolean hasBlockLight; -+ private final boolean hasSkyLight; -+ -+ public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) { -+ this.lightAccess = lightAccess; -+ this.world = lightAccess == null ? null : (Level)lightAccess.getLevel(); -+ this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null; -+ this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null; -+ this.isClientSide = !(this.world instanceof ServerLevel); -+ if (this.world == null) { -+ this.minSection = -4; -+ this.maxSection = 19; -+ this.minLightSection = -5; -+ this.maxLightSection = 20; -+ } else { -+ this.minSection = WorldUtil.getMinSection(this.world); -+ this.maxSection = WorldUtil.getMaxSection(this.world); -+ this.minLightSection = WorldUtil.getMinLightSection(this.world); -+ this.maxLightSection = WorldUtil.getMaxLightSection(this.world); -+ } -+ -+ if (this.world instanceof ServerLevel) { -+ this.lightQueue = new ServerLightQueue(this); -+ } else { -+ this.lightQueue = new ClientLightQueue(this); -+ } -+ -+ this.lightEngine = lightEngine; -+ this.hasBlockLight = hasBlockLight; -+ this.hasSkyLight = hasSkyLight; -+ this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { -+ @Override -+ public void checkBlock(final BlockPos blockPos) { -+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); -+ } -+ -+ @Override -+ public void propagateLightSources(final ChunkPos chunkPos) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public boolean hasLightWork() { -+ // not really correct... -+ return StarLightInterface.this.hasUpdates(); -+ } -+ -+ @Override -+ public int runLightUpdates() { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public DataLayer getDataLayerData(final SectionPos pos) { -+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); -+ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) { -+ return null; -+ } -+ -+ final int sectionY = pos.getY(); -+ -+ if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) { -+ return null; -+ } -+ -+ if (((StarlightChunk)chunk).starlight$getSkyEmptinessMap() == null) { -+ return null; -+ } -+ -+ return ((StarlightChunk)chunk).starlight$getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble(); -+ } -+ -+ @Override -+ public int getLightValue(final BlockPos blockPos) { -+ return StarLightInterface.this.getSkyLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4)); -+ } -+ -+ @Override -+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) { -+ StarLightInterface.this.sectionChange(pos, notReady); -+ } -+ }; -+ this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { -+ @Override -+ public void checkBlock(final BlockPos blockPos) { -+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); -+ } -+ -+ @Override -+ public void propagateLightSources(final ChunkPos chunkPos) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public boolean hasLightWork() { -+ // not really correct... -+ return StarLightInterface.this.hasUpdates(); -+ } -+ -+ @Override -+ public int runLightUpdates() { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public DataLayer getDataLayerData(final SectionPos pos) { -+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); -+ -+ if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) { -+ return null; -+ } -+ -+ return ((StarlightChunk)chunk).starlight$getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble(); -+ } -+ -+ @Override -+ public int getLightValue(final BlockPos blockPos) { -+ return StarLightInterface.this.getBlockLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4)); -+ } -+ -+ @Override -+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) { -+ StarLightInterface.this.sectionChange(pos, notReady); -+ } -+ }; -+ } -+ -+ public ClientLightQueue getClientLightQueue() { -+ if (this.lightQueue instanceof ClientLightQueue clientLightQueue) { -+ return clientLightQueue; -+ } -+ return null; -+ } -+ -+ public ServerLightQueue getServerLightQueue() { -+ if (this.lightQueue instanceof ServerLightQueue serverLightQueue) { -+ return serverLightQueue; -+ } -+ return null; -+ } -+ -+ public boolean hasSkyLight() { -+ return this.hasSkyLight; -+ } -+ -+ public boolean hasBlockLight() { -+ return this.hasBlockLight; -+ } -+ -+ public int getSkyLightValue(final BlockPos blockPos, final ChunkAccess chunk) { -+ if (!this.hasSkyLight) { -+ return 0; -+ } -+ final int x = blockPos.getX(); -+ int y = blockPos.getY(); -+ final int z = blockPos.getZ(); -+ -+ final int minSection = this.minSection; -+ final int maxSection = this.maxSection; -+ final int minLightSection = this.minLightSection; -+ final int maxLightSection = this.maxLightSection; -+ -+ if (chunk == null || (!this.isClientSide && !chunk.isLightCorrect()) || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) { -+ return 15; -+ } -+ -+ int sectionY = y >> 4; -+ -+ if (sectionY > maxLightSection) { -+ return 15; -+ } -+ -+ if (sectionY < minLightSection) { -+ sectionY = minLightSection; -+ y = sectionY << 4; -+ } -+ -+ final SWMRNibbleArray[] nibbles = ((StarlightChunk)chunk).starlight$getSkyNibbles(); -+ final SWMRNibbleArray immediate = nibbles[sectionY - minLightSection]; -+ -+ if (!immediate.isNullNibbleVisible()) { -+ return immediate.getVisible(x, y, z); -+ } -+ -+ final boolean[] emptinessMap = ((StarlightChunk)chunk).starlight$getSkyEmptinessMap(); -+ -+ if (emptinessMap == null) { -+ return 15; -+ } -+ -+ // are we above this chunk's lowest empty section? -+ int lowestY = minLightSection - 1; -+ for (int currY = maxSection; currY >= minSection; --currY) { -+ if (emptinessMap[currY - minSection]) { -+ continue; -+ } -+ -+ // should always be full lit here -+ lowestY = currY; -+ break; -+ } -+ -+ if (sectionY > lowestY) { -+ return 15; -+ } -+ -+ // this nibble is going to depend solely on the skylight data above it -+ // find first non-null data above (there does exist one, as we just found it above) -+ for (int currY = sectionY + 1; currY <= maxLightSection; ++currY) { -+ final SWMRNibbleArray nibble = nibbles[currY - minLightSection]; -+ if (!nibble.isNullNibbleVisible()) { -+ return nibble.getVisible(x, 0, z); -+ } -+ } -+ -+ // should never reach here -+ return 15; -+ } -+ -+ public int getBlockLightValue(final BlockPos blockPos, final ChunkAccess chunk) { -+ if (!this.hasBlockLight) { -+ return 0; -+ } -+ final int y = blockPos.getY(); -+ final int cy = y >> 4; -+ -+ final int minLightSection = this.minLightSection; -+ final int maxLightSection = this.maxLightSection; -+ -+ if (cy < minLightSection || cy > maxLightSection) { -+ return 0; -+ } -+ -+ if (chunk == null) { -+ return 0; -+ } -+ -+ final SWMRNibbleArray nibble = ((StarlightChunk)chunk).starlight$getBlockNibbles()[cy - minLightSection]; -+ return nibble.getVisible(blockPos.getX(), y, blockPos.getZ()); -+ } -+ -+ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) { -+ final ChunkAccess chunk = this.getAnyChunkNow(pos.getX() >> 4, pos.getZ() >> 4); -+ -+ final int sky = this.getSkyLightValue(pos, chunk) - ambientDarkness; -+ // Don't fetch the block light level if the skylight level is 15, since the value will never be higher. -+ if (sky == 15) { -+ return 15; -+ } -+ final int block = this.getBlockLightValue(pos, chunk); -+ return Math.max(sky, block); -+ } -+ -+ public LayerLightEventListener getSkyReader() { -+ return this.skyReader; -+ } -+ -+ public LayerLightEventListener getBlockReader() { -+ return this.blockReader; -+ } -+ -+ public boolean isClientSide() { -+ return this.isClientSide; -+ } -+ -+ public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) { -+ if (this.world == null) { -+ // empty world -+ return null; -+ } -+ return ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ); -+ } -+ -+ public boolean hasUpdates() { -+ return !this.lightQueue.isEmpty(); -+ } -+ -+ public Level getWorld() { -+ return this.world; -+ } -+ -+ public LightChunkGetter getLightAccess() { -+ return this.lightAccess; -+ } -+ -+ public SkyStarLightEngine getSkyLightEngine() { -+ if (this.cachedSkyPropagators == null) { -+ return null; -+ } -+ final SkyStarLightEngine ret; -+ synchronized (this.cachedSkyPropagators) { -+ ret = this.cachedSkyPropagators.pollFirst(); -+ } -+ -+ if (ret == null) { -+ return new SkyStarLightEngine(this.world); -+ } -+ return ret; -+ } -+ -+ public void releaseSkyLightEngine(final SkyStarLightEngine engine) { -+ if (this.cachedSkyPropagators == null) { -+ return; -+ } -+ synchronized (this.cachedSkyPropagators) { -+ this.cachedSkyPropagators.addFirst(engine); -+ } -+ } -+ -+ public BlockStarLightEngine getBlockLightEngine() { -+ if (this.cachedBlockPropagators == null) { -+ return null; -+ } -+ final BlockStarLightEngine ret; -+ synchronized (this.cachedBlockPropagators) { -+ ret = this.cachedBlockPropagators.pollFirst(); -+ } -+ -+ if (ret == null) { -+ return new BlockStarLightEngine(this.world); -+ } -+ return ret; -+ } -+ -+ public void releaseBlockLightEngine(final BlockStarLightEngine engine) { -+ if (this.cachedBlockPropagators == null) { -+ return; -+ } -+ synchronized (this.cachedBlockPropagators) { -+ this.cachedBlockPropagators.addFirst(engine); -+ } -+ } -+ -+ public LightQueue.ChunkTasks blockChange(final BlockPos pos) { -+ if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world -+ return null; -+ } -+ -+ return this.lightQueue.queueBlockChange(pos); -+ } -+ -+ public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) { -+ if (this.world == null) { // empty world -+ return null; -+ } -+ -+ return this.lightQueue.queueSectionChange(pos, newEmptyValue); -+ } -+ -+ public void forceLoadInChunk(final ChunkAccess chunk, final Boolean[] emptySections) { -+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); -+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); -+ -+ try { -+ if (skyEngine != null) { -+ skyEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); -+ } -+ if (blockEngine != null) { -+ blockEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); -+ } -+ } finally { -+ this.releaseSkyLightEngine(skyEngine); -+ this.releaseBlockLightEngine(blockEngine); -+ } -+ } -+ -+ public void loadInChunk(final int chunkX, final int chunkZ, final Boolean[] emptySections) { -+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); -+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); -+ -+ try { -+ if (skyEngine != null) { -+ skyEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); -+ } -+ if (blockEngine != null) { -+ blockEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); -+ } -+ } finally { -+ this.releaseSkyLightEngine(skyEngine); -+ this.releaseBlockLightEngine(blockEngine); -+ } -+ } -+ -+ public void lightChunk(final ChunkAccess chunk, final Boolean[] emptySections) { -+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); -+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); -+ -+ try { -+ if (skyEngine != null) { -+ skyEngine.light(this.lightAccess, chunk, emptySections); -+ } -+ if (blockEngine != null) { -+ blockEngine.light(this.lightAccess, chunk, emptySections); -+ } -+ } finally { -+ this.releaseSkyLightEngine(skyEngine); -+ this.releaseBlockLightEngine(blockEngine); -+ } -+ } -+ -+ public void relightChunks(final Set<ChunkPos> chunks, final Consumer<ChunkPos> chunkLightCallback, -+ final IntConsumer onComplete) { -+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); -+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); -+ -+ try { -+ if (skyEngine != null) { -+ skyEngine.relightChunks(this.lightAccess, chunks, blockEngine == null ? chunkLightCallback : null, -+ blockEngine == null ? onComplete : null); -+ } -+ if (blockEngine != null) { -+ blockEngine.relightChunks(this.lightAccess, chunks, chunkLightCallback, onComplete); -+ } -+ } finally { -+ this.releaseSkyLightEngine(skyEngine); -+ this.releaseBlockLightEngine(blockEngine); -+ } -+ } -+ -+ public void checkChunkEdges(final int chunkX, final int chunkZ) { -+ this.checkSkyEdges(chunkX, chunkZ); -+ this.checkBlockEdges(chunkX, chunkZ); -+ } -+ -+ public void checkSkyEdges(final int chunkX, final int chunkZ) { -+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); -+ -+ try { -+ if (skyEngine != null) { -+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); -+ } -+ } finally { -+ this.releaseSkyLightEngine(skyEngine); -+ } -+ } -+ -+ public void checkBlockEdges(final int chunkX, final int chunkZ) { -+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); -+ try { -+ if (blockEngine != null) { -+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); -+ } -+ } finally { -+ this.releaseBlockLightEngine(blockEngine); -+ } -+ } -+ -+ public void propagateChanges() { -+ final LightQueue lightQueue = this.lightQueue; -+ if (lightQueue instanceof ClientLightQueue clientLightQueue) { -+ clientLightQueue.drainTasks(); -+ } // else: invalid usage, although we won't throw because mods... -+ } -+ -+ public static abstract class LightQueue { -+ -+ protected final StarLightInterface lightInterface; -+ -+ public LightQueue(final StarLightInterface lightInterface) { -+ this.lightInterface = lightInterface; -+ } -+ -+ public abstract boolean isEmpty(); -+ -+ public abstract ChunkTasks queueBlockChange(final BlockPos pos); -+ -+ public abstract ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue); -+ -+ public abstract ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections); -+ -+ public abstract ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections); -+ -+ public static abstract class ChunkTasks implements Runnable { -+ -+ public final long chunkCoordinate; -+ -+ protected final StarLightInterface lightEngine; -+ protected final LightQueue queue; -+ protected final MultiThreadedQueue<Runnable> onComplete = new MultiThreadedQueue<>(); -+ protected final Set<BlockPos> changedPositions = new HashSet<>(); -+ protected Boolean[] changedSectionSet; -+ protected ShortOpenHashSet queuedEdgeChecksSky; -+ protected ShortOpenHashSet queuedEdgeChecksBlock; -+ protected List<BooleanSupplier> lightTasks; -+ -+ public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue) { -+ this.chunkCoordinate = chunkCoordinate; -+ this.lightEngine = lightEngine; -+ this.queue = queue; -+ } -+ -+ @Override -+ public abstract void run(); -+ -+ public void queueOrRunTask(final Runnable run) { -+ if (!this.onComplete.add(run)) { -+ run.run(); -+ } -+ } -+ -+ protected void addChangedPosition(final BlockPos pos) { -+ this.changedPositions.add(pos.immutable()); -+ } -+ -+ protected void setChangedSection(final int y, final Boolean newEmptyValue) { -+ if (this.changedSectionSet == null) { -+ this.changedSectionSet = new Boolean[this.lightEngine.maxSection - this.lightEngine.minSection + 1]; -+ } -+ this.changedSectionSet[y - this.lightEngine.minSection] = newEmptyValue; -+ } -+ -+ protected void addLightTask(final BooleanSupplier lightTask) { -+ if (this.lightTasks == null) { -+ this.lightTasks = new ArrayList<>(); -+ } -+ this.lightTasks.add(lightTask); -+ } -+ -+ protected void addEdgeChecksSky(final ShortCollection values) { -+ if (this.queuedEdgeChecksSky == null) { -+ this.queuedEdgeChecksSky = new ShortOpenHashSet(Math.max(8, values.size())); -+ } -+ this.queuedEdgeChecksSky.addAll(values); -+ } -+ -+ protected void addEdgeChecksBlock(final ShortCollection values) { -+ if (this.queuedEdgeChecksBlock == null) { -+ this.queuedEdgeChecksBlock = new ShortOpenHashSet(Math.max(8, values.size())); -+ } -+ this.queuedEdgeChecksBlock.addAll(values); -+ } -+ -+ protected final void runTasks() { -+ boolean litChunk = false; -+ if (this.lightTasks != null) { -+ for (final BooleanSupplier run : this.lightTasks) { -+ if (run.getAsBoolean()) { -+ litChunk = true; -+ break; -+ } -+ } -+ } -+ -+ if (!litChunk) { -+ final SkyStarLightEngine skyEngine = this.lightEngine.getSkyLightEngine(); -+ final BlockStarLightEngine blockEngine = this.lightEngine.getBlockLightEngine(); -+ try { -+ final long coordinate = this.chunkCoordinate; -+ final int chunkX = CoordinateUtils.getChunkX(coordinate); -+ final int chunkZ = CoordinateUtils.getChunkZ(coordinate); -+ -+ final Set<BlockPos> positions = this.changedPositions; -+ final Boolean[] sectionChanges = this.changedSectionSet; -+ -+ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { -+ skyEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges); -+ } -+ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { -+ blockEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges); -+ } -+ -+ if (skyEngine != null && this.queuedEdgeChecksSky != null) { -+ skyEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksSky); -+ } -+ if (blockEngine != null && this.queuedEdgeChecksBlock != null) { -+ blockEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksBlock); -+ } -+ } finally { -+ this.lightEngine.releaseSkyLightEngine(skyEngine); -+ this.lightEngine.releaseBlockLightEngine(blockEngine); -+ } -+ } -+ -+ Runnable run; -+ while ((run = this.onComplete.pollOrBlockAdds()) != null) { -+ run.run(); -+ } -+ } -+ } -+ } -+ -+ public static final class ClientLightQueue extends LightQueue { -+ -+ private final Long2ObjectLinkedOpenHashMap<ClientChunkTasks> chunkTasks = new Long2ObjectLinkedOpenHashMap<>(); -+ -+ public ClientLightQueue(final StarLightInterface lightInterface) { -+ super(lightInterface); -+ } -+ -+ @Override -+ public synchronized boolean isEmpty() { -+ return this.chunkTasks.isEmpty(); -+ } -+ -+ // must hold synchronized lock on this object -+ private ClientChunkTasks getOrCreate(final long key) { -+ return this.chunkTasks.computeIfAbsent(key, (final long keyInMap) -> { -+ return new ClientChunkTasks(keyInMap, ClientLightQueue.this.lightInterface, ClientLightQueue.this); -+ }); -+ } -+ -+ @Override -+ public synchronized ClientChunkTasks queueBlockChange(final BlockPos pos) { -+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); -+ tasks.addChangedPosition(pos); -+ return tasks; -+ } -+ -+ @Override -+ public synchronized ClientChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { -+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); -+ -+ tasks.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue)); -+ -+ return tasks; -+ } -+ -+ @Override -+ public synchronized ClientChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { -+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); -+ -+ tasks.addEdgeChecksSky(sections); -+ -+ return tasks; -+ } -+ -+ @Override -+ public synchronized ClientChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { -+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); -+ -+ tasks.addEdgeChecksBlock(sections); -+ -+ return tasks; -+ } -+ -+ public synchronized ClientChunkTasks removeFirstTask() { -+ if (this.chunkTasks.isEmpty()) { -+ return null; -+ } -+ return this.chunkTasks.removeFirst(); -+ } -+ -+ public void drainTasks() { -+ ClientChunkTasks task; -+ while ((task = this.removeFirstTask()) != null) { -+ task.runTasks(); -+ } -+ } -+ -+ public static final class ClientChunkTasks extends ChunkTasks { -+ -+ public ClientChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final ClientLightQueue queue) { -+ super(chunkCoordinate, lightEngine, queue); -+ } -+ -+ @Override -+ public void run() { -+ this.runTasks(); -+ } -+ } -+ } -+ -+ public static final class ServerLightQueue extends LightQueue { -+ -+ private final ConcurrentLong2ReferenceChainedHashTable<ServerChunkTasks> chunkTasks = new ConcurrentLong2ReferenceChainedHashTable<>(); -+ -+ public ServerLightQueue(final StarLightInterface lightInterface) { -+ super(lightInterface); -+ } -+ -+ public void lowerPriority(final int chunkX, final int chunkZ, final Priority priority) { -+ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ if (task != null) { -+ task.lowerPriority(priority); -+ } -+ } -+ -+ public void setPriority(final int chunkX, final int chunkZ, final Priority priority) { -+ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ if (task != null) { -+ task.setPriority(priority); -+ } -+ } -+ -+ public void raisePriority(final int chunkX, final int chunkZ, final Priority priority) { -+ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ if (task != null) { -+ task.raisePriority(priority); -+ } -+ } -+ -+ public Priority getPriority(final int chunkX, final int chunkZ) { -+ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ if (task != null) { -+ return task.getPriority(); -+ } -+ -+ return Priority.COMPLETING; -+ } -+ -+ @Override -+ public boolean isEmpty() { -+ return this.chunkTasks.isEmpty(); -+ } -+ -+ @Override -+ public ServerChunkTasks queueBlockChange(final BlockPos pos) { -+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { -+ if (valueInMap == null) { -+ valueInMap = new ServerChunkTasks( -+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this -+ ); -+ } -+ valueInMap.addChangedPosition(pos); -+ return valueInMap; -+ }); -+ -+ ret.schedule(); -+ -+ return ret; -+ } -+ -+ @Override -+ public ServerChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { -+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { -+ if (valueInMap == null) { -+ valueInMap = new ServerChunkTasks( -+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this -+ ); -+ } -+ -+ valueInMap.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue)); -+ -+ return valueInMap; -+ }); -+ -+ ret.schedule(); -+ -+ return ret; -+ } -+ -+ public ServerChunkTasks queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final Priority priority) { -+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { -+ if (valueInMap == null) { -+ valueInMap = new ServerChunkTasks( -+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this, priority -+ ); -+ } -+ -+ valueInMap.addLightTask(lightTask); -+ -+ return valueInMap; -+ }); -+ -+ ret.schedule(); -+ -+ return ret; -+ } -+ -+ @Override -+ public ServerChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { -+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { -+ if (valueInMap == null) { -+ valueInMap = new ServerChunkTasks( -+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this -+ ); -+ } -+ -+ valueInMap.addEdgeChecksSky(sections); -+ -+ return valueInMap; -+ }); -+ -+ ret.schedule(); -+ -+ return ret; -+ } -+ -+ @Override -+ public ServerChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { -+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { -+ if (valueInMap == null) { -+ valueInMap = new ServerChunkTasks( -+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this -+ ); -+ } -+ -+ valueInMap.addEdgeChecksBlock(sections); -+ -+ return valueInMap; -+ }); -+ -+ ret.schedule(); -+ -+ return ret; -+ } -+ -+ public static final class ServerChunkTasks extends ChunkTasks { -+ -+ private final AtomicBoolean ticketAdded = new AtomicBoolean(); -+ private final PrioritisedExecutor.PrioritisedTask task; -+ -+ public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, -+ final ServerLightQueue queue) { -+ this(chunkCoordinate, lightEngine, queue, Priority.NORMAL); -+ } -+ -+ public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, -+ final ServerLightQueue queue, final Priority priority) { -+ super(chunkCoordinate, lightEngine, queue); -+ this.task = ((ChunkSystemServerLevel)(ServerLevel)lightEngine.getWorld()).moonrise$getChunkTaskScheduler().radiusAwareScheduler.createTask( -+ CoordinateUtils.getChunkX(chunkCoordinate), CoordinateUtils.getChunkZ(chunkCoordinate), -+ ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$getWriteRadius(), this, priority -+ ); -+ } -+ -+ public boolean markTicketAdded() { -+ return !this.ticketAdded.get() && !this.ticketAdded.getAndSet(true); -+ } -+ -+ public void schedule() { -+ this.task.queue(); -+ } -+ -+ public boolean cancel() { -+ return this.task.cancel(); -+ } -+ -+ public Priority getPriority() { -+ return this.task.getPriority(); -+ } -+ -+ public void lowerPriority(final Priority priority) { -+ this.task.lowerPriority(priority); -+ } -+ -+ public void setPriority(final Priority priority) { -+ this.task.setPriority(priority); -+ } -+ -+ public void raisePriority(final Priority priority) { -+ this.task.raisePriority(priority); -+ } -+ -+ @Override -+ public void run() { -+ ((ServerLightQueue)this.queue).chunkTasks.remove(this.chunkCoordinate, this); -+ -+ this.runTasks(); -+ } -+ } -+ } -+} -diff --git a/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<ChunkPos> chunks, -+ final Consumer<ChunkPos> 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<Set<String>, 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<Biome> 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<String> tabComplete(final CommandSender sender, final String subCommand, final String[] args) { -+ switch (subCommand) { -+ case "debug" -> { -+ if (args.length == 1) { -+ return CommandUtil.getListMatchingLast(sender, args, "help", "chunks"); -+ } -+ } -+ case "holderinfo" -> { -+ List<String> worldNames = new ArrayList<>(); -+ worldNames.add("*"); -+ for (org.bukkit.World world : Bukkit.getWorlds()) { -+ worldNames.add(world.getName()); -+ } -+ if (args.length == 1) { -+ return CommandUtil.getListMatchingLast(sender, args, worldNames); -+ } -+ } -+ case "chunkinfo" -> { -+ List<String> worldNames = new ArrayList<>(); -+ worldNames.add("*"); -+ for (org.bukkit.World world : Bukkit.getWorlds()) { -+ worldNames.add(world.getName()); -+ } -+ if (args.length == 1) { -+ return CommandUtil.getListMatchingLast(sender, args, worldNames); -+ } -+ } -+ } -+ return Collections.emptyList(); -+ } -+ -+ private void doChunkInfo(final CommandSender sender, final String[] args) { -+ List<org.bukkit.World> worlds; -+ if (args.length < 1 || args[0].equals("*")) { -+ worlds = Bukkit.getWorlds(); -+ } else { -+ worlds = new ArrayList<>(args.length); -+ for (final String arg : args) { -+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg); -+ if (world == null) { -+ sender.sendMessage(text("World '" + arg + "' is invalid", RED)); -+ return; -+ } -+ worlds.add(world); -+ } -+ } -+ -+ int accumulatedTotal = 0; -+ int accumulatedInactive = 0; -+ int accumulatedBorder = 0; -+ int accumulatedTicking = 0; -+ int accumulatedEntityTicking = 0; -+ -+ for (final org.bukkit.World bukkitWorld : worlds) { -+ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); -+ -+ int total = 0; -+ int inactive = 0; -+ int full = 0; -+ int blockTicking = 0; -+ int entityTicking = 0; -+ -+ for (final NewChunkHolder holder : ((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders()) { -+ final NewChunkHolder.ChunkCompletion completion = holder.getLastChunkCompletion(); -+ final ChunkAccess chunk = completion == null ? null : completion.chunk(); -+ -+ if (!(chunk instanceof LevelChunk fullChunk)) { -+ continue; -+ } -+ -+ ++total; -+ -+ switch (holder.getChunkStatus()) { -+ case INACCESSIBLE: { -+ ++inactive; -+ break; -+ } -+ case FULL: { -+ ++full; -+ break; -+ } -+ case BLOCK_TICKING: { -+ ++blockTicking; -+ break; -+ } -+ case ENTITY_TICKING: { -+ ++entityTicking; -+ break; -+ } -+ } -+ } -+ -+ accumulatedTotal += total; -+ accumulatedInactive += inactive; -+ accumulatedBorder += full; -+ accumulatedTicking += blockTicking; -+ accumulatedEntityTicking += entityTicking; -+ -+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":"))); -+ sender.sendMessage(text().color(DARK_AQUA).append( -+ text("Total: ", BLUE), text(total), -+ text(" Inactive: ", BLUE), text(inactive), -+ text(" Full: ", BLUE), text(full), -+ text(" Block Ticking: ", BLUE), text(blockTicking), -+ text(" Entity Ticking: ", BLUE), text(entityTicking) -+ )); -+ } -+ if (worlds.size() > 1) { -+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA))); -+ sender.sendMessage(text().color(DARK_AQUA).append( -+ text("Total: ", BLUE), text(accumulatedTotal), -+ text(" Inactive: ", BLUE), text(accumulatedInactive), -+ text(" Full: ", BLUE), text(accumulatedBorder), -+ text(" Block Ticking: ", BLUE), text(accumulatedTicking), -+ text(" Entity Ticking: ", BLUE), text(accumulatedEntityTicking) -+ )); -+ } -+ } -+ -+ private void doHolderInfo(final CommandSender sender, final String[] args) { -+ List<org.bukkit.World> worlds; -+ if (args.length < 1 || args[0].equals("*")) { -+ worlds = Bukkit.getWorlds(); -+ } else { -+ worlds = new ArrayList<>(args.length); -+ for (final String arg : args) { -+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg); -+ if (world == null) { -+ sender.sendMessage(text("World '" + arg + "' is invalid", RED)); -+ return; -+ } -+ worlds.add(world); -+ } -+ } -+ -+ int accumulatedTotal = 0; -+ int accumulatedCanUnload = 0; -+ int accumulatedNull = 0; -+ int accumulatedReadOnly = 0; -+ int accumulatedProtoChunk = 0; -+ int accumulatedFullChunk = 0; -+ -+ for (final org.bukkit.World bukkitWorld : worlds) { -+ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); -+ -+ int total = 0; -+ int canUnload = 0; -+ int nullChunks = 0; -+ int readOnly = 0; -+ int protoChunk = 0; -+ int fullChunk = 0; -+ -+ for (final NewChunkHolder holder : ((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders()) { -+ final NewChunkHolder.ChunkCompletion completion = holder.getLastChunkCompletion(); -+ final ChunkAccess chunk = completion == null ? null : completion.chunk(); -+ -+ ++total; -+ -+ if (chunk == null) { -+ ++nullChunks; -+ } else if (chunk instanceof ImposterProtoChunk) { -+ ++readOnly; -+ } else if (chunk instanceof ProtoChunk) { -+ ++protoChunk; -+ } else if (chunk instanceof LevelChunk) { -+ ++fullChunk; -+ } -+ -+ if (holder.isSafeToUnload() == null) { -+ ++canUnload; -+ } -+ } -+ -+ accumulatedTotal += total; -+ accumulatedCanUnload += canUnload; -+ accumulatedNull += nullChunks; -+ accumulatedReadOnly += readOnly; -+ accumulatedProtoChunk += protoChunk; -+ accumulatedFullChunk += fullChunk; -+ -+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":"))); -+ sender.sendMessage(text().color(DARK_AQUA).append( -+ text("Total: ", BLUE), text(total), -+ text(" Unloadable: ", BLUE), text(canUnload), -+ text(" Null: ", BLUE), text(nullChunks), -+ text(" ReadOnly: ", BLUE), text(readOnly), -+ text(" Proto: ", BLUE), text(protoChunk), -+ text(" Full: ", BLUE), text(fullChunk) -+ )); -+ } -+ if (worlds.size() > 1) { -+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA))); -+ sender.sendMessage(text().color(DARK_AQUA).append( -+ text("Total: ", BLUE), text(accumulatedTotal), -+ text(" Unloadable: ", BLUE), text(accumulatedCanUnload), -+ text(" Null: ", BLUE), text(accumulatedNull), -+ text(" ReadOnly: ", BLUE), text(accumulatedReadOnly), -+ text(" Proto: ", BLUE), text(accumulatedProtoChunk), -+ text(" Full: ", BLUE), text(accumulatedFullChunk) -+ )); -+ } -+ } -+ -+ private void doDebug(final CommandSender sender, final String[] args) { -+ if (args.length < 1) { -+ sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED)); -+ return; -+ } -+ -+ final String debugType = args[0].toLowerCase(Locale.ROOT); -+ switch (debugType) { -+ case "chunks" -> { -+ if (args.length >= 2 && args[1].toLowerCase(Locale.ROOT).equals("help")) { -+ sender.sendMessage(text("Use /paper debug chunks to dump loaded chunk information to a file", RED)); -+ break; -+ } -+ final File file = ChunkTaskScheduler.getChunkDebugFile(); -+ sender.sendMessage(text("Writing chunk information dump to " + file, GREEN)); -+ try { -+ JsonUtil.writeJson(ChunkTaskScheduler.debugAllWorlds(MinecraftServer.getServer()), file); -+ sender.sendMessage(text("Successfully written chunk information!", GREEN)); -+ } catch (Throwable thr) { -+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr); -+ sender.sendMessage(text("Failed to dump chunk information, see console", RED)); -+ } -+ } -+ // "help" & default -+ default -> sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED)); -+ } -+ } -+ -+} -diff --git a/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<DecimalFormat> ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { -+ return new DecimalFormat("#,##0.0"); -+ }); -+ -+ @Override -+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { -+ this.doFixLight(sender, args); -+ return true; -+ } -+ -+ private void doFixLight(final CommandSender sender, final String[] args) { -+ if (!(sender instanceof Player)) { -+ sender.sendMessage(text("Only players can use this command", RED)); -+ return; -+ } -+ @Nullable Runnable post = null; -+ int radius = 2; -+ if (args.length > 0) { -+ try { -+ final int parsed = Integer.parseInt(args[0]); -+ if (parsed < 0) { -+ sender.sendMessage(text("Radius cannot be negative!", RED)); -+ return; -+ } -+ final int maxRadius = 32; -+ radius = Math.min(maxRadius, parsed); -+ if (radius != parsed) { -+ post = () -> sender.sendMessage(text("Radius '" + parsed + "' was not in the required range [0, " + maxRadius + "], it was lowered to the maximum (" + maxRadius + " chunks).", RED)); -+ } -+ } catch (final Exception e) { -+ sender.sendMessage(text("'" + args[0] + "' is not a valid number.", RED)); -+ return; -+ } -+ } -+ -+ CraftPlayer player = (CraftPlayer) sender; -+ ServerPlayer handle = player.getHandle(); -+ ServerLevel world = (ServerLevel) handle.level(); -+ ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine(); -+ this.starlightFixLight(handle, world, lightengine, radius, post); -+ } -+ -+ private void starlightFixLight( -+ final ServerPlayer sender, -+ final ServerLevel world, -+ final ThreadedLevelLightEngine lightengine, -+ final int radius, -+ final @Nullable Runnable done -+ ) { -+ final long start = System.nanoTime(); -+ final java.util.LinkedHashSet<ChunkPos> chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos -+ -+ final int[] pending = new int[1]; -+ for (java.util.Iterator<ChunkPos> iterator = chunks.iterator(); iterator.hasNext(); ) { -+ final ChunkPos chunkPos = iterator.next(); -+ -+ final @Nullable ChunkAccess chunk = (ChunkAccess) world.getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z); -+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) { -+ // cannot relight this chunk -+ iterator.remove(); -+ continue; -+ } -+ -+ ++pending[0]; -+ } -+ -+ final int[] relitChunks = new int[1]; -+ ((StarLightLightingProvider)lightengine).starlight$serverRelightChunks(chunks, -+ (final ChunkPos chunkPos) -> { -+ ++relitChunks[0]; -+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append( -+ text("Relit chunk ", BLUE), text(chunkPos.toString()), -+ text(", progress: ", BLUE), text(ONE_DECIMAL_PLACES.get().format(100.0 * (double) (relitChunks[0]) / (double) pending[0]) + "%") -+ )); -+ }, -+ (final int totalRelit) -> { -+ final long end = System.nanoTime(); -+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append( -+ text("Relit ", BLUE), text(totalRelit), -+ text(" chunks. Took ", BLUE), text(ONE_DECIMAL_PLACES.get().format(1.0e-6 * (end - start)) + "ms") -+ )); -+ if (done != null) { -+ done.run(); -+ } -+ } -+ ); -+ sender.getBukkitEntity().sendMessage(text().color(BLUE).append(text("Relighting "), text(pending[0], DARK_AQUA), text(" chunks"))); -+ } -+} -diff --git a/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<T> implements WritableRegistry<T> { - return this.getTags(); - } - -+ // Paper start - fluid method optimisations -+ private void injectFluidRegister( -+ final ResourceKey<?> resourceKey, -+ final T object -+ ) { -+ if (resourceKey.registryKey() == (Object)net.minecraft.core.registries.Registries.FLUID) { -+ for (final net.minecraft.world.level.material.FluidState possibleState : ((net.minecraft.world.level.material.Fluid)object).getStateDefinition().getPossibleStates()) { -+ ((ca.spottedleaf.moonrise.patches.fluid.FluidFluidState)(Object)possibleState).moonrise$initCaches(); -+ } -+ } -+ } -+ // Paper end - fluid method optimisations -+ - public MappedRegistry(ResourceKey<? extends Registry<T>> key, Lifecycle lifecycle) { - this(key, lifecycle, false); - } -@@ -114,6 +127,7 @@ public class MappedRegistry<T> implements WritableRegistry<T> { - this.toId.put(value, i); - this.registrationInfos.put(key, info); - this.registryLifecycle = this.registryLifecycle.add(info.lifecycle()); -+ this.injectFluidRegister(key, value); // Paper - fluid method optimisations - return reference; - } - } -diff --git a/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<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource { -+public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource, ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer { // Paper - rewrite chunk system - - private static MinecraftServer SERVER; // Paper - public static final Logger LOGGER = LogUtils.getLogger(); -@@ -333,7 +333,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa - public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) { - ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system - AtomicReference<S> atomicreference = new AtomicReference(); -- Thread thread = new Thread(() -> { -+ Thread thread = new ca.spottedleaf.moonrise.common.util.TickThread(() -> { // Paper - rewrite chunk system - ((MinecraftServer) atomicreference.get()).runServer(); - }, "Server thread"); - -@@ -352,6 +352,77 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa - return s0; - } - -+ // Paper start - rewrite chunk system -+ private volatile Throwable chunkSystemCrash; -+ -+ @Override -+ public final void moonrise$setChunkSystemCrash(final Throwable throwable) { -+ this.chunkSystemCrash = throwable; -+ } -+ -+ private static final long CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME = 25L * 1000L; // 25us -+ private static final long MAX_CHUNK_EXEC_TIME = 1000L; // 1us -+ private static final long TASK_EXECUTION_FAILURE_BACKOFF = 5L * 1000L; // 5us -+ -+ private long lastMidTickExecute; -+ private long lastMidTickExecuteFailure; -+ -+ private boolean tickMidTickTasks() { -+ // give all worlds a fair chance at by targeting them all. -+ // if we execute too many tasks, that's fine - we have logic to correctly handle overuse of allocated time. -+ boolean executed = false; -+ for (final ServerLevel world : this.getAllLevels()) { -+ long currTime = System.nanoTime(); -+ if (currTime - ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getLastMidTickFailure() <= TASK_EXECUTION_FAILURE_BACKOFF) { -+ continue; -+ } -+ if (!world.getChunkSource().pollTask()) { -+ // we need to back off if this fails -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$setLastMidTickFailure(currTime); -+ } else { -+ executed = true; -+ } -+ } -+ -+ return executed; -+ } -+ -+ @Override -+ public final void moonrise$executeMidTickTasks() { -+ final long startTime = System.nanoTime(); -+ if ((startTime - this.lastMidTickExecute) <= CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME || (startTime - this.lastMidTickExecuteFailure) <= TASK_EXECUTION_FAILURE_BACKOFF) { -+ // it's shown to be bad to constantly hit the queue (chunk loads slow to a crawl), even if no tasks are executed. -+ // so, backoff to prevent this -+ return; -+ } -+ -+ for (;;) { -+ final boolean moreTasks = this.tickMidTickTasks(); -+ final long currTime = System.nanoTime(); -+ final long diff = currTime - startTime; -+ -+ if (!moreTasks || diff >= MAX_CHUNK_EXEC_TIME) { -+ if (!moreTasks) { -+ this.lastMidTickExecuteFailure = currTime; -+ } -+ -+ // note: negative values reduce the time -+ long overuse = diff - MAX_CHUNK_EXEC_TIME; -+ if (overuse >= (10L * 1000L * 1000L)) { // 10ms -+ // make sure something like a GC or dumb plugin doesn't screw us over... -+ overuse = 10L * 1000L * 1000L; // 10ms -+ } -+ -+ final double overuseCount = (double)overuse/(double)MAX_CHUNK_EXEC_TIME; -+ final long extraSleep = (long)Math.round(overuseCount*CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME); -+ -+ this.lastMidTickExecute = currTime + extraSleep; -+ return; -+ } -+ } -+ } -+ // Paper end - rewrite chunk system -+ - public MinecraftServer(OptionSet options, WorldLoader.DataLoadContext worldLoader, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, Proxy proxy, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) { - super("Server"); - SERVER = this; // Paper - better singleton -@@ -672,7 +743,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa - this.forceDifficulty(); - for (ServerLevel worldserver : this.getAllLevels()) { - this.prepareLevels(worldserver.getChunkSource().chunkMap.progressListener, worldserver); -- worldserver.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API -+ // Paper - rewrite chunk system - this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(worldserver.getWorld())); - } - -@@ -888,6 +959,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa - public abstract boolean shouldRconBroadcast(); - - public boolean saveAllChunks(boolean suppressLogs, boolean flush, boolean force) { -+ // Paper start - add close param -+ return this.saveAllChunks(suppressLogs, flush, force, false); -+ } -+ public boolean saveAllChunks(boolean suppressLogs, boolean flush, boolean force, boolean close) { -+ // Paper end - add close param - boolean flag3 = false; - - for (Iterator iterator = this.getAllLevels().iterator(); iterator.hasNext(); flag3 = true) { -@@ -897,7 +973,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa - MinecraftServer.LOGGER.info("Saving chunks for level '{}'/{}", worldserver, worldserver.dimension().location()); - } - -- worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force); -+ worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force, close); // Paper - add close param - } - - // CraftBukkit start - moved to WorldServer.save -@@ -998,7 +1074,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa - } - } - -- while (this.levels.values().stream().anyMatch((worldserver1) -> { -+ while (false && this.levels.values().stream().anyMatch((worldserver1) -> { // Paper - rewrite chunk system - return worldserver1.getChunkSource().chunkMap.hasWork(); - })) { - this.nextTickTimeNanos = Util.getNanos() + TimeUtil.NANOSECONDS_PER_MILLISECOND; -@@ -1015,19 +1091,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa - this.waitUntilNextTick(); - } - -- this.saveAllChunks(false, true, false); -- iterator = this.getAllLevels().iterator(); -- -- while (iterator.hasNext()) { -- worldserver = (ServerLevel) iterator.next(); -- if (worldserver != null) { -- try { -- worldserver.close(); -- } catch (IOException ioexception) { -- MinecraftServer.LOGGER.error("Exception closing the level", ioexception); -- } -- } -- } -+ this.saveAllChunks(false, true, true, true); // Paper - rewrite chunk system - - this.isSaving = false; - this.resources.close(); -@@ -1047,6 +1111,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa - } - // Spigot end - -+ // Paper start - rewrite chunk system -+ LOGGER.info("Waiting for I/O tasks to complete..."); -+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.flush((MinecraftServer)(Object)this); -+ LOGGER.info("All I/O tasks to complete"); -+ if ((Object)this instanceof DedicatedServer) { -+ ca.spottedleaf.moonrise.common.util.MoonriseCommon.haltExecutors(); -+ } -+ // Paper end - rewrite chunk system - } - - public String getLocalIp() { -@@ -1228,6 +1300,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa - this.tickServer(flag ? () -> { - return false; - } : this::haveTime); -+ // Paper start - rewrite chunk system -+ final Throwable crash = this.chunkSystemCrash; -+ if (crash != null) { -+ this.chunkSystemCrash = null; -+ throw new RuntimeException("Chunk system crash propagated to tick()", crash); -+ } -+ // Paper end - rewrite chunk system - this.tickFrame.end(); - gameprofilerfiller.popPush("nextTickWait"); - this.mayHaveDelayedTasks = true; -@@ -1432,6 +1511,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa - - private boolean pollTaskInternal() { - if (super.pollTask()) { -+ this.moonrise$executeMidTickTasks(); // Paper - rewrite chunk system - return true; - } else { - boolean ret = false; // Paper - force execution of all worlds, do not just bias the first -@@ -2713,6 +2793,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa - - } - -+ // Paper start - rewrite chunk system -+ @Override -+ public boolean isSameThread() { -+ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThread(); -+ } -+ // Paper end - rewrite chunk system -+ - // CraftBukkit start - public boolean isDebugging() { - return false; -diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java -index 2f47d95943c00020a24ea3ff1a49e64e114de675..0dd9ed7465d222505d5368781654ec4954f6e5c3 100644 ---- a/net/minecraft/server/dedicated/DedicatedServer.java -+++ b/net/minecraft/server/dedicated/DedicatedServer.java -@@ -458,7 +458,33 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface - return world.dimension() == net.minecraft.world.level.Level.NETHER ? this.getProperties().allowNether : true; - } - -+ private static final java.util.concurrent.atomic.AtomicInteger ASYNC_DEBUG_CHUNKS_COUNT = new java.util.concurrent.atomic.AtomicInteger(); // Paper - rewrite chunk system -+ - public void handleConsoleInput(String command, CommandSourceStack commandSource) { -+ // Paper start - rewrite chunk system -+ if (command.equalsIgnoreCase("paper debug chunks --async")) { -+ LOGGER.info("Scheduling async debug chunks"); -+ Runnable run = () -> { -+ LOGGER.info("Async debug chunks executing"); -+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(this, false); -+ CommandSender sender = MinecraftServer.getServer().console; -+ java.io.File file = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getChunkDebugFile(); -+ sender.sendMessage(net.kyori.adventure.text.Component.text("Writing chunk information dump to " + file, net.kyori.adventure.text.format.NamedTextColor.GREEN)); -+ try { -+ ca.spottedleaf.moonrise.common.util.JsonUtil.writeJson(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.debugAllWorlds(this), file); -+ sender.sendMessage(net.kyori.adventure.text.Component.text("Successfully written chunk information!", net.kyori.adventure.text.format.NamedTextColor.GREEN)); -+ } catch (Throwable thr) { -+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr); -+ sender.sendMessage(net.kyori.adventure.text.Component.text("Failed to dump chunk information, see console", net.kyori.adventure.text.format.NamedTextColor.RED)); -+ } -+ }; -+ Thread t = new Thread(run); -+ t.setName("Async debug thread #" + ASYNC_DEBUG_CHUNKS_COUNT.getAndIncrement()); -+ t.setDaemon(true); -+ t.start(); -+ return; -+ } -+ // Paper end - rewrite chunk system - this.serverCommandQueue.add(new ConsoleInput(command, commandSource)); // Paper - Perf: use proper queue - } - -diff --git a/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<LevelChunk> UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk"); - private static final CompletableFuture<ChunkResult<LevelChunk>> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK); - private final LevelHeightAccessor levelHeightAccessor; -- private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage -- private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage -- private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage -- public int oldTicketLevel; -- private int ticketLevel; -- private int queueLevel; -+ // Paper - rewrite chunk system - private boolean hasChangedSections; - private final ShortSet[] changedBlocksPerSection; - private final BitSet blockChangedLightSectionFilter; - private final BitSet skyChangedLightSectionFilter; - private final LevelLightEngine lightEngine; -- private final ChunkHolder.LevelChangeListener onLevelChange; -+ // Paper - rewrite chunk system - public final ChunkHolder.PlayerProvider playerProvider; -- private boolean wasAccessibleSinceLastSave; -- private CompletableFuture<?> pendingFullStateConfirmation; -- private CompletableFuture<?> sendSync; -- private CompletableFuture<?> saveSync; -+ // Paper - rewrite chunk system -+ -+ // Paper start - rewrite chunk system -+ private ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder; -+ -+ private static final ServerPlayer[] EMPTY_PLAYER_ARRAY = new ServerPlayer[0]; -+ private final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> playersSentChunkTo = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_ARRAY); -+ -+ private ChunkMap getChunkMap() { -+ return (ChunkMap)this.playerProvider; -+ } -+ -+ @Override -+ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder moonrise$getRealChunkHolder() { -+ return this.newChunkHolder; -+ } -+ -+ @Override -+ public final void moonrise$setRealChunkHolder(final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder) { -+ this.newChunkHolder = newChunkHolder; -+ } -+ -+ @Override -+ public final void moonrise$addReceivedChunk(final ServerPlayer player) { -+ if (!this.playersSentChunkTo.add(player)) { -+ throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player); -+ } -+ } -+ -+ @Override -+ public final void moonrise$removeReceivedChunk(final ServerPlayer player) { -+ if (!this.playersSentChunkTo.remove(player)) { -+ throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player); -+ } -+ } -+ -+ @Override -+ public final boolean moonrise$hasChunkBeenSent() { -+ return this.playersSentChunkTo.size() != 0; -+ } -+ -+ @Override -+ public final boolean moonrise$hasChunkBeenSent(final ServerPlayer to) { -+ return this.playersSentChunkTo.contains(to); -+ } -+ -+ @Override -+ public final List<ServerPlayer> moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge) { -+ final List<ServerPlayer> ret = new java.util.ArrayList<>(); -+ final ServerPlayer[] raw = this.playersSentChunkTo.getRawDataUnchecked(); -+ for (int i = 0, len = this.playersSentChunkTo.size(); i < len; ++i) { -+ final ServerPlayer player = raw[i]; -+ if (onlyOnWatchDistanceEdge && !((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getPlayerChunkLoader().isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) { -+ continue; -+ } -+ ret.add(player); -+ } -+ -+ return ret; -+ } -+ -+ @Override -+ public final LevelChunk moonrise$getFullChunk() { -+ if (this.newChunkHolder.isFullChunkReady()) { -+ if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) { -+ return levelChunk; -+ } // else: race condition: chunk unload -+ } -+ return null; -+ } -+ -+ private boolean isRadiusLoaded(final int radius) { -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getChunkTaskScheduler() -+ .chunkHolderManager; -+ final ChunkPos pos = this.pos; -+ final int chunkX = pos.x; -+ final int chunkZ = pos.z; -+ for (int dz = -radius; dz <= radius; ++dz) { -+ for (int dx = -radius; dx <= radius; ++dx) { -+ if ((dx | dz) == 0) { -+ continue; -+ } -+ -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = manager.getChunkHolder(dx + chunkX, dz + chunkZ); -+ -+ if (holder == null || !holder.isFullChunkReady()) { -+ return false; -+ } -+ } -+ } -+ -+ return true; -+ } -+ // Paper end - rewrite chunk system - - public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) { - super(pos); -- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; -- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; -- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; -+ // Paper - rewrite chunk system - this.blockChangedLightSectionFilter = new BitSet(); - this.skyChangedLightSectionFilter = new BitSet(); -- this.pendingFullStateConfirmation = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error -- this.sendSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error -- this.saveSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error -+ // Paper - rewrite chunk system - this.levelHeightAccessor = world; - this.lightEngine = lightingProvider; -- this.onLevelChange = levelUpdateListener; -+ // Paper - rewrite chunk system - this.playerProvider = playersWatchingChunkProvider; -- this.oldTicketLevel = ChunkLevel.MAX_LEVEL + 1; -- this.ticketLevel = this.oldTicketLevel; -- this.queueLevel = this.oldTicketLevel; -+ // Paper - rewrite chunk system - this.setTicketLevel(level); - this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()]; - } -@@ -79,7 +158,7 @@ public class ChunkHolder extends GenerationChunkHolder { - // CraftBukkit start - public LevelChunk getFullChunkNow() { - // Note: We use the oldTicketLevel for isLoaded checks. -- if (!ChunkLevel.fullStatus(this.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) return null; -+ if (!this.newChunkHolder.isFullChunkReady()) return null; // Paper - rewrite chunk system - return this.getFullChunkNowUnchecked(); - } - -@@ -89,64 +168,65 @@ public class ChunkHolder extends GenerationChunkHolder { - // CraftBukkit end - - public CompletableFuture<ChunkResult<LevelChunk>> getTickingChunkFuture() { -- return this.tickingChunkFuture; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public CompletableFuture<ChunkResult<LevelChunk>> getEntityTickingChunkFuture() { -- return this.entityTickingChunkFuture; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public CompletableFuture<ChunkResult<LevelChunk>> getFullChunkFuture() { -- return this.fullChunkFuture; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Nullable - public final LevelChunk getTickingChunk() { // Paper - final for inline -- return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error -+ // Paper start - rewrite chunk system -+ if (this.newChunkHolder.isTickingReady()) { -+ if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) { -+ return levelChunk; -+ } // else: race condition: chunk unload -+ } -+ return null; -+ // Paper end - rewrite chunk system - } - - @Nullable - public LevelChunk getChunkToSend() { -- return !this.sendSync.isDone() ? null : this.getTickingChunk(); -+ // Paper start - rewrite chunk system -+ final LevelChunk ret = this.moonrise$getFullChunk(); -+ if (ret != null && this.isRadiusLoaded(1)) { -+ return ret; -+ } -+ return null; -+ // Paper end - rewrite chunk system - } - - public CompletableFuture<?> getSendSyncFuture() { -- return this.sendSync; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public void addSendDependency(CompletableFuture<?> postProcessingFuture) { -- if (this.sendSync.isDone()) { -- this.sendSync = postProcessingFuture; -- } else { -- this.sendSync = this.sendSync.thenCombine(postProcessingFuture, (object, object1) -> { -- return null; -- }); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - - } - - public CompletableFuture<?> getSaveSyncFuture() { -- return this.saveSync; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public boolean isReadyForSaving() { -- return this.saveSync.isDone(); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Override - protected void addSaveDependency(CompletableFuture<?> savingFuture) { -- if (this.saveSync.isDone()) { -- this.saveSync = savingFuture; -- } else { -- this.saveSync = this.saveSync.thenCombine(savingFuture, (object, object1) -> { -- return null; -- }); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - - } - - public boolean blockChanged(BlockPos pos) { -- LevelChunk chunk = this.getTickingChunk(); -+ LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system - - if (chunk == null) { - return false; -@@ -172,7 +252,7 @@ public class ChunkHolder extends GenerationChunkHolder { - return false; - } else { - ichunkaccess.markUnsaved(); -- LevelChunk chunk = this.getTickingChunk(); -+ LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system - - if (chunk == null) { - return false; -@@ -207,7 +287,7 @@ public class ChunkHolder extends GenerationChunkHolder { - List list; - - if (!this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) { -- list = this.playerProvider.getPlayers(this.pos, true); -+ list = this.moonrise$getPlayers(true); // Paper - rewrite chunk system - if (!list.isEmpty()) { - ClientboundLightUpdatePacket packetplayoutlightupdate = new ClientboundLightUpdatePacket(chunk.getPos(), this.lightEngine, this.skyChangedLightSectionFilter, this.blockChangedLightSectionFilter); - -@@ -219,7 +299,7 @@ public class ChunkHolder extends GenerationChunkHolder { - } - - if (this.hasChangedSections) { -- list = this.playerProvider.getPlayers(this.pos, false); -+ list = this.moonrise$getPlayers(false); // Paper - rewrite chunk system - - for (int i = 0; i < this.changedBlocksPerSection.length; ++i) { - ShortSet shortset = this.changedBlocksPerSection[i]; -@@ -285,201 +365,48 @@ public class ChunkHolder extends GenerationChunkHolder { - - @Override - public int getTicketLevel() { -- return this.ticketLevel; -+ return this.newChunkHolder.getTicketLevel(); // Paper - rewrite chunk system - } - - @Override - public int getQueueLevel() { -- return this.queueLevel; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private void setQueueLevel(int level) { -- this.queueLevel = level; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public void setTicketLevel(int level) { -- this.ticketLevel = level; -+ // Paper - rewrite chunk system - } - - private void scheduleFullChunkPromotion(ChunkMap chunkLoadingManager, CompletableFuture<ChunkResult<LevelChunk>> chunkFuture, Executor executor, FullChunkStatus target) { -- this.pendingFullStateConfirmation.cancel(false); -- CompletableFuture<Void> completablefuture1 = new CompletableFuture(); -- -- completablefuture1.thenRunAsync(() -> { -- chunkLoadingManager.onFullChunkStatusChange(this.pos, target); -- }, executor); -- this.pendingFullStateConfirmation = completablefuture1; -- chunkFuture.thenAccept((chunkresult) -> { -- chunkresult.ifSuccess((chunk) -> { -- completablefuture1.complete(null); // CraftBukkit - decompile error -- }); -- }); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private void demoteFullChunk(ChunkMap chunkLoadingManager, FullChunkStatus target) { -- this.pendingFullStateConfirmation.cancel(false); -- chunkLoadingManager.onFullChunkStatusChange(this.pos, target); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - // CraftBukkit start - // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins. - // SPIGOT-7780: Moved out of updateFutures to call all chunk unload events before calling updateHighestAllowedStatus for all chunks - protected void callEventIfUnloading(ChunkMap playerchunkmap) { -- FullChunkStatus oldFullChunkStatus = ChunkLevel.fullStatus(this.oldTicketLevel); -- FullChunkStatus newFullChunkStatus = ChunkLevel.fullStatus(this.ticketLevel); -- boolean oldIsFull = oldFullChunkStatus.isOrAfter(FullChunkStatus.FULL); -- boolean newIsFull = newFullChunkStatus.isOrAfter(FullChunkStatus.FULL); -- if (oldIsFull && !newIsFull) { -- this.getFullChunkFuture().thenAccept((either) -> { -- LevelChunk chunk = (LevelChunk) either.orElse(null); -- if (chunk != null) { -- playerchunkmap.callbackExecutor.execute(() -> { -- // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick -- // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag. -- // These actions may however happen deferred, so we manually set the needsSaving flag already here. -- chunk.markUnsaved(); -- chunk.unloadCallback(); -- }); -- } -- }).exceptionally((throwable) -> { -- // ensure exceptions are printed, by default this is not the case -- MinecraftServer.LOGGER.error("Failed to schedule unload callback for chunk " + ChunkHolder.this.pos, throwable); -- return null; -- }); -- -- // Run callback right away if the future was already done -- playerchunkmap.callbackExecutor.run(); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - // CraftBukkit end - - protected void updateFutures(ChunkMap chunkLoadingManager, Executor executor) { -- FullChunkStatus fullchunkstatus = ChunkLevel.fullStatus(this.oldTicketLevel); -- FullChunkStatus fullchunkstatus1 = ChunkLevel.fullStatus(this.ticketLevel); -- boolean flag = fullchunkstatus.isOrAfter(FullChunkStatus.FULL); -- boolean flag1 = fullchunkstatus1.isOrAfter(FullChunkStatus.FULL); -- -- this.wasAccessibleSinceLastSave |= flag1; -- if (!flag && flag1) { -- int expectCreateCount = ++this.fullChunkCreateCount; // Paper -- this.fullChunkFuture = chunkLoadingManager.prepareAccessibleChunk(this); -- this.scheduleFullChunkPromotion(chunkLoadingManager, this.fullChunkFuture, executor, FullChunkStatus.FULL); -- // Paper start - cache ticking ready status -- this.fullChunkFuture.thenAccept(chunkResult -> { -- chunkResult.ifSuccess(chunk -> { -- if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) { -- ChunkHolder.this.isFullChunkReady = true; -- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkBorder(chunk, this); -- } -- }); -- }); -- // Paper end - cache ticking ready status -- this.addSaveDependency(this.fullChunkFuture); -- } -- -- if (flag && !flag1) { -- // Paper start -- if (this.isFullChunkReady) { -- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper -- } -- // Paper end -- this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); -- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; -- } -- -- boolean flag2 = fullchunkstatus.isOrAfter(FullChunkStatus.BLOCK_TICKING); -- boolean flag3 = fullchunkstatus1.isOrAfter(FullChunkStatus.BLOCK_TICKING); -- -- if (!flag2 && flag3) { -- this.tickingChunkFuture = chunkLoadingManager.prepareTickingChunk(this); -- this.scheduleFullChunkPromotion(chunkLoadingManager, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING); -- // Paper start - cache ticking ready status -- this.tickingChunkFuture.thenAccept(chunkResult -> { -- chunkResult.ifSuccess(chunk -> { -- // note: Here is a very good place to add callbacks to logic waiting on this. -- ChunkHolder.this.isTickingReady = true; -- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkTicking(chunk, this); -- }); -- }); -- // Paper end -- this.addSaveDependency(this.tickingChunkFuture); -- } -- -- if (flag2 && !flag3) { -- // Paper start -- if (this.isTickingReady) { -- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper -- } -- // Paper end -- this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage -- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; -- } -- -- boolean flag4 = fullchunkstatus.isOrAfter(FullChunkStatus.ENTITY_TICKING); -- boolean flag5 = fullchunkstatus1.isOrAfter(FullChunkStatus.ENTITY_TICKING); -- -- if (!flag4 && flag5) { -- if (this.entityTickingChunkFuture != ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE) { -- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException()); -- } -- -- this.entityTickingChunkFuture = chunkLoadingManager.prepareEntityTickingChunk(this); -- this.scheduleFullChunkPromotion(chunkLoadingManager, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING); -- // Paper start - cache ticking ready status -- this.entityTickingChunkFuture.thenAccept(chunkResult -> { -- chunkResult.ifSuccess(chunk -> { -- ChunkHolder.this.isEntityTickingReady = true; -- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkEntityTicking(chunk, this); -- }); -- }); -- // Paper end -- this.addSaveDependency(this.entityTickingChunkFuture); -- } -- -- if (flag4 && !flag5) { -- // Paper start -- if (this.isEntityTickingReady) { -- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); -- } -- // Paper end -- this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage -- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; -- } -- -- if (!fullchunkstatus1.isOrAfter(fullchunkstatus)) { -- this.demoteFullChunk(chunkLoadingManager, fullchunkstatus1); -- } -- -- this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel); -- this.oldTicketLevel = this.ticketLevel; -- // CraftBukkit start -- // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins. -- if (!fullchunkstatus.isOrAfter(FullChunkStatus.FULL) && fullchunkstatus1.isOrAfter(FullChunkStatus.FULL)) { -- this.getFullChunkFuture().thenAccept((either) -> { -- LevelChunk chunk = (LevelChunk) either.orElse(null); -- if (chunk != null) { -- chunkLoadingManager.callbackExecutor.execute(() -> { -- chunk.loadCallback(); -- }); -- } -- }).exceptionally((throwable) -> { -- // ensure exceptions are printed, by default this is not the case -- MinecraftServer.LOGGER.error("Failed to schedule load callback for chunk " + ChunkHolder.this.pos, throwable); -- return null; -- }); -- -- // Run callback right away if the future was already done -- chunkLoadingManager.callbackExecutor.run(); -- } -- // CraftBukkit end -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public boolean wasAccessibleSinceLastSave() { -- return this.wasAccessibleSinceLastSave; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public void refreshAccessibility() { -- this.wasAccessibleSinceLastSave = ChunkLevel.fullStatus(this.ticketLevel).isOrAfter(FullChunkStatus.FULL); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @FunctionalInterface -diff --git a/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<List<ChunkAccess>> UNLOADED_CHUNK_LIST_RESULT = ChunkResult.error("Unloaded chunks found in range"); - private static final CompletableFuture<ChunkResult<List<ChunkAccess>>> UNLOADED_CHUNK_LIST_FUTURE = CompletableFuture.completedFuture(ChunkMap.UNLOADED_CHUNK_LIST_RESULT); -@@ -123,10 +123,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - public static final int MIN_VIEW_DISTANCE = 2; - public static final int MAX_VIEW_DISTANCE = 32; - public static final int FORCED_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING); -- public final Long2ObjectLinkedOpenHashMap<ChunkHolder> updatingChunkMap = new Long2ObjectLinkedOpenHashMap(); -- public volatile Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunkMap; -- private final Long2ObjectLinkedOpenHashMap<ChunkHolder> pendingUnloads; -- private final List<ChunkGenerationTask> pendingGenerationTasks; -+ // Paper - rewrite chunk system - public final ServerLevel level; - private final ThreadedLevelLightEngine lightEngine; - private final BlockableEventLoop<Runnable> mainThreadExecutor; -@@ -136,22 +133,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - private final PoiManager poiManager; - public final LongSet toDrop; - private boolean modified; -- private final ChunkTaskDispatcher worldgenTaskDispatcher; -- private final ChunkTaskDispatcher lightTaskDispatcher; -+ // Paper - rewrite chunk system - public final ChunkProgressListener progressListener; - private final ChunkStatusUpdateListener chunkStatusListener; - public final ChunkMap.ChunkDistanceManager distanceManager; -- private final AtomicInteger tickingGenerated; -+ public final AtomicInteger tickingGenerated; // Paper - public - private final String storageName; - private final PlayerMap playerMap; - public final Int2ObjectMap<ChunkMap.TrackedEntity> entityMap; - private final Long2ByteMap chunkTypeCache; -- private final Long2LongMap nextChunkSaveTime; -- private final LongSet chunksToEagerlySave; -- private final Queue<Runnable> unloadQueue; -- private final AtomicInteger activeChunkWrites; -+ // Paper - rewrite chunk system - public int serverViewDistance; -- private final WorldGenContext worldGenContext; -+ public final WorldGenContext worldGenContext; // Paper - public - - // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback() - public final CallbackExecutor callbackExecutor = new CallbackExecutor(); -@@ -176,24 +169,26 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - // Paper start - public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) { -- return this.pendingUnloads.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ return null; // Paper - rewrite chunk system - } - // Paper end -+ // Paper start - rewrite chunk system -+ @Override -+ public final void moonrise$writeFinishCallback(final ChunkPos pos) throws IOException { -+ // see ChunkStorage#write -+ this.handleLegacyStructureIndex(pos); -+ } -+ // Paper end - rewrite chunk system - - public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) { - super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync); -- this.visibleChunkMap = this.updatingChunkMap.clone(); -- this.pendingUnloads = new Long2ObjectLinkedOpenHashMap(); -- this.pendingGenerationTasks = new ArrayList(); -+ // Paper - rewrite chunk system - this.toDrop = new LongOpenHashSet(); - this.tickingGenerated = new AtomicInteger(); - this.playerMap = new PlayerMap(); - this.entityMap = new Int2ObjectOpenHashMap(); - this.chunkTypeCache = new Long2ByteOpenHashMap(); -- this.nextChunkSaveTime = new Long2LongOpenHashMap(); -- this.chunksToEagerlySave = new LongLinkedOpenHashSet(); -- this.unloadQueue = Queues.newConcurrentLinkedQueue(); -- this.activeChunkWrites = new AtomicInteger(); -+ // Paper - rewrite chunk system - Path path = session.getDimensionPath(world.dimension()); - - this.storageName = path.getFileName().toString(); -@@ -221,18 +216,16 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - this.chunkStatusListener = chunkStatusChangeListener; - ConsecutiveExecutor consecutiveexecutor1 = new ConsecutiveExecutor(executor, "light"); - -- this.worldgenTaskDispatcher = new ChunkTaskDispatcher(consecutiveexecutor, executor); -- this.lightTaskDispatcher = new ChunkTaskDispatcher(consecutiveexecutor1, executor); -- this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), consecutiveexecutor1, this.lightTaskDispatcher); -+ this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), consecutiveexecutor1, null); // Paper - rewrite chunk system - this.distanceManager = new ChunkMap.ChunkDistanceManager(executor, mainThreadExecutor); - this.overworldDataStorage = persistentStateManagerFactory; - this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world.getServer(), world); - this.setServerViewDistance(viewDistance); -- this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, mainThreadExecutor, this::setChunkUnsaved); -+ this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, null, this::setChunkUnsaved); // Paper - rewrite chunk system - } - - private void setChunkUnsaved(ChunkPos pos) { -- this.chunksToEagerlySave.add(pos.toLong()); -+ // Paper - rewrite chunk system - } - - // Paper start -@@ -263,23 +256,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - boolean isChunkTracked(ServerPlayer player, int chunkX, int chunkZ) { -- return player.getChunkTrackingView().contains(chunkX, chunkZ) && !player.connection.chunkSender.isPending(ChunkPos.asLong(chunkX, chunkZ)); -+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ); // Paper - rewrite chunk system - } - - private boolean isChunkOnTrackedBorder(ServerPlayer player, int chunkX, int chunkZ) { -- if (!this.isChunkTracked(player, chunkX, chunkZ)) { -- return false; -- } else { -- for (int k = -1; k <= 1; ++k) { -- for (int l = -1; l <= 1; ++l) { -- if ((k != 0 || l != 0) && !this.isChunkTracked(player, chunkX + k, chunkZ + l)) { -- return true; -- } -- } -- } -- -- return false; -- } -+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ, true); // Paper - rewrite chunk system - } - - protected ThreadedLevelLightEngine getLightEngine() { -@@ -288,20 +269,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - @Nullable - protected ChunkHolder getUpdatingChunkIfPresent(long pos) { -- return (ChunkHolder) this.updatingChunkMap.get(pos); -+ // Paper start - rewrite chunk system -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos); -+ return holder == null ? null : holder.vanillaChunkHolder; -+ // Paper end - rewrite chunk system - } - - @Nullable - public ChunkHolder getVisibleChunkIfPresent(long pos) { -- return (ChunkHolder) this.visibleChunkMap.get(pos); -+ // Paper start - rewrite chunk system -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos); -+ return holder == null ? null : holder.vanillaChunkHolder; -+ // Paper end - rewrite chunk system - } - - protected IntSupplier getChunkQueueLevel(long pos) { -- return () -> { -- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); -- -- return playerchunk == null ? ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1 : Math.min(playerchunk.getQueueLevel(), ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1); -- }; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public String getChunkDebugData(ChunkPos chunkPos) { -@@ -330,56 +313,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - private CompletableFuture<ChunkResult<List<ChunkAccess>>> getChunkRangeFuture(ChunkHolder centerChunk, int margin, IntFunction<ChunkStatus> distanceToStatus) { -- if (margin == 0) { -- ChunkStatus chunkstatus = (ChunkStatus) distanceToStatus.apply(0); -- -- return centerChunk.scheduleChunkGenerationTask(chunkstatus, this).thenApply((chunkresult) -> { -- return chunkresult.map(List::of); -- }); -- } else { -- int j = Mth.square(margin * 2 + 1); -- List<CompletableFuture<ChunkResult<ChunkAccess>>> list = new ArrayList(j); -- ChunkPos chunkcoordintpair = centerChunk.getPos(); -- -- for (int k = -margin; k <= margin; ++k) { -- for (int l = -margin; l <= margin; ++l) { -- int i1 = Math.max(Math.abs(l), Math.abs(k)); -- long j1 = ChunkPos.asLong(chunkcoordintpair.x + l, chunkcoordintpair.z + k); -- ChunkHolder playerchunk1 = this.getUpdatingChunkIfPresent(j1); -- -- if (playerchunk1 == null) { -- return ChunkMap.UNLOADED_CHUNK_LIST_FUTURE; -- } -- -- ChunkStatus chunkstatus1 = (ChunkStatus) distanceToStatus.apply(i1); -- -- list.add(playerchunk1.scheduleChunkGenerationTask(chunkstatus1, this)); -- } -- } -- -- return Util.sequence(list).thenApply((list1) -> { -- List<ChunkAccess> list2 = new ArrayList(list1.size()); -- Iterator iterator = list1.iterator(); -- -- while (iterator.hasNext()) { -- ChunkResult<ChunkAccess> chunkresult = (ChunkResult) iterator.next(); -- -- if (chunkresult == null) { -- throw this.debugFuturesAndCreateReportedException(new IllegalStateException("At least one of the chunk futures were null"), "n/a"); -- } -- -- ChunkAccess ichunkaccess = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error -- -- if (ichunkaccess == null) { -- return ChunkMap.UNLOADED_CHUNK_LIST_RESULT; -- } -- -- list2.add(ichunkaccess); -- } -- -- return ChunkResult.of(list2); -- }); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public ReportedException debugFuturesAndCreateReportedException(IllegalStateException exception, String details) { -@@ -409,104 +343,30 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - public CompletableFuture<ChunkResult<LevelChunk>> prepareEntityTickingChunk(ChunkHolder holder) { -- return this.getChunkRangeFuture(holder, 2, (i) -> { -- return ChunkStatus.FULL; -- }).thenApply((chunkresult) -> { -- return chunkresult.map((list) -> { -- return (LevelChunk) list.get(list.size() / 2); -- }); -- }); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Nullable - ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k) { -- if (!ChunkLevel.isLoaded(k) && !ChunkLevel.isLoaded(level)) { -- return holder; -- } else { -- if (holder != null) { -- holder.setTicketLevel(level); -- } -- -- if (holder != null) { -- if (!ChunkLevel.isLoaded(level)) { -- this.toDrop.add(pos); -- } else { -- this.toDrop.remove(pos); -- } -- } -- -- if (ChunkLevel.isLoaded(level) && holder == null) { -- holder = (ChunkHolder) this.pendingUnloads.remove(pos); -- if (holder != null) { -- holder.setTicketLevel(level); -- } else { -- holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this::onLevelChange, this); -- // Paper start -- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderCreate(this.level, holder); -- // Paper end -- } -- -- this.updatingChunkMap.put(pos, holder); -- this.modified = true; -- } -- -- return holder; -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private void onLevelChange(ChunkPos pos, IntSupplier levelGetter, int targetLevel, IntConsumer levelSetter) { -- this.worldgenTaskDispatcher.onLevelChange(pos, levelGetter, targetLevel, levelSetter); -- this.lightTaskDispatcher.onLevelChange(pos, levelGetter, targetLevel, levelSetter); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Override - public void close() throws IOException { -- try { -- this.worldgenTaskDispatcher.close(); -- this.lightTaskDispatcher.close(); -- this.poiManager.close(); -- } finally { -- super.close(); -- } -+ throw new UnsupportedOperationException("Use ServerChunkCache#close"); // Paper - rewrite chunk system - - } - - protected void saveAllChunks(boolean flush) { -- if (flush) { -- List<ChunkHolder> list = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper -- MutableBoolean mutableboolean = new MutableBoolean(); -- -- do { -- mutableboolean.setFalse(); -- list.stream().map((playerchunk) -> { -- BlockableEventLoop iasynctaskhandler = this.mainThreadExecutor; -- -- Objects.requireNonNull(playerchunk); -- iasynctaskhandler.managedBlock(playerchunk::isReadyForSaving); -- return playerchunk.getLatestChunk(); -- }).filter((ichunkaccess) -> { -- return ichunkaccess instanceof ImposterProtoChunk || ichunkaccess instanceof LevelChunk; -- }).filter(this::save).forEach((ichunkaccess) -> { -- mutableboolean.setTrue(); -- }); -- } while (mutableboolean.isTrue()); -- -- this.poiManager.flushAll(); -- this.processUnloads(() -> { -- return true; -- }); -- this.flushWorker(); -- } else { -- this.nextChunkSaveTime.clear(); -- long i = Util.getMillis(); -- Iterator<ChunkHolder> objectiterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper -- -- while (objectiterator.hasNext()) { -- ChunkHolder playerchunk = (ChunkHolder) objectiterator.next(); -- -- this.saveChunkIfNeeded(playerchunk, i); -- } -- } -+ // Paper start - rewrite chunk system -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.saveAllChunks( -+ flush, false, false -+ ); -+ // Paper end - rewrite chunk system - - } - -@@ -524,143 +384,29 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - public boolean hasWork() { -- return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || ca.spottedleaf.moonrise.common.util.ChunkSystem.hasAnyChunkHolders(this.level) || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.worldgenTaskDispatcher.hasWork() || this.lightTaskDispatcher.hasWork() || this.distanceManager.hasTickets(); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private void processUnloads(BooleanSupplier shouldKeepTicking) { -- for (LongIterator longiterator = this.toDrop.iterator(); longiterator.hasNext(); longiterator.remove()) { -- long i = longiterator.nextLong(); -- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.get(i); -- -- if (playerchunk != null) { -- this.updatingChunkMap.remove(i); -- this.pendingUnloads.put(i, playerchunk); -- this.modified = true; -- this.scheduleUnload(i, playerchunk); -- } -- } -- -- int j = Math.max(0, this.unloadQueue.size() - 2000); -- -- Runnable runnable; -- -- while ((j > 0 || shouldKeepTicking.getAsBoolean()) && (runnable = (Runnable) this.unloadQueue.poll()) != null) { -- --j; -- runnable.run(); -- } -- -- this.saveChunksEagerly(shouldKeepTicking); -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.processUnloads(); // Paper - rewrite chunk system -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.autoSave(); // Paper - rewrite chunk system - } - - private void saveChunksEagerly(BooleanSupplier shouldKeepTicking) { -- long i = Util.getMillis(); -- int j = 0; -- LongIterator longiterator = this.chunksToEagerlySave.iterator(); -- -- while (j < 20 && this.activeChunkWrites.get() < 128 && shouldKeepTicking.getAsBoolean() && longiterator.hasNext()) { -- long k = longiterator.nextLong(); -- ChunkHolder playerchunk = (ChunkHolder) this.visibleChunkMap.get(k); -- ChunkAccess ichunkaccess = playerchunk != null ? playerchunk.getLatestChunk() : null; -- -- if (ichunkaccess != null && ichunkaccess.isUnsaved()) { -- if (this.saveChunkIfNeeded(playerchunk, i)) { -- ++j; -- longiterator.remove(); -- } -- } else { -- longiterator.remove(); -- } -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - - } - - private void scheduleUnload(long pos, ChunkHolder chunk) { -- CompletableFuture<?> completablefuture = chunk.getSaveSyncFuture(); -- Runnable runnable = () -> { -- CompletableFuture<?> completablefuture1 = chunk.getSaveSyncFuture(); -- -- if (completablefuture1 != completablefuture) { -- this.scheduleUnload(pos, chunk); -- } else { -- ChunkAccess ichunkaccess = chunk.getLatestChunk(); -- // Paper start -- boolean removed; -- if ((removed = this.pendingUnloads.remove(pos, chunk)) && ichunkaccess != null) { -- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunk); -- // Paper end -- LevelChunk chunk1; -- -- if (ichunkaccess instanceof LevelChunk) { -- chunk1 = (LevelChunk) ichunkaccess; -- chunk1.setLoaded(false); -- } -- -- this.save(ichunkaccess); -- if (ichunkaccess instanceof LevelChunk) { -- chunk1 = (LevelChunk) ichunkaccess; -- this.level.unload(chunk1); -- } -- -- this.lightEngine.updateChunkStatus(ichunkaccess.getPos()); -- this.lightEngine.tryScheduleUpdate(); -- this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null); -- this.nextChunkSaveTime.remove(ichunkaccess.getPos().toLong()); -- } else if (removed) { // Paper start -- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunk); -- } // Paper end -- -- } -- }; -- Queue queue = this.unloadQueue; -- -- Objects.requireNonNull(this.unloadQueue); -- completablefuture.thenRunAsync(runnable, queue::add).whenComplete((ovoid, throwable) -> { -- if (throwable != null) { -- ChunkMap.LOGGER.error("Failed to save chunk {}", chunk.getPos(), throwable); -- } -- -- }); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - protected boolean promoteChunkMap() { -- if (!this.modified) { -- return false; -- } else { -- this.visibleChunkMap = this.updatingChunkMap.clone(); -- this.modified = false; -- return true; -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private CompletableFuture<ChunkAccess> scheduleChunkLoad(ChunkPos pos) { -- CompletableFuture<Optional<SerializableChunkData>> completablefuture = this.readChunk(pos).thenApplyAsync((optional) -> { -- return optional.map((nbttagcompound) -> { -- SerializableChunkData serializablechunkdata = SerializableChunkData.parse(this.level, this.level.registryAccess(), nbttagcompound); -- -- if (serializablechunkdata == null) { -- ChunkMap.LOGGER.error("Chunk file at {} is missing level data, skipping", pos); -- } -- -- return serializablechunkdata; -- }); -- }, Util.backgroundExecutor().forName("parseChunk")); -- CompletableFuture<?> completablefuture1 = this.poiManager.prefetch(pos); -- -- return completablefuture.thenCombine(completablefuture1, (optional, object) -> { -- return optional; -- }).thenApplyAsync((optional) -> { -- Profiler.get().incrementCounter("chunkLoad"); -- if (optional.isPresent()) { -- ProtoChunk protochunk = ((SerializableChunkData) optional.get()).read(this.level, this.poiManager, this.storageInfo(), pos); -- -- this.markPosition(pos, protochunk.getPersistedStatus().getChunkType()); -- return protochunk; -- } else { -- return this.createEmptyChunk(pos); -- } -- }, this.mainThreadExecutor).exceptionallyAsync((throwable) -> { -- return this.handleChunkLoadFailure(throwable, pos); -- }, this.mainThreadExecutor); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private ChunkAccess handleChunkLoadFailure(Throwable throwable, ChunkPos chunkPos) { -@@ -716,139 +462,43 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - @Override - public GenerationChunkHolder acquireGeneration(long pos) { -- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.get(pos); -- -- playerchunk.increaseGenerationRefCount(); -- return playerchunk; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Override - public void releaseGeneration(GenerationChunkHolder chunkHolder) { -- chunkHolder.decreaseGenerationRefCount(); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Override - public CompletableFuture<ChunkAccess> applyStep(GenerationChunkHolder chunkHolder, ChunkStep step, StaticCache2D<GenerationChunkHolder> chunks) { -- ChunkPos chunkcoordintpair = chunkHolder.getPos(); -- -- if (step.targetStatus() == ChunkStatus.EMPTY) { -- return this.scheduleChunkLoad(chunkcoordintpair); -- } else { -- try { -- GenerationChunkHolder generationchunkholder1 = (GenerationChunkHolder) chunks.get(chunkcoordintpair.x, chunkcoordintpair.z); -- ChunkAccess ichunkaccess = generationchunkholder1.getChunkIfPresentUnchecked(step.targetStatus().getParent()); -- -- if (ichunkaccess == null) { -- throw new IllegalStateException("Parent chunk missing"); -- } else { -- CompletableFuture<ChunkAccess> completablefuture = step.apply(this.worldGenContext, chunks, ichunkaccess); -- -- this.progressListener.onStatusChange(chunkcoordintpair, step.targetStatus()); -- return completablefuture; -- } -- } catch (Exception exception) { -- exception.getStackTrace(); -- CrashReport crashreport = CrashReport.forThrowable(exception, "Exception generating new chunk"); -- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk to be generated"); -- -- crashreportsystemdetails.setDetail("Status being generated", () -> { -- return step.targetStatus().getName(); -- }); -- crashreportsystemdetails.setDetail("Location", (Object) String.format(Locale.ROOT, "%d,%d", chunkcoordintpair.x, chunkcoordintpair.z)); -- crashreportsystemdetails.setDetail("Position hash", (Object) ChunkPos.asLong(chunkcoordintpair.x, chunkcoordintpair.z)); -- crashreportsystemdetails.setDetail("Generator", (Object) this.generator()); -- this.mainThreadExecutor.execute(() -> { -- throw new ReportedException(crashreport); -- }); -- throw new ReportedException(crashreport); -- } -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Override - public ChunkGenerationTask scheduleGenerationTask(ChunkStatus requestedStatus, ChunkPos pos) { -- ChunkGenerationTask chunkgenerationtask = ChunkGenerationTask.create(this, requestedStatus, pos); -- -- this.pendingGenerationTasks.add(chunkgenerationtask); -- return chunkgenerationtask; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private void runGenerationTask(ChunkGenerationTask loader) { -- GenerationChunkHolder generationchunkholder = loader.getCenter(); -- ChunkTaskDispatcher chunktaskdispatcher = this.worldgenTaskDispatcher; -- Runnable runnable = () -> { -- CompletableFuture<?> completablefuture = loader.runUntilWait(); -- -- if (completablefuture != null) { -- completablefuture.thenRun(() -> { -- this.runGenerationTask(loader); -- }); -- } -- }; -- long i = generationchunkholder.getPos().toLong(); -- -- Objects.requireNonNull(generationchunkholder); -- chunktaskdispatcher.submit(runnable, i, generationchunkholder::getQueueLevel); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Override - public void runGenerationTasks() { -- this.pendingGenerationTasks.forEach(this::runGenerationTask); -- this.pendingGenerationTasks.clear(); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public CompletableFuture<ChunkResult<LevelChunk>> prepareTickingChunk(ChunkHolder holder) { -- CompletableFuture<ChunkResult<List<ChunkAccess>>> completablefuture = this.getChunkRangeFuture(holder, 1, (i) -> { -- return ChunkStatus.FULL; -- }); -- CompletableFuture<ChunkResult<LevelChunk>> completablefuture1 = completablefuture.thenApplyAsync((chunkresult) -> { -- return chunkresult.map((list) -> { -- LevelChunk chunk = (LevelChunk) list.get(list.size() / 2); -- -- chunk.postProcessGeneration(this.level); -- this.level.startTickingChunk(chunk); -- CompletableFuture<?> completablefuture2 = holder.getSendSyncFuture(); -- -- if (completablefuture2.isDone()) { -- this.onChunkReadyToSend(holder, chunk); -- } else { -- completablefuture2.thenAcceptAsync((object) -> { -- this.onChunkReadyToSend(holder, chunk); -- }, this.mainThreadExecutor); -- } -- -- return chunk; -- }); -- }, this.mainThreadExecutor); -- -- completablefuture1.handle((chunkresult, throwable) -> { -- this.tickingGenerated.getAndIncrement(); -- return null; -- }); -- return completablefuture1; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private void onChunkReadyToSend(ChunkHolder chunkHolder, LevelChunk chunk) { -- ChunkPos chunkcoordintpair = chunk.getPos(); -- Iterator iterator = this.playerMap.getAllPlayers().iterator(); -- -- while (iterator.hasNext()) { -- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); -- -- if (entityplayer.getChunkTrackingView().contains(chunkcoordintpair)) { -- ChunkMap.markChunkPendingToSend(entityplayer, chunk); -- } -- } -- -- this.level.getChunkSource().onChunkReadyToSend(chunkHolder); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public CompletableFuture<ChunkResult<LevelChunk>> prepareAccessibleChunk(ChunkHolder holder) { -- return this.getChunkRangeFuture(holder, 1, ChunkLevel::getStatusAroundFullChunk).thenApply((chunkresult) -> { -- return chunkresult.map((list) -> { -- return (LevelChunk) list.get(list.size() / 2); -- }); -- }); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public int getTickingGenerated() { -@@ -856,144 +506,80 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - private boolean saveChunkIfNeeded(ChunkHolder chunkHolder, long currentTime) { -- if (chunkHolder.wasAccessibleSinceLastSave() && chunkHolder.isReadyForSaving()) { -- ChunkAccess ichunkaccess = chunkHolder.getLatestChunk(); -- -- if (!(ichunkaccess instanceof ImposterProtoChunk) && !(ichunkaccess instanceof LevelChunk)) { -- return false; -- } else if (!ichunkaccess.isUnsaved()) { -- return false; -- } else { -- long j = ichunkaccess.getPos().toLong(); -- long k = this.nextChunkSaveTime.getOrDefault(j, -1L); -- -- if (currentTime < k) { -- return false; -- } else { -- boolean flag = this.save(ichunkaccess); -- -- chunkHolder.refreshAccessibility(); -- if (flag) { -- this.nextChunkSaveTime.put(j, currentTime + 10000L); -- } -- -- return flag; -- } -- } -- } else { -- return false; -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public boolean save(ChunkAccess chunk) { -- this.poiManager.flush(chunk.getPos()); -- if (!chunk.tryMarkSaved()) { -- return false; -- } else { -- ChunkPos chunkcoordintpair = chunk.getPos(); -- -- try { -- ChunkStatus chunkstatus = chunk.getPersistedStatus(); -- -- if (chunkstatus.getChunkType() != ChunkType.LEVELCHUNK) { -- if (this.isExistingChunkFull(chunkcoordintpair)) { -- return false; -- } -- -- if (chunkstatus == ChunkStatus.EMPTY && chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid)) { -- return false; -- } -- } -- -- Profiler.get().incrementCounter("chunkSave"); -- this.activeChunkWrites.incrementAndGet(); -- SerializableChunkData serializablechunkdata = SerializableChunkData.copyOf(this.level, chunk); -- -- Objects.requireNonNull(serializablechunkdata); -- CompletableFuture<CompoundTag> completablefuture = CompletableFuture.supplyAsync(serializablechunkdata::write, Util.backgroundExecutor()); -- -- Objects.requireNonNull(completablefuture); -- this.write(chunkcoordintpair, completablefuture::join).handle((ovoid, throwable) -> { -- if (throwable != null) { -- this.level.getServer().reportChunkSaveFailure(throwable, this.storageInfo(), chunkcoordintpair); -- } -- -- this.activeChunkWrites.decrementAndGet(); -- return null; -- }); -- this.markPosition(chunkcoordintpair, chunkstatus.getChunkType()); -- return true; -- } catch (Exception exception) { -- this.level.getServer().reportChunkSaveFailure(exception, this.storageInfo(), chunkcoordintpair); -- return false; -- } -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private boolean isExistingChunkFull(ChunkPos pos) { -- byte b0 = this.chunkTypeCache.get(pos.toLong()); -- -- if (b0 != 0) { -- return b0 == 1; -- } else { -- CompoundTag nbttagcompound; -- -- try { -- nbttagcompound = (CompoundTag) ((Optional) this.readChunk(pos).join()).orElse((Object) null); -- if (nbttagcompound == null) { -- this.markPositionReplaceable(pos); -- return false; -- } -- } catch (Exception exception) { -- ChunkMap.LOGGER.error("Failed to read chunk {}", pos, exception); -- this.markPositionReplaceable(pos); -- return false; -- } -- -- ChunkType chunktype = SerializableChunkData.getChunkTypeFromTag(nbttagcompound); -- -- return this.markPosition(pos, chunktype) == 1; -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public void setServerViewDistance(int watchDistance) { // Paper - public -- int j = Mth.clamp(watchDistance, 2, 32); -- -- if (j != this.serverViewDistance) { -- this.serverViewDistance = j; -- this.distanceManager.updatePlayerTickets(this.serverViewDistance); -- Iterator iterator = this.playerMap.getAllPlayers().iterator(); -- -- while (iterator.hasNext()) { -- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); -- -- this.updateChunkTracking(entityplayer); -- } -+ // Paper start - rewrite chunk system -+ final int clamped = Mth.clamp(watchDistance, 2, ca.spottedleaf.moonrise.common.util.MoonriseConstants.MAX_VIEW_DISTANCE); -+ if (clamped == this.serverViewDistance) { -+ return; - } - -+ this.serverViewDistance = clamped; -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().setLoadDistance(this.serverViewDistance + 1); -+ // Paper end - rewrite chunk system - } - - public int getPlayerViewDistance(ServerPlayer player) { // Paper - public -- return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance); -+ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getSendViewDistance(player); // Paper - rewrite chunk system - } - - private void markChunkPendingToSend(ServerPlayer player, ChunkPos pos) { -- LevelChunk chunk = this.getChunkToSend(pos.toLong()); -- -- if (chunk != null) { -- ChunkMap.markChunkPendingToSend(player, chunk); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - - } - - private static void markChunkPendingToSend(ServerPlayer player, LevelChunk chunk) { -- player.connection.chunkSender.markChunkPendingToSend(chunk); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private static void dropChunk(ServerPlayer player, ChunkPos pos) { -- player.connection.chunkSender.dropChunk(player, pos); -+ // Paper - rewrite chunk system -+ } -+ -+ // Paper start - rewrite chunk system -+ @Override -+ public CompletableFuture<Optional<CompoundTag>> read(final ChunkPos pos) { -+ final CompletableFuture<Optional<CompoundTag>> ret = new CompletableFuture<>(); -+ -+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.loadDataAsync( -+ this.level, pos.x, pos.z, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, -+ (final CompoundTag data, final Throwable thr) -> { -+ if (thr != null) { -+ ret.completeExceptionally(thr); -+ } else { -+ ret.complete(Optional.ofNullable(data)); -+ } -+ }, false -+ ); -+ -+ return ret; -+ } -+ -+ @Override -+ public CompletableFuture<Void> write(final ChunkPos pos, final Supplier<CompoundTag> tag) { -+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.scheduleSave( -+ this.level, pos.x, pos.z, tag.get(), -+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionFileType.CHUNK_DATA -+ ); -+ return null; - } - -+ @Override -+ public void flushWorker() { -+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.flush(this.level); -+ } -+ // Paper end - rewrite chunk system -+ - @Nullable - public LevelChunk getChunkToSend(long pos) { - ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); -@@ -1059,7 +645,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - // CraftBukkit start -- private CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) { -+ public CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) { // Paper - public - return this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, this.generator().getTypeNameForDataFixer(), chunkcoordintpair, this.level); - // CraftBukkit end - } -@@ -1069,7 +655,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - while (longiterator.hasNext()) { - long i = longiterator.nextLong(); -- ChunkHolder playerchunk = (ChunkHolder) this.visibleChunkMap.get(i); -+ ChunkHolder playerchunk = (ChunkHolder) this.getVisibleChunkIfPresent(i); // Paper - rewrite chunk system - - if (playerchunk != null && this.anyPlayerCloseEnoughForSpawningInternal(playerchunk.getPos())) { - callback.accept(playerchunk); -@@ -1084,7 +670,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - boolean anyPlayerCloseEnoughForSpawning(ChunkPos chunkcoordintpair, boolean reducedRange) { -- return !this.distanceManager.hasPlayersNearby(chunkcoordintpair.toLong()) ? false : this.anyPlayerCloseEnoughForSpawningInternal(chunkcoordintpair, reducedRange); -+ return this.anyPlayerCloseEnoughForSpawningInternal(chunkcoordintpair, reducedRange); // Paper - chunk tick iteration optimisation - // Spigot end - } - -@@ -1096,16 +682,20 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos chunkcoordintpair, boolean reducedRange) { - double blockRange; // Paper - use from event - // Spigot end -- Iterator iterator = this.playerMap.getAllPlayers().iterator(); -- -- ServerPlayer entityplayer; -+ // Paper start - chunk tick iteration optimisation -+ final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers().getPlayers( -+ chunkcoordintpair, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.SPAWN_RANGE -+ ); -+ if (players == null) { -+ return false; -+ } - -- do { -- if (!iterator.hasNext()) { -- return false; -- } -+ final ServerPlayer[] raw = players.getRawDataUnchecked(); -+ final int len = players.size(); - -- entityplayer = (ServerPlayer) iterator.next(); -+ Objects.checkFromIndexSize(0, len, raw.length); -+ for (int i = 0; i < len; ++i) { -+ final ServerPlayer entityplayer = raw[i]; - // Paper start - PlayerNaturallySpawnCreaturesEvent - com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event; - blockRange = 16384.0D; -@@ -1115,33 +705,47 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - blockRange = (double) ((event.getSpawnRadius() << 4) * (event.getSpawnRadius() << 4)); - } - // Paper end - PlayerNaturallySpawnCreaturesEvent -- } while (!this.playerIsCloseEnoughForSpawning(entityplayer, chunkcoordintpair, blockRange)); // Spigot -+ if (this.playerIsCloseEnoughForSpawning(entityplayer, chunkcoordintpair, blockRange)) { -+ return true; -+ } -+ } - -- return true; -+ return false; -+ // Paper end - chunk tick iteration optimisation - } - - public List<ServerPlayer> getPlayersCloseForSpawning(ChunkPos pos) { -- long i = pos.toLong(); -+ // Paper start - chunk tick iteration optimisation -+ final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers().getPlayers( -+ pos, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.SPAWN_RANGE -+ ); -+ if (players == null) { -+ return new ArrayList<>(); -+ } - -- if (!this.distanceManager.hasPlayersNearby(i)) { -- return List.of(); -- } else { -- Builder<ServerPlayer> builder = ImmutableList.builder(); -- Iterator iterator = this.playerMap.getAllPlayers().iterator(); -+ List<ServerPlayer> ret = null; - -- while (iterator.hasNext()) { -- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); -+ final ServerPlayer[] raw = players.getRawDataUnchecked(); -+ final int len = players.size(); - -- if (this.playerIsCloseEnoughForSpawning(entityplayer, pos, 16384.0D)) { // Spigot -- builder.add(entityplayer); -+ Objects.checkFromIndexSize(0, len, raw.length); -+ for (int i = 0; i < len; ++i) { -+ final ServerPlayer player = raw[i]; -+ if (this.playerIsCloseEnoughForSpawning(player, pos, 16384.0D)) { // Spigot -+ if (ret == null) { -+ ret = new ArrayList<>(len - i); -+ ret.add(player); -+ } else { -+ ret.add(player); - } - } -- -- return builder.build(); - } -+ -+ return ret == null ? new ArrayList<>() : ret; -+ // Paper end - chunk tick iteration optimisation - } - -- private boolean playerIsCloseEnoughForSpawning(ServerPlayer entityplayer, ChunkPos chunkcoordintpair, double range) { // Spigot -+ public boolean playerIsCloseEnoughForSpawning(ServerPlayer entityplayer, ChunkPos chunkcoordintpair, double range) { // Spigot // Paper - chunk tick iteration optimisation - public - if (entityplayer.isSpectator()) { - return false; - } else { -@@ -1164,19 +768,21 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - this.updatePlayerPos(player); - if (!flag1) { - this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player); -+ ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$addPlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation - } - - player.setChunkTrackingView(ChunkTrackingView.EMPTY); -- this.updateChunkTracking(player); -+ ca.spottedleaf.moonrise.common.util.ChunkSystem.addPlayerToDistanceMaps(this.level, player); // Paper - rewrite chunk system - } else { - SectionPos sectionposition = player.getLastSectionPos(); - - this.playerMap.removePlayer(player); - if (!flag2) { - this.distanceManager.removePlayer(sectionposition, player); -+ ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$removePlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation - } - -- this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY); -+ ca.spottedleaf.moonrise.common.util.ChunkSystem.removePlayerFromDistanceMaps(this.level, player); // Paper - rewrite chunk system - } - - } -@@ -1188,17 +794,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - public void move(ServerPlayer player) { -- ObjectIterator objectiterator = this.entityMap.values().iterator(); -- -- while (objectiterator.hasNext()) { -- ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); -- -- if (playerchunkmap_entitytracker.entity == player) { -- playerchunkmap_entitytracker.updatePlayers(this.level.players()); -- } else { -- playerchunkmap_entitytracker.updatePlayer(player); -- } -- } -+ // Paper - optimise entity tracker - - SectionPos sectionposition = player.getLastSectionPos(); - SectionPos sectionposition1 = SectionPos.of((EntityAccess) player); -@@ -1208,6 +804,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - if (flag2 || flag != flag1) { - this.updatePlayerPos(player); -+ ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, sectionposition, sectionposition1, flag, flag1); // Paper - chunk tick iteration optimisation - if (!flag) { - this.distanceManager.removePlayer(sectionposition, player); - } -@@ -1224,70 +821,30 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - this.playerMap.unIgnorePlayer(player); - } - -- this.updateChunkTracking(player); -+ // Paper - rewrite chunk system - } - -+ ca.spottedleaf.moonrise.common.util.ChunkSystem.updateMaps(this.level, player); // Paper - rewrite chunk system - } - - private void updateChunkTracking(ServerPlayer player) { -- ChunkPos chunkcoordintpair = player.chunkPosition(); -- int i = this.getPlayerViewDistance(player); -- ChunkTrackingView chunktrackingview = player.getChunkTrackingView(); -- -- if (chunktrackingview instanceof ChunkTrackingView.Positioned chunktrackingview_a) { -- if (chunktrackingview_a.center().equals(chunkcoordintpair) && chunktrackingview_a.viewDistance() == i) { -- return; -- } -- } -- -- this.applyChunkTrackingView(player, ChunkTrackingView.of(chunkcoordintpair, i)); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private void applyChunkTrackingView(ServerPlayer player, ChunkTrackingView chunkFilter) { -- if (player.level() == this.level) { -- ChunkTrackingView chunktrackingview1 = player.getChunkTrackingView(); -- -- if (chunkFilter instanceof ChunkTrackingView.Positioned) { -- label15: -- { -- ChunkTrackingView.Positioned chunktrackingview_a = (ChunkTrackingView.Positioned) chunkFilter; -- -- if (chunktrackingview1 instanceof ChunkTrackingView.Positioned) { -- ChunkTrackingView.Positioned chunktrackingview_a1 = (ChunkTrackingView.Positioned) chunktrackingview1; -- -- if (chunktrackingview_a1.center().equals(chunktrackingview_a.center())) { -- break label15; -- } -- } -- -- player.connection.send(new ClientboundSetChunkCacheCenterPacket(chunktrackingview_a.center().x, chunktrackingview_a.center().z)); -- } -- } -- -- ChunkTrackingView.difference(chunktrackingview1, chunkFilter, (chunkcoordintpair) -> { -- this.markChunkPendingToSend(player, chunkcoordintpair); -- }, (chunkcoordintpair) -> { -- ChunkMap.dropChunk(player, chunkcoordintpair); -- }); -- player.setChunkTrackingView(chunkFilter); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Override - public List<ServerPlayer> getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge) { -- Set<ServerPlayer> set = this.playerMap.getAllPlayers(); -- Builder<ServerPlayer> builder = ImmutableList.builder(); -- Iterator iterator = set.iterator(); -- -- while (iterator.hasNext()) { -- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); -- -- if (onlyOnWatchDistanceEdge && this.isChunkOnTrackedBorder(entityplayer, chunkPos.x, chunkPos.z) || !onlyOnWatchDistanceEdge && this.isChunkTracked(entityplayer, chunkPos.x, chunkPos.z)) { -- builder.add(entityplayer); -- } -+ // Paper start - rewrite chunk system -+ final ChunkHolder holder = this.getVisibleChunkIfPresent(chunkPos.toLong()); -+ if (holder == null) { -+ return new ArrayList<>(); -+ } else { -+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)holder).moonrise$getPlayers(onlyOnWatchDistanceEdge); - } -- -- return builder.build(); -+ // Paper end - rewrite chunk system - } - - public void addEntity(Entity entity) { -@@ -1314,6 +871,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - ChunkMap.TrackedEntity playerchunkmap_entitytracker = new ChunkMap.TrackedEntity(entity, i, j, entitytypes.trackDeltas()); - - this.entityMap.put(entity.getId(), playerchunkmap_entitytracker); -+ // Paper start - optimise entity tracker -+ if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity() != null) { -+ throw new IllegalStateException("Entity is already tracked"); -+ } -+ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(playerchunkmap_entitytracker); -+ // Paper end - optimise entity tracker - playerchunkmap_entitytracker.updatePlayers(this.level.players()); - if (entity instanceof ServerPlayer) { - ServerPlayer entityplayer = (ServerPlayer) entity; -@@ -1354,16 +917,38 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - playerchunkmap_entitytracker1.broadcastRemoved(); - } - -+ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(null); // Paper - optimise entity tracker - } - -- protected void tick() { -- Iterator iterator = this.playerMap.getAllPlayers().iterator(); -+ // Paper start - optimise entity tracker -+ private void newTrackerTick() { -+ final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup entityLookup = (ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup)((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getEntityLookup();; - -- while (iterator.hasNext()) { -- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); -+ final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.world.entity.Entity> trackerEntities = entityLookup.trackerEntities; -+ final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked(); -+ for (int i = 0, len = trackerEntities.size(); i < len; ++i) { -+ final Entity entity = trackerEntitiesRaw[i]; -+ final ChunkMap.TrackedEntity tracker = ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity(); -+ if (tracker == null) { -+ continue; -+ } -+ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$tick(((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$getChunkData().nearbyPlayers); -+ if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$hasPlayers() -+ || ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING)) { -+ tracker.serverEntity.sendChanges(); -+ } -+ } -+ } -+ // Paper end - optimise entity tracker - -- this.updateChunkTracking(entityplayer); -+ protected void tick() { -+ // Paper start - optimise entity tracker -+ if (true) { -+ this.newTrackerTick(); -+ return; - } -+ // Paper end - optimise entity tracker -+ // Paper - rewrite chunk system - - List<ServerPlayer> list = Lists.newArrayList(); - List<ServerPlayer> list1 = this.level.players(); -@@ -1466,27 +1051,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - public void waitForLightBeforeSending(ChunkPos centerPos, int radius) { -- int j = radius + 1; -- -- ChunkPos.rangeClosed(centerPos, j).forEach((chunkcoordintpair1) -> { -- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(chunkcoordintpair1.toLong()); -- -- if (playerchunk != null) { -- playerchunk.addSendDependency(this.lightEngine.waitForPendingTasks(chunkcoordintpair1.x, chunkcoordintpair1.z)); -- } -- -- }); -+ // Paper - rewrite chunk system - } - -- public class ChunkDistanceManager extends DistanceManager { // Paper - public -+ public class ChunkDistanceManager extends DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager { // Paper - public // Paper - rewrite chunk system - - protected ChunkDistanceManager(final Executor workerExecutor, final Executor mainThreadExecutor) { - super(workerExecutor, mainThreadExecutor); - } - -+ // Paper start - rewrite chunk system -+ @Override -+ public final ChunkMap moonrise$getChunkMap() { -+ return ChunkMap.this; -+ } -+ // Paper end - rewrite chunk system -+ - @Override - protected boolean isChunkToRemove(long pos) { -- return ChunkMap.this.toDrop.contains(pos); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Nullable -@@ -1502,7 +1085,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - } - -- public class TrackedEntity { -+ public class TrackedEntity implements ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity { // Paper - optimise entity tracker - - public final ServerEntity serverEntity; - final Entity entity; -@@ -1510,6 +1093,89 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - SectionPos lastSectionPos; - public final Set<ServerPlayerConnection> seenBy = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Paper - Perf: optimise map impl - -+ // Paper start - optimise entity tracker -+ private long lastChunkUpdate = -1L; -+ private ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk lastTrackedChunk; -+ -+ @Override -+ public final void moonrise$tick(final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk chunk) { -+ if (chunk == null) { -+ this.moonrise$clearPlayers(); -+ return; -+ } -+ -+ final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> players = chunk.getPlayers(ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.VIEW_DISTANCE); -+ -+ if (players == null) { -+ this.moonrise$clearPlayers(); -+ return; -+ } -+ -+ final long lastChunkUpdate = this.lastChunkUpdate; -+ final long currChunkUpdate = chunk.getUpdateCount(); -+ final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk lastTrackedChunk = this.lastTrackedChunk; -+ this.lastChunkUpdate = currChunkUpdate; -+ this.lastTrackedChunk = chunk; -+ -+ final ServerPlayer[] playersRaw = players.getRawDataUnchecked(); -+ -+ for (int i = 0, len = players.size(); i < len; ++i) { -+ final ServerPlayer player = playersRaw[i]; -+ this.updatePlayer(player); -+ } -+ -+ if (lastChunkUpdate != currChunkUpdate || lastTrackedChunk != chunk) { -+ // need to purge any players possible not in the chunk list -+ for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) { -+ final ServerPlayer player = conn.getPlayer(); -+ if (!players.contains(player)) { -+ this.removePlayer(player); -+ } -+ } -+ } -+ } -+ -+ @Override -+ public final void moonrise$removeNonTickThreadPlayers() { -+ boolean foundToRemove = false; -+ for (final ServerPlayerConnection conn : this.seenBy) { -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(conn.getPlayer())) { -+ foundToRemove = true; -+ break; -+ } -+ } -+ -+ if (!foundToRemove) { -+ return; -+ } -+ -+ for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) { -+ ServerPlayer player = conn.getPlayer(); -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) { -+ this.removePlayer(player); -+ } -+ } -+ } -+ -+ @Override -+ public final void moonrise$clearPlayers() { -+ this.lastChunkUpdate = -1; -+ this.lastTrackedChunk = null; -+ if (this.seenBy.isEmpty()) { -+ return; -+ } -+ for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) { -+ ServerPlayer player = conn.getPlayer(); -+ this.removePlayer(player); -+ } -+ } -+ -+ @Override -+ public final boolean moonrise$hasPlayers() { -+ return !this.seenBy.isEmpty(); -+ } -+ // Paper end - optimise entity tracker -+ - public TrackedEntity(final Entity entity, final int i, final int j, final boolean flag) { - this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast, this.seenBy); // CraftBukkit - this.entity = entity; -@@ -1612,20 +1278,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - private int getEffectiveRange() { -- int i = this.range; -- Iterator iterator = this.entity.getIndirectPassengers().iterator(); -+ // Paper start - optimise entity tracker -+ final Entity entity = this.entity; -+ int range = this.range; - -- while (iterator.hasNext()) { -- Entity entity = (Entity) iterator.next(); -- int j = entity.getType().clientTrackingRange() * 16; -- j = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, j); // Paper -+ if (entity.getPassengers() == ImmutableList.<Entity>of()) { -+ return this.scaledRange(range); -+ } - -- if (j > i) { -- i = j; -- } -+ // note: we change to List -+ final List<Entity> passengers = (List<Entity>)entity.getIndirectPassengers(); -+ for (int i = 0, len = passengers.size(); i < len; ++i) { -+ final Entity passenger = passengers.get(i); -+ // note: max should be branchless -+ range = Math.max(range, ca.spottedleaf.moonrise.common.PlatformHooks.get().modifyEntityTrackingRange(passenger, passenger.getType().clientTrackingRange() << 4)); - } - -- return this.scaledRange(i); -+ return this.scaledRange(range); -+ // Paper end - optimise entity tracker - } - - public void updatePlayers(List<ServerPlayer> players) { -diff --git a/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<ObjectSet<ServerPlayer>> playersPerChunk = new Long2ObjectOpenHashMap(); -- public final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> tickets = new Long2ObjectOpenHashMap(); -- private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); -- private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8); -- private final TickingTracker tickingTicketsTracker = new TickingTracker(); -- private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(32); -- final Set<ChunkHolder> chunksToUpdateFutures = new ReferenceOpenHashSet(); -- final ThrottlingChunkTaskDispatcher ticketDispatcher; -- final LongSet ticketsToRelease = new LongOpenHashSet(); -- final Executor mainThreadExecutor; -+ // Paper - rewrite chunk system -+ // Paper - chunk tick iteration optimisation -+ // Paper - rewrite chunk system - private long ticketTickCounter; -- public int simulationDistance = 10; -+ // Paper - rewrite chunk system - - protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor) { - TaskScheduler<Runnable> taskscheduler = TaskScheduler.wrapExecutor("player ticket throttler", mainThreadExecutor); - -- this.ticketDispatcher = new ThrottlingChunkTaskDispatcher(taskscheduler, workerExecutor, 4); -- this.mainThreadExecutor = mainThreadExecutor; -+ // Paper - rewrite chunk system - } - -- protected void purgeStaleTickets() { -- ++this.ticketTickCounter; -- ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); -- -- while (objectiterator.hasNext()) { -- Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next(); -- Iterator<Ticket<?>> iterator = ((SortedArraySet) entry.getValue()).iterator(); -- boolean flag = false; -- -- while (iterator.hasNext()) { -- Ticket<?> ticket = (Ticket) iterator.next(); -+ // Paper start - rewrite chunk system -+ @Override -+ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager moonrise$getChunkHolderManager() { -+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getChunkTaskScheduler().chunkHolderManager; -+ } -+ // Paper end - rewrite chunk system -+ // Paper start - chunk tick iteration optimisation -+ private final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<ServerPlayer> spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>(); - -- if (ticket.timedOut(this.ticketTickCounter)) { -- iterator.remove(); -- flag = true; -- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); -- } -- } -+ @Override -+ public final void moonrise$addPlayer(final ServerPlayer player, final SectionPos pos) { -+ this.spawnChunkTracker.add(player, pos.x(), pos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); -+ } - -- if (flag) { -- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false); -- } -+ @Override -+ public final void moonrise$removePlayer(final ServerPlayer player, final SectionPos pos) { -+ this.spawnChunkTracker.remove(player); -+ } - -- if (((SortedArraySet) entry.getValue()).isEmpty()) { -- objectiterator.remove(); -- } -+ @Override -+ public final void moonrise$updatePlayer(final ServerPlayer player, -+ final SectionPos oldPos, final SectionPos newPos, -+ final boolean oldIgnore, final boolean newIgnore) { -+ if (newIgnore) { -+ this.spawnChunkTracker.remove(player); -+ } else { -+ this.spawnChunkTracker.addOrUpdate(player, newPos.x(), newPos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); - } -+ } -+ // Paper end - chunk tick iteration optimisation -+ -+ protected void purgeStaleTickets() { -+ this.moonrise$getChunkHolderManager().tick(); // Paper - rewrite chunk system - - } - -@@ -102,105 +101,15 @@ public abstract class DistanceManager { - protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k); - - public boolean runAllUpdates(ChunkMap chunkLoadingManager) { -- this.naturalSpawnChunkCounter.runAllUpdates(); -- this.tickingTicketsTracker.runAllUpdates(); -- this.playerTicketManager.runAllUpdates(); -- int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE); -- boolean flag = i != 0; -- -- if (flag) { -- ; -- } -- -- if (!this.chunksToUpdateFutures.isEmpty()) { -- Iterator iterator = this.chunksToUpdateFutures.iterator(); -- -- ChunkHolder playerchunk; -- -- // CraftBukkit start - SPIGOT-7780: Call chunk unload events before updateHighestAllowedStatus -- while (iterator.hasNext()) { -- playerchunk = (ChunkHolder) iterator.next(); -- playerchunk.callEventIfUnloading(chunkLoadingManager); -- } -- -- iterator = this.chunksToUpdateFutures.iterator(); -- // CraftBukkit end -- -- while (iterator.hasNext()) { -- playerchunk = (ChunkHolder) iterator.next(); -- playerchunk.updateHighestAllowedStatus(chunkLoadingManager); -- } -- -- iterator = this.chunksToUpdateFutures.iterator(); -- -- while (iterator.hasNext()) { -- playerchunk = (ChunkHolder) iterator.next(); -- playerchunk.updateFutures(chunkLoadingManager, this.mainThreadExecutor); -- } -- -- this.chunksToUpdateFutures.clear(); -- return true; -- } else { -- if (!this.ticketsToRelease.isEmpty()) { -- LongIterator longiterator = this.ticketsToRelease.iterator(); -- -- while (longiterator.hasNext()) { -- long j = longiterator.nextLong(); -- -- if (this.getTickets(j).stream().anyMatch((ticket) -> { -- return ticket.getType() == TicketType.PLAYER; -- })) { -- ChunkHolder playerchunk1 = chunkLoadingManager.getUpdatingChunkIfPresent(j); -- -- if (playerchunk1 == null) { -- throw new IllegalStateException(); -- } -- -- CompletableFuture<ChunkResult<LevelChunk>> completablefuture = playerchunk1.getEntityTickingChunkFuture(); -- -- completablefuture.thenAccept((chunkresult) -> { -- this.mainThreadExecutor.execute(() -> { -- this.ticketDispatcher.release(j, () -> { -- }, false); -- }); -- }); -- } -- } -- -- this.ticketsToRelease.clear(); -- } -- -- return flag; -- } -+ return this.moonrise$getChunkHolderManager().processTicketUpdates(); // Paper - rewrite chunk system - } - - boolean addTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean -- SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i); -- int j = DistanceManager.getTicketLevelAt(arraysetsorted); -- Ticket<?> ticket1 = (Ticket) arraysetsorted.addOrGet(ticket); -- -- ticket1.setCreatedTick(this.ticketTickCounter); -- if (ticket.getTicketLevel() < j) { -- this.ticketTracker.update(i, ticket.getTicketLevel(), true); -- } -- -- return ticket == ticket1; // CraftBukkit -+ return this.moonrise$getChunkHolderManager().addTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system - } - - boolean removeTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean -- SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i); -- -- boolean removed = false; // CraftBukkit -- if (arraysetsorted.remove(ticket)) { -- removed = true; // CraftBukkit -- } -- -- if (arraysetsorted.isEmpty()) { -- this.tickets.remove(i); -- } -- -- this.ticketTracker.update(i, DistanceManager.getTicketLevelAt(arraysetsorted), false); -- return removed; // CraftBukkit -+ return this.moonrise$getChunkHolderManager().removeTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system - } - - public <T> void addTicket(TicketType<T> type, ChunkPos pos, int level, T argument) { -@@ -219,13 +128,7 @@ public abstract class DistanceManager { - } - - public <T> boolean addRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) { -- // CraftBukkit end -- Ticket<T> ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); -- long j = chunkcoordintpair.toLong(); -- -- boolean added = this.addTicket(j, ticket); // CraftBukkit -- this.tickingTicketsTracker.addTicket(j, ticket); -- return added; // CraftBukkit -+ return this.moonrise$getChunkHolderManager().addTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system - } - - public <T> void removeRegionTicket(TicketType<T> type, ChunkPos pos, int radius, T argument) { -@@ -234,32 +137,21 @@ public abstract class DistanceManager { - } - - public <T> boolean removeRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) { -- // CraftBukkit end -- Ticket<T> ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); -- long j = chunkcoordintpair.toLong(); -- -- boolean removed = this.removeTicket(j, ticket); // CraftBukkit -- this.tickingTicketsTracker.removeTicket(j, ticket); -- return removed; // CraftBukkit -+ return this.moonrise$getChunkHolderManager().removeTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system - } - - private SortedArraySet<Ticket<?>> getTickets(long position) { -- return (SortedArraySet) this.tickets.computeIfAbsent(position, (j) -> { -- return SortedArraySet.create(4); -- }); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - protected void updateChunkForced(ChunkPos pos, boolean forced) { -- Ticket<ChunkPos> ticket = new Ticket<>(TicketType.FORCED, ChunkMap.FORCED_TICKET_LEVEL, pos); -- long i = pos.toLong(); -- -+ // Paper start - rewrite chunk system - if (forced) { -- this.addTicket(i, ticket); -- this.tickingTicketsTracker.addTicket(i, ticket); -+ this.moonrise$getChunkHolderManager().addTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos); - } else { -- this.removeTicket(i, ticket); -- this.tickingTicketsTracker.removeTicket(i, ticket); -+ this.moonrise$getChunkHolderManager().removeTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos); - } -+ // Paper end - rewrite chunk system - - } - -@@ -270,9 +162,8 @@ public abstract class DistanceManager { - ((ObjectSet) this.playersPerChunk.computeIfAbsent(i, (j) -> { - return new ObjectOpenHashSet(); - })).add(player); -- this.naturalSpawnChunkCounter.update(i, 0, true); -- this.playerTicketManager.update(i, 0, true); -- this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); -+ // Paper - chunk tick iteration optimisation -+ // Paper - rewrite chunk system - } - - public void removePlayer(SectionPos pos, ServerPlayer player) { -@@ -284,160 +175,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<Ticket<?>> arraysetsorted = (SortedArraySet) this.tickets.get(pos); -- -- return arraysetsorted != null && !arraysetsorted.isEmpty() ? ((Ticket) arraysetsorted.first()).toString() : "no_ticket"; -+ return this.moonrise$getChunkHolderManager().getTicketDebugString(pos); // Paper - rewrite chunk system - } - - protected void updatePlayerTickets(int viewDistance) { -- this.playerTicketManager.updateViewDistance(viewDistance); -+ this.moonrise$getChunkMap().setServerViewDistance(viewDistance); // Paper - rewrite chunk system - } - - public void updateSimulationDistance(int simulationDistance) { -- if (simulationDistance != this.simulationDistance) { -- this.simulationDistance = simulationDistance; -- this.tickingTicketsTracker.replacePlayerTicketsLevel(this.getPlayerTicketLevel()); -- } -+ // 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<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next(); -- ChunkPos chunkcoordintpair = new ChunkPos(entry.getLongKey()); -- Iterator iterator = ((SortedArraySet) entry.getValue()).iterator(); -- -- while (iterator.hasNext()) { -- Ticket<?> ticket = (Ticket) iterator.next(); -- -- fileoutputstream.write((chunkcoordintpair.x + "\t" + chunkcoordintpair.z + "\t" + String.valueOf(ticket.getType()) + "\t" + ticket.getTicketLevel() + "\t\n").getBytes(StandardCharsets.UTF_8)); -- } -- } -- } catch (Throwable throwable) { -- try { -- fileoutputstream.close(); -- } catch (Throwable throwable1) { -- throwable.addSuppressed(throwable1); -- } -- -- throw throwable; -- } -- -- fileoutputstream.close(); -- } catch (IOException ioexception) { -- DistanceManager.LOGGER.error("Failed to dump tickets to {}", path, ioexception); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - - } - - @VisibleForTesting - TickingTracker tickingTracker() { -- return this.tickingTicketsTracker; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public LongSet getTickingChunks() { -- return this.tickingTicketsTracker.getTickingChunks(); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public void removeTicketsOnClosing() { -- ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve -- ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); -- -- while (objectiterator.hasNext()) { -- Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next(); -- Iterator<Ticket<?>> iterator = ((SortedArraySet) entry.getValue()).iterator(); -- boolean flag = false; -- -- while (iterator.hasNext()) { -- Ticket<?> ticket = (Ticket) iterator.next(); -- -- if (!immutableset.contains(ticket.getType())) { -- iterator.remove(); -- flag = true; -- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); -- } -- } -- -- if (flag) { -- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false); -- } -- -- if (((SortedArraySet) entry.getValue()).isEmpty()) { -- objectiterator.remove(); -- } -- } -+ // Paper - rewrite chunk system - - } - - public boolean hasTickets() { -- return !this.tickets.isEmpty(); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - // CraftBukkit start - public <T> void removeAllTicketsFor(TicketType<T> ticketType, int ticketLevel, T ticketIdentifier) { -- Ticket<T> target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier); -- -- for (java.util.Iterator<Entry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -- Entry<SortedArraySet<Ticket<?>>> entry = iterator.next(); -- SortedArraySet<Ticket<?>> tickets = entry.getValue(); -- if (tickets.remove(target)) { -- // copied from removeTicket -- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false); -- -- // can't use entry after it's removed -- if (tickets.isEmpty()) { -- iterator.remove(); -- } -- } -- } -+ this.moonrise$getChunkHolderManager().removeAllTicketsFor(ticketType, ticketLevel, ticketIdentifier); // Paper - rewrite chunk system - } - // CraftBukkit end - -+ /* // Paper - rewrite chunk system - private class ChunkTicketTracker extends ChunkTracker { - - private static final int MAX_LEVEL = ChunkLevel.MAX_LEVEL + 1; -@@ -483,7 +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<ChunkAccess> UNLOADED_CHUNK = ChunkResult.error("Unloaded chunk"); - public static final CompletableFuture<ChunkResult<ChunkAccess>> UNLOADED_CHUNK_FUTURE = CompletableFuture.completedFuture(UNLOADED_CHUNK); - protected final ChunkPos pos; -- @Nullable -- private volatile ChunkStatus highestAllowedStatus; -- private final AtomicReference<ChunkStatus> startedWork = new AtomicReference<>(); -- private final AtomicReferenceArray<CompletableFuture<ChunkResult<ChunkAccess>>> futures = new AtomicReferenceArray<>(CHUNK_STATUSES.size()); -- private final AtomicReference<ChunkGenerationTask> task = new AtomicReference<>(); -- private final AtomicInteger generationRefCount = new AtomicInteger(); -- private volatile CompletableFuture<Void> generationSaveSyncFuture = CompletableFuture.completedFuture(null); -+ // Paper - rewrite chunk system - - public GenerationChunkHolder(ChunkPos pos) { - this.pos = pos; -@@ -43,243 +37,96 @@ public abstract class GenerationChunkHolder { - } - - public CompletableFuture<ChunkResult<ChunkAccess>> scheduleChunkGenerationTask(ChunkStatus requestedStatus, ChunkMap chunkLoadingManager) { -- if (this.isStatusDisallowed(requestedStatus)) { -- return UNLOADED_CHUNK_FUTURE; -- } else { -- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.getOrCreateFuture(requestedStatus); -- if (completableFuture.isDone()) { -- return completableFuture; -- } else { -- ChunkGenerationTask chunkGenerationTask = this.task.get(); -- if (chunkGenerationTask == null || requestedStatus.isAfter(chunkGenerationTask.targetStatus)) { -- this.rescheduleChunkTask(chunkLoadingManager, requestedStatus); -- } -- -- return completableFuture; -- } -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - CompletableFuture<ChunkResult<ChunkAccess>> applyStep(ChunkStep step, GeneratingChunkMap chunkLoadingManager, StaticCache2D<GenerationChunkHolder> chunks) { -- if (this.isStatusDisallowed(step.targetStatus())) { -- return UNLOADED_CHUNK_FUTURE; -- } else { -- return this.acquireStatusBump(step.targetStatus()) ? chunkLoadingManager.applyStep(this, step, chunks).handle((chunk, throwable) -> { -- if (throwable != null) { -- CrashReport crashReport = CrashReport.forThrowable(throwable, "Exception chunk generation/loading"); -- MinecraftServer.setFatalException(new ReportedException(crashReport)); -- } else { -- this.completeFuture(step.targetStatus(), chunk); -- } -- -- return ChunkResult.of(chunk); -- }) : this.getOrCreateFuture(step.targetStatus()); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - protected void updateHighestAllowedStatus(ChunkMap chunkLoadingManager) { -- ChunkStatus chunkStatus = this.highestAllowedStatus; -- ChunkStatus chunkStatus2 = ChunkLevel.generationStatus(this.getTicketLevel()); -- this.highestAllowedStatus = chunkStatus2; -- boolean bl = chunkStatus != null && (chunkStatus2 == null || chunkStatus2.isBefore(chunkStatus)); -- if (bl) { -- this.failAndClearPendingFuturesBetween(chunkStatus2, chunkStatus); -- if (this.task.get() != null) { -- this.rescheduleChunkTask(chunkLoadingManager, this.findHighestStatusWithPendingFuture(chunkStatus2)); -- } -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public void replaceProtoChunk(ImposterProtoChunk chunk) { -- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = CompletableFuture.completedFuture(ChunkResult.of(chunk)); -- -- for (int i = 0; i < this.futures.length() - 1; i++) { -- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture2 = this.futures.get(i); -- Objects.requireNonNull(completableFuture2); -- ChunkAccess chunkAccess = completableFuture2.getNow(NOT_DONE_YET).orElse(null); -- if (!(chunkAccess instanceof ProtoChunk)) { -- throw new IllegalStateException("Trying to replace a ProtoChunk, but found " + chunkAccess); -- } -- -- if (!this.futures.compareAndSet(i, completableFuture2, completableFuture)) { -- throw new IllegalStateException("Future changed by other thread while trying to replace it"); -- } -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - void removeTask(ChunkGenerationTask loader) { -- this.task.compareAndSet(loader, null); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private void rescheduleChunkTask(ChunkMap chunkLoadingManager, @Nullable ChunkStatus requestedStatus) { -- ChunkGenerationTask chunkGenerationTask; -- if (requestedStatus != null) { -- chunkGenerationTask = chunkLoadingManager.scheduleGenerationTask(requestedStatus, this.getPos()); -- } else { -- chunkGenerationTask = null; -- } -- -- ChunkGenerationTask chunkGenerationTask3 = this.task.getAndSet(chunkGenerationTask); -- if (chunkGenerationTask3 != null) { -- chunkGenerationTask3.markForCancellation(); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private CompletableFuture<ChunkResult<ChunkAccess>> getOrCreateFuture(ChunkStatus status) { -- if (this.isStatusDisallowed(status)) { -- return UNLOADED_CHUNK_FUTURE; -- } else { -- int i = status.getIndex(); -- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(i); -- -- while (completableFuture == null) { -- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture2 = new CompletableFuture<>(); -- completableFuture = this.futures.compareAndExchange(i, null, completableFuture2); -- if (completableFuture == null) { -- if (this.isStatusDisallowed(status)) { -- this.failAndClearPendingFuture(i, completableFuture2); -- return UNLOADED_CHUNK_FUTURE; -- } -- -- return completableFuture2; -- } -- } -- -- return completableFuture; -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private void failAndClearPendingFuturesBetween(@Nullable ChunkStatus from, ChunkStatus to) { -- int i = from == null ? 0 : from.getIndex() + 1; -- int j = to.getIndex(); -- -- for (int k = i; k <= j; k++) { -- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(k); -- if (completableFuture != null) { -- this.failAndClearPendingFuture(k, completableFuture); -- } -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private void failAndClearPendingFuture(int statusIndex, CompletableFuture<ChunkResult<ChunkAccess>> previousFuture) { -- if (previousFuture.complete(UNLOADED_CHUNK) && !this.futures.compareAndSet(statusIndex, previousFuture, null)) { -- throw new IllegalStateException("Nothing else should replace the future here"); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private void completeFuture(ChunkStatus status, ChunkAccess chunk) { -- ChunkResult<ChunkAccess> chunkResult = ChunkResult.of(chunk); -- int i = status.getIndex(); -- -- while (true) { -- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(i); -- if (completableFuture == null) { -- if (this.futures.compareAndSet(i, null, CompletableFuture.completedFuture(chunkResult))) { -- return; -- } -- } else { -- if (completableFuture.complete(chunkResult)) { -- return; -- } -- -- if (completableFuture.getNow(NOT_DONE_YET).isSuccess()) { -- throw new IllegalStateException("Trying to complete a future but found it to be completed successfully already"); -- } -- -- Thread.yield(); -- } -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Nullable - private ChunkStatus findHighestStatusWithPendingFuture(@Nullable ChunkStatus checkUpperBound) { -- if (checkUpperBound == null) { -- return null; -- } else { -- ChunkStatus chunkStatus = checkUpperBound; -- -- for (ChunkStatus chunkStatus2 = this.startedWork.get(); -- chunkStatus2 == null || chunkStatus.isAfter(chunkStatus2); -- chunkStatus = chunkStatus.getParent() -- ) { -- if (this.futures.get(chunkStatus.getIndex()) != null) { -- return chunkStatus; -- } -- -- if (chunkStatus == ChunkStatus.EMPTY) { -- break; -- } -- } -- -- return null; -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private boolean acquireStatusBump(ChunkStatus nextStatus) { -- ChunkStatus chunkStatus = nextStatus == ChunkStatus.EMPTY ? null : nextStatus.getParent(); -- ChunkStatus chunkStatus2 = this.startedWork.compareAndExchange(chunkStatus, nextStatus); -- if (chunkStatus2 == chunkStatus) { -- return true; -- } else if (chunkStatus2 != null && !nextStatus.isAfter(chunkStatus2)) { -- return false; -- } else { -- throw new IllegalStateException("Unexpected last startedWork status: " + chunkStatus2 + " while trying to start: " + nextStatus); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private boolean isStatusDisallowed(ChunkStatus status) { -- ChunkStatus chunkStatus = this.highestAllowedStatus; -- return chunkStatus == null || status.isAfter(chunkStatus); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - protected abstract void addSaveDependency(CompletableFuture<?> savingFuture); - - public void increaseGenerationRefCount() { -- if (this.generationRefCount.getAndIncrement() == 0) { -- this.generationSaveSyncFuture = new CompletableFuture<>(); -- this.addSaveDependency(this.generationSaveSyncFuture); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public void decreaseGenerationRefCount() { -- CompletableFuture<Void> completableFuture = this.generationSaveSyncFuture; -- int i = this.generationRefCount.decrementAndGet(); -- if (i == 0) { -- completableFuture.complete(null); -- } -- -- if (i < 0) { -- throw new IllegalStateException("More releases than claims. Count: " + i); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Nullable - public ChunkAccess getChunkIfPresentUnchecked(ChunkStatus requestedStatus) { -- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(requestedStatus.getIndex()); -- return completableFuture == null ? null : completableFuture.getNow(NOT_DONE_YET).orElse(null); -+ // Paper start - rewrite chunk system -+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkIfPresentUnchecked(requestedStatus); -+ // Paper end - rewrite chunk system - } - - @Nullable - public ChunkAccess getChunkIfPresent(ChunkStatus requestedStatus) { -- return this.isStatusDisallowed(requestedStatus) ? null : this.getChunkIfPresentUnchecked(requestedStatus); -+ // Paper start - rewrite chunk system -+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkIfPresent(requestedStatus); -+ // Paper end - rewrite chunk system - } - - @Nullable - public ChunkAccess getLatestChunk() { -- ChunkStatus chunkStatus = this.startedWork.get(); -- if (chunkStatus == null) { -- return null; -- } else { -- ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(chunkStatus); -- return chunkAccess != null ? chunkAccess : this.getChunkIfPresentUnchecked(chunkStatus.getParent()); -- } -+ // Paper start - rewrite chunk system -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion(); -+ return lastCompletion == null ? null : lastCompletion.chunk(); -+ // Paper end - rewrite chunk system - } - - @Nullable - public ChunkStatus getPersistedStatus() { -- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(ChunkStatus.EMPTY.getIndex()); -- ChunkAccess chunkAccess = completableFuture == null ? null : completableFuture.getNow(NOT_DONE_YET).orElse(null); -- return chunkAccess == null ? null : chunkAccess.getPersistedStatus(); -+ // Paper start - rewrite chunk system -+ final ChunkAccess chunk = this.getLatestChunk(); -+ return chunk == null ? null : chunk.getPersistedStatus(); -+ // Paper end - rewrite chunk system - } - - public ChunkPos getPos() { -@@ -287,7 +134,7 @@ public abstract class GenerationChunkHolder { - } - - public FullChunkStatus getFullStatus() { -- return ChunkLevel.fullStatus(this.getTicketLevel()); -+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkStatus(); // Paper - rewrite chunk system - } - - public abstract int getTicketLevel(); -@@ -296,26 +143,15 @@ public abstract class GenerationChunkHolder { - - @VisibleForDebug - public List<Pair<ChunkStatus, CompletableFuture<ChunkResult<ChunkAccess>>>> getAllFutures() { -- List<Pair<ChunkStatus, CompletableFuture<ChunkResult<ChunkAccess>>>> list = new ArrayList<>(); -- -- for (int i = 0; i < CHUNK_STATUSES.size(); i++) { -- list.add(Pair.of(CHUNK_STATUSES.get(i), this.futures.get(i))); -- } -- -- return list; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Nullable - @VisibleForDebug - public ChunkStatus getLatestStatus() { -- for (int i = CHUNK_STATUSES.size() - 1; i >= 0; i--) { -- ChunkStatus chunkStatus = CHUNK_STATUSES.get(i); -- ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(chunkStatus); -- if (chunkAccess != null) { -- return chunkStatus; -- } -- } -- -- return null; -+ // Paper start - rewrite chunk system -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion(); -+ return lastCompletion == null ? null : lastCompletion.genStatus(); -+ // Paper end - rewrite chunk system - } - } -diff --git a/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<ChunkAccess> 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<ServerPlayer> players = nearbyPlayers.getPlayers(ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.SPAWN_RANGE); -+ -+ if (players == null) { -+ return false; -+ } -+ -+ final ServerPlayer[] raw = players.getRawDataUnchecked(); -+ final int len = players.size(); -+ -+ Objects.checkFromIndexSize(0, len, raw.length); -+ for (int i = 0; i < len; ++i) { -+ if (chunkMap.playerIsCloseEnoughForSpawning(raw[i], chunkPos, 16384.0D)) { // Spigot (reducedRange = false) -+ return true; -+ } -+ } -+ -+ return false; -+ } -+ // Paper end - chunk tick iteration optimisations -+ - - public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory) { - this.level = world; -@@ -112,13 +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<ChunkResult<ChunkAccess>> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create); -- ServerChunkCache.MainThreadExecutor chunkproviderserver_b = this.mainThreadProcessor; -- -- Objects.requireNonNull(completablefuture); -- chunkproviderserver_b.managedBlock(completablefuture::isDone); -- // com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads -- ChunkResult<ChunkAccess> chunkresult = (ChunkResult) completablefuture.join(); -- ChunkAccess ichunkaccess1 = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error -- -- if (ichunkaccess1 == null && create) { -- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("Chunk not there when requested: " + chunkresult.getError())); -- } else { -- this.storeInCache(k, ichunkaccess1, leastStatus); -- return ichunkaccess1; -- } -+ return create ? this.getChunkFallback(x, z, leastStatus, create) : null; - } -+ -+ return this.getChunkFallback(x, z, leastStatus, create); -+ // Paper end - rewrite chunk system - } - - @Nullable - @Override - public LevelChunk getChunkNow(int chunkX, int chunkZ) { -- if (Thread.currentThread() != this.mainThread) { -- return null; -- } else { -- return this.getChunkAtIfLoadedMainThread(chunkX, chunkZ); // Paper - Perf: Optimise getChunkAt calls for loaded chunks -+ // Paper start - rewrite chunk system -+ final LevelChunk ret = this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ if (!ca.spottedleaf.moonrise.common.PlatformHooks.get().hasCurrentlyLoadingChunk()) { -+ return ret; -+ } -+ -+ if (ret != null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { -+ return ret; -+ } -+ -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler() -+ .chunkHolderManager.getChunkHolder(chunkX, chunkZ); -+ if (holder == null) { -+ return ret; - } -+ -+ return ca.spottedleaf.moonrise.common.PlatformHooks.get().getCurrentlyLoadingChunk(holder.vanillaChunkHolder); -+ // Paper end - rewrite chunk system - } - - private void clearCache() { -@@ -272,56 +350,59 @@ public class ServerChunkCache extends ChunkSource { - } - - private CompletableFuture<ChunkResult<ChunkAccess>> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { -- ChunkPos chunkcoordintpair = new ChunkPos(chunkX, chunkZ); -- long k = chunkcoordintpair.toLong(); -- int l = ChunkLevel.byStatus(leastStatus); -- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k); -- -- // CraftBukkit start - don't add new ticket for currently unloading chunk -- boolean currentlyUnloading = false; -- if (playerchunk != null) { -- FullChunkStatus oldChunkState = ChunkLevel.fullStatus(playerchunk.oldTicketLevel); -- FullChunkStatus currentChunkState = ChunkLevel.fullStatus(playerchunk.getTicketLevel()); -- currentlyUnloading = (oldChunkState.isOrAfter(FullChunkStatus.FULL) && !currentChunkState.isOrAfter(FullChunkStatus.FULL)); -- } -- if (create && !currentlyUnloading) { -- // CraftBukkit end -- this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair); -- if (this.chunkAbsent(playerchunk, l)) { -- ProfilerFiller gameprofilerfiller = Profiler.get(); -- -- gameprofilerfiller.push("chunkLoad"); -- this.runDistanceManagerUpdates(); -- playerchunk = this.getVisibleChunkIfPresent(k); -- gameprofilerfiller.pop(); -- if (this.chunkAbsent(playerchunk, l)) { -- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("No chunk holder after ticket has been added")); -- } -- } -+ // Paper start - rewrite chunk system -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Scheduling chunk load off-main"); -+ -+ final int minLevel = ChunkLevel.byStatus(leastStatus); -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ); -+ -+ final boolean needsFullScheduling = leastStatus == ChunkStatus.FULL && (chunkHolder == null || !chunkHolder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)); -+ -+ if ((chunkHolder == null || chunkHolder.getTicketLevel() > minLevel || needsFullScheduling) && !create) { -+ return ChunkHolder.UNLOADED_CHUNK_FUTURE; - } - -- return this.chunkAbsent(playerchunk, l) ? GenerationChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.scheduleChunkGenerationTask(leastStatus, this.chunkMap); -- } -+ final ChunkAccess ifPresent = chunkHolder == null ? null : chunkHolder.getChunkIfPresent(leastStatus); -+ if (needsFullScheduling || ifPresent == null) { -+ // schedule -+ final CompletableFuture<ChunkResult<ChunkAccess>> ret = new CompletableFuture<>(); -+ final Consumer<ChunkAccess> complete = (ChunkAccess chunk) -> { -+ if (chunk == null) { -+ ret.complete(ChunkHolder.UNLOADED_CHUNK); -+ } else { -+ ret.complete(ChunkResult.of(chunk)); -+ } -+ }; - -- private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) { -- return holder == null || holder.oldTicketLevel > maxLevel; // CraftBukkit using oldTicketLevel for isLoaded checks -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().scheduleChunkLoad( -+ chunkX, chunkZ, leastStatus, true, -+ ca.spottedleaf.concurrentutil.util.Priority.HIGHER, -+ complete -+ ); -+ -+ return ret; -+ } else { -+ // can return now -+ return CompletableFuture.completedFuture(ChunkResult.of(ifPresent)); -+ } -+ // Paper end - rewrite chunk system - } - - @Override - public boolean hasChunk(int x, int z) { -- ChunkHolder playerchunk = this.getVisibleChunkIfPresent((new ChunkPos(x, z)).toLong()); -- int k = ChunkLevel.byStatus(ChunkStatus.FULL); -- -- return !this.chunkAbsent(playerchunk, k); -+ return this.getChunkNow(x, z) != null; // Paper - rewrite chunk system - } - - @Nullable - @Override - public LightChunk getChunkForLighting(int chunkX, int chunkZ) { -- long k = ChunkPos.asLong(chunkX, chunkZ); -- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k); -- -- return playerchunk == null ? null : playerchunk.getChunkIfPresentUnchecked(ChunkStatus.INITIALIZE_LIGHT.getParent()); -+ // Paper start - rewrite chunk system -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ); -+ if (newChunkHolder == null) { -+ return null; -+ } -+ return newChunkHolder.getChunkIfPresentUnchecked(ChunkStatus.INITIALIZE_LIGHT.getParent()); -+ // Paper end - rewrite chunk system - } - - @Override -@@ -334,30 +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<LevelChunk> chunks) { -- this.chunkMap.forEachSpawnCandidateChunk((playerchunk) -> { -- LevelChunk chunk = playerchunk.getTickingChunk(); -+ // Paper start - chunk tick iteration optimisation -+ final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> tickingChunks = -+ ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)this.level).moonrise$getPlayerTickingChunks(); -+ -+ final ServerChunkCache.ChunkAndHolder[] raw = tickingChunks.getRawDataUnchecked(); -+ final int size = tickingChunks.size(); - -- if (chunk != null && this.level.isNaturalSpawningAllowed(playerchunk.getPos())) { -- chunks.add(chunk); -+ final ChunkMap chunkMap = this.chunkMap; -+ -+ for (int i = 0; i < size; ++i) { -+ final ServerChunkCache.ChunkAndHolder chunkAndHolder = raw[i]; -+ final LevelChunk levelChunk = chunkAndHolder.chunk(); -+ -+ if (!this.isChunkNearPlayer(chunkMap, levelChunk.getPos(), levelChunk)) { -+ continue; - } - -- }); -+ chunks.add(levelChunk); -+ } -+ // Paper end - chunk tick iteration optimisation - } - - private void tickChunks(ProfilerFiller profiler, long timeDelta, List<LevelChunk> chunks) { -@@ -508,7 +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<LevelChunk> chunkConsumer) { -- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); -- -- if (playerchunk != null) { -- ((ChunkResult) playerchunk.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).ifSuccess(chunkConsumer); -+ // Paper start - rewrite chunk system -+ // note: bypass currentlyLoaded from getChunkNow -+ final LevelChunk fullChunk = this.fullChunks.get(pos); -+ if (fullChunk != null) { -+ chunkConsumer.accept(fullChunk); - } -+ // Paper end - rewrite chunk system - - } - -@@ -619,6 +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<Entity> 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<Entity> 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<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) { -- if (Thread.currentThread() != this.thread) { -- this.getChunkSource().mainThreadProcessor.execute(() -> { -- this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad); -- }); -- return; -- } -+ // Paper - rewrite chunk system - int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; - int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; - -@@ -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<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) { -- List<net.minecraft.world.level.chunk.ChunkAccess> ret = new java.util.ArrayList<>(); -- it.unimi.dsi.fastutil.ints.IntArrayList ticketLevels = new it.unimi.dsi.fastutil.ints.IntArrayList(); -- ServerChunkCache chunkProvider = this.getChunkSource(); -+ this.moonrise$loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, priority, onLoad); // Paper - rewrite chunk system -+ } -+ // Paper end -+ -+ // Paper start - optimise getPlayerByUUID -+ @Nullable -+ @Override -+ public Player getPlayerByUUID(UUID uuid) { -+ final Player player = this.getServer().getPlayerList().getPlayer(uuid); -+ return player != null && player.level() == this ? player : null; -+ } -+ // Paper end - optimise getPlayerByUUID -+ // Paper start - rewrite chunk system -+ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder(); -+ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader chunkLoader = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader((ServerLevel)(Object)this); -+ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController entityDataController; -+ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.PoiDataController poiDataController; -+ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController chunkDataController; -+ private final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler; -+ private long lastMidTickFailure; -+ private long tickedBlocksOrFluids; -+ private final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = new ca.spottedleaf.moonrise.common.misc.NearbyPlayers((ServerLevel)(Object)this); -+ private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDERS = new ServerChunkCache.ChunkAndHolder[0]; -+ private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> loadedChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); -+ private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> tickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); -+ private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> entityTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); -+ -+ @Override -+ public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) { -+ return this.chunkSource.getChunkNow(chunkX, chunkZ); -+ } -+ -+ @Override -+ public final ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) { -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ if (newChunkHolder == null) { -+ return null; -+ } -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion(); -+ return lastCompletion == null ? null : lastCompletion.chunk(); -+ } - -- int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1); -- int[] loadedChunks = new int[1]; -+ @Override -+ public final ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus leastStatus) { -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ); -+ if (newChunkHolder == null) { -+ return null; -+ } -+ return newChunkHolder.getChunkIfPresentUnchecked(leastStatus); -+ } -+ -+ @Override -+ public final void moonrise$midTickTasks() { -+ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks(); -+ } -+ -+ @Override -+ public final ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus status) { -+ return this.moonrise$getChunkTaskScheduler().syncLoadNonFull(chunkX, chunkZ, status); -+ } - -- Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++); -+ @Override -+ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler moonrise$getChunkTaskScheduler() { -+ return this.chunkTaskScheduler; -+ } - -- java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> { -+ @Override -+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController moonrise$getChunkDataController() { -+ return this.chunkDataController; -+ } -+ -+ @Override -+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController moonrise$getPoiChunkDataController() { -+ return this.poiDataController; -+ } -+ -+ @Override -+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController moonrise$getEntityChunkDataController() { -+ return this.entityDataController; -+ } -+ -+ @Override -+ public final int moonrise$getRegionChunkShift() { -+ return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(); -+ } -+ -+ @Override -+ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader() { -+ return this.chunkLoader; -+ } -+ -+ @Override -+ public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, -+ final ca.spottedleaf.concurrentutil.util.Priority priority, -+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) { -+ this.moonrise$loadChunksAsync( -+ (pos.getX() - radiusBlocks) >> 4, -+ (pos.getX() + radiusBlocks) >> 4, -+ (pos.getZ() - radiusBlocks) >> 4, -+ (pos.getZ() + radiusBlocks) >> 4, -+ priority, onLoad -+ ); -+ } -+ -+ @Override -+ public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, -+ final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.util.Priority priority, -+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) { -+ this.moonrise$loadChunksAsync( -+ (pos.getX() - radiusBlocks) >> 4, -+ (pos.getX() + radiusBlocks) >> 4, -+ (pos.getZ() - radiusBlocks) >> 4, -+ (pos.getZ() + radiusBlocks) >> 4, -+ chunkStatus, priority, onLoad -+ ); -+ } -+ -+ @Override -+ public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, -+ final ca.spottedleaf.concurrentutil.util.Priority priority, -+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) { -+ this.moonrise$loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, priority, onLoad); -+ } -+ -+ @Override -+ public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, -+ final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.util.Priority priority, -+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) { -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = this.moonrise$getChunkTaskScheduler(); -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager; -+ -+ final int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1); -+ final java.util.concurrent.atomic.AtomicInteger loadedChunks = new java.util.concurrent.atomic.AtomicInteger(); -+ final Long holderIdentifier = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getNextChunkLoadId(); -+ final int ticketLevel = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getTicketLevel(chunkStatus); -+ -+ final List<ChunkAccess> ret = new ArrayList<>(requiredChunks); -+ -+ final java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (final ChunkAccess chunk) -> { - if (chunk != null) { -- int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel()); -- ret.add(chunk); -- ticketLevels.add(ticketLevel); -- chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier); -+ synchronized (ret) { -+ ret.add(chunk); -+ } -+ chunkHolderManager.addTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunk.getPos(), ticketLevel, holderIdentifier); - } -- if (++loadedChunks[0] == requiredChunks) { -+ if (loadedChunks.incrementAndGet() == requiredChunks) { - try { - onLoad.accept(java.util.Collections.unmodifiableList(ret)); - } finally { - for (int i = 0, len = ret.size(); i < len; ++i) { -- ChunkPos chunkPos = ret.get(i).getPos(); -- int ticketLevel = ticketLevels.getInt(i); -+ final ChunkPos chunkPos = ret.get(i).getPos(); - -- chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); -- chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier); -+ chunkHolderManager.removeTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunkPos, ticketLevel, holderIdentifier); - } - } - } -@@ -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<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getLoadedChunks() { -+ return this.loadedChunks; -+ } -+ -+ @Override -+ public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getTickingChunks() { -+ return this.tickingChunks; -+ } -+ -+ @Override -+ public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getEntityTickingChunks() { -+ return this.entityTickingChunks; -+ } -+ -+ @Override -+ public final boolean moonrise$areChunksLoaded(final int fromX, final int fromZ, final int toX, final int toZ) { -+ final ServerChunkCache chunkSource = this.chunkSource; -+ -+ for (int currZ = fromZ; currZ <= toZ; ++currZ) { -+ for (int currX = fromX; currX <= toX; ++currX) { -+ if (!chunkSource.hasChunk(currX, currZ)) { -+ return false; -+ } -+ } -+ } -+ -+ return true; -+ } -+ // Paper end - rewrite chunk system -+ // Paper start - chunk tick iteration -+ private static final ServerChunkCache.ChunkAndHolder[] EMPTY_PLAYER_CHUNK_HOLDERS = new ServerChunkCache.ChunkAndHolder[0]; -+ private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> playerTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_CHUNK_HOLDERS); -+ private final it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap playerTickingRequests = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(); -+ -+ @Override -+ public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getPlayerTickingChunks() { -+ return this.playerTickingChunks; -+ } -+ -+ @Override -+ public final void moonrise$markChunkForPlayerTicking(final LevelChunk chunk) { -+ final ChunkPos pos = chunk.getPos(); -+ if (!this.playerTickingRequests.containsKey(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos))) { -+ return; -+ } -+ -+ this.playerTickingChunks.add(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()); -+ } -+ -+ @Override -+ public final void moonrise$removeChunkForPlayerTicking(final LevelChunk chunk) { -+ this.playerTickingChunks.remove(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()); -+ } -+ -+ @Override -+ public final void moonrise$addPlayerTickingRequest(final int chunkX, final int chunkZ) { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot add ticking request async"); -+ -+ final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ -+ if (this.playerTickingRequests.addTo(chunkKey, 1) != 0) { -+ // already added -+ return; -+ } -+ -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler() -+ .chunkHolderManager.getChunkHolder(chunkKey); -+ -+ if (chunkHolder == null || !chunkHolder.isTickingReady()) { -+ return; -+ } -+ -+ this.playerTickingChunks.add( -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder() -+ ); -+ } -+ -+ @Override -+ public final void moonrise$removePlayerTickingRequest(final int chunkX, final int chunkZ) { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot remove ticking request async"); -+ -+ final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ final int val = this.playerTickingRequests.addTo(chunkKey, -1); -+ -+ if (val <= 0) { -+ throw new IllegalStateException("Negative counter"); -+ } -+ -+ if (val != 1) { -+ // still has at least one request -+ return; -+ } -+ -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler() -+ .chunkHolderManager.getChunkHolder(chunkKey); -+ -+ if (chunkHolder == null || !chunkHolder.isTickingReady()) { -+ return; -+ } -+ -+ this.playerTickingChunks.remove( -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder() -+ ); -+ } -+ // Paper end - chunk tick iteration - - // Add env and gen to constructor, IWorldDataServer -> WorldDataServer - public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { -@@ -379,14 +616,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - DataFixer datafixer = minecraftserver.getFixerUpper(); - EntityPersistentStorage<Entity> entitypersistentstorage = new EntityStorage(new SimpleRegionStorage(new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"), convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, DataFixTypes.ENTITY_CHUNK), this, minecraftserver); - -- this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage); -+ // Paper - rewrite chunk system - StructureTemplateManager structuretemplatemanager = minecraftserver.getStructureManager(); - int j = this.spigotConfig.viewDistance; // Spigot - int k = this.spigotConfig.simulationDistance; // Spigot -- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; -+ // Paper - rewrite chunk system - -- Objects.requireNonNull(this.entityManager); -- this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, persistententitysectionmanager::updateChunkStatus, () -> { -+ this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, null, () -> { // Paper - rewrite chunk system - return minecraftserver.overworld().getDataStorage(); - }); - this.chunkSource.getGeneratorState().ensureStructuresGenerated(); -@@ -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<net.minecraft.world.level.block.state.BlockState> states = section.states; -+ if (!section.isRandomlyTickingBlocks()) { -+ continue; -+ } -+ -+ final ca.spottedleaf.moonrise.common.list.ShortList tickList = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getTickingBlockList(); -+ -+ for (int i = 0; i < tickSpeed; ++i) { -+ final int tickingBlocks = tickList.size(); -+ final int index = simpleRandom.nextInt() & ((16 * 16 * 16) - 1); -+ -+ if (index >= tickingBlocks) { -+ // most of the time we fall here -+ continue; -+ } -+ -+ final int location = (int)tickList.getRaw(index) & 0xFFFF; -+ final BlockState state = states.get(location); -+ -+ // do not use a mutable pos, as some random tick implementations store the input without calling immutable()! -+ final BlockPos pos = new BlockPos((location & 15) | offsetX, ((location >>> (4 + 4)) & 15) | offsetY, ((location >>> 4) & 15) | offsetZ); -+ -+ state.randomTick((ServerLevel)(Object)this, pos, simpleRandom); -+ if (doubleTickFluids) { -+ final FluidState fluidState = state.getFluidState(); -+ if (fluidState.isRandomlyTicking()) { -+ fluidState.randomTick((ServerLevel)(Object)this, pos, simpleRandom); -+ } -+ } -+ } -+ } -+ -+ return; -+ } -+ // Paper end - optimise random ticking -+ - public void tickChunk(LevelChunk chunk, int randomTickSpeed) { -+ final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking - ChunkPos chunkcoordintpair = chunk.getPos(); - boolean flag = this.isRaining(); - int j = chunkcoordintpair.getMinBlockX(); -@@ -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<UUID> stream = entity.getSelfAndPassengers().map(Entity::getUUID); // CraftBukkit - decompile error -- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; -- -- Objects.requireNonNull(this.entityManager); -- if (stream.anyMatch(persistententitysectionmanager::isLoaded)) { -+ if (entity.getSelfAndPassengers().map(Entity::getUUID).anyMatch(this.moonrise$getEntityLookup()::hasEntity)) { // Paper - rewrite chunk system - return false; - } else { - this.addFreshEntityWithPassengers(entity, reason); // CraftBukkit -@@ -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<Entity> getEntities() { - org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot -- return this.entityManager.getEntityGetter(); -+ return this.moonrise$getEntityLookup(); // Paper - rewrite chunk system - } - - public void addLegacyChunkEntities(Stream<Entity> entities) { -- this.entityManager.addLegacyChunkEntities(entities); -+ // Paper start - add chunkpos param -+ this.addLegacyChunkEntities(entities, null); -+ } -+ public void addLegacyChunkEntities(Stream<Entity> entities, ChunkPos chunkPos) { -+ // Paper end - add chunkpos param -+ this.moonrise$getEntityLookup().addLegacyChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system - } - - public void addWorldGenChunkEntities(Stream<Entity> entities) { -- this.entityManager.addWorldGenChunkEntities(entities); -+ // Paper start - add chunkpos param -+ this.addWorldGenChunkEntities(entities, null); -+ } -+ public void addWorldGenChunkEntities(Stream<Entity> entities, ChunkPos chunkPos) { -+ // Paper end - add chunkpos param -+ this.moonrise$getEntityLookup().addWorldGenChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system - } - - public void startTickingChunk(LevelChunk chunk) { -@@ -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<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> lightTasks = new ObjectArrayList<>(); -+ // Paper - rewrite chunk sytem - private final ChunkMap chunkMap; -- private final ChunkTaskDispatcher taskDispatcher; -+ // Paper - rewrite chunk sytem - private final int taskPerBatch = 1000; -- private final AtomicBoolean scheduled = new AtomicBoolean(); -+ // Paper - rewrite chunk sytem -+ -+ // Paper start - rewrite chunk system -+ private final java.util.concurrent.atomic.AtomicLong chunkWorkCounter = new java.util.concurrent.atomic.AtomicLong(); -+ private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, -+ final java.util.function.Supplier<ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.LightQueue.ChunkTasks> supplier) { -+ final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld(); -+ -+ final ChunkAccess center = this.starlight$getLightEngine().getAnyChunkNow(chunkX, chunkZ); -+ if (center == null || !center.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) { -+ // do not accept updates in unlit chunks, unless we might be generating a chunk -+ return; -+ } -+ -+ final ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.ServerLightQueue.ServerChunkTasks scheduledTask = (ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.ServerLightQueue.ServerChunkTasks)supplier.get(); -+ -+ if (scheduledTask == null) { -+ // not scheduled -+ return; -+ } -+ -+ if (!scheduledTask.markTicketAdded()) { -+ // ticket already added -+ return; -+ } -+ -+ final Long ticketId = Long.valueOf(this.chunkWorkCounter.getAndIncrement()); -+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ); -+ world.getChunkSource().addRegionTicket(ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId); -+ -+ scheduledTask.queueOrRunTask(() -> { -+ world.getChunkSource().removeRegionTicket(ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId); -+ }); -+ } -+ -+ @Override -+ public final int starlight$serverRelightChunks(final java.util.Collection<net.minecraft.world.level.ChunkPos> chunks0, -+ final java.util.function.Consumer<net.minecraft.world.level.ChunkPos> chunkLightCallback, -+ final java.util.function.IntConsumer onComplete) { -+ final java.util.Set<net.minecraft.world.level.ChunkPos> chunks = new java.util.LinkedHashSet<>(chunks0); -+ final java.util.Map<net.minecraft.world.level.ChunkPos, Long> ticketIds = new java.util.HashMap<>(); -+ final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld(); -+ -+ for (final java.util.Iterator<net.minecraft.world.level.ChunkPos> iterator = chunks.iterator(); iterator.hasNext();) { -+ final ChunkPos pos = iterator.next(); -+ -+ final Long id = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getNextChunkRelightId(); -+ world.getChunkSource().addRegionTicket(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, id); -+ ticketIds.put(pos, id); -+ -+ final ChunkAccess chunk = (ChunkAccess)world.getChunkSource().getChunkForLighting(pos.x, pos.z); -+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) { -+ // cannot relight this chunk -+ iterator.remove(); -+ ticketIds.remove(pos); -+ world.getChunkSource().removeRegionTicket(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, id); -+ continue; -+ } -+ } -+ -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().radiusAwareScheduler.queueInfiniteRadiusTask(() -> { -+ ThreadedLevelLightEngine.this.starlight$getLightEngine().relightChunks( -+ chunks, -+ (final ChunkPos pos) -> { -+ if (chunkLightCallback != null) { -+ chunkLightCallback.accept(pos); -+ } -+ -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().scheduleChunkTask(pos.x, pos.z, () -> { -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder( -+ pos.x, pos.z -+ ); -+ -+ if (chunkHolder == null) { -+ return; -+ } -+ -+ final java.util.List<ServerPlayer> players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)chunkHolder.vanillaChunkHolder).moonrise$getPlayers(false); -+ -+ if (players.isEmpty()) { -+ return; -+ } -+ -+ final net.minecraft.network.protocol.Packet<?> relightPacket = new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket( -+ pos, (ThreadedLevelLightEngine)(Object)ThreadedLevelLightEngine.this, -+ null, null -+ ); -+ -+ for (final ServerPlayer player : players) { -+ final net.minecraft.server.network.ServerGamePacketListenerImpl conn = player.connection; -+ if (conn != null) { -+ conn.send(relightPacket); -+ } -+ } -+ }); -+ }, -+ (final int relight) -> { -+ if (onComplete != null) { -+ onComplete.accept(relight); -+ } -+ -+ for (final java.util.Map.Entry<ChunkPos, Long> entry : ticketIds.entrySet()) { -+ world.getChunkSource().removeRegionTicket( -+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, entry.getKey(), -+ ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, entry.getValue() -+ ); -+ } -+ } -+ ); -+ }); -+ -+ return chunks.size(); -+ } -+ // Paper end - rewrite chunk system - - public ThreadedLevelLightEngine( - LightChunkGetter chunkProvider, ChunkMap chunkLoadingManager, boolean hasBlockLight, ConsecutiveExecutor processor, ChunkTaskDispatcher executor - ) { - super(chunkProvider, true, hasBlockLight); - this.chunkMap = chunkLoadingManager; -- this.taskDispatcher = executor; -- this.consecutiveExecutor = processor; -+ // Paper - rewrite chunk sytem - } - - @Override -@@ -52,164 +163,73 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - - @Override - public void checkBlock(BlockPos pos) { -- BlockPos blockPos = pos.immutable(); -- this.addTask( -- SectionPos.blockToSectionCoord(pos.getX()), -- SectionPos.blockToSectionCoord(pos.getZ()), -- ThreadedLevelLightEngine.TaskType.PRE_UPDATE, -- Util.name(() -> super.checkBlock(blockPos), () -> "checkBlock " + blockPos) -- ); -+ // Paper start - rewrite chunk system -+ final BlockPos posCopy = pos.immutable(); -+ this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> { -+ return ThreadedLevelLightEngine.this.starlight$getLightEngine().blockChange(posCopy); -+ }); -+ // Paper end - rewrite chunk system - } - - protected void updateChunkStatus(ChunkPos pos) { -- this.addTask(pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { -- super.retainData(pos, false); -- super.setLightEnabled(pos, false); -- -- for (int i = this.getMinLightSection(); i < this.getMaxLightSection(); i++) { -- super.queueSectionData(LightLayer.BLOCK, SectionPos.of(pos, i), null); -- super.queueSectionData(LightLayer.SKY, SectionPos.of(pos, i), null); -- } -- -- for (int j = this.levelHeightAccessor.getMinSectionY(); j <= this.levelHeightAccessor.getMaxSectionY(); j++) { -- super.updateSectionStatus(SectionPos.of(pos, j), true); -- } -- }, () -> "updateChunkStatus " + pos + " true")); -+ // Paper - rewrite chunk system - } - - @Override - public void updateSectionStatus(SectionPos pos, boolean notReady) { -- this.addTask( -- pos.x(), -- pos.z(), -- () -> 0, -- ThreadedLevelLightEngine.TaskType.PRE_UPDATE, -- Util.name(() -> super.updateSectionStatus(pos, notReady), () -> "updateSectionStatus " + pos + " " + notReady) -- ); -+ // Paper start - rewrite chunk system -+ this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> { -+ return ThreadedLevelLightEngine.this.starlight$getLightEngine().sectionChange(pos, notReady); -+ }); -+ // Paper end - rewrite chunk system - } - - @Override - public void propagateLightSources(ChunkPos chunkPos) { -- this.addTask( -- chunkPos.x, -- chunkPos.z, -- ThreadedLevelLightEngine.TaskType.PRE_UPDATE, -- Util.name(() -> super.propagateLightSources(chunkPos), () -> "propagateLight " + chunkPos) -- ); -+ // Paper - rewrite chunk system - } - - @Override - public void setLightEnabled(ChunkPos pos, boolean retainData) { -- this.addTask( -- pos.x, -- pos.z, -- ThreadedLevelLightEngine.TaskType.PRE_UPDATE, -- Util.name(() -> super.setLightEnabled(pos, retainData), () -> "enableLight " + pos + " " + retainData) -- ); -+ // Paper start - rewrite chunk system - } - - @Override - public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) { -- this.addTask( -- pos.x(), -- pos.z(), -- () -> 0, -- ThreadedLevelLightEngine.TaskType.PRE_UPDATE, -- Util.name(() -> super.queueSectionData(lightType, pos, nibbles), () -> "queueData " + pos) -- ); -+ // Paper start - rewrite chunk system - } - - private void addTask(int x, int z, ThreadedLevelLightEngine.TaskType stage, Runnable task) { -- this.addTask(x, z, this.chunkMap.getChunkQueueLevel(ChunkPos.asLong(x, z)), stage, task); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private void addTask(int x, int z, IntSupplier completedLevelSupplier, ThreadedLevelLightEngine.TaskType stage, Runnable task) { -- this.taskDispatcher.submit(() -> { -- this.lightTasks.add(Pair.of(stage, task)); -- if (this.lightTasks.size() >= 1000) { -- this.runUpdate(); -- } -- }, ChunkPos.asLong(x, z), completedLevelSupplier); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Override - public void retainData(ChunkPos pos, boolean retainData) { -- this.addTask( -- pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> super.retainData(pos, retainData), () -> "retainData " + pos) -- ); -+ // Paper start - rewrite chunk system - } - - public CompletableFuture<ChunkAccess> initializeLight(ChunkAccess chunk, boolean bl) { -- ChunkPos chunkPos = chunk.getPos(); -- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { -- LevelChunkSection[] levelChunkSections = chunk.getSections(); -- -- for (int i = 0; i < chunk.getSectionsCount(); i++) { -- LevelChunkSection levelChunkSection = levelChunkSections[i]; -- if (!levelChunkSection.hasOnlyAir()) { -- int j = this.levelHeightAccessor.getSectionYFromSectionIndex(i); -- super.updateSectionStatus(SectionPos.of(chunkPos, j), false); -- } -- } -- }, () -> "initializeLight: " + chunkPos)); -- return CompletableFuture.supplyAsync(() -> { -- super.setLightEnabled(chunkPos, bl); -- super.retainData(chunkPos, false); -- return chunk; -- }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task)); -+ return CompletableFuture.completedFuture(chunk); // Paper start - rewrite chunk system - } - - public CompletableFuture<ChunkAccess> lightChunk(ChunkAccess chunk, boolean excludeBlocks) { -- ChunkPos chunkPos = chunk.getPos(); -- chunk.setLightCorrect(false); -- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { -- if (!excludeBlocks) { -- super.propagateLightSources(chunkPos); -- } -- }, () -> "lightChunk " + chunkPos + " " + excludeBlocks)); -- return CompletableFuture.supplyAsync(() -> { -- chunk.setLightCorrect(true); -- return chunk; -- }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task)); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public void tryScheduleUpdate() { -- if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { -- this.consecutiveExecutor.schedule(() -> { -- this.runUpdate(); -- this.scheduled.set(false); -- }); -- } -+ // Paper - rewrite chunk system - } - - private void runUpdate() { -- int i = Math.min(this.lightTasks.size(), 1000); -- ObjectListIterator<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> objectListIterator = this.lightTasks.iterator(); -- -- int j; -- for (j = 0; objectListIterator.hasNext() && j < i; j++) { -- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair = objectListIterator.next(); -- if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.PRE_UPDATE) { -- pair.getSecond().run(); -- } -- } -- -- objectListIterator.back(j); -- super.runLightUpdates(); -- -- for (int var5 = 0; objectListIterator.hasNext() && var5 < i; var5++) { -- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair2 = objectListIterator.next(); -- if (pair2.getFirst() == ThreadedLevelLightEngine.TaskType.POST_UPDATE) { -- pair2.getSecond().run(); -- } -- -- objectListIterator.remove(); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public CompletableFuture<?> waitForPendingTasks(int x, int z) { -- return CompletableFuture.runAsync(() -> { -- }, callback -> this.addTask(x, z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, callback)); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - static enum TaskType { -diff --git a/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<T> implements Comparable<Ticket<?>> { -+public final class Ticket<T> implements Comparable<Ticket<?>>, ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket<T> { // Paper - rewrite chunk system - private final TicketType<T> type; - private final int ticketLevel; - public final T key; -- private long createdTick; -+ // Paper start - rewrite chunk system -+ private long removeDelay; - -- protected Ticket(TicketType<T> type, int level, T argument) { -+ @Override -+ public final long moonrise$getRemoveDelay() { -+ return this.removeDelay; -+ } -+ -+ @Override -+ public final void moonrise$setRemoveDelay(final long removeDelay) { -+ this.removeDelay = removeDelay; -+ } -+ // Paper end - rewerite chunk system -+ -+ public Ticket(TicketType<T> type, int level, T argument) { // Paper - public - this.type = type; - this.ticketLevel = level; - this.key = argument; -@@ -41,7 +53,7 @@ public final class Ticket<T> implements Comparable<Ticket<?>> { - - @Override - public String toString() { -- return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] at " + this.createdTick; -+ return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] to die in " + this.removeDelay; // Paper - rewrite chunk system - } - - public TicketType<T> getType() { -@@ -53,11 +65,10 @@ public final class Ticket<T> implements Comparable<Ticket<?>> { - } - - protected void setCreatedTick(long tickCreated) { -- this.createdTick = tickCreated; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - protected boolean timedOut(long currentTick) { -- long l = this.type.timeout(); -- return l != 0L && currentTick - this.createdTick > l; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - } -diff --git a/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<GenerationChunkHolder> 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<it.unimi.dsi.fastutil.shorts.ShortArrayList> moonrise$countEntries() { -+ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(); -+ -+ final int size = this.getSize(); -+ for (int index = 0; index < size; ++index) { -+ final int paletteIdx = this.get(index); -+ ret.computeIfAbsent(paletteIdx, (final int key) -> { -+ return new it.unimi.dsi.fastutil.shorts.ShortArrayList(); -+ }).add((short)index); -+ } -+ -+ return ret; -+ } -+ // Paper end - block counting - } -diff --git a/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<K> implements IdMap<K> { -+public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<K> { // Paper - optimise palette reads - private static final int NOT_FOUND = -1; - private static final Object EMPTY_SLOT = null; - private static final float LOADFACTOR = 0.8F; -@@ -17,6 +17,16 @@ public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K> { - private int nextId; - private int size; - -+ // Paper start - optimise palette reads -+ private ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<K> reference; -+ -+ @Override -+ public final K[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<K> src) { -+ this.reference = src; -+ return this.byId; -+ } -+ // Paper end - optimise palette reads -+ - private CrudeIncrementalIntIdentityHashBiMap(int size) { - this.keys = (K[])(new Object[size]); - this.values = new int[size]; -@@ -88,6 +98,12 @@ public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K> { - this.byId = crudeIncrementalIntIdentityHashBiMap.byId; - this.nextId = crudeIncrementalIntIdentityHashBiMap.nextId; - this.size = crudeIncrementalIntIdentityHashBiMap.size; -+ // Paper start - optimise palette reads -+ final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<K> ref = this.reference; -+ if (ref != null) { -+ ref.moonrise$setPalette(this.byId); -+ } -+ // Paper end - optimise palette reads - } - - public void addMapping(K value, int id) { -diff --git a/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<it.unimi.dsi.fastutil.shorts.ShortArrayList> moonrise$countEntries() { -+ final int valuesPerLong = this.valuesPerLong; -+ final int bits = this.bits; -+ final long mask = (1L << bits) - 1L; -+ final int size = this.size; -+ -+ if (bits <= 6) { -+ final it.unimi.dsi.fastutil.shorts.ShortArrayList[] byId = new it.unimi.dsi.fastutil.shorts.ShortArrayList[1 << bits]; -+ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1 << bits); -+ -+ int index = 0; -+ -+ for (long value : this.data) { -+ int li = 0; -+ do { -+ final int paletteIdx = (int)(value & mask); -+ value >>= bits; -+ ++li; -+ -+ final it.unimi.dsi.fastutil.shorts.ShortArrayList coords = byId[paletteIdx]; -+ if (coords != null) { -+ coords.add((short)index++); -+ continue; -+ } else { -+ final it.unimi.dsi.fastutil.shorts.ShortArrayList newCoords = new it.unimi.dsi.fastutil.shorts.ShortArrayList(64); -+ byId[paletteIdx] = newCoords; -+ newCoords.add((short)index++); -+ ret.put(paletteIdx, newCoords); -+ continue; -+ } -+ } while (li < valuesPerLong && index < size); -+ } -+ -+ return ret; -+ } else { -+ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>( -+ 1 << 6 -+ ); -+ -+ int index = 0; -+ -+ for (long value : this.data) { -+ int li = 0; -+ do { -+ final int paletteIdx = (int)(value & mask); -+ value >>= bits; -+ ++li; -+ -+ ret.computeIfAbsent(paletteIdx, (final int key) -> { -+ return new it.unimi.dsi.fastutil.shorts.ShortArrayList(64); -+ }).add((short)index++); -+ } while (li < valuesPerLong && index < size); -+ } -+ -+ return ret; -+ } -+ } -+ // Paper end - block counting -+ - public static class InitializationException extends RuntimeException { - InitializationException(String message) { - super(message); -diff --git a/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<T> extends AbstractSet<T> { -+public class SortedArraySet<T> extends AbstractSet<T> implements ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet<T> { // Paper - rewrite chunk system - private static final int DEFAULT_INITIAL_CAPACITY = 10; - private final Comparator<T> comparator; - T[] contents; - int size; - -+ // Paper start - rewrite chunk system -+ @Override -+ public final boolean removeIf(final java.util.function.Predicate<? super T> filter) { -+ // prev. impl used an iterator, which could be n^2 and creates garbage -+ int i = 0; -+ final int len = this.size; -+ final T[] backingArray = this.contents; -+ -+ for (;;) { -+ if (i >= len) { -+ return false; -+ } -+ if (!filter.test(backingArray[i])) { -+ ++i; -+ continue; -+ } -+ break; -+ } -+ -+ // we only want to write back to backingArray if we really need to -+ -+ int lastIndex = i; // this is where new elements are shifted to -+ -+ for (; i < len; ++i) { -+ final T curr = backingArray[i]; -+ if (!filter.test(curr)) { // if test throws we're screwed -+ backingArray[lastIndex++] = curr; -+ } -+ } -+ -+ // cleanup end -+ Arrays.fill(backingArray, lastIndex, len, null); -+ this.size = lastIndex; -+ return true; -+ } -+ -+ @Override -+ public final T moonrise$replace(final T object) { -+ final int index = this.findIndex(object); -+ if (index >= 0) { -+ final T old = this.contents[index]; -+ this.contents[index] = object; -+ return old; -+ } else { -+ this.addInternal(object, getInsertionPosition(index)); -+ return object; -+ } -+ } -+ -+ @Override -+ public final T moonrise$removeAndGet(final T object) { -+ int i = this.findIndex(object); -+ if (i >= 0) { -+ final T ret = this.contents[i]; -+ this.removeInternal(i); -+ return ret; -+ } else { -+ return null; -+ } -+ } -+ -+ @Override -+ public final SortedArraySet<T> moonrise$copy() { -+ final SortedArraySet<T> ret = SortedArraySet.create(this.comparator, 0); -+ -+ ret.size = this.size; -+ ret.contents = Arrays.copyOf(this.contents, this.size); -+ -+ return ret; -+ } -+ -+ @Override -+ public Object[] moonrise$copyBackingArray() { -+ return this.contents.clone(); -+ } -+ // Paper end - rewrite chunk system -+ - private SortedArraySet(int initialCapacity, Comparator<T> comparator) { - this.comparator = comparator; - if (initialCapacity < 0) { -diff --git a/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<it.unimi.dsi.fastutil.shorts.ShortArrayList> moonrise$countEntries() { -+ final int size = this.size; -+ -+ final short[] raw = new short[size]; -+ for (int i = 0; i < size; ++i) { -+ raw[i] = (short)i; -+ } -+ -+ final it.unimi.dsi.fastutil.shorts.ShortArrayList coordinates = it.unimi.dsi.fastutil.shorts.ShortArrayList.wrap(raw, size); -+ -+ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1); -+ ret.put(0, coordinates); -+ return ret; -+ } -+ // Paper end - block counting - } -diff --git a/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<VoxelShape> voxels, final List<AABB> aabbs, final float stepHeight, -+ final float collidedY) { -+ final FloatArraySet ret = new FloatArraySet(); -+ -+ for (int i = 0, len = voxels.size(); i < len; ++i) { -+ final VoxelShape shape = voxels.get(i); -+ -+ final double[] yCoords = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$rootCoordinatesY(); -+ final double yOffset = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$offsetY(); -+ -+ for (final double yUnoffset : yCoords) { -+ final double y = yUnoffset + yOffset; -+ -+ final float step = (float)(y - box.minY); -+ -+ if (step > stepHeight) { -+ break; -+ } -+ -+ if (step < 0.0f || !(step != collidedY)) { -+ continue; -+ } -+ -+ ret.add(step); -+ } -+ } -+ -+ for (int i = 0, len = aabbs.size(); i < len; ++i) { -+ final AABB shape = aabbs.get(i); -+ -+ final float step1 = (float)(shape.minY - box.minY); -+ final float step2 = (float)(shape.maxY - box.minY); -+ -+ if (!(step1 < 0.0f) && step1 != collidedY && !(step1 > stepHeight)) { -+ ret.add(step1); -+ } -+ -+ if (!(step2 < 0.0f) && step2 != collidedY && !(step2 > stepHeight)) { -+ ret.add(step2); -+ } -+ } -+ -+ final float[] steps = ret.toFloatArray(); -+ FloatArrays.unstableSort(steps); -+ return steps; -+ } -+ // Paper end - optimise collisions -+ // Paper start - optimise entity tracker -+ private net.minecraft.server.level.ChunkMap.TrackedEntity trackedEntity; -+ -+ @Override -+ public final net.minecraft.server.level.ChunkMap.TrackedEntity moonrise$getTrackedEntity() { -+ return this.trackedEntity; -+ } -+ -+ @Override -+ public final void moonrise$setTrackedEntity(final net.minecraft.server.level.ChunkMap.TrackedEntity trackedEntity) { -+ this.trackedEntity = trackedEntity; -+ } -+ -+ private static void collectIndirectPassengers(final List<Entity> into, final List<Entity> from) { -+ for (final Entity passenger : from) { -+ into.add(passenger); -+ collectIndirectPassengers(into, ((Entity)(Object)passenger).passengers); -+ } -+ } -+ // Paper end - optimise entity tracker - - public Entity(EntityType<?> type, Level world) { - this.id = Entity.ENTITY_COUNTER.incrementAndGet(); -@@ -1387,41 +1493,76 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - - private Vec3 collide(Vec3 movement) { -- AABB axisalignedbb = this.getBoundingBox(); -- List<VoxelShape> list = this.level().getEntityCollisions(this, axisalignedbb.expandTowards(movement)); -- Vec3 vec3d1 = movement.lengthSqr() == 0.0D ? movement : Entity.collideBoundingBox(this, movement, axisalignedbb, this.level(), list); -- boolean flag = movement.x != vec3d1.x; -- boolean flag1 = movement.y != vec3d1.y; -- boolean flag2 = movement.z != vec3d1.z; -- boolean flag3 = flag1 && movement.y < 0.0D; -- -- if (this.maxUpStep() > 0.0F && (flag3 || this.onGround()) && (flag || flag2)) { -- AABB axisalignedbb1 = flag3 ? axisalignedbb.move(0.0D, vec3d1.y, 0.0D) : axisalignedbb; -- AABB axisalignedbb2 = axisalignedbb1.expandTowards(movement.x, (double) this.maxUpStep(), movement.z); -- -- if (!flag3) { -- axisalignedbb2 = axisalignedbb2.expandTowards(0.0D, -9.999999747378752E-6D, 0.0D); -- } -+ // Paper start - optimise collisions -+ final boolean xZero = movement.x == 0.0; -+ final boolean yZero = movement.y == 0.0; -+ final boolean zZero = movement.z == 0.0; -+ if (xZero & yZero & zZero) { -+ return movement; -+ } -+ -+ final AABB currentBox = this.getBoundingBox(); -+ -+ final List<VoxelShape> potentialCollisionsVoxel = new ArrayList<>(); -+ final List<AABB> potentialCollisionsBB = new ArrayList<>(); - -- List<VoxelShape> list1 = Entity.collectColliders(this, this.level, list, axisalignedbb2); -- float f = (float) vec3d1.y; -- float[] afloat = Entity.collectCandidateStepUpHeights(axisalignedbb1, list1, this.maxUpStep(), f); -- float[] afloat1 = afloat; -- int i = afloat.length; -+ final AABB initialCollisionBox; -+ if (xZero & zZero) { -+ // note: xZero & zZero -> collision on x/z == 0 -> no step height calculation -+ // this specifically optimises entities standing still -+ initialCollisionBox = movement.y < 0.0 ? -+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutDownwards(currentBox, movement.y) : ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutUpwards(currentBox, movement.y); -+ } else { -+ initialCollisionBox = currentBox.expandTowards(movement); -+ } - -- for (int j = 0; j < i; ++j) { -- float f1 = afloat1[j]; -- Vec3 vec3d2 = Entity.collideWithShapes(new Vec3(movement.x, (double) f1, movement.z), axisalignedbb1, list1); -+ final List<AABB> entityAABBs = new ArrayList<>(); -+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getEntityHardCollisions( -+ this.level, (Entity)(Object)this, initialCollisionBox, entityAABBs, 0, null -+ ); - -- if (vec3d2.horizontalDistanceSqr() > vec3d1.horizontalDistanceSqr()) { -- double d0 = axisalignedbb.minY - axisalignedbb1.minY; -+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder( -+ this.level, (Entity)(Object)this, initialCollisionBox, potentialCollisionsVoxel, potentialCollisionsBB, -+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, null -+ ); -+ potentialCollisionsBB.addAll(entityAABBs); -+ final Vec3 collided = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(movement, currentBox, potentialCollisionsVoxel, potentialCollisionsBB); - -- return vec3d2.add(0.0D, -d0, 0.0D); -- } -+ final boolean collidedX = collided.x != movement.x; -+ final boolean collidedY = collided.y != movement.y; -+ final boolean collidedZ = collided.z != movement.z; -+ -+ final boolean collidedDownwards = collidedY && movement.y < 0.0; -+ -+ final double stepHeight; -+ -+ if ((!collidedDownwards && !this.onGround) || (!collidedX && !collidedZ) || (stepHeight = (double)this.maxUpStep()) <= 0.0) { -+ return collided; -+ } -+ -+ final AABB collidedYBox = collidedDownwards ? currentBox.move(0.0, collided.y, 0.0) : currentBox; -+ AABB stepRetrievalBox = collidedYBox.expandTowards(movement.x, stepHeight, movement.z); -+ if (!collidedDownwards) { -+ stepRetrievalBox = stepRetrievalBox.expandTowards(0.0, (double)-1.0E-5F, 0.0); -+ } -+ -+ final List<VoxelShape> stepVoxels = new ArrayList<>(); -+ final List<AABB> stepAABBs = entityAABBs; -+ -+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder( -+ this.level, (Entity)(Object)this, stepRetrievalBox, stepVoxels, stepAABBs, -+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, null -+ ); -+ -+ for (final float step : calculateStepHeights(collidedYBox, stepVoxels, stepAABBs, (float)stepHeight, (float)collided.y)) { -+ final Vec3 stepResult = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(movement.x, (double)step, movement.z), collidedYBox, stepVoxels, stepAABBs); -+ if (stepResult.horizontalDistanceSqr() > collided.horizontalDistanceSqr()) { -+ return stepResult.add(0.0, collidedYBox.minY - currentBox.minY, 0.0); - } - } - -- return vec3d1; -+ return collided; -+ // Paper end - optimise collisions - } - - private static float[] collectCandidateStepUpHeights(AABB collisionBox, List<VoxelShape> collisions, float f, float stepHeight) { -@@ -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<net.minecraft.world.level.block.state.BlockState> blocks = section.states; -+ -+ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) : 0; -+ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) : 15; -+ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) : 0; -+ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) : 15; -+ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) : 0; -+ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) : 15; -+ -+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) { -+ final int blockY = currY | (currChunkY << 4); -+ mutablePos.setY(blockY); -+ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { -+ final int blockZ = currZ | (currChunkZ << 4); -+ mutablePos.setZ(blockZ); -+ for (int currX = minXIterate; currX <= maxXIterate; ++currX) { -+ final int blockX = currX | (currChunkX << 4); -+ mutablePos.setX(blockX); -+ -+ final BlockState blockState = blocks.get((currX) | (currZ << 4) | ((currY) << 8)); -+ -+ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$emptyCollisionShape() -+ || !blockState.isSuffocating(world, mutablePos)) { -+ continue; -+ } -+ -+ // Yes, it does not use the Entity context stuff. -+ final VoxelShape collisionShape = blockState.getCollisionShape(world, mutablePos); -+ -+ if (collisionShape.isEmpty()) { -+ continue; -+ } -+ -+ final AABB toCollide = boundingBox.move(-(double)blockX, -(double)blockY, -(double)blockZ); -+ -+ final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)collisionShape).moonrise$getSingleAABBRepresentation(); -+ if (singleAABB != null) { -+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(singleAABB, toCollide)) { -+ return true; -+ } -+ continue; -+ } -+ -+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(collisionShape, toCollide)) { -+ return true; -+ } -+ continue; -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ return false; -+ // Paper end - optimise collisions - } - - public InteractionResult interact(Player player, InteractionHand hand) { -@@ -4310,14 +4543,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - - public Iterable<Entity> getIndirectPassengers() { -- // Paper start - Optimize indirect passenger iteration -- if (this.passengers.isEmpty()) { return ImmutableList.of(); } -- ImmutableList.Builder<Entity> indirectPassengers = ImmutableList.builder(); -- for (Entity passenger : this.passengers) { -- indirectPassengers.add(passenger); -- indirectPassengers.addAll(passenger.getIndirectPassengers()); -+ // Paper start - optimise entity tracker -+ final List<Entity> ret = new ArrayList<>(); -+ -+ if (this.passengers.isEmpty()) { -+ return ret; - } -- return indirectPassengers.build(); -+ -+ collectIndirectPassengers(ret, this.passengers); -+ -+ return ret; -+ // Paper end - optimise entity tracker - } - private Iterable<Entity> getIndirectPassengers_old() { - // Paper end - Optimize indirect passenger iteration -@@ -4475,82 +4711,136 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - return Mth.lerp(delta, this.yRotO, this.yRot); - } - -- public boolean updateFluidHeightAndDoFluidPushing(TagKey<Fluid> tag, double speed) { -+ // Paper start - optimise collisions -+ public boolean updateFluidHeightAndDoFluidPushing(final TagKey<Fluid> fluid, final double flowScale) { - if (this.touchingUnloadedChunk()) { - return false; -- } else { -- AABB axisalignedbb = this.getBoundingBox().deflate(0.001D); -- int i = Mth.floor(axisalignedbb.minX); -- int j = Mth.ceil(axisalignedbb.maxX); -- int k = Mth.floor(axisalignedbb.minY); -- int l = Mth.ceil(axisalignedbb.maxY); -- int i1 = Mth.floor(axisalignedbb.minZ); -- int j1 = Mth.ceil(axisalignedbb.maxZ); -- double d1 = 0.0D; -- boolean flag = this.isPushedByFluid(); -- boolean flag1 = false; -- Vec3 vec3d = Vec3.ZERO; -- int k1 = 0; -- BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos(); -- -- for (int l1 = i; l1 < j; ++l1) { -- for (int i2 = k; i2 < l; ++i2) { -- for (int j2 = i1; j2 < j1; ++j2) { -- blockposition_mutableblockposition.set(l1, i2, j2); -- FluidState fluid = this.level().getFluidState(blockposition_mutableblockposition); -- -- if (fluid.is(tag)) { -- double d2 = (double) ((float) i2 + fluid.getHeight(this.level(), blockposition_mutableblockposition)); -- -- if (d2 >= axisalignedbb.minY) { -- flag1 = true; -- d1 = Math.max(d2 - axisalignedbb.minY, d1); -- if (flag) { -- Vec3 vec3d1 = fluid.getFlow(this.level(), blockposition_mutableblockposition); -- -- if (d1 < 0.4D) { -- vec3d1 = vec3d1.scale(d1); -- } -+ } -+ -+ final AABB boundingBox = this.getBoundingBox().deflate(1.0E-3); -+ -+ final Level world = this.level; -+ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(world); -+ -+ final int minBlockX = Mth.floor(boundingBox.minX); -+ final int minBlockY = Math.max((minSection << 4), Mth.floor(boundingBox.minY)); -+ final int minBlockZ = Mth.floor(boundingBox.minZ); -+ -+ // note: bounds are exclusive in Vanilla, so we subtract 1 - our loop expects bounds to be inclusive -+ final int maxBlockX = Mth.ceil(boundingBox.maxX) - 1; -+ final int maxBlockY = Math.min((ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(world) << 4) | 15, Mth.ceil(boundingBox.maxY) - 1); -+ final int maxBlockZ = Mth.ceil(boundingBox.maxZ) - 1; -+ -+ final boolean isPushable = this.isPushedByFluid(); -+ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); -+ -+ Vec3 pushVector = Vec3.ZERO; -+ double totalPushes = 0.0; -+ double maxHeightDiff = 0.0; -+ boolean inFluid = false; -+ -+ final int minChunkX = minBlockX >> 4; -+ final int maxChunkX = maxBlockX >> 4; -+ -+ final int minChunkY = minBlockY >> 4; -+ final int maxChunkY = maxBlockY >> 4; -+ -+ final int minChunkZ = minBlockZ >> 4; -+ final int maxChunkZ = maxBlockZ >> 4; -+ -+ final net.minecraft.world.level.chunk.ChunkSource chunkSource = world.getChunkSource(); -+ -+ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { -+ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { -+ final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, false).getSections(); -+ -+ // bound y -+ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { -+ final int sectionIdx = currChunkY - minSection; -+ if (sectionIdx < 0 || sectionIdx >= sections.length) { -+ continue; -+ } -+ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx]; -+ if (section.hasOnlyAir()) { -+ // empty -+ continue; -+ } -+ -+ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> blocks = section.states; -+ -+ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) : 0; -+ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) : 15; -+ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) : 0; -+ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) : 15; -+ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) : 0; -+ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) : 15; - -- vec3d = vec3d.add(vec3d1); -- ++k1; -+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) { -+ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { -+ for (int currX = minXIterate; currX <= maxXIterate; ++currX) { -+ final FluidState fluidState = blocks.get((currX) | (currZ << 4) | ((currY) << 8)).getFluidState(); -+ -+ if (fluidState.isEmpty() || !fluidState.is(fluid)) { -+ continue; - } -- // CraftBukkit start - store last lava contact location -- if (tag == FluidTags.LAVA) { -- this.lastLavaContact = blockposition_mutableblockposition.immutable(); -+ -+ mutablePos.set(currX | (currChunkX << 4), currY | (currChunkY << 4), currZ | (currChunkZ << 4)); -+ -+ final double height = (double)((float)mutablePos.getY() + fluidState.getHeight(world, mutablePos)); -+ final double diff = height - boundingBox.minY; -+ -+ if (diff < 0.0) { -+ continue; -+ } -+ -+ inFluid = true; -+ maxHeightDiff = Math.max(maxHeightDiff, diff); -+ -+ if (!isPushable) { -+ continue; -+ } -+ -+ ++totalPushes; -+ -+ final Vec3 flow = fluidState.getFlow(world, mutablePos); -+ -+ if (diff < 0.4) { -+ pushVector = pushVector.add(flow.scale(diff)); -+ } else { -+ pushVector = pushVector.add(flow); - } -- // CraftBukkit end - } - } - } - } - } -+ } - -- if (vec3d.length() > 0.0D) { -- if (k1 > 0) { -- vec3d = vec3d.scale(1.0D / (double) k1); -- } -+ this.fluidHeight.put(fluid, maxHeightDiff); - -- if (!(this instanceof Player)) { -- vec3d = vec3d.normalize(); -- } -+ if (pushVector.lengthSqr() == 0.0) { -+ return inFluid; -+ } - -- Vec3 vec3d2 = this.getDeltaMovement(); -+ // note: totalPushes != 0 as pushVector != 0 -+ pushVector = pushVector.scale(1.0 / totalPushes); -+ final Vec3 currMovement = this.getDeltaMovement(); - -- vec3d = vec3d.scale(speed); -- double d3 = 0.003D; -+ if (!((Entity)(Object)this instanceof Player)) { -+ pushVector = pushVector.normalize(); -+ } - -- if (Math.abs(vec3d2.x) < 0.003D && Math.abs(vec3d2.z) < 0.003D && vec3d.length() < 0.0045000000000000005D) { -- vec3d = vec3d.normalize().scale(0.0045000000000000005D); -- } -+ pushVector = pushVector.scale(flowScale); -+ if (Math.abs(currMovement.x) < 0.003 && Math.abs(currMovement.z) < 0.003 && pushVector.length() < 0.0045000000000000005) { -+ pushVector = pushVector.normalize().scale(0.0045000000000000005); -+ } - -- this.setDeltaMovement(this.getDeltaMovement().add(vec3d)); -- } -+ this.setDeltaMovement(currMovement.add(pushVector)); - -- this.fluidHeight.put(tag, d1); -- return flag1; -- } -+ // note: inFluid = true here as pushVector != 0 -+ return true; - } -+ // Paper end - optimise collisions - - public boolean touchingUnloadedChunk() { - AABB axisalignedbb = this.getBoundingBox().inflate(1.0D); -@@ -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<PoiSection, PoiSection.Packed> { -+public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> implements ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager { // Paper - rewrite chunk system - public static final int MAX_VILLAGE_DISTANCE = 6; - public static final int VILLAGE_SECTION_SIZE = 1; - private final PoiManager.DistanceTracker distanceTracker; - private final LongSet loadedChunks = new LongOpenHashSet(); - -+ // Paper start - rewrite chunk system -+ private final net.minecraft.server.level.ServerLevel world; -+ -+ // the vanilla tracker needs to be replaced because it does not support level removes, and we need level removes -+ // to support poi unloading -+ private final ca.spottedleaf.moonrise.common.misc.Delayed26WayDistancePropagator3D villageDistanceTracker = new ca.spottedleaf.moonrise.common.misc.Delayed26WayDistancePropagator3D(); -+ -+ private static final int POI_DATA_SOURCE = 7; -+ -+ private static int convertBetweenLevels(final int level) { -+ return POI_DATA_SOURCE - level; -+ } -+ -+ private void updateDistanceTracking(long section) { -+ if (this.isVillageCenter(section)) { -+ this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE); -+ } else { -+ this.villageDistanceTracker.removeSource(section); -+ } -+ } -+ -+ @Override -+ public Optional<PoiSection> get(final long pos) { -+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos); -+ final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos); -+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos); -+ -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); -+ -+ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getPoiChunkIfLoaded(chunkX, chunkZ, true); -+ -+ return ret == null ? Optional.empty() : ret.getSectionForVanilla(chunkY); -+ } -+ -+ @Override -+ public Optional<PoiSection> getOrLoad(final long pos) { -+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos); -+ final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos); -+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos); -+ -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); -+ -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager; -+ -+ if (chunkY >= ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world) && chunkY <= ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world)) { -+ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); -+ if (ret != null) { -+ return ret.getSectionForVanilla(chunkY); -+ } else { -+ return manager.loadPoiChunk(chunkX, chunkZ).getSectionForVanilla(chunkY); -+ } -+ } -+ // retain vanilla behavior: do not load section if out of bounds! -+ return Optional.empty(); -+ } -+ -+ @Override -+ protected PoiSection getOrCreate(final long pos) { -+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos); -+ final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos); -+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos); -+ -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); -+ -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager; -+ -+ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); -+ if (ret != null) { -+ return ret.getOrCreateSection(chunkY); -+ } else { -+ return manager.loadPoiChunk(chunkX, chunkZ).getOrCreateSection(chunkY); -+ } -+ } -+ -+ @Override -+ public final net.minecraft.server.level.ServerLevel moonrise$getWorld() { -+ return this.world; -+ } -+ -+ @Override -+ public final void moonrise$onUnload(final long coordinate) { // Paper - rewrite chunk system -+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(coordinate); -+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(coordinate); -+ -+ final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world); -+ final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world); -+ -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Unloading poi chunk off-main"); -+ for (int sectionY = minY; sectionY <= maxY; ++sectionY) { -+ final long sectionPos = SectionPos.asLong(chunkX, sectionY, chunkZ); -+ this.updateDistanceTracking(sectionPos); -+ } -+ } -+ -+ @Override -+ public final void moonrise$loadInPoiChunk(final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk poiChunk) { -+ final int chunkX = poiChunk.chunkX; -+ final int chunkZ = poiChunk.chunkZ; -+ -+ final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world); -+ final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world); -+ -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Loading poi chunk off-main"); -+ for (int sectionY = minY; sectionY <= maxY; ++sectionY) { -+ final PoiSection section = poiChunk.getSection(sectionY); -+ if (section != null && !((ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection)section).moonrise$isEmpty()) { -+ this.onSectionLoad(SectionPos.asLong(chunkX, sectionY, chunkZ)); -+ } -+ } -+ } -+ -+ @Override -+ public final void moonrise$checkConsistency(final net.minecraft.world.level.chunk.ChunkAccess chunk) { -+ final int chunkX = chunk.getPos().x; -+ final int chunkZ = chunk.getPos().z; -+ -+ final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(chunk); -+ final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(chunk); -+ final LevelChunkSection[] sections = chunk.getSections(); -+ for (int section = minY; section <= maxY; ++section) { -+ this.checkConsistencyWithBlocks(SectionPos.of(chunkX, section, chunkZ), sections[section - minY]); -+ } -+ } -+ // Paper end - rewrite chunk system -+ - public PoiManager( - RegionStorageInfo storageKey, - Path directory, -@@ -64,6 +189,7 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> { - world - ); - this.distanceTracker = new PoiManager.DistanceTracker(); -+ this.world = (net.minecraft.server.level.ServerLevel)world; // Paper - rewrite chunk system - } - - public void add(BlockPos pos, Holder<PoiType> type) { -@@ -197,8 +323,10 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> { - } - - public int sectionsToVillage(SectionPos pos) { -- this.distanceTracker.runAllUpdates(); -- return this.distanceTracker.getLevel(pos.asLong()); -+ // Paper start - rewrite chunk system -+ this.villageDistanceTracker.propagateUpdates(); -+ return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(pos))); -+ // Paper end - rewrite chunk system - } - - boolean isVillageCenter(long pos) { -@@ -212,19 +340,26 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> { - - @Override - public void tick(BooleanSupplier shouldKeepTicking) { -- super.tick(shouldKeepTicking); -- this.distanceTracker.runAllUpdates(); -+ this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system - } - - @Override -- protected void setDirty(long pos) { -- super.setDirty(pos); -- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false); -+ public void setDirty(long pos) { // Paper - public -+ // Paper start - rewrite chunk system -+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos); -+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos); -+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager; -+ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk chunk = manager.getPoiChunkIfLoaded(chunkX, chunkZ, false); -+ if (chunk != null) { -+ chunk.setDirty(true); -+ } -+ this.updateDistanceTracking(pos); -+ // Paper end - rewrite chunk system - } - - @Override - protected void onSectionLoad(long pos) { -- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false); -+ this.updateDistanceTracking(pos); // Paper - rewrite chunk system - } - - public void checkConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection chunkSection) { -@@ -263,7 +398,7 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> { - .map(sectionPos -> Pair.of(sectionPos, this.getOrLoad(sectionPos.asLong()))) - .filter(pair -> !pair.getSecond().map(PoiSection::isValid).orElse(false)) - .map(pair -> pair.getFirst().chunk()) -- .filter(chunkPos -> this.loadedChunks.add(chunkPos.toLong())) -+ // Paper - rewrite chunk system - .forEach(chunkPos -> world.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.EMPTY)); - } - -diff --git a/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<PoiRecord> records = new Short2ObjectOpenHashMap<>(); - private final Map<Holder<PoiType>, Set<PoiRecord>> byType = Maps.newHashMap(); - private final Runnable setDirty; - private boolean isValid; - -+ // Paper start - rewrite chunk system -+ private final Optional<PoiSection> noAllocOptional = Optional.of((PoiSection)(Object)this); -+ -+ @Override -+ public final boolean moonrise$isEmpty() { -+ return this.isValid && this.records.isEmpty() && this.byType.isEmpty(); -+ } -+ -+ @Override -+ public final Optional<PoiSection> moonrise$asOptional() { -+ return this.noAllocOptional; -+ } -+ // Paper end - rewrite chunk system -+ - public PoiSection(Runnable updateListener) { - this(updateListener, true, ImmutableList.of()); - } -diff --git a/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<Entity> list = this.level().getEntities((Entity) this, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS); -+ List<AbstractMinecart> list = this.level().getEntitiesOfClass(AbstractMinecart.class, this.getBoundingBox(), RIDABLE_MINECARTS); // Paper - optimise collisions - Iterator iterator = list.iterator(); - - while (iterator.hasNext()) { -diff --git a/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<Entity> getEntities(@Nullable Entity except, AABB box, Predicate<? super Entity> predicate); - - <T extends Entity> List<T> getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate); -@@ -30,21 +30,44 @@ public interface EntityGetter { - return this.getEntities(except, box, EntitySelector.NO_SPECTATORS); - } - -- default boolean isUnobstructed(@Nullable Entity except, VoxelShape shape) { -- if (shape.isEmpty()) { -- return true; -- } else { -- for (Entity entity : this.getEntities(except, shape.bounds())) { -- if (!entity.isRemoved() -- && entity.blocksBuilding -- && (except == null || !entity.isPassengerOfSameVehicle(except)) -- && Shapes.joinIsNotEmpty(shape, Shapes.create(entity.getBoundingBox()), BooleanOp.AND)) { -- return false; -+ // Paper start - rewrite chunk system -+ @Override -+ default List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate) { -+ return this.getEntities(entity, box, predicate); -+ } -+ // Paper end - rewrite chunk system -+ -+ // Paper start - optimise collisions -+ default boolean isUnobstructed(@Nullable Entity entity, VoxelShape voxel) { -+ if (voxel.isEmpty()) { -+ return false; -+ } -+ -+ final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation(); -+ final List<Entity> entities = this.getEntities( -+ entity, -+ singleAABB == null ? voxel.bounds() : singleAABB.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) -+ ); -+ -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ final Entity otherEntity = entities.get(i); -+ -+ if (otherEntity.isRemoved() || !otherEntity.blocksBuilding || (entity != null && otherEntity.isPassengerOfSameVehicle(entity))) { -+ continue; -+ } -+ -+ if (singleAABB == null) { -+ final AABB entityBB = otherEntity.getBoundingBox(); -+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(entityBB) || !ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(voxel, entityBB)) { -+ continue; - } - } - -- return true; -+ return false; - } -+ -+ return true; -+ // Paper end - optimise collisions - } - - default <T extends Entity> List<T> getEntitiesOfClass(Class<T> entityClass, AABB box) { -@@ -52,23 +75,41 @@ public interface EntityGetter { - } - - default List<VoxelShape> getEntityCollisions(@Nullable Entity entity, AABB box) { -- if (box.getSize() < 1.0E-7) { -- return List.of(); -+ // Paper start - optimise collisions -+ // first behavior change is to correctly check for empty AABB -+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(box)) { -+ // reduce indirection by always returning type with same class -+ return new java.util.ArrayList<>(); -+ } -+ -+ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with. -+ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems -+ // specifically with boat collisions. -+ box = box.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON); -+ -+ final List<Entity> entities; -+ if (entity != null && ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$isHardColliding()) { -+ entities = this.getEntities(entity, box, null); - } else { -- Predicate<Entity> predicate = entity == null ? EntitySelector.CAN_BE_COLLIDED_WITH : EntitySelector.NO_SPECTATORS.and(entity::canCollideWith); -- List<Entity> list = this.getEntities(entity, box.inflate(1.0E-7), predicate); -- if (list.isEmpty()) { -- return List.of(); -- } else { -- Builder<VoxelShape> builder = ImmutableList.builderWithExpectedSize(list.size()); -- -- for (Entity entity2 : list) { -- builder.add(Shapes.create(entity2.getBoundingBox())); -- } -+ entities = ((ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter)this).moonrise$getHardCollidingEntities(entity, box, null); -+ } - -- return builder.build(); -+ final List<VoxelShape> ret = new java.util.ArrayList<>(Math.min(25, entities.size())); -+ -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ final Entity otherEntity = entities.get(i); -+ -+ if (otherEntity.isSpectator()) { -+ continue; -+ } -+ -+ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) { -+ ret.add(Shapes.create(otherEntity.getBoundingBox())); - } - } -+ -+ return ret; -+ // Paper end - optimise collisions - } - - // Paper start - Affects Spawning API -diff --git a/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<ResourceKey<Level>> RESOURCE_KEY_CODEC = ResourceKey.codec(Registries.DIMENSION); - public static final ResourceKey<Level> 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<LevelStem> getTypeKey(); - -+ // Paper start - rewrite chunk system -+ private ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup; -+ private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData> chunkData = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>(); -+ -+ @Override -+ public final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup moonrise$getEntityLookup() { -+ return this.entityLookup; -+ } -+ -+ @Override -+ public final void moonrise$setEntityLookup(final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup) { -+ if (this.entityLookup != null && !(this.entityLookup instanceof ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl.DefaultEntityLookup)) { -+ throw new IllegalStateException("Entity lookup already initialised"); -+ } -+ this.entityLookup = entityLookup; -+ } -+ -+ @Override -+ public final <T extends Entity> List<T> getEntitiesOfClass(final Class<T> entityClass, final AABB boundingBox, final Predicate<? super T> predicate) { -+ Profiler.get().incrementCounter("getEntities"); -+ final List<T> ret = new java.util.ArrayList<>(); -+ -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(entityClass, null, boundingBox, ret, predicate); -+ -+ return ret; -+ } -+ -+ @Override -+ public final List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate) { -+ Profiler.get().incrementCounter("getEntities"); -+ final List<Entity> ret = new java.util.ArrayList<>(); -+ -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getHardCollidingEntities(entity, box, ret, predicate); -+ -+ return ret; -+ } -+ -+ @Override -+ public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) { -+ return (LevelChunk)this.getChunkSource().getChunk(chunkX, chunkZ, ChunkStatus.FULL, false); -+ } -+ -+ @Override -+ public ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) { -+ return this.getChunkSource().getChunk(chunkX, chunkZ, ChunkStatus.EMPTY, false); -+ } -+ -+ @Override -+ public ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus) { -+ return this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, false); -+ } -+ -+ @Override -+ public void moonrise$midTickTasks() { -+ // no-op on ClientLevel -+ } -+ -+ @Override -+ public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$getChunkData(final long chunkKey) { -+ return this.chunkData.get(chunkKey); -+ } -+ -+ @Override -+ public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$getChunkData(final int chunkX, final int chunkZ) { -+ return this.chunkData.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ -+ @Override -+ public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$requestChunkData(final long chunkKey) { -+ return this.chunkData.compute(chunkKey, (final long keyInMap, final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData valueInMap) -> { -+ if (valueInMap == null) { -+ final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData ret = new ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData(); -+ ret.increaseRef(); -+ return ret; -+ } -+ -+ valueInMap.increaseRef(); -+ return valueInMap; -+ }); -+ } -+ -+ @Override -+ public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$releaseChunkData(final long chunkKey) { -+ return this.chunkData.compute(chunkKey, (final long keyInMap, final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData) -> { -+ return chunkData.decreaseRef() == 0 ? null : chunkData; -+ }); -+ } -+ -+ @Override -+ public boolean moonrise$areChunksLoaded(final int fromX, final int fromZ, final int toX, final int toZ) { -+ final ChunkSource chunkSource = this.getChunkSource(); -+ -+ for (int currZ = fromZ; currZ <= toZ; ++currZ) { -+ for (int currX = fromX; currX <= toX; ++currX) { -+ if (!chunkSource.hasChunk(currX, currZ)) { -+ return false; -+ } -+ } -+ } -+ -+ return true; -+ } -+ -+ @Override -+ public boolean hasChunksAt(final int minBlockX, final int minBlockZ, final int maxBlockX, final int maxBlockZ) { -+ return this.moonrise$areChunksLoaded( -+ minBlockX >> 4, minBlockZ >> 4, maxBlockX >> 4, maxBlockZ >> 4 -+ ); -+ } -+ -+ /** -+ * @reason Turn all getChunk(x, z, status) calls into virtual invokes, instead of interface invokes: -+ * 1. The interface invoke is expensive -+ * 2. The method makes other interface invokes (again, expensive) -+ * Instead, we just directly call getChunk(x, z, status, true) which avoids the interface invokes entirely. -+ * @author Spottedleaf -+ */ -+ @Override -+ public ChunkAccess getChunk(final int x, final int z, final ChunkStatus status) { -+ return ((Level)(Object)this).getChunk(x, z, status, true); -+ } -+ -+ @Override -+ public BlockPos getHeightmapPos(Heightmap.Types types, BlockPos blockPos) { -+ return new BlockPos(blockPos.getX(), this.getHeight(types, blockPos.getX(), blockPos.getZ()), blockPos.getZ()); -+ } -+ // Paper end - rewrite chunk system -+ // Paper start - optimise collisions -+ /** -+ * Route to faster lookup. -+ * See {@link EntityGetter#isUnobstructed(Entity, VoxelShape)} for expected behavior -+ * @author Spottedleaf -+ */ -+ @Override -+ public boolean isUnobstructed(final Entity entity) { -+ final AABB boundingBox = entity.getBoundingBox(); -+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(boundingBox)) { -+ return false; -+ } -+ -+ final List<Entity> entities = this.getEntities( -+ entity, -+ boundingBox.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON), -+ null -+ ); -+ -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ final Entity otherEntity = entities.get(i); -+ -+ if (otherEntity.isSpectator() || otherEntity.isRemoved() || !otherEntity.blocksBuilding || otherEntity.isPassengerOfSameVehicle(entity)) { -+ continue; -+ } -+ -+ return false; -+ } -+ -+ return true; -+ } -+ -+ -+ private static net.minecraft.world.phys.BlockHitResult miss(final ClipContext clipContext) { -+ final Vec3 to = clipContext.getTo(); -+ final Vec3 from = clipContext.getFrom(); -+ -+ return net.minecraft.world.phys.BlockHitResult.miss(to, Direction.getApproximateNearest(from.x - to.x, from.y - to.y, from.z - to.z), BlockPos.containing(to.x, to.y, to.z)); -+ } -+ -+ private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState(); -+ -+ private static net.minecraft.world.phys.BlockHitResult fastClip(final Vec3 from, final Vec3 to, final Level level, -+ final ClipContext clipContext) { -+ final double adjX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.x - to.x); -+ final double adjY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.y - to.y); -+ final double adjZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.z - to.z); -+ -+ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) { -+ return miss(clipContext); -+ } -+ -+ final double toXAdj = to.x - adjX; -+ final double toYAdj = to.y - adjY; -+ final double toZAdj = to.z - adjZ; -+ final double fromXAdj = from.x + adjX; -+ final double fromYAdj = from.y + adjY; -+ final double fromZAdj = from.z + adjZ; -+ -+ int currX = Mth.floor(fromXAdj); -+ int currY = Mth.floor(fromYAdj); -+ int currZ = Mth.floor(fromZAdj); -+ -+ final BlockPos.MutableBlockPos currPos = new BlockPos.MutableBlockPos(); -+ -+ final double diffX = toXAdj - fromXAdj; -+ final double diffY = toYAdj - fromYAdj; -+ final double diffZ = toZAdj - fromZAdj; -+ -+ final double dxDouble = Math.signum(diffX); -+ final double dyDouble = Math.signum(diffY); -+ final double dzDouble = Math.signum(diffZ); -+ -+ final int dx = (int)dxDouble; -+ final int dy = (int)dyDouble; -+ final int dz = (int)dzDouble; -+ -+ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX; -+ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY; -+ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ; -+ -+ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj)); -+ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj)); -+ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj)); -+ -+ net.minecraft.world.level.chunk.LevelChunkSection[] lastChunk = null; -+ net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> lastSection = null; -+ int lastChunkX = Integer.MIN_VALUE; -+ int lastChunkY = Integer.MIN_VALUE; -+ int lastChunkZ = Integer.MIN_VALUE; -+ -+ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(level); -+ -+ for (;;) { -+ currPos.set(currX, currY, currZ); -+ -+ final int newChunkX = currX >> 4; -+ final int newChunkY = currY >> 4; -+ final int newChunkZ = currZ >> 4; -+ -+ final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); -+ final int chunkYDiff = newChunkY ^ lastChunkY; -+ -+ if ((chunkDiff | chunkYDiff) != 0) { -+ if (chunkDiff != 0) { -+ lastChunk = level.getChunk(newChunkX, newChunkZ).getSections(); -+ } -+ final int sectionY = newChunkY - minSection; -+ lastSection = sectionY >= 0 && sectionY < lastChunk.length ? lastChunk[sectionY].states : null; -+ -+ lastChunkX = newChunkX; -+ lastChunkY = newChunkY; -+ lastChunkZ = newChunkZ; -+ } -+ -+ final BlockState blockState; -+ if (lastSection != null && !(blockState = lastSection.get((currX & 15) | ((currZ & 15) << 4) | ((currY & 15) << (4+4)))).isAir()) { -+ final VoxelShape blockCollision = clipContext.getBlockShape(blockState, level, currPos); -+ -+ final net.minecraft.world.phys.BlockHitResult blockHit = blockCollision.isEmpty() ? null : level.clipWithInteractionOverride(from, to, currPos, blockCollision, blockState); -+ -+ final VoxelShape fluidCollision; -+ final FluidState fluidState; -+ if (clipContext.fluid != ClipContext.Fluid.NONE && (fluidState = blockState.getFluidState()) != AIR_FLUIDSTATE) { -+ fluidCollision = clipContext.getFluidShape(fluidState, level, currPos); -+ -+ final net.minecraft.world.phys.BlockHitResult fluidHit = fluidCollision.clip(from, to, currPos); -+ -+ if (fluidHit != null) { -+ if (blockHit == null) { -+ return fluidHit; -+ } -+ -+ return from.distanceToSqr(blockHit.getLocation()) <= from.distanceToSqr(fluidHit.getLocation()) ? blockHit : fluidHit; -+ } -+ } -+ -+ if (blockHit != null) { -+ return blockHit; -+ } -+ } // else: usually fall here -+ -+ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) { -+ return miss(clipContext); -+ } -+ -+ // inc the smallest normalized coordinate -+ -+ if (normalizedCurrX < normalizedCurrY) { -+ if (normalizedCurrX < normalizedCurrZ) { -+ currX += dx; -+ normalizedCurrX += normalizedDiffX; -+ } else { -+ // x < y && x >= z <--> z < y && z <= x -+ currZ += dz; -+ normalizedCurrZ += normalizedDiffZ; -+ } -+ } else if (normalizedCurrY < normalizedCurrZ) { -+ // y <= x && y < z -+ currY += dy; -+ normalizedCurrY += normalizedDiffY; -+ } else { -+ // y <= x && z <= y <--> z <= y && z <= x -+ currZ += dz; -+ normalizedCurrZ += normalizedDiffZ; -+ } -+ } -+ } -+ -+ /** -+ * @reason Route to optimized call -+ * @author Spottedleaf -+ */ -+ @Override -+ public net.minecraft.world.phys.BlockHitResult clip(final ClipContext clipContext) { -+ // can only do this in this class, as not everything that implements BlockGetter can retrieve chunks -+ return fastClip(clipContext.getFrom(), clipContext.getTo(), (Level)(Object)this, clipContext); -+ } -+ -+ /** -+ * @reason Route to faster logic -+ * @author Spottedleaf -+ */ -+ @Override -+ public boolean collidesWithSuffocatingBlock(final Entity entity, final AABB box) { -+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder((Level)(Object)this, entity, box, null, null, -+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_ONLY, -+ (final BlockState state, final BlockPos pos) -> { -+ return state.isSuffocating((Level)(Object)Level.this, pos); -+ } -+ ); -+ } -+ -+ private static VoxelShape inflateAABBToVoxel(final AABB aabb, final double x, final double y, final double z) { -+ return net.minecraft.world.phys.shapes.Shapes.create( -+ aabb.minX - x, -+ aabb.minY - y, -+ aabb.minZ - z, -+ -+ aabb.maxX + x, -+ aabb.maxY + y, -+ aabb.maxZ + z -+ ); -+ } -+ -+ /** -+ * @reason Use optimised OR operator join strategy, avoid streams -+ * @author Spottedleaf -+ */ -+ @Override -+ public java.util.Optional<net.minecraft.world.phys.Vec3> findFreePosition(final Entity entity, final VoxelShape boundsShape, final Vec3 fromPosition, -+ final double rangeX, final double rangeY, final double rangeZ) { -+ if (boundsShape.isEmpty()) { -+ return java.util.Optional.empty(); -+ } -+ -+ final double expandByX = rangeX * 0.5; -+ final double expandByY = rangeY * 0.5; -+ final double expandByZ = rangeZ * 0.5; -+ -+ // note: it is useless to look at shapes outside of range / 2.0 -+ final AABB collectionVolume = boundsShape.bounds().inflate(expandByX, expandByY, expandByZ); -+ -+ final List<AABB> aabbs = new java.util.ArrayList<>(); -+ final List<VoxelShape> voxels = new java.util.ArrayList<>(); -+ -+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder( -+ (Level)(Object)this, entity, collectionVolume, voxels, aabbs, -+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, -+ null -+ ); -+ -+ final WorldBorder worldBorder = this.getWorldBorder(); -+ if (worldBorder != null) { -+ aabbs.removeIf((final AABB aabb) -> { -+ return !worldBorder.isWithinBounds(aabb); -+ }); -+ voxels.removeIf((final VoxelShape shape) -> { -+ return !worldBorder.isWithinBounds(shape.bounds()); -+ }); -+ } -+ -+ // push voxels into aabbs -+ for (int i = 0, len = voxels.size(); i < len; ++i) { -+ aabbs.addAll(voxels.get(i).toAabbs()); -+ } -+ -+ // expand AABBs -+ final VoxelShape first = aabbs.isEmpty() ? net.minecraft.world.phys.shapes.Shapes.empty() : inflateAABBToVoxel(aabbs.get(0), expandByX, expandByY, expandByZ); -+ final VoxelShape[] rest = new VoxelShape[Math.max(0, aabbs.size() - 1)]; -+ -+ for (int i = 1, len = aabbs.size(); i < len; ++i) { -+ rest[i - 1] = inflateAABBToVoxel(aabbs.get(i), expandByX, expandByY, expandByZ); -+ } -+ -+ // use optimized implementation of ORing the shapes together -+ final VoxelShape joined = net.minecraft.world.phys.shapes.Shapes.or(first, rest); -+ -+ // find free space -+ // can use unoptimized join here (instead of join()), as closestPointTo uses toAabbs() -+ final VoxelShape freeSpace = net.minecraft.world.phys.shapes.Shapes.joinUnoptimized(boundsShape, joined, net.minecraft.world.phys.shapes.BooleanOp.ONLY_FIRST); -+ -+ return freeSpace.closestPointTo(fromPosition); -+ } -+ -+ /** -+ * @reason Route to faster logic -+ * @author Spottedleaf -+ */ -+ @Override -+ public java.util.Optional<net.minecraft.core.BlockPos> findSupportingBlock(final Entity entity, final AABB aabb) { -+ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection((Level)(Object)this); -+ -+ final int minBlockX = Mth.floor(aabb.minX - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1; -+ final int maxBlockX = Mth.floor(aabb.maxX + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1; -+ -+ final int minBlockY = Math.max((minSection << 4) - 1, Mth.floor(aabb.minY - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1); -+ final int maxBlockY = Math.min((ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection((Level)(Object)this) << 4) + 16, Mth.floor(aabb.maxY + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1); -+ -+ final int minBlockZ = Mth.floor(aabb.minZ - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1; -+ final int maxBlockZ = Mth.floor(aabb.maxZ + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1; -+ -+ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); -+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext collisionShape = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(entity); -+ BlockPos selected = null; -+ double selectedDistance = Double.MAX_VALUE; -+ final Vec3 entityPos = entity.position(); -+ -+ // special cases: -+ if (minBlockY > maxBlockY) { -+ // no point in checking -+ return java.util.Optional.empty(); -+ } -+ -+ final int minChunkX = minBlockX >> 4; -+ final int maxChunkX = maxBlockX >> 4; -+ -+ final int minChunkY = minBlockY >> 4; -+ final int maxChunkY = maxBlockY >> 4; -+ -+ final int minChunkZ = minBlockZ >> 4; -+ final int maxChunkZ = maxBlockZ >> 4; -+ -+ final ChunkSource chunkSource = this.getChunkSource(); -+ -+ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { -+ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { -+ final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, false); -+ -+ if (chunk == null) { -+ continue; -+ } -+ -+ final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections(); -+ -+ // bound y -+ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { -+ final int sectionIdx = currChunkY - minSection; -+ if (sectionIdx < 0 || sectionIdx >= sections.length) { -+ continue; -+ } -+ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx]; -+ if (section.hasOnlyAir()) { -+ // empty -+ continue; -+ } -+ -+ final boolean hasSpecial = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$hasSpecialCollidingBlocks(); -+ final int sectionAdjust = !hasSpecial ? 1 : 0; -+ -+ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> blocks = section.states; -+ -+ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0; -+ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15; -+ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) + sectionAdjust : 0; -+ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) - sectionAdjust : 15; -+ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) + sectionAdjust : 0; -+ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) - sectionAdjust : 15; -+ -+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) { -+ final int blockY = currY | (currChunkY << 4); -+ mutablePos.setY(blockY); -+ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { -+ final int blockZ = currZ | (currChunkZ << 4); -+ mutablePos.setZ(blockZ); -+ for (int currX = minXIterate; currX <= maxXIterate; ++currX) { -+ final int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8); -+ final int blockX = currX | (currChunkX << 4); -+ mutablePos.setX(blockX); -+ -+ final int edgeCount = hasSpecial ? ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + -+ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + -+ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0) : 0; -+ if (edgeCount == 3) { -+ continue; -+ } -+ -+ final double distance = mutablePos.distToCenterSqr(entityPos); -+ if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(mutablePos) >= 0)) { -+ continue; -+ } -+ -+ final BlockState blockData = blocks.get(localBlockIndex); -+ -+ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$emptyContextCollisionShape()) { -+ continue; -+ } -+ -+ VoxelShape blockCollision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$getConstantContextCollisionShape(); -+ -+ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) { -+ if (blockCollision == null) { -+ blockCollision = blockData.getCollisionShape((Level)(Object)this, mutablePos, collisionShape); -+ -+ if (blockCollision.isEmpty()) { -+ continue; -+ } -+ } -+ -+ // avoid VoxelShape#move by shifting the entity collision shape instead -+ final AABB shiftedAABB = aabb.move(-(double)blockX, -(double)blockY, -(double)blockZ); -+ -+ final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation(); -+ if (singleAABB != null) { -+ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(singleAABB, shiftedAABB)) { -+ continue; -+ } -+ -+ selected = mutablePos.immutable(); -+ selectedDistance = distance; -+ continue; -+ } -+ -+ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(blockCollision, shiftedAABB)) { -+ continue; -+ } -+ -+ selected = mutablePos.immutable(); -+ selectedDistance = distance; -+ continue; -+ } -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ return java.util.Optional.ofNullable(selected); -+ } -+ // Paper end - optimise collisions -+ // Paper start - getblock optimisations - cache world height/sections -+ private final int minY; -+ private final int height; -+ private final int maxY; -+ private final int minSectionY; -+ private final int maxSectionY; -+ private final int sectionsCount; -+ -+ @Override -+ public int getMinY() { -+ return this.minY; -+ } -+ -+ @Override -+ public int getHeight() { -+ return this.height; -+ } -+ -+ @Override -+ public int getMaxY() { -+ return this.maxY; -+ } -+ -+ @Override -+ public int getSectionsCount() { -+ return this.sectionsCount; -+ } -+ -+ @Override -+ public int getMinSectionY() { -+ return this.minSectionY; -+ } -+ -+ @Override -+ public int getMaxSectionY() { -+ return this.maxSectionY; -+ } -+ -+ @Override -+ public boolean isInsideBuildHeight(final int blockY) { -+ return blockY >= this.minY && blockY <= this.maxY; -+ } -+ -+ @Override -+ public boolean isOutsideBuildHeight(final BlockPos pos) { -+ return this.isOutsideBuildHeight(pos.getY()); -+ } -+ -+ @Override -+ public boolean isOutsideBuildHeight(final int blockY) { -+ return blockY < this.minY || blockY > this.maxY; -+ } -+ -+ @Override -+ public int getSectionIndex(final int blockY) { -+ return (blockY >> 4) - this.minSectionY; -+ } -+ -+ @Override -+ public int getSectionIndexFromSectionY(final int sectionY) { -+ return sectionY - this.minSectionY; -+ } -+ -+ @Override -+ public int getSectionYFromSectionIndex(final int sectionIdx) { -+ return sectionIdx + this.minSectionY; -+ } -+ // Paper end - getblock optimisations - cache world height/sections -+ // Paper start - optimise random ticking -+ @Override -+ public abstract Holder<Biome> getUncachedNoiseBiome(final int x, final int y, final int z); -+ -+ /** -+ * @reason Make getChunk and getUncachedNoiseBiome virtual calls instead of interface calls -+ * by implementing the superclass method in this class. -+ * @author Spottedleaf -+ */ -+ @Override -+ public Holder<Biome> getNoiseBiome(final int x, final int y, final int z) { -+ final ChunkAccess chunk = this.getChunk(x >> 2, z >> 2, ChunkStatus.BIOMES, false); -+ -+ return chunk != null ? chunk.getNoiseBiome(x, y, z) : this.getUncachedNoiseBiome(x, y, z); -+ } -+ // Paper end - optimise random ticking -+ - protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, RegistryAccess iregistrycustom, Holder<DimensionType> holder, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - create paper world config & Anti-Xray -+ // Paper start - getblock optimisations - cache world height/sections -+ final DimensionType dimType = holder.value(); -+ this.minY = dimType.minY(); -+ this.height = dimType.height(); -+ this.maxY = this.minY + this.height - 1; -+ this.minSectionY = this.minY >> 4; -+ this.maxSectionY = this.maxY >> 4; -+ this.sectionsCount = this.maxSectionY - this.minSectionY + 1; -+ // Paper end - getblock optimisations - cache world height/sections - this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot - this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config - this.generator = gen; -@@ -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<TickingBlockEntity> iterator = this.blockEntityTickers.iterator(); - boolean flag = this.tickRateManager().runsNormally(); - -+ int tickedEntities = 0; // Paper - rewrite chunk system -+ - int tilesThisCycle = 0; - var toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<TickingBlockEntity>(); // Paper - Fix MC-117075; use removeAll - toRemove.add(null); // Paper - Fix MC-117075 -@@ -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<Entity> 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<Entity> 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 <T extends Entity> void getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate, List<? super T> result, int limit) { -+ // Paper start - rewrite chunk system -+ public <T extends Entity> void getEntities(final EntityTypeTest<Entity, T> entityTypeTest, -+ final AABB boundingBox, final Predicate<? super T> predicate, -+ final List<? super T> into, final int maxCount) { - Profiler.get().incrementCounter("getEntities"); -- this.getEntities().get(filter, box, (entity) -> { -- if (predicate.test(entity)) { -- result.add(entity); -- if (result.size() >= limit) { -- return AbortableIterationConsumer.Continuation.ABORT; -- } -+ -+ if (entityTypeTest instanceof net.minecraft.world.entity.EntityType<T> byType) { -+ if (maxCount != Integer.MAX_VALUE) { -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(byType, boundingBox, into, predicate, maxCount); -+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); -+ return; -+ } else { -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(byType, boundingBox, into, predicate); -+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); -+ return; - } -+ } - -- if (entity instanceof EnderDragon entityenderdragon) { -- EnderDragonPart[] aentitycomplexpart = entityenderdragon.getSubEntities(); -- int j = aentitycomplexpart.length; -+ if (entityTypeTest == null) { -+ if (maxCount != Integer.MAX_VALUE) { -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate, maxCount); -+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); -+ return; -+ } else { -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate); -+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); -+ return; -+ } -+ } - -- for (int k = 0; k < j; ++k) { -- EnderDragonPart entitycomplexpart = aentitycomplexpart[k]; -- T t0 = filter.tryCast(entitycomplexpart); // CraftBukkit - decompile error -+ final Class<? extends Entity> base = entityTypeTest.getBaseClass(); - -- if (t0 != null && predicate.test(t0)) { -- result.add(t0); -- if (result.size() >= limit) { -- return AbortableIterationConsumer.Continuation.ABORT; -- } -- } -+ final Predicate<? super T> modifiedPredicate; -+ if (predicate == null) { -+ modifiedPredicate = (final T obj) -> { -+ return entityTypeTest.tryCast(obj) != null; -+ }; -+ } else { -+ modifiedPredicate = (final Entity obj) -> { -+ final T casted = entityTypeTest.tryCast(obj); -+ if (casted == null) { -+ return false; - } -+ -+ return predicate.test(casted); -+ }; -+ } -+ -+ if (base == null || base == Entity.class) { -+ if (maxCount != Integer.MAX_VALUE) { -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount); -+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); -+ return; -+ } else { -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate); -+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); -+ return; -+ } -+ } else { -+ if (maxCount != Integer.MAX_VALUE) { -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount); -+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); -+ return; -+ } else { -+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate); -+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); -+ return; - } -+ } -+ } - -- return AbortableIterationConsumer.Continuation.CONTINUE; -- }); -+ public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) { -+ ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices slices = ((ServerLevel)this).moonrise$getEntityLookup().getChunk(chunkX, chunkZ); -+ if (slices == null) { -+ return new org.bukkit.entity.Entity[0]; -+ } -+ -+ List<org.bukkit.entity.Entity> ret = new java.util.ArrayList<>(); -+ for (Entity entity : slices.getAllEntities()) { -+ org.bukkit.entity.Entity bukkit = entity.getBukkitEntity(); -+ if (bukkit != null && bukkit.isValid()) { -+ ret.add(bukkit); -+ } -+ } -+ -+ return ret.toArray(new org.bukkit.entity.Entity[0]); - } -+ // Paper end - rewrite chunk system - - @Nullable - public abstract Entity getEntity(int id); -diff --git a/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<ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache> blockCache = null; -+ private long[] chunkPosCache = null; -+ private net.minecraft.world.level.chunk.LevelChunk[] chunkCache = null; -+ private ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] directMappedBlockCache; -+ private BlockPos.MutableBlockPos mutablePos; -+ -+ private ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache getOrCacheExplosionBlock(final int x, final int y, final int z, -+ final long key, final boolean calculateResistance) { -+ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache ret = this.blockCache.get(key); -+ if (ret != null) { -+ return ret; -+ } -+ -+ BlockPos pos = new BlockPos(x, y, z); -+ -+ if (!this.level.isInWorldBounds(pos)) { -+ ret = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache(key, pos, null, null, 0.0f, true); -+ } else { -+ net.minecraft.world.level.chunk.LevelChunk chunk; -+ long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x >> 4, z >> 4); -+ int chunkCacheKey = ((x >> 4) & CHUNK_CACHE_MASK) | (((z >> 4) << CHUNK_CACHE_SHIFT) & (CHUNK_CACHE_MASK << CHUNK_CACHE_SHIFT)); -+ if (this.chunkPosCache[chunkCacheKey] == chunkKey) { -+ chunk = this.chunkCache[chunkCacheKey]; -+ } else { -+ this.chunkPosCache[chunkCacheKey] = chunkKey; -+ this.chunkCache[chunkCacheKey] = chunk = this.level.getChunk(x >> 4, z >> 4); -+ } -+ -+ BlockState blockState = ((ca.spottedleaf.moonrise.patches.getblock.GetBlockChunk)chunk).moonrise$getBlock(x, y, z); -+ FluidState fluidState = blockState.getFluidState(); -+ -+ Optional<Float> resistance = !calculateResistance ? Optional.empty() : this.damageCalculator.getBlockExplosionResistance((Explosion)(Object)this, this.level, pos, blockState, fluidState); -+ -+ ret = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache( -+ key, pos, blockState, fluidState, -+ (resistance.orElse(ZERO_RESISTANCE).floatValue() + 0.3f) * 0.3f, -+ false -+ ); -+ } -+ -+ this.blockCache.put(key, ret); -+ -+ return ret; -+ } -+ -+ private boolean clipsAnything(final Vec3 from, final Vec3 to, -+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext context, -+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache, -+ final BlockPos.MutableBlockPos currPos) { -+ // assume that context.delegated = false -+ final double adjX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.x - to.x); -+ final double adjY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.y - to.y); -+ final double adjZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.z - to.z); -+ -+ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) { -+ return false; -+ } -+ -+ final double toXAdj = to.x - adjX; -+ final double toYAdj = to.y - adjY; -+ final double toZAdj = to.z - adjZ; -+ final double fromXAdj = from.x + adjX; -+ final double fromYAdj = from.y + adjY; -+ final double fromZAdj = from.z + adjZ; -+ -+ int currX = Mth.floor(fromXAdj); -+ int currY = Mth.floor(fromYAdj); -+ int currZ = Mth.floor(fromZAdj); -+ -+ final double diffX = toXAdj - fromXAdj; -+ final double diffY = toYAdj - fromYAdj; -+ final double diffZ = toZAdj - fromZAdj; -+ -+ final double dxDouble = Math.signum(diffX); -+ final double dyDouble = Math.signum(diffY); -+ final double dzDouble = Math.signum(diffZ); -+ -+ final int dx = (int)dxDouble; -+ final int dy = (int)dyDouble; -+ final int dz = (int)dzDouble; -+ -+ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX; -+ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY; -+ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ; -+ -+ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj)); -+ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj)); -+ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj)); -+ -+ for (;;) { -+ currPos.set(currX, currY, currZ); -+ -+ // ClipContext.Block.COLLIDER -> BlockBehaviour.BlockStateBase::getCollisionShape -+ // ClipContext.Fluid.NONE -> ignore fluids -+ -+ // read block from cache -+ final long key = BlockPos.asLong(currX, currY, currZ); -+ -+ final int cacheKey = -+ (currX & BLOCK_EXPLOSION_CACHE_MASK) | -+ (currY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | -+ (currZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); -+ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache cachedBlock = blockCache[cacheKey]; -+ if (cachedBlock == null || cachedBlock.key != key) { -+ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(currX, currY, currZ, key, false); -+ } -+ -+ final BlockState blockState = cachedBlock.blockState; -+ if (blockState != null && !((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$emptyContextCollisionShape()) { -+ net.minecraft.world.phys.shapes.VoxelShape collision = cachedBlock.cachedCollisionShape; -+ if (collision == null) { -+ collision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$getConstantContextCollisionShape(); -+ if (collision == null) { -+ collision = blockState.getCollisionShape(this.level, currPos, context); -+ if (!context.isDelegated()) { -+ // if it was not delegated during this call, assume that for any future ones it will not be delegated -+ // again, and cache the result -+ cachedBlock.cachedCollisionShape = collision; -+ } -+ } else { -+ cachedBlock.cachedCollisionShape = collision; -+ } -+ } -+ -+ if (!collision.isEmpty() && collision.clip(from, to, currPos) != null) { -+ return true; -+ } -+ } -+ -+ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) { -+ return false; -+ } -+ -+ // inc the smallest normalized coordinate -+ -+ if (normalizedCurrX < normalizedCurrY) { -+ if (normalizedCurrX < normalizedCurrZ) { -+ currX += dx; -+ normalizedCurrX += normalizedDiffX; -+ } else { -+ // x < y && x >= z <--> z < y && z <= x -+ currZ += dz; -+ normalizedCurrZ += normalizedDiffZ; -+ } -+ } else if (normalizedCurrY < normalizedCurrZ) { -+ // y <= x && y < z -+ currY += dy; -+ normalizedCurrY += normalizedDiffY; -+ } else { -+ // y <= x && z <= y <--> z <= y && z <= x -+ currZ += dz; -+ normalizedCurrZ += normalizedDiffZ; -+ } -+ } -+ } -+ -+ private float getSeenFraction(final Vec3 source, final Entity target, -+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache, -+ final BlockPos.MutableBlockPos blockPos) { -+ final AABB boundingBox = target.getBoundingBox(); -+ final double diffX = boundingBox.maxX - boundingBox.minX; -+ final double diffY = boundingBox.maxY - boundingBox.minY; -+ final double diffZ = boundingBox.maxZ - boundingBox.minZ; -+ -+ final double incX = 1.0 / (diffX * 2.0 + 1.0); -+ final double incY = 1.0 / (diffY * 2.0 + 1.0); -+ final double incZ = 1.0 / (diffZ * 2.0 + 1.0); -+ -+ if (incX < 0.0 || incY < 0.0 || incZ < 0.0) { -+ return 0.0f; -+ } -+ -+ final double offX = (1.0 - Math.floor(1.0 / incX) * incX) * 0.5 + boundingBox.minX; -+ final double offY = boundingBox.minY; -+ final double offZ = (1.0 - Math.floor(1.0 / incZ) * incZ) * 0.5 + boundingBox.minZ; -+ -+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext context = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(target); -+ -+ int totalRays = 0; -+ int missedRays = 0; -+ -+ for (double dx = 0.0; dx <= 1.0; dx += incX) { -+ final double fromX = Math.fma(dx, diffX, offX); -+ for (double dy = 0.0; dy <= 1.0; dy += incY) { -+ final double fromY = Math.fma(dy, diffY, offY); -+ for (double dz = 0.0; dz <= 1.0; dz += incZ) { -+ ++totalRays; -+ -+ final Vec3 from = new Vec3( -+ fromX, -+ fromY, -+ Math.fma(dz, diffZ, offZ) -+ ); -+ -+ if (!this.clipsAnything(from, source, context, blockCache, blockPos)) { -+ ++missedRays; -+ } -+ } -+ } -+ } -+ -+ return (float)missedRays / (float)totalRays; -+ } -+ // Paper end - collisions optimisations - - public ServerExplosion(ServerLevel world, @Nullable Entity entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, Vec3 pos, float power, boolean createFire, Explosion.BlockInteraction destructionType) { - this.level = world; -@@ -127,65 +370,101 @@ public class ServerExplosion implements Explosion { - } - - private List<BlockPos> calculateExplodedPositions() { -- Set<BlockPos> set = new HashSet(); -- boolean flag = true; -- -- for (int i = 0; i < 16; ++i) { -- for (int j = 0; j < 16; ++j) { -- for (int k = 0; k < 16; ++k) { -- if (i == 0 || i == 15 || j == 0 || j == 15 || k == 0 || k == 15) { -- double d0 = (double) ((float) i / 15.0F * 2.0F - 1.0F); -- double d1 = (double) ((float) j / 15.0F * 2.0F - 1.0F); -- double d2 = (double) ((float) k / 15.0F * 2.0F - 1.0F); -- double d3 = Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2); -- -- d0 /= d3; -- d1 /= d3; -- d2 /= d3; -- float f = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F); -- double d4 = this.center.x; -- double d5 = this.center.y; -- double d6 = this.center.z; -- -- for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) { -- BlockPos blockposition = BlockPos.containing(d4, d5, d6); -- BlockState iblockdata = this.level.getBlockState(blockposition); -- if (!iblockdata.isDestroyable()) continue; // Paper - Protect Bedrock and End Portal/Frames from being destroyed -- FluidState fluid = iblockdata.getFluidState(); // Paper - Perf: Optimize call to getFluid for explosions -- -- if (!this.level.isInWorldBounds(blockposition)) { -- break; -- } -+ // Paper start - collision optimisations -+ final ObjectArrayList<BlockPos> ret = new ObjectArrayList<>(); - -- Optional<Float> optional = this.damageCalculator.getBlockExplosionResistance(this, this.level, blockposition, iblockdata, fluid); -+ final Vec3 center = this.center; - -- if (optional.isPresent()) { -- f -= ((Float) optional.get() + 0.3F) * 0.3F; -- } -+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache = this.directMappedBlockCache; - -- if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockposition, iblockdata, f)) { -- set.add(blockposition); -- // Paper start - prevent headless pistons from forming -- if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) { -- net.minecraft.world.level.block.entity.BlockEntity extension = this.level.getBlockEntity(blockposition); -- if (extension instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity blockEntity && blockEntity.isSourcePiston()) { -- net.minecraft.core.Direction direction = iblockdata.getValue(net.minecraft.world.level.block.piston.PistonHeadBlock.FACING); -- set.add(blockposition.relative(direction.getOpposite())); -- } -+ // use initial cache value that is most likely to be used: the source position -+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache initialCache; -+ { -+ final int blockX = Mth.floor(center.x); -+ final int blockY = Mth.floor(center.y); -+ final int blockZ = Mth.floor(center.z); -+ -+ final long key = BlockPos.asLong(blockX, blockY, blockZ); -+ -+ initialCache = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); -+ } -+ -+ // only ~1/3rd of the loop iterations in vanilla will result in a ray, as it is iterating the perimeter of -+ // a 16x16x16 cube -+ // we can cache the rays and their normals as well, so that we eliminate the excess iterations / checks and -+ // calculations in one go -+ // additional aggressive caching of block retrieval is very significant, as at low power (i.e tnt) most -+ // block retrievals are not unique -+ for (int ray = 0, len = CACHED_RAYS.length; ray < len;) { -+ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache cachedBlock = initialCache; -+ -+ double currX = center.x; -+ double currY = center.y; -+ double currZ = center.z; -+ -+ final double incX = CACHED_RAYS[ray]; -+ final double incY = CACHED_RAYS[ray + 1]; -+ final double incZ = CACHED_RAYS[ray + 2]; -+ -+ ray += 3; -+ -+ float power = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F); -+ -+ do { -+ final int blockX = Mth.floor(currX); -+ final int blockY = Mth.floor(currY); -+ final int blockZ = Mth.floor(currZ); -+ -+ final long key = BlockPos.asLong(blockX, blockY, blockZ); -+ -+ if (cachedBlock.key != key) { -+ final int cacheKey = -+ (blockX & BLOCK_EXPLOSION_CACHE_MASK) | -+ (blockY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | -+ (blockZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); -+ cachedBlock = blockCache[cacheKey]; -+ if (cachedBlock == null || cachedBlock.key != key) { -+ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); -+ } -+ } -+ -+ if (cachedBlock.outOfWorld) { -+ break; -+ } -+ final BlockState iblockdata = cachedBlock.blockState; -+ -+ power -= cachedBlock.resistance; -+ -+ if (power > 0.0f && cachedBlock.shouldExplode == null) { -+ // note: we expect shouldBlockExplode to be pure with respect to power, as Vanilla currently is. -+ // basically, it is unused, which allows us to cache the result -+ final boolean shouldExplode = iblockdata.isDestroyable() && this.damageCalculator.shouldBlockExplode((Explosion)(Object)this, this.level, cachedBlock.immutablePos, cachedBlock.blockState, power); // Paper - Protect Bedrock and End Portal/Frames from being destroyed -+ cachedBlock.shouldExplode = shouldExplode ? Boolean.TRUE : Boolean.FALSE; -+ if (shouldExplode) { -+ if (this.fire || !cachedBlock.blockState.isAir()) { -+ ret.add(cachedBlock.immutablePos); -+ // Paper start - prevent headless pistons from forming -+ if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) { -+ net.minecraft.world.level.block.entity.BlockEntity extension = this.level.getBlockEntity(cachedBlock.immutablePos); // Paper - optimise collisions -+ if (extension instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity blockEntity && blockEntity.isSourcePiston()) { -+ net.minecraft.core.Direction direction = iblockdata.getValue(net.minecraft.world.level.block.piston.PistonHeadBlock.FACING); -+ ret.add(cachedBlock.immutablePos.relative(direction.getOpposite())); // Paper - optimise collisions - } -- // Paper end - prevent headless pistons from forming - } -- -- d4 += d0 * 0.30000001192092896D; -- d5 += d1 * 0.30000001192092896D; -- d6 += d2 * 0.30000001192092896D; -+ // Paper end - prevent headless pistons from forming - } - } - } -- } -+ -+ power -= 0.22500001F; -+ currX += incX; -+ currY += incY; -+ currZ += incZ; -+ } while (power > 0.0f); - } - -- return new ObjectArrayList(set); -+ return ret; -+ // Paper end - collision optimisations - } - - private void hurtEntities() { -@@ -391,6 +670,14 @@ public class ServerExplosion implements Explosion { - return; - } - // CraftBukkit end -+ // Paper start - collision optimisations -+ this.blockCache = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(); -+ this.chunkPosCache = new long[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; -+ java.util.Arrays.fill(this.chunkPosCache, ChunkPos.INVALID_CHUNK_POS); -+ this.chunkCache = new net.minecraft.world.level.chunk.LevelChunk[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; -+ this.directMappedBlockCache = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH]; -+ this.mutablePos = new BlockPos.MutableBlockPos(); -+ // Paper end - collision optimisations - this.level.gameEvent(this.source, (Holder) GameEvent.EXPLODE, this.center); - List<BlockPos> list = this.calculateExplodedPositions(); - -@@ -406,6 +693,13 @@ public class ServerExplosion implements Explosion { - if (this.fire) { - this.createFire(list); - } -+ // Paper start - collision optimisations -+ this.blockCache = null; -+ this.chunkPosCache = null; -+ this.chunkCache = null; -+ this.directMappedBlockCache = null; -+ this.mutablePos = null; -+ // Paper end - collision optimisations - - } - -@@ -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<Block, BlockState> { -+ public abstract static class BlockStateBase extends StateHolder<Block, BlockState> implements ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState, ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState { // Paper - rewrite chunk system // Paper - optimise collisions - - private static final Direction[] DIRECTIONS = Direction.values(); - private static final VoxelShape[] EMPTY_OCCLUSION_SHAPES = (VoxelShape[]) Util.make(new VoxelShape[BlockBehaviour.BlockStateBase.DIRECTIONS.length], (avoxelshape) -> { -@@ -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<Property<?>, Comparable<?>> propertyMap, MapCodec<BlockState> 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<O, S> { -+public abstract class StateHolder<O, S> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccessStateHolder { // Paper - optimise blockstate property access - public static final String NAME_TAG = "Name"; - public static final String PROPERTIES_TAG = "Properties"; - public static final Function<Entry<Property<?>, Comparable<?>>, String> PROPERTY_ENTRY_TO_STRING_FUNCTION = new Function<Entry<Property<?>, Comparable<?>>, String>() { -@@ -34,14 +34,28 @@ public abstract class StateHolder<O, S> { - } - }; - protected final O owner; -- private final Reference2ObjectArrayMap<Property<?>, Comparable<?>> values; -+ private Reference2ObjectArrayMap<Property<?>, Comparable<?>> values; // Paper - optimise blockstate property access - remove final - private Map<Property<?>, S[]> neighbours; - protected final MapCodec<S> propertiesCodec; - -+ // Paper start - optimise blockstate property access -+ protected ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<O, S> optimisedTable; -+ protected final long tableIndex; -+ -+ @Override -+ public final long moonrise$getTableIndex() { -+ return this.tableIndex; -+ } -+ // Paper end - optimise blockstate property access -+ - protected StateHolder(O owner, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<S> codec) { - this.owner = owner; - this.values = propertyMap; - this.propertiesCodec = codec; -+ // Paper start - optimise blockstate property access -+ this.optimisedTable = new ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<>(this.values.keySet()); -+ this.tableIndex = this.optimisedTable.getIndex((StateHolder<O, S>)(Object)this); -+ // Paper end - optimise blockstate property access - } - - public <T extends Comparable<T>> S cycle(Property<T> property) { -@@ -67,20 +81,21 @@ public abstract class StateHolder<O, S> { - } - - public Collection<Property<?>> getProperties() { -- return Collections.unmodifiableCollection(this.values.keySet()); -+ return this.optimisedTable.getProperties(); // Paper - optimise blockstate property access - } - - public <T extends Comparable<T>> boolean hasProperty(Property<T> property) { -- return this.values.containsKey(property); -+ return property != null && this.optimisedTable.hasProperty(property); // Paper - optimise blockstate property access - } - - public <T extends Comparable<T>> T getValue(Property<T> property) { -- Comparable<?> comparable = this.values.get(property); -- if (comparable == null) { -- throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner); -- } else { -- return property.getValueClass().cast(comparable); -+ // Paper start - optimise blockstate property access -+ final T ret = this.optimisedTable.get(this.tableIndex, property); -+ if (ret != null) { -+ return ret; - } -+ throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner); -+ // Paper end - optimise blockstate property access - } - - public <T extends Comparable<T>> Optional<T> getOptionalValue(Property<T> property) { -@@ -93,22 +108,30 @@ public abstract class StateHolder<O, S> { - - @Nullable - public <T extends Comparable<T>> T getNullableValue(Property<T> property) { -- Comparable<?> comparable = this.values.get(property); -- return comparable == null ? null : property.getValueClass().cast(comparable); -+ return property == null ? null : this.optimisedTable.get(this.tableIndex, property); // Paper - optimise blockstate property access - } - - public <T extends Comparable<T>, V extends T> S setValue(Property<T> property, V value) { -- Comparable<?> comparable = this.values.get(property); -- if (comparable == null) { -- throw new IllegalArgumentException("Cannot set property " + property + " as it does not exist in " + this.owner); -- } else { -- return this.setValueInternal(property, value, comparable); -+ // Paper start - optimise blockstate property access -+ final S ret = this.optimisedTable.set(this.tableIndex, property, value); -+ if (ret != null) { -+ return ret; - } -+ throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner); -+ // Paper end - optimise blockstate property access - } - - public <T extends Comparable<T>, V extends T> S trySetValue(Property<T> property, V value) { -- Comparable<?> comparable = this.values.get(property); -- return (S)(comparable == null ? this : this.setValueInternal(property, value, comparable)); -+ // Paper start - optimise blockstate property access -+ if (property == null) { -+ return (S)(StateHolder<O, S>)(Object)this; -+ } -+ final S ret = this.optimisedTable.trySet(this.tableIndex, property, value, (S)(StateHolder<O, S>)(Object)this); -+ if (ret != null) { -+ return ret; -+ } -+ throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner); -+ // Paper end - optimise blockstate property access - } - - private <T extends Comparable<T>, V extends T> S setValueInternal(Property<T> property, V newValue, Comparable<?> oldValue) { -@@ -125,18 +148,27 @@ public abstract class StateHolder<O, S> { - } - - public void populateNeighbours(Map<Map<Property<?>, Comparable<?>>, S> states) { -- if (this.neighbours != null) { -- throw new IllegalStateException(); -- } else { -- Map<Property<?>, S[]> map = new Reference2ObjectArrayMap<>(this.values.size()); -+ // Paper start - optimise blockstate property access -+ final Map<Map<Property<?>, Comparable<?>>, S> map = states; -+ if (this.optimisedTable.isLoaded()) { -+ return; -+ } -+ this.optimisedTable.loadInTable(map); - -- for (Entry<Property<?>, Comparable<?>> entry : this.values.entrySet()) { -- Property<?> property = entry.getKey(); -- map.put(property, property.getPossibleValues().stream().map(value -> states.get(this.makeNeighbourValues(property, value))).toArray()); -- } -+ // de-duplicate the tables -+ for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : map.entrySet()) { -+ final S value = entry.getValue(); -+ ((StateHolder<O, S>)value).optimisedTable = this.optimisedTable; -+ } - -- this.neighbours = map; -+ // remove values arrays -+ for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : map.entrySet()) { -+ final S value = entry.getValue(); -+ ((StateHolder<O, S>)value).values = null; - } -+ -+ return; -+ // Paper end optimise blockstate property access - } - - private Map<Property<?>, Comparable<?>> makeNeighbourValues(Property<?> property, Comparable<?> value) { -@@ -146,7 +178,11 @@ public abstract class StateHolder<O, S> { - } - - public Map<Property<?>, Comparable<?>> getValues() { -- return this.values; -+ // Paper start - optimise blockstate property access -+ ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<O, S> table = this.optimisedTable; -+ // We have to use this.values until the table is loaded -+ return table.isLoaded() ? table.getMapView(this.tableIndex) : this.values; -+ // Paper end - optimise blockstate property access - } - - protected static <O, S extends StateHolder<O, S>> Codec<S> codec(Codec<O> codec, Function<O, S> ownerToStateFunction) { -diff --git a/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<Boolean> { -+public final class BooleanProperty extends Property<Boolean> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<Boolean> { // Paper - optimise blockstate property access - private static final List<Boolean> VALUES = List.of(true, false); - private static final int TRUE_INDEX = 0; - private static final int FALSE_INDEX = 1; - -+ // Paper start - optimise blockstate property access -+ private static final Boolean[] BY_ID = new Boolean[]{ Boolean.FALSE, Boolean.TRUE }; -+ -+ @Override -+ public final int moonrise$getIdFor(final Boolean value) { -+ return value.booleanValue() ? 1 : 0; -+ } -+ // Paper end - optimise blockstate property access -+ - private BooleanProperty(String name) { - super(name, Boolean.class); -+ this.moonrise$setById(BY_ID); // Paper - optimise blockstate property access - } - - @Override -diff --git a/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<T extends Enum<T> & StringRepresentable> extends Property<T> { -+public final class EnumProperty<T extends Enum<T> & StringRepresentable> extends Property<T> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<T> { // Paper - optimise blockstate property access - private final List<T> values; - private final Map<String, T> names; - private final int[] ordinalToIndex; - -+ // Paper start - optimise blockstate property access -+ private int[] idLookupTable; -+ -+ @Override -+ public final int moonrise$getIdFor(final T value) { -+ final Class<T> target = this.getValueClass(); -+ return ((value.getClass() != target && value.getDeclaringClass() != target)) ? -1 : this.idLookupTable[value.ordinal()]; -+ } -+ -+ private void init() { -+ final java.util.Collection<T> values = this.getPossibleValues(); -+ final Class<T> clazz = this.getValueClass(); -+ -+ int id = 0; -+ this.idLookupTable = new int[clazz.getEnumConstants().length]; -+ Arrays.fill(this.idLookupTable, -1); -+ final T[] byId = (T[])java.lang.reflect.Array.newInstance(clazz, values.size()); -+ -+ for (final T value : values) { -+ final int valueId = id++; -+ this.idLookupTable[value.ordinal()] = valueId; -+ byId[valueId] = value; -+ } -+ -+ this.moonrise$setById(byId); -+ } -+ // Paper end - optimise blockstate property access -+ - private EnumProperty(String name, Class<T> type, List<T> values) { - super(name, type); - if (values.isEmpty()) { -@@ -37,6 +65,7 @@ public final class EnumProperty<T extends Enum<T> & StringRepresentable> extends - - this.names = builder.buildOrThrow(); - } -+ this.init(); // Paper - optimise blockstate property access - } - - @Override -diff --git a/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<Integer> { -+public final class IntegerProperty extends Property<Integer> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<Integer> { // Paper - optimise blockstate property access - private final IntImmutableList values; - public final int min; - public final int max; - -+ // Paper start - optimise blockstate property access -+ @Override -+ public final int moonrise$getIdFor(final Integer value) { -+ final int val = value.intValue(); -+ final int ret = val - this.min; -+ -+ return ret | ((this.max - ret) >> 31); -+ } -+ -+ private void init() { -+ final int min = this.min; -+ final int max = this.max; -+ -+ final Integer[] byId = new Integer[max - min + 1]; -+ for (int i = min; i <= max; ++i) { -+ byId[i - min] = Integer.valueOf(i); -+ } -+ -+ this.moonrise$setById(byId); -+ } -+ // Paper end - optimise blockstate property access -+ - private IntegerProperty(String name, int min, int max) { - super(name, Integer.class); - if (min < 0) { -@@ -21,6 +43,7 @@ public final class IntegerProperty extends Property<Integer> { - this.max = max; - this.values = IntImmutableList.toList(IntStream.range(min, max + 1)); - } -+ this.init(); // Paper - optimise blockstate property access - } - - @Override -diff --git a/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<T extends Comparable<T>> { -+public abstract class Property<T extends Comparable<T>> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<T> { // Paper - optimise blockstate property access - private final Class<T> clazz; - private final String name; - @Nullable -@@ -24,9 +24,38 @@ public abstract class Property<T extends Comparable<T>> { - ); - private final Codec<Property.Value<T>> valueCodec = this.codec.xmap(this::value, Property.Value::value); - -+ // Paper start - optimise blockstate property access -+ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger(); -+ private final int id; -+ private T[] byId; -+ -+ @Override -+ public final int moonrise$getId() { -+ return this.id; -+ } -+ -+ @Override -+ public final T moonrise$getById(final int id) { -+ final T[] byId = this.byId; -+ return id < 0 || id >= byId.length ? null : this.byId[id]; -+ } -+ -+ @Override -+ public final void moonrise$setById(final T[] byId) { -+ if (this.byId != null) { -+ throw new IllegalStateException(); -+ } -+ this.byId = byId; -+ } -+ -+ @Override -+ public abstract int moonrise$getIdFor(final T value); -+ // Paper end - optimise blockstate property access -+ - protected Property(String name, Class<T> type) { - this.clazz = type; - this.name = name; -+ this.id = ID_GENERATOR.getAndIncrement(); // Paper - optimise blockstate property access - } - - public Property.Value<T> value(T value) { -diff --git a/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<Heightmap.Types, Heightmap> heightmaps = Maps.newEnumMap(Heightmap.Types.class); -- protected ChunkSkyLightSources skyLightSources; -+ // Paper - rewrite chunk system - private final Map<Structure, StructureStart> structureStarts = Maps.newHashMap(); - private final Map<Structure, LongSet> structuresRefences = Maps.newHashMap(); - protected final Map<BlockPos, CompoundTag> pendingBlockEntities = Maps.newHashMap(); -@@ -90,6 +90,57 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh - public org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer(ChunkAccess.DATA_TYPE_REGISTRY); - // CraftBukkit end - -+ // Paper start - rewrite chunk system -+ private volatile ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles; -+ private volatile ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] skyNibbles; -+ private volatile boolean[] skyEmptinessMap; -+ private volatile boolean[] blockEmptinessMap; -+ -+ @Override -+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() { -+ return this.blockNibbles; -+ } -+ -+ @Override -+ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) { -+ this.blockNibbles = nibbles; -+ } -+ -+ @Override -+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() { -+ return this.skyNibbles; -+ } -+ -+ @Override -+ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) { -+ this.skyNibbles = nibbles; -+ } -+ -+ @Override -+ public boolean[] starlight$getSkyEmptinessMap() { -+ return this.skyEmptinessMap; -+ } -+ -+ @Override -+ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) { -+ this.skyEmptinessMap = emptinessMap; -+ } -+ -+ @Override -+ public boolean[] starlight$getBlockEmptinessMap() { -+ return this.blockEmptinessMap; -+ } -+ -+ @Override -+ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) { -+ this.blockEmptinessMap = emptinessMap; -+ } -+ // Paper end - rewrite chunk system -+ // Paper start - get block chunk optimisation -+ private final int minSection; -+ private final int maxSection; -+ // Paper end - get block chunk optimisation -+ - public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry<Biome> biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) { - this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups - this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key -@@ -99,7 +150,7 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh - this.inhabitedTime = inhabitedTime; - this.postProcessing = new ShortList[heightLimitView.getSectionsCount()]; - this.blendingData = blendingData; -- this.skyLightSources = new ChunkSkyLightSources(heightLimitView); -+ // Paper - rewrite chunk system - if (sectionArray != null) { - if (this.sections.length == sectionArray.length) { - System.arraycopy(sectionArray, 0, this.sections, 0, this.sections.length); -@@ -111,6 +162,16 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh - this.replaceMissingSections(biomeRegistry, this.sections); // Paper - Anti-Xray - make it a non-static method - // CraftBukkit start - this.biomeRegistry = biomeRegistry; -+ // Paper start - rewrite chunk system -+ if (!((Object)this instanceof ImposterProtoChunk)) { -+ this.starlight$setBlockNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(heightLimitView)); -+ this.starlight$setSkyNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(heightLimitView)); -+ } -+ // Paper end - rewrite chunk system -+ // Paper start - get block chunk optimisation -+ this.minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(levelHeightAccessor); -+ this.maxSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(levelHeightAccessor); -+ // Paper end - get block chunk optimisation - } - public final Registry<Biome> biomeRegistry; - // CraftBukkit end -@@ -457,22 +518,22 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh - - @Override - public Holder<Biome> getNoiseBiome(int biomeX, int biomeY, int biomeZ) { -- try { -- int l = QuartPos.fromBlock(this.getMinY()); -- int i1 = l + QuartPos.fromBlock(this.getHeight()) - 1; -- int j1 = Mth.clamp(biomeY, l, i1); -- int k1 = this.getSectionIndex(QuartPos.toBlock(j1)); -- -- return this.sections[k1].getNoiseBiome(biomeX & 3, j1 & 3, biomeZ & 3); -- } catch (Throwable throwable) { -- CrashReport crashreport = CrashReport.forThrowable(throwable, "Getting biome"); -- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Biome being got"); -- -- crashreportsystemdetails.setDetail("Location", () -> { -- return CrashReportCategory.formatLocation(this, biomeX, biomeY, biomeZ); -- }); -- throw new ReportedException(crashreport); -+ // Paper start - get block chunk optimisation -+ int sectionY = (biomeY >> 2) - this.minSection; -+ int rel = biomeY & 3; -+ -+ final LevelChunkSection[] sections = this.sections; -+ -+ if (sectionY < 0) { -+ sectionY = 0; -+ rel = 0; -+ } else if (sectionY >= sections.length) { -+ sectionY = sections.length - 1; -+ rel = 3; - } -+ -+ return sections[sectionY].getNoiseBiome(biomeX & 3, rel, biomeZ & 3); -+ // Paper end - get block chunk optimisation - } - - // CraftBukkit start -@@ -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<SavedTick<Block>> blocks, List<SavedTick<Fluid>> 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> biome; - - public EmptyLevelChunk(Level world, ChunkPos pos, Holder<Biome> biomeEntry) { -@@ -21,6 +21,40 @@ public class EmptyLevelChunk extends LevelChunk { - this.biome = biomeEntry; - } - -+ // Paper start - rewrite chunk system -+ @Override -+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() { -+ return ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(this.getLevel()); -+ } -+ -+ @Override -+ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {} -+ -+ @Override -+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() { -+ return ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(this.getLevel()); -+ } -+ -+ @Override -+ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {} -+ -+ @Override -+ public boolean[] starlight$getSkyEmptinessMap() { -+ return null; -+ } -+ -+ @Override -+ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) {} -+ -+ @Override -+ public boolean[] starlight$getBlockEmptinessMap() { -+ return null; -+ } -+ -+ @Override -+ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) {} -+ // Paper end - rewrite chunk system -+ - @Override - public BlockState getBlockState(BlockPos pos) { - return Blocks.VOID_AIR.defaultBlockState(); -diff --git a/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<T> implements Palette<T> { -+public class HashMapPalette<T> implements Palette<T>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads - private final IdMap<T> registry; - private final CrudeIncrementalIntIdentityHashBiMap<T> values; - private final PaletteResize<T> resizeHandler; - private final int bits; - -+ // Paper start - optimise palette reads -+ @Override -+ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> container) { -+ return ((ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T>)this.values).moonrise$getRawPalette(container); -+ } -+ // Paper end - optimise palette reads -+ - public HashMapPalette(IdMap<T> idList, int bits, PaletteResize<T> listener, List<T> entries) { - this(idList, bits, listener); - entries.forEach(this.values::add); -diff --git a/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<BlockState> states; - private PalettedContainer<Holder<Biome>> biomes; // CraftBukkit - read/write - -+ // Paper start - block counting -+ private static final it.unimi.dsi.fastutil.shorts.ShortArrayList FULL_LIST = new it.unimi.dsi.fastutil.shorts.ShortArrayList(16*16*16); -+ static { -+ for (short i = 0; i < (16*16*16); ++i) { -+ FULL_LIST.add(i); -+ } -+ } -+ -+ private boolean isClient; -+ private static final short CLIENT_FORCED_SPECIAL_COLLIDING_BLOCKS = (short)9999; -+ private short specialCollidingBlocks; -+ private final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = new ca.spottedleaf.moonrise.common.list.ShortList(); -+ -+ @Override -+ public final boolean moonrise$hasSpecialCollidingBlocks() { -+ return this.specialCollidingBlocks != 0; -+ } -+ -+ @Override -+ public final ca.spottedleaf.moonrise.common.list.ShortList moonrise$getTickingBlockList() { -+ return this.tickingBlocks; -+ } -+ // Paper end - block counting -+ - private LevelChunkSection(LevelChunkSection section) { - this.nonEmptyBlockCount = section.nonEmptyBlockCount; - this.tickingBlockCount = section.tickingBlockCount; -@@ -67,6 +91,45 @@ public class LevelChunkSection { - return this.setBlockState(x, y, z, state, true); - } - -+ // Paper start - block counting -+ private void updateBlockCallback(final int x, final int y, final int z, final BlockState newState, -+ final BlockState oldState) { -+ if (oldState == newState) { -+ return; -+ } -+ -+ if (this.isClient) { -+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(newState)) { -+ this.specialCollidingBlocks = CLIENT_FORCED_SPECIAL_COLLIDING_BLOCKS; -+ } -+ return; -+ } -+ -+ final boolean isSpecialOld = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(oldState); -+ final boolean isSpecialNew = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(newState); -+ if (isSpecialOld != isSpecialNew) { -+ if (isSpecialOld) { -+ --this.specialCollidingBlocks; -+ } else { -+ ++this.specialCollidingBlocks; -+ } -+ } -+ -+ final boolean oldTicking = oldState.isRandomlyTicking(); -+ final boolean newTicking = newState.isRandomlyTicking(); -+ if (oldTicking != newTicking) { -+ final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = this.tickingBlocks; -+ final short position = (short)(x | (z << 4) | (y << (4+4))); -+ -+ if (oldTicking) { -+ tickingBlocks.remove(position); -+ } else { -+ tickingBlocks.add(position); -+ } -+ } -+ } -+ // Paper end - block counting -+ - public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) { - BlockState iblockdata1; - -@@ -86,7 +149,7 @@ public class LevelChunkSection { - } - } - -- if (!fluid.isEmpty()) { -+ if (!!fluid.isRandomlyTicking()) { // Paper - block counting - --this.tickingFluidCount; - } - -@@ -97,10 +160,12 @@ public class LevelChunkSection { - } - } - -- if (!fluid1.isEmpty()) { -+ if (!!fluid1.isRandomlyTicking()) { // Paper - block counting - ++this.tickingFluidCount; - } - -+ this.updateBlockCallback(x, y, z, state, iblockdata1); // Paper - block counting -+ - return iblockdata1; - } - -@@ -121,40 +186,70 @@ public class LevelChunkSection { - } - - public void recalcBlockCounts() { -- class a implements PalettedContainer.CountConsumer<BlockState> { -+ // Paper start - block counting -+ // reset, then recalculate -+ this.nonEmptyBlockCount = (short)0; -+ this.tickingBlockCount = (short)0; -+ this.tickingFluidCount = (short)0; -+ this.specialCollidingBlocks = (short)0; -+ this.tickingBlocks.clear(); -+ -+ if (this.maybeHas((final BlockState state) -> !state.isAir())) { -+ final PalettedContainer.Data<BlockState> data = this.states.data; -+ final Palette<BlockState> palette = data.palette(); -+ final int paletteSize = palette.getSize(); -+ final net.minecraft.util.BitStorage storage = data.storage(); -+ -+ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> counts; -+ if (paletteSize == 1) { -+ counts = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1); -+ counts.put(0, FULL_LIST); -+ } else { -+ counts = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingBitStorage)storage).moonrise$countEntries(); -+ } -+ -+ for (final java.util.Iterator<it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<it.unimi.dsi.fastutil.shorts.ShortArrayList>> iterator = counts.int2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<it.unimi.dsi.fastutil.shorts.ShortArrayList> entry = iterator.next(); -+ final int paletteIdx = entry.getIntKey(); -+ final it.unimi.dsi.fastutil.shorts.ShortArrayList coordinates = entry.getValue(); -+ final int paletteCount = coordinates.size(); - -- public int nonEmptyBlockCount; -- public int tickingBlockCount; -- public int tickingFluidCount; -+ final BlockState state = palette.valueFor(paletteIdx); - -- a(final LevelChunkSection chunksection) {} -+ if (state.isAir()) { -+ continue; -+ } -+ -+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(state)) { -+ this.specialCollidingBlocks += (short)paletteCount; -+ } -+ this.nonEmptyBlockCount += (short)paletteCount; -+ if (state.isRandomlyTicking()) { -+ this.tickingBlockCount += (short)paletteCount; -+ final short[] raw = coordinates.elements(); -+ final int rawLen = raw.length; -+ -+ final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = this.tickingBlocks; - -- public void accept(BlockState iblockdata, int i) { -- FluidState fluid = iblockdata.getFluidState(); -+ tickingBlocks.setMinCapacity(Math.min((rawLen + tickingBlocks.size()) * 3 / 2, 16*16*16)); - -- if (!iblockdata.isAir()) { -- this.nonEmptyBlockCount += i; -- if (iblockdata.isRandomlyTicking()) { -- this.tickingBlockCount += i; -+ java.util.Objects.checkFromToIndex(0, paletteCount, raw.length); -+ for (int i = 0; i < paletteCount; ++i) { -+ tickingBlocks.add(raw[i]); - } - } - -+ final FluidState fluid = state.getFluidState(); -+ - if (!fluid.isEmpty()) { -- this.nonEmptyBlockCount += i; -+ //this.nonEmptyBlockCount += count; // fix vanilla bug: make non-empty block count correct - if (fluid.isRandomlyTicking()) { -- this.tickingFluidCount += i; -+ this.tickingFluidCount += (short)paletteCount; - } - } -- - } - } -- -- a a0 = new a(this); -- -- this.states.count(a0); -- this.nonEmptyBlockCount = (short) a0.nonEmptyBlockCount; -- this.tickingBlockCount = (short) a0.tickingBlockCount; -- this.tickingFluidCount = (short) a0.tickingFluidCount; -+ // Paper end - block counting - } - - public PalettedContainer<BlockState> getStates() { -@@ -172,6 +267,11 @@ public class LevelChunkSection { - - datapaletteblock.read(buf); - this.biomes = datapaletteblock; -+ // Paper start - block counting -+ this.isClient = true; -+ // force has special colliding blocks to be true -+ this.specialCollidingBlocks = this.nonEmptyBlockCount != (short)0 && this.maybeHas(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil::isSpecialCollidingBlock) ? CLIENT_FORCED_SPECIAL_COLLIDING_BLOCKS : (short)0; -+ // Paper end - block counting - } - - public void readBiomes(FriendlyByteBuf buf) { -diff --git a/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<T> implements Palette<T> { -+public class LinearPalette<T> implements Palette<T>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads - private final IdMap<T> registry; - private final T[] values; - private final PaletteResize<T> resizeHandler; - private final int bits; - private int size; - -+ // Paper start - optimise palette reads -+ @Override -+ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> container) { -+ return this.values; -+ } -+ // Paper end - optimise palette reads -+ - private LinearPalette(IdMap<T> idList, int bits, PaletteResize<T> listener, List<T> list) { - this.registry = idList; - this.values = (T[])(new Object[1 << bits]); -diff --git a/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<T> { -+public interface Palette<T> extends ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads - int idFor(T object); - - boolean maybeHas(Predicate<T> predicate); -diff --git a/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<T> implements PaletteResize<T>, PalettedContainer - private final PaletteResize<T> dummyPaletteResize = (newSize, added) -> 0; - public final IdMap<T> registry; - private final T @org.jetbrains.annotations.Nullable [] presetValues; // Paper - Anti-Xray - Add preset values -- private volatile PalettedContainer.Data<T> data; -+ public volatile PalettedContainer.Data<T> data; // Paper - optimise collisions - public - private final PalettedContainer.Strategy strategy; - // private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused - -@@ -77,6 +77,33 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer - ); - } - -+ // Paper start - optimise palette reads -+ private void updateData(final PalettedContainer.Data<T> data) { -+ if (data != null) { -+ ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T>)(Object)data).moonrise$setPalette( -+ ((ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T>)data.palette).moonrise$getRawPalette((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T>)(Object)data) -+ ); -+ } -+ } -+ -+ private T readPaletteSlow(final PalettedContainer.Data<T> data, final int paletteIdx) { -+ return data.palette.valueFor(paletteIdx); -+ } -+ -+ private T readPalette(final PalettedContainer.Data<T> data, final int paletteIdx) { -+ final T[] palette = ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T>)(Object)data).moonrise$getPalette(); -+ if (palette == null) { -+ return this.readPaletteSlow(data, paletteIdx); -+ } -+ -+ final T ret = palette[paletteIdx]; -+ if (ret == null) { -+ throw new IllegalArgumentException("Palette index out of bounds"); -+ } -+ return ret; -+ } -+ // Paper end - optimise palette reads -+ - // Paper start - Anti-Xray - Add preset values - @Deprecated @io.papermc.paper.annotation.DoNotUse public PalettedContainer(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Configuration<T> dataProvider, BitStorage storage, List<T> paletteEntries) { this(idList, paletteProvider, dataProvider, storage, paletteEntries, null, null); } - public PalettedContainer( -@@ -113,6 +140,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer - } - } - // Paper end -+ this.updateData(this.data); // Paper - optimise palette reads - } - - // Paper start - Anti-Xray - Add preset values -@@ -122,6 +150,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer - this.registry = idList; - this.strategy = paletteProvider; - this.data = data; -+ this.updateData(this.data); // Paper - optimise palette reads - } - - private PalettedContainer(PalettedContainer<T> container, T @org.jetbrains.annotations.Nullable [] presetValues) { // Paper - Anti-Xray - Add preset values -@@ -140,6 +169,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer - this.registry = idList; - this.data = this.createOrReuseData(null, 0); - this.data.palette.idFor(object); -+ this.updateData(this.data); // Paper - optimise palette reads - } - - private PalettedContainer.Data<T> createOrReuseData(@Nullable PalettedContainer.Data<T> previousData, int bits) { -@@ -166,6 +196,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer - data2.copyFrom(data.palette, data.storage); - this.data = data2; - this.addPresetValues(); -+ this.updateData(this.data); // Paper - optimise palette reads - return object == null ? -1 : data2.palette.idFor(object); - // Paper end - } -@@ -198,9 +229,12 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer - } - - private synchronized T getAndSet(int index, T value) { // Paper - synchronize -- int i = this.data.palette.idFor(value); -- int j = this.data.storage.getAndSet(index, i); -- return this.data.palette.valueFor(j); -+ // Paper start - optimise palette reads -+ final int paletteIdx = this.data.palette.idFor(value); -+ final PalettedContainer.Data<T> data = this.data; -+ final int prev = data.storage.getAndSet(index, paletteIdx); -+ return this.readPalette(data, prev); -+ // Paper end - optimise palette reads - } - - public void set(int x, int y, int z, T value) { -@@ -223,9 +257,11 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer - return this.get(this.strategy.getIndex(x, y, z)); - } - -- protected T get(int index) { -- PalettedContainer.Data<T> data = this.data; -- return data.palette.valueFor(data.storage.get(index)); -+ public T get(int index) { // Paper - public -+ // Paper start - optimise palette reads -+ final PalettedContainer.Data<T> data = this.data; -+ return this.readPalette(data, data.storage.get(index)); -+ // Paper end - optimise palette reads - } - - @Override -@@ -246,6 +282,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer - buf.readLongArray(data.storage.getRaw()); - this.data = data; - this.addPresetValues(); // Paper - Anti-Xray - Add preset values (inefficient, but this isn't used by the server) -+ this.updateData(this.data); // Paper - optimise palette reads - } finally { - this.release(); - } -@@ -394,7 +431,44 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer - void accept(T object, int count); - } - -- static record Data<T>(PalettedContainer.Configuration<T> configuration, BitStorage storage, Palette<T> palette) { -+ // Paper start - optimise palette reads -+ public static final class Data<T> implements ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> { -+ -+ private final PalettedContainer.Configuration<T> configuration; -+ private final BitStorage storage; -+ private final Palette<T> palette; -+ -+ private T[] moonrise$palette; -+ -+ public Data(final PalettedContainer.Configuration<T> configuration, final BitStorage storage, final Palette<T> palette) { -+ this.configuration = configuration; -+ this.storage = storage; -+ this.palette = palette; -+ } -+ -+ public PalettedContainer.Configuration<T> configuration() { -+ return this.configuration; -+ } -+ -+ public BitStorage storage() { -+ return this.storage; -+ } -+ -+ public Palette<T> palette() { -+ return this.palette; -+ } -+ -+ @Override -+ public final T[] moonrise$getPalette() { -+ return this.moonrise$palette; -+ } -+ -+ @Override -+ public final void moonrise$setPalette(final T[] palette) { -+ this.moonrise$palette = palette; -+ } -+ // Paper end - optimise palette reads -+ - public void copyFrom(Palette<T> palette, BitStorage storage) { - for (int i = 0; i < storage.getSize(); i++) { - T object = palette.valueFor(storage.get(i)); -diff --git a/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<T> implements Palette<T> { -+public class SingleValuePalette<T> implements Palette<T>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads - private final IdMap<T> registry; - @Nullable - private T value; - private final PaletteResize<T> resizeHandler; - -+ // Paper start - optimise palette reads -+ private T[] rawPalette; -+ -+ @Override -+ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> container) { -+ if (this.rawPalette != null) { -+ return this.rawPalette; -+ } -+ return this.rawPalette = (T[])new Object[] { this.value }; -+ } -+ // Paper end - optimise palette reads -+ - public SingleValuePalette(IdMap<T> idList, PaletteResize<T> listener, List<T> entries) { - this.registry = idList; - this.resizeHandler = listener; -@@ -33,6 +45,11 @@ public class SingleValuePalette<T> implements Palette<T> { - return this.resizeHandler.onResize(1, object); - } else { - this.value = object; -+ // Paper start - optimise palette reads -+ if (this.rawPalette != null) { -+ this.rawPalette[0] = object; -+ } -+ // Paper end - optimise palette reads - return 0; - } - } -@@ -58,6 +75,11 @@ public class SingleValuePalette<T> implements Palette<T> { - @Override - public void read(FriendlyByteBuf buf) { - this.value = this.registry.byIdOrThrow(buf.readVarInt()); -+ // Paper start - optimise palette reads -+ if (this.rawPalette != null) { -+ this.rawPalette[0] = this.value; -+ } -+ // Paper end - optimise palette reads - } - - @Override -diff --git a/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<ChunkStep> steps) { - .step(ChunkStatus.CARVERS, builder -> builder) - .step(ChunkStatus.FEATURES, builder -> builder) - .step(ChunkStatus.INITIALIZE_LIGHT, builder -> builder.setTask(ChunkStatusTasks::initializeLight)) -- .step(ChunkStatus.LIGHT, builder -> builder.addRequirement(ChunkStatus.INITIALIZE_LIGHT, 1).setTask(ChunkStatusTasks::light)) -+ .step(ChunkStatus.LIGHT, builder -> builder.setTask(ChunkStatusTasks::light)) // Paper - rewrite chunk system - starlight does not need neighbours - .step(ChunkStatus.SPAWN, builder -> builder) - .step(ChunkStatus.FULL, builder -> builder.setTask(ChunkStatusTasks::full)) - .build(); -diff --git a/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<Heightmap.Types> WORLDGEN_HEIGHTMAPS = EnumSet.of(Heightmap.Types.OCEAN_FLOOR_WG, Heightmap.Types.WORLD_SURFACE_WG); - public static final EnumSet<Heightmap.Types> FINAL_HEIGHTMAPS = EnumSet.of( -@@ -51,8 +51,68 @@ public class ChunkStatus { - return list; - } - -+ // Paper start - rewrite chunk system -+ private boolean isParallelCapable; -+ private boolean emptyLoadTask; -+ private int writeRadius; -+ private ChunkStatus nextStatus; -+ private java.util.concurrent.atomic.AtomicBoolean warnedAboutNoImmediateComplete; -+ -+ @Override -+ public final boolean moonrise$isParallelCapable() { -+ return this.isParallelCapable; -+ } -+ -+ @Override -+ public final void moonrise$setParallelCapable(final boolean value) { -+ this.isParallelCapable = value; -+ } -+ -+ @Override -+ public final int moonrise$getWriteRadius() { -+ return this.writeRadius; -+ } -+ -+ @Override -+ public final void moonrise$setWriteRadius(final int value) { -+ this.writeRadius = value; -+ } -+ -+ @Override -+ public final ChunkStatus moonrise$getNextStatus() { -+ return this.nextStatus; -+ } -+ -+ @Override -+ public final boolean moonrise$isEmptyLoadStatus() { -+ return this.emptyLoadTask; -+ } -+ -+ @Override -+ public void moonrise$setEmptyLoadStatus(final boolean value) { -+ this.emptyLoadTask = value; -+ } -+ -+ @Override -+ public final boolean moonrise$isEmptyGenStatus() { -+ return (Object)this == ChunkStatus.EMPTY; -+ } -+ -+ @Override -+ public final java.util.concurrent.atomic.AtomicBoolean moonrise$getWarnedAboutNoImmediateComplete() { -+ return this.warnedAboutNoImmediateComplete; -+ } -+ // Paper end - rewrite chunk system -+ - @VisibleForTesting - protected ChunkStatus(@Nullable ChunkStatus previous, EnumSet<Heightmap.Types> heightMapTypes, ChunkType chunkType) { -+ this.isParallelCapable = false; -+ this.writeRadius = -1; -+ this.nextStatus = (ChunkStatus)(Object)this; -+ if (previous != null) { -+ previous.nextStatus = (ChunkStatus)(Object)this; -+ } -+ this.warnedAboutNoImmediateComplete = new java.util.concurrent.atomic.AtomicBoolean(); - this.parent = previous == null ? this : previous; - this.chunkType = chunkType; - this.heightmapsAfter = heightMapTypes; -diff --git a/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<CompoundTag> entities) { // Paper - public -+ public static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities, ChunkPos pos) { // Paper - public // Paper - rewrite chunk system - add ChunkPos param - if (!entities.isEmpty()) { - // CraftBukkit start - these are spawned serialized (DefinedStructure) and we don't call an add event below at the moment due to ordering complexities - world.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(entities, world, EntitySpawnReason.LOAD).filter((entity) -> { -@@ -180,7 +180,7 @@ public class ChunkStatusTasks { - } - checkDupeUUID(world, entity); // Paper - duplicate uuid resolving - return !needsRemoval; -- })); -+ }), pos); // Paper - rewrite chunk system - // CraftBukkit end - } - -diff --git a/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<Optional<CompoundTag>> read(ChunkPos chunkPos) { -- return this.worker.loadAsync(chunkPos); -+ // Paper start - rewrite chunk system -+ try { -+ return CompletableFuture.completedFuture(Optional.ofNullable(this.storage.read(chunkPos))); -+ } catch (final Throwable throwable) { -+ return CompletableFuture.failedFuture(throwable); -+ } -+ // Paper end - rewrite chunk system - } - - public CompletableFuture<Void> write(ChunkPos chunkPos, Supplier<CompoundTag> nbtSupplier) { -@@ -185,29 +203,54 @@ public class ChunkStorage implements AutoCloseable { - }; - // Paper end - guard against possible chunk pos desync - this.handleLegacyStructureIndex(chunkPos); -- return this.worker.store(chunkPos, guardedPosCheck); // Paper - guard against possible chunk pos desync -+ // Paper start - rewrite chunk system -+ try { -+ this.storage.write(chunkPos, guardedPosCheck.get()); -+ return CompletableFuture.completedFuture(null); -+ } catch (final Throwable throwable) { -+ return CompletableFuture.failedFuture(throwable); -+ } -+ // Paper end - rewrite chunk system - } - - protected void handleLegacyStructureIndex(ChunkPos chunkPos) { - if (this.legacyStructureHandler != null) { -+ synchronized (this.legacyStructureHandler) { // Paper - rewrite chunk system - this.legacyStructureHandler.removeIndex(chunkPos.toLong()); -+ } // Paper - rewrite chunk system - } - - } - - public void flushWorker() { -- this.worker.synchronize(true).join(); -+ // Paper start - rewrite chunk system -+ try { -+ this.storage.flush(); -+ } catch (final IOException ex) { -+ LOGGER.error("Failed to flush chunk storage", ex); -+ } -+ // Paper end - rewrite chunk system - } - - public void close() throws IOException { -- this.worker.close(); -+ this.storage.close(); // Paper - rewrite chunk system - } - - public ChunkScanAccess chunkScanner() { -- return this.worker; -+ // Paper start - rewrite chunk system -+ // TODO ChunkMap implementation? -+ return (chunkPos, streamTagVisitor) -> { -+ try { -+ this.storage.scanChunk(chunkPos, streamTagVisitor); -+ return java.util.concurrent.CompletableFuture.completedFuture(null); -+ } catch (IOException e) { -+ throw new RuntimeException(e); -+ } -+ }; -+ // Paper end - rewrite chunk system - } - -- protected RegionStorageInfo storageInfo() { -- return this.worker.storageInfo(); -+ public RegionStorageInfo storageInfo() { // Paper - public -+ return this.storage.info(); // Paper - rewrite chunk system - } - } -diff --git a/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<Entity> { - } - } - -- private static ChunkPos readChunkPos(CompoundTag chunkNbt) { -+ public static ChunkPos readChunkPos(CompoundTag chunkNbt) { // Paper - public - int[] is = chunkNbt.getIntArray("Position"); - return new ChunkPos(is[0], is[1]); - } - -- private static void writeChunkPos(CompoundTag chunkNbt, ChunkPos pos) { -+ public static void writeChunkPos(CompoundTag chunkNbt, ChunkPos pos) { // Paper - public - chunkNbt.put("Position", new IntArrayTag(new int[]{pos.x, pos.z})); - } - -diff --git a/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<ChunkPos, IOWorker.PendingStore> pendingWrites = new LinkedHashMap<>(); - private final Long2ObjectLinkedOpenHashMap<CompletableFuture<BitSet>> regionCacheForBlender = new Long2ObjectLinkedOpenHashMap<>(); - private static final int REGION_CACHE_SIZE = 1024; -diff --git a/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<IOException> exceptionsuppressor = new ExceptionCollector<>(); -- ObjectIterator objectiterator = this.regionCache.values().iterator(); -- -- while (objectiterator.hasNext()) { -- RegionFile regionfile = (RegionFile) objectiterator.next(); -- -- try { -- regionfile.close(); -- } catch (IOException ioexception) { -- exceptionsuppressor.add(ioexception); -+ // Paper start - rewrite chunk system -+ synchronized (this) { -+ final ExceptionCollector<IOException> exceptionCollector = new ExceptionCollector<>(); -+ for (final RegionFile regionFile : this.regionCache.values()) { -+ try { -+ regionFile.close(); -+ } catch (final IOException ex) { -+ exceptionCollector.add(ex); -+ } - } -- } - -- exceptionsuppressor.throwIfPresent(); -+ exceptionCollector.throwIfPresent(); -+ } -+ // Paper end - rewrite chunk system - } - - public void flush() throws IOException { -- ObjectIterator objectiterator = this.regionCache.values().iterator(); -- -- while (objectiterator.hasNext()) { -- RegionFile regionfile = (RegionFile) objectiterator.next(); -+ // Paper start - rewrite chunk system -+ synchronized (this) { -+ final ExceptionCollector<IOException> exceptionCollector = new ExceptionCollector<>(); -+ for (final RegionFile regionFile : this.regionCache.values()) { -+ try { -+ regionFile.flush(); -+ } catch (final IOException ex) { -+ exceptionCollector.add(ex); -+ } -+ } - -- regionfile.flush(); -+ exceptionCollector.throwIfPresent(); - } -+ // Paper end - rewrite chunk system - - } - -diff --git a/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<R, P> implements AutoCloseable { -+public class SectionStorage<R, P> implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage { // Paper - rewrite chunk system - static final Logger LOGGER = LogUtils.getLogger(); - private static final String SECTIONS_TAG = "Sections"; -- private final SimpleRegionStorage simpleRegionStorage; -+ // Paper - rewrite chunk system - private final Long2ObjectMap<Optional<R>> storage = new Long2ObjectOpenHashMap<>(); - private final LongLinkedOpenHashSet dirtyChunks = new LongLinkedOpenHashSet(); - private final Codec<P> codec; -@@ -57,6 +57,18 @@ public class SectionStorage<R, P> implements AutoCloseable { - private final Long2ObjectMap<CompletableFuture<Optional<SectionStorage.PackedChunk<P>>>> pendingLoads = new Long2ObjectOpenHashMap<>(); - private final Object loadLock = new Object(); - -+ // Paper start - rewrite chunk system -+ private final RegionFileStorage regionStorage; -+ -+ @Override -+ public final RegionFileStorage moonrise$getRegionStorage() { -+ return this.regionStorage; -+ } -+ -+ @Override -+ public void moonrise$close() throws IOException {} -+ // Paper end - rewrite chunk system -+ - public SectionStorage( - SimpleRegionStorage storageAccess, - Codec<P> codec, -@@ -67,7 +79,7 @@ public class SectionStorage<R, P> implements AutoCloseable { - ChunkIOErrorReporter errorHandler, - LevelHeightAccessor world - ) { -- this.simpleRegionStorage = storageAccess; -+ // Paper - rewrite chunk system - this.codec = codec; - this.packer = serializer; - this.unpacker = deserializer; -@@ -75,6 +87,7 @@ public class SectionStorage<R, P> implements AutoCloseable { - this.registryAccess = registryManager; - this.errorReporter = errorHandler; - this.levelHeightAccessor = world; -+ this.regionStorage = storageAccess.worker.storage; // Paper - rewrite chunk system - } - - protected void tick(BooleanSupplier shouldKeepTicking) { -@@ -188,64 +201,15 @@ public class SectionStorage<R, P> implements AutoCloseable { - } - - private CompletableFuture<Optional<SectionStorage.PackedChunk<P>>> tryRead(ChunkPos chunkPos) { -- RegistryOps<Tag> registryOps = this.registryAccess.createSerializationContext(NbtOps.INSTANCE); -- return this.simpleRegionStorage -- .read(chunkPos) -- .thenApplyAsync( -- chunkNbt -> chunkNbt.map( -- nbt -> SectionStorage.PackedChunk.parse(this.codec, registryOps, nbt, this.simpleRegionStorage, this.levelHeightAccessor) -- ), -- Util.backgroundExecutor().forName("parseSection") -- ) -- .exceptionally(throwable -> { -- if (throwable instanceof 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<P> result) { -- if (result == null) { -- for (int i = this.levelHeightAccessor.getMinSectionY(); i <= this.levelHeightAccessor.getMaxSectionY(); i++) { -- this.storage.put(getKey(chunkPos, i), Optional.empty()); -- } -- } else { -- boolean bl = result.versionChanged(); -- -- for (int j = this.levelHeightAccessor.getMinSectionY(); j <= this.levelHeightAccessor.getMaxSectionY(); j++) { -- long l = getKey(chunkPos, j); -- Optional<R> optional = Optional.ofNullable(result.sectionsByY.get(j)).map(section -> this.unpacker.apply((P)section, () -> this.setDirty(l))); -- this.storage.put(l, optional); -- optional.ifPresent(object -> { -- this.onSectionLoad(l); -- if (bl) { -- this.setDirty(l); -- } -- }); -- } -- } -+ throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system - } - - private void writeChunk(ChunkPos pos) { -- RegistryOps<Tag> registryOps = this.registryAccess.createSerializationContext(NbtOps.INSTANCE); -- Dynamic<Tag> dynamic = this.writeChunk(pos, registryOps); -- Tag tag = dynamic.getValue(); -- if (tag instanceof CompoundTag) { -- this.simpleRegionStorage.write(pos, (CompoundTag)tag).exceptionally(throwable -> { -- this.errorReporter.reportChunkSaveFailure(throwable, this.simpleRegionStorage.storageInfo(), pos); -- return null; -- }); -- } else { -- LOGGER.error("Expected compound tag, got {}", tag); -- } -+ throw new IllegalStateException("Only chunk system can write state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system - } - - private <T> Dynamic<T> writeChunk(ChunkPos chunkPos, DynamicOps<T> ops) { -@@ -281,7 +245,7 @@ public class SectionStorage<R, P> implements AutoCloseable { - protected void onSectionLoad(long pos) { - } - -- protected void setDirty(long pos) { -+ public void setDirty(long pos) { // Paper - public - Optional<R> optional = this.storage.get(pos); - if (optional != null && !optional.isEmpty()) { - this.dirtyChunks.add(ChunkPos.asLong(SectionPos.x(pos), SectionPos.z(pos))); -@@ -302,7 +266,7 @@ public class SectionStorage<R, P> implements AutoCloseable { - - @Override - public void close() throws IOException { -- this.simpleRegionStorage.close(); -+ this.moonrise$close(); // Paper - rewrite chunk system - } - - static record PackedChunk<T>(Int2ObjectMap<T> sectionsByY, boolean versionChanged) { -diff --git a/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<Biome> biomeRegistry, ChunkPos chun - long j = nbt.getLong("InhabitedTime"); - ChunkStatus chunkstatus = ChunkStatus.byName(nbt.getString("Status")); - UpgradeData chunkconverter = nbt.contains("UpgradeData", 10) ? new UpgradeData(nbt.getCompound("UpgradeData"), world) : UpgradeData.EMPTY; -- boolean flag = nbt.getBoolean("isLightOn"); -+ boolean flag = chunkstatus.isOrAfter(ChunkStatus.LIGHT) && (nbt.get("isLightOn") != null && nbt.getInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_VERSION_TAG) == ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_LIGHT_VERSION); // Paper - starlight - DataResult dataresult; - Logger logger; - BlendingData.Packed blendingdata_d; -@@ -246,7 +246,17 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun - DataLayer nibblearray = nbttagcompound3.contains("BlockLight", 7) ? new DataLayer(nbttagcompound3.getByteArray("BlockLight")) : null; - DataLayer nibblearray1 = nbttagcompound3.contains("SkyLight", 7) ? new DataLayer(nbttagcompound3.getByteArray("SkyLight")) : null; - -- list4.add(new SerializableChunkData.SectionData(b0, chunksection, nibblearray, nibblearray1)); -+ // Paper start - starlight -+ SerializableChunkData.SectionData serializableChunkData = new SerializableChunkData.SectionData(b0, chunksection, nibblearray, nibblearray1); -+ if (sectionData.contains(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.BLOCKLIGHT_STATE_TAG, Tag.TAG_ANY_NUMERIC)) { -+ ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)serializableChunkData).starlight$setBlockLightState(sectionData.getInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.BLOCKLIGHT_STATE_TAG)); -+ } -+ -+ if (sectionData.contains(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.SKYLIGHT_STATE_TAG, Tag.TAG_ANY_NUMERIC)) { -+ ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)serializableChunkData).starlight$setSkyLightState(sectionData.getInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.SKYLIGHT_STATE_TAG)); -+ } -+ list4.add(serializableChunkData); -+ // Paper end - starlight - } - - // CraftBukkit - ChunkBukkitValues -@@ -254,6 +264,59 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun - } - } - -+ // Paper start - starlight -+ private ProtoChunk loadStarlightLightData(final ServerLevel world, final ProtoChunk ret) { -+ -+ final boolean hasSkyLight = world.dimensionType().hasSkyLight(); -+ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinLightSection(world); -+ -+ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles = ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(world); -+ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] skyNibbles = ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(world); -+ -+ if (!this.lightCorrect) { -+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)ret).starlight$setBlockNibbles(blockNibbles); -+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)ret).starlight$setSkyNibbles(skyNibbles); -+ return ret; -+ } -+ -+ try { -+ for (final SerializableChunkData.SectionData sectionData : this.sectionData) { -+ final int y = sectionData.y(); -+ final DataLayer blockLight = sectionData.blockLight(); -+ final DataLayer skyLight = sectionData.skyLight(); -+ -+ final int blockState = ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$getBlockLightState(); -+ final int skyState = ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$getSkyLightState(); -+ -+ if (blockState >= 0) { -+ if (blockLight != null) { -+ blockNibbles[y - minSection] = new ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray(ca.spottedleaf.moonrise.common.util.MixinWorkarounds.clone(blockLight.getData()), blockState); // clone for data safety -+ } else { -+ blockNibbles[y - minSection] = new ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray(null, blockState); -+ } -+ } -+ -+ if (skyState >= 0 && hasSkyLight) { -+ if (skyLight != null) { -+ skyNibbles[y - minSection] = new ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray(ca.spottedleaf.moonrise.common.util.MixinWorkarounds.clone(skyLight.getData()), skyState); // clone for data safety -+ } else { -+ skyNibbles[y - minSection] = new ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray(null, skyState); -+ } -+ } -+ } -+ -+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)ret).starlight$setBlockNibbles(blockNibbles); -+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)ret).starlight$setSkyNibbles(skyNibbles); -+ } catch (final Throwable thr) { -+ ret.setLightCorrect(false); -+ -+ LOGGER.error("Failed to parse light data for chunk " + ret.getPos() + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(world) + "'", thr); -+ } -+ -+ return ret; -+ } -+ // Paper end - starlight -+ - public ProtoChunk read(ServerLevel world, PoiManager poiStorage, RegionStorageInfo key, ChunkPos expectedPos) { - if (!Objects.equals(expectedPos, this.chunkPos)) { - SerializableChunkData.LOGGER.error("Chunk file at {} is in the wrong location; relocating. (Expected {}, got {})", new Object[]{expectedPos, expectedPos, this.chunkPos}); -@@ -275,7 +338,7 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun - - if (serializablechunkdata_b.chunkSection != null) { - achunksection[world.getSectionIndexFromSectionY(serializablechunkdata_b.y)] = serializablechunkdata_b.chunkSection; -- poiStorage.checkConsistencyWithBlocks(sectionposition, serializablechunkdata_b.chunkSection); -+ //poiStorage.checkConsistencyWithBlocks(sectionposition, serializablechunkdata_b.chunkSection); // Paper - rewrite chunk system - } - - boolean flag2 = serializablechunkdata_b.blockLight != null; -@@ -352,7 +415,7 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun - } - - if (chunktype == ChunkType.LEVELCHUNK) { -- return new ImposterProtoChunk((LevelChunk) object, false); -+ return this.loadStarlightLightData(world, new ImposterProtoChunk((LevelChunk) object, false)); // Paper - starlight - } else { - ProtoChunk protochunk1 = (ProtoChunk) object; - Iterator iterator2 = this.entities.iterator(); -@@ -382,7 +445,7 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun - protochunk1.setCarvingMask(new CarvingMask(this.carvingMask, ((ChunkAccess) object).getMinY())); - } - -- return protochunk1; -+ return this.loadStarlightLightData(world, protochunk1); // Paper - starlight - } - } - -@@ -405,24 +468,48 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun - throw new IllegalArgumentException("Chunk can't be serialized: " + String.valueOf(chunk)); - } else { - ChunkPos chunkcoordintpair = chunk.getPos(); -- List<SerializableChunkData.SectionData> list = new ArrayList(); -+ List<SerializableChunkData.SectionData> list = new ArrayList(); final List<SerializableChunkData.SectionData> sections = list; // Paper - starlight - OBFHELPER - LevelChunkSection[] achunksection = chunk.getSections(); - ThreadedLevelLightEngine lightenginethreaded = world.getChunkSource().getLightEngine(); - -- for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) { -- int j = chunk.getSectionIndexFromSectionY(i); -- boolean flag = j >= 0 && j < achunksection.length; -- DataLayer nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); -- DataLayer nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); -- DataLayer nibblearray2 = nibblearray != null && !nibblearray.isEmpty() ? nibblearray.copy() : null; -- DataLayer nibblearray3 = nibblearray1 != null && !nibblearray1.isEmpty() ? nibblearray1.copy() : null; -+ // Paper start - starlight -+ final int minLightSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinLightSection(world); -+ final int maxLightSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxLightSection(world); -+ final int minBlockSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(world); -+ -+ final LevelChunkSection[] chunkSections = chunk.getSections(); -+ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles = ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)chunk).starlight$getBlockNibbles(); -+ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] skyNibbles = ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)chunk).starlight$getSkyNibbles(); -+ -+ for (int lightSection = minLightSection; lightSection <= maxLightSection; ++lightSection) { -+ final int lightSectionIdx = lightSection - minLightSection; -+ final int blockSectionIdx = lightSection - minBlockSection; - -- if (flag || nibblearray2 != null || nibblearray3 != null) { -- LevelChunkSection chunksection = flag ? achunksection[j].copy() : null; -+ final LevelChunkSection chunkSection = (blockSectionIdx >= 0 && blockSectionIdx < chunkSections.length) ? chunkSections[blockSectionIdx].copy() : null; -+ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray.SaveState blockNibble = blockNibbles[lightSectionIdx].getSaveState(); -+ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray.SaveState skyNibble = skyNibbles[lightSectionIdx].getSaveState(); - -- list.add(new SerializableChunkData.SectionData(i, chunksection, nibblearray2, nibblearray3)); -+ if (chunkSection == null && blockNibble == null && skyNibble == null) { -+ continue; - } -+ -+ final SerializableChunkData.SectionData sectionData = new SerializableChunkData.SectionData( -+ lightSection, chunkSection, -+ blockNibble == null ? null : (blockNibble.data == null ? null : new DataLayer(blockNibble.data)), -+ skyNibble == null ? null : (skyNibble.data == null ? null : new DataLayer(skyNibble.data)) -+ ); -+ -+ if (blockNibble != null) { -+ ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$setBlockLightState(blockNibble.state); -+ } -+ -+ if (skyNibble != null) { -+ ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$setSkyLightState(skyNibble.state); -+ } -+ -+ sections.add(sectionData); - } -+ // Paper end - starlight - - List<CompoundTag> list1 = new ArrayList(chunk.getBlockEntitiesPos().size()); - Iterator iterator = chunk.getBlockEntitiesPos().iterator(); -@@ -521,8 +608,8 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun - Iterator iterator = this.sectionData.iterator(); - - while (iterator.hasNext()) { -- SerializableChunkData.SectionData serializablechunkdata_b = (SerializableChunkData.SectionData) iterator.next(); -- CompoundTag nbttagcompound1 = new CompoundTag(); -+ SerializableChunkData.SectionData serializablechunkdata_b = (SerializableChunkData.SectionData) iterator.next(); final SerializableChunkData.SectionData sectionData = serializablechunkdata_b; // Paper - starlight - OBFHELPER -+ CompoundTag nbttagcompound1 = new CompoundTag(); final CompoundTag sectionNBT = nbttagcompound1; // Paper - starlight - OBFHELPER - LevelChunkSection chunksection = serializablechunkdata_b.chunkSection; - - if (chunksection != null) { -@@ -538,6 +625,19 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun - nbttagcompound1.putByteArray("SkyLight", serializablechunkdata_b.skyLight.getData()); - } - -+ // Paper start - starlight -+ final int blockState = ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$getBlockLightState(); -+ final int skyState = ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$getSkyLightState(); -+ -+ if (blockState > 0) { -+ sectionNBT.putInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.BLOCKLIGHT_STATE_TAG, blockState); -+ } -+ -+ if (skyState > 0) { -+ sectionNBT.putInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.SKYLIGHT_STATE_TAG, skyState); -+ } -+ // Paper end - starlight -+ - if (!nbttagcompound1.isEmpty()) { - nbttagcompound1.putByte("Y", (byte) serializablechunkdata_b.y); - nbttaglist.add(nbttagcompound1); -@@ -577,6 +677,14 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun - nbttagcompound.put("ChunkBukkitValues", this.persistentDataContainer); - } - // CraftBukkit end -+ // Paper start - starlight -+ if (this.lightCorrect && !this.chunkStatus.isBefore(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) { -+ // clobber vanilla value to force vanilla to relight -+ nbttagcompound.putBoolean("isLightOn", false); -+ // store our light version -+ nbttagcompound.putInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_VERSION_TAG, ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_LIGHT_VERSION); -+ } -+ // Paper end - starlight - return nbttagcompound; - } - -@@ -763,7 +871,67 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun - return nbttaglist; - } - -- public static record SectionData(int y, @Nullable LevelChunkSection chunkSection, @Nullable DataLayer blockLight, @Nullable DataLayer skyLight) { -+ // Paper start - starlight - convert from record -+ public static final class SectionData implements ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData { // Paper - starlight - our diff -+ private final int y; -+ @javax.annotation.Nullable -+ private final net.minecraft.world.level.chunk.LevelChunkSection chunkSection; -+ @javax.annotation.Nullable -+ private final net.minecraft.world.level.chunk.DataLayer blockLight; -+ @javax.annotation.Nullable -+ private final net.minecraft.world.level.chunk.DataLayer skyLight; -+ -+ // Paper start - starlight - our diff -+ private int blockLightState = -1; -+ private int skyLightState = -1; -+ -+ @Override -+ public final int starlight$getBlockLightState() { -+ return this.blockLightState; -+ } -+ -+ @Override -+ public final void starlight$setBlockLightState(final int state) { -+ this.blockLightState = state; -+ } -+ -+ @Override -+ public final int starlight$getSkyLightState() { -+ return this.skyLightState; -+ } -+ -+ @Override -+ public final void starlight$setSkyLightState(final int state) { -+ this.skyLightState = state; -+ } -+ // Paper end - starlight - our diff -+ -+ public SectionData(int y, @javax.annotation.Nullable net.minecraft.world.level.chunk.LevelChunkSection chunkSection, @javax.annotation.Nullable net.minecraft.world.level.chunk.DataLayer blockLight, @javax.annotation.Nullable net.minecraft.world.level.chunk.DataLayer skyLight) { -+ this.y = y; -+ this.chunkSection = chunkSection; -+ this.blockLight = blockLight; -+ this.skyLight = skyLight; -+ } -+ -+ public int y() { -+ return y; -+ } -+ -+ @javax.annotation.Nullable -+ public net.minecraft.world.level.chunk.LevelChunkSection chunkSection() { -+ return chunkSection; -+ } -+ -+ @javax.annotation.Nullable -+ public net.minecraft.world.level.chunk.DataLayer blockLight() { -+ return blockLight; -+ } -+ -+ @javax.annotation.Nullable -+ public net.minecraft.world.level.chunk.DataLayer skyLight() { -+ return skyLight; -+ } -+ // Paper end - starlight - convert from record - - } - -diff --git a/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<Entity> active = new Int2ObjectLinkedOpenHashMap<>(); -- private Int2ObjectMap<Entity> passive = new Int2ObjectLinkedOpenHashMap<>(); -- @Nullable -- private Int2ObjectMap<Entity> iterated; -+ private final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<net.minecraft.world.entity.Entity> entities = new ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<>(); // Paper - rewrite chunk system - - private void ensureActiveIsNotIterated() { -- if (this.iterated == this.active) { -- this.passive.clear(); -- -- for (Entry<Entity> entry : Int2ObjectMaps.fastIterable(this.active)) { -- this.passive.put(entry.getIntKey(), entry.getValue()); -- } -- -- Int2ObjectMap<Entity> int2ObjectMap = this.active; -- this.active = this.passive; -- this.passive = int2ObjectMap; -- } -+ // Paper - rewrite chunk system - } - - public void add(Entity entity) { - this.ensureActiveIsNotIterated(); -- this.active.put(entity.getId(), entity); -+ this.entities.add(entity); // Paper - rewrite chunk system - } - - public void remove(Entity entity) { - this.ensureActiveIsNotIterated(); -- this.active.remove(entity.getId()); -+ this.entities.remove(entity); // Paper - rewrite chunk system - } - - public boolean contains(Entity entity) { -- return this.active.containsKey(entity.getId()); -+ return this.entities.contains(entity); // Paper - rewrite chunk system - } - - public void forEach(Consumer<Entity> action) { -- if (this.iterated != null) { -- throw new UnsupportedOperationException("Only one concurrent iteration supported"); -- } else { -- this.iterated = this.active; -- -- try { -- for (Entity entity : this.active.values()) { -- action.accept(entity); -- } -- } finally { -- this.iterated = null; -+ // Paper start - rewrite chunk system -+ // To ensure nothing weird happens with dimension travelling, do not iterate over new entries... -+ // (by dfl iterator() is configured to not iterate over new entries) -+ final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet.Iterator<Entity> iterator = this.entities.iterator(); -+ try { -+ while (iterator.hasNext()) { -+ action.accept(iterator.next()); - } -+ } finally { -+ iterator.finishedIterating(); - } -+ // Paper end - rewrite chunk system - } - } -diff --git a/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<Object2IntMap<Structure>> loadedChunks = new Long2ObjectOpenHashMap<>(); -- private final Map<Structure, Long2BooleanMap> featureChecks = new HashMap<>(); -+ // Paper start - rewrite chunk system -+ // make sure to purge entries from the maps to prevent memory leaks -+ private static final int CHUNK_TOTAL_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups -+ private static final int PER_FEATURE_CHECK_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups -+ private final ca.spottedleaf.moonrise.common.map.SynchronisedLong2ObjectMap<it.unimi.dsi.fastutil.objects.Object2IntMap<Structure>> loadedChunksSafe = new ca.spottedleaf.moonrise.common.map.SynchronisedLong2ObjectMap<>(CHUNK_TOTAL_LIMIT); -+ private final java.util.concurrent.ConcurrentHashMap<Structure, ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap> featureChecksSafe = new java.util.concurrent.ConcurrentHashMap<>(); -+ // Paper end - rewrite chunk system - - public StructureCheck( - ChunkScanAccess chunkIoWorker, -@@ -90,7 +95,7 @@ public class StructureCheck { - - public StructureCheckResult checkStart(ChunkPos pos, Structure type, StructurePlacement placement, boolean skipReferencedStructures) { - long l = pos.toLong(); -- Object2IntMap<Structure> object2IntMap = this.loadedChunks.get(l); -+ Object2IntMap<Structure> object2IntMap = this.loadedChunksSafe.get(l); // Paper - rewrite chunk system - if (object2IntMap != null) { - return this.checkStructureInfo(object2IntMap, type, skipReferencedStructures); - } else { -@@ -100,9 +105,11 @@ public class StructureCheck { - } else if (!placement.applyAdditionalChunkRestrictions(pos.x, pos.z, this.seed, this.getSaltOverride(type))) { // Paper - add missing structure seed configs - return StructureCheckResult.START_NOT_PRESENT; - } else { -- boolean bl = this.featureChecks -- .computeIfAbsent(type, structure2 -> new Long2BooleanOpenHashMap()) -- .computeIfAbsent(l, chunkPos -> this.canCreateStructure(pos, type)); -+ // Paper start - rewrite chunk system -+ boolean bl = this.featureChecksSafe -+ .computeIfAbsent(type, structure2 -> new ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap(PER_FEATURE_CHECK_LIMIT)) -+ .getOrCompute(l, chunkPos -> this.canCreateStructure(pos, type)); -+ // Paper end - rewrite chunk system - return !bl ? StructureCheckResult.START_NOT_PRESENT : StructureCheckResult.CHUNK_LOAD_NEEDED; - } - } -@@ -228,15 +235,25 @@ public class StructureCheck { - } - - private void storeFullResults(long pos, Object2IntMap<Structure> referencesByStructure) { -- this.loadedChunks.put(pos, deduplicateEmptyMap(referencesByStructure)); -- this.featureChecks.values().forEach(generationPossibilityByChunkPos -> generationPossibilityByChunkPos.remove(pos)); -+ // Paper start - rewrite chunk system -+ this.loadedChunksSafe.put(pos, deduplicateEmptyMap(referencesByStructure)); -+ // once we insert into loadedChunks, we don't really need to be very careful about removing everything -+ // from this map, as everything that checks this map uses loadedChunks first -+ // so, one way or another it's a race condition that doesn't matter -+ for (ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap value : this.featureChecksSafe.values()) { -+ value.remove(pos); -+ } -+ // Paper end - rewrite chunk system - } - - public void incrementReference(ChunkPos pos, Structure structure) { -- this.loadedChunks.compute(pos.toLong(), (posx, referencesByStructure) -> { -- if (referencesByStructure == null || referencesByStructure.isEmpty()) { -+ this.loadedChunksSafe.compute(pos.toLong(), (posx, referencesByStructure) -> { // Paper start - rewrite chunk system -+ if (referencesByStructure == null) { - referencesByStructure = new Object2IntOpenHashMap<>(); -+ } else { -+ referencesByStructure = referencesByStructure instanceof Object2IntOpenHashMap<Structure> fastClone ? fastClone.clone() : new Object2IntOpenHashMap<>(referencesByStructure); - } -+ // Paper end - rewrite chunk system - - referencesByStructure.computeInt(structure, (feature, references) -> references == null ? 1 : references + 1); - return referencesByStructure; -diff --git a/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<FluidState, VoxelShape> shapes = Maps.newIdentityHashMap(); - -+ // Paper start - fluid method optimisations -+ private FluidState sourceFalling; -+ private FluidState sourceNotFalling; -+ -+ private static final int TOTAL_FLOWING_STATES = FALLING.getPossibleValues().size() * LEVEL.getPossibleValues().size(); -+ private static final int MIN_LEVEL = LEVEL.getPossibleValues().stream().sorted().findFirst().get().intValue(); -+ -+ // index = (falling ? 1 : 0) + level*2 -+ private FluidState[] flowingLookUp; -+ private volatile boolean init; -+ -+ private static final int COLLISION_OCCLUSION_CACHE_SIZE = 2048; -+ private static final ThreadLocal<ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey[]> COLLISION_OCCLUSION_CACHE = ThreadLocal.withInitial(() -> new ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey[COLLISION_OCCLUSION_CACHE_SIZE]); -+ -+ -+ /** -+ * Due to init order, we need to use callbacks to initialise our state -+ */ -+ private void init() { -+ synchronized (this) { -+ if (this.init) { -+ return; -+ } -+ this.flowingLookUp = new FluidState[TOTAL_FLOWING_STATES]; -+ final FluidState defaultFlowState = this.getFlowing().defaultFluidState(); -+ for (int i = 0; i < TOTAL_FLOWING_STATES; ++i) { -+ final int falling = i & 1; -+ final int level = (i >>> 1) + MIN_LEVEL; -+ -+ this.flowingLookUp[i] = defaultFlowState.setValue(FALLING, falling == 1 ? Boolean.TRUE : Boolean.FALSE) -+ .setValue(LEVEL, Integer.valueOf(level)); -+ } -+ -+ final FluidState defaultFallState = this.getSource().defaultFluidState(); -+ this.sourceFalling = defaultFallState.setValue(FALLING, Boolean.TRUE); -+ this.sourceNotFalling = defaultFallState.setValue(FALLING, Boolean.FALSE); -+ -+ this.init = true; -+ } -+ } -+ // Paper end - fluid method optimisations -+ - public FlowingFluid() {} - - @Override -@@ -246,65 +288,70 @@ public abstract class FlowingFluid extends Fluid { - } - } - -- private static boolean canPassThroughWall(Direction face, BlockGetter world, BlockPos pos, BlockState state, BlockPos fromPos, BlockState fromState) { -- VoxelShape voxelshape = fromState.getCollisionShape(world, fromPos); -+ // Paper start - fluid method optimisations -+ private static boolean canPassThroughWall(final Direction direction, final BlockGetter level, -+ final BlockPos fromPos, final BlockState fromState, -+ final BlockPos toPos, final BlockState toState) { -+ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$emptyCollisionShape() & ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$emptyCollisionShape()) { -+ // don't even try to cache simple cases -+ return true; -+ } - -- if (voxelshape == Shapes.block()) { -+ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$occludesFullBlock() | ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$occludesFullBlock()) { -+ // don't even try to cache simple cases - return false; -- } else { -- VoxelShape voxelshape1 = state.getCollisionShape(world, pos); -- -- if (voxelshape1 == Shapes.block()) { -- return false; -- } else if (voxelshape1 == Shapes.empty() && voxelshape == Shapes.empty()) { -- return true; -- } else { -- Object2ByteLinkedOpenHashMap object2bytelinkedopenhashmap; -- -- if (!state.getBlock().hasDynamicShape() && !fromState.getBlock().hasDynamicShape()) { -- object2bytelinkedopenhashmap = (Object2ByteLinkedOpenHashMap) FlowingFluid.OCCLUSION_CACHE.get(); -- } else { -- object2bytelinkedopenhashmap = null; -- } -+ } - -- FlowingFluid.BlockStatePairKey fluidtypeflowing_a; -+ final ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey[] cache = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$hasCache() & ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$hasCache() ? -+ COLLISION_OCCLUSION_CACHE.get() : null; - -- if (object2bytelinkedopenhashmap != null) { -- fluidtypeflowing_a = new FlowingFluid.BlockStatePairKey(state, fromState, face); -- byte b0 = object2bytelinkedopenhashmap.getAndMoveToFirst(fluidtypeflowing_a); -+ final int keyIndex -+ = (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$uniqueId1() ^ ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$uniqueId2() ^ ((ca.spottedleaf.moonrise.patches.collisions.util.CollisionDirection)(Object)direction).moonrise$uniqueId()) -+ & (COLLISION_OCCLUSION_CACHE_SIZE - 1); - -- if (b0 != 127) { -- return b0 != 0; -- } -- } else { -- fluidtypeflowing_a = null; -- } -- -- boolean flag = !Shapes.mergedFaceOccludes(voxelshape1, voxelshape, face); -+ if (cache != null) { -+ final ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey cached = cache[keyIndex]; -+ if (cached != null && cached.first() == fromState && cached.second() == toState && cached.direction() == direction) { -+ return cached.result(); -+ } -+ } - -- if (object2bytelinkedopenhashmap != null) { -- if (object2bytelinkedopenhashmap.size() == 200) { -- object2bytelinkedopenhashmap.removeLastByte(); -- } -+ final VoxelShape shape1 = fromState.getCollisionShape(level, fromPos); -+ final VoxelShape shape2 = toState.getCollisionShape(level, toPos); - -- object2bytelinkedopenhashmap.putAndMoveToFirst(fluidtypeflowing_a, (byte) (flag ? 1 : 0)); -- } -+ final boolean result = !Shapes.mergedFaceOccludes(shape1, shape2, direction); - -- return flag; -- } -+ if (cache != null) { -+ // we can afford to replace in-use keys more often due to the excessive caching the collision patch does in mergedFaceOccludes -+ cache[keyIndex] = new ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey(fromState, toState, direction, result); - } -+ -+ return result; - } -+ // Paper end - fluid method optimisations - - public abstract Fluid getFlowing(); - - public FluidState getFlowing(int level, boolean falling) { -- return (FluidState) ((FluidState) this.getFlowing().defaultFluidState().setValue(FlowingFluid.LEVEL, level)).setValue(FlowingFluid.FALLING, falling); -+ // Paper start - fluid method optimisations -+ final int amount = level; -+ if (!this.init) { -+ this.init(); -+ } -+ final int index = (falling ? 1 : 0) | ((amount - MIN_LEVEL) << 1); -+ return this.flowingLookUp[index]; -+ // Paper end - fluid method optimisations - } - - public abstract Fluid getSource(); - - public FluidState getSource(boolean falling) { -- return (FluidState) this.getSource().defaultFluidState().setValue(FlowingFluid.FALLING, falling); -+ // Paper start - fluid method optimisations -+ if (!this.init) { -+ this.init(); -+ } -+ return falling ? this.sourceFalling : this.sourceNotFalling; -+ // Paper end - fluid method optimisations - } - - protected abstract boolean canConvertToSource(ServerLevel world); -diff --git a/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<Fluid, FluidState> { -+public final class FluidState extends StateHolder<Fluid, FluidState> implements ca.spottedleaf.moonrise.patches.fluid.FluidFluidState { // Paper - fluid method optimisations - public static final Codec<FluidState> CODEC = codec(BuiltInRegistries.FLUID.byNameCodec(), Fluid::defaultFluidState).stable(); - public static final int AMOUNT_MAX = 9; - public static final int AMOUNT_FULL = 8; - protected final boolean isEmpty; // Paper - Perf: moved from isEmpty() - -+ // Paper start - fluid method optimisations -+ private int amount; -+ //private boolean isEmpty; -+ private boolean isSource; -+ private float ownHeight; -+ private boolean isRandomlyTicking; -+ private BlockState legacyBlock; -+ -+ @Override -+ public final void moonrise$initCaches() { -+ this.amount = this.getType().getAmount((FluidState)(Object)this); -+ //this.isEmpty = this.getType().isEmpty(); -+ this.isSource = this.getType().isSource((FluidState)(Object)this); -+ this.ownHeight = this.getType().getOwnHeight((FluidState)(Object)this); -+ this.isRandomlyTicking = this.getType().isRandomlyTicking(); -+ } -+ // Paper end - fluid method optimisations -+ - public FluidState(Fluid fluid, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<FluidState> codec) { - super(fluid, propertyMap, codec); - this.isEmpty = fluid.isEmpty(); // Paper - Perf: moved from isEmpty() -@@ -38,11 +56,11 @@ public final class FluidState extends StateHolder<Fluid, FluidState> { - } - - public boolean isSource() { -- return this.getType().isSource(this); -+ return this.isSource; // Paper - fluid method optimisations - } - - public boolean isSourceOfType(Fluid fluid) { -- return this.owner == fluid && this.owner.isSource(this); -+ return this.isSource && this.owner == fluid; // Paper - fluid method optimisations - } - - public boolean isEmpty() { -@@ -54,11 +72,11 @@ public final class FluidState extends StateHolder<Fluid, FluidState> { - } - - public float getOwnHeight() { -- return this.getType().getOwnHeight(this); -+ return this.ownHeight; // Paper - fluid method optimisations - } - - public int getAmount() { -- return this.getType().getAmount(this); -+ return this.amount; // Paper - fluid method optimisations - } - - public boolean shouldRenderBackwardUpFace(BlockGetter world, BlockPos pos) { -@@ -84,7 +102,7 @@ public final class FluidState extends StateHolder<Fluid, FluidState> { - } - - public boolean isRandomlyTicking() { -- return this.getType().isRandomlyTicking(); -+ return this.isRandomlyTicking; // Paper - fluid method optimisations - } - - public void randomTick(ServerLevel world, BlockPos pos, RandomSource random) { -@@ -96,7 +114,12 @@ public final class FluidState extends StateHolder<Fluid, FluidState> { - } - - public BlockState createLegacyBlock() { -- return this.getType().createLegacyBlock(this); -+ // Paper start - fluid method optimisations -+ if (this.legacyBlock != null) { -+ return this.legacyBlock; -+ } -+ return this.legacyBlock = this.getType().createLegacyBlock((FluidState)(Object)this); -+ // Paper end - fluid method optimisations - } - - @Nullable -diff --git a/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<AABB> toAabbsUncached() { -+ final List<AABB> ret; -+ if (this.singleAABBRepresentation != null) { -+ ret = new java.util.ArrayList<>(1); -+ ret.add(this.singleAABBRepresentation); -+ } else { -+ ret = new java.util.ArrayList<>(); -+ final double[] coordsX = this.rootCoordinatesX; -+ final double[] coordsY = this.rootCoordinatesY; -+ final double[] coordsZ = this.rootCoordinatesZ; -+ -+ final double offX = this.offsetX; -+ final double offY = this.offsetY; -+ final double offZ = this.offsetZ; -+ -+ this.shape.forAllBoxes((final int minX, final int minY, final int minZ, -+ final int maxX, final int maxY, final int maxZ) -> { -+ ret.add(new AABB( -+ coordsX[minX] + offX, -+ coordsY[minY] + offY, -+ coordsZ[minZ] + offZ, -+ -+ -+ coordsX[maxX] + offX, -+ coordsY[maxY] + offY, -+ coordsZ[maxZ] + offZ -+ )); -+ }, true); -+ } -+ -+ // cache result -+ this.cachedToAABBs = new ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs(ret, false, 0.0, 0.0, 0.0); -+ -+ return ret; -+ } -+ -+ private boolean computeFullBlock() { -+ Boolean ret; -+ if (this.isEmpty) { -+ ret = Boolean.FALSE; -+ } else if ((VoxelShape)(Object)this == Shapes.block()) { -+ ret = Boolean.TRUE; -+ } else { -+ final AABB singleAABB = this.singleAABBRepresentation; -+ if (singleAABB == null) { -+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeData = this.cachedShapeData; -+ final int sMinX = shapeData.minFullX(); -+ final int sMinY = shapeData.minFullY(); -+ final int sMinZ = shapeData.minFullZ(); -+ -+ final int sMaxX = shapeData.maxFullX(); -+ final int sMaxY = shapeData.maxFullY(); -+ final int sMaxZ = shapeData.maxFullZ(); -+ -+ if (Math.abs(this.rootCoordinatesX[sMinX] + this.offsetX) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(this.rootCoordinatesY[sMinY] + this.offsetY) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(this.rootCoordinatesZ[sMinZ] + this.offsetZ) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && -+ -+ Math.abs(1.0 - (this.rootCoordinatesX[sMaxX] + this.offsetX)) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(1.0 - (this.rootCoordinatesY[sMaxY] + this.offsetY)) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(1.0 - (this.rootCoordinatesZ[sMaxZ] + this.offsetZ)) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) { -+ -+ // index = z + y*sizeZ + x*(sizeZ*sizeY) -+ -+ final int sizeY = shapeData.sizeY(); -+ final int sizeZ = shapeData.sizeZ(); -+ -+ final long[] bitset = shapeData.voxelSet(); -+ -+ ret = Boolean.TRUE; -+ -+ check_full: -+ for (int x = sMinX; x < sMaxX; ++x) { -+ for (int y = sMinY; y < sMaxY; ++y) { -+ final int baseIndex = y*sizeZ + x*(sizeZ*sizeY); -+ if (!ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.isRangeSet(bitset, baseIndex + sMinZ, baseIndex + sMaxZ)) { -+ ret = Boolean.FALSE; -+ break check_full; -+ } -+ } -+ } -+ } else { -+ ret = Boolean.FALSE; -+ } -+ } else { -+ ret = Boolean.valueOf( -+ Math.abs(singleAABB.minX) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(singleAABB.minY) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(singleAABB.minZ) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && -+ -+ Math.abs(1.0 - singleAABB.maxX) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(1.0 - singleAABB.maxY) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(1.0 - singleAABB.maxZ) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON -+ ); -+ } -+ } -+ -+ this.isFullBlock = ret; -+ -+ return ret.booleanValue(); -+ } -+ -+ @Override -+ public final boolean moonrise$isFullBlock() { -+ final Boolean ret = this.isFullBlock; -+ -+ if (ret != null) { -+ return ret.booleanValue(); -+ } -+ -+ return this.computeFullBlock(); -+ } -+ -+ private static BlockHitResult clip(final AABB aabb, final Vec3 from, final Vec3 to, final BlockPos offset) { -+ final double[] minDistanceArr = new double[] { 1.0 }; -+ final double diffX = to.x - from.x; -+ final double diffY = to.y - from.y; -+ final double diffZ = to.z - from.z; -+ -+ final Direction direction = AABB.getDirection(aabb.move(offset), from, minDistanceArr, null, diffX, diffY, diffZ); -+ -+ if (direction == null) { -+ return null; -+ } -+ -+ final double minDistance = minDistanceArr[0]; -+ return new BlockHitResult(from.add(minDistance * diffX, minDistance * diffY, minDistance * diffZ), direction, offset, false); -+ } -+ -+ private VoxelShape calculateFaceDirect(final Direction direction, final Direction.Axis axis, final double[] coords, final double offset) { -+ if (coords.length == 2 && -+ DoubleMath.fuzzyEquals(coords[0] + offset, 0.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) && -+ DoubleMath.fuzzyEquals(coords[1] + offset, 1.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) { -+ return (VoxelShape)(Object)this; -+ } -+ -+ final boolean positiveDir = direction.getAxisDirection() == Direction.AxisDirection.POSITIVE; -+ -+ // see findIndex -+ final int index = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor( -+ coords, 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<AABB> aabbs = this.toAabbs(); -+ -+ if (aabbs.isEmpty()) { -+ // We are a SliceShape, which does not properly fill isEmpty for every case -+ return Shapes.empty(); -+ } -+ -+ if (aabbs.size() == 1) { -+ final AABB singleAABB = aabbs.get(0); -+ final VoxelShape ret = Shapes.create(singleAABB); -+ -+ // forward AABB cache -+ if (((VoxelShape)(Object)ret).cachedToAABBs == null) { -+ ((VoxelShape)(Object)ret).cachedToAABBs = this.cachedToAABBs; -+ } -+ -+ return ret; -+ } else { -+ // reduce complexity of joins by splitting the merges (old complexity: n^2, new: nlogn) -+ -+ // set up flat array so that this merge is done in-place -+ final VoxelShape[] tmp = new VoxelShape[aabbs.size()]; -+ -+ // initialise as unmerged -+ for (int i = 0, len = aabbs.size(); i < len; ++i) { -+ tmp[i] = Shapes.create(aabbs.get(i)); -+ } -+ -+ int size = aabbs.size(); -+ while (size > 1) { -+ int newSize = 0; -+ for (int i = 0; i < size; i += 2) { -+ final int next = i + 1; -+ if (next >= size) { -+ // nothing to merge with, so leave it for next iteration -+ tmp[newSize++] = tmp[i]; -+ break; -+ } else { -+ // merge with adjacent -+ final VoxelShape first = tmp[i]; -+ final VoxelShape second = tmp[next]; -+ -+ tmp[newSize++] = Shapes.joinUnoptimized(first, second, BooleanOp.OR); -+ } -+ } -+ size = newSize; -+ } -+ -+ final VoxelShape ret = tmp[0]; -+ -+ // forward AABB cache -+ if (((VoxelShape)(Object)ret).cachedToAABBs == null) { -+ ((VoxelShape)(Object)ret).cachedToAABBs = this.cachedToAABBs; -+ } -+ -+ return ret; -+ } -+ // Paper end - optimise collisions - } - - public void forAllEdges(Shapes.DoubleLineConsumer consumer) { -@@ -131,9 +688,24 @@ public abstract class VoxelShape { - } - - public List<AABB> toAabbs() { -- List<AABB> list = Lists.newArrayList(); -- this.forAllBoxes((x1, y1, z1, x2, y2, z2) -> list.add(new AABB(x1, y1, z1, x2, y2, z2))); -- return list; -+ // Paper start - optimise collisions -+ ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cachedToAABBs = this.cachedToAABBs; -+ if (cachedToAABBs != null) { -+ if (!cachedToAABBs.isOffset()) { -+ return cachedToAABBs.aabbs(); -+ } -+ -+ // all we need to do is offset the cache -+ cachedToAABBs = cachedToAABBs.removeOffset(); -+ // update cache -+ this.cachedToAABBs = cachedToAABBs; -+ -+ return cachedToAABBs.aabbs(); -+ } -+ -+ // make new cache -+ return this.toAabbsUncached(); -+ // Paper end - optimise collisions - } - - public double min(Direction.Axis axis, double from, double to) { -@@ -155,46 +727,92 @@ public abstract class VoxelShape { - } - - protected int findIndex(Direction.Axis axis, double coord) { -- return Mth.binarySearch(0, this.shape.getSize(axis) + 1, i -> coord < this.get(axis, i)) - 1; -+ // Paper start - optimise collisions -+ final double value = coord; -+ switch (axis) { -+ case X: { -+ final double[] values = this.rootCoordinatesX; -+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor( -+ values, 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<Vec3> closestPointTo(Vec3 target) { -- if (this.isEmpty()) { -+ // Paper start - optimise collisions -+ public Optional<Vec3> closestPointTo(Vec3 point) { -+ if (this.isEmpty) { - return Optional.empty(); -- } else { -- Vec3[] vec3s = new Vec3[1]; -- this.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { -- double d = Mth.clamp(target.x(), minX, maxX); -- double e = Mth.clamp(target.y(), minY, maxY); -- double f = Mth.clamp(target.z(), minZ, maxZ); -- if (vec3s[0] == null || target.distanceToSqr(d, e, f) < target.distanceToSqr(vec3s[0])) { -- vec3s[0] = new Vec3(d, e, f); -- } -- }); -- return Optional.of(vec3s[0]); - } -+ -+ Vec3 ret = null; -+ double retDistance = Double.MAX_VALUE; -+ -+ final List<AABB> aabbs = this.toAabbs(); -+ for (int i = 0, len = aabbs.size(); i < len; ++i) { -+ final AABB aabb = aabbs.get(i); -+ final double x = Mth.clamp(point.x, aabb.minX, aabb.maxX); -+ final double y = Mth.clamp(point.y, aabb.minY, aabb.maxY); -+ final double z = Mth.clamp(point.z, aabb.minZ, aabb.maxZ); -+ -+ double dist = point.distanceToSqr(x, y, z); -+ if (dist < retDistance) { -+ ret = new Vec3(x, y, z); -+ retDistance = dist; -+ } -+ } -+ -+ return Optional.ofNullable(ret); -+ // Paper end - optimise collisions - } - - public VoxelShape getFaceShape(Direction facing) { -@@ -216,20 +834,24 @@ public abstract class VoxelShape { - } - } - -- private VoxelShape calculateFace(Direction facing) { -- Direction.Axis axis = facing.getAxis(); -- if (this.isCubeLikeAlong(axis)) { -- return this; -- } else { -- Direction.AxisDirection axisDirection = facing.getAxisDirection(); -- int i = this.findIndex(axis, axisDirection == Direction.AxisDirection.POSITIVE ? 0.9999999 : 1.0E-7); -- SliceShape sliceShape = new SliceShape(this, axis, i); -- if (sliceShape.isEmpty()) { -- return Shapes.empty(); -- } else { -- return (VoxelShape)(sliceShape.isCubeLike() ? Shapes.block() : sliceShape); -+ private VoxelShape calculateFace(Direction direction) { -+ // Paper start - optimise collisions -+ final Direction.Axis axis = direction.getAxis(); -+ switch (axis) { -+ case X: { -+ return this.calculateFaceDirect(direction, axis, this.rootCoordinatesX, this.offsetX); -+ } -+ case Y: { -+ return this.calculateFaceDirect(direction, axis, this.rootCoordinatesY, this.offsetY); -+ } -+ case Z: { -+ return this.calculateFaceDirect(direction, axis, this.rootCoordinatesZ, this.offsetZ); -+ } -+ default: { -+ throw new IllegalStateException("Unknown axis: " + axis); - } - } -+ // Paper end - optimise collisions - } - - protected boolean isCubeLike() { -@@ -249,9 +871,30 @@ public abstract class VoxelShape { - && DoubleMath.fuzzyEquals(doubleList.getDouble(1), 1.0, 1.0E-7); - } - -- public double collide(Direction.Axis axis, AABB box, double maxDist) { -- return this.collideX(AxisCycle.between(axis, Direction.Axis.X), box, maxDist); -+ // Paper start - optimise collisions -+ public double collide(final Direction.Axis axis, final AABB source, final double source_move) { -+ if (this.isEmpty) { -+ return source_move; -+ } -+ if (Math.abs(source_move) < ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) { -+ return 0.0; -+ } -+ switch (axis) { -+ case X: { -+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.collideX((VoxelShape) (Object) this, source, source_move); -+ } -+ case Y: { -+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.collideY((VoxelShape) (Object) this, source, source_move); -+ } -+ case Z: { -+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.collideZ((VoxelShape) (Object) this, source, source_move); -+ } -+ default: { -+ throw new RuntimeException("Unknown axis: " + axis); -+ } -+ } - } -+ // Paper end - optimise collisions - - protected double collideX(AxisCycle axisCycle, AABB box, double maxDist) { - if (this.isEmpty()) { -diff --git a/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<T> implements SerializableTickContainer<T>, TickContainerAccess<T> { -+public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickContainerAccess<T>, ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks { // Paper - rewrite chunk system - private final Queue<ScheduledTick<T>> tickQueue = new PriorityQueue<>(ScheduledTick.DRAIN_ORDER); - @Nullable - private List<SavedTick<T>> pendingTicks; -@@ -25,6 +25,30 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon - @Nullable - private BiConsumer<LevelChunkTicks<T>, ScheduledTick<T>> onTickAdded; - -+ // Paper start - rewrite chunk system -+ /* -+ * Since ticks are saved using relative delays, we need to consider the entire tick list dirty when there are scheduled ticks -+ * and the last saved tick is not equal to the current tick -+ */ -+ /* -+ * In general, it would be nice to be able to "re-pack" ticks once the chunk becomes non-ticking again, but that is a -+ * bit out of scope for the chunk system -+ */ -+ -+ private boolean dirty; -+ private long lastSaved = Long.MIN_VALUE; -+ -+ @Override -+ public final boolean moonrise$isDirty(final long tick) { -+ return this.dirty || (!this.tickQueue.isEmpty() && tick != this.lastSaved); -+ } -+ -+ @Override -+ public final void moonrise$clearDirty() { -+ this.dirty = false; -+ } -+ // Paper end - rewrite chunk system -+ - public LevelChunkTicks() { - } - -@@ -49,7 +73,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon - public ScheduledTick<T> poll() { - ScheduledTick<T> scheduledTick = this.tickQueue.poll(); - if (scheduledTick != null) { -- this.ticksPerPosition.remove(scheduledTick); -+ this.ticksPerPosition.remove(scheduledTick); this.dirty = true; // Paper - rewrite chunk system - } - - return scheduledTick; -@@ -58,7 +82,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon - @Override - public void schedule(ScheduledTick<T> orderedTick) { - if (this.ticksPerPosition.add(orderedTick)) { -- this.scheduleUnchecked(orderedTick); -+ this.scheduleUnchecked(orderedTick); this.dirty = true; // Paper - rewrite chunk system - } - } - -@@ -80,7 +104,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon - while (iterator.hasNext()) { - ScheduledTick<T> scheduledTick = iterator.next(); - if (predicate.test(scheduledTick)) { -- iterator.remove(); -+ iterator.remove(); this.dirty = true; // Paper - rewrite chunk system - this.ticksPerPosition.remove(scheduledTick); - } - } -@@ -110,6 +134,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon - } - - public ListTag save(long time, Function<T, String> typeToNameFunction) { -+ this.lastSaved = time; // Paper - rewrite chunk system - ListTag listTag = new ListTag(); - - for (SavedTick<T> savedTick : this.pack(time)) { -@@ -121,6 +146,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon - - public void unpack(long time) { - if (this.pendingTicks != null) { -+ this.lastSaved = time; // Paper - rewrite chunk system - int i = -this.pendingTicks.size(); - - for (SavedTick<T> savedTick : this.pendingTicks) { -diff --git a/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<net.minecraft.world.entity.Entity> entityManager = this.getCraftWorld().getHandle().entityManager; -- long pair = ChunkPos.asLong(this.x, this.z); -- -- if (entityManager.areEntitiesLoaded(pair)) { -- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream() -- .map(net.minecraft.world.entity.Entity::getBukkitEntity) -- .filter(Objects::nonNull).toArray(Entity[]::new); -- } -- -- entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading -- -- // SPIGOT-6772: Use entity mailbox and re-schedule entities if they get unloaded -- ConsecutiveExecutor mailbox = ((EntityStorage) entityManager.permanentStorage).entityDeserializerQueue; -- BooleanSupplier supplier = () -> { -- // only execute inbox if our entities are not present -- if (entityManager.areEntitiesLoaded(pair)) { -- return true; -- } -- -- if (!entityManager.isPending(pair)) { -- // Our entities got unloaded, this should normally not happen. -- entityManager.ensureChunkQueuedForLoad(pair); // Re-start entity loading -- } -- -- // tick loading inbox, which loads the created entities to the world -- // (if present) -- entityManager.tick(); -- // check if our entities are loaded -- return entityManager.areEntitiesLoaded(pair); -- }; -- -- // now we wait until the entities are loaded, -- // the converting from NBT to entity object is done on the main Thread which is why we wait -- while (!supplier.getAsBoolean()) { -- if (mailbox.size() != 0) { -- mailbox.run(); -- } else { -- Thread.yield(); -- LockSupport.parkNanos("waiting for entity loading", 100000L); -- } -- } -- -- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream() -- .map(net.minecraft.world.entity.Entity::getBukkitEntity) -- .filter(Objects::nonNull).toArray(Entity[]::new); -+ return this.getCraftWorld().getHandle().getChunkEntities(this.x, this.z); // Paper - rewrite chunk system - } - - @Override -diff --git a/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<ServerPlayer> 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<Plugin> getPluginChunkTickets(int x, int z) { - DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; -- SortedArraySet<Ticket<?>> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z)); -- -- if (tickets == null) { -- return Collections.emptyList(); -- } - -- ImmutableList.Builder<Plugin> ret = ImmutableList.builder(); -- for (Ticket<?> ticket : tickets) { -- if (ticket.getType() == TicketType.PLUGIN_TICKET) { -- ret.add((Plugin) ticket.key); -- } -- } -- -- return ret.build(); -+ return chunkDistanceManager.moonrise$getChunkHolderManager().getPluginChunkTickets(x, z); // Paper - rewrite chunk system - } - - @Override -@@ -639,7 +629,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { - Map<Plugin, ImmutableList.Builder<Chunk>> ret = new HashMap<>(); - DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; - -- for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) { -+ for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.moonrise$getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { // Paper - rewrite chunk system - long chunkKey = chunkTickets.getLongKey(); - SortedArraySet<Ticket<?>> tickets = chunkTickets.getValue(); - -@@ -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<net.minecraft.world.entity.Entity> moonrise$getHardCollidingEntities(final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB box, final java.util.function.Predicate<? super net.minecraft.world.entity.Entity> predicate) { -+ return this.handle.moonrise$getHardCollidingEntities(entity, box, predicate); -+ } -+ // Paper end - rewrite chunk system - // Paper end - } - -diff --git a/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<ChunkHolder> list = this.visibleChunkMap - .values() -+ List<ChunkHolder> list = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level) // Paper - moonrise ++ List<ChunkHolder> 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<ChunkHolder> 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<ChunkHolder> 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<ChunkAccess> onComplete) { - if (gen) { - scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); - return; - } - scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { - if (chunk == null) { - if (onComplete != null) { - onComplete.accept(null); - } - } else { - if (chunk.getPersistedStatus().isOrAfter(toStatus)) { - scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); - } else { - if (onComplete != null) { - onComplete.accept(null); - } - } - } - }); - } - - static final net.minecraft.server.level.TicketType<Long> CHUNK_LOAD = net.minecraft.server.level.TicketType.create("chunk_load", Long::compareTo); - - private static long chunkLoadCounter = 0L; - public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, - final boolean addTicket, final Priority priority, final Consumer<ChunkAccess> onComplete) { - if (!org.bukkit.Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) { - scheduleChunkTask(level, chunkX, chunkZ, () -> { - scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); - }, priority); - return; - } - - final int minLevel = 33 + getDistance(toStatus); - final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; - final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ); - - if (addTicket) { - level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); - } - level.chunkSource.runDistanceManagerUpdates(); - - final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> { - try { - if (onComplete != null) { - onComplete.accept(chunk); - } - } catch (final Throwable thr) { - LOGGER.error("Exception handling chunk load callback", thr); - com.destroystokyo.paper.util.SneakyThrow.sneaky(thr); - } finally { - if (addTicket) { - level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); - level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); - } - } - }; - - final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); - - if (holder == null || holder.getTicketLevel() > minLevel) { - loadCallback.accept(null); - return; - } - - final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap); - - if (loadFuture.isDone()) { - loadCallback.accept(loadFuture.join().orElse(null)); - return; - } - - loadFuture.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess> result, final Throwable thr) -> { - if (thr != null) { - loadCallback.accept(null); - return; - } - loadCallback.accept(result.orElse(null)); - }, (final Runnable r) -> { - scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST); - }); - } - - public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, - final FullChunkStatus toStatus, final boolean addTicket, - final Priority priority, final Consumer<LevelChunk> onComplete) { - // This method goes unused until the chunk system rewrite - if (toStatus == FullChunkStatus.INACCESSIBLE) { - throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); - } - - if (!org.bukkit.Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) { - scheduleChunkTask(level, chunkX, chunkZ, () -> { - scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); - }, priority); - return; - } - - final int minLevel = 33 - (toStatus.ordinal() - 1); - final int radius = toStatus.ordinal() - 1; - final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; - final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ); - - if (addTicket) { - level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); - } - level.chunkSource.runDistanceManagerUpdates(); - - final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> { - try { - if (onComplete != null) { - onComplete.accept(chunk); - } - } catch (final Throwable thr) { - LOGGER.error("Exception handling chunk load callback", thr); - com.destroystokyo.paper.util.SneakyThrow.sneaky(thr); - } finally { - if (addTicket) { - level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); - level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); - } - } - }; - - final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); - - if (holder == null || holder.getTicketLevel() > minLevel) { - loadCallback.accept(null); - return; - } - - final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk>> tickingState; - switch (toStatus) { - case FULL: { - tickingState = holder.getFullChunkFuture(); - break; - } - case BLOCK_TICKING: { - tickingState = holder.getTickingChunkFuture(); - break; - } - case ENTITY_TICKING: { - tickingState = holder.getEntityTickingChunkFuture(); - break; - } - default: { - throw new IllegalStateException("Cannot reach here"); - } - } - - if (tickingState.isDone()) { - loadCallback.accept(tickingState.join().orElse(null)); - return; - } - - tickingState.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk> result, final Throwable thr) -> { - if (thr != null) { - loadCallback.accept(null); - return; - } - loadCallback.accept(result.orElse(null)); - }, (final Runnable r) -> { - scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST); - }); - } - - public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) { - return new java.util.ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values()); - } - - public static List<ChunkHolder> 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<ChunkAccess> onComplete); + + public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, + final boolean addTicket, final Priority priority, final Consumer<ChunkAccess> onComplete); + + public void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, + final FullChunkStatus toStatus, final boolean addTicket, + final Priority priority, final Consumer<LevelChunk> onComplete); + + public List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level); + + public List<ChunkHolder> 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<ChunkAccess> 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<ChunkHolder> chunks = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.world); // Paper + List<ChunkHolder> 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<? super Chunk> 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 |