From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Fri, 14 Jun 2024 11:57:26 -0700 Subject: [PATCH] Moonrise optimisation patches Currently includes: - Starlight + Chunk System - Entity tracker optimisations - Collision optimisations - Random block ticking optimisations - Chunk tick iteration optimisations - Bitstorage optimisations - Block/Biome Palette read optimisations - StateHolder (BlockState/FluidState) property access optimisations - Basic Fluid property read optimisations - Entity/Level random replacement See https://github.com/Tuinity/Moonrise diff --git a/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java new file mode 100644 index 0000000000000000000000000000000000000000..1b8193587814225c2ef2c5d9e667436eb50ff6c5 --- /dev/null +++ b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java @@ -0,0 +1,273 @@ +package ca.spottedleaf.moonrise.common.misc; + +import ca.spottedleaf.moonrise.common.PlatformHooks; +import ca.spottedleaf.moonrise.common.list.ReferenceList; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.MoonriseConstants; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; +import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants; +import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.ChunkPos; +import java.util.ArrayList; + +public final class NearbyPlayers { + + public static enum NearbyMapType { + GENERAL, + GENERAL_SMALL, + GENERAL_REALLY_SMALL, + TICK_VIEW_DISTANCE, + VIEW_DISTANCE, + // Moonrise start - chunk tick iteration + SPAWN_RANGE { + @Override + void addTo(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { + ((ChunkTickServerLevel)world).moonrise$addPlayerTickingRequest(chunkX, chunkZ); + } + + @Override + void removeFrom(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { + ((ChunkTickServerLevel)world).moonrise$removePlayerTickingRequest(chunkX, chunkZ); + } + }; + // Moonrise end - chunk tick iteration + + void addTo(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { + + } + + void removeFrom(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) { + + } + } + + private static final NearbyMapType[] MAP_TYPES = NearbyMapType.values(); + public static final int TOTAL_MAP_TYPES = MAP_TYPES.length; + + private static final int GENERAL_AREA_VIEW_DISTANCE = MoonriseConstants.MAX_VIEW_DISTANCE + 1; + private static final int GENERAL_SMALL_VIEW_DISTANCE = 10; + private static final int GENERAL_REALLY_SMALL_VIEW_DISTANCE = 3; + + public static final int GENERAL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_AREA_VIEW_DISTANCE << 4); + public static final int GENERAL_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_SMALL_VIEW_DISTANCE << 4); + public static final int GENERAL_REALLY_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_REALLY_SMALL_VIEW_DISTANCE << 4); + + private final ServerLevel world; + private final Reference2ReferenceOpenHashMap players = new Reference2ReferenceOpenHashMap<>(); + private final Long2ReferenceOpenHashMap byChunk = new Long2ReferenceOpenHashMap<>(); + private final Long2ReferenceOpenHashMap>[] directByChunk = new Long2ReferenceOpenHashMap[TOTAL_MAP_TYPES]; + { + for (int i = 0; i < this.directByChunk.length; ++i) { + this.directByChunk[i] = new Long2ReferenceOpenHashMap<>(); + } + } + + public NearbyPlayers(final ServerLevel world) { + this.world = world; + } + + public void addPlayer(final ServerPlayer player) { + final TrackedPlayer[] newTrackers = new TrackedPlayer[TOTAL_MAP_TYPES]; + if (this.players.putIfAbsent(player, newTrackers) != null) { + throw new IllegalStateException("Already have player " + player); + } + + final ChunkPos chunk = player.chunkPosition(); + + for (int i = 0; i < TOTAL_MAP_TYPES; ++i) { + // use 0 for default, will be updated by tickPlayer + (newTrackers[i] = new TrackedPlayer(player, MAP_TYPES[i])).add(chunk.x, chunk.z, 0); + } + + // update view distances + this.tickPlayer(player); + } + + public void removePlayer(final ServerPlayer player) { + final TrackedPlayer[] players = this.players.remove(player); + if (players == null) { + return; // May be called during teleportation before the player is actually placed + } + + for (final TrackedPlayer tracker : players) { + tracker.remove(); + } + } + + public void clear() { + if (this.players.isEmpty()) { + return; + } + + for (final ServerPlayer player : new ArrayList<>(this.players.keySet())) { + this.removePlayer(player); + } + } + + public void tickPlayer(final ServerPlayer player) { + final TrackedPlayer[] players = this.players.get(player); + if (players == null) { + throw new IllegalStateException("Don't have player " + player); + } + + final ChunkPos chunk = player.chunkPosition(); + + players[NearbyMapType.GENERAL.ordinal()].update(chunk.x, chunk.z, GENERAL_AREA_VIEW_DISTANCE); + players[NearbyMapType.GENERAL_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_SMALL_VIEW_DISTANCE); + players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE); + players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, PlatformHooks.get().getTickViewDistance(player)); + players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, PlatformHooks.get().getViewDistance(player)); + players[NearbyMapType.SPAWN_RANGE.ordinal()].update(chunk.x, chunk.z, ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Moonrise - chunk tick iteration + } + + public TrackedChunk getChunk(final ChunkPos pos) { + return this.byChunk.get(CoordinateUtils.getChunkKey(pos)); + } + + public TrackedChunk getChunk(final BlockPos pos) { + return this.byChunk.get(CoordinateUtils.getChunkKey(pos)); + } + + public TrackedChunk getChunk(final int chunkX, final int chunkZ) { + return this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } + + public ReferenceList getPlayers(final BlockPos pos, final NearbyMapType type) { + return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos)); + } + + public ReferenceList getPlayers(final ChunkPos pos, final NearbyMapType type) { + return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos)); + } + + public ReferenceList getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) { + return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } + + public ReferenceList getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) { + return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4)); + } + + public static final class TrackedChunk { + + private static final ServerPlayer[] EMPTY_PLAYERS_ARRAY = new ServerPlayer[0]; + + private final long chunkKey; + private final NearbyPlayers nearbyPlayers; + private final ReferenceList[] players = new ReferenceList[TOTAL_MAP_TYPES]; + private int nonEmptyLists; + private long updateCount; + + public TrackedChunk(final long chunkKey, final NearbyPlayers nearbyPlayers) { + this.chunkKey = chunkKey; + this.nearbyPlayers = nearbyPlayers; + } + + public boolean isEmpty() { + return this.nonEmptyLists == 0; + } + + public long getUpdateCount() { + return this.updateCount; + } + + public ReferenceList getPlayers(final NearbyMapType type) { + return this.players[type.ordinal()]; + } + + public void addPlayer(final ServerPlayer player, final NearbyMapType type) { + ++this.updateCount; + + final int idx = type.ordinal(); + final ReferenceList list = this.players[idx]; + if (list == null) { + ++this.nonEmptyLists; + final ReferenceList players = (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY)); + this.nearbyPlayers.directByChunk[idx].put(this.chunkKey, players); + players.add(player); + return; + } + + if (!list.add(player)) { + throw new IllegalStateException("Already contains player " + player); + } + } + + public void removePlayer(final ServerPlayer player, final NearbyMapType type) { + ++this.updateCount; + + final int idx = type.ordinal(); + final ReferenceList list = this.players[idx]; + if (list == null) { + throw new IllegalStateException("Does not contain player " + player); + } + + if (!list.remove(player)) { + throw new IllegalStateException("Does not contain player " + player); + } + + if (list.size() == 0) { + this.players[idx] = null; + this.nearbyPlayers.directByChunk[idx].remove(this.chunkKey); + --this.nonEmptyLists; + } + } + } + + private final class TrackedPlayer extends SingleUserAreaMap { + + private final NearbyMapType type; + + public TrackedPlayer(final ServerPlayer player, final NearbyMapType type) { + super(player); + this.type = type; + } + + @Override + protected void addCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) { + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey); + final NearbyMapType type = this.type; + if (chunk != null) { + chunk.addPlayer(parameter, type); + type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ); + } else { + final TrackedChunk created = new TrackedChunk(chunkKey, NearbyPlayers.this); + NearbyPlayers.this.byChunk.put(chunkKey, created); + created.addPlayer(parameter, type); + type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ); + + ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$requestChunkData(chunkKey).nearbyPlayers = created; + } + } + + @Override + protected void removeCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) { + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey); + if (chunk == null) { + throw new IllegalStateException("Chunk should exist at " + new ChunkPos(chunkKey)); + } + + final NearbyMapType type = this.type; + chunk.removePlayer(parameter, type); + type.removeFrom(parameter, NearbyPlayers.this.world, chunkX, chunkZ); + + if (chunk.isEmpty()) { + NearbyPlayers.this.byChunk.remove(chunkKey); + final ChunkData chunkData = ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$releaseChunkData(chunkKey); + if (chunkData != null) { + chunkData.nearbyPlayers = null; + } + } + } + } +} diff --git a/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java index c2dfbce23af5741b7f78ddd6df9bcbce69915ae9..5955a56a63e91edafbac07ac1f0c640a4f7cbb26 100644 --- a/ca/spottedleaf/moonrise/paper/PaperHooks.java +++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java @@ -268,7 +268,7 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo @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/paper/util/BaseChunkSystemHooks.java b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java index 34b45bc11124efb22f0f3ae5b2ad8f445c719476..62a9e62711a46283931d22b0e72b2b1903d973a1 100644 --- a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java +++ b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java @@ -23,218 +23,59 @@ import java.util.function.Consumer; public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.common.util.ChunkSystemHooks { - private static final Logger LOGGER = LogUtils.getLogger(); - private static final ChunkStep FULL_CHUNK_STEP = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL); - private static final TicketType CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo); - - private long chunkLoadCounter = 0L; - - private static int getDistance(final ChunkStatus status) { - return FULL_CHUNK_STEP.getAccumulatedRadiusOf(status); - } - @Override public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) { - this.scheduleChunkTask(level, chunkX, chunkZ, run, Priority.NORMAL); + 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); + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkTask(chunkX, chunkZ, run, priority); } @Override public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen, final ChunkStatus toStatus, final boolean addTicket, final Priority priority, final Consumer onComplete) { - 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); - } - } - } - }); + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete); } @Override public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket, final Priority priority, final Consumer onComplete) { - if (!Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) { - this.scheduleChunkTask(level, chunkX, chunkZ, () -> { - BaseChunkSystemHooks.this.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); - }, priority); - return; - } - - final int minLevel = 33 + getDistance(toStatus); - final Long chunkReference = addTicket ? Long.valueOf(++this.chunkLoadCounter) : null; - final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); - - if (addTicket) { - level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); - } - level.chunkSource.runDistanceManagerUpdates(); - - final Consumer loadCallback = (final ChunkAccess chunk) -> { - try { - if (onComplete != null) { - onComplete.accept(chunk); - } - } catch (final Throwable thr) { - LOGGER.error("Exception handling chunk load callback", thr); - com.destroystokyo.paper.util.SneakyThrow.sneaky(thr); - } finally { - if (addTicket) { - level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); - level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); - } - } - }; - - final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); - - if (holder == null || holder.getTicketLevel() > minLevel) { - loadCallback.accept(null); - return; - } - - final CompletableFuture> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap); - - if (loadFuture.isDone()) { - loadCallback.accept(loadFuture.join().orElse(null)); - return; - } - - loadFuture.whenCompleteAsync((final ChunkResult result, final Throwable thr) -> { - if (thr != null) { - loadCallback.accept(null); - return; - } - loadCallback.accept(result.orElse(null)); - }, (final Runnable r) -> { - BaseChunkSystemHooks.this.scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST); - }); + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); } @Override public void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, final FullChunkStatus toStatus, final boolean addTicket, final Priority priority, final Consumer onComplete) { - // This method goes unused until the chunk system rewrite - if (toStatus == FullChunkStatus.INACCESSIBLE) { - throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); - } - - if (!Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) { - this.scheduleChunkTask(level, chunkX, chunkZ, () -> { - BaseChunkSystemHooks.this.scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); - }, priority); - return; - } - - final int minLevel = 33 - (toStatus.ordinal() - 1); - final int radius = toStatus.ordinal() - 1; - final Long chunkReference = addTicket ? Long.valueOf(++this.chunkLoadCounter) : null; - final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); - - if (addTicket) { - level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); - } - level.chunkSource.runDistanceManagerUpdates(); - - final Consumer loadCallback = (final LevelChunk chunk) -> { - try { - if (onComplete != null) { - onComplete.accept(chunk); - } - } catch (final Throwable thr) { - LOGGER.error("Exception handling chunk load callback", thr); - com.destroystokyo.paper.util.SneakyThrow.sneaky(thr); - } finally { - if (addTicket) { - level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); - level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); - } - } - }; - - final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); - - if (holder == null || holder.getTicketLevel() > minLevel) { - loadCallback.accept(null); - return; - } - - final CompletableFuture> tickingState; - switch (toStatus) { - case FULL: { - tickingState = holder.getFullChunkFuture(); - break; - } - case BLOCK_TICKING: { - tickingState = holder.getTickingChunkFuture(); - break; - } - case ENTITY_TICKING: { - tickingState = holder.getEntityTickingChunkFuture(); - break; - } - default: { - throw new IllegalStateException("Cannot reach here"); - } - } - - if (tickingState.isDone()) { - loadCallback.accept(tickingState.join().orElse(null)); - return; - } - - tickingState.whenCompleteAsync((final ChunkResult result, final Throwable thr) -> { - if (thr != null) { - loadCallback.accept(null); - return; - } - loadCallback.accept(result.orElse(null)); - }, (final Runnable r) -> { - BaseChunkSystemHooks.this.scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST); - }); + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); } @Override public List getVisibleChunkHolders(final ServerLevel level) { - return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values()); + return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders(); } @Override public List getUpdatingChunkHolders(final ServerLevel level) { - return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values()); + return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders(); } @Override public int getVisibleChunkHolderCount(final ServerLevel level) { - return level.chunkSource.chunkMap.visibleChunkMap.size(); + return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size(); } @Override public int getUpdatingChunkHolderCount(final ServerLevel level) { - return level.chunkSource.chunkMap.updatingChunkMap.size(); + return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size(); } @Override public boolean hasAnyChunkHolders(final ServerLevel level) { - return this.getUpdatingChunkHolderCount(level) != 0; + return getUpdatingChunkHolderCount(level) != 0; } @Override @@ -244,89 +85,110 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co @Override public void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { - + // Update progress listener for LevelLoadingScreen + final net.minecraft.server.level.progress.ChunkProgressListener progressListener = level.getChunkSource().chunkMap.progressListener; + if (progressListener != null) { + this.scheduleChunkTask(level, holder.getPos().x, holder.getPos().z, () -> { + progressListener.onStatusChange(holder.getPos(), null); + }); + } } @Override public void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) { - + ((ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource()) + .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, chunk); } @Override public void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { - + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().add( + ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() + ); + chunk.loadCallback(); } @Override public void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { - + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().remove( + ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() + ); + chunk.unloadCallback(); } @Override public void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder) { - + ((ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource()) + .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, null); } @Override public void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { - + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().add( + ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() + ); + if (!((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) { + chunk.postProcessGeneration((ServerLevel)chunk.getLevel()); + } + ((ServerLevel)chunk.getLevel()).startTickingChunk(chunk); + ((ServerLevel)chunk.getLevel()).getChunkSource().chunkMap.tickingGenerated.incrementAndGet(); + ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$markChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration } @Override public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { - + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove( + ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() + ); + ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$removeChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration } @Override public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { - + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().add( + ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() + ); } @Override public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { - + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().remove( + ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() + ); } @Override public ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { - return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ); + return null; } @Override public int getSendViewDistance(final ServerPlayer player) { - return this.getViewDistance(player); + return ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.getAPISendViewDistance(player); } @Override public int getViewDistance(final ServerPlayer player) { - final ServerLevel level = player.serverLevel(); - if (level == null) { - return Bukkit.getViewDistance(); - } - return level.chunkSource.chunkMap.serverViewDistance; + return ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.getAPIViewDistance(player); } @Override public int getTickViewDistance(final ServerPlayer player) { - final ServerLevel level = player.serverLevel(); - if (level == null) { - return Bukkit.getSimulationDistance(); - } - return level.chunkSource.chunkMap.distanceManager.simulationDistance; + return ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.getAPITickViewDistance(player); } @Override public void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player) { - + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().addPlayer(player); } @Override public void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player) { - + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().removePlayer(player); } @Override public void updateMaps(final ServerLevel world, final ServerPlayer player) { - + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().updatePlayer(player); } } diff --git a/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..93bc56daec4526f373c84763b8c7ccb4a30e800b --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java @@ -0,0 +1,10 @@ +package ca.spottedleaf.moonrise.patches.block_counting; + +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.shorts.ShortArrayList; + +public interface BlockCountingBitStorage { + + public Int2ObjectOpenHashMap moonrise$countEntries(); + +} diff --git a/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java new file mode 100644 index 0000000000000000000000000000000000000000..0d1443a113c07d7655e7b927a899447f70db8fa9 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java @@ -0,0 +1,11 @@ +package ca.spottedleaf.moonrise.patches.block_counting; + +import ca.spottedleaf.moonrise.common.list.ShortList; + +public interface BlockCountingChunkSection { + + public boolean moonrise$hasSpecialCollidingBlocks(); + + public ShortList moonrise$getTickingBlockList(); + +} diff --git a/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java new file mode 100644 index 0000000000000000000000000000000000000000..89e75b454695e174c5619104eeb15eb923a2d9a7 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java @@ -0,0 +1,12 @@ +package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess; + +public interface PropertyAccess { + + public int moonrise$getId(); + + public int moonrise$getIdFor(final T value); + + public T moonrise$getById(final int id); + + public void moonrise$setById(final T[] values); +} diff --git a/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java new file mode 100644 index 0000000000000000000000000000000000000000..01da52b9e8a786824f199a057b62ce0431ecbc43 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java @@ -0,0 +1,7 @@ +package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess; + +public interface PropertyAccessStateHolder { + + public long moonrise$getTableIndex(); + +} diff --git a/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java new file mode 100644 index 0000000000000000000000000000000000000000..866f38eb0f379ffbe2888023a7d1c290f521a231 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java @@ -0,0 +1,230 @@ +package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util; + +import ca.spottedleaf.concurrentutil.util.IntegerUtil; +import ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess; +import ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccessStateHolder; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.AbstractObjectSet; +import it.unimi.dsi.fastutil.objects.AbstractReference2ObjectMap; +import it.unimi.dsi.fastutil.objects.ObjectIterator; +import it.unimi.dsi.fastutil.objects.ObjectSet; +import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; +import it.unimi.dsi.fastutil.objects.ReferenceArrayList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import net.minecraft.world.level.block.state.StateHolder; +import net.minecraft.world.level.block.state.properties.Property; + +public final class ZeroCollidingReferenceStateTable { + + private final Int2ObjectOpenHashMap propertyToIndexer; + private S[] lookup; + private final Collection> properties; + + public ZeroCollidingReferenceStateTable(final Collection> properties) { + this.propertyToIndexer = new Int2ObjectOpenHashMap<>(properties.size()); + this.properties = new ReferenceArrayList<>(properties); + + final List> sortedProperties = new ArrayList<>(properties); + + // important that each table sees the same property order given the same _set_ of properties, + // as each table will calculate the index for the block state + sortedProperties.sort((final Property p1, final Property p2) -> { + return Integer.compare( + ((PropertyAccess)p1).moonrise$getId(), + ((PropertyAccess)p2).moonrise$getId() + ); + }); + + int currentMultiple = 1; + for (final Property property : sortedProperties) { + final int totalValues = property.getPossibleValues().size(); + + this.propertyToIndexer.put( + ((PropertyAccess)property).moonrise$getId(), + new Indexer( + totalValues, + currentMultiple, + IntegerUtil.getUnsignedDivisorMagic((long)currentMultiple, 32), + IntegerUtil.getUnsignedDivisorMagic((long)totalValues, 32) + ) + ); + + currentMultiple *= totalValues; + } + } + + public > boolean hasProperty(final Property property) { + return this.propertyToIndexer.containsKey(((PropertyAccess)property).moonrise$getId()); + } + + public long getIndex(final StateHolder stateHolder) { + long ret = 0L; + + for (final Map.Entry, Comparable> entry : stateHolder.getValues().entrySet()) { + final Property property = entry.getKey(); + final Comparable value = entry.getValue(); + + final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess)property).moonrise$getId()); + + ret += (((PropertyAccess)property).moonrise$getIdFor(value)) * indexer.multiple; + } + + return ret; + } + + public boolean isLoaded() { + return this.lookup != null; + } + + public void loadInTable(final Map, Comparable>, S> universe) { + if (this.lookup != null) { + throw new IllegalStateException(); + } + + this.lookup = (S[])new StateHolder[universe.size()]; + + for (final Map.Entry, Comparable>, S> entry : universe.entrySet()) { + final S value = entry.getValue(); + if (value == null) { + continue; + } + this.lookup[(int)((PropertyAccessStateHolder)(StateHolder)value).moonrise$getTableIndex()] = value; + } + + for (final S value : this.lookup) { + if (value == null) { + throw new IllegalStateException(); + } + } + } + + public > T get(final long index, final Property property) { + final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess)property).moonrise$getId()); + if (indexer == null) { + return null; + } + + final long divided = (index * indexer.multipleDivMagic) >>> 32; + final long modded = (((divided * indexer.modMagic) & 0xFFFFFFFFL) * indexer.totalValues) >>> 32; + // equiv to: divided = index / multiple + // modded = divided % totalValues + + return ((PropertyAccess)property).moonrise$getById((int)modded); + } + + public > S set(final long index, final Property property, final T with) { + final int newValueId = ((PropertyAccess)property).moonrise$getIdFor(with); + if (newValueId < 0) { + return null; + } + + final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess)property).moonrise$getId()); + if (indexer == null) { + return null; + } + + final long divided = (index * indexer.multipleDivMagic) >>> 32; + final long modded = (((divided * indexer.modMagic) & 0xFFFFFFFFL) * indexer.totalValues) >>> 32; + // equiv to: divided = index / multiple + // modded = divided % totalValues + + // subtract out the old value, add in the new + final long newIndex = (((long)newValueId - modded) * indexer.multiple) + index; + + return this.lookup[(int)newIndex]; + } + + public > S trySet(final long index, final Property property, final T with, final S dfl) { + final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess)property).moonrise$getId()); + if (indexer == null) { + return dfl; + } + + final int newValueId = ((PropertyAccess)property).moonrise$getIdFor(with); + if (newValueId < 0) { + return null; + } + + final long divided = (index * indexer.multipleDivMagic) >>> 32; + final long modded = (((divided * indexer.modMagic) & 0xFFFFFFFFL) * indexer.totalValues) >>> 32; + // equiv to: divided = index / multiple + // modded = divided % totalValues + + // subtract out the old value, add in the new + final long newIndex = (((long)newValueId - modded) * indexer.multiple) + index; + + return this.lookup[(int)newIndex]; + } + + public Collection> getProperties() { + return Collections.unmodifiableCollection(this.properties); + } + + public Map, Comparable> getMapView(final long stateIndex) { + return new MapView(stateIndex); + } + + private static final record Indexer( + int totalValues, int multiple, long multipleDivMagic, long modMagic + ) {} + + private class MapView extends AbstractReference2ObjectMap, Comparable> { + private final long stateIndex; + private EntrySet entrySet; + + MapView(final long stateIndex) { + this.stateIndex = stateIndex; + } + + @Override + public boolean containsKey(final Object key) { + return key instanceof Property prop && ZeroCollidingReferenceStateTable.this.hasProperty(prop); + } + + @Override + public int size() { + return ZeroCollidingReferenceStateTable.this.properties.size(); + } + + @Override + public ObjectSet, Comparable>> reference2ObjectEntrySet() { + if (this.entrySet == null) + this.entrySet = new EntrySet(); + return this.entrySet; + } + + @Override + public Comparable get(final Object key) { + return key instanceof Property prop ? ZeroCollidingReferenceStateTable.this.get(this.stateIndex, prop) : null; + } + + class EntrySet extends AbstractObjectSet, Comparable>> { + @Override + public ObjectIterator, Comparable>> iterator() { + final Iterator> propIterator = ZeroCollidingReferenceStateTable.this.properties.iterator(); + return new ObjectIterator<>() { + @Override + public boolean hasNext() { + return propIterator.hasNext(); + } + + @Override + public Entry, Comparable> next() { + Property prop = propIterator.next(); + return new AbstractReference2ObjectMap.BasicEntry<>(prop, ZeroCollidingReferenceStateTable.this.get(MapView.this.stateIndex, prop)); + } + }; + } + + @Override + public int size() { + return ZeroCollidingReferenceStateTable.this.properties.size(); + } + } + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java new file mode 100644 index 0000000000000000000000000000000000000000..44bb25554634af2ec0b2e9b3d9231304d5dff034 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java @@ -0,0 +1,39 @@ +package ca.spottedleaf.moonrise.patches.chunk_system; + +import ca.spottedleaf.moonrise.common.PlatformHooks; +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.datafix.fixes.References; + +public final class ChunkSystemConverters { + + // See SectionStorage#getVersion + private static final int DEFAULT_POI_DATA_VERSION = 1945; + + private static final int DEFAULT_ENTITY_CHUNK_DATA_VERSION = -1; + + private static int getCurrentVersion() { + return SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + } + + private static int getDataVersion(final CompoundTag data, final int dfl) { + return !data.contains(SharedConstants.DATA_VERSION_TAG, Tag.TAG_ANY_NUMERIC) + ? dfl : data.getInt(SharedConstants.DATA_VERSION_TAG); + } + + public static CompoundTag convertPoiCompoundTag(final CompoundTag data, final ServerLevel world) { + final int dataVersion = getDataVersion(data, DEFAULT_POI_DATA_VERSION); + + return PlatformHooks.get().convertNBT(References.POI_CHUNK, world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion()); + } + + public static CompoundTag convertEntityChunkCompoundTag(final CompoundTag data, final ServerLevel world) { + final int dataVersion = getDataVersion(data, DEFAULT_ENTITY_CHUNK_DATA_VERSION); + + return PlatformHooks.get().convertNBT(References.ENTITY_CHUNK, world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion()); + } + + private ChunkSystemConverters() {} +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java b/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..c7da23900228aab3a5673eb5adfada5091140319 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java @@ -0,0 +1,44 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.entity; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.monster.Shulker; +import net.minecraft.world.entity.vehicle.AbstractMinecart; +import net.minecraft.world.entity.vehicle.Boat; + +public interface ChunkSystemEntity { + + public boolean moonrise$isHardColliding(); + + // for mods to override + public default boolean moonrise$isHardCollidingUncached() { + return this instanceof Boat || this instanceof AbstractMinecart || this instanceof Shulker || ((Entity)this).canBeCollidedWith(); + } + + public FullChunkStatus moonrise$getChunkStatus(); + + public void moonrise$setChunkStatus(final FullChunkStatus status); + + public ChunkData moonrise$getChunkData(); + + public void moonrise$setChunkData(final ChunkData chunkData); + + public int moonrise$getSectionX(); + + public void moonrise$setSectionX(final int x); + + public int moonrise$getSectionY(); + + public void moonrise$setSectionY(final int y); + + public int moonrise$getSectionZ(); + + public void moonrise$setSectionZ(final int z); + + public boolean moonrise$isUpdatingSectionStatus(); + + public void moonrise$setUpdatingSectionStatus(final boolean to); + + public boolean moonrise$hasAnyPlayerPassengers(); +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..a814512fcfb85312474ae2c2c21443843bf57831 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java @@ -0,0 +1,31 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.io; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.chunk.storage.RegionFile; +import java.io.IOException; + +public interface ChunkSystemRegionFileStorage { + + public boolean moonrise$doesRegionFileNotExistNoIO(final int chunkX, final int chunkZ); + + public RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ); + + public RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; + + public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite( + final int chunkX, final int chunkZ, final CompoundTag compound + ) throws IOException; + + public void moonrise$finishWrite( + final int chunkX, final int chunkZ, final MoonriseRegionFileIO.RegionDataController.WriteData writeData + ) throws IOException; + + public MoonriseRegionFileIO.RegionDataController.ReadData moonrise$readData( + final int chunkX, final int chunkZ + ) throws IOException; + + // if the return value is null, then the caller needs to re-try with a new call to readData() + public CompoundTag moonrise$finishRead( + final int chunkX, final int chunkZ, final MoonriseRegionFileIO.RegionDataController.ReadData readData + ) throws IOException; +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java new file mode 100644 index 0000000000000000000000000000000000000000..1acea58838f057ab87efd103cbecb6f5aeaef393 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java @@ -0,0 +1,1700 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.io; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.completable.CallbackCompletable; +import ca.spottedleaf.concurrentutil.completable.Completable; +import ca.spottedleaf.concurrentutil.executor.Cancellable; +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.executor.queue.PrioritisedTaskQueue; +import ca.spottedleaf.concurrentutil.function.BiLong1Function; +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.storage.RegionFile; +import net.minecraft.world.level.chunk.storage.RegionFileStorage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.invoke.VarHandle; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +public final class MoonriseRegionFileIO { + + private static final int REGION_FILE_SHIFT = 5; + private static final Logger LOGGER = LoggerFactory.getLogger(MoonriseRegionFileIO.class); + + /** + * The types of RegionFiles controlled by the I/O thread(s). + */ + public static enum RegionFileType { + CHUNK_DATA, + POI_DATA, + ENTITY_DATA; + } + + public static RegionDataController getControllerFor(final ServerLevel world, final RegionFileType type) { + switch (type) { + case CHUNK_DATA: + return ((ChunkSystemServerLevel)world).moonrise$getChunkDataController(); + case POI_DATA: + return ((ChunkSystemServerLevel)world).moonrise$getPoiChunkDataController(); + case ENTITY_DATA: + return ((ChunkSystemServerLevel)world).moonrise$getEntityChunkDataController(); + default: + throw new IllegalStateException("Unknown controller type " + type); + } + } + + private static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values(); + + /** + * Collects RegionFile data for a certain chunk. + */ + public static final class RegionFileData { + + private final boolean[] hasResult = new boolean[CACHED_REGIONFILE_TYPES.length]; + private final CompoundTag[] data = new CompoundTag[CACHED_REGIONFILE_TYPES.length]; + private final Throwable[] throwables = new Throwable[CACHED_REGIONFILE_TYPES.length]; + + /** + * Sets the result associated with the specified RegionFile type. Note that + * results can only be set once per RegionFile type. + * + * @param type The RegionFile type. + * @param data The result to set. + */ + public void setData(final MoonriseRegionFileIO.RegionFileType type, final CompoundTag data) { + final int index = type.ordinal(); + + if (this.hasResult[index]) { + throw new IllegalArgumentException("Result already exists for type " + type); + } + this.hasResult[index] = true; + this.data[index] = data; + } + + /** + * Sets the result associated with the specified RegionFile type. Note that + * results can only be set once per RegionFile type. + * + * @param type The RegionFile type. + * @param throwable The result to set. + */ + public void setThrowable(final MoonriseRegionFileIO.RegionFileType type, final Throwable throwable) { + final int index = type.ordinal(); + + if (this.hasResult[index]) { + throw new IllegalArgumentException("Result already exists for type " + type); + } + this.hasResult[index] = true; + this.throwables[index] = throwable; + } + + /** + * Returns whether there is a result for the specified RegionFile type. + * + * @param type Specified RegionFile type. + * + * @return Whether a result exists for {@code type}. + */ + public boolean hasResult(final MoonriseRegionFileIO.RegionFileType type) { + return this.hasResult[type.ordinal()]; + } + + /** + * Returns the data result for the RegionFile type. + * + * @param type Specified RegionFile type. + * + * @throws IllegalArgumentException If the result has not been set for {@code type}. + * @return The data result for the specified type. If the result is a {@code Throwable}, + * then returns {@code null}. + */ + public CompoundTag getData(final MoonriseRegionFileIO.RegionFileType type) { + final int index = type.ordinal(); + + if (!this.hasResult[index]) { + throw new IllegalArgumentException("Result does not exist for type " + type); + } + + return this.data[index]; + } + + /** + * Returns the throwable result for the RegionFile type. + * + * @param type Specified RegionFile type. + * + * @throws IllegalArgumentException If the result has not been set for {@code type}. + * @return The throwable result for the specified type. If the result is an {@code CompoundTag}, + * then returns {@code null}. + */ + public Throwable getThrowable(final MoonriseRegionFileIO.RegionFileType type) { + final int index = type.ordinal(); + + if (!this.hasResult[index]) { + throw new IllegalArgumentException("Result does not exist for type " + type); + } + + return this.throwables[index]; + } + } + + public static void flushRegionStorages(final ServerLevel world) throws IOException { + for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { + flushRegionStorages(world, type); + } + } + + public static void flushRegionStorages(final ServerLevel world, final RegionFileType type) throws IOException { + getControllerFor(world, type).getCache().flush(); + } + + public static void flush(final MinecraftServer server) { + for (final ServerLevel world : server.getAllLevels()) { + flush(world); + } + } + + public static void flush(final ServerLevel world) { + for (final RegionFileType regionFileType : CACHED_REGIONFILE_TYPES) { + flush(world, regionFileType); + } + } + + public static void flush(final ServerLevel world, final RegionFileType type) { + final RegionDataController taskController = getControllerFor(world, type); + + long failures = 1L; // start at 0.13ms + + while (taskController.hasTasks()) { + Thread.yield(); + failures = ConcurrentUtil.linearLongBackoff(failures, 125_000L, 5_000_000L); // 125us, 5ms + } + } + + public static void partialFlush(final ServerLevel world, final int tasksRemaining) { + for (long failures = 1L;;) { // start at 0.13ms + long totalTasks = 0L; + for (final RegionFileType regionFileType : CACHED_REGIONFILE_TYPES) { + totalTasks += getControllerFor(world, regionFileType).getTotalWorkingTasks(); + } + + if (totalTasks > (long)tasksRemaining) { + Thread.yield(); + failures = ConcurrentUtil.linearLongBackoff(failures, 125_000L, 5_000_000L); // 125us, 5ms + } else { + return; + } + } + } + + /** + * Returns the priority associated with blocking I/O based on the current thread. The goal is to avoid + * dumb plugins from taking away priority from threads we consider crucial. + * @return The priroity to use with blocking I/O on the current thread. + */ + public static Priority getIOBlockingPriorityForCurrentThread() { + if (TickThread.isTickThread()) { + return Priority.BLOCKING; + } + return Priority.HIGHEST; + } + + /** + * Returns the priority for the specified regionfile type for the specified chunk. + * @param world Specified world. + * @param chunkX Specified chunk x. + * @param chunkZ Specified chunk z. + * @param type Specified regionfile type. + * @return The priority for the chunk + */ + public static Priority getPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { + final RegionDataController taskController = getControllerFor(world, type); + final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + if (task == null) { + return Priority.COMPLETING; + } + + return task.getPriority(); + } + + /** + * Sets the priority for all regionfile types for the specified chunk. Note that great care should + * be taken using this method, as there can be multiple tasks tied to the same chunk that want different + * priorities. + * + * @param world Specified world. + * @param chunkX Specified chunk x. + * @param chunkZ Specified chunk z. + * @param priority New priority. + * + * @see #raisePriority(ServerLevel, int, int, Priority) + * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) + * @see #lowerPriority(ServerLevel, int, int, Priority) + * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) + */ + public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, + final Priority priority) { + for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { + MoonriseRegionFileIO.setPriority(world, chunkX, chunkZ, type, priority); + } + } + + /** + * Sets the priority for the specified regionfile type for the specified chunk. Note that great care should + * be taken using this method, as there can be multiple tasks tied to the same chunk that want different + * priorities. + * + * @param world Specified world. + * @param chunkX Specified chunk x. + * @param chunkZ Specified chunk z. + * @param type Specified regionfile type. + * @param priority New priority. + * + * @see #raisePriority(ServerLevel, int, int, Priority) + * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) + * @see #lowerPriority(ServerLevel, int, int, Priority) + * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) + */ + public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, + final Priority priority) { + final RegionDataController taskController = getControllerFor(world, type); + final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + if (task != null) { + task.setPriority(priority); + } + } + + /** + * Raises the priority for all regionfile types for the specified chunk. + * + * @param world Specified world. + * @param chunkX Specified chunk x. + * @param chunkZ Specified chunk z. + * @param priority New priority. + * + * @see #setPriority(ServerLevel, int, int, Priority) + * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) + * @see #lowerPriority(ServerLevel, int, int, Priority) + * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) + */ + public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, + final Priority priority) { + for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { + MoonriseRegionFileIO.raisePriority(world, chunkX, chunkZ, type, priority); + } + } + + /** + * Raises the priority for the specified regionfile type for the specified chunk. + * + * @param world Specified world. + * @param chunkX Specified chunk x. + * @param chunkZ Specified chunk z. + * @param type Specified regionfile type. + * @param priority New priority. + * + * @see #setPriority(ServerLevel, int, int, Priority) + * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) + * @see #lowerPriority(ServerLevel, int, int, Priority) + * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) + */ + public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, + final Priority priority) { + final RegionDataController taskController = getControllerFor(world, type); + final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + if (task != null) { + task.raisePriority(priority); + } + } + + /** + * Lowers the priority for all regionfile types for the specified chunk. + * + * @param world Specified world. + * @param chunkX Specified chunk x. + * @param chunkZ Specified chunk z. + * @param priority New priority. + * + * @see #raisePriority(ServerLevel, int, int, Priority) + * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) + * @see #setPriority(ServerLevel, int, int, Priority) + * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) + */ + public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, + final Priority priority) { + for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { + MoonriseRegionFileIO.lowerPriority(world, chunkX, chunkZ, type, priority); + } + } + + /** + * Lowers the priority for the specified regionfile type for the specified chunk. + * + * @param world Specified world. + * @param chunkX Specified chunk x. + * @param chunkZ Specified chunk z. + * @param type Specified regionfile type. + * @param priority New priority. + * + * @see #raisePriority(ServerLevel, int, int, Priority) + * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) + * @see #setPriority(ServerLevel, int, int, Priority) + * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) + */ + public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, + final Priority priority) { + final RegionDataController taskController = getControllerFor(world, type); + final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + if (task != null) { + task.lowerPriority(priority); + } + } + + /** + * Schedules the chunk data to be written asynchronously. + *

+ * Impl notes: + *

+ *
  • + * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means + * saves must be scheduled before a chunk is unloaded. + *
  • + *
  • + * Writes may be called concurrently, although only the "later" write will go through. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param data Chunk's data + * @param type The regionfile type to write to. + * + * @throws IllegalStateException If the file io thread has shutdown. + */ + public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, + final RegionFileType type) { + MoonriseRegionFileIO.scheduleSave(world, chunkX, chunkZ, data, type, Priority.NORMAL); + } + + /** + * Schedules the chunk data to be written asynchronously. + *

    + * Impl notes: + *

    + *
  • + * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means + * saves must be scheduled before a chunk is unloaded. + *
  • + *
  • + * Writes may be called concurrently, although only the "later" write will go through. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param data Chunk's data + * @param type The regionfile type to write to. + * @param priority The minimum priority to schedule at. + * + * @throws IllegalStateException If the file io thread has shutdown. + */ + public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, + final RegionFileType type, final Priority priority) { + scheduleSave( + world, chunkX, chunkZ, + (final BiConsumer consumer) -> { + consumer.accept(data, null); + }, null, type, priority + ); + } + + /** + * Schedules the chunk data to be written asynchronously. + *

    + * Impl notes: + *

    + *
  • + * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means + * saves must be scheduled before a chunk is unloaded. + *
  • + *
  • + * Writes may be called concurrently, although only the "later" write will go through. + *
  • + *
  • + * The specified write task, if not null, will have its priority controlled by the scheduler. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param completable Chunk's pending data + * @param writeTask The task responsible for completing the pending chunk data + * @param type The regionfile type to write to. + * @param priority The minimum priority to schedule at. + * + * @throws IllegalStateException If the file io thread has shutdown. + */ + public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CallbackCompletable completable, + final PrioritisedExecutor.PrioritisedTask writeTask, final RegionFileType type, final Priority priority) { + scheduleSave(world, chunkX, chunkZ, completable::addWaiter, writeTask, type, priority); + } + + /** + * Schedules the chunk data to be written asynchronously. + *

    + * Impl notes: + *

    + *
  • + * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means + * saves must be scheduled before a chunk is unloaded. + *
  • + *
  • + * Writes may be called concurrently, although only the "later" write will go through. + *
  • + *
  • + * The specified write task, if not null, will have its priority controlled by the scheduler. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param completable Chunk's pending data + * @param writeTask The task responsible for completing the pending chunk data + * @param type The regionfile type to write to. + * @param priority The minimum priority to schedule at. + * + * @throws IllegalStateException If the file io thread has shutdown. + */ + public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final Completable completable, + final PrioritisedExecutor.PrioritisedTask writeTask, final RegionFileType type, final Priority priority) { + scheduleSave(world, chunkX, chunkZ, completable::whenComplete, writeTask, type, priority); + } + + private static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final Consumer> scheduler, + final PrioritisedExecutor.PrioritisedTask writeTask, final RegionFileType type, final Priority priority) { + final RegionDataController taskController = getControllerFor(world, type); + + final boolean[] created = new boolean[1]; + final ChunkIOTask.InProgressWrite write = new ChunkIOTask.InProgressWrite(writeTask); + final ChunkIOTask task = taskController.chunkTasks.compute(CoordinateUtils.getChunkKey(chunkX, chunkZ), + (final long keyInMap, final ChunkIOTask taskRunning) -> { + if (taskRunning == null || taskRunning.failedWrite) { + // no task is scheduled or the previous write failed - meaning we need to overwrite it + + // create task + final ChunkIOTask newTask = new ChunkIOTask( + world, taskController, chunkX, chunkZ, priority, new ChunkIOTask.InProgressRead() + ); + + newTask.pushPendingWrite(write); + + created[0] = true; + + return newTask; + } + + taskRunning.pushPendingWrite(write); + + return taskRunning; + } + ); + + write.schedule(task, scheduler); + + if (created[0]) { + taskController.startTask(task); + task.scheduleWriteCompress(); + } else { + task.raisePriority(priority); + } + } + + /** + * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call + * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} + * for single load. + *

    + * Impl notes: + *

    + *
  • + * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may + * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of + * data is undefined behaviour, and can cause deadlock. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param onComplete Consumer to execute once this task has completed + * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost + * of this call. + * + * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. + * + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) + */ + public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, + final Consumer onComplete, final boolean intendingToBlock) { + return MoonriseRegionFileIO.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL); + } + + /** + * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call + * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} + * for single load. + *

    + * Impl notes: + *

    + *
  • + * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may + * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of + * data is undefined behaviour, and can cause deadlock. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param onComplete Consumer to execute once this task has completed + * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost + * of this call. + * @param priority The minimum priority to load the data at. + * + * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. + * + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) + */ + public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, + final Consumer onComplete, final boolean intendingToBlock, + final Priority priority) { + return MoonriseRegionFileIO.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES); + } + + /** + * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and + * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} + * for single load. + *

    + * Impl notes: + *

    + *
  • + * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may + * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of + * data is undefined behaviour, and can cause deadlock. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param onComplete Consumer to execute once this task has completed + * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost + * of this call. + * @param types The regionfile type(s) to load. + * + * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. + * + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) + */ + public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, + final Consumer onComplete, final boolean intendingToBlock, + final RegionFileType... types) { + return MoonriseRegionFileIO.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL, types); + } + + /** + * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and + * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} + * for single load. + *

    + * Impl notes: + *

    + *
  • + * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may + * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of + * data is undefined behaviour, and can cause deadlock. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param onComplete Consumer to execute once this task has completed + * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost + * of this call. + * @param types The regionfile type(s) to load. + * @param priority The minimum priority to load the data at. + * + * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. + * + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) + */ + public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, + final Consumer onComplete, final boolean intendingToBlock, + final Priority priority, final RegionFileType... types) { + if (types == null) { + throw new NullPointerException("Types cannot be null"); + } + if (types.length == 0) { + throw new IllegalArgumentException("Types cannot be empty"); + } + + final RegionFileData ret = new RegionFileData(); + + final Cancellable[] reads = new CancellableRead[types.length]; + final AtomicInteger completions = new AtomicInteger(); + final int expectedCompletions = types.length; + + for (int i = 0; i < expectedCompletions; ++i) { + final RegionFileType type = types[i]; + reads[i] = MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, + (final CompoundTag data, final Throwable throwable) -> { + if (throwable != null) { + ret.setThrowable(type, throwable); + } else { + ret.setData(type, data); + } + + if (completions.incrementAndGet() == expectedCompletions) { + onComplete.accept(ret); + } + }, intendingToBlock, priority); + } + + return new CancellableReads(reads); + } + + /** + * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call + * {@code onComplete}. + *

    + * Impl notes: + *

    + *
  • + * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may + * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of + * data is undefined behaviour, and can cause deadlock. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param onComplete Consumer to execute once this task has completed + * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost + * of this call. + * + * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. + * + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) + */ + public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, + final RegionFileType type, final BiConsumer onComplete, + final boolean intendingToBlock) { + return MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, Priority.NORMAL); + } + + /** + * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call + * {@code onComplete}. + *

    + * Impl notes: + *

    + *
  • + * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may + * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of + * data is undefined behaviour, and can cause deadlock. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param onComplete Consumer to execute once this task has completed + * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost + * of this call. + * @param priority Minimum priority to load the data at. + * + * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. + * + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) + */ + public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, + final RegionFileType type, final BiConsumer onComplete, + final boolean intendingToBlock, final Priority priority) { + final RegionDataController taskController = getControllerFor(world, type); + + final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion(); + + final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); + final BiLong1Function compute = (final long keyInMap, final ChunkIOTask running) -> { + if (running == null) { + // not scheduled + + // set up task + final ChunkIOTask newTask = new ChunkIOTask( + world, taskController, chunkX, chunkZ, priority, new ChunkIOTask.InProgressRead() + ); + newTask.inProgressRead.addToAsyncWaiters(onComplete); + + callbackInfo.tasksNeedReadScheduling = true; + return newTask; + } + + final ChunkIOTask.InProgressWrite pendingWrite = running.inProgressWrite; + + if (pendingWrite == null) { + // need to add to waiters here, because the regionfile thread will use compute() to lock and check for cancellations + if (!running.inProgressRead.addToAsyncWaiters(onComplete)) { + callbackInfo.data = running.inProgressRead.value; + callbackInfo.throwable = running.inProgressRead.throwable; + callbackInfo.completeNow = true; + return running; + } + + callbackInfo.read = running.inProgressRead; + + return running; + } + + // at this stage we have to use the in progress write's data to avoid an order issue + + if (!pendingWrite.addToAsyncWaiters(onComplete)) { + // data is ready now + callbackInfo.data = pendingWrite.value; + callbackInfo.throwable = pendingWrite.throwable; + callbackInfo.completeNow = true; + return running; + } + + callbackInfo.write = pendingWrite; + + return running; + }; + + final ChunkIOTask ret = taskController.chunkTasks.compute(key, compute); + + // needs to be scheduled + if (callbackInfo.tasksNeedReadScheduling) { + taskController.startTask(ret); + ret.scheduleReadIO(); + } else if (callbackInfo.completeNow) { + try { + onComplete.accept(callbackInfo.data == null ? null : callbackInfo.data.copy(), callbackInfo.throwable); + } catch (final Throwable thr) { + LOGGER.error("Callback " + ConcurrentUtil.genericToString(onComplete) + " synchronously failed to handle chunk data for task " + ret.toString(), thr); + } + } else { + // we're waiting on a task we didn't schedule, so raise its priority to what we want + ret.raisePriority(priority); + } + + return new CancellableRead(onComplete, callbackInfo.read, callbackInfo.write); + } + + private static final class ImmediateCallbackCompletion { + + private CompoundTag data; + private Throwable throwable; + private boolean completeNow; + private boolean tasksNeedReadScheduling; + private ChunkIOTask.InProgressRead read; + private ChunkIOTask.InProgressWrite write; + + } + + /** + * Schedules a load task to be executed asynchronously, and blocks on that task. + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param type Regionfile type + * @param priority Minimum priority to load the data at. + * + * @return The chunk data for the chunk. Note that a {@code null} result means the chunk or regionfile does not exist on disk. + * + * @throws IOException If the load fails for any reason + */ + public static CompoundTag loadData(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, + final Priority priority) throws IOException { + final CompletableFuture ret = new CompletableFuture<>(); + + MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> { + if (thr != null) { + ret.completeExceptionally(thr); + } else { + ret.complete(compound); + } + }, true, priority); + + try { + return ret.join(); + } catch (final CompletionException ex) { + throw new IOException(ex); + } + } + + private static final class CancellableRead implements Cancellable { + + private BiConsumer callback; + private ChunkIOTask.InProgressRead read; + private ChunkIOTask.InProgressWrite write; + + private CancellableRead(final BiConsumer callback, + final ChunkIOTask.InProgressRead read, + final ChunkIOTask.InProgressWrite write) { + this.callback = callback; + this.read = read; + this.write = write; + } + + @Override + public boolean cancel() { + final BiConsumer callback = this.callback; + final ChunkIOTask.InProgressRead read = this.read; + final ChunkIOTask.InProgressWrite write = this.write; + + if (callback == null || (read == null && write == null)) { + return false; + } + + this.callback = null; + this.read = null; + this.write = null; + + if (read != null) { + return read.cancel(callback); + } + if (write != null) { + return write.cancel(callback); + } + + // unreachable + throw new InternalError(); + } + } + + private static final class CancellableReads implements Cancellable { + + private Cancellable[] reads; + private static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class); + + private CancellableReads(final Cancellable[] reads) { + this.reads = reads; + } + + @Override + public boolean cancel() { + final Cancellable[] reads = (Cancellable[])READS_HANDLE.getAndSet((CancellableReads)this, (Cancellable[])null); + + if (reads == null) { + return false; + } + + boolean ret = false; + + for (final Cancellable read : reads) { + ret |= read.cancel(); + } + + return ret; + } + } + + private static final class ChunkIOTask { + + private final ServerLevel world; + private final RegionDataController regionDataController; + private final int chunkX; + private final int chunkZ; + private Priority priority; + private PrioritisedExecutor.PrioritisedTask currentTask; + + private final InProgressRead inProgressRead; + private volatile InProgressWrite inProgressWrite; + private final ReferenceOpenHashSet allPendingWrites = new ReferenceOpenHashSet<>(); + + private RegionDataController.ReadData readData; + private RegionDataController.WriteData writeData; + private boolean failedWrite; + + public ChunkIOTask(final ServerLevel world, final RegionDataController regionDataController, + final int chunkX, final int chunkZ, final Priority priority, final InProgressRead inProgressRead) { + this.world = world; + this.regionDataController = regionDataController; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.priority = priority; + this.inProgressRead = inProgressRead; + } + + public Priority getPriority() { + synchronized (this) { + return this.priority; + } + } + + // must hold lock on this object + private void updatePriority(final Priority priority) { + this.priority = priority; + if (this.currentTask != null) { + this.currentTask.setPriority(priority); + } + for (final InProgressWrite write : this.allPendingWrites) { + if (write.writeTask != null) { + write.writeTask.setPriority(priority); + } + } + } + + public boolean setPriority(final Priority priority) { + synchronized (this) { + if (this.priority == priority) { + return false; + } + + this.updatePriority(priority); + + return true; + } + } + + public boolean raisePriority(final Priority priority) { + synchronized (this) { + if (this.priority.isHigherOrEqualPriority(priority)) { + return false; + } + + this.updatePriority(priority); + + return true; + } + } + + public boolean lowerPriority(final Priority priority) { + synchronized (this) { + if (this.priority.isLowerOrEqualPriority(priority)) { + return false; + } + + this.updatePriority(priority); + + return true; + } + } + + private void pushPendingWrite(final InProgressWrite write) { + this.inProgressWrite = write; + synchronized (this) { + this.allPendingWrites.add(write); + if (write.writeTask != null) { + write.writeTask.setPriority(this.priority); + } + } + } + + private void pendingWriteComplete(final InProgressWrite write) { + synchronized (this) { + this.allPendingWrites.remove(write); + } + } + + public void scheduleReadIO() { + final PrioritisedExecutor.PrioritisedTask task; + synchronized (this) { + task = this.regionDataController.ioScheduler.createTask(this.chunkX, this.chunkZ, this::performReadIO, this.priority); + this.currentTask = task; + } + task.queue(); + } + + private void performReadIO() { + final InProgressRead read = this.inProgressRead; + final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ); + + final boolean[] canRead = new boolean[] { true }; + + if (read.hasNoWaiters()) { + // cancelled read? go to task controller to confirm + final ChunkIOTask inMap = this.regionDataController.chunkTasks.compute(chunkKey, (final long keyInMap, final ChunkIOTask valueInMap) -> { + if (valueInMap == null) { + throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkIOTask.this.toString() + ", report this!"); + } + if (valueInMap != ChunkIOTask.this) { + throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkIOTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); + } + + if (!read.hasNoWaiters()) { + return valueInMap; + } else { + canRead[0] = false; + } + + if (valueInMap.inProgressWrite != null) { + return valueInMap; + } + + return null; + }); + + if (inMap == null) { + this.regionDataController.endTask(this); + // read is cancelled - and no write pending, so we're done + return; + } + // if there is a write in progress, we don't actually have to worry about waiters gaining new entries - + // the readers will just use the in progress write, so the value in canRead is good to use without + // further synchronisation. + } + + if (canRead[0]) { + RegionDataController.ReadData readData = null; + Throwable throwable = null; + + try { + readData = this.regionDataController.readData(this.chunkX, this.chunkZ); + } catch (final Throwable thr) { + throwable = thr; + LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr); + } + + if (throwable != null) { + this.finishRead(null, throwable); + } else { + switch (readData.result()) { + case NO_DATA: + case SYNC_READ: { + this.finishRead(readData.syncRead(), null); + break; + } + case HAS_DATA: { + this.readData = readData; + this.scheduleReadDecompress(); + // read will handle write scheduling + return; + } + default: { + throw new IllegalStateException("Unknown state: " + readData.result()); + } + } + } + } + + if (!this.tryAbortWrite()) { + this.scheduleWriteCompress(); + } + } + + private void scheduleReadDecompress() { + final PrioritisedExecutor.PrioritisedTask task; + synchronized (this) { + task = this.regionDataController.compressionExecutor.createTask(this::performReadDecompress, this.priority); + this.currentTask = task; + } + task.queue(); + } + + private void performReadDecompress() { + final RegionDataController.ReadData readData = this.readData; + this.readData = null; + + CompoundTag compoundTag = null; + Throwable throwable = null; + + try { + compoundTag = this.regionDataController.finishRead(this.chunkX, this.chunkZ, readData); + } catch (final Throwable thr) { + throwable = thr; + LOGGER.error("Failed to decompress chunk data for task: " + this.toString(), thr); + } + + if (compoundTag == null) { + // need to re-try from the start + this.scheduleReadIO(); + return; + } + + this.finishRead(compoundTag, throwable); + if (!this.tryAbortWrite()) { + this.scheduleWriteCompress(); + } + } + + private void finishRead(final CompoundTag compoundTag, final Throwable throwable) { + this.inProgressRead.complete(this, compoundTag, throwable); + } + + public void scheduleWriteCompress() { + final InProgressWrite inProgressWrite = this.inProgressWrite; + + final PrioritisedExecutor.PrioritisedTask task; + synchronized (this) { + task = this.regionDataController.compressionExecutor.createTask(() -> { + ChunkIOTask.this.performWriteCompress(inProgressWrite); + }, this.priority); + this.currentTask = task; + } + + inProgressWrite.addToWaiters(this, (final CompoundTag data, final Throwable throwable) -> { + task.queue(); + }); + } + + private boolean tryAbortWrite() { + final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ); + if (this.inProgressWrite == null) { + final ChunkIOTask inMap = this.regionDataController.chunkTasks.compute(chunkKey, (final long keyInMap, final ChunkIOTask valueInMap) -> { + if (valueInMap == null) { + throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkIOTask.this.toString() + ", report this!"); + } + if (valueInMap != ChunkIOTask.this) { + throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkIOTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); + } + + if (valueInMap.inProgressWrite != null) { + return valueInMap; + } + + return null; + }); + + if (inMap == null) { + this.regionDataController.endTask(this); + return true; // set the task value to null, indicating we're done + } // else: inProgressWrite changed, so now we have something to write + } + + return false; + } + + private void performWriteCompress(final InProgressWrite inProgressWrite) { + final CompoundTag write = inProgressWrite.value; + if (!inProgressWrite.isComplete()) { + throw new IllegalStateException("Should be writable"); + } + + RegionDataController.WriteData writeData = null; + boolean failedWrite = false; + + try { + writeData = this.regionDataController.startWrite(this.chunkX, this.chunkZ, write); + } catch (final Throwable thr) { + failedWrite = thr instanceof IOException; + LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr); + } + + if (writeData == null) { + // null if a throwable was encountered + + // we cannot continue to the I/O stage here, so try to complete + + if (this.tryCompleteWrite(inProgressWrite, failedWrite)) { + return; + } else { + // fetch new data and try again + this.scheduleWriteCompress(); + return; + } + } else { + // writeData != null && !failedWrite + // we can continue to I/O stage + this.writeData = writeData; + this.scheduleWriteIO(inProgressWrite); + return; + } + } + + private void scheduleWriteIO(final InProgressWrite inProgressWrite) { + final PrioritisedExecutor.PrioritisedTask task; + synchronized (this) { + task = this.regionDataController.ioScheduler.createTask(this.chunkX, this.chunkZ, () -> { + ChunkIOTask.this.runWriteIO(inProgressWrite); + }, this.priority); + this.currentTask = task; + } + task.queue(); + } + + private void runWriteIO(final InProgressWrite inProgressWrite) { + RegionDataController.WriteData writeData = this.writeData; + this.writeData = null; + + boolean failedWrite = false; + + try { + this.regionDataController.finishWrite(this.chunkX, this.chunkZ, writeData); + } catch (final Throwable thr) { + failedWrite = thr instanceof IOException; + LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr); + } + + if (!this.tryCompleteWrite(inProgressWrite, failedWrite)) { + // fetch new data and try again + this.scheduleWriteCompress(); + } + return; + } + + private boolean tryCompleteWrite(final InProgressWrite written, final boolean failedWrite) { + final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ); + + final boolean[] done = new boolean[] { false }; + + this.regionDataController.chunkTasks.compute(chunkKey, (final long keyInMap, final ChunkIOTask valueInMap) -> { + if (valueInMap == null) { + throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkIOTask.this.toString() + ", report this!"); + } + if (valueInMap != ChunkIOTask.this) { + throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkIOTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); + } + if (valueInMap.inProgressWrite == written) { + valueInMap.failedWrite = failedWrite; + done[0] = true; + // keep the data in map if we failed the write so we can try to prevent data loss + return failedWrite ? valueInMap : null; + } + // different data than expected, means we need to retry write + return valueInMap; + }); + + if (done[0]) { + this.regionDataController.endTask(this); + return true; + } + return false; + } + + @Override + public String toString() { + return "Task for world: '" + WorldUtil.getWorldName(this.world) + "' at (" + this.chunkX + "," + + this.chunkZ + ") type: " + this.regionDataController.type.name() + ", hash: " + this.hashCode(); + } + + private static final class InProgressRead { + + private static final Logger LOGGER = LoggerFactory.getLogger(InProgressRead.class); + + private CompoundTag value; + private Throwable throwable; + private final MultiThreadedQueue> callbacks = new MultiThreadedQueue<>(); + + public boolean hasNoWaiters() { + return this.callbacks.isEmpty(); + } + + public boolean addToAsyncWaiters(final BiConsumer callback) { + return this.callbacks.add(callback); + } + + public boolean cancel(final BiConsumer callback) { + return this.callbacks.remove(callback); + } + + public void complete(final ChunkIOTask task, final CompoundTag value, final Throwable throwable) { + this.value = value; + this.throwable = throwable; + + BiConsumer consumer; + while ((consumer = this.callbacks.pollOrBlockAdds()) != null) { + try { + consumer.accept(value == null ? null : value.copy(), throwable); + } catch (final Throwable thr) { + LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data (read) for task " + task.toString(), thr); + } + } + } + } + + private static final class InProgressWrite { + + private static final Logger LOGGER = LoggerFactory.getLogger(InProgressWrite.class); + + private CompoundTag value; + private Throwable throwable; + private volatile boolean complete; + private final MultiThreadedQueue> callbacks = new MultiThreadedQueue<>(); + + private final PrioritisedExecutor.PrioritisedTask writeTask; + + public InProgressWrite(final PrioritisedExecutor.PrioritisedTask writeTask) { + this.writeTask = writeTask; + } + + public boolean isComplete() { + return this.complete; + } + + public void schedule(final ChunkIOTask task, final Consumer> scheduler) { + scheduler.accept((final CompoundTag data, final Throwable throwable) -> { + InProgressWrite.this.complete(task, data, throwable); + }); + } + + public boolean addToAsyncWaiters(final BiConsumer callback) { + return this.callbacks.add(callback); + } + + public void addToWaiters(final ChunkIOTask task, final BiConsumer consumer) { + if (!this.callbacks.add(consumer)) { + this.syncAccept(task, consumer, this.value, this.throwable); + } + } + + private void syncAccept(final ChunkIOTask task, final BiConsumer consumer, final CompoundTag value, final Throwable throwable) { + try { + consumer.accept(value == null ? null : value.copy(), throwable); + } catch (final Throwable thr) { + LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data (write) for task " + task.toString(), thr); + } + } + + public void complete(final ChunkIOTask task, final CompoundTag value, final Throwable throwable) { + this.value = value; + this.throwable = throwable; + this.complete = true; + + task.pendingWriteComplete(this); + + BiConsumer consumer; + while ((consumer = this.callbacks.pollOrBlockAdds()) != null) { + this.syncAccept(task, consumer, value, throwable); + } + } + + public boolean cancel(final BiConsumer callback) { + return this.callbacks.remove(callback); + } + } + } + + public static abstract class RegionDataController { + + public final RegionFileType type; + private final PrioritisedExecutor compressionExecutor; + private final IOScheduler ioScheduler; + private final ConcurrentLong2ReferenceChainedHashTable chunkTasks = new ConcurrentLong2ReferenceChainedHashTable<>(); + + private final AtomicLong inProgressTasks = new AtomicLong(); + + public RegionDataController(final RegionFileType type, final PrioritisedExecutor ioExecutor, + final PrioritisedExecutor compressionExecutor) { + this.type = type; + this.compressionExecutor = compressionExecutor; + this.ioScheduler = new IOScheduler(ioExecutor); + } + + final void startTask(final ChunkIOTask task) { + this.inProgressTasks.getAndIncrement(); + } + + final void endTask(final ChunkIOTask task) { + this.inProgressTasks.getAndDecrement(); + } + + public boolean hasTasks() { + return this.inProgressTasks.get() != 0L; + } + + public long getTotalWorkingTasks() { + return this.inProgressTasks.get(); + } + + public abstract RegionFileStorage getCache(); + + public static record WriteData(CompoundTag input, WriteResult result, DataOutputStream output, IORunnable write) { + public static enum WriteResult { + WRITE, + DELETE; + } + } + + public abstract WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException; + + public abstract void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException; + + public static record ReadData(ReadResult result, DataInputStream input, CompoundTag syncRead) { + public static enum ReadResult { + NO_DATA, + HAS_DATA, + SYNC_READ; + } + } + + public abstract ReadData readData(final int chunkX, final int chunkZ) throws IOException; + + // if the return value is null, then the caller needs to re-try with a new call to readData() + public abstract CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException; + + public static interface IORunnable { + + public void run(final RegionFile regionFile) throws IOException; + + } + } + + private static final class IOScheduler { + + private final ConcurrentLong2ReferenceChainedHashTable regionTasks = new ConcurrentLong2ReferenceChainedHashTable<>(); + private final PrioritisedExecutor executor; + + public IOScheduler(final PrioritisedExecutor executor) { + this.executor = executor; + } + + public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, + final Runnable run, final Priority priority) { + final PrioritisedExecutor.PrioritisedTask[] ret = new PrioritisedExecutor.PrioritisedTask[1]; + final long subOrder = this.executor.generateNextSubOrder(); + this.regionTasks.compute(CoordinateUtils.getChunkKey(chunkX >> REGION_FILE_SHIFT, chunkZ >> REGION_FILE_SHIFT), + (final long regionKey, final RegionIOTasks existing) -> { + final RegionIOTasks res; + if (existing != null) { + res = existing; + } else { + res = new RegionIOTasks(regionKey, IOScheduler.this); + } + + ret[0] = res.createTask(run, priority, subOrder); + + return res; + }); + + return ret[0]; + } + } + + private static final class RegionIOTasks implements Runnable { + + private static final Logger LOGGER = LoggerFactory.getLogger(RegionIOTasks.class); + + private final PrioritisedTaskQueue queue = new PrioritisedTaskQueue(); + private final long regionKey; + private final IOScheduler ioScheduler; + private long createdTasks; + private long executedTasks; + + private PrioritisedExecutor.PrioritisedTask task; + + public RegionIOTasks(final long regionKey, final IOScheduler ioScheduler) { + this.regionKey = regionKey; + this.ioScheduler = ioScheduler; + } + + public PrioritisedExecutor.PrioritisedTask createTask(final Runnable run, final Priority priority, + final long subOrder) { + ++this.createdTasks; + return new WrappedTask(this.queue.createTask(run, priority, subOrder)); + } + + private void adjustTaskPriority() { + final PrioritisedTaskQueue.PrioritySubOrderPair priority = this.queue.getHighestPrioritySubOrder(); + if (this.task == null) { + if (priority == null) { + return; + } + this.task = this.ioScheduler.executor.createTask(this, priority.priority(), priority.subOrder()); + this.task.queue(); + } else { + if (priority == null) { + throw new IllegalStateException(); + } else { + this.task.setPriorityAndSubOrder(priority.priority(), priority.subOrder()); + } + } + } + + @Override + public void run() { + final Runnable run; + synchronized (this) { + run = this.queue.pollTask(); + } + + try { + run.run(); + } finally { + synchronized (this) { + this.task = null; + this.adjustTaskPriority(); + } + this.ioScheduler.regionTasks.compute(this.regionKey, (final long keyInMap, final RegionIOTasks tasks) -> { + if (tasks != RegionIOTasks.this) { + throw new IllegalStateException("Region task mismatch"); + } + ++tasks.executedTasks; + if (tasks.createdTasks != tasks.executedTasks) { + return tasks; + } + + if (tasks.task != null) { + throw new IllegalStateException("Task may not be null when created==executed"); + } + + return null; + }); + } + } + + private final class WrappedTask implements PrioritisedExecutor.PrioritisedTask { + + private final PrioritisedExecutor.PrioritisedTask wrapped; + + public WrappedTask(final PrioritisedExecutor.PrioritisedTask wrap) { + this.wrapped = wrap; + } + + @Override + public PrioritisedExecutor getExecutor() { + return RegionIOTasks.this.ioScheduler.executor; + } + + @Override + public boolean queue() { + synchronized (RegionIOTasks.this) { + if (this.wrapped.queue()) { + RegionIOTasks.this.adjustTaskPriority(); + return true; + } + return false; + } + } + + @Override + public boolean isQueued() { + return this.wrapped.isQueued(); + } + + @Override + public boolean cancel() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean execute() { + throw new UnsupportedOperationException(); + } + + @Override + public Priority getPriority() { + return this.wrapped.getPriority(); + } + + @Override + public boolean setPriority(final Priority priority) { + synchronized (RegionIOTasks.this) { + if (this.wrapped.setPriority(priority) && this.wrapped.isQueued()) { + RegionIOTasks.this.adjustTaskPriority(); + return true; + } + return false; + } + } + + @Override + public boolean raisePriority(final Priority priority) { + synchronized (RegionIOTasks.this) { + if (this.wrapped.raisePriority(priority) && this.wrapped.isQueued()) { + RegionIOTasks.this.adjustTaskPriority(); + return true; + } + return false; + } + } + + @Override + public boolean lowerPriority(final Priority priority) { + synchronized (RegionIOTasks.this) { + if (this.wrapped.lowerPriority(priority) && this.wrapped.isQueued()) { + RegionIOTasks.this.adjustTaskPriority(); + return true; + } + return false; + } + } + + @Override + public long getSubOrder() { + return this.wrapped.getSubOrder(); + } + + @Override + public boolean setSubOrder(final long subOrder) { + synchronized (RegionIOTasks.this) { + if (this.wrapped.setSubOrder(subOrder) && this.wrapped.isQueued()) { + RegionIOTasks.this.adjustTaskPriority(); + return true; + } + return false; + } + } + + @Override + public boolean raiseSubOrder(final long subOrder) { + synchronized (RegionIOTasks.this) { + if (this.wrapped.raiseSubOrder(subOrder) && this.wrapped.isQueued()) { + RegionIOTasks.this.adjustTaskPriority(); + return true; + } + return false; + } + } + + @Override + public boolean lowerSubOrder(final long subOrder) { + synchronized (RegionIOTasks.this) { + if (this.wrapped.lowerSubOrder(subOrder) && this.wrapped.isQueued()) { + RegionIOTasks.this.adjustTaskPriority(); + return true; + } + return false; + } + } + + @Override + public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) { + synchronized (RegionIOTasks.this) { + if (this.wrapped.setPriorityAndSubOrder(priority, subOrder) && this.wrapped.isQueued()) { + RegionIOTasks.this.adjustTaskPriority(); + return true; + } + return false; + } + } + } + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..a36ab89f5c37f5f9ab0152f087bb4cf3560f8581 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java @@ -0,0 +1,50 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller; + +import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemChunkMap; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkStorage; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.RegionFileStorage; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public final class ChunkDataController extends MoonriseRegionFileIO.RegionDataController { + + private final ServerLevel world; + + public ChunkDataController(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { + super(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor); + this.world = world; + } + + @Override + public RegionFileStorage getCache() { + return ((ChunkSystemChunkStorage)this.world.getChunkSource().chunkMap).moonrise$getRegionStorage(); + } + + @Override + public WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { + return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$startWrite(chunkX, chunkZ, compound); + } + + @Override + public void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException { + ((ChunkSystemChunkMap)this.world.getChunkSource().chunkMap).moonrise$writeFinishCallback(new ChunkPos(chunkX, chunkZ)); + ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishWrite(chunkX, chunkZ, writeData); + } + + @Override + public ReadData readData(final int chunkX, final int chunkZ) throws IOException { + return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$readData(chunkX, chunkZ); + } + + @Override + public CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException { + return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishRead(chunkX, chunkZ, readData); + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..828c868f68c2a20bf90d0f7ec253fdeb591f15f6 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java @@ -0,0 +1,73 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller; + +import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.EntityStorage; +import net.minecraft.world.level.chunk.storage.RegionFileStorage; +import net.minecraft.world.level.chunk.storage.RegionStorageInfo; +import java.io.IOException; +import java.nio.file.Path; + +public final class EntityDataController extends MoonriseRegionFileIO.RegionDataController { + + private final EntityRegionFileStorage storage; + + public EntityDataController(final EntityRegionFileStorage storage, final ChunkTaskScheduler taskScheduler) { + super(MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor); + this.storage = storage; + } + + @Override + public RegionFileStorage getCache() { + return this.storage; + } + + @Override + public WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { + checkPosition(new ChunkPos(chunkX, chunkZ), compound); + + return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$startWrite(chunkX, chunkZ, compound); + } + + @Override + public void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException { + ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishWrite(chunkX, chunkZ, writeData); + } + + @Override + public ReadData readData(final int chunkX, final int chunkZ) throws IOException { + return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$readData(chunkX, chunkZ); + } + + @Override + public CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException { + return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishRead(chunkX, chunkZ, readData); + } + + private static void checkPosition(final ChunkPos pos, final CompoundTag nbt) { + final ChunkPos nbtPos = nbt == null ? null : EntityStorage.readChunkPos(nbt); + if (nbtPos != null && !pos.equals(nbtPos)) { + throw new IllegalArgumentException( + "Entity chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos.toString() + + " but compound says coordinate is " + nbtPos + ); + } + } + + public static final class EntityRegionFileStorage extends RegionFileStorage { + + public EntityRegionFileStorage(final RegionStorageInfo regionStorageInfo, final Path directory, + final boolean dsync) { + super(regionStorageInfo, directory, dsync); + } + + @Override + public void write(final ChunkPos pos, final CompoundTag nbt) throws IOException { + checkPosition(pos, nbt); + super.write(pos, nbt); + } + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..bd0d782852f9cfe5bc0b5339ecf4d82c10332ec9 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java @@ -0,0 +1,45 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller; + +import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.storage.RegionFileStorage; +import java.io.IOException; + +public final class PoiDataController extends MoonriseRegionFileIO.RegionDataController { + + private final ServerLevel world; + + public PoiDataController(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { + super(MoonriseRegionFileIO.RegionFileType.POI_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor); + this.world = world; + } + + @Override + public RegionFileStorage getCache() { + return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$getRegionStorage(); + } + + @Override + public WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { + return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$startWrite(chunkX, chunkZ, compound); + } + + @Override + public void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException { + ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishWrite(chunkX, chunkZ, writeData); + } + + @Override + public ReadData readData(final int chunkX, final int chunkZ) throws IOException { + return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$readData(chunkX, chunkZ); + } + + @Override + public CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException { + return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishRead(chunkX, chunkZ, readData); + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java new file mode 100644 index 0000000000000000000000000000000000000000..47a4d3376d08dde94a39254bec21473ff27f53e6 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java @@ -0,0 +1,10 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level; + +import net.minecraft.world.level.ChunkPos; +import java.io.IOException; + +public interface ChunkSystemChunkMap { + + public void moonrise$writeFinishCallback(final ChunkPos pos) throws IOException; + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java new file mode 100644 index 0000000000000000000000000000000000000000..5d4d650186b18eb00782429d53d861564d8e4ba9 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java @@ -0,0 +1,33 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; + +public interface ChunkSystemLevel { + + public EntityLookup moonrise$getEntityLookup(); + + public void moonrise$setEntityLookup(final EntityLookup entityLookup); + + public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ); + + public ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ); + + public ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus); + + public void moonrise$midTickTasks(); + + public ChunkData moonrise$getChunkData(final long chunkKey); + + public ChunkData moonrise$getChunkData(final int chunkX, final int chunkZ); + + public ChunkData moonrise$requestChunkData(final long chunkKey); + + public ChunkData moonrise$releaseChunkData(final long chunkKey); + + public boolean moonrise$areChunksLoaded(final int fromX, final int fromZ, final int toX, final int toZ); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java new file mode 100644 index 0000000000000000000000000000000000000000..0b58701342d573fa43cdd06681534854a0e51d77 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java @@ -0,0 +1,10 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level; + +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; + +public interface ChunkSystemLevelReader { + + public ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java new file mode 100644 index 0000000000000000000000000000000000000000..c278f8ef806f0b45c28cc3040c7db052cb51e053 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java @@ -0,0 +1,62 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level; + +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.list.ReferenceList; +import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import java.util.List; +import java.util.function.Consumer; + +public interface ChunkSystemServerLevel extends ChunkSystemLevel { + + public ChunkTaskScheduler moonrise$getChunkTaskScheduler(); + + public MoonriseRegionFileIO.RegionDataController moonrise$getChunkDataController(); + + public MoonriseRegionFileIO.RegionDataController moonrise$getPoiChunkDataController(); + + public MoonriseRegionFileIO.RegionDataController moonrise$getEntityChunkDataController(); + + public int moonrise$getRegionChunkShift(); + + // Paper + + public RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader(); + + public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, + final Priority priority, + final Consumer> onLoad); + + public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, + final ChunkStatus chunkStatus, final Priority priority, + final Consumer> onLoad); + + public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, + final Priority priority, + final Consumer> onLoad); + + public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, + final ChunkStatus chunkStatus, final Priority priority, + final Consumer> onLoad); + + public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder(); + + public long moonrise$getLastMidTickFailure(); + + public void moonrise$setLastMidTickFailure(final long time); + + public NearbyPlayers moonrise$getNearbyPlayers(); + + public ReferenceList moonrise$getLoadedChunks(); + + public ReferenceList moonrise$getTickingChunks(); + + public ReferenceList moonrise$getEntityTickingChunks(); +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java new file mode 100644 index 0000000000000000000000000000000000000000..8b9dc582627b46843f4b5ea6f8c3df2d8cac46fa --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java @@ -0,0 +1,21 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; + +import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; + +public final class ChunkData { + + private int referenceCount = 0; + public NearbyPlayers.TrackedChunk nearbyPlayers; // Moonrise - nearby players + + public ChunkData() { + + } + + public int increaseRef() { + return ++this.referenceCount; + } + + public int decreaseRef() { + return --this.referenceCount; + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java new file mode 100644 index 0000000000000000000000000000000000000000..7d049d750df88762566f13a9c4fc7574a2df4825 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java @@ -0,0 +1,26 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; + +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.chunk.LevelChunk; +import java.util.List; + +public interface ChunkSystemChunkHolder { + + public NewChunkHolder moonrise$getRealChunkHolder(); + + public void moonrise$setRealChunkHolder(final NewChunkHolder newChunkHolder); + + public void moonrise$addReceivedChunk(final ServerPlayer player); + + public void moonrise$removeReceivedChunk(final ServerPlayer player); + + public boolean moonrise$hasChunkBeenSent(); + + public boolean moonrise$hasChunkBeenSent(final ServerPlayer to); + + public List moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge); + + public LevelChunk moonrise$getFullChunk(); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..f4bc44bb266763345c4e6f859c89352c769a104d --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java @@ -0,0 +1,26 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; + +import net.minecraft.world.level.chunk.status.ChunkStatus; +import java.util.concurrent.atomic.AtomicBoolean; + +public interface ChunkSystemChunkStatus { + + public boolean moonrise$isParallelCapable(); + + public void moonrise$setParallelCapable(final boolean value); + + public int moonrise$getWriteRadius(); + + public void moonrise$setWriteRadius(final int value); + + public ChunkStatus moonrise$getNextStatus(); + + public boolean moonrise$isEmptyLoadStatus(); + + public void moonrise$setEmptyLoadStatus(final boolean value); + + public boolean moonrise$isEmptyGenStatus(); + + public AtomicBoolean moonrise$getWarnedAboutNoImmediateComplete(); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java new file mode 100644 index 0000000000000000000000000000000000000000..aacd543f03b35908011d0c2891e978cc093ebcf5 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java @@ -0,0 +1,12 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; + +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; +import net.minecraft.server.level.ChunkMap; + +public interface ChunkSystemDistanceManager { + + public ChunkMap moonrise$getChunkMap(); + + public ChunkHolderManager moonrise$getChunkHolderManager(); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java new file mode 100644 index 0000000000000000000000000000000000000000..5b092bca7027e37aeee8f4b852ad896dd0d5febc --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java @@ -0,0 +1,13 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; + +import net.minecraft.server.level.ServerChunkCache; + +public interface ChunkSystemLevelChunk { + + public boolean moonrise$isPostProcessingDone(); + + public ServerChunkCache.ChunkAndHolder moonrise$getChunkAndHolder(); + + public void moonrise$setChunkAndHolder(final ServerChunkCache.ChunkAndHolder holder); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java new file mode 100644 index 0000000000000000000000000000000000000000..7aea4e343581b977d11af90f9f65eac3532eade1 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java @@ -0,0 +1,569 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity; + +import ca.spottedleaf.moonrise.common.PlatformHooks; +import ca.spottedleaf.moonrise.common.list.EntityList; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; +import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity; +import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.boss.EnderDragonPart; +import net.minecraft.world.entity.boss.enderdragon.EnderDragon; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.storage.EntityStorage; +import net.minecraft.world.level.entity.Visibility; +import net.minecraft.world.phys.AABB; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.function.Predicate; + +public final class ChunkEntitySlices { + + public final int minSection; + public final int maxSection; + public final int chunkX; + public final int chunkZ; + public final Level world; + + private final EntityCollectionBySection allEntities; + private final EntityCollectionBySection hardCollidingEntities; + private final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass; + private final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByType; + private final EntityList entities = new EntityList(); + + public FullChunkStatus status; + public final ChunkData chunkData; + + private boolean isTransient; + + public boolean isTransient() { + return this.isTransient; + } + + public void setTransient(final boolean value) { + this.isTransient = value; + } + + public ChunkEntitySlices(final Level world, final int chunkX, final int chunkZ, final FullChunkStatus status, + final ChunkData chunkData, final int minSection, final int maxSection) { // inclusive, inclusive + this.minSection = minSection; + this.maxSection = maxSection; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.world = world; + + this.allEntities = new EntityCollectionBySection(this); + this.hardCollidingEntities = new EntityCollectionBySection(this); + this.entitiesByClass = new Reference2ObjectOpenHashMap<>(); + this.entitiesByType = new Reference2ObjectOpenHashMap<>(); + + this.status = status; + this.chunkData = chunkData; + } + + public static List readEntities(final ServerLevel world, final CompoundTag compoundTag) { + // TODO check this and below on update for format changes + return EntityType.loadEntitiesRecursive(compoundTag.getList("Entities", 10), world, EntitySpawnReason.LOAD).collect(ImmutableList.toImmutableList()); + } + + // Paper start - rewrite chunk system + public static void copyEntities(final CompoundTag from, final CompoundTag into) { + if (from == null) { + return; + } + final ListTag entitiesFrom = from.getList("Entities", Tag.TAG_COMPOUND); + if (entitiesFrom == null || entitiesFrom.isEmpty()) { + return; + } + + final ListTag entitiesInto = into.getList("Entities", Tag.TAG_COMPOUND); + into.put("Entities", entitiesInto); // this is in case into doesn't have any entities + entitiesInto.addAll(0, entitiesFrom); + } + + public static CompoundTag saveEntityChunk(final List entities, final ChunkPos chunkPos, final ServerLevel world) { + return saveEntityChunk0(entities, chunkPos, world, false); + } + + public static CompoundTag saveEntityChunk0(final List entities, final ChunkPos chunkPos, final ServerLevel world, final boolean force) { + if (!force && entities.isEmpty()) { + return null; + } + + final ListTag entitiesTag = new ListTag(); + for (final Entity entity : PlatformHooks.get().modifySavedEntities(world, chunkPos.x, chunkPos.z, entities)) { + CompoundTag compoundTag = new CompoundTag(); + if (entity.save(compoundTag)) { + entitiesTag.add(compoundTag); + } + } + final CompoundTag ret = NbtUtils.addCurrentDataVersion(new CompoundTag()); + ret.put("Entities", entitiesTag); + EntityStorage.writeChunkPos(ret, chunkPos); + + return !force && entitiesTag.isEmpty() ? null : ret; + } + + public CompoundTag save() { + final int len = this.entities.size(); + if (len == 0) { + return null; + } + + final Entity[] rawData = this.entities.getRawData(); + final List collectedEntities = new ArrayList<>(len); + for (int i = 0; i < len; ++i) { + final Entity entity = rawData[i]; + if (entity.shouldBeSaved()) { + collectedEntities.add(entity); + } + } + + if (collectedEntities.isEmpty()) { + return null; + } + + return saveEntityChunk(collectedEntities, new ChunkPos(this.chunkX, this.chunkZ), (ServerLevel)this.world); + } + + // returns true if this chunk has transient entities remaining + public boolean unload() { + final int len = this.entities.size(); + final Entity[] collectedEntities = Arrays.copyOf(this.entities.getRawData(), len); + + for (int i = 0; i < len; ++i) { + final Entity entity = collectedEntities[i]; + if (entity.isRemoved()) { + // removed by us below + continue; + } + if (entity.shouldBeSaved()) { + PlatformHooks.get().unloadEntity(entity); + if (entity.isVehicle()) { + // we cannot assume that these entities are contained within this chunk, because entities can + // desync - so we need to remove them all + for (final Entity passenger : entity.getIndirectPassengers()) { + PlatformHooks.get().unloadEntity(passenger); + } + } + } + } + + return this.entities.size() != 0; + } + + public List getAllEntities() { + final int len = this.entities.size(); + if (len == 0) { + return new ArrayList<>(); + } + + final Entity[] rawData = this.entities.getRawData(); + final List collectedEntities = new ArrayList<>(len); + for (int i = 0; i < len; ++i) { + collectedEntities.add(rawData[i]); + } + + return collectedEntities; + } + + public boolean isEmpty() { + return this.entities.size() == 0; + } + + public void mergeInto(final ChunkEntitySlices slices) { + final Entity[] entities = this.entities.getRawData(); + for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { + final Entity entity = entities[i]; + slices.addEntity(entity, ((ChunkSystemEntity)entity).moonrise$getSectionY()); + } + } + + private boolean preventStatusUpdates; + public boolean startPreventingStatusUpdates() { + final boolean ret = this.preventStatusUpdates; + this.preventStatusUpdates = true; + return ret; + } + + public boolean isPreventingStatusUpdates() { + return this.preventStatusUpdates; + } + + public void stopPreventingStatusUpdates(final boolean prev) { + this.preventStatusUpdates = prev; + } + + public void updateStatus(final FullChunkStatus status, final EntityLookup lookup) { + this.status = status; + + final Entity[] entities = this.entities.getRawData(); + + for (int i = 0, size = this.entities.size(); i < size; ++i) { + final Entity entity = entities[i]; + + final Visibility oldVisibility = EntityLookup.getEntityStatus(entity); + ((ChunkSystemEntity)entity).moonrise$setChunkStatus(status); + final Visibility newVisibility = EntityLookup.getEntityStatus(entity); + + lookup.entityStatusChange(entity, this, oldVisibility, newVisibility, false, false, false); + } + } + + public boolean addEntity(final Entity entity, final int chunkSection) { + if (!this.entities.add(entity)) { + return false; + } + ((ChunkSystemEntity)entity).moonrise$setChunkStatus(this.status); + ((ChunkSystemEntity)entity).moonrise$setChunkData(this.chunkData); + final int sectionIndex = chunkSection - this.minSection; + + this.allEntities.addEntity(entity, sectionIndex); + + if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) { + this.hardCollidingEntities.addEntity(entity, sectionIndex); + } + + for (final Iterator, EntityCollectionBySection>> iterator = + this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); + + if (entry.getKey().isInstance(entity)) { + entry.getValue().addEntity(entity, sectionIndex); + } + } + + EntityCollectionBySection byType = this.entitiesByType.get(entity.getType()); + if (byType != null) { + byType.addEntity(entity, sectionIndex); + } else { + this.entitiesByType.put(entity.getType(), byType = new EntityCollectionBySection(this)); + byType.addEntity(entity, sectionIndex); + } + + return true; + } + + public boolean removeEntity(final Entity entity, final int chunkSection) { + if (!this.entities.remove(entity)) { + return false; + } + ((ChunkSystemEntity)entity).moonrise$setChunkStatus(null); + ((ChunkSystemEntity)entity).moonrise$setChunkData(null); + final int sectionIndex = chunkSection - this.minSection; + + this.allEntities.removeEntity(entity, sectionIndex); + + if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) { + this.hardCollidingEntities.removeEntity(entity, sectionIndex); + } + + for (final Iterator, EntityCollectionBySection>> iterator = + this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); + + if (entry.getKey().isInstance(entity)) { + entry.getValue().removeEntity(entity, sectionIndex); + } + } + + final EntityCollectionBySection byType = this.entitiesByType.get(entity.getType()); + byType.removeEntity(entity, sectionIndex); + + return true; + } + + public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + this.hardCollidingEntities.getEntities(except, box, into, predicate); + } + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + this.allEntities.getEntities(except, box, into, predicate); + } + + + public boolean getEntities(final Entity except, final AABB box, final List into, final Predicate predicate, + final int maxCount) { + return this.allEntities.getEntitiesLimited(except, box, into, predicate, maxCount); + } + + public void getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate) { + final EntityCollectionBySection byType = this.entitiesByType.get(type); + + if (byType != null) { + byType.getEntities((Entity)null, box, (List)into, (Predicate) predicate); + } + } + + public boolean getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate, final int maxCount) { + final EntityCollectionBySection byType = this.entitiesByType.get(type); + + if (byType != null) { + return byType.getEntitiesLimited((Entity)null, box, (List)into, (Predicate)predicate, maxCount); + } + + return false; + } + + protected EntityCollectionBySection initClass(final Class clazz) { + final EntityCollectionBySection ret = new EntityCollectionBySection(this); + + for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) { + final BasicEntityList sectionEntities = this.allEntities.entitiesBySection[sectionIndex]; + if (sectionEntities == null) { + continue; + } + + final Entity[] storage = sectionEntities.storage; + + for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (clazz.isInstance(entity)) { + ret.addEntity(entity, sectionIndex); + } + } + } + + return ret; + } + + public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, + final Predicate predicate) { + EntityCollectionBySection collection = this.entitiesByClass.get(clazz); + if (collection != null) { + collection.getEntities(except, box, (List)into, (Predicate)predicate); + } else { + this.entitiesByClass.put(clazz, collection = this.initClass(clazz)); + collection.getEntities(except, box, (List)into, (Predicate)predicate); + } + } + + public boolean getEntities(final Class clazz, final Entity except, final AABB box, final List into, + final Predicate predicate, final int maxCount) { + EntityCollectionBySection collection = this.entitiesByClass.get(clazz); + if (collection != null) { + return collection.getEntitiesLimited(except, box, (List)into, (Predicate)predicate, maxCount); + } else { + this.entitiesByClass.put(clazz, collection = this.initClass(clazz)); + return collection.getEntitiesLimited(except, box, (List)into, (Predicate)predicate, maxCount); + } + } + + private static final class BasicEntityList { + + private static final Entity[] EMPTY = new Entity[0]; + private static final int DEFAULT_CAPACITY = 4; + + private E[] storage; + private int size; + + public BasicEntityList() { + this(0); + } + + public BasicEntityList(final int cap) { + this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]); + } + + public boolean isEmpty() { + return this.size == 0; + } + + public int size() { + return this.size; + } + + private void resize() { + if (this.storage == EMPTY) { + this.storage = (E[])new Entity[DEFAULT_CAPACITY]; + } else { + this.storage = Arrays.copyOf(this.storage, this.storage.length * 2); + } + } + + public void add(final E entity) { + final int idx = this.size++; + if (idx >= this.storage.length) { + this.resize(); + this.storage[idx] = entity; + } else { + this.storage[idx] = entity; + } + } + + public int indexOf(final E entity) { + final E[] storage = this.storage; + + for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) { + if (storage[i] == entity) { + return i; + } + } + + return -1; + } + + public boolean remove(final E entity) { + final int idx = this.indexOf(entity); + if (idx == -1) { + return false; + } + + final int size = --this.size; + final E[] storage = this.storage; + if (idx != size) { + System.arraycopy(storage, idx + 1, storage, idx, size - idx); + } + + storage[size] = null; + + return true; + } + + public boolean has(final E entity) { + return this.indexOf(entity) != -1; + } + } + + private static final class EntityCollectionBySection { + + private final ChunkEntitySlices slices; + private final BasicEntityList[] entitiesBySection; + private int count; + + public EntityCollectionBySection(final ChunkEntitySlices slices) { + this.slices = slices; + + final int sectionCount = slices.maxSection - slices.minSection + 1; + + this.entitiesBySection = new BasicEntityList[sectionCount]; + } + + public void addEntity(final Entity entity, final int sectionIndex) { + BasicEntityList list = this.entitiesBySection[sectionIndex]; + + if (list != null && list.has(entity)) { + return; + } + + if (list == null) { + this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>(); + } + + list.add(entity); + ++this.count; + } + + public void removeEntity(final Entity entity, final int sectionIndex) { + final BasicEntityList list = this.entitiesBySection[sectionIndex]; + + if (list == null || !list.remove(entity)) { + return; + } + + --this.count; + + if (list.isEmpty()) { + this.entitiesBySection[sectionIndex] = null; + } + } + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + if (this.count == 0) { + return; + } + + final int minSection = this.slices.minSection; + final int maxSection = this.slices.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate != null && !predicate.test(entity)) { + continue; + } + + into.add(entity); + } + } + } + + public boolean getEntitiesLimited(final Entity except, final AABB box, final List into, final Predicate predicate, + final int maxCount) { + if (this.count == 0) { + return false; + } + + final int minSection = this.slices.minSection; + final int maxSection = this.slices.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate != null && !predicate.test(entity)) { + continue; + } + + into.add(entity); + if (into.size() >= maxCount) { + return true; + } + } + } + + return false; + } + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java new file mode 100644 index 0000000000000000000000000000000000000000..7554c109c35397bc1a43dd80e87764fd78645bbf --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java @@ -0,0 +1,1002 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity; + +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; +import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; +import ca.spottedleaf.moonrise.common.list.EntityList; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.util.AbortableIterationConsumer; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.entity.EntityInLevelCallback; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.entity.LevelCallback; +import net.minecraft.world.level.entity.LevelEntityGetter; +import net.minecraft.world.level.entity.Visibility; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public abstract class EntityLookup implements LevelEntityGetter { + + private static final Logger LOGGER = LoggerFactory.getLogger(EntityLookup.class); + + protected static final int REGION_SHIFT = 5; + protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1; + protected static final int REGION_SIZE = 1 << REGION_SHIFT; + + public final Level world; + + protected final SWMRLong2ObjectHashTable regions = new SWMRLong2ObjectHashTable<>(128, 0.5f); + + protected final LevelCallback worldCallback; + + protected final ConcurrentLong2ReferenceChainedHashTable entityById = new ConcurrentLong2ReferenceChainedHashTable<>(); + protected final ConcurrentHashMap entityByUUID = new ConcurrentHashMap<>(); + protected final EntityList accessibleEntities = new EntityList(); + + public EntityLookup(final Level world, final LevelCallback worldCallback) { + this.world = world; + this.worldCallback = worldCallback; + } + + protected abstract Boolean blockTicketUpdates(); + + protected abstract void setBlockTicketUpdates(final Boolean value); + + protected abstract void checkThread(final int chunkX, final int chunkZ, final String reason); + + protected abstract void checkThread(final Entity entity, final String reason); + + protected abstract ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk); + + protected abstract void onEmptySlices(final int chunkX, final int chunkZ); + + protected abstract void entitySectionChangeCallback( + final Entity entity, + final int oldSectionX, final int oldSectionY, final int oldSectionZ, + final int newSectionX, final int newSectionY, final int newSectionZ + ); + + protected abstract void addEntityCallback(final Entity entity); + + protected abstract void removeEntityCallback(final Entity entity); + + protected abstract void entityStartLoaded(final Entity entity); + + protected abstract void entityEndLoaded(final Entity entity); + + protected abstract void entityStartTicking(final Entity entity); + + protected abstract void entityEndTicking(final Entity entity); + + protected abstract boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event); + + private static Entity maskNonAccessible(final Entity entity) { + if (entity == null) { + return null; + } + final Visibility visibility = EntityLookup.getEntityStatus(entity); + return visibility.isAccessible() ? entity : null; + } + + @Override + public Entity get(final int id) { + return maskNonAccessible(this.entityById.get((long)id)); + } + + @Override + public Entity get(final UUID id) { + return maskNonAccessible(id == null ? null : this.entityByUUID.get(id)); + } + + public boolean hasEntity(final UUID uuid) { + return this.get(uuid) != null; + } + + public String getDebugInfo() { + return "count_id:" + this.entityById.size() + ",count_uuid:" + this.entityByUUID.size() + ",count_accessible:" + this.getEntityCount() + ",region_count:" + this.regions.size(); + } + + protected static final class ArrayIterable implements Iterable { + + private final T[] array; + private final int off; + private final int length; + + public ArrayIterable(final T[] array, final int off, final int length) { + this.array = array; + this.off = off; + this.length = length; + if (length > array.length) { + throw new IllegalArgumentException("Length must be no greater-than the array length"); + } + } + + @Override + public Iterator iterator() { + return new ArrayIterator<>(this.array, this.off, this.length); + } + + protected static final class ArrayIterator implements Iterator { + + private final T[] array; + private int off; + private final int length; + + public ArrayIterator(final T[] array, final int off, final int length) { + this.array = array; + this.off = off; + this.length = length; + } + + @Override + public boolean hasNext() { + return this.off < this.length; + } + + @Override + public T next() { + if (this.off >= this.length) { + throw new NoSuchElementException(); + } + return this.array[this.off++]; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + } + + @Override + public Iterable getAll() { + synchronized (this.accessibleEntities) { + final int len = this.accessibleEntities.size(); + final Entity[] cpy = Arrays.copyOf(this.accessibleEntities.getRawData(), len, Entity[].class); + + Objects.checkFromToIndex(0, len, cpy.length); + + return new ArrayIterable<>(cpy, 0, len); + } + } + + public int getEntityCount() { + synchronized (this.accessibleEntities) { + return this.accessibleEntities.size(); + } + } + + public Entity[] getAllCopy() { + synchronized (this.accessibleEntities) { + return Arrays.copyOf(this.accessibleEntities.getRawData(), this.accessibleEntities.size(), Entity[].class); + } + } + + @Override + public void get(final EntityTypeTest filter, final AbortableIterationConsumer action) { + for (final Iterator iterator = this.entityById.valueIterator(); iterator.hasNext();) { + final Entity entity = iterator.next(); + final Visibility visibility = EntityLookup.getEntityStatus(entity); + if (!visibility.isAccessible()) { + continue; + } + final U casted = filter.tryCast(entity); + if (casted != null && action.accept(casted).shouldAbort()) { + break; + } + } + } + + @Override + public void get(final AABB box, final Consumer action) { + List entities = new ArrayList<>(); + this.getEntities((Entity)null, box, entities, null); + for (int i = 0, len = entities.size(); i < len; ++i) { + action.accept(entities.get(i)); + } + } + + @Override + public void get(final EntityTypeTest filter, final AABB box, final AbortableIterationConsumer action) { + List entities = new ArrayList<>(); + this.getEntities((Entity)null, box, entities, null); + for (int i = 0, len = entities.size(); i < len; ++i) { + final U casted = filter.tryCast(entities.get(i)); + if (casted != null && action.accept(casted).shouldAbort()) { + break; + } + } + } + + public void entityStatusChange(final Entity entity, final ChunkEntitySlices slices, final Visibility oldVisibility, final Visibility newVisibility, final boolean moved, + final boolean created, final boolean destroyed) { + this.checkThread(entity, "Entity status change must only happen on the main thread"); + + if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) { + // recursive status update + LOGGER.error("Cannot recursively update entity chunk status for entity " + entity, new Throwable()); + return; + } + + final boolean entityStatusUpdateBefore = slices == null ? false : slices.startPreventingStatusUpdates(); + + if (entityStatusUpdateBefore) { + LOGGER.error("Cannot update chunk status for entity " + entity + " since entity chunk (" + slices.chunkX + "," + slices.chunkZ + ") is receiving update", new Throwable()); + return; + } + + try { + final Boolean ticketBlockBefore = this.blockTicketUpdates(); + try { + ((ChunkSystemEntity)entity).moonrise$setUpdatingSectionStatus(true); + try { + if (created) { + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onCreated(entity); + } + } + + if (oldVisibility == newVisibility) { + if (moved && newVisibility.isAccessible()) { + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onSectionChange(entity); + } + } + return; + } + + if (newVisibility.ordinal() > oldVisibility.ordinal()) { + // status upgrade + if (!oldVisibility.isAccessible() && newVisibility.isAccessible()) { + EntityLookup.this.entityStartLoaded(entity); + synchronized (this.accessibleEntities) { + this.accessibleEntities.add(entity); + } + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onTrackingStart(entity); + } + } + + if (!oldVisibility.isTicking() && newVisibility.isTicking()) { + EntityLookup.this.entityStartTicking(entity); + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onTickingStart(entity); + } + } + } else { + // status downgrade + if (oldVisibility.isTicking() && !newVisibility.isTicking()) { + EntityLookup.this.entityEndTicking(entity); + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onTickingEnd(entity); + } + } + + if (oldVisibility.isAccessible() && !newVisibility.isAccessible()) { + EntityLookup.this.entityEndLoaded(entity); + synchronized (this.accessibleEntities) { + this.accessibleEntities.remove(entity); + } + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onTrackingEnd(entity); + } + } + } + + if (moved && newVisibility.isAccessible()) { + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onSectionChange(entity); + } + } + + if (destroyed) { + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onDestroyed(entity); + } + } + } finally { + ((ChunkSystemEntity)entity).moonrise$setUpdatingSectionStatus(false); + } + } finally { + this.setBlockTicketUpdates(ticketBlockBefore); + } + } finally { + if (slices != null) { + slices.stopPreventingStatusUpdates(false); + } + } + } + + public void chunkStatusChange(final int x, final int z, final FullChunkStatus newStatus) { + this.getChunk(x, z).updateStatus(newStatus, this); + } + + public void addLegacyChunkEntities(final List entities, final ChunkPos forChunk) { + this.addEntityChunk(entities, forChunk, true); + } + + public void addEntityChunkEntities(final List entities, final ChunkPos forChunk) { + this.addEntityChunk(entities, forChunk, true); + } + + public void addWorldGenChunkEntities(final List entities, final ChunkPos forChunk) { + this.addEntityChunk(entities, forChunk, false); + } + + protected void addRecursivelySafe(final Entity root, final boolean fromDisk) { + if (!this.addEntity(root, fromDisk, true)) { + // possible we are a passenger, and so should dismount from any valid entity in the world + root.stopRiding(); + return; + } + for (final Entity passenger : root.getPassengers()) { + this.addRecursivelySafe(passenger, fromDisk); + } + } + + protected void addEntityChunk(final List entities, final ChunkPos forChunk, final boolean fromDisk) { + for (int i = 0, len = entities.size(); i < len; ++i) { + final Entity entity = entities.get(i); + if (entity.isPassenger()) { + continue; + } + + if (forChunk != null && !entity.chunkPosition().equals(forChunk)) { + LOGGER.warn("Root entity " + entity + " is outside of serialized chunk " + forChunk); + // can't set removed here, as we may not own the chunk position + // skip the entity + continue; + } + + final Vec3 rootPosition = entity.position(); + + // always adjust positions before adding passengers in case plugins access the entity, and so that + // they are added to the right entity chunk + for (final Entity passenger : entity.getIndirectPassengers()) { + if (forChunk != null && !passenger.chunkPosition().equals(forChunk)) { + passenger.setPosRaw(rootPosition.x, rootPosition.y, rootPosition.z); + } + } + + this.addRecursivelySafe(entity, fromDisk); + } + } + + public boolean addNewEntity(final Entity entity) { + return this.addNewEntity(entity, true); + } + + public boolean addNewEntity(final Entity entity, final boolean event) { + return this.addEntity(entity, false, event); + } + + public static Visibility getEntityStatus(final Entity entity) { + if (entity.isAlwaysTicking()) { + return Visibility.TICKING; + } + final FullChunkStatus entityStatus = ((ChunkSystemEntity)entity).moonrise$getChunkStatus(); + return Visibility.fromFullChunkStatus(entityStatus == null ? FullChunkStatus.INACCESSIBLE : entityStatus); + } + + protected boolean addEntity(final Entity entity, final boolean fromDisk, final boolean event) { + final BlockPos pos = entity.blockPosition(); + final int sectionX = pos.getX() >> 4; + final int sectionY = Mth.clamp(pos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); + final int sectionZ = pos.getZ() >> 4; + this.checkThread(sectionX, sectionZ, "Cannot add entity off-main thread"); + + if (entity.isRemoved()) { + LOGGER.warn("Refusing to add removed entity: " + entity); + return false; + } + + if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) { + LOGGER.warn("Entity " + entity + " is currently prevented from being added/removed to world since it is processing section status updates", new Throwable()); + return false; + } + + if (!this.screenEntity(entity, fromDisk, event)) { + return false; + } + + Entity currentlyMapped = this.entityById.putIfAbsent((long)entity.getId(), entity); + if (currentlyMapped != null) { + LOGGER.warn("Entity id already exists: " + entity.getId() + ", mapped to " + currentlyMapped + ", can't add " + entity); + return false; + } + + currentlyMapped = this.entityByUUID.putIfAbsent(entity.getUUID(), entity); + if (currentlyMapped != null) { + // need to remove mapping for id + this.entityById.remove((long)entity.getId(), entity); + LOGGER.warn("Entity uuid already exists: " + entity.getUUID() + ", mapped to " + currentlyMapped + ", can't add " + entity); + return false; + } + + ((ChunkSystemEntity)entity).moonrise$setSectionX(sectionX); + ((ChunkSystemEntity)entity).moonrise$setSectionY(sectionY); + ((ChunkSystemEntity)entity).moonrise$setSectionZ(sectionZ); + final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ); + if (!slices.addEntity(entity, sectionY)) { + LOGGER.warn("Entity " + entity + " added to world '" + WorldUtil.getWorldName(this.world) + "', but was already contained in entity chunk (" + sectionX + "," + sectionZ + ")"); + } + + entity.setLevelCallback(new EntityCallback(entity)); + + this.addEntityCallback(entity); + + this.entityStatusChange(entity, slices, Visibility.HIDDEN, getEntityStatus(entity), false, !fromDisk, false); + + return true; + } + + public boolean canRemoveEntity(final Entity entity) { + if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) { + return false; + } + + final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX(); + final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ(); + final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ); + return slices == null || !slices.isPreventingStatusUpdates(); + } + + protected void removeEntity(final Entity entity) { + final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX(); + final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY(); + final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ(); + this.checkThread(sectionX, sectionZ, "Cannot remove entity off-main"); + if (!entity.isRemoved()) { + throw new IllegalStateException("Only call Entity#setRemoved to remove an entity"); + } + final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ); + // all entities should be in a chunk + if (slices == null) { + LOGGER.warn("Cannot remove entity " + entity + " from null entity slices (" + sectionX + "," + sectionZ + ")"); + } else { + if (slices.isPreventingStatusUpdates()) { + throw new IllegalStateException("Attempting to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ") that is receiving status updates"); + } + if (!slices.removeEntity(entity, sectionY)) { + LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")"); + } + } + ((ChunkSystemEntity)entity).moonrise$setSectionX(Integer.MIN_VALUE); + ((ChunkSystemEntity)entity).moonrise$setSectionY(Integer.MIN_VALUE); + ((ChunkSystemEntity)entity).moonrise$setSectionZ(Integer.MIN_VALUE); + + + Entity currentlyMapped; + if ((currentlyMapped = this.entityById.remove(entity.getId(), entity)) != entity) { + LOGGER.warn("Failed to remove entity " + entity + " by id, current entity mapped: " + currentlyMapped); + } + + Entity[] currentlyMappedArr = new Entity[1]; + + // need reference equality + this.entityByUUID.compute(entity.getUUID(), (final UUID keyInMap, final Entity valueInMap) -> { + currentlyMappedArr[0] = valueInMap; + if (valueInMap != entity) { + return valueInMap; + } + return null; + }); + + if (currentlyMappedArr[0] != entity) { + LOGGER.warn("Failed to remove entity " + entity + " by uuid, current entity mapped: " + currentlyMappedArr[0]); + } + + if (slices != null && slices.isEmpty()) { + this.onEmptySlices(sectionX, sectionZ); + } + } + + protected ChunkEntitySlices moveEntity(final Entity entity) { + // ensure we own the entity + this.checkThread(entity, "Cannot move entity off-main"); + + final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX(); + final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY(); + final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ(); + final BlockPos newPos = entity.blockPosition(); + final int newSectionX = newPos.getX() >> 4; + final int newSectionY = Mth.clamp(newPos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); + final int newSectionZ = newPos.getZ() >> 4; + + if (newSectionX == sectionX && newSectionY == sectionY && newSectionZ == sectionZ) { + return null; + } + + // ensure the new section is owned by this tick thread + this.checkThread(newSectionX, newSectionZ, "Cannot move entity off-main"); + + // ensure the old section is owned by this tick thread + this.checkThread(sectionX, sectionZ, "Cannot move entity off-main"); + + final ChunkEntitySlices old = this.getChunk(sectionX, sectionZ); + final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ); + + if (!old.removeEntity(entity, sectionY)) { + LOGGER.warn("Could not remove entity " + entity + " from its old chunk section (" + sectionX + "," + sectionY + "," + sectionZ + ") since it was not contained in the section"); + } + + if (!slices.addEntity(entity, newSectionY)) { + LOGGER.warn("Could not add entity " + entity + " to its new chunk section (" + newSectionX + "," + newSectionY + "," + newSectionZ + ") as it is already contained in the section"); + } + + ((ChunkSystemEntity)entity).moonrise$setSectionX(newSectionX); + ((ChunkSystemEntity)entity).moonrise$setSectionY(newSectionY); + ((ChunkSystemEntity)entity).moonrise$setSectionZ(newSectionZ); + + if (old.isEmpty()) { + this.onEmptySlices(sectionX, sectionZ); + } + + this.entitySectionChangeCallback( + entity, + sectionX, sectionY, sectionZ, + newSectionX, newSectionY, newSectionZ + ); + + return slices; + } + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + chunk.getEntities(except, box, into, predicate); + } + } + } + } + } + + public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + chunk.getHardCollidingEntities(except, box, into, predicate); + } + } + } + } + } + + public void getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + chunk.getEntities(type, box, (List)into, (Predicate)predicate); + } + } + } + } + } + + public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, + final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + chunk.getEntities(clazz, except, box, into, predicate); + } + } + } + } + } + + //////// Limited //////// + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate, + final int maxCount) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + if (chunk.getEntities(except, box, into, predicate, maxCount)) { + return; + } + } + } + } + } + } + + public void getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate, final int maxCount) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + if (chunk.getEntities(type, box, (List)into, (Predicate)predicate, maxCount)) { + return; + } + } + } + } + } + } + + public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, + final Predicate predicate, final int maxCount) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + if (chunk.getEntities(clazz, except, box, into, predicate, maxCount)) { + return; + } + } + } + } + } + } + + public void entitySectionLoad(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { + this.checkThread(chunkX, chunkZ, "Cannot load in entity section off-main"); + synchronized (this) { + final ChunkEntitySlices curr = this.getChunk(chunkX, chunkZ); + if (curr != null) { + this.removeChunk(chunkX, chunkZ); + + curr.mergeInto(slices); + + this.addChunk(chunkX, chunkZ, slices); + } else { + this.addChunk(chunkX, chunkZ, slices); + } + } + } + + public void entitySectionUnload(final int chunkX, final int chunkZ) { + this.checkThread(chunkX, chunkZ, "Cannot unload entity section off-main"); + this.removeChunk(chunkX, chunkZ); + } + + public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) { + final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + if (region == null) { + return null; + } + + return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT)); + } + + public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) { + final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + final ChunkEntitySlices ret; + if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) { + return this.createEntityChunk(chunkX, chunkZ, true); + } + + return ret; + } + + public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) { + final long key = CoordinateUtils.getChunkKey(regionX, regionZ); + + return this.regions.get(key); + } + + protected synchronized void removeChunk(final int chunkX, final int chunkZ) { + final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); + + final ChunkSlicesRegion region = this.regions.get(key); + final int remaining = region.remove(relIndex); + + if (remaining == 0) { + this.regions.remove(key); + } + } + + public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { + final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); + + ChunkSlicesRegion region = this.regions.get(key); + if (region != null) { + region.add(relIndex, slices); + } else { + region = new ChunkSlicesRegion(); + region.add(relIndex, slices); + this.regions.put(key, region); + } + } + + public static final class ChunkSlicesRegion { + + private final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE]; + private int sliceCount; + + public ChunkEntitySlices get(final int index) { + return this.slices[index]; + } + + public int remove(final int index) { + final ChunkEntitySlices slices = this.slices[index]; + if (slices == null) { + throw new IllegalStateException(); + } + + this.slices[index] = null; + + return --this.sliceCount; + } + + public void add(final int index, final ChunkEntitySlices slices) { + final ChunkEntitySlices curr = this.slices[index]; + if (curr != null) { + throw new IllegalStateException(); + } + + this.slices[index] = slices; + + ++this.sliceCount; + } + } + + protected final class EntityCallback implements EntityInLevelCallback { + + public final Entity entity; + + public EntityCallback(final Entity entity) { + this.entity = entity; + } + + @Override + public void onMove() { + final Entity entity = this.entity; + final Visibility oldVisibility = getEntityStatus(entity); + final ChunkEntitySlices newSlices = EntityLookup.this.moveEntity(this.entity); + if (newSlices == null) { + // no new section, so didn't change sections + return; + } + + final Visibility newVisibility = getEntityStatus(entity); + + EntityLookup.this.entityStatusChange(entity, newSlices, oldVisibility, newVisibility, true, false, false); + } + + @Override + public void onRemove(final Entity.RemovalReason reason) { + final Entity entity = this.entity; + EntityLookup.this.checkThread(entity, "Cannot remove entity off-main"); + final Visibility tickingState = EntityLookup.getEntityStatus(entity); + + EntityLookup.this.removeEntity(entity); + + EntityLookup.this.entityStatusChange(entity, null, tickingState, Visibility.HIDDEN, false, false, reason.shouldDestroy()); + + EntityLookup.this.removeEntityCallback(entity); + + this.entity.setLevelCallback(NoOpCallback.INSTANCE); + } + } + + protected static final class NoOpCallback implements EntityInLevelCallback { + + public static final NoOpCallback INSTANCE = new NoOpCallback(); + + @Override + public void onMove() {} + + @Override + public void onRemove(final Entity.RemovalReason reason) {} + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java new file mode 100644 index 0000000000000000000000000000000000000000..a038215156a163b0b1cbc870ada5b4ac85ed1335 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java @@ -0,0 +1,129 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.client; + +import ca.spottedleaf.moonrise.common.PlatformHooks; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.entity.LevelCallback; + +public final class ClientEntityLookup extends EntityLookup { + + private final LongOpenHashSet tickingChunks = new LongOpenHashSet(); + + public ClientEntityLookup(final Level world, final LevelCallback worldCallback) { + super(world, worldCallback); + } + + @Override + protected Boolean blockTicketUpdates() { + // not present on client + return null; + } + + @Override + protected void setBlockTicketUpdates(Boolean value) { + // not present on client + } + + @Override + protected void checkThread(final int chunkX, final int chunkZ, final String reason) { + // TODO implement? + } + + @Override + protected void checkThread(final Entity entity, final String reason) { + // TODO implement? + } + + @Override + protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { + final boolean ticking = this.tickingChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + final ChunkEntitySlices ret = new ChunkEntitySlices( + this.world, chunkX, chunkZ, + ticking ? FullChunkStatus.ENTITY_TICKING : FullChunkStatus.FULL, null, + WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) + ); + + // note: not handled by superclass + this.addChunk(chunkX, chunkZ, ret); + + return ret; + } + + @Override + protected void onEmptySlices(final int chunkX, final int chunkZ) { + this.removeChunk(chunkX, chunkZ); + } + + @Override + protected void entitySectionChangeCallback(final Entity entity, + final int oldSectionX, final int oldSectionY, final int oldSectionZ, + final int newSectionX, final int newSectionY, final int newSectionZ) { + PlatformHooks.get().entityMove( + entity, + CoordinateUtils.getChunkSectionKey(oldSectionX, oldSectionY, oldSectionZ), + CoordinateUtils.getChunkSectionKey(newSectionX, newSectionY, newSectionZ) + ); + } + + @Override + protected void addEntityCallback(final Entity entity) { + + } + + @Override + protected void removeEntityCallback(final Entity entity) { + + } + + @Override + protected void entityStartLoaded(final Entity entity) { + + } + + @Override + protected void entityEndLoaded(final Entity entity) { + + } + + @Override + protected void entityStartTicking(final Entity entity) { + + } + + @Override + protected void entityEndTicking(final Entity entity) { + + } + + @Override + protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) { + return true; + } + + public void markTicking(final long pos) { + if (this.tickingChunks.add(pos)) { + final int chunkX = CoordinateUtils.getChunkX(pos); + final int chunkZ = CoordinateUtils.getChunkZ(pos); + if (this.getChunk(chunkX, chunkZ) != null) { + this.chunkStatusChange(chunkX, chunkZ, FullChunkStatus.ENTITY_TICKING); + } + } + } + + public void markNonTicking(final long pos) { + if (this.tickingChunks.remove(pos)) { + final int chunkX = CoordinateUtils.getChunkX(pos); + final int chunkZ = CoordinateUtils.getChunkZ(pos); + if (this.getChunk(chunkX, chunkZ) != null) { + this.chunkStatusChange(chunkX, chunkZ, FullChunkStatus.FULL); + } + } + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java new file mode 100644 index 0000000000000000000000000000000000000000..2ff58cf753c60913ee73aae015182e9c5560d529 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java @@ -0,0 +1,114 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.entity.LevelCallback; + +public final class DefaultEntityLookup extends EntityLookup { + public DefaultEntityLookup(final Level world) { + super(world, new DefaultLevelCallback()); + } + + @Override + protected Boolean blockTicketUpdates() { + return null; + } + + @Override + protected void setBlockTicketUpdates(final Boolean value) {} + + @Override + protected void checkThread(final int chunkX, final int chunkZ, final String reason) {} + + @Override + protected void checkThread(final Entity entity, final String reason) {} + + @Override + protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { + final ChunkEntitySlices ret = new ChunkEntitySlices( + this.world, chunkX, chunkZ, FullChunkStatus.FULL, + null, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) + ); + + // note: not handled by superclass + this.addChunk(chunkX, chunkZ, ret); + + return ret; + } + + @Override + protected void onEmptySlices(final int chunkX, final int chunkZ) { + this.removeChunk(chunkX, chunkZ); + } + + @Override + protected void entitySectionChangeCallback(final Entity entity, + final int oldSectionX, final int oldSectionY, final int oldSectionZ, + final int newSectionX, final int newSectionY, final int newSectionZ) { + + } + + @Override + protected void addEntityCallback(final Entity entity) { + + } + + @Override + protected void removeEntityCallback(final Entity entity) { + + } + + @Override + protected void entityStartLoaded(final Entity entity) { + + } + + @Override + protected void entityEndLoaded(final Entity entity) { + + } + + @Override + protected void entityStartTicking(final Entity entity) { + + } + + @Override + protected void entityEndTicking(final Entity entity) { + + } + + @Override + protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) { + return true; + } + + protected static final class DefaultLevelCallback implements LevelCallback { + + @Override + public void onCreated(final Entity entity) {} + + @Override + public void onDestroyed(final Entity entity) {} + + @Override + public void onTickingStart(final Entity entity) {} + + @Override + public void onTickingEnd(final Entity entity) {} + + @Override + public void onTrackingStart(final Entity entity) {} + + @Override + public void onTrackingEnd(final Entity entity) {} + + @Override + public void onSectionChange(final Entity entity) {} + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java new file mode 100644 index 0000000000000000000000000000000000000000..26207443b1223119c03db478d7e816d9cdf8e618 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java @@ -0,0 +1,115 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server; + +import ca.spottedleaf.moonrise.common.PlatformHooks; +import ca.spottedleaf.moonrise.common.list.ReferenceList; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.entity.LevelCallback; + +public final class ServerEntityLookup extends EntityLookup { + + private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0]; + + private final ServerLevel serverWorld; + public final ReferenceList trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker + + public ServerEntityLookup(final ServerLevel world, final LevelCallback worldCallback) { + super(world, worldCallback); + this.serverWorld = world; + } + + @Override + protected Boolean blockTicketUpdates() { + return ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager.blockTicketUpdates(); + } + + @Override + protected void setBlockTicketUpdates(final Boolean value) { + ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager.unblockTicketUpdates(value); + } + + @Override + protected void checkThread(final int chunkX, final int chunkZ, final String reason) { + TickThread.ensureTickThread(this.serverWorld, chunkX, chunkZ, reason); + } + + @Override + protected void checkThread(final Entity entity, final String reason) { + TickThread.ensureTickThread(entity, reason); + } + + @Override + protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { + // loadInEntityChunk will call addChunk for us + return ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager + .getOrCreateEntityChunk(chunkX, chunkZ, transientChunk); + } + + @Override + protected void onEmptySlices(final int chunkX, final int chunkZ) { + // entity slices unloading is managed by ticket levels in chunk system + } + + @Override + protected void entitySectionChangeCallback(final Entity entity, + final int oldSectionX, final int oldSectionY, final int oldSectionZ, + final int newSectionX, final int newSectionY, final int newSectionZ) { + if (entity instanceof ServerPlayer player) { + ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().tickPlayer(player); + } + PlatformHooks.get().entityMove( + entity, + CoordinateUtils.getChunkSectionKey(oldSectionX, oldSectionY, oldSectionZ), + CoordinateUtils.getChunkSectionKey(newSectionX, newSectionY, newSectionZ) + ); + } + + @Override + protected void addEntityCallback(final Entity entity) { + if (entity instanceof ServerPlayer player) { + ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().addPlayer(player); + } + } + + @Override + protected void removeEntityCallback(final Entity entity) { + if (entity instanceof ServerPlayer player) { + ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().removePlayer(player); + } + } + + @Override + protected void entityStartLoaded(final Entity entity) { + // Moonrise start - entity tracker + this.trackerEntities.add(entity); + // Moonrise end - entity tracker + } + + @Override + protected void entityEndLoaded(final Entity entity) { + // Moonrise start - entity tracker + this.trackerEntities.remove(entity); + // Moonrise end - entity tracker + } + + @Override + protected void entityStartTicking(final Entity entity) { + + } + + @Override + protected void entityEndTicking(final Entity entity) { + + } + + @Override + protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) { + return PlatformHooks.get().screenEntity(this.serverWorld, entity, fromDisk, event); + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java new file mode 100644 index 0000000000000000000000000000000000000000..458d1fc5e1222912512e6c59b56f6fca347d9ee9 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java @@ -0,0 +1,17 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.poi; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkAccess; + +public interface ChunkSystemPoiManager extends ChunkSystemSectionStorage { + + public ServerLevel moonrise$getWorld(); + + public void moonrise$onUnload(final long coordinate); + + public void moonrise$loadInPoiChunk(final PoiChunk poiChunk); + + public void moonrise$checkConsistency(final ChunkAccess chunk); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java new file mode 100644 index 0000000000000000000000000000000000000000..89b956b8fdf1a0d862a843104511005e2990a897 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java @@ -0,0 +1,12 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.poi; + +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import java.util.Optional; + +public interface ChunkSystemPoiSection { + + public boolean moonrise$isEmpty(); + + public Optional moonrise$asOptional(); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java new file mode 100644 index 0000000000000000000000000000000000000000..bbf9d6c1c9525d97160806819a57be03eca290f1 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java @@ -0,0 +1,204 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.poi; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import com.mojang.serialization.DataResult; +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.RegistryOps; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.Optional; + +public final class PoiChunk { + + private static final Logger LOGGER = LoggerFactory.getLogger(PoiChunk.class); + + public final ServerLevel world; + public final int chunkX; + public final int chunkZ; + public final int minSection; + public final int maxSection; + + private final PoiSection[] sections; + + private boolean isDirty; + private boolean loaded; + + public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection) { + this(world, chunkX, chunkZ, minSection, maxSection, new PoiSection[maxSection - minSection + 1]); + } + + public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection, final PoiSection[] sections) { + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.minSection = minSection; + this.maxSection = maxSection; + this.sections = sections; + if (this.sections.length != (maxSection - minSection + 1)) { + throw new IllegalStateException("Incorrect length used, expected " + (maxSection - minSection + 1) + ", got " + this.sections.length); + } + } + + public void load() { + TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Loading in poi chunk off-main"); + if (this.loaded) { + return; + } + this.loaded = true; + ((ChunkSystemPoiManager)this.world.getChunkSource().getPoiManager()).moonrise$loadInPoiChunk(this); + } + + public boolean isLoaded() { + return this.loaded; + } + + public boolean isEmpty() { + for (final PoiSection section : this.sections) { + if (section != null && !((ChunkSystemPoiSection)section).moonrise$isEmpty()) { + return false; + } + } + + return true; + } + + public PoiSection getOrCreateSection(final int chunkY) { + if (chunkY >= this.minSection && chunkY <= this.maxSection) { + final int idx = chunkY - this.minSection; + final PoiSection ret = this.sections[idx]; + if (ret != null) { + return ret; + } + + final PoiManager poiManager = this.world.getPoiManager(); + final long key = CoordinateUtils.getChunkSectionKey(this.chunkX, chunkY, this.chunkZ); + + return this.sections[idx] = new PoiSection(() -> { + poiManager.setDirty(key); + }); + } + throw new IllegalArgumentException("chunkY is out of bounds, chunkY: " + chunkY + " outside [" + this.minSection + "," + this.maxSection + "]"); + } + + public PoiSection getSection(final int chunkY) { + if (chunkY >= this.minSection && chunkY <= this.maxSection) { + return this.sections[chunkY - this.minSection]; + } + return null; + } + + public Optional getSectionForVanilla(final int chunkY) { + if (chunkY >= this.minSection && chunkY <= this.maxSection) { + final PoiSection ret = this.sections[chunkY - this.minSection]; + return ret == null ? Optional.empty() : ((ChunkSystemPoiSection)ret).moonrise$asOptional(); + } + return Optional.empty(); + } + + public boolean isDirty() { + return this.isDirty; + } + + public void setDirty(final boolean dirty) { + this.isDirty = dirty; + } + + // returns null if empty + public CompoundTag save() { + final RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, this.world.registryAccess()); + + final CompoundTag ret = new CompoundTag(); + final CompoundTag sections = new CompoundTag(); + ret.put("Sections", sections); + + ret.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion()); + + final ServerLevel world = this.world; + final int chunkX = this.chunkX; + final int chunkZ = this.chunkZ; + + for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { + final PoiSection section = this.sections[sectionY - this.minSection]; + if (section == null || ((ChunkSystemPoiSection)section).moonrise$isEmpty()) { + continue; + } + + // I do not believe asynchronously converting to CompoundTag is worth the scheduling. + final DataResult serializedResult = PoiSection.Packed.CODEC.encodeStart(registryOps, section.pack()); + final int finalSectionY = sectionY; + final Tag serialized = serializedResult.resultOrPartial((final String description) -> { + LOGGER.error("Failed to serialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); + }).orElse(null); + if (serialized == null) { + // failed, should be logged from the resultOrPartial + continue; + } + + sections.put(Integer.toString(sectionY), serialized); + } + + return sections.isEmpty() ? null : ret; + } + + public static PoiChunk empty(final ServerLevel world, final int chunkX, final int chunkZ) { + final PoiChunk ret = new PoiChunk(world, chunkX, chunkZ, WorldUtil.getMinSection(world), WorldUtil.getMaxSection(world)); + ret.loaded = true; + return ret; + } + + public static PoiChunk parse(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data) { + final PoiChunk ret = empty(world, chunkX, chunkZ); + + final RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, world.registryAccess()); + + final CompoundTag sections = data.getCompound("Sections"); + + if (sections.isEmpty()) { + // nothing to parse + return ret; + } + + final PoiManager poiManager = world.getPoiManager(); + + boolean readAnything = false; + + for (int sectionY = ret.minSection; sectionY <= ret.maxSection; ++sectionY) { + final String key = Integer.toString(sectionY); + if (!sections.contains(key)) { + continue; + } + + final CompoundTag section = sections.getCompound(key); + final DataResult deserializeResult = PoiSection.Packed.CODEC.parse(registryOps, section); + final int finalSectionY = sectionY; + final PoiSection.Packed packed = deserializeResult.resultOrPartial((final String description) -> { + LOGGER.error("Failed to deserialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); + }).orElse(null); + + final long coordinateKey = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ); + final PoiSection deserialized = packed == null ? null : packed.unpack(() -> { + poiManager.setDirty(coordinateKey); + }); + + if (deserialized == null || ((ChunkSystemPoiSection)deserialized).moonrise$isEmpty()) { + // completely empty, no point in storing this + continue; + } + + readAnything = true; + ret.sections[sectionY - ret.minSection] = deserialized; + } + + ret.loaded = !readAnything; // Set loaded to false if we read anything to ensure proper callbacks to PoiManager are made on #load + + return ret; + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..524752744e37a2db0e3ea089468bdf497129bfef --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java @@ -0,0 +1,13 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.storage; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.chunk.storage.RegionFileStorage; +import java.io.IOException; + +public interface ChunkSystemSectionStorage { + + public RegionFileStorage moonrise$getRegionStorage(); + + public void moonrise$close() throws IOException; + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java new file mode 100644 index 0000000000000000000000000000000000000000..003a857e70ead858e8437e3c1bfaf22f4daba0df --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java @@ -0,0 +1,15 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.player; + +public interface ChunkSystemServerPlayer { + + public boolean moonrise$isRealPlayer(); + + public void moonrise$setRealPlayer(final boolean real); + + public RegionizedPlayerChunkLoader.PlayerChunkLoaderData moonrise$getChunkLoader(); + + public void moonrise$setChunkLoader(final RegionizedPlayerChunkLoader.PlayerChunkLoaderData loader); + + public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder(); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..dd2509996bfd08e8c3f9f2be042229eac6d7692d --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java @@ -0,0 +1,1092 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.player; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.PlatformHooks; +import ca.spottedleaf.moonrise.common.misc.AllocatingRateLimiter; +import ca.spottedleaf.moonrise.common.misc.SingleUserAreaMap; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.MoonriseConstants; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.util.ParallelSearchRadiusIteration; +import com.google.gson.JsonObject; +import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongComparator; +import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket; +import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket; +import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket; +import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket; +import net.minecraft.server.level.ChunkTrackingView; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; +import net.minecraft.server.network.PlayerChunkSender; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.BelowZeroRetrogen; +import java.lang.invoke.VarHandle; +import java.util.ArrayDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; + +public final class RegionizedPlayerChunkLoader { + + public static final TicketType PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo); + public static final TicketType PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 5 * 20); + + public static final int MIN_VIEW_DISTANCE = 2; + public static final int MAX_VIEW_DISTANCE = 32; + + public static final int GENERATED_TICKET_LEVEL = ChunkHolderManager.FULL_LOADED_TICKET_LEVEL; + public static final int LOADED_TICKET_LEVEL = ChunkTaskScheduler.getTicketLevel(ChunkStatus.EMPTY); + public static final int TICK_TICKET_LEVEL = ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL; + + public static final class ViewDistanceHolder { + + private volatile ViewDistances viewDistances; + private static final VarHandle VIEW_DISTANCES_HANDLE = ConcurrentUtil.getVarHandle(ViewDistanceHolder.class, "viewDistances", ViewDistances.class); + + public ViewDistanceHolder() { + VIEW_DISTANCES_HANDLE.setVolatile(this, new ViewDistances(-1, -1, -1)); + } + + public ViewDistances getViewDistances() { + return (ViewDistances)VIEW_DISTANCES_HANDLE.getVolatile(this); + } + + public ViewDistances compareAndExchangeViewDistance(final ViewDistances expect, final ViewDistances update) { + return (ViewDistances)VIEW_DISTANCES_HANDLE.compareAndExchange(this, expect, update); + } + + public void updateViewDistance(final Function update) { + int failures = 0; + for (ViewDistances curr = this.getViewDistances();;) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = this.compareAndExchangeViewDistance(curr, update.apply(curr)))) { + return; + } + ++failures; + } + } + + public void setTickViewDistance(final int distance) { + this.updateViewDistance((final ViewDistances param) -> { + return param.setTickViewDistance(distance); + }); + } + + public void setLoadViewDistance(final int distance) { + this.updateViewDistance((final ViewDistances param) -> { + return param.setLoadViewDistance(distance); + }); + } + + public void setSendViewDistance(final int distance) { + this.updateViewDistance((final ViewDistances param) -> { + return param.setSendViewDistance(distance); + }); + } + + public JsonObject toJson() { + return this.getViewDistances().toJson(); + } + } + + public static final record ViewDistances( + int tickViewDistance, + int loadViewDistance, + int sendViewDistance + ) { + public ViewDistances setTickViewDistance(final int distance) { + if (distance != -1 && (distance < (0) || distance > (MoonriseConstants.MAX_VIEW_DISTANCE))) { + throw new IllegalArgumentException(Integer.toString(distance)); + } + return new ViewDistances(distance, this.loadViewDistance, this.sendViewDistance); + } + + public ViewDistances setLoadViewDistance(final int distance) { + // note: load view distance = api view distance + 1 + if (distance != -1 && (distance < (2 + 1) || distance > (MoonriseConstants.MAX_VIEW_DISTANCE + 1))) { + throw new IllegalArgumentException(Integer.toString(distance)); + } + return new ViewDistances(this.tickViewDistance, distance, this.sendViewDistance); + } + + public ViewDistances setSendViewDistance(final int distance) { + // note: send view distance <= load view distance - 1 + if (distance != -1 && (distance < (0) || distance > (MoonriseConstants.MAX_VIEW_DISTANCE))) { + throw new IllegalArgumentException(Integer.toString(distance)); + } + return new ViewDistances(this.tickViewDistance, this.loadViewDistance, distance); + } + + public JsonObject toJson() { + final JsonObject ret = new JsonObject(); + + ret.addProperty("tick-view-distance", this.tickViewDistance); + ret.addProperty("load-view-distance", this.loadViewDistance); + ret.addProperty("send-view-distance", this.sendViewDistance); + + return ret; + } + } + + public static int getAPITickViewDistance(final ServerPlayer player) { + final ServerLevel level = player.serverLevel(); + final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (data == null) { + return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPITickDistance(); + } + return data.lastTickDistance; + } + + public static int getAPIViewDistance(final ServerPlayer player) { + final ServerLevel level = player.serverLevel(); + final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (data == null) { + return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPIViewDistance(); + } + // view distance = load distance + 1 + return data.lastLoadDistance - 1; + } + + public static int getAPISendViewDistance(final ServerPlayer player) { + final ServerLevel level = player.serverLevel(); + final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (data == null) { + return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPISendViewDistance(); + } + return data.lastSendDistance; + } + + private final ServerLevel world; + + public RegionizedPlayerChunkLoader(final ServerLevel world) { + this.world = world; + } + + public void addPlayer(final ServerPlayer player) { + TickThread.ensureTickThread(player, "Cannot add player to player chunk loader async"); + if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) { + return; + } + + if (((ChunkSystemServerPlayer)player).moonrise$getChunkLoader() != null) { + throw new IllegalStateException("Player is already added to player chunk loader"); + } + + final PlayerChunkLoaderData loader = new PlayerChunkLoaderData(this.world, player); + + ((ChunkSystemServerPlayer)player).moonrise$setChunkLoader(loader); + loader.add(); + } + + public void updatePlayer(final ServerPlayer player) { + final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (loader != null) { + loader.update(); + // update view distances for nearby players + ((ChunkSystemServerLevel)loader.world).moonrise$getNearbyPlayers().tickPlayer(player); + } + } + + public void removePlayer(final ServerPlayer player) { + TickThread.ensureTickThread(player, "Cannot remove player from player chunk loader async"); + if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) { + return; + } + + final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + + if (loader == null) { + return; + } + + loader.remove(); + ((ChunkSystemServerPlayer)player).moonrise$setChunkLoader(null); + } + + public void setSendDistance(final int distance) { + ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setSendViewDistance(distance); + } + + public void setLoadDistance(final int distance) { + ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setLoadViewDistance(distance); + } + + public void setTickDistance(final int distance) { + ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setTickViewDistance(distance); + } + + // Note: follow the player chunk loader so everything stays consistent... + public int getAPITickDistance() { + final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances(); + final int tickViewDistance = PlayerChunkLoaderData.getTickDistance( + -1, distances.tickViewDistance, + -1, distances.loadViewDistance + ); + return tickViewDistance; + } + + public int getAPIViewDistance() { + final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances(); + final int tickViewDistance = PlayerChunkLoaderData.getTickDistance( + -1, distances.tickViewDistance, + -1, distances.loadViewDistance + ); + final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance); + + // loadDistance = api view distance + 1 + return loadDistance - 1; + } + + public int getAPISendViewDistance() { + final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances(); + final int tickViewDistance = PlayerChunkLoaderData.getTickDistance( + -1, distances.tickViewDistance, + -1, distances.loadViewDistance + ); + final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance); + final int sendViewDistance = PlayerChunkLoaderData.getSendViewDistance( + loadDistance, -1, -1, distances.sendViewDistance + ); + + return sendViewDistance; + } + + public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ, final boolean borderOnly) { + return borderOnly ? this.isChunkSentBorderOnly(player, chunkX, chunkZ) : this.isChunkSent(player, chunkX, chunkZ); + } + + public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) { + final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (loader == null) { + return false; + } + + return loader.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } + + public boolean isChunkSentBorderOnly(final ServerPlayer player, final int chunkX, final int chunkZ) { + final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (loader == null) { + return false; + } + + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + if (!loader.sentChunks.contains(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ))) { + return true; + } + } + } + + return false; + } + + public void tick() { + TickThread.ensureTickThread("Cannot tick player chunk loader async"); + long currTime = System.nanoTime(); + for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) { + final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (loader == null || loader.removed || loader.world != this.world) { + // not our problem anymore + continue; + } + loader.update(); // can't invoke plugin logic + loader.updateQueues(currTime); + } + } + + public static final class PlayerChunkLoaderData { + + private static final AtomicLong ID_GENERATOR = new AtomicLong(); + private final long id = ID_GENERATOR.incrementAndGet(); + private final Long idBoxed = Long.valueOf(this.id); + + private static final long MAX_RATE = 10_000L; + + private final ServerPlayer player; + private final ServerLevel world; + + private int lastChunkX = Integer.MIN_VALUE; + private int lastChunkZ = Integer.MIN_VALUE; + + private int lastSendDistance = Integer.MIN_VALUE; + private int lastLoadDistance = Integer.MIN_VALUE; + private int lastTickDistance = Integer.MIN_VALUE; + + private int lastSentChunkCenterX = Integer.MIN_VALUE; + private int lastSentChunkCenterZ = Integer.MIN_VALUE; + + private int lastSentChunkRadius = Integer.MIN_VALUE; + private int lastSentSimulationDistance = Integer.MIN_VALUE; + + private boolean canGenerateChunks = true; + + private final ArrayDeque> delayedTicketOps = new ArrayDeque<>(); + private final LongOpenHashSet sentChunks = new LongOpenHashSet(); + + private static final byte CHUNK_TICKET_STAGE_NONE = 0; + private static final byte CHUNK_TICKET_STAGE_LOADING = 1; + private static final byte CHUNK_TICKET_STAGE_LOADED = 2; + private static final byte CHUNK_TICKET_STAGE_GENERATING = 3; + private static final byte CHUNK_TICKET_STAGE_GENERATED = 4; + private static final byte CHUNK_TICKET_STAGE_TICK = 5; + private static final int[] TICKET_STAGE_TO_LEVEL = new int[] { + ChunkHolderManager.MAX_TICKET_LEVEL + 1, + LOADED_TICKET_LEVEL, + LOADED_TICKET_LEVEL, + GENERATED_TICKET_LEVEL, + GENERATED_TICKET_LEVEL, + TICK_TICKET_LEVEL + }; + private final Long2ByteOpenHashMap chunkTicketStage = new Long2ByteOpenHashMap(); + { + this.chunkTicketStage.defaultReturnValue(CHUNK_TICKET_STAGE_NONE); + } + + // rate limiting + private static final long ALLOCATION_GRANULARITY = TimeUnit.SECONDS.toNanos(1L); + private final AllocatingRateLimiter chunkSendLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY); + private final AllocatingRateLimiter chunkLoadTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY); + private final AllocatingRateLimiter chunkGenerateTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY); + + // queues + private final LongComparator CLOSEST_MANHATTAN_DIST = (final long c1, final long c2) -> { + final int c1x = CoordinateUtils.getChunkX(c1); + final int c1z = CoordinateUtils.getChunkZ(c1); + + final int c2x = CoordinateUtils.getChunkX(c2); + final int c2z = CoordinateUtils.getChunkZ(c2); + + final int centerX = PlayerChunkLoaderData.this.lastChunkX; + final int centerZ = PlayerChunkLoaderData.this.lastChunkZ; + + return Integer.compare( + Math.abs(c1x - centerX) + Math.abs(c1z - centerZ), + Math.abs(c2x - centerX) + Math.abs(c2z - centerZ) + ); + }; + private final LongHeapPriorityQueue sendQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + private final LongHeapPriorityQueue tickingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + private final LongHeapPriorityQueue generatingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + private final LongHeapPriorityQueue genQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + private final LongHeapPriorityQueue loadingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + private final LongHeapPriorityQueue loadQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + + private volatile boolean removed; + + public PlayerChunkLoaderData(final ServerLevel world, final ServerPlayer player) { + this.world = world; + this.player = player; + } + + private void flushDelayedTicketOps() { + if (this.delayedTicketOps.isEmpty()) { + return; + } + ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.performTicketUpdates(this.delayedTicketOps); + this.delayedTicketOps.clear(); + } + + private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation op) { + this.delayedTicketOps.addLast(op); + } + + private void sendChunk(final int chunkX, final int chunkZ) { + if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { + ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager + .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$addReceivedChunk(this.player); + + final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ); + + PlatformHooks.get().onChunkWatch(this.world, chunk, this.player); + PlayerChunkSender.sendChunk(this.player.connection, this.world, chunk); + return; + } + throw new IllegalStateException(); + } + + private void sendUnloadChunk(final int chunkX, final int chunkZ) { + if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { + return; + } + this.sendUnloadChunkRaw(chunkX, chunkZ); + } + + private void sendUnloadChunkRaw(final int chunkX, final int chunkZ) { + PlatformHooks.get().onChunkUnWatch(this.world, new ChunkPos(chunkX, chunkZ), this.player); + // Note: Check PlayerChunkSender#dropChunk for other logic + // Note: drop isAlive() check so that chunks properly unload client-side when the player dies + ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager + .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$removeReceivedChunk(this.player); + this.player.connection.send(new ClientboundForgetLevelChunkPacket(new ChunkPos(chunkX, chunkZ))); + // Paper start - PlayerChunkUnloadEvent + if (io.papermc.paper.event.packet.PlayerChunkUnloadEvent.getHandlerList().getRegisteredListeners().length > 0) { + new io.papermc.paper.event.packet.PlayerChunkUnloadEvent(player.getBukkitEntity().getWorld().getChunkAt(new ChunkPos(chunkX, chunkZ).longKey), player.getBukkitEntity()).callEvent(); + } + // Paper end - PlayerChunkUnloadEvent + } + + private final SingleUserAreaMap broadcastMap = new SingleUserAreaMap<>(this) { + @Override + protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { + // do nothing, we only care about remove + } + + @Override + protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { + parameter.sendUnloadChunk(chunkX, chunkZ); + } + }; + private final SingleUserAreaMap loadTicketCleanup = new SingleUserAreaMap<>(this) { + @Override + protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { + // do nothing, we only care about remove + } + + @Override + protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { + final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); + final byte ticketStage = parameter.chunkTicketStage.remove(chunk); + final int level = TICKET_STAGE_TO_LEVEL[ticketStage]; + if (level > ChunkHolderManager.MAX_TICKET_LEVEL) { + return; + } + + parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove( + chunk, + PLAYER_TICKET_DELAYED, level, parameter.idBoxed, + PLAYER_TICKET, level, parameter.idBoxed + )); + } + }; + private final SingleUserAreaMap tickMap = new SingleUserAreaMap<>(this) { + @Override + protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { + // do nothing, we will detect ticking chunks when we try to load them + } + + @Override + protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { + final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); + // note: by the time this is called, the tick cleanup should have ran - so, if the chunk is at + // the tick stage it was deemed in range for loading. Thus, we need to move it to generated + if (!parameter.chunkTicketStage.replace(chunk, CHUNK_TICKET_STAGE_TICK, CHUNK_TICKET_STAGE_GENERATED)) { + return; + } + + // Since we are possibly downgrading the ticket level, we add the delayed unload ticket so that + // the level is kept for a short period of time + parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove( + chunk, + PLAYER_TICKET_DELAYED, TICK_TICKET_LEVEL, parameter.idBoxed, + PLAYER_TICKET, TICK_TICKET_LEVEL, parameter.idBoxed + )); + // keep chunk at new generated level + parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addOp( + chunk, PLAYER_TICKET, GENERATED_TICKET_LEVEL, parameter.idBoxed + )); + } + }; + + private static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ, + final int sendRadius) { + // expect sendRadius to be = 1 + target viewable radius + return ChunkTrackingView.isWithinDistance(centerX, centerZ, sendRadius, chunkX, chunkZ, true); + } + + private static int getClientViewDistance(final ServerPlayer player) { + final Integer vd = player.requestedViewDistance(); + return vd == null ? -1 : Math.max(0, vd.intValue()); + } + + private static int getTickDistance(final int playerTickViewDistance, final int worldTickViewDistance, + final int playerLoadViewDistance, final int worldLoadViewDistance) { + return Math.min( + playerTickViewDistance < 0 ? worldTickViewDistance : playerTickViewDistance, + playerLoadViewDistance < 0 ? (worldLoadViewDistance - 1) : (playerLoadViewDistance - 1) + ); + } + + private static int getLoadViewDistance(final int tickViewDistance, final int playerLoadViewDistance, + final int worldLoadViewDistance) { + return Math.max(tickViewDistance + 1, playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance); + } + + private static int getSendViewDistance(final int loadViewDistance, final int clientViewDistance, + final int playerSendViewDistance, final int worldSendViewDistance) { + return Math.min( + loadViewDistance - 1, + playerSendViewDistance < 0 ? (!PlatformHooks.get().configAutoConfigSendDistance() || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance + ); + } + + private Packet updateClientChunkRadius(final int radius) { + this.lastSentChunkRadius = radius; + return new ClientboundSetChunkCacheRadiusPacket(radius); + } + + private Packet updateClientSimulationDistance(final int distance) { + this.lastSentSimulationDistance = distance; + return new ClientboundSetSimulationDistancePacket(distance); + } + + private Packet updateClientChunkCenter(final int chunkX, final int chunkZ) { + this.lastSentChunkCenterX = chunkX; + this.lastSentChunkCenterZ = chunkZ; + return new ClientboundSetChunkCacheCenterPacket(chunkX, chunkZ); + } + + private boolean canPlayerGenerateChunks() { + return !this.player.isSpectator() || this.world.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS); + } + + private double getMaxChunkLoadRate() { + final double configRate = PlatformHooks.get().configPlayerMaxLoadRate(); + + return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); + } + + private double getMaxChunkGenRate() { + final double configRate = PlatformHooks.get().configPlayerMaxGenRate(); + + return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); + } + + private double getMaxChunkSendRate() { + final double configRate = PlatformHooks.get().configPlayerMaxSendRate(); + + return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); + } + + private long getMaxChunkLoads() { + final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L); + long configLimit = (long)PlatformHooks.get().configPlayerMaxConcurrentLoads(); + if (configLimit == 0L) { + // by default, only allow 1/5th of the chunks in the view distance to be concurrently active + configLimit = Math.max(5L, radiusChunks / 5L); + } else if (configLimit < 0L) { + configLimit = Integer.MAX_VALUE; + } // else: use the value configured + configLimit = configLimit - this.loadingQueue.size(); + + return configLimit; + } + + private long getMaxChunkGenerates() { + final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L); + long configLimit = (long)PlatformHooks.get().configPlayerMaxConcurrentGens(); + if (configLimit == 0L) { + // by default, only allow 1/5th of the chunks in the view distance to be concurrently active + configLimit = Math.max(5L, radiusChunks / 5L); + } else if (configLimit < 0L) { + configLimit = Integer.MAX_VALUE; + } // else: use the value configured + configLimit = configLimit - this.generatingQueue.size(); + + return configLimit; + } + + private boolean wantChunkSent(final int chunkX, final int chunkZ) { + final int dx = this.lastChunkX - chunkX; + final int dz = this.lastChunkZ - chunkZ; + return (Math.max(Math.abs(dx), Math.abs(dz)) <= (this.lastSendDistance + 1)) && wantChunkLoaded( + this.lastChunkX, this.lastChunkZ, chunkX, chunkZ, this.lastSendDistance + ); + } + + private boolean wantChunkTicked(final int chunkX, final int chunkZ) { + final int dx = this.lastChunkX - chunkX; + final int dz = this.lastChunkZ - chunkZ; + return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance; + } + + private boolean areNeighboursGenerated(final int chunkX, final int chunkZ, final int radius) { + for (int dz = -radius; dz <= radius; ++dz) { + for (int dx = -radius; dx <= radius; ++dx) { + if ((dx | dz) == 0) { + continue; + } + + final long neighbour = CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ); + final byte stage = this.chunkTicketStage.get(neighbour); + + if (stage != CHUNK_TICKET_STAGE_GENERATED && stage != CHUNK_TICKET_STAGE_TICK) { + return false; + } + } + } + + return true; + } + + void updateQueues(final long time) { + TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async"); + if (this.removed) { + throw new IllegalStateException("Ticking removed player chunk loader"); + } + // update rate limits + final double loadRate = this.getMaxChunkLoadRate(); + final double genRate = this.getMaxChunkGenRate(); + final double sendRate = this.getMaxChunkSendRate(); + + this.chunkLoadTicketLimiter.tickAllocation(time, loadRate, loadRate); + this.chunkGenerateTicketLimiter.tickAllocation(time, genRate, genRate); + this.chunkSendLimiter.tickAllocation(time, sendRate, sendRate); + + // try to progress chunk loads + while (!this.loadingQueue.isEmpty()) { + final long pendingLoadChunk = this.loadingQueue.firstLong(); + final int pendingChunkX = CoordinateUtils.getChunkX(pendingLoadChunk); + final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingLoadChunk); + final ChunkAccess pending = ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(pendingChunkX, pendingChunkZ); + if (pending == null) { + // nothing to do here + break; + } + // chunk has loaded, so we can take it out of the queue + this.loadingQueue.dequeueLong(); + + // try to move to generate queue + final byte prev = this.chunkTicketStage.put(pendingLoadChunk, CHUNK_TICKET_STAGE_LOADED); + if (prev != CHUNK_TICKET_STAGE_LOADING) { + throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADING + ", not " + prev); + } + + if (this.canGenerateChunks || this.isLoadedChunkGeneratable(pending)) { + this.genQueue.enqueue(pendingLoadChunk); + } // else: don't want to generate, so just leave it loaded + } + + // try to push more chunk loads + final long maxLoads = Math.max(0L, Math.min(MAX_RATE, Math.min(this.loadQueue.size(), this.getMaxChunkLoads()))); + final int maxLoadsThisTick = (int)this.chunkLoadTicketLimiter.takeAllocation(time, loadRate, maxLoads); + if (maxLoadsThisTick > 0) { + final LongArrayList chunks = new LongArrayList(maxLoadsThisTick); + for (int i = 0; i < maxLoadsThisTick; ++i) { + final long chunk = this.loadQueue.dequeueLong(); + final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_LOADING); + if (prev != CHUNK_TICKET_STAGE_NONE) { + throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_NONE + ", not " + prev); + } + this.pushDelayedTicketOp( + ChunkHolderManager.TicketOperation.addOp( + chunk, + PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed + ) + ); + chunks.add(chunk); + this.loadingQueue.enqueue(chunk); + } + + // here we need to flush tickets, as scheduleChunkLoad requires tickets to be propagated with addTicket = false + this.flushDelayedTicketOps(); + // we only need to call scheduleChunkLoad because the loaded ticket level is not enough to start the chunk + // load - only generate ticket levels start anything, but they start generation... + // propagate levels + // Note: this CAN call plugin logic, so it is VITAL that our bookkeeping logic is completely done by the time this is invoked + ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); + + if (this.removed) { + // process ticket updates may invoke plugin logic, which may remove this player + return; + } + + for (int i = 0; i < maxLoadsThisTick; ++i) { + final long queuedLoadChunk = chunks.getLong(i); + final int queuedChunkX = CoordinateUtils.getChunkX(queuedLoadChunk); + final int queuedChunkZ = CoordinateUtils.getChunkZ(queuedLoadChunk); + ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkLoad( + queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, Priority.NORMAL, null + ); + if (this.removed) { + return; + } + } + } + + // try to progress chunk generations + while (!this.generatingQueue.isEmpty()) { + final long pendingGenChunk = this.generatingQueue.firstLong(); + final int pendingChunkX = CoordinateUtils.getChunkX(pendingGenChunk); + final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingGenChunk); + final LevelChunk pending = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(pendingChunkX, pendingChunkZ); + if (pending == null) { + // nothing to do here + break; + } + + // chunk has generated, so we can take it out of queue + this.generatingQueue.dequeueLong(); + + final byte prev = this.chunkTicketStage.put(pendingGenChunk, CHUNK_TICKET_STAGE_GENERATED); + if (prev != CHUNK_TICKET_STAGE_GENERATING) { + throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATING + ", not " + prev); + } + + // try to move to send queue + if (this.wantChunkSent(pendingChunkX, pendingChunkZ)) { + this.sendQueue.enqueue(pendingGenChunk); + } + // try to move to tick queue + if (this.wantChunkTicked(pendingChunkX, pendingChunkZ)) { + this.tickingQueue.enqueue(pendingGenChunk); + } + } + + // try to push more chunk generations + final long maxGens = Math.max(0L, Math.min(MAX_RATE, Math.min(this.genQueue.size(), this.getMaxChunkGenerates()))); + // preview the allocations, as we may not actually utilise all of them + final long maxGensThisTick = this.chunkGenerateTicketLimiter.previewAllocation(time, genRate, maxGens); + long ratedGensThisTick = 0L; + while (!this.genQueue.isEmpty()) { + final long chunkKey = this.genQueue.firstLong(); + final int chunkX = CoordinateUtils.getChunkX(chunkKey); + final int chunkZ = CoordinateUtils.getChunkZ(chunkKey); + final ChunkAccess chunk = ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ); + if (chunk.getPersistedStatus() != ChunkStatus.FULL) { + // only rate limit actual generations + if ((ratedGensThisTick + 1L) > maxGensThisTick) { + break; + } + ++ratedGensThisTick; + } + + this.genQueue.dequeueLong(); + + final byte prev = this.chunkTicketStage.put(chunkKey, CHUNK_TICKET_STAGE_GENERATING); + if (prev != CHUNK_TICKET_STAGE_LOADED) { + throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADED + ", not " + prev); + } + this.pushDelayedTicketOp( + ChunkHolderManager.TicketOperation.addAndRemove( + chunkKey, + PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed, + PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed + ) + ); + this.generatingQueue.enqueue(chunkKey); + } + // take the allocations we actually used + this.chunkGenerateTicketLimiter.takeAllocation(time, genRate, ratedGensThisTick); + + // try to pull ticking chunks + while (!this.tickingQueue.isEmpty()) { + final long pendingTicking = this.tickingQueue.firstLong(); + final int pendingChunkX = CoordinateUtils.getChunkX(pendingTicking); + final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingTicking); + + if (!this.areNeighboursGenerated(pendingChunkX, pendingChunkZ, + ChunkHolderManager.FULL_LOADED_TICKET_LEVEL - ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL)) { + break; + } + + // only gets here if all neighbours were marked as generated or ticking themselves + this.tickingQueue.dequeueLong(); + this.pushDelayedTicketOp( + ChunkHolderManager.TicketOperation.addAndRemove( + pendingTicking, + PLAYER_TICKET, TICK_TICKET_LEVEL, this.idBoxed, + PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed + ) + ); + // note: there is no queue to add after ticking + final byte prev = this.chunkTicketStage.put(pendingTicking, CHUNK_TICKET_STAGE_TICK); + if (prev != CHUNK_TICKET_STAGE_GENERATED) { + throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATED + ", not " + prev); + } + } + + // try to pull sending chunks + final long maxSends = Math.max(0L, Math.min(MAX_RATE, Integer.MAX_VALUE)); // note: no logic to track concurrent sends + final int maxSendsThisTick = Math.min((int)this.chunkSendLimiter.takeAllocation(time, sendRate, maxSends), this.sendQueue.size()); + // we do not return sends that we took from the allocation back because we want to limit the max send rate, not target it + for (int i = 0; i < maxSendsThisTick; ++i) { + final long pendingSend = this.sendQueue.firstLong(); + final int pendingSendX = CoordinateUtils.getChunkX(pendingSend); + final int pendingSendZ = CoordinateUtils.getChunkZ(pendingSend); + final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(pendingSendX, pendingSendZ); + if (!this.areNeighboursGenerated(pendingSendX, pendingSendZ, 1) || !TickThread.isTickThreadFor(this.world, pendingSendX, pendingSendZ)) { + // nothing to do + // the target chunk may not be owned by this region, but this should be resolved in the future + break; + } + if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) { + // not yet post-processed, need to do this so that tile entities can properly be sent to clients + chunk.postProcessGeneration(this.world); + // check if there was any recursive action + if (this.removed || this.sendQueue.isEmpty() || this.sendQueue.firstLong() != pendingSend) { + return; + } // else: good to dequeue and send, fall through + } + this.sendQueue.dequeueLong(); + + this.sendChunk(pendingSendX, pendingSendZ); + + if (this.removed) { + // sendChunk may invoke plugin logic + return; + } + } + + this.flushDelayedTicketOps(); + } + + void add() { + TickThread.ensureTickThread(this.player, "Cannot add player asynchronously"); + if (this.removed) { + throw new IllegalStateException("Adding removed player chunk loader"); + } + final ViewDistances playerDistances = ((ChunkSystemServerPlayer)this.player).moonrise$getViewDistanceHolder().getViewDistances(); + final ViewDistances worldDistances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances(); + final int chunkX = this.player.chunkPosition().x; + final int chunkZ = this.player.chunkPosition().z; + + final int tickViewDistance = getTickDistance( + playerDistances.tickViewDistance, worldDistances.tickViewDistance, + playerDistances.loadViewDistance, worldDistances.loadViewDistance + ); + // load view cannot be less-than tick view + 1 + final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance); + // send view cannot be greater-than load view + final int clientViewDistance = getClientViewDistance(this.player); + final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance); + + // send view distances + this.player.connection.send(this.updateClientChunkRadius(sendViewDistance)); + this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance)); + + // add to distance maps + this.broadcastMap.add(chunkX, chunkZ, sendViewDistance + 1); + this.loadTicketCleanup.add(chunkX, chunkZ, loadViewDistance + 1); + this.tickMap.add(chunkX, chunkZ, tickViewDistance); + + // update chunk center + this.player.connection.send(this.updateClientChunkCenter(chunkX, chunkZ)); + + // reset limiters, they will start at a zero allocation + final long time = System.nanoTime(); + this.chunkLoadTicketLimiter.reset(time); + this.chunkGenerateTicketLimiter.reset(time); + this.chunkSendLimiter.reset(time); + + // now we can update + this.update(); + } + + private boolean isLoadedChunkGeneratable(final int chunkX, final int chunkZ) { + return this.isLoadedChunkGeneratable(((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ)); + } + + private boolean isLoadedChunkGeneratable(final ChunkAccess chunkAccess) { + final BelowZeroRetrogen belowZeroRetrogen; + // see PortalForcer#findPortalAround + return chunkAccess != null && ( + chunkAccess.getPersistedStatus() == ChunkStatus.FULL || + ((belowZeroRetrogen = chunkAccess.getBelowZeroRetrogen()) != null && belowZeroRetrogen.targetStatus().isOrAfter(ChunkStatus.SPAWN)) + ); + } + + void update() { + TickThread.ensureTickThread(this.player, "Cannot update player asynchronously"); + if (this.removed) { + throw new IllegalStateException("Updating removed player chunk loader"); + } + final ViewDistances playerDistances = ((ChunkSystemServerPlayer)this.player).moonrise$getViewDistanceHolder().getViewDistances(); + final ViewDistances worldDistances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances(); + + final int tickViewDistance = getTickDistance( + playerDistances.tickViewDistance, worldDistances.tickViewDistance, + playerDistances.loadViewDistance, worldDistances.loadViewDistance + ); + // load view cannot be less-than tick view + 1 + final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance); + // send view cannot be greater-than load view + final int clientViewDistance = getClientViewDistance(this.player); + final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance); + + final ChunkPos playerPos = this.player.chunkPosition(); + final boolean canGenerateChunks = this.canPlayerGenerateChunks(); + final int currentChunkX = playerPos.x; + final int currentChunkZ = playerPos.z; + + final int prevChunkX = this.lastChunkX; + final int prevChunkZ = this.lastChunkZ; + + if ( + // has view distance stayed the same? + sendViewDistance == this.lastSendDistance + && loadViewDistance == this.lastLoadDistance + && tickViewDistance == this.lastTickDistance + + // has our chunk stayed the same? + && prevChunkX == currentChunkX + && prevChunkZ == currentChunkZ + + // can we still generate chunks? + && this.canGenerateChunks == canGenerateChunks + ) { + // nothing we care about changed, so we're not re-calculating + return; + } + + // update distance maps + this.broadcastMap.update(currentChunkX, currentChunkZ, sendViewDistance + 1); + this.loadTicketCleanup.update(currentChunkX, currentChunkZ, loadViewDistance + 1); + this.tickMap.update(currentChunkX, currentChunkZ, tickViewDistance); + if (sendViewDistance > loadViewDistance || tickViewDistance > loadViewDistance) { + throw new IllegalStateException(); + } + + // update VDs for client + // this should be after the distance map updates, as they will send unload packets + if (this.lastSentChunkRadius != sendViewDistance) { + this.player.connection.send(this.updateClientChunkRadius(sendViewDistance)); + } + if (this.lastSentSimulationDistance != tickViewDistance) { + this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance)); + } + + this.sendQueue.clear(); + this.tickingQueue.clear(); + this.generatingQueue.clear(); + this.genQueue.clear(); + this.loadingQueue.clear(); + this.loadQueue.clear(); + + this.lastChunkX = currentChunkX; + this.lastChunkZ = currentChunkZ; + this.lastSendDistance = sendViewDistance; + this.lastLoadDistance = loadViewDistance; + this.lastTickDistance = tickViewDistance; + this.canGenerateChunks = canGenerateChunks; + + // +1 since we need to load chunks +1 around the load view distance... + final long[] toIterate = ParallelSearchRadiusIteration.getSearchIteration(loadViewDistance + 1); + // the iteration order is by increasing manhattan distance - so, we do NOT need to + // sort anything in the queue! + for (final long deltaChunk : toIterate) { + final int dx = CoordinateUtils.getChunkX(deltaChunk); + final int dz = CoordinateUtils.getChunkZ(deltaChunk); + final int chunkX = dx + currentChunkX; + final int chunkZ = dz + currentChunkZ; + final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); + final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); + final int manhattanDistance = Math.abs(dx) + Math.abs(dz); + + // since chunk sending is not by radius alone, we need an extra check here to account for + // everything <= sendDistance + // Note: Vanilla may want to send chunks outside the send view distance, so we do need + // the dist <= view check + final boolean sendChunk = (squareDistance <= (sendViewDistance + 1)) + && wantChunkLoaded(currentChunkX, currentChunkZ, chunkX, chunkZ, sendViewDistance); + final boolean sentChunk = sendChunk ? this.sentChunks.contains(chunk) : this.sentChunks.remove(chunk); + + if (!sendChunk && sentChunk) { + // have sent the chunk, but don't want it anymore + // unload it now + this.sendUnloadChunkRaw(chunkX, chunkZ); + } + + final byte stage = this.chunkTicketStage.get(chunk); + switch (stage) { + case CHUNK_TICKET_STAGE_NONE: { + // we want the chunk to be at least loaded + this.loadQueue.enqueue(chunk); + break; + } + case CHUNK_TICKET_STAGE_LOADING: { + this.loadingQueue.enqueue(chunk); + break; + } + case CHUNK_TICKET_STAGE_LOADED: { + if (canGenerateChunks || this.isLoadedChunkGeneratable(chunkX, chunkZ)) { + this.genQueue.enqueue(chunk); + } + break; + } + case CHUNK_TICKET_STAGE_GENERATING: { + this.generatingQueue.enqueue(chunk); + break; + } + case CHUNK_TICKET_STAGE_GENERATED: { + if (sendChunk && !sentChunk) { + this.sendQueue.enqueue(chunk); + } + if (squareDistance <= tickViewDistance) { + this.tickingQueue.enqueue(chunk); + } + break; + } + case CHUNK_TICKET_STAGE_TICK: { + if (sendChunk && !sentChunk) { + this.sendQueue.enqueue(chunk); + } + break; + } + default: { + throw new IllegalStateException("Unknown stage: " + stage); + } + } + } + + // update the chunk center + // this must be done last so that the client does not ignore any of our unload chunk packets above + if (this.lastSentChunkCenterX != currentChunkX || this.lastSentChunkCenterZ != currentChunkZ) { + this.player.connection.send(this.updateClientChunkCenter(currentChunkX, currentChunkZ)); + } + + this.flushDelayedTicketOps(); + } + + void remove() { + TickThread.ensureTickThread(this.player, "Cannot add player asynchronously"); + if (this.removed) { + throw new IllegalStateException("Removing removed player chunk loader"); + } + this.removed = true; + // sends the chunk unload packets + this.broadcastMap.remove(); + // cleans up loading/generating tickets + this.loadTicketCleanup.remove(); + // cleans up ticking tickets + this.tickMap.remove(); + + // purge queues + this.sendQueue.clear(); + this.tickingQueue.clear(); + this.generatingQueue.clear(); + this.genQueue.clear(); + this.loadingQueue.clear(); + this.loadQueue.clear(); + + // flush ticket changes + this.flushDelayedTicketOps(); + + // now all tickets should be removed, which is all of our external state + } + + public LongOpenHashSet getSentChunksRaw() { + return this.sentChunks; + } + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..7eafc5b7cba23d8dec92ecc1050afe3fd8c9e309 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java @@ -0,0 +1,144 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.queue; + +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +public final class ChunkUnloadQueue { + + public final int coordinateShift; + private final AtomicLong orderGenerator = new AtomicLong(); + private final ConcurrentLong2ReferenceChainedHashTable unloadSections = new ConcurrentLong2ReferenceChainedHashTable<>(); + + /* + * Note: write operations do not occur in parallel for any given section. + * Note: coordinateShift <= region shift in order for retrieveForCurrentRegion() to function correctly + */ + + public ChunkUnloadQueue(final int coordinateShift) { + this.coordinateShift = coordinateShift; + } + + public static record SectionToUnload(int sectionX, int sectionZ, long order, int count) {} + + public List retrieveForAllRegions() { + final List ret = new ArrayList<>(); + + for (final Iterator> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) { + final ConcurrentLong2ReferenceChainedHashTable.TableEntry entry = iterator.next(); + final long key = entry.getKey(); + final UnloadSection section = entry.getValue(); + final int sectionX = CoordinateUtils.getChunkX(key); + final int sectionZ = CoordinateUtils.getChunkZ(key); + + ret.add(new SectionToUnload(sectionX, sectionZ, section.order, section.chunks.size())); + } + + ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> { + return Long.compare(s1.order, s2.order); + }); + + return ret; + } + + public UnloadSection getSectionUnsynchronized(final int sectionX, final int sectionZ) { + return this.unloadSections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ)); + } + + public UnloadSection removeSection(final int sectionX, final int sectionZ) { + return this.unloadSections.remove(CoordinateUtils.getChunkKey(sectionX, sectionZ)); + } + + // write operation + public boolean addChunk(final int chunkX, final int chunkZ) { + // write operations do not occur in parallel for a given section + final int shift = this.coordinateShift; + final int sectionX = chunkX >> shift; + final int sectionZ = chunkZ >> shift; + final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + UnloadSection section = this.unloadSections.get(sectionKey); + if (section == null) { + section = new UnloadSection(this.orderGenerator.getAndIncrement()); + this.unloadSections.put(sectionKey, section); + } + + return section.chunks.add(chunkKey); + } + + // write operation + public boolean removeChunk(final int chunkX, final int chunkZ) { + // write operations do not occur in parallel for a given section + final int shift = this.coordinateShift; + final int sectionX = chunkX >> shift; + final int sectionZ = chunkZ >> shift; + final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + final UnloadSection section = this.unloadSections.get(sectionKey); + + if (section == null) { + return false; + } + + if (!section.chunks.remove(chunkKey)) { + return false; + } + + if (section.chunks.isEmpty()) { + this.unloadSections.remove(sectionKey); + } + + return true; + } + + public JsonElement toDebugJson() { + final JsonArray ret = new JsonArray(); + + for (final SectionToUnload section : this.retrieveForAllRegions()) { + final JsonObject sectionJson = new JsonObject(); + ret.add(sectionJson); + + sectionJson.addProperty("sectionX", section.sectionX()); + sectionJson.addProperty("sectionZ", section.sectionX()); + sectionJson.addProperty("order", section.order()); + + final JsonArray coordinates = new JsonArray(); + sectionJson.add("coordinates", coordinates); + + final UnloadSection actualSection = this.getSectionUnsynchronized(section.sectionX(), section.sectionZ()); + if (actualSection != null) { + for (final LongIterator iterator = actualSection.chunks.clone().iterator(); iterator.hasNext(); ) { + final long coordinate = iterator.nextLong(); + + final JsonObject coordinateJson = new JsonObject(); + coordinates.add(coordinateJson); + + coordinateJson.addProperty("chunkX", Integer.valueOf(CoordinateUtils.getChunkX(coordinate))); + coordinateJson.addProperty("chunkZ", Integer.valueOf(CoordinateUtils.getChunkZ(coordinate))); + } + } + } + + return ret; + } + + public static final class UnloadSection { + + public final long order; + public final LongLinkedOpenHashSet chunks = new LongLinkedOpenHashSet(); + + public UnloadSection(final long order) { + this.order = order; + } + } +} \ No newline at end of file diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java new file mode 100644 index 0000000000000000000000000000000000000000..b5817aa8f537593f6d9fc6b612c82ccccb250ac7 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java @@ -0,0 +1,1456 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; + +import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.PlatformHooks; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; +import ca.spottedleaf.moonrise.patches.chunk_system.queue.ChunkUnloadQueue; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.GenericDataLoadTask; +import ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket; +import ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.mojang.logging.LogUtils; +import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ByteMap; +import it.unimi.dsi.fastutil.longs.Long2IntMap; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.Ticket; +import net.minecraft.server.level.TicketType; +import net.minecraft.util.SortedArraySet; +import net.minecraft.util.Unit; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.LevelChunk; +import org.slf4j.Logger; +import java.io.IOException; +import java.text.DecimalFormat; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.PrimitiveIterator; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.LockSupport; +import java.util.function.Predicate; + +public final class ChunkHolderManager { + + private static final Logger LOGGER = LogUtils.getClassLogger(); + + public static final int FULL_LOADED_TICKET_LEVEL = ChunkLevel.FULL_CHUNK_LEVEL; + public static final int BLOCK_TICKING_TICKET_LEVEL = ChunkLevel.BLOCK_TICKING_LEVEL; + public static final int ENTITY_TICKING_TICKET_LEVEL = ChunkLevel.ENTITY_TICKING_LEVEL; + public static final int MAX_TICKET_LEVEL = ChunkLevel.MAX_LEVEL; // inclusive + + public static final TicketType UNLOAD_COOLDOWN = TicketType.create("unload_cooldown", (u1, u2) -> 0, 5 * 20); + + private static final long NO_TIMEOUT_MARKER = Long.MIN_VALUE; + private static final long PROBE_MARKER = Long.MIN_VALUE + 1; + public final ReentrantAreaLock ticketLockArea; + + private final ConcurrentLong2ReferenceChainedHashTable>> tickets = new ConcurrentLong2ReferenceChainedHashTable<>(); + private final ConcurrentLong2ReferenceChainedHashTable sectionToChunkToExpireCount = new ConcurrentLong2ReferenceChainedHashTable<>(); + final ChunkUnloadQueue unloadQueue; + + private final ConcurrentLong2ReferenceChainedHashTable chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f); + private final ServerLevel world; + private final ChunkTaskScheduler taskScheduler; + private long currentTick; + + private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); + private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { + if (c1 == c2) { + return 0; + } + + final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); + + if (saveTickCompare != 0) { + return saveTickCompare; + } + + final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); + final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); + + if (coord1 == coord2) { + throw new IllegalStateException("Duplicate chunkholder in auto save queue"); + } + + return Long.compare(coord1, coord2); + }); + + public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { + this.world = world; + this.taskScheduler = taskScheduler; + this.ticketLockArea = new ReentrantAreaLock(taskScheduler.getChunkSystemLockShift()); + this.unloadQueue = new ChunkUnloadQueue(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift()); + } + + public boolean processTicketUpdates(final int posX, final int posZ) { + final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT; + final int ticketMask = (1 << ticketShift) - 1; + final List scheduledTasks = new ArrayList<>(); + final List changedFullStatus = new ArrayList<>(); + final boolean ret; + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( + ((posX >> ticketShift) - 1) << ticketShift, + ((posZ >> ticketShift) - 1) << ticketShift, + (((posX >> ticketShift) + 1) << ticketShift) | ticketMask, + (((posZ >> ticketShift) + 1) << ticketShift) | ticketMask + ); + try { + ret = this.processTicketUpdatesNoLock(posX >> ticketShift, posZ >> ticketShift, scheduledTasks, changedFullStatus); + } finally { + this.ticketLockArea.unlock(ticketLock); + } + + this.addChangedStatuses(changedFullStatus); + + for (int i = 0, len = scheduledTasks.size(); i < len; ++i) { + scheduledTasks.get(i).schedule(); + } + + return ret; + } + + private boolean processTicketUpdatesNoLock(final int sectionX, final int sectionZ, final List scheduledTasks, + final List changedFullStatus) { + return this.ticketLevelPropagator.performUpdate( + sectionX, sectionZ, this.taskScheduler.schedulingLockArea, scheduledTasks, changedFullStatus + ); + } + + public List getOldChunkHolders() { + final List ret = new ArrayList<>(this.chunkHolders.size() + 1); + for (final Iterator iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) { + ret.add(iterator.next().vanillaChunkHolder); + } + return ret; + } + + public List getChunkHolders() { + final List ret = new ArrayList<>(this.chunkHolders.size() + 1); + for (final Iterator iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) { + ret.add(iterator.next()); + } + return ret; + } + + public int size() { + return this.chunkHolders.size(); + } + + // TODO replace the need for this, specifically: optimise ServerChunkCache#tickChunks + public Iterable getOldChunkHoldersIterable() { + return new Iterable() { + @Override + public Iterator iterator() { + final Iterator iterator = ChunkHolderManager.this.chunkHolders.valueIterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public ChunkHolder next() { + return iterator.next().vanillaChunkHolder; + } + }; + } + }; + } + + public void close(final boolean save, final boolean halt) { + TickThread.ensureTickThread("Closing world off-main"); + if (halt) { + LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'"); + if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) { + LOGGER.warn("Failed to halt generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'"); + } else { + LOGGER.info("Halted chunk system for world '" + WorldUtil.getWorldName(this.world) + "'"); + } + } + + if (save) { + this.saveAllChunks(true, true, true); + } + + MoonriseRegionFileIO.flush(this.world); + + if (halt) { + LOGGER.info("Waiting 60s for chunk I/O to halt for world '" + WorldUtil.getWorldName(this.world) + "'"); + if (!this.taskScheduler.haltIO(true, TimeUnit.SECONDS.toNanos(60L))) { + LOGGER.warn("Failed to halt I/O tasks for world '" + WorldUtil.getWorldName(this.world) + "'"); + } else { + LOGGER.info("Halted I/O scheduler for world '" + WorldUtil.getWorldName(this.world) + "'"); + } + } + + // kill regionfile cache + for (final MoonriseRegionFileIO.RegionFileType type : MoonriseRegionFileIO.RegionFileType.values()) { + try { + MoonriseRegionFileIO.getControllerFor(this.world, type).getCache().close(); + } catch (final IOException ex) { + LOGGER.error("Failed to close '" + type.name() + "' regionfile cache for world '" + WorldUtil.getWorldName(this.world) + "'", ex); + } + } + + this.taskScheduler.setShutdown(true); + } + + void ensureInAutosave(final NewChunkHolder holder) { + if (!this.autoSaveQueue.contains(holder)) { + holder.lastAutoSave = this.currentTick; + this.autoSaveQueue.add(holder); + } + } + + public void autoSave() { + final List reschedule = new ArrayList<>(); + final long currentTick = this.currentTick; + final long maxSaveTime = currentTick - Math.max(1L, PlatformHooks.get().configAutoSaveInterval(this.world)); + final int maxToSave = PlatformHooks.get().configMaxAutoSavePerTick(this.world); + for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) { + final NewChunkHolder holder = this.autoSaveQueue.first(); + + if (holder.lastAutoSave > maxSaveTime) { + break; + } + + this.autoSaveQueue.remove(holder); + + holder.lastAutoSave = currentTick; + if (holder.save(false) != null) { + ++autoSaved; + } + + if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) { + reschedule.add(holder); + } + } + + for (final NewChunkHolder holder : reschedule) { + if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) { + this.autoSaveQueue.add(holder); + } + } + } + + public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) { + final List holders = this.getChunkHolders(); + + if (logProgress) { + LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'"); + } + + final DecimalFormat format = new DecimalFormat("#0.00"); + + int saved = 0; + + long start = System.nanoTime(); + long lastLog = start; + final int flushInterval = 200; + int lastFlush = 0; + + int savedChunk = 0; + int savedEntity = 0; + int savedPoi = 0; + + if (shutdown) { + // Normal unload process does not occur during shutdown: fire event manually + // for mods that expect ChunkEvent.Unload to fire on shutdown (before LevelEvent.Unload) + for (int i = 0, len = holders.size(); i < len; ++i) { + final NewChunkHolder holder = holders.get(i); + if (holder.getCurrentChunk() instanceof LevelChunk levelChunk) { + PlatformHooks.get().chunkUnloadFromWorld(levelChunk); + } + } + } + for (int i = 0, len = holders.size(); i < len; ++i) { + final NewChunkHolder holder = holders.get(i); + try { + final NewChunkHolder.SaveStat saveStat = holder.save(shutdown); + if (saveStat != null) { + if (saveStat.savedChunk()) { + ++savedChunk; + ++saved; + } + if (saveStat.savedEntityChunk()) { + ++savedEntity; + ++saved; + } + if (saveStat.savedPoiChunk()) { + ++savedPoi; + ++saved; + } + } + } catch (final Throwable thr) { + LOGGER.error("Failed to save chunk (" + holder.chunkX + "," + holder.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr); + } + if (flush && (saved - lastFlush) > (flushInterval / 2)) { + lastFlush = saved; + MoonriseRegionFileIO.partialFlush(this.world, flushInterval / 2); + } + if (logProgress) { + final long currTime = System.nanoTime(); + if ((currTime - lastLog) > TimeUnit.SECONDS.toNanos(10L)) { + lastLog = currTime; + LOGGER.info( + "Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi + + " poi chunks in world '" + WorldUtil.getWorldName(this.world) + "', progress: " + + format.format((double)(i+1)/(double)len * 100.0) + ); + } + } + } + if (flush) { + MoonriseRegionFileIO.flush(this.world); + try { + MoonriseRegionFileIO.flushRegionStorages(this.world); + } catch (final IOException ex) { + LOGGER.error("Exception when flushing regions in world '" + WorldUtil.getWorldName(this.world) + "'", ex); + } + } + if (logProgress) { + LOGGER.info( + "Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi + + " poi chunks in world '" + WorldUtil.getWorldName(this.world) + "' in " + + format.format(1.0E-9 * (System.nanoTime() - start)) + "s" + ); + } + } + + private final ThreadedTicketLevelPropagator ticketLevelPropagator = new ThreadedTicketLevelPropagator() { + @Override + protected void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates) { + // first the necessary chunkholders must be created, so just update the ticket levels + for (final Iterator iterator = updates.long2ByteEntrySet().fastIterator(); iterator.hasNext();) { + final Long2ByteMap.Entry entry = iterator.next(); + final long key = entry.getLongKey(); + final int newLevel = convertBetweenTicketLevels((int)entry.getByteValue()); + + NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key); + if (current == null && newLevel > MAX_TICKET_LEVEL) { + // not loaded and it shouldn't be loaded! + iterator.remove(); + continue; + } + + final int currentLevel = current == null ? MAX_TICKET_LEVEL + 1 : current.getCurrentTicketLevel(); + if (currentLevel == newLevel) { + // nothing to do + iterator.remove(); + continue; + } + + if (current == null) { + // must create + current = ChunkHolderManager.this.createChunkHolder(key); + ChunkHolderManager.this.chunkHolders.put(key, current); + current.updateTicketLevel(newLevel); + } else { + current.updateTicketLevel(newLevel); + } + } + } + + @Override + protected void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List scheduledTasks, + final List changedFullStatus) { + final List prev = CURRENT_TICKET_UPDATE_SCHEDULING.get(); + CURRENT_TICKET_UPDATE_SCHEDULING.set(scheduledTasks); + try { + for (final LongIterator iterator = updates.keySet().iterator(); iterator.hasNext();) { + final long key = iterator.nextLong(); + final NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key); + + if (current == null) { + throw new IllegalStateException("Expected chunk holder to be created"); + } + + current.processTicketLevelUpdate(scheduledTasks, changedFullStatus); + } + } finally { + CURRENT_TICKET_UPDATE_SCHEDULING.set(prev); + } + } + }; + // function for converting between ticket levels and propagator levels and vice versa + // the problem is the ticket level propagator will propagate from a set source down to zero, whereas mojang expects + // levels to propagate from a set value up to a maximum value. so we need to convert the levels we put into the propagator + // and the levels we get out of the propagator + + public static int convertBetweenTicketLevels(final int level) { + return ChunkLevel.MAX_LEVEL - level + 1; + } + + public String getTicketDebugString(final long coordinate) { + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate)); + try { + final SortedArraySet> tickets = this.tickets.get(coordinate); + + return tickets != null ? tickets.first().toString() : "no_ticket"; + } finally { + if (ticketLock != null) { + this.ticketLockArea.unlock(ticketLock); + } + } + } + + public Long2ObjectOpenHashMap>> getTicketsCopy() { + final Long2ObjectOpenHashMap>> ret = new Long2ObjectOpenHashMap<>(); + final Long2ObjectOpenHashMap sections = new Long2ObjectOpenHashMap<>(); + final int sectionShift = this.taskScheduler.getChunkSystemLockShift(); + for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) { + final long coord = iterator.nextLong(); + sections.computeIfAbsent( + CoordinateUtils.getChunkKey( + CoordinateUtils.getChunkX(coord) >> sectionShift, + CoordinateUtils.getChunkZ(coord) >> sectionShift + ), + (final long keyInMap) -> { + return new LongArrayList(); + } + ).add(coord); + } + + for (final Iterator> iterator = sections.long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry entry = iterator.next(); + final long sectionKey = entry.getLongKey(); + final LongArrayList coordinates = entry.getValue(); + + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( + CoordinateUtils.getChunkX(sectionKey) << sectionShift, + CoordinateUtils.getChunkZ(sectionKey) << sectionShift + ); + try { + for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) { + final long coord = iterator2.nextLong(); + final SortedArraySet> tickets = this.tickets.get(coord); + if (tickets == null) { + // removed before we acquired lock + continue; + } + ret.put(coord, ((ChunkSystemSortedArraySet>)tickets).moonrise$copy()); + } + } finally { + this.ticketLockArea.unlock(ticketLock); + } + } + + return ret; + } + + // Paper start + public Collection getPluginChunkTickets(int x, int z) { + com.google.common.collect.ImmutableList.Builder ret; + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(x, z); + try { + final long coordinate = CoordinateUtils.getChunkKey(x, z); + final SortedArraySet> tickets = this.tickets.get(coordinate); + + if (tickets == null) { + return java.util.Collections.emptyList(); + } + + ret = com.google.common.collect.ImmutableList.builder(); + for (Ticket ticket : tickets) { + if (ticket.getType() == TicketType.PLUGIN_TICKET) { + ret.add((org.bukkit.plugin.Plugin)ticket.key); + } + } + } finally { + this.ticketLockArea.unlock(ticketLock); + } + + return ret.build(); + } + // Paper end + + protected final void updateTicketLevel(final long coordinate, final int ticketLevel) { + if (ticketLevel > ChunkLevel.MAX_LEVEL) { + this.ticketLevelPropagator.removeSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate)); + } else { + this.ticketLevelPropagator.setSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate), convertBetweenTicketLevels(ticketLevel)); + } + } + + private static int getTicketLevelAt(SortedArraySet> tickets) { + return !tickets.isEmpty() ? tickets.first().getTicketLevel() : MAX_TICKET_LEVEL + 1; + } + + public boolean addTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, + final T identifier) { + return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); + } + + public boolean addTicketAtLevel(final TicketType type, final int chunkX, final int chunkZ, final int level, + final T identifier) { + return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); + } + + private void addExpireCount(final int chunkX, final int chunkZ) { + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); + final long sectionKey = CoordinateUtils.getChunkKey( + chunkX >> sectionShift, + chunkZ >> sectionShift + ); + + this.sectionToChunkToExpireCount.computeIfAbsent(sectionKey, (final long keyInMap) -> { + return new Long2IntOpenHashMap(); + }).addTo(chunkKey, 1); + } + + private void removeExpireCount(final int chunkX, final int chunkZ) { + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); + final long sectionKey = CoordinateUtils.getChunkKey( + chunkX >> sectionShift, + chunkZ >> sectionShift + ); + + final Long2IntOpenHashMap removeCounts = this.sectionToChunkToExpireCount.get(sectionKey); + final int prevCount = removeCounts.addTo(chunkKey, -1); + + if (prevCount == 1) { + removeCounts.remove(chunkKey); + if (removeCounts.isEmpty()) { + this.sectionToChunkToExpireCount.remove(sectionKey); + } + } + } + + // supposed to return true if the ticket was added and did not replace another + // but, we always return false if the ticket cannot be added + public boolean addTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) { + return this.addTicketAtLevel(type, chunk, level, identifier, true); + } + + boolean addTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier, final boolean lock) { + final long removeDelay = type.timeout <= 0 ? NO_TIMEOUT_MARKER : type.timeout; + if (level > MAX_TICKET_LEVEL) { + return false; + } + + final int chunkX = CoordinateUtils.getChunkX(chunk); + final int chunkZ = CoordinateUtils.getChunkZ(chunk); + final Ticket ticket = new Ticket<>(type, level, identifier); + ((ChunkSystemTicket)(Object)ticket).moonrise$setRemoveDelay(removeDelay); + + final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; + try { + final SortedArraySet> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> { + return SortedArraySet.create(4); + }); + + final int levelBefore = getTicketLevelAt(ticketsAtChunk); + final Ticket current = (Ticket)((ChunkSystemSortedArraySet>)ticketsAtChunk).moonrise$replace(ticket); + final int levelAfter = getTicketLevelAt(ticketsAtChunk); + + if (current != ticket) { + final long oldRemoveDelay = ((ChunkSystemTicket)(Object)current).moonrise$getRemoveDelay(); + if (removeDelay != oldRemoveDelay) { + if (oldRemoveDelay != NO_TIMEOUT_MARKER && removeDelay == NO_TIMEOUT_MARKER) { + this.removeExpireCount(chunkX, chunkZ); + } else if (oldRemoveDelay == NO_TIMEOUT_MARKER) { + // since old != new, we have that NO_TIMEOUT_MARKER != new + this.addExpireCount(chunkX, chunkZ); + } + } + } else { + if (removeDelay != NO_TIMEOUT_MARKER) { + this.addExpireCount(chunkX, chunkZ); + } + } + + if (levelBefore != levelAfter) { + this.updateTicketLevel(chunk, levelAfter); + } + + return current == ticket; + } finally { + if (ticketLock != null) { + this.ticketLockArea.unlock(ticketLock); + } + } + } + + public boolean removeTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, final T identifier) { + return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); + } + + public boolean removeTicketAtLevel(final TicketType type, final int chunkX, final int chunkZ, final int level, final T identifier) { + return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); + } + + public boolean removeTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) { + return this.removeTicketAtLevel(type, chunk, level, identifier, true); + } + + boolean removeTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier, final boolean lock) { + if (level > MAX_TICKET_LEVEL) { + return false; + } + + final int chunkX = CoordinateUtils.getChunkX(chunk); + final int chunkZ = CoordinateUtils.getChunkZ(chunk); + final Ticket probe = new Ticket<>(type, level, identifier); + + final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; + try { + final SortedArraySet> ticketsAtChunk = this.tickets.get(chunk); + if (ticketsAtChunk == null) { + return false; + } + + final int oldLevel = getTicketLevelAt(ticketsAtChunk); + final Ticket ticket = (Ticket)((ChunkSystemSortedArraySet>)ticketsAtChunk).moonrise$removeAndGet(probe); + + if (ticket == null) { + return false; + } + + final int newLevel = getTicketLevelAt(ticketsAtChunk); + // we should not change the ticket levels while the target region may be ticking + if (oldLevel != newLevel) { + final Ticket unknownTicket = new Ticket<>(TicketType.UNKNOWN, level, new ChunkPos(chunk)); + ((ChunkSystemTicket)(Object)unknownTicket).moonrise$setRemoveDelay(Math.max(1, TicketType.UNKNOWN.timeout)); + if (ticketsAtChunk.add(unknownTicket)) { + this.addExpireCount(chunkX, chunkZ); + } else { + throw new IllegalStateException("Should have been able to add " + unknownTicket + " to " + ticketsAtChunk); + } + } + + final long removeDelay = ((ChunkSystemTicket)(Object)ticket).moonrise$getRemoveDelay(); + if (removeDelay != NO_TIMEOUT_MARKER) { + this.removeExpireCount(chunkX, chunkZ); + } + + return true; + } finally { + if (ticketLock != null) { + this.ticketLockArea.unlock(ticketLock); + } + } + } + + // atomic with respect to all add/remove/addandremove ticket calls for the given chunk + public void addAndRemoveTickets(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier, + final TicketType removeType, final int removeLevel, final V removeIdentifier) { + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)); + try { + this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false); + this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false); + } finally { + this.ticketLockArea.unlock(ticketLock); + } + } + + // atomic with respect to all add/remove/addandremove ticket calls for the given chunk + public boolean addIfRemovedTicket(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier, + final TicketType removeType, final int removeLevel, final V removeIdentifier) { + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)); + try { + if (this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false)) { + this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false); + return true; + } + return false; + } finally { + this.ticketLockArea.unlock(ticketLock); + } + } + + public void removeAllTicketsFor(final TicketType ticketType, final int ticketLevel, final T ticketIdentifier) { + if (ticketLevel > MAX_TICKET_LEVEL) { + return; + } + + final Long2ObjectOpenHashMap sections = new Long2ObjectOpenHashMap<>(); + final int sectionShift = this.taskScheduler.getChunkSystemLockShift(); + for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) { + final long coord = iterator.nextLong(); + sections.computeIfAbsent( + CoordinateUtils.getChunkKey( + CoordinateUtils.getChunkX(coord) >> sectionShift, + CoordinateUtils.getChunkZ(coord) >> sectionShift + ), + (final long keyInMap) -> { + return new LongArrayList(); + } + ).add(coord); + } + + for (final Iterator> iterator = sections.long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry entry = iterator.next(); + final long sectionKey = entry.getLongKey(); + final LongArrayList coordinates = entry.getValue(); + + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( + CoordinateUtils.getChunkX(sectionKey) << sectionShift, + CoordinateUtils.getChunkZ(sectionKey) << sectionShift + ); + try { + for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) { + final long coord = iterator2.nextLong(); + this.removeTicketAtLevel(ticketType, coord, ticketLevel, ticketIdentifier, false); + } + } finally { + this.ticketLockArea.unlock(ticketLock); + } + } + } + + public void tick() { + ++this.currentTick; + + final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); + + final Predicate> expireNow = (final Ticket ticket) -> { + long removeDelay = ((ChunkSystemTicket)(Object)ticket).moonrise$getRemoveDelay(); + if (removeDelay == NO_TIMEOUT_MARKER) { + return false; + } + --removeDelay; + ((ChunkSystemTicket)(Object)ticket).moonrise$setRemoveDelay(removeDelay); + return removeDelay <= 0L; + }; + + for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) { + final long sectionKey = iterator.nextLong(); + + if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) { + // removed concurrently + continue; + } + + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( + CoordinateUtils.getChunkX(sectionKey) << sectionShift, + CoordinateUtils.getChunkZ(sectionKey) << sectionShift + ); + + try { + final Long2IntOpenHashMap chunkToExpireCount = this.sectionToChunkToExpireCount.get(sectionKey); + if (chunkToExpireCount == null) { + // lost to some race + continue; + } + + for (final Iterator iterator1 = chunkToExpireCount.long2IntEntrySet().fastIterator(); iterator1.hasNext();) { + final Long2IntMap.Entry entry = iterator1.next(); + + final long chunkKey = entry.getLongKey(); + final int expireCount = entry.getIntValue(); + + final SortedArraySet> tickets = this.tickets.get(chunkKey); + final int levelBefore = getTicketLevelAt(tickets); + + final int sizeBefore = tickets.size(); + tickets.removeIf(expireNow); + final int sizeAfter = tickets.size(); + final int levelAfter = getTicketLevelAt(tickets); + + if (tickets.isEmpty()) { + this.tickets.remove(chunkKey); + } + if (levelBefore != levelAfter) { + this.updateTicketLevel(chunkKey, levelAfter); + } + + final int newExpireCount = expireCount - (sizeBefore - sizeAfter); + + if (newExpireCount == expireCount) { + continue; + } + + if (newExpireCount != 0) { + entry.setValue(newExpireCount); + } else { + iterator1.remove(); + } + } + + if (chunkToExpireCount.isEmpty()) { + this.sectionToChunkToExpireCount.remove(sectionKey); + } + } finally { + this.ticketLockArea.unlock(ticketLock); + } + } + + this.processTicketUpdates(); + } + + public NewChunkHolder getChunkHolder(final int chunkX, final int chunkZ) { + return this.chunkHolders.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } + + public NewChunkHolder getChunkHolder(final long position) { + return this.chunkHolders.get(position); + } + + public void raisePriority(final int x, final int z, final Priority priority) { + final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); + if (chunkHolder != null) { + chunkHolder.raisePriority(priority); + } + } + + public void setPriority(final int x, final int z, final Priority priority) { + final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); + if (chunkHolder != null) { + chunkHolder.setPriority(priority); + } + } + + public void lowerPriority(final int x, final int z, final Priority priority) { + final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); + if (chunkHolder != null) { + chunkHolder.lowerPriority(priority); + } + } + + private NewChunkHolder createChunkHolder(final long position) { + final NewChunkHolder ret = new NewChunkHolder(this.world, CoordinateUtils.getChunkX(position), CoordinateUtils.getChunkZ(position), this.taskScheduler); + + PlatformHooks.get().onChunkHolderCreate(this.world, ret.vanillaChunkHolder); + + return ret; + } + + // because this function creates the chunk holder without a ticket, it is the caller's responsibility to ensure + // the chunk holder eventually unloads. this should only be used to avoid using processTicketUpdates to create chunkholders, + // as processTicketUpdates may call plugin logic; in every other case a ticket is appropriate + private NewChunkHolder getOrCreateChunkHolder(final int chunkX, final int chunkZ) { + return this.getOrCreateChunkHolder(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } + + private NewChunkHolder getOrCreateChunkHolder(final long position) { + final int chunkX = CoordinateUtils.getChunkX(position); + final int chunkZ = CoordinateUtils.getChunkZ(position); + + if (!this.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ)) { + throw new IllegalStateException("Must hold ticket level update lock!"); + } + if (!this.taskScheduler.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ)) { + throw new IllegalStateException("Must hold scheduler lock!!"); + } + + // we could just acquire these locks, but... + // must own the locks because the caller needs to ensure that no unload can occur AFTER this function returns + + NewChunkHolder current = this.chunkHolders.get(position); + if (current != null) { + return current; + } + + current = this.createChunkHolder(position); + this.chunkHolders.put(position, current); + + + return current; + } + + public ChunkEntitySlices getOrCreateEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { + TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create entity chunk off-main"); + ChunkEntitySlices ret; + + NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ); + if (current != null && (ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) { + return ret; + } + + final AtomicBoolean isCompleted = new AtomicBoolean(); + final Thread waiter = Thread.currentThread(); + final Long entityLoadId = ChunkTaskScheduler.getNextEntityLoadId(); + NewChunkHolder.GenericDataLoadTaskCallback loadTask = null; + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ); + try { + this.addTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); + final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ); + try { + current = this.getOrCreateChunkHolder(chunkX, chunkZ); + if ((ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) { + this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); + return ret; + } + + if (!transientChunk) { + if (current.isEntityChunkNBTLoaded()) { + isCompleted.setPlain(true); + } else { + loadTask = current.getOrLoadEntityData((final GenericDataLoadTask.TaskResult result) -> { + isCompleted.set(true); + LockSupport.unpark(waiter); + }); + final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask(); + + if (entityLoad != null) { + entityLoad.raisePriority(Priority.BLOCKING); + } + } + } + } finally { + this.taskScheduler.schedulingLockArea.unlock(schedulingLock); + } + } finally { + this.ticketLockArea.unlock(ticketLock); + } + + if (loadTask != null) { + loadTask.schedule(); + } + + if (!transientChunk) { + // Note: no need to busy wait on the chunk queue, entity load will complete off-main + boolean interrupted = false; + while (!isCompleted.get()) { + interrupted |= Thread.interrupted(); + LockSupport.park(); + } + + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + + // now that the entity data is loaded, we can load it into the world + + ret = current.loadInEntityChunk(transientChunk); + + this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); + + return ret; + } + + public PoiChunk getPoiChunkIfLoaded(final int chunkX, final int chunkZ, final boolean checkLoadInCallback) { + final NewChunkHolder holder = this.getChunkHolder(chunkX, chunkZ); + if (holder != null) { + final PoiChunk ret = holder.getPoiChunk(); + return ret == null || (checkLoadInCallback && !ret.isLoaded()) ? null : ret; + } + return null; + } + + public PoiChunk loadPoiChunk(final int chunkX, final int chunkZ) { + TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create poi chunk off-main"); + PoiChunk ret; + + NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ); + if (current != null && (ret = current.getPoiChunk()) != null) { + ret.load(); + return ret; + } + + final AtomicReference completed = new AtomicReference<>(); + final AtomicBoolean isCompleted = new AtomicBoolean(); + final Thread waiter = Thread.currentThread(); + final Long poiLoadId = ChunkTaskScheduler.getNextPoiLoadId(); + NewChunkHolder.GenericDataLoadTaskCallback loadTask = null; + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ); + try { + this.addTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId); + final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ); + try { + current = this.getOrCreateChunkHolder(chunkX, chunkZ); + if (null == (ret = current.getPoiChunk())) { + loadTask = current.getOrLoadPoiData((final GenericDataLoadTask.TaskResult result) -> { + completed.setPlain(result.left()); + isCompleted.set(true); + LockSupport.unpark(waiter); + }); + final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask(); + + if (poiLoad != null) { + poiLoad.raisePriority(Priority.BLOCKING); + } + } + } finally { + this.taskScheduler.schedulingLockArea.unlock(schedulingLock); + } + } finally { + this.ticketLockArea.unlock(ticketLock); + } + + if (loadTask != null) { + loadTask.schedule(); + + // Note: no need to busy wait on the chunk queue, poi load will complete off-main + + boolean interrupted = false; + while (!isCompleted.get()) { + interrupted |= Thread.interrupted(); + LockSupport.park(); + } + + if (interrupted) { + Thread.currentThread().interrupt(); + } + + ret = completed.getPlain(); + } // else: became loaded during the scheduling attempt, need to ensure load() is invoked + + ret.load(); + + this.removeTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId); + + return ret; + } + + void addChangedStatuses(final List changedFullStatus) { + if (changedFullStatus.isEmpty()) { + return; + } + if (!TickThread.isTickThread()) { + this.taskScheduler.scheduleChunkTask(() -> { + final ArrayDeque pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate; + for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { + pendingFullLoadUpdate.add(changedFullStatus.get(i)); + } + + ChunkHolderManager.this.processPendingFullUpdate(); + }, Priority.HIGHEST); + } else { + final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; + for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { + pendingFullLoadUpdate.add(changedFullStatus.get(i)); + } + } + } + + private void removeChunkHolder(final NewChunkHolder holder) { + holder.onUnload(); + this.autoSaveQueue.remove(holder); + PlatformHooks.get().onChunkHolderDelete(this.world, holder.vanillaChunkHolder); + this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ)); + } + + // note: never call while inside the chunk system, this will absolutely break everything + public void processUnloads() { + TickThread.ensureTickThread("Cannot unload chunks off-main"); + + if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { + throw new IllegalStateException("Cannot unload chunks recursively"); + } + final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift + final List unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions(); + int unloadCountTentative = 0; + for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) { + final ChunkUnloadQueue.UnloadSection section + = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ()); + + if (section == null) { + // removed concurrently + continue; + } + + // technically reading the size field is unsafe, and it may be incorrect. + // We assume that the error here cumulatively goes away over many ticks. If it did not, then it is possible + // for chunks to never unload or not unload fast enough. + unloadCountTentative += section.chunks.size(); + } + + if (unloadCountTentative <= 0) { + // no work to do + return; + } + + // We do need to process updates here so that any addTicket that is synchronised before this call does not go missed. + this.processTicketUpdates(); + + final int toUnloadCount = Math.max(50, (int)(unloadCountTentative * 0.05)); + int processedCount = 0; + + for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) { + final List stage1 = new ArrayList<>(); + final List stage2 = new ArrayList<>(); + + final int sectionLowerX = sectionRef.sectionX() << sectionShift; + final int sectionLowerZ = sectionRef.sectionZ() << sectionShift; + + // stage 1: set up for stage 2 while holding critical locks + ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ); + try { + final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ); + try { + final ChunkUnloadQueue.UnloadSection section + = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ()); + + if (section == null) { + // removed concurrently + continue; + } + + // collect the holders to run stage 1 on + final int sectionCount = section.chunks.size(); + + if ((sectionCount + processedCount) <= toUnloadCount) { + // we can just drain the entire section + + for (final LongIterator iterator = section.chunks.iterator(); iterator.hasNext();) { + final NewChunkHolder holder = this.chunkHolders.get(iterator.nextLong()); + if (holder == null) { + throw new IllegalStateException(); + } + stage1.add(holder); + } + + // remove section + this.unloadQueue.removeSection(sectionRef.sectionX(), sectionRef.sectionZ()); + } else { + // processedCount + len = toUnloadCount + // we cannot drain the entire section + for (int i = 0, len = toUnloadCount - processedCount; i < len; ++i) { + final NewChunkHolder holder = this.chunkHolders.get(section.chunks.removeFirstLong()); + if (holder == null) { + throw new IllegalStateException(); + } + stage1.add(holder); + } + } + + // run stage 1 + for (int i = 0, len = stage1.size(); i < len; ++i) { + final NewChunkHolder chunkHolder = stage1.get(i); + chunkHolder.removeFromUnloadQueue(); + if (chunkHolder.isSafeToUnload() != null) { + LOGGER.error("Chunkholder " + chunkHolder + " is not safe to unload but is inside the unload queue?"); + continue; + } + final NewChunkHolder.UnloadState state = chunkHolder.unloadStage1(); + if (state == null) { + // can unload immediately + this.removeChunkHolder(chunkHolder); + continue; + } + stage2.add(state); + } + } finally { + this.taskScheduler.schedulingLockArea.unlock(scheduleLock); + } + } finally { + this.ticketLockArea.unlock(ticketLock); + } + + // stage 2: invoke expensive unload logic, designed to run without locks thanks to stage 1 + final List stage3 = new ArrayList<>(stage2.size()); + + final Boolean before = this.blockTicketUpdates(); + try { + for (int i = 0, len = stage2.size(); i < len; ++i) { + final NewChunkHolder.UnloadState state = stage2.get(i); + final NewChunkHolder holder = state.holder(); + + holder.unloadStage2(state); + stage3.add(holder); + } + } finally { + this.unblockTicketUpdates(before); + } + + // stage 3: actually attempt to remove the chunk holders + ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ); + try { + final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ); + try { + for (int i = 0, len = stage3.size(); i < len; ++i) { + final NewChunkHolder holder = stage3.get(i); + + if (holder.unloadStage3()) { + this.removeChunkHolder(holder); + } else { + // add cooldown so the next unload check is not immediately next tick + this.addTicketAtLevel(UNLOAD_COOLDOWN, CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ), MAX_TICKET_LEVEL, Unit.INSTANCE, false); + } + } + } finally { + this.taskScheduler.schedulingLockArea.unlock(scheduleLock); + } + } finally { + this.ticketLockArea.unlock(ticketLock); + } + + processedCount += stage1.size(); + + if (processedCount >= toUnloadCount) { + break; + } + } + } + + public enum TicketOperationType { + ADD, REMOVE, ADD_IF_REMOVED, ADD_AND_REMOVE + } + + public static record TicketOperation ( + TicketOperationType op, long chunkCoord, + TicketType ticketType, int ticketLevel, T identifier, + TicketType ticketType2, int ticketLevel2, V identifier2 + ) { + + private TicketOperation(TicketOperationType op, long chunkCoord, + TicketType ticketType, int ticketLevel, T identifier) { + this(op, chunkCoord, ticketType, ticketLevel, identifier, null, 0, null); + } + + public static TicketOperation addOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { + return addOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); + } + + public static TicketOperation addOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { + return addOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); + } + + public static TicketOperation addOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { + return new TicketOperation<>(TicketOperationType.ADD, chunk, type, ticketLevel, identifier); + } + + public static TicketOperation removeOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { + return removeOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); + } + + public static TicketOperation removeOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { + return removeOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); + } + + public static TicketOperation removeOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { + return new TicketOperation<>(TicketOperationType.REMOVE, chunk, type, ticketLevel, identifier); + } + + public static TicketOperation addIfRemovedOp(final long chunk, + final TicketType addType, final int addLevel, final T addIdentifier, + final TicketType removeType, final int removeLevel, final V removeIdentifier) { + return new TicketOperation<>( + TicketOperationType.ADD_IF_REMOVED, chunk, addType, addLevel, addIdentifier, + removeType, removeLevel, removeIdentifier + ); + } + + public static TicketOperation addAndRemove(final long chunk, + final TicketType addType, final int addLevel, final T addIdentifier, + final TicketType removeType, final int removeLevel, final V removeIdentifier) { + return new TicketOperation<>( + TicketOperationType.ADD_AND_REMOVE, chunk, addType, addLevel, addIdentifier, + removeType, removeLevel, removeIdentifier + ); + } + } + + private boolean processTicketOp(TicketOperation operation) { + boolean ret = false; + switch (operation.op) { + case ADD: { + ret |= this.addTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier); + break; + } + case REMOVE: { + ret |= this.removeTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier); + break; + } + case ADD_IF_REMOVED: { + ret |= this.addIfRemovedTicket( + operation.chunkCoord, + operation.ticketType, operation.ticketLevel, operation.identifier, + operation.ticketType2, operation.ticketLevel2, operation.identifier2 + ); + break; + } + case ADD_AND_REMOVE: { + ret = true; + this.addAndRemoveTickets( + operation.chunkCoord, + operation.ticketType, operation.ticketLevel, operation.identifier, + operation.ticketType2, operation.ticketLevel2, operation.identifier2 + ); + break; + } + } + + return ret; + } + + public void performTicketUpdates(final Collection> operations) { + for (final TicketOperation operation : operations) { + this.processTicketOp(operation); + } + } + + private final ThreadLocal BLOCK_TICKET_UPDATES = ThreadLocal.withInitial(() -> { + return Boolean.FALSE; + }); + + public Boolean blockTicketUpdates() { + final Boolean ret = BLOCK_TICKET_UPDATES.get(); + BLOCK_TICKET_UPDATES.set(Boolean.TRUE); + return ret; + } + + public void unblockTicketUpdates(final Boolean before) { + BLOCK_TICKET_UPDATES.set(before); + } + + public boolean processTicketUpdates() { + return this.processTicketUpdates(true, null); + } + + private static final ThreadLocal> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>(); + + static List getCurrentTicketUpdateScheduling() { + return CURRENT_TICKET_UPDATE_SCHEDULING.get(); + } + + private boolean processTicketUpdates(final boolean processFullUpdates, List scheduledTasks) { + if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { + throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager"); + } + if (!PlatformHooks.get().allowAsyncTicketUpdates() && !TickThread.isTickThread()) { + TickThread.ensureTickThread("Cannot asynchronously process ticket updates"); + } + + List changedFullStatus = null; + + final boolean isTickThread = TickThread.isTickThread(); + + boolean ret = false; + final boolean canProcessFullUpdates = processFullUpdates & isTickThread; + final boolean canProcessScheduling = scheduledTasks == null; + + if (this.ticketLevelPropagator.hasPendingUpdates()) { + if (scheduledTasks == null) { + scheduledTasks = new ArrayList<>(); + } + changedFullStatus = new ArrayList<>(); + + this.blockTicketUpdates(); + try { + ret |= this.ticketLevelPropagator.performUpdates( + this.ticketLockArea, this.taskScheduler.schedulingLockArea, + scheduledTasks, changedFullStatus + ); + } finally { + this.unblockTicketUpdates(Boolean.FALSE); + } + } + + if (changedFullStatus != null) { + this.addChangedStatuses(changedFullStatus); + } + + if (canProcessScheduling && scheduledTasks != null) { + for (int i = 0, len = scheduledTasks.size(); i < len; ++i) { + scheduledTasks.get(i).schedule(); + } + } + + if (canProcessFullUpdates) { + ret |= this.processPendingFullUpdate(); + } + + return ret; + } + + // only call on tick thread + private boolean processPendingFullUpdate() { + final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; + + boolean ret = false; + + final List changedFullStatus = new ArrayList<>(); + + NewChunkHolder holder; + while ((holder = pendingFullLoadUpdate.poll()) != null) { + ret |= holder.handleFullStatusChange(changedFullStatus); + + if (!changedFullStatus.isEmpty()) { + for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { + pendingFullLoadUpdate.add(changedFullStatus.get(i)); + } + changedFullStatus.clear(); + } + } + + return ret; + } + + public JsonObject getDebugJson() { + final JsonObject ret = new JsonObject(); + + ret.add("unload_queue", this.unloadQueue.toDebugJson()); + + final JsonArray holders = new JsonArray(); + ret.add("chunkholders", holders); + + for (final NewChunkHolder holder : this.getChunkHolders()) { + holders.add(holder.getDebugJson()); + } + + final JsonArray allTicketsJson = new JsonArray(); + ret.add("tickets", allTicketsJson); + + for (final Iterator>>> iterator = this.tickets.entryIterator(); + iterator.hasNext();) { + final ConcurrentLong2ReferenceChainedHashTable.TableEntry>> coordinateTickets = iterator.next(); + final long coordinate = coordinateTickets.getKey(); + final SortedArraySet> tickets = coordinateTickets.getValue(); + + final JsonObject coordinateJson = new JsonObject(); + allTicketsJson.add(coordinateJson); + + coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); + coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); + + final JsonArray ticketsSerialized = new JsonArray(); + coordinateJson.add("tickets", ticketsSerialized); + + // note: by using a copy of the backing array, we can avoid explicit exceptions we may trip when iterating + // directly over the set using the iterator + // however, it also means we need to null-check the values, and there is a possibility that we _miss_ an + // entry OR iterate over an entry multiple times + for (final Object ticketUncasted : ((ChunkSystemSortedArraySet>)tickets).moonrise$copyBackingArray()) { + if (ticketUncasted == null) { + continue; + } + final Ticket ticket = (Ticket)ticketUncasted; + final JsonObject ticketSerialized = new JsonObject(); + ticketsSerialized.add(ticketSerialized); + + ticketSerialized.addProperty("type", ticket.getType().toString()); + ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel())); + ticketSerialized.addProperty("identifier", Objects.toString(ticket.key)); + ticketSerialized.addProperty("remove_tick", Long.valueOf(((ChunkSystemTicket)(Object)ticket).moonrise$getRemoveDelay())); + } + } + + return ret; + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java new file mode 100644 index 0000000000000000000000000000000000000000..67532b85073b7978254a0b04caadfe822679e61f --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java @@ -0,0 +1,1055 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; + +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.executor.queue.PrioritisedTaskQueue; +import ca.spottedleaf.concurrentutil.executor.thread.PrioritisedThreadPool; +import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.JsonUtil; +import ca.spottedleaf.moonrise.common.util.MoonriseCommon; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; +import ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor.RadiusAwarePrioritisedExecutor; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkFullTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLightTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkUpgradeGenericStatusTask; +import ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer; +import ca.spottedleaf.moonrise.patches.chunk_system.status.ChunkSystemChunkStep; +import ca.spottedleaf.moonrise.patches.chunk_system.util.ParallelSearchRadiusIteration; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.mojang.logging.LogUtils; +import net.minecraft.CrashReport; +import net.minecraft.CrashReportCategory; +import net.minecraft.ReportedException; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.GenerationChunkHolder; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; +import net.minecraft.util.StaticCache2D; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkPyramid; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.status.ChunkStep; +import net.minecraft.world.phys.Vec3; +import org.slf4j.Logger; +import java.io.File; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +public final class ChunkTaskScheduler { + + private static final Logger LOGGER = LogUtils.getClassLogger(); + + public static void init(final boolean useParallelGen) { + for (final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor executor : MoonriseCommon.RADIUS_AWARE_GROUP.getAllExecutors()) { + executor.setMaxParallelism(useParallelGen ? -1 : 1); + } + + LOGGER.info("Chunk system is using population gen parallelism: " + useParallelGen); + } + + public static final TicketType CHUNK_LOAD = TicketType.create("chunk_system:chunk_load", Long::compareTo); + private static final AtomicLong CHUNK_LOAD_IDS = new AtomicLong(); + + public static Long getNextChunkLoadId() { + return Long.valueOf(CHUNK_LOAD_IDS.getAndIncrement()); + } + + public static final TicketType NON_FULL_CHUNK_LOAD = TicketType.create("chunk_system:non_full_load", Long::compareTo); + private static final AtomicLong NON_FULL_CHUNK_LOAD_IDS = new AtomicLong(); + + public static Long getNextNonFullLoadId() { + return Long.valueOf(NON_FULL_CHUNK_LOAD_IDS.getAndIncrement()); + } + + public static final TicketType ENTITY_LOAD = TicketType.create("chunk_system:entity_load", Long::compareTo); + private static final AtomicLong ENTITY_LOAD_IDS = new AtomicLong(); + + public static Long getNextEntityLoadId() { + return Long.valueOf(ENTITY_LOAD_IDS.getAndIncrement()); + } + + public static final TicketType POI_LOAD = TicketType.create("chunk_system:poi_load", Long::compareTo); + private static final AtomicLong POI_LOAD_IDS = new AtomicLong(); + + public static Long getNextPoiLoadId() { + return Long.valueOf(POI_LOAD_IDS.getAndIncrement()); + } + + public static final TicketType CHUNK_RELIGHT = TicketType.create("starlight:chunk_relight", Long::compareTo); + private static final AtomicLong CHUNK_RELIGHT_IDS = new AtomicLong(); + + public static Long getNextChunkRelightId() { + return Long.valueOf(CHUNK_RELIGHT_IDS.getAndIncrement()); + } + + + public static int getTicketLevel(final ChunkStatus status) { + return ChunkLevel.byStatus(status); + } + + public final ServerLevel world; + public final RadiusAwarePrioritisedExecutor radiusAwareScheduler; + public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor parallelGenExecutor; + private final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor radiusAwareGenExecutor; + public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor loadExecutor; + public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor ioExecutor; + public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor compressionExecutor; + public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor saveExecutor; + + private final PrioritisedTaskQueue mainThreadExecutor = new PrioritisedTaskQueue(); + + public final ChunkHolderManager chunkHolderManager; + + static { + ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_STARTS).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setWriteRadius(1); + ((ChunkSystemChunkStatus)ChunkStatus.INITIALIZE_LIGHT).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$setWriteRadius(2); + ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.FULL).moonrise$setWriteRadius(0); + + ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setEmptyLoadStatus(true); + ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setEmptyLoadStatus(true); + ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setEmptyLoadStatus(true); + ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setEmptyLoadStatus(true); + ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setEmptyLoadStatus(true); + ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setEmptyLoadStatus(true); + ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setEmptyLoadStatus(true); + ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setEmptyLoadStatus(true); + + /* + It's important that the neighbour read radius is taken into account. If _any_ later status is using some chunk as + a neighbour, it must be also safe if that neighbour is being generated. i.e for any status later than FEATURES, + for a status to be parallel safe it must not read the block data from its neighbours. + */ + final List parallelCapableStatus = Arrays.asList( + // No-op executor. + ChunkStatus.EMPTY, + + // This is parallel capable, as CB has fixed the concurrency issue with stronghold generations. + // Does not touch neighbour chunks. + ChunkStatus.STRUCTURE_STARTS, + + // Surprisingly this is parallel capable. It is simply reading the already-created structure starts + // into the structure references for the chunk. So while it reads from it neighbours, its neighbours + // will not change, even if executed in parallel. + ChunkStatus.STRUCTURE_REFERENCES, + + // Safe. Mojang runs it in parallel as well. + ChunkStatus.BIOMES, + + // Safe. Mojang runs it in parallel as well. + ChunkStatus.NOISE, + + // Parallel safe. Only touches the target chunk. Biome retrieval is now noise based, which is + // completely thread-safe. + ChunkStatus.SURFACE, + + // No global state is modified in the carvers. It only touches the specified chunk. So it is parallel safe. + ChunkStatus.CARVERS, + + // FEATURES is not parallel safe. It writes to neighbours. + + // no-op executor + ChunkStatus.INITIALIZE_LIGHT + + // LIGHT is not parallel safe. It also doesn't run on the generation executor, so no point. + + // Only writes to the specified chunk. State is not read by later statuses. Parallel safe. + // Note: it may look unsafe because it writes to a worldgenregion, but the region size is always 0 - + // see the task margin. + // However, if the neighbouring FEATURES chunk is unloaded, but then fails to load in again (for whatever + // reason), then it would write to this chunk - and since this status reads blocks from itself, it's not + // safe to execute this in parallel. + // SPAWN + + // FULL is executed on main. + ); + + for (final ChunkStatus status : parallelCapableStatus) { + ((ChunkSystemChunkStatus)status).moonrise$setParallelCapable(true); + } + } + + private static final int[] ACCESS_RADIUS_TABLE_LOAD = new int[ChunkStatus.getStatusList().size()]; + private static final int[] ACCESS_RADIUS_TABLE_GEN = new int[ChunkStatus.getStatusList().size()]; + private static final int[] ACCESS_RADIUS_TABLE = new int[ChunkStatus.getStatusList().size()]; + static { + Arrays.fill(ACCESS_RADIUS_TABLE_LOAD, -1); + Arrays.fill(ACCESS_RADIUS_TABLE_GEN, -1); + Arrays.fill(ACCESS_RADIUS_TABLE, -1); + } + + private static int getAccessRadius0(final ChunkStatus toStatus, final ChunkPyramid pyramid) { + if (toStatus == ChunkStatus.EMPTY) { + return 0; + } + + final ChunkStep chunkStep = pyramid.getStepTo(toStatus); + + final int radius = chunkStep.getAccumulatedRadiusOf(ChunkStatus.EMPTY); + int maxRange = radius; + + for (int dist = 0; dist <= radius; ++dist) { + final ChunkStatus requiredNeighbourStatus = ((ChunkSystemChunkStep)(Object)chunkStep).moonrise$getRequiredStatusAtRadius(dist); + final int rad = ACCESS_RADIUS_TABLE[requiredNeighbourStatus.getIndex()]; + if (rad == -1) { + throw new IllegalStateException(); + } + + maxRange = Math.max(maxRange, dist + rad); + } + + return maxRange; + } + + private static final int MAX_ACCESS_RADIUS; + + static { + final List statuses = ChunkStatus.getStatusList(); + for (int i = 0, len = statuses.size(); i < len; ++i) { + final ChunkStatus status = statuses.get(i); + ACCESS_RADIUS_TABLE_LOAD[i] = getAccessRadius0(status, ChunkPyramid.LOADING_PYRAMID); + ACCESS_RADIUS_TABLE_GEN[i] = getAccessRadius0(status, ChunkPyramid.GENERATION_PYRAMID); + ACCESS_RADIUS_TABLE[i] = Math.max( + ACCESS_RADIUS_TABLE_LOAD[i], + ACCESS_RADIUS_TABLE_GEN[i] + ); + } + MAX_ACCESS_RADIUS = ACCESS_RADIUS_TABLE[ACCESS_RADIUS_TABLE.length - 1]; + } + + public static int getMaxAccessRadius() { + return MAX_ACCESS_RADIUS; + } + + public static int getAccessRadius(final ChunkStatus genStatus) { + return ACCESS_RADIUS_TABLE[genStatus.getIndex()]; + } + + public static int getAccessRadius(final FullChunkStatus status) { + return (status.ordinal() - 1) + getAccessRadius(ChunkStatus.FULL); + } + + + public final ReentrantAreaLock schedulingLockArea; + private final int lockShift; + + public final int getChunkSystemLockShift() { + return this.lockShift; + } + + private volatile boolean shutdown; + + public boolean hasShutdown() { + return this.shutdown; + } + + public void setShutdown(final boolean shutdown) { + this.shutdown = shutdown; + } + + public ChunkTaskScheduler(final ServerLevel world) { + this.world = world; + // must be >= region shift (in paper, doesn't exist) and must be >= ticket propagator section shift + // it must be >= region shift since the regioniser assumes ticket updates do not occur in parallel for the region sections + // it must be >= ticket propagator section shift so that the ticket propagator can assume that owning a position implies owning + // the entire section + // we just take the max, as we want the smallest shift that satisfies these properties + this.lockShift = Math.max(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift(), ThreadedTicketLevelPropagator.SECTION_SHIFT); + this.schedulingLockArea = new ReentrantAreaLock(this.getChunkSystemLockShift()); + + this.parallelGenExecutor = MoonriseCommon.PARALLEL_GEN_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0); + this.radiusAwareGenExecutor = MoonriseCommon.RADIUS_AWARE_GROUP.createExecutor(1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0); + this.loadExecutor = MoonriseCommon.LOAD_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0); + this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, 16); + this.ioExecutor = MoonriseCommon.SERVER_REGION_IO_GROUP.createExecutor(-1, MoonriseCommon.IO_QUEUE_HOLD_TIME, 0); + // we need a separate executor here so that on shutdown we can continue to process I/O tasks + this.compressionExecutor = MoonriseCommon.LOAD_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0); + this.saveExecutor = MoonriseCommon.LOAD_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0); + this.chunkHolderManager = new ChunkHolderManager(world, this); + } + + private final AtomicBoolean failedChunkSystem = new AtomicBoolean(); + + public static Object stringIfNull(final Object obj) { + return obj == null ? "null" : obj; + } + + public void unrecoverableChunkSystemFailure(final int chunkX, final int chunkZ, final Map objectsOfInterest, final Throwable thr) { + final NewChunkHolder holder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ); + LOGGER.error("Chunk system error at chunk (" + chunkX + "," + chunkZ + "), holder: " + holder + ", exception:", new Throwable(thr)); + + if (this.failedChunkSystem.getAndSet(true)) { + return; + } + + final ReportedException reportedException = thr instanceof ReportedException ? (ReportedException)thr : new ReportedException(new CrashReport("Chunk system error", thr)); + + CrashReportCategory crashReportCategory = reportedException.getReport().addCategory("Chunk system details"); + crashReportCategory.setDetail("Chunk coordinate", new ChunkPos(chunkX, chunkZ).toString()); + crashReportCategory.setDetail("ChunkHolder", Objects.toString(holder)); + crashReportCategory.setDetail("unrecoverableChunkSystemFailure caller thread", Thread.currentThread().getName()); + + crashReportCategory = reportedException.getReport().addCategory("Chunk System Objects of Interest"); + for (final Map.Entry entry : objectsOfInterest.entrySet()) { + if (entry.getValue() instanceof Throwable thrObject) { + crashReportCategory.setDetailError(Objects.toString(entry.getKey()), thrObject); + } else { + crashReportCategory.setDetail(Objects.toString(entry.getKey()), Objects.toString(entry.getValue())); + } + } + + final Runnable crash = () -> { + throw new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException); + }; + + // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions + this.scheduleChunkTask(chunkX, chunkZ, crash, Priority.BLOCKING); + // so, make the main thread pick it up + ((ChunkSystemMinecraftServer)this.world.getServer()).moonrise$setChunkSystemCrash(new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException)); + } + + public boolean executeMainThreadTask() { + TickThread.ensureTickThread("Cannot execute main thread task off-main"); + return this.mainThreadExecutor.executeTask(); + } + + public void raisePriority(final int x, final int z, final Priority priority) { + this.chunkHolderManager.raisePriority(x, z, priority); + } + + public void setPriority(final int x, final int z, final Priority priority) { + this.chunkHolderManager.setPriority(x, z, priority); + } + + public void lowerPriority(final int x, final int z, final Priority priority) { + this.chunkHolderManager.lowerPriority(x, z, priority); + } + + public void scheduleTickingState(final int chunkX, final int chunkZ, final FullChunkStatus toStatus, + final boolean addTicket, final Priority priority, + final Consumer onComplete) { + final int radius = toStatus.ordinal() - 1; // 0 -> BORDER, 1 -> TICKING, 2 -> ENTITY_TICKING + + if (!TickThread.isTickThreadFor(this.world, chunkX, chunkZ, Math.max(0, radius))) { + this.scheduleChunkTask(chunkX, chunkZ, () -> { + ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + }, priority); + return; + } + final int accessRadius = getAccessRadius(toStatus); + if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { + throw new IllegalStateException("Cannot schedule chunk load during ticket level update"); + } + if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { + throw new IllegalStateException("Cannot schedule chunk loading recursively"); + } + + if (toStatus == FullChunkStatus.INACCESSIBLE) { + throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); + } + + final int minLevel = 33 - (toStatus.ordinal() - 1); + final Long chunkReference = addTicket ? getNextChunkLoadId() : null; + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + if (addTicket) { + this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); + this.chunkHolderManager.processTicketUpdates(); + } + + final Consumer loadCallback = onComplete == null && !addTicket ? null : (final LevelChunk chunk) -> { + try { + if (onComplete != null) { + onComplete.accept(chunk); + } + } finally { + if (addTicket) { + ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); + } + } + }; + + final boolean scheduled; + final LevelChunk chunk; + final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); + try { + final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); + try { + final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); + if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { + scheduled = false; + chunk = null; + } else { + final FullChunkStatus currStatus = chunkHolder.getChunkStatus(); + if (currStatus.isOrAfter(toStatus)) { + scheduled = false; + chunk = (LevelChunk)chunkHolder.getCurrentChunk(); + } else { + scheduled = true; + chunk = null; + + for (int dz = -radius; dz <= radius; ++dz) { + for (int dx = -radius; dx <= radius; ++dx) { + final NewChunkHolder neighbour = + (dx | dz) == 0 ? chunkHolder : this.chunkHolderManager.getChunkHolder(dx + chunkX, dz + chunkZ); + if (neighbour != null) { + neighbour.raisePriority(priority); + } + } + } + + // ticket level should schedule for us + if (loadCallback != null) { + chunkHolder.addFullStatusConsumer(toStatus, loadCallback); + } + } + } + } finally { + this.schedulingLockArea.unlock(schedulingLock); + } + } finally { + this.chunkHolderManager.ticketLockArea.unlock(ticketLock); + } + + if (loadCallback != null && !scheduled) { + // couldn't schedule + try { + loadCallback.accept(chunk); + } catch (final Throwable thr) { + LOGGER.error("Failed to process chunk full status callback", thr); + } + } + } + + public void scheduleChunkLoad(final int chunkX, final int chunkZ, final boolean gen, final ChunkStatus toStatus, final boolean addTicket, + final Priority priority, final Consumer onComplete) { + if (gen) { + this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + return; + } + this.scheduleChunkLoad(chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { + if (chunk == null) { + if (onComplete != null) { + onComplete.accept(null); + } + } else { + if (chunk.getPersistedStatus().isOrAfter(toStatus)) { + this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + } else { + if (onComplete != null) { + onComplete.accept(null); + } + } + } + }); + } + + // only appropriate to use with syncLoadNonFull + public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus, + final Priority priority) { + final int accessRadius = getAccessRadius(toStatus); + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus); + final List tasks = new ArrayList<>(); + final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention + try { + final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention + try { + final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); + if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { + return false; + } else { + final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus(); + if (genStatus != null && genStatus.isOrAfter(toStatus)) { + return true; + } else { + chunkHolder.raisePriority(priority); + + if (!chunkHolder.upgradeGenTarget(toStatus)) { + this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); + } + } + } + } finally { + this.schedulingLockArea.unlock(schedulingLock); + } + } finally { + this.chunkHolderManager.ticketLockArea.unlock(ticketLock); + } + + for (int i = 0, len = tasks.size(); i < len; ++i) { + tasks.get(i).schedule(); + } + + return true; + } + + // Note: on Moonrise the non-full sync load requires blocking on managedBlock, but this is fine since there is only + // one main thread. On Folia, it is required that the non-full load can occur completely asynchronously to avoid deadlock + // between regions + public ChunkAccess syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status) { + if (status == null || status.isOrAfter(ChunkStatus.FULL)) { + throw new IllegalArgumentException("Status: " + status); + } + + if (!TickThread.isTickThread()) { + return this.world.getChunkSource().getChunk(chunkX, chunkZ, status, true); + } + + ChunkAccess loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status); + if (loaded != null) { + return loaded; + } + + if (this.hasShutdown()) { + throw new IllegalStateException( + "Chunk system has shut down, cannot process chunk requests in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.world) + "' at " + + "(" + chunkX + "," + chunkZ + ") status: " + status + ); + } + + final Long ticketId = getNextNonFullLoadId(); + final int ticketLevel = getTicketLevel(status); + this.chunkHolderManager.addTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId); + this.chunkHolderManager.processTicketUpdates(); + + this.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, Priority.BLOCKING); + + // we could do a simple spinwait here, since we do not need to process tasks while performing this load + // but we process tasks only because it's a better use of the time spent + this.world.getChunkSource().mainThreadProcessor.managedBlock(() -> { + return ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status) != null; + }); + + loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status); + + this.chunkHolderManager.removeTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId); + + if (loaded == null) { + throw new IllegalStateException("Expected chunk to be loaded for status " + status); + } + + return loaded; + } + + public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket, + final Priority priority, final Consumer onComplete) { + if (!TickThread.isTickThreadFor(this.world, chunkX, chunkZ)) { + this.scheduleChunkTask(chunkX, chunkZ, () -> { + ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + }, priority); + return; + } + final int accessRadius = getAccessRadius(toStatus); + if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { + throw new IllegalStateException("Cannot schedule chunk load during ticket level update"); + } + if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { + throw new IllegalStateException("Cannot schedule chunk loading recursively"); + } + + if (toStatus == ChunkStatus.FULL) { + this.scheduleTickingState(chunkX, chunkZ, FullChunkStatus.FULL, addTicket, priority, (Consumer)onComplete); + return; + } + + final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus); + final Long chunkReference = addTicket ? getNextChunkLoadId() : null; + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + if (addTicket) { + this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); + this.chunkHolderManager.processTicketUpdates(); + } + + final Consumer loadCallback = onComplete == null && !addTicket ? null : (final ChunkAccess chunk) -> { + try { + if (onComplete != null) { + onComplete.accept(chunk); + } + } finally { + if (addTicket) { + ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); + } + } + }; + + final List tasks = new ArrayList<>(); + + final boolean scheduled; + final ChunkAccess chunk; + final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); + try { + final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); + try { + final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); + if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { + scheduled = false; + chunk = null; + } else { + final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus(); + if (genStatus != null && genStatus.isOrAfter(toStatus)) { + scheduled = false; + chunk = chunkHolder.getCurrentChunk(); + } else { + scheduled = true; + chunk = null; + chunkHolder.raisePriority(priority); + + if (!chunkHolder.upgradeGenTarget(toStatus)) { + this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); + } + if (loadCallback != null) { + chunkHolder.addStatusConsumer(toStatus, loadCallback); + } + } + } + } finally { + this.schedulingLockArea.unlock(schedulingLock); + } + } finally { + this.chunkHolderManager.ticketLockArea.unlock(ticketLock); + } + + for (int i = 0, len = tasks.size(); i < len; ++i) { + tasks.get(i).schedule(); + } + + if (loadCallback != null && !scheduled) { + // couldn't schedule + try { + loadCallback.accept(chunk); + } catch (final Throwable thr) { + LOGGER.error("Failed to process chunk status callback", thr); + } + } + } + + private ChunkProgressionTask createTask(final int chunkX, final int chunkZ, final ChunkAccess chunk, + final NewChunkHolder chunkHolder, final StaticCache2D neighbours, + final ChunkStatus toStatus, final Priority initialPriority) { + if (toStatus == ChunkStatus.EMPTY) { + return new ChunkLoadTask(this, this.world, chunkX, chunkZ, chunkHolder, initialPriority); + } + if (toStatus == ChunkStatus.LIGHT) { + return new ChunkLightTask(this, this.world, chunkX, chunkZ, chunk, initialPriority); + } + if (toStatus == ChunkStatus.FULL) { + return new ChunkFullTask(this, this.world, chunkX, chunkZ, chunkHolder, chunk, initialPriority); + } + + return new ChunkUpgradeGenericStatusTask(this, this.world, chunkX, chunkZ, chunk, neighbours, toStatus, initialPriority); + } + + ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, final NewChunkHolder chunkHolder, + final List allTasks) { + return this.schedule(chunkX, chunkZ, targetStatus, chunkHolder, allTasks, chunkHolder.getEffectivePriority(Priority.NORMAL)); + } + + // rets new task scheduled for the _specified_ chunk + // note: this must hold the scheduling lock + // minPriority is only used to pass the priority through to neighbours, as priority calculation has not yet been done + // schedule will ignore the generation target, so it should be checked by the caller to ensure the target is not regressed! + private ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, + final NewChunkHolder chunkHolder, final List allTasks, + final Priority minPriority) { + if (!this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, getAccessRadius(targetStatus))) { + throw new IllegalStateException("Not holding scheduling lock"); + } + + if (chunkHolder.hasGenerationTask()) { + chunkHolder.upgradeGenTarget(targetStatus); + return null; + } + + final Priority requestedPriority = Priority.max( + minPriority, chunkHolder.getEffectivePriority(Priority.NORMAL) + ); + final ChunkStatus currentGenStatus = chunkHolder.getCurrentGenStatus(); + final ChunkAccess chunk = chunkHolder.getCurrentChunk(); + + if (currentGenStatus == null) { + // not yet loaded + final ChunkProgressionTask task = this.createTask( + chunkX, chunkZ, chunk, chunkHolder, null, ChunkStatus.EMPTY, requestedPriority + ); + + allTasks.add(task); + + final List chunkHolderNeighbours = new ArrayList<>(1); + chunkHolderNeighbours.add(chunkHolder); + + chunkHolder.setGenerationTarget(targetStatus); + chunkHolder.setGenerationTask(task, ChunkStatus.EMPTY, chunkHolderNeighbours); + + return task; + } + + if (currentGenStatus.isOrAfter(targetStatus)) { + // nothing to do + return null; + } + + // we know for sure now that we want to schedule _something_, so set the target + chunkHolder.setGenerationTarget(targetStatus); + + final ChunkStatus chunkRealStatus = chunk.getPersistedStatus(); + final ChunkStatus toStatus = ((ChunkSystemChunkStatus)currentGenStatus).moonrise$getNextStatus(); + final ChunkPyramid chunkPyramid = chunkRealStatus.isOrAfter(toStatus) ? ChunkPyramid.LOADING_PYRAMID : ChunkPyramid.GENERATION_PYRAMID; + final ChunkStep chunkStep = chunkPyramid.getStepTo(toStatus); + + final int neighbourReadRadius = Math.max( + 0, + chunkStep.getAccumulatedRadiusOf(ChunkStatus.EMPTY) + ); + + boolean unGeneratedNeighbours = false; + + if (neighbourReadRadius > 0) { + final ChunkMap chunkMap = this.world.getChunkSource().chunkMap; + for (final long pos : ParallelSearchRadiusIteration.getSearchIteration(neighbourReadRadius)) { + final int x = CoordinateUtils.getChunkX(pos); + final int z = CoordinateUtils.getChunkZ(pos); + final int radius = Math.max(Math.abs(x), Math.abs(z)); + final ChunkStatus requiredNeighbourStatus = ((ChunkSystemChunkStep)(Object)chunkStep).moonrise$getRequiredStatusAtRadius(radius); + + unGeneratedNeighbours |= this.checkNeighbour( + chunkX + x, chunkZ + z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority + ); + } + } + + if (unGeneratedNeighbours) { + // can't schedule, but neighbour completion will schedule for us when they're ALL done + + // propagate our priority to neighbours + chunkHolder.recalculateNeighbourPriorities(); + return null; + } + + // need to gather neighbours + + final List chunkHolderNeighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1)); + final StaticCache2D neighbours = StaticCache2D + .create(chunkX, chunkZ, neighbourReadRadius, (final int nx, final int nz) -> { + final NewChunkHolder holder = nx == chunkX && nz == chunkZ ? chunkHolder : this.chunkHolderManager.getChunkHolder(nx, nz); + chunkHolderNeighbours.add(holder); + + return holder.vanillaChunkHolder; + }); + + final ChunkProgressionTask task = this.createTask( + chunkX, chunkZ, chunk, chunkHolder, neighbours, toStatus, + chunkHolder.getEffectivePriority(Priority.NORMAL) + ); + allTasks.add(task); + + chunkHolder.setGenerationTask(task, toStatus, chunkHolderNeighbours); + + return task; + } + + // rets true if the neighbour is not at the required status, false otherwise + private boolean checkNeighbour(final int chunkX, final int chunkZ, final ChunkStatus requiredStatus, final NewChunkHolder center, + final List tasks, final Priority minPriority) { + final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ); + + if (chunkHolder == null) { + throw new IllegalStateException("Missing chunkholder when required"); + } + + final ChunkStatus holderStatus = chunkHolder.getCurrentGenStatus(); + if (holderStatus != null && holderStatus.isOrAfter(requiredStatus)) { + return false; + } + + if (chunkHolder.hasFailedGeneration()) { + return true; + } + + center.addGenerationBlockingNeighbour(chunkHolder); + chunkHolder.addWaitingNeighbour(center, requiredStatus); + + if (chunkHolder.upgradeGenTarget(requiredStatus)) { + return true; + } + + // not at status required, so we need to schedule its generation + this.schedule( + chunkX, chunkZ, requiredStatus, chunkHolder, tasks, minPriority + ); + + return true; + } + + /** + * @deprecated Chunk tasks must be tied to coordinates in the future + */ + @Deprecated + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) { + return this.scheduleChunkTask(run, Priority.NORMAL); + } + + /** + * @deprecated Chunk tasks must be tied to coordinates in the future + */ + @Deprecated + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final Priority priority) { + return this.mainThreadExecutor.queueTask(run, priority); + } + + public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) { + return this.createChunkTask(chunkX, chunkZ, run, Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run, + final Priority priority) { + return this.mainThreadExecutor.createTask(run, priority); + } + + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) { + return this.scheduleChunkTask(chunkX, chunkZ, run, Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run, + final Priority priority) { + return this.mainThreadExecutor.queueTask(run, priority); + } + + public boolean halt(final boolean sync, final long maxWaitNS) { + this.radiusAwareGenExecutor.halt(); + this.parallelGenExecutor.halt(); + this.loadExecutor.halt(); + if (sync) { + final long time = System.nanoTime(); + for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { + if ( + !this.radiusAwareGenExecutor.isActive() && + !this.parallelGenExecutor.isActive() && + !this.loadExecutor.isActive() + ) { + return true; + } + if ((System.nanoTime() - time) >= maxWaitNS) { + return false; + } + } + } + + return true; + } + + public boolean haltIO(final boolean sync, final long maxWaitNS) { + this.ioExecutor.halt(); + this.saveExecutor.halt(); + this.compressionExecutor.halt(); + if (sync) { + final long time = System.nanoTime(); + for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { + if ( + !this.ioExecutor.isActive() && + !this.saveExecutor.isActive() && + !this.compressionExecutor.isActive() + ) { + return true; + } + if ((System.nanoTime() - time) >= maxWaitNS) { + return false; + } + } + } + + return true; + } + + public static final ArrayDeque WAITING_CHUNKS = new ArrayDeque<>(); // stack + + public static final class ChunkInfo { + + public final int chunkX; + public final int chunkZ; + public final ServerLevel world; + + public ChunkInfo(final int chunkX, final int chunkZ, final ServerLevel world) { + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.world = world; + } + + public JsonObject toJson() { + final JsonObject ret = new JsonObject(); + + ret.addProperty("chunk-x", Integer.valueOf(this.chunkX)); + ret.addProperty("chunk-z", Integer.valueOf(this.chunkZ)); + ret.addProperty("world-name", WorldUtil.getWorldName(this.world)); + + return ret; + } + + @Override + public String toString() { + return "[( " + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "']"; + } + } + + public static void pushChunkWait(final ServerLevel world, final int chunkX, final int chunkZ) { + synchronized (WAITING_CHUNKS) { + WAITING_CHUNKS.push(new ChunkInfo(chunkX, chunkZ, world)); + } + } + + public static void popChunkWait() { + synchronized (WAITING_CHUNKS) { + WAITING_CHUNKS.pop(); + } + } + + public static ChunkInfo[] getChunkInfos() { + synchronized (WAITING_CHUNKS) { + return WAITING_CHUNKS.toArray(new ChunkInfo[0]); + } + } + + private static JsonObject debugPlayer(final ServerPlayer player) { + final Level world = player.level(); + + final JsonObject ret = new JsonObject(); + + ret.addProperty("name", player.getScoreboardName()); + ret.addProperty("uuid", player.getUUID().toString()); + ret.addProperty("real", ((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()); + + ret.addProperty("world-name", WorldUtil.getWorldName(world)); + + final Vec3 pos = player.position(); + + ret.addProperty("x", pos.x); + ret.addProperty("y", pos.y); + ret.addProperty("z", pos.z); + + final Entity.RemovalReason removalReason = player.getRemovalReason(); + + ret.addProperty("removal-reason", removalReason == null ? "null" : removalReason.name()); + + ret.add("view-distances", ((ChunkSystemServerPlayer)player).moonrise$getViewDistanceHolder().toJson()); + + return ret; + } + + public JsonObject getDebugJson() { + final JsonObject ret = new JsonObject(); + + ret.addProperty("lock_shift", Integer.valueOf(this.getChunkSystemLockShift())); + ret.addProperty("ticket_shift", Integer.valueOf(ThreadedTicketLevelPropagator.SECTION_SHIFT)); + ret.addProperty("region_shift", Integer.valueOf(((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift())); + + ret.addProperty("name", WorldUtil.getWorldName(this.world)); + ret.addProperty("view-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPIViewDistance()); + ret.addProperty("tick-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPITickDistance()); + ret.addProperty("send-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPISendViewDistance()); + + final JsonArray players = new JsonArray(); + ret.add("players", players); + + for (final ServerPlayer player : this.world.players()) { + players.add(debugPlayer(player)); + } + + ret.add("chunk-holder-manager", this.chunkHolderManager.getDebugJson()); + + return ret; + } + + public static JsonObject debugAllWorlds(final MinecraftServer server) { + final JsonObject ret = new JsonObject(); + + ret.addProperty("data-version", 2); + + final JsonArray allPlayers = new JsonArray(); + ret.add("all-players", allPlayers); + + for (final ServerPlayer player : server.getPlayerList().getPlayers()) { + allPlayers.add(debugPlayer(player)); + } + + final JsonArray chunkWaitInfos = new JsonArray(); + ret.add("chunk-wait-infos", chunkWaitInfos); + + for (final ChunkTaskScheduler.ChunkInfo info : getChunkInfos()) { + chunkWaitInfos.add(info.toJson()); + } + + final JsonArray worlds = new JsonArray(); + ret.add("worlds", worlds); + + for (final ServerLevel world : server.getAllLevels()) { + worlds.add(((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().getDebugJson()); + } + + return ret; + } + + public static File getChunkDebugFile() { + return new File( + new File(new File("."), "debug"), + "chunks-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt" + ); + } + + public static void dumpAllChunkLoadInfo(final MinecraftServer server, final boolean writeDebugInfo) { + final ChunkInfo[] chunkInfos = getChunkInfos(); + if (chunkInfos.length > 0) { + LOGGER.error("Chunk wait task info below: "); + for (final ChunkInfo chunkInfo : chunkInfos) { + final NewChunkHolder holder = ((ChunkSystemServerLevel)chunkInfo.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkInfo.chunkX, chunkInfo.chunkZ); + LOGGER.error("Chunk wait: " + chunkInfo); + LOGGER.error("Chunk holder: " + holder); + } + + if (writeDebugInfo) { + final File file = getChunkDebugFile(); + LOGGER.error("Writing chunk information dump to " + file); + try { + JsonUtil.writeJson(ChunkTaskScheduler.debugAllWorlds(server), file); + LOGGER.error("Successfully written chunk information!"); + } catch (final Throwable thr) { + LOGGER.error("Failed to dump chunk information to file " + file.toString(), thr); + } + } + } + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java new file mode 100644 index 0000000000000000000000000000000000000000..e4a5fa25ed368fc4662c30934da2963ef446d782 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java @@ -0,0 +1,1997 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; + +import ca.spottedleaf.concurrentutil.completable.CallbackCompletable; +import ca.spottedleaf.concurrentutil.executor.Cancellable; +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.PlatformHooks; +import ca.spottedleaf.moonrise.common.misc.LazyRunnable; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager; +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.GenericDataLoadTask; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.progress.ChunkProgressListener; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ImposterProtoChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.storage.SerializableChunkData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.lang.invoke.VarHandle; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public final class NewChunkHolder { + + private static final Logger LOGGER = LoggerFactory.getLogger(NewChunkHolder.class); + + public final ChunkData holderData; + + public final ServerLevel world; + public final int chunkX; + public final int chunkZ; + + public final ChunkTaskScheduler scheduler; + + // load/unload state + + // chunk data state + + private ChunkEntitySlices entityChunk; + // entity chunk that is loaded, but not yet deserialized + private CompoundTag pendingEntityChunk; + + ChunkEntitySlices loadInEntityChunk(final boolean transientChunk) { + TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot sync load entity data off-main"); + final CompoundTag entityChunk; + final ChunkEntitySlices ret; + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + if (this.entityChunk != null && (transientChunk || !this.entityChunk.isTransient())) { + return this.entityChunk; + } + final CompoundTag pendingEntityChunk = this.pendingEntityChunk; + if (!transientChunk && pendingEntityChunk == null) { + throw new IllegalStateException("Must load entity data from disk before loading in the entity chunk!"); + } + + if (this.entityChunk == null) { + ret = this.entityChunk = new ChunkEntitySlices( + this.world, this.chunkX, this.chunkZ, this.getChunkStatus(), + this.holderData, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) + ); + + ret.setTransient(transientChunk); + + ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().entitySectionLoad(this.chunkX, this.chunkZ, ret); + } else { + // transientChunk = false here + ret = this.entityChunk; + this.entityChunk.setTransient(false); + } + + if (!transientChunk) { + this.pendingEntityChunk = null; + entityChunk = pendingEntityChunk == EMPTY_ENTITY_CHUNK ? null : pendingEntityChunk; + } else { + entityChunk = null; + } + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + + if (!transientChunk) { + if (entityChunk != null) { + final List entities = ChunkEntitySlices.readEntities(this.world, entityChunk); + + ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().addEntityChunkEntities(entities, new ChunkPos(this.chunkX, this.chunkZ)); + } + } + + return ret; + } + + // needed to distinguish whether the entity chunk has been read from disk but is empty or whether it has _not_ + // been read from disk + private static final CompoundTag EMPTY_ENTITY_CHUNK = new CompoundTag(); + + private ChunkLoadTask.EntityDataLoadTask entityDataLoadTask; + // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0, + // then the task is rescheduled + private List entityDataLoadTaskWaiters; + + public ChunkLoadTask.EntityDataLoadTask getEntityDataLoadTask() { + return this.entityDataLoadTask; + } + + // must hold schedule lock for the two below functions + + // returns only if the data has been loaded from disk, DOES NOT relate to whether it has been deserialized + // or added into the world (or even into entityChunk) + public boolean isEntityChunkNBTLoaded() { + return (this.entityChunk != null && !this.entityChunk.isTransient()) || this.pendingEntityChunk != null; + } + + private void completeEntityLoad(final GenericDataLoadTask.TaskResult result) { + final List completeWaiters; + ChunkLoadTask.EntityDataLoadTask entityDataLoadTask = null; + boolean scheduleEntityTask = false; + ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + final List waiters = this.entityDataLoadTaskWaiters; + this.entityDataLoadTask = null; + if (result != null) { + this.entityDataLoadTaskWaiters = null; + this.pendingEntityChunk = result.left() == null ? EMPTY_ENTITY_CHUNK : result.left(); + if (result.right() != null) { + LOGGER.error("Unhandled entity data load exception, data data will be lost: ", result.right()); + } + + for (final GenericDataLoadTaskCallback callback : waiters) { + callback.markCompleted(); + } + + completeWaiters = waiters; + } else { + // cancelled + completeWaiters = null; + + // need to re-schedule? + if (waiters.isEmpty()) { + this.entityDataLoadTaskWaiters = null; + // no tasks to schedule _for_ + } else { + entityDataLoadTask = this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( + this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL) + ); + entityDataLoadTask.addCallback(this::completeEntityLoad); + // need one schedule() per waiter + for (final GenericDataLoadTaskCallback callback : waiters) { + scheduleEntityTask |= entityDataLoadTask.schedule(true); + } + } + } + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + + if (scheduleEntityTask) { + entityDataLoadTask.scheduleNow(); + } + + // avoid holding the scheduling lock while completing + if (completeWaiters != null) { + for (final GenericDataLoadTaskCallback callback : completeWaiters) { + callback.acceptCompleted(result); + } + } + + schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + this.checkUnload(); + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + } + + // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held + // however, when the consumer is invoked, it will hold the schedule lock + public GenericDataLoadTaskCallback getOrLoadEntityData(final Consumer> consumer) { + if (this.isEntityChunkNBTLoaded()) { + throw new IllegalStateException("Cannot load entity data, it is already loaded"); + } + // why not just acquire the lock? because the caller NEEDS to call isEntityChunkNBTLoaded before this! + if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) { + throw new IllegalStateException("Must hold scheduling lock"); + } + + final GenericDataLoadTaskCallback ret = new EntityDataLoadTaskCallback((Consumer)consumer, this); + + if (this.entityDataLoadTask == null) { + this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( + this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL) + ); + this.entityDataLoadTask.addCallback(this::completeEntityLoad); + this.entityDataLoadTaskWaiters = new ArrayList<>(); + } + this.entityDataLoadTaskWaiters.add(ret); + if (this.entityDataLoadTask.schedule(true)) { + ret.schedule = this.entityDataLoadTask; + } + this.checkUnload(); + + return ret; + } + + private static final class EntityDataLoadTaskCallback extends GenericDataLoadTaskCallback { + + public EntityDataLoadTaskCallback(final Consumer> consumer, final NewChunkHolder chunkHolder) { + super(consumer, chunkHolder); + } + + @Override + void internalCancel() { + this.chunkHolder.entityDataLoadTaskWaiters.remove(this); + this.chunkHolder.entityDataLoadTask.cancel(); + } + } + + private PoiChunk poiChunk; + + private ChunkLoadTask.PoiDataLoadTask poiDataLoadTask; + // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0, + // then the task is rescheduled + private List poiDataLoadTaskWaiters; + + public ChunkLoadTask.PoiDataLoadTask getPoiDataLoadTask() { + return this.poiDataLoadTask; + } + + // must hold schedule lock for the two below functions + + public boolean isPoiChunkLoaded() { + return this.poiChunk != null; + } + + private void completePoiLoad(final GenericDataLoadTask.TaskResult result) { + final List completeWaiters; + ChunkLoadTask.PoiDataLoadTask poiDataLoadTask = null; + boolean schedulePoiTask = false; + ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + final List waiters = this.poiDataLoadTaskWaiters; + this.poiDataLoadTask = null; + if (result != null) { + this.poiDataLoadTaskWaiters = null; + this.poiChunk = result.left(); + if (result.right() != null) { + LOGGER.error("Unhandled poi load exception, poi data will be lost: ", result.right()); + } + + for (final GenericDataLoadTaskCallback callback : waiters) { + callback.markCompleted(); + } + + completeWaiters = waiters; + } else { + // cancelled + completeWaiters = null; + + // need to re-schedule? + if (waiters.isEmpty()) { + this.poiDataLoadTaskWaiters = null; + // no tasks to schedule _for_ + } else { + poiDataLoadTask = this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( + this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL) + ); + poiDataLoadTask.addCallback(this::completePoiLoad); + // need one schedule() per waiter + for (final GenericDataLoadTaskCallback callback : waiters) { + schedulePoiTask |= poiDataLoadTask.schedule(true); + } + } + } + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + + if (schedulePoiTask) { + poiDataLoadTask.scheduleNow(); + } + + // avoid holding the scheduling lock while completing + if (completeWaiters != null) { + for (final GenericDataLoadTaskCallback callback : completeWaiters) { + callback.acceptCompleted(result); + } + } + schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + this.checkUnload(); + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + } + + // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held + // however, when the consumer is invoked, it will hold the schedule lock + public GenericDataLoadTaskCallback getOrLoadPoiData(final Consumer> consumer) { + if (this.isPoiChunkLoaded()) { + throw new IllegalStateException("Cannot load poi data, it is already loaded"); + } + // why not just acquire the lock? because the caller NEEDS to call isPoiChunkLoaded before this! + if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) { + throw new IllegalStateException("Must hold scheduling lock"); + } + + final GenericDataLoadTaskCallback ret = new PoiDataLoadTaskCallback((Consumer)consumer, this); + + if (this.poiDataLoadTask == null) { + this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( + this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL) + ); + this.poiDataLoadTask.addCallback(this::completePoiLoad); + this.poiDataLoadTaskWaiters = new ArrayList<>(); + } + this.poiDataLoadTaskWaiters.add(ret); + if (this.poiDataLoadTask.schedule(true)) { + ret.schedule = this.poiDataLoadTask; + } + this.checkUnload(); + + return ret; + } + + private static final class PoiDataLoadTaskCallback extends GenericDataLoadTaskCallback { + + public PoiDataLoadTaskCallback(final Consumer> consumer, final NewChunkHolder chunkHolder) { + super(consumer, chunkHolder); + } + + @Override + void internalCancel() { + this.chunkHolder.poiDataLoadTaskWaiters.remove(this); + this.chunkHolder.poiDataLoadTask.cancel(); + } + } + + public static abstract class GenericDataLoadTaskCallback implements Cancellable { + + protected final Consumer> consumer; + protected final NewChunkHolder chunkHolder; + protected boolean completed; + protected GenericDataLoadTask schedule; + protected final AtomicBoolean scheduled = new AtomicBoolean(); + + public GenericDataLoadTaskCallback(final Consumer> consumer, + final NewChunkHolder chunkHolder) { + this.consumer = consumer; + this.chunkHolder = chunkHolder; + } + + public void schedule() { + if (this.scheduled.getAndSet(true)) { + throw new IllegalStateException("Double calling schedule()"); + } + if (this.schedule != null) { + this.schedule.scheduleNow(); + this.schedule = null; + } + } + + boolean isCompleted() { + return this.completed; + } + + // must hold scheduling lock + private boolean setCompleted() { + if (this.completed) { + return false; + } + return this.completed = true; + } + + // must hold scheduling lock + void markCompleted() { + if (this.completed) { + throw new IllegalStateException("May not be completed here"); + } + this.completed = true; + } + + void acceptCompleted(final GenericDataLoadTask.TaskResult result) { + if (result != null) { + if (this.completed) { + this.consumer.accept(result); + } else { + throw new IllegalStateException("Cannot be uncompleted at this point"); + } + } else { + throw new NullPointerException("Result cannot be null (cancelled)"); + } + } + + // holds scheduling lock + abstract void internalCancel(); + + @Override + public boolean cancel() { + final NewChunkHolder holder = this.chunkHolder; + final ReentrantAreaLock.Node schedulingLock = holder.scheduler.schedulingLockArea.lock(holder.chunkX, holder.chunkZ); + try { + if (!this.completed) { + this.completed = true; + this.internalCancel(); + return true; + } + return false; + } finally { + holder.scheduler.schedulingLockArea.unlock(schedulingLock); + } + } + } + + private ChunkAccess currentChunk; + + // generation status state + + /** + * Current status the chunk has been brought up to by the chunk system. null indicates no work at all + */ + private ChunkStatus currentGenStatus; + + // This allows lockless access to the chunk and last gen status + private static final ChunkStatus[] ALL_STATUSES = ChunkStatus.getStatusList().toArray(new ChunkStatus[0]); + + public static final record ChunkCompletion(ChunkAccess chunk, ChunkStatus genStatus) {}; + private static final VarHandle CHUNK_COMPLETION_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(ChunkCompletion[].class); + private final ChunkCompletion[] chunkCompletions = new ChunkCompletion[ALL_STATUSES.length]; + + private volatile ChunkCompletion lastChunkCompletion; + + public ChunkCompletion getLastChunkCompletion() { + return this.lastChunkCompletion; + } + + public ChunkAccess getChunkIfPresentUnchecked(final ChunkStatus status) { + final ChunkCompletion completion = (ChunkCompletion)CHUNK_COMPLETION_ARRAY_HANDLE.getVolatile(this.chunkCompletions, status.getIndex()); + return completion == null ? null : completion.chunk; + } + + public ChunkAccess getChunkIfPresent(final ChunkStatus status) { + final ChunkStatus maxStatus = ChunkLevel.generationStatus(this.getTicketLevel()); + + if (maxStatus == null || status.isAfter(maxStatus)) { + return null; + } + + return this.getChunkIfPresentUnchecked(status); + } + + public void replaceProtoChunk(final ImposterProtoChunk imposterProtoChunk) { + for (int i = 0, max = ChunkStatus.FULL.getIndex(); i < max; ++i) { + CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, i, new ChunkCompletion(imposterProtoChunk, ALL_STATUSES[i])); + } + } + + /** + * The target final chunk status the chunk system will bring the chunk to. + */ + private ChunkStatus requestedGenStatus; + + private ChunkProgressionTask generationTask; + private ChunkStatus generationTaskStatus; + + /** + * contains the neighbours that this chunk generation is blocking on + */ + private final ReferenceLinkedOpenHashSet neighboursBlockingGenTask = new ReferenceLinkedOpenHashSet<>(4); + + /** + * map of ChunkHolder -> Required Status for this chunk + */ + private final Reference2ObjectLinkedOpenHashMap neighboursWaitingForUs = new Reference2ObjectLinkedOpenHashMap<>(); + + public void addGenerationBlockingNeighbour(final NewChunkHolder neighbour) { + this.neighboursBlockingGenTask.add(neighbour); + } + + public void addWaitingNeighbour(final NewChunkHolder neighbour, final ChunkStatus requiredStatus) { + final boolean wasEmpty = this.neighboursWaitingForUs.isEmpty(); + this.neighboursWaitingForUs.put(neighbour, requiredStatus); + if (wasEmpty) { + this.checkUnload(); + } + } + + // priority state + + // the target priority for this chunk to generate at + private Priority priority = null; + private boolean priorityLocked; + + // the priority neighbouring chunks have requested this chunk generate at + private Priority neighbourRequestedPriority = null; + + public Priority getEffectivePriority(final Priority dfl) { + final Priority neighbour = this.neighbourRequestedPriority; + final Priority us = this.priority; + + if (neighbour == null) { + return us == null ? dfl : us; + } + if (us == null) { + return neighbour; + } + + return Priority.max(us, neighbour); + } + + private void recalculateNeighbourRequestedPriority() { + if (this.neighboursWaitingForUs.isEmpty()) { + this.neighbourRequestedPriority = null; + return; + } + + Priority max = null; + + for (final NewChunkHolder holder : this.neighboursWaitingForUs.keySet()) { + final Priority neighbourPriority = holder.getEffectivePriority(null); + if (neighbourPriority != null && (max == null || neighbourPriority.isHigherPriority(max))) { + max = neighbourPriority; + } + } + + final Priority current = this.getEffectivePriority(Priority.NORMAL); + this.neighbourRequestedPriority = max; + final Priority next = this.getEffectivePriority(Priority.NORMAL); + + if (current == next) { + return; + } + + // our effective priority has changed, so change our task + if (this.generationTask != null) { + this.generationTask.setPriority(next); + } + + // now propagate this to our neighbours + this.recalculateNeighbourPriorities(); + } + + public void recalculateNeighbourPriorities() { + for (final NewChunkHolder holder : this.neighboursBlockingGenTask) { + holder.recalculateNeighbourRequestedPriority(); + } + } + + // must hold scheduling lock + public void raisePriority(final Priority priority) { + if (this.priority != null && this.priority.isHigherOrEqualPriority(priority)) { + return; + } + this.setPriority(priority); + } + + private void lockPriority() { + this.priority = null; + this.priorityLocked = true; + } + + // must hold scheduling lock + public void setPriority(final Priority priority) { + if (this.priorityLocked) { + return; + } + final Priority old = this.getEffectivePriority(null); + this.priority = priority; + final Priority newPriority = this.getEffectivePriority(Priority.NORMAL); + + if (old != newPriority) { + if (this.generationTask != null) { + this.generationTask.setPriority(newPriority); + } + } + + this.recalculateNeighbourPriorities(); + } + + // must hold scheduling lock + public void lowerPriority(final Priority priority) { + if (this.priority != null && this.priority.isLowerOrEqualPriority(priority)) { + return; + } + this.setPriority(priority); + } + + // error handling state + private ChunkStatus failedGenStatus; + private Throwable genTaskException; + private Thread genTaskFailedThread; + + private boolean failedLightUpdate; + + public void failedLightUpdate() { + this.failedLightUpdate = true; + } + + public boolean hasFailedGeneration() { + return this.genTaskException != null; + } + + // ticket level state + private int oldTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1; + private int currentTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1; + + public int getTicketLevel() { + return this.currentTicketLevel; + } + + public final ChunkHolder vanillaChunkHolder; + + public NewChunkHolder(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkTaskScheduler scheduler) { + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.scheduler = scheduler; + this.vanillaChunkHolder = new ChunkHolder( + new ChunkPos(chunkX, chunkZ), ChunkHolderManager.MAX_TICKET_LEVEL, world, + world.getLightEngine(), null, world.getChunkSource().chunkMap + ); + ((ChunkSystemChunkHolder)this.vanillaChunkHolder).moonrise$setRealChunkHolder(this); + this.holderData = ((ChunkSystemLevel)this.world).moonrise$requestChunkData(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } + + public ChunkAccess getCurrentChunk() { + return this.currentChunk; + } + + int getCurrentTicketLevel() { + return this.currentTicketLevel; + } + + void updateTicketLevel(final int toLevel) { + this.currentTicketLevel = toLevel; + } + + private int totalNeighboursUsingThisChunk = 0; + + // holds schedule lock + public void addNeighbourUsingChunk() { + final int now = ++this.totalNeighboursUsingThisChunk; + + if (now == 1) { + this.checkUnload(); + } + } + + // holds schedule lock + public void removeNeighbourUsingChunk() { + final int now = --this.totalNeighboursUsingThisChunk; + + if (now == 0) { + this.checkUnload(); + } + + if (now < 0) { + throw new IllegalStateException("Neighbours using this chunk cannot be negative"); + } + } + + // must hold scheduling lock + // returns string reason for why chunk should remain loaded, null otherwise + public final String isSafeToUnload() { + // is ticket level below threshold? + if (this.oldTicketLevel <= ChunkHolderManager.MAX_TICKET_LEVEL) { + return "ticket_level"; + } + + // are we being used by another chunk for generation? + if (this.totalNeighboursUsingThisChunk != 0) { + return "neighbours_generating"; + } + + // are we going to be used by another chunk for generation? + if (!this.neighboursWaitingForUs.isEmpty()) { + return "neighbours_waiting"; + } + + // chunk must be marked inaccessible (i.e. unloaded to plugins) + if (this.getChunkStatus() != FullChunkStatus.INACCESSIBLE) { + return "fullchunkstatus"; + } + + // are we currently generating anything, or have requested generation? + if (this.generationTask != null) { + return "generating"; + } + if (this.requestedGenStatus != null) { + return "requested_generation"; + } + + // entity data requested? + if (this.entityDataLoadTask != null) { + return "entity_data_requested"; + } + + // poi data requested? + if (this.poiDataLoadTask != null) { + return "poi_data_requested"; + } + + // are we pending serialization? + if (this.entityDataUnload != null) { + return "entity_serialization"; + } + if (this.poiDataUnload != null) { + return "poi_serialization"; + } + if (this.chunkDataUnload != null) { + return "chunk_serialization"; + } + + // Note: light tasks do not need a check, as they add a ticket. + + // nothing is using this chunk, so it should be unloaded + return null; + } + + /** Unloaded from chunk map */ + private boolean unloaded; + + void onUnload() { + this.unloaded = true; + ((ChunkSystemLevel)this.world).moonrise$releaseChunkData(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ)); + } + + private boolean inUnloadQueue = false; + + void removeFromUnloadQueue() { + this.inUnloadQueue = false; + } + + // must hold scheduling lock + private void checkUnload() { + if (this.unloaded) { + return; + } + if (this.isSafeToUnload() == null) { + // ensure in unload queue + if (!this.inUnloadQueue) { + this.inUnloadQueue = true; + this.scheduler.chunkHolderManager.unloadQueue.addChunk(this.chunkX, this.chunkZ); + } + } else { + // ensure not in unload queue + if (this.inUnloadQueue) { + this.inUnloadQueue = false; + this.scheduler.chunkHolderManager.unloadQueue.removeChunk(this.chunkX, this.chunkZ); + } + } + } + + static final record UnloadState(NewChunkHolder holder, ChunkAccess chunk, ChunkEntitySlices entityChunk, PoiChunk poiChunk) {}; + + // note: these are completed with null to indicate that no write occurred + // they are also completed with null to indicate a null write occurred + private UnloadTask chunkDataUnload; + private UnloadTask entityDataUnload; + private UnloadTask poiDataUnload; + + public static final record UnloadTask(CallbackCompletable completable, PrioritisedExecutor.PrioritisedTask task, + LazyRunnable toRun) {} + + public UnloadTask getUnloadTask(final MoonriseRegionFileIO.RegionFileType type) { + switch (type) { + case CHUNK_DATA: + return this.chunkDataUnload; + case ENTITY_DATA: + return this.entityDataUnload; + case POI_DATA: + return this.poiDataUnload; + default: + throw new IllegalStateException("Unknown regionfile type " + type); + } + } + + private void removeUnloadTask(final MoonriseRegionFileIO.RegionFileType type) { + switch (type) { + case CHUNK_DATA: { + this.chunkDataUnload = null; + return; + } + case ENTITY_DATA: { + this.entityDataUnload = null; + return; + } + case POI_DATA: { + this.poiDataUnload = null; + return; + } + default: + throw new IllegalStateException("Unknown regionfile type " + type); + } + } + + private UnloadState unloadState; + + // holds schedule lock + UnloadState unloadStage1() { + // because we hold the scheduling lock, we cannot actually unload anything + // so, what we do here instead is to null this chunk's state and setup the unload tasks + // the unload tasks will ensure that any loads that take place after stage1 (i.e during stage2, in which + // we do not hold the lock) c + final ChunkAccess chunk = this.currentChunk; + final ChunkEntitySlices entityChunk = this.entityChunk; + final PoiChunk poiChunk = this.poiChunk; + // chunk state + this.currentChunk = null; + this.currentGenStatus = null; + for (int i = 0; i < this.chunkCompletions.length; ++i) { + CHUNK_COMPLETION_ARRAY_HANDLE.setRelease(this.chunkCompletions, i, (ChunkCompletion)null); + } + this.lastChunkCompletion = null; + // entity chunk state + this.entityChunk = null; + this.pendingEntityChunk = null; + + // poi chunk state + this.poiChunk = null; + + // priority state + this.priorityLocked = false; + + if (chunk != null) { + final LazyRunnable toRun = new LazyRunnable(); + this.chunkDataUnload = new UnloadTask(new CallbackCompletable<>(), this.scheduler.saveExecutor.createTask(toRun), toRun); + } + if (poiChunk != null) { + this.poiDataUnload = new UnloadTask(new CallbackCompletable<>(), null, null); + } + if (entityChunk != null) { + this.entityDataUnload = new UnloadTask(new CallbackCompletable<>(), null, null); + } + + return this.unloadState = (chunk != null || entityChunk != null || poiChunk != null) ? new UnloadState(this, chunk, entityChunk, poiChunk) : null; + } + + // data is null if failed or does not need to be saved + void completeAsyncUnloadDataSave(final MoonriseRegionFileIO.RegionFileType type, final CompoundTag data) { + if (data != null) { + MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, data, type); + } + + this.getUnloadTask(type).completable().complete(data); + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + // can only write to these fields while holding the schedule lock + this.removeUnloadTask(type); + this.checkUnload(); + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + } + + void unloadStage2(final UnloadState state) { + this.unloadState = null; + final ChunkAccess chunk = state.chunk(); + final ChunkEntitySlices entityChunk = state.entityChunk(); + final PoiChunk poiChunk = state.poiChunk(); + + final boolean shouldLevelChunkNotSave = PlatformHooks.get().forceNoSave(chunk); + + // unload chunk data + if (chunk != null) { + if (chunk instanceof LevelChunk levelChunk) { + levelChunk.setLoaded(false); + PlatformHooks.get().chunkUnloadFromWorld(levelChunk); + } + + if (!shouldLevelChunkNotSave) { + this.saveChunk(chunk, true); + } else { + this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, null); + } + + if (chunk instanceof LevelChunk levelChunk) { + this.world.unload(levelChunk); + } + } + + // unload entity data + if (entityChunk != null) { + this.saveEntities(entityChunk, true); + // yes this is a hack to pass the compound tag through... + final CompoundTag lastEntityUnload = this.lastEntityUnload; + this.lastEntityUnload = null; + + if (entityChunk.unload()) { + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + entityChunk.setTransient(true); + this.entityChunk = entityChunk; + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + } else { + ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().entitySectionUnload(this.chunkX, this.chunkZ); + } + // we need to delay the callback until after determining transience, otherwise a potential loader could + // set entityChunk before we do + this.entityDataUnload.completable().complete(lastEntityUnload); + } + + // unload poi data + if (poiChunk != null) { + if (poiChunk.isDirty() && !shouldLevelChunkNotSave) { + this.savePOI(poiChunk, true); + } else { + this.poiDataUnload.completable().complete(null); + } + + if (poiChunk.isLoaded()) { + ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$onUnload(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ)); + } + } + } + + boolean unloadStage3() { + // can only write to these while holding the schedule lock, and we instantly complete them in stage2 + this.poiDataUnload = null; + this.entityDataUnload = null; + + // we need to check if anything has been loaded in the meantime (or if we have transient entities) + if (this.entityChunk != null || this.poiChunk != null || this.currentChunk != null) { + return false; + } + + return this.isSafeToUnload() == null; + } + + private void cancelGenTask() { + if (this.generationTask != null) { + this.generationTask.cancel(); + } else { + // otherwise, we are blocking on neighbours, so remove them + if (!this.neighboursBlockingGenTask.isEmpty()) { + for (final NewChunkHolder neighbour : this.neighboursBlockingGenTask) { + if (neighbour.neighboursWaitingForUs.remove(this) == null) { + throw new IllegalStateException("Corrupt state"); + } + if (neighbour.neighboursWaitingForUs.isEmpty()) { + neighbour.checkUnload(); + } + } + this.neighboursBlockingGenTask.clear(); + this.checkUnload(); + } + } + } + + // holds: ticket level update lock + // holds: schedule lock + public void processTicketLevelUpdate(final List scheduledTasks, final List changedLoadStatus) { + final int oldLevel = this.oldTicketLevel; + final int newLevel = this.currentTicketLevel; + + if (oldLevel == newLevel) { + return; + } + + this.oldTicketLevel = newLevel; + + final FullChunkStatus oldState = ChunkLevel.fullStatus(oldLevel); + final FullChunkStatus newState = ChunkLevel.fullStatus(newLevel); + final boolean oldUnloaded = oldLevel > ChunkHolderManager.MAX_TICKET_LEVEL; + final boolean newUnloaded = newLevel > ChunkHolderManager.MAX_TICKET_LEVEL; + + final ChunkStatus maxGenerationStatusOld = ChunkLevel.generationStatus(oldLevel); + final ChunkStatus maxGenerationStatusNew = ChunkLevel.generationStatus(newLevel); + + // check for cancellations from downgrading ticket level + if (this.requestedGenStatus != null && !newState.isOrAfter(FullChunkStatus.FULL) && newLevel > oldLevel) { + // note: cancel() may invoke onChunkGenComplete synchronously here + if (newUnloaded) { + // need to cancel all tasks + // note: requested status must be set to null here before cancellation, to indicate to the + // completion logic that we do not want rescheduling to occur + this.requestedGenStatus = null; + this.cancelGenTask(); + } else { + final ChunkStatus toCancel = ((ChunkSystemChunkStatus)maxGenerationStatusNew).moonrise$getNextStatus(); + final ChunkStatus currentRequestedStatus = this.requestedGenStatus; + + if (currentRequestedStatus.isOrAfter(toCancel)) { + // we do have to cancel something here + // clamp requested status to the maximum + if (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(maxGenerationStatusNew)) { + // already generated to status, so we must cancel + this.requestedGenStatus = null; + this.cancelGenTask(); + } else { + // not generated to status, so we may have to cancel + // note: gen task is always 1 status above current gen status if not null + this.requestedGenStatus = maxGenerationStatusNew; + if (this.generationTaskStatus != null && this.generationTaskStatus.isOrAfter(toCancel)) { + // TOOD is this even possible? i don't think so + throw new IllegalStateException("?????"); + } + } + } + } + } + + if (oldState != newState) { + if (newState.isOrAfter(oldState)) { + // status upgrade + if (!oldState.isOrAfter(FullChunkStatus.FULL) && newState.isOrAfter(FullChunkStatus.FULL)) { + // may need to schedule full load + if (this.currentGenStatus != ChunkStatus.FULL) { + if (this.requestedGenStatus != null) { + this.requestedGenStatus = ChunkStatus.FULL; + } else { + this.scheduler.schedule( + this.chunkX, this.chunkZ, ChunkStatus.FULL, this, scheduledTasks + ); + } + } + } + } else { + // status downgrade + if (!newState.isOrAfter(FullChunkStatus.ENTITY_TICKING) && oldState.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { + this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, null); + } + + if (!newState.isOrAfter(FullChunkStatus.BLOCK_TICKING) && oldState.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { + this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, null); + } + + if (!newState.isOrAfter(FullChunkStatus.FULL) && oldState.isOrAfter(FullChunkStatus.FULL)) { + this.completeFullStatusConsumers(FullChunkStatus.FULL, null); + } + } + + if (this.updatePendingStatus()) { + changedLoadStatus.add(this); + } + } + + if (oldUnloaded != newUnloaded) { + this.checkUnload(); + } + + // Don't really have a choice but to place this hook here + PlatformHooks.get().onChunkHolderTicketChange(this.world, this.vanillaChunkHolder, oldLevel, newLevel); + } + + static final int NEIGHBOUR_RADIUS = 2; + private long fullNeighbourChunksLoadedBitset; + + private static int getFullNeighbourIndex(final int relativeX, final int relativeZ) { + // index = (relativeX + NEIGHBOUR_CACHE_RADIUS) + (relativeZ + NEIGHBOUR_CACHE_RADIUS) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1) + // optimised variant of the above by moving some of the ops to compile time + return relativeX + (relativeZ * (NEIGHBOUR_RADIUS * 2 + 1)) + (NEIGHBOUR_RADIUS + NEIGHBOUR_RADIUS * ((NEIGHBOUR_RADIUS * 2 + 1))); + } + public final boolean isNeighbourFullLoaded(final int relativeX, final int relativeZ) { + return (this.fullNeighbourChunksLoadedBitset & (1L << getFullNeighbourIndex(relativeX, relativeZ))) != 0; + } + + // returns true if this chunk changed pending full status + // must hold scheduling lock + public final boolean setNeighbourFullLoaded(final int relativeX, final int relativeZ) { + final int index = getFullNeighbourIndex(relativeX, relativeZ); + this.fullNeighbourChunksLoadedBitset |= (1L << index); + return this.updatePendingStatus(); + } + + // returns true if this chunk changed pending full status + // must hold scheduling lock + public final boolean setNeighbourFullUnloaded(final int relativeX, final int relativeZ) { + final int index = getFullNeighbourIndex(relativeX, relativeZ); + this.fullNeighbourChunksLoadedBitset &= ~(1L << index); + return this.updatePendingStatus(); + } + + private static long getLoadedMask(final int radius) { + long mask = 0L; + for (int dx = -radius; dx <= radius; ++dx) { + for (int dz = -radius; dz <= radius; ++dz) { + mask |= (1L << getFullNeighbourIndex(dx, dz)); + } + } + + return mask; + } + + private static final long CHUNK_LOADED_MASK_RAD0 = getLoadedMask(0); + private static final long CHUNK_LOADED_MASK_RAD1 = getLoadedMask(1); + private static final long CHUNK_LOADED_MASK_RAD2 = getLoadedMask(2); + + // only updated while holding scheduling lock + private FullChunkStatus pendingFullChunkStatus = FullChunkStatus.INACCESSIBLE; + // updated while holding no locks, but adds a ticket before to prevent pending status from dropping + // so, current will never update to a value higher than pending + private FullChunkStatus currentFullChunkStatus = FullChunkStatus.INACCESSIBLE; + + public FullChunkStatus getChunkStatus() { + // no volatile access, access off-main is considered racey anyways + return this.currentFullChunkStatus; + } + + public boolean isEntityTickingReady() { + return this.getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING); + } + + public boolean isTickingReady() { + return this.getChunkStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING); + } + + public boolean isFullChunkReady() { + return this.getChunkStatus().isOrAfter(FullChunkStatus.FULL); + } + + private static FullChunkStatus getStatusForBitset(final long bitset) { + if ((bitset & CHUNK_LOADED_MASK_RAD2) == CHUNK_LOADED_MASK_RAD2) { + return FullChunkStatus.ENTITY_TICKING; + } else if ((bitset & CHUNK_LOADED_MASK_RAD1) == CHUNK_LOADED_MASK_RAD1) { + return FullChunkStatus.BLOCK_TICKING; + } else if ((bitset & CHUNK_LOADED_MASK_RAD0) == CHUNK_LOADED_MASK_RAD0) { + return FullChunkStatus.FULL; + } else { + return FullChunkStatus.INACCESSIBLE; + } + } + + // must hold scheduling lock + // returns whether the pending status was changed + private boolean updatePendingStatus() { + final FullChunkStatus byTicketLevel = ChunkLevel.fullStatus(this.oldTicketLevel); // oldTicketLevel is controlled by scheduling lock + + FullChunkStatus pending = getStatusForBitset(this.fullNeighbourChunksLoadedBitset); + if (pending == FullChunkStatus.INACCESSIBLE && byTicketLevel.isOrAfter(FullChunkStatus.FULL) && this.currentGenStatus == ChunkStatus.FULL) { + // the bitset is only for chunks that have gone through the status updater + // but here we are ready to go to FULL + pending = FullChunkStatus.FULL; + } + + if (pending.isOrAfter(byTicketLevel)) { // pending >= byTicketLevel + // cannot set above ticket level + pending = byTicketLevel; + } + + if (this.pendingFullChunkStatus == pending) { + return false; + } + + this.pendingFullChunkStatus = pending; + + return true; + } + + private void onFullChunkLoadChange(final boolean loaded, final List changedFullStatus) { + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, NEIGHBOUR_RADIUS); + try { + for (int dz = -NEIGHBOUR_RADIUS; dz <= NEIGHBOUR_RADIUS; ++dz) { + for (int dx = -NEIGHBOUR_RADIUS; dx <= NEIGHBOUR_RADIUS; ++dx) { + final NewChunkHolder holder = (dx | dz) == 0 ? this : this.scheduler.chunkHolderManager.getChunkHolder(dx + this.chunkX, dz + this.chunkZ); + if (loaded) { + if (holder.setNeighbourFullLoaded(-dx, -dz)) { + changedFullStatus.add(holder); + } + } else { + if (holder != null && holder.setNeighbourFullUnloaded(-dx, -dz)) { + changedFullStatus.add(holder); + } + } + } + } + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + } + + private void changeEntityChunkStatus(final FullChunkStatus toStatus) { + ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().chunkStatusChange(this.chunkX, this.chunkZ, toStatus); + } + + private boolean processingFullStatus = false; + + private void updateCurrentState(final FullChunkStatus to) { + this.currentFullChunkStatus = to; + } + + // only to be called on the main thread, no locks need to be held + public boolean handleFullStatusChange(final List changedFullStatus) { + TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot update full status thread off-main"); + + boolean ret = false; + + if (this.processingFullStatus) { + // we cannot process updates recursively, as we may be in the middle of logic to upgrade/downgrade status + return ret; + } + + this.processingFullStatus = true; + try { + for (;;) { + // check if we have any remaining work to do + + // we do not need to hold the scheduling lock to read pending, as changes to pending + // will queue a status update + + final FullChunkStatus pending = this.pendingFullChunkStatus; + FullChunkStatus current = this.currentFullChunkStatus; + + if (pending == current) { + if (pending == FullChunkStatus.INACCESSIBLE) { + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + this.checkUnload(); + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + } + return ret; + } + + ret = true; + + // note: because the chunk system delays any ticket downgrade to the chunk holder manager tick, we + // do not need to consider cases where the ticket level may decrease during this call by asynchronous + // ticket changes + + // chunks cannot downgrade state while status is pending a change + // note: currentChunk must be LevelChunk, as current != pending which means that at least one is not ACCESSIBLE + final LevelChunk chunk = (LevelChunk)this.currentChunk; + + // Note: we assume that only load/unload contain plugin logic + // plugin logic is anything stupid enough to possibly change the chunk status while it is already + // being changed (i.e during load it is possible it will try to set to full ticking) + // in order to allow this change, we also need this plugin logic to be contained strictly after all + // of the chunk system load callbacks are invoked + if (pending.isOrAfter(current)) { + // state upgrade + if (!current.isOrAfter(FullChunkStatus.FULL) && pending.isOrAfter(FullChunkStatus.FULL)) { + this.updateCurrentState(FullChunkStatus.FULL); + PlatformHooks.get().onChunkPreBorder(chunk, this.vanillaChunkHolder); + this.scheduler.chunkHolderManager.ensureInAutosave(this); + this.changeEntityChunkStatus(FullChunkStatus.FULL); + PlatformHooks.get().onChunkBorder(chunk, this.vanillaChunkHolder); + this.onFullChunkLoadChange(true, changedFullStatus); + this.completeFullStatusConsumers(FullChunkStatus.FULL, chunk); + } + + if (!current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { + this.updateCurrentState(FullChunkStatus.BLOCK_TICKING); + this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING); + PlatformHooks.get().onChunkTicking(chunk, this.vanillaChunkHolder); + this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, chunk); + } + + if (!current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { + this.updateCurrentState(FullChunkStatus.ENTITY_TICKING); + this.changeEntityChunkStatus(FullChunkStatus.ENTITY_TICKING); + PlatformHooks.get().onChunkEntityTicking(chunk, this.vanillaChunkHolder); + this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, chunk); + } + } else { + if (current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && !pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { + this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING); + PlatformHooks.get().onChunkNotEntityTicking(chunk, this.vanillaChunkHolder); + this.updateCurrentState(FullChunkStatus.BLOCK_TICKING); + } + + if (current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && !pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { + this.changeEntityChunkStatus(FullChunkStatus.FULL); + PlatformHooks.get().onChunkNotTicking(chunk, this.vanillaChunkHolder); + this.updateCurrentState(FullChunkStatus.FULL); + } + + if (current.isOrAfter(FullChunkStatus.FULL) && !pending.isOrAfter(FullChunkStatus.FULL)) { + this.onFullChunkLoadChange(false, changedFullStatus); + this.changeEntityChunkStatus(FullChunkStatus.INACCESSIBLE); + PlatformHooks.get().onChunkNotBorder(chunk, this.vanillaChunkHolder); + PlatformHooks.get().onChunkPostNotBorder(chunk, this.vanillaChunkHolder); + this.updateCurrentState(FullChunkStatus.INACCESSIBLE); + } + } + } + } finally { + this.processingFullStatus = false; + } + } + + // note: must hold scheduling lock + // rets true if the current requested gen status is not null (effectively, whether further scheduling is not needed) + boolean upgradeGenTarget(final ChunkStatus toStatus) { + if (toStatus == null) { + throw new NullPointerException("toStatus cannot be null"); + } + if (this.requestedGenStatus == null && this.generationTask == null) { + return false; + } + if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(toStatus)) { + this.requestedGenStatus = toStatus; + } + return true; + } + + public void setGenerationTarget(final ChunkStatus toStatus) { + this.requestedGenStatus = toStatus; + } + + public boolean hasGenerationTask() { + return this.generationTask != null; + } + + public ChunkStatus getCurrentGenStatus() { + return this.currentGenStatus; + } + + public ChunkStatus getRequestedGenStatus() { + return this.requestedGenStatus; + } + + private final Reference2ObjectOpenHashMap>> statusWaiters = new Reference2ObjectOpenHashMap<>(); + + void addStatusConsumer(final ChunkStatus status, final Consumer consumer) { + this.statusWaiters.computeIfAbsent(status, (final ChunkStatus keyInMap) -> { + return new ArrayList<>(4); + }).add(consumer); + } + + private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) { + // Update progress listener for LevelLoadingScreen + if (chunk != null) { + final ChunkProgressListener progressListener = this.world.getChunkSource().chunkMap.progressListener; + if (progressListener != null) { + final ChunkStatus finalStatus = status; + this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { + progressListener.onStatusChange(this.vanillaChunkHolder.getPos(), finalStatus); + }); + } + } + + // need to tell future statuses to complete if cancelled + do { + this.completeStatusConsumers0(status, chunk); + } while (chunk == null && status != (status = ((ChunkSystemChunkStatus)status).moonrise$getNextStatus())); + } + + private void completeStatusConsumers0(final ChunkStatus status, final ChunkAccess chunk) { + final List> consumers; + consumers = this.statusWaiters.remove(status); + + if (consumers == null) { + return; + } + + // must be scheduled to main, we do not trust the callback to not do anything stupid + this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { + for (final Consumer consumer : consumers) { + try { + consumer.accept(chunk); + } catch (final Throwable thr) { + LOGGER.error("Failed to process chunk status callback", thr); + } + } + }, Priority.HIGHEST); + } + + private final Reference2ObjectOpenHashMap>> fullStatusWaiters = new Reference2ObjectOpenHashMap<>(); + + void addFullStatusConsumer(final FullChunkStatus status, final Consumer consumer) { + this.fullStatusWaiters.computeIfAbsent(status, (final FullChunkStatus keyInMap) -> { + return new ArrayList<>(4); + }).add(consumer); + } + + private void completeFullStatusConsumers(FullChunkStatus status, final LevelChunk chunk) { + final List> consumers; + consumers = this.fullStatusWaiters.remove(status); + + if (consumers == null) { + return; + } + + // must be scheduled to main, we do not trust the callback to not do anything stupid + this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { + for (final Consumer consumer : consumers) { + try { + consumer.accept(chunk); + } catch (final Throwable thr) { + LOGGER.error("Failed to process chunk status callback", thr); + } + } + }, Priority.HIGHEST); + } + + // note: must hold scheduling lock + private void onChunkGenComplete(final ChunkAccess newChunk, final ChunkStatus newStatus, + final List scheduleList, final List changedLoadStatus) { + if (!this.neighboursBlockingGenTask.isEmpty()) { + throw new IllegalStateException("Cannot have neighbours blocking this gen task"); + } + if (newChunk != null || (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(newStatus))) { + this.completeStatusConsumers(newStatus, newChunk); + } + // done now, clear state (must be done before scheduling new tasks) + this.generationTask = null; + this.generationTaskStatus = null; + if (newChunk == null) { + // task was cancelled + // should be careful as this could be called while holding the schedule lock and/or inside the + // ticket level update + // while a task may be cancelled, it is possible for it to be later re-scheduled + // however, because generationTask is only set to null on _completion_, the scheduler leaves + // the rescheduling logic to us here + final ChunkStatus requestedGenStatus = this.requestedGenStatus; + this.requestedGenStatus = null; + if (requestedGenStatus != null) { + // it looks like it has been requested, so we must reschedule + if (!this.neighboursWaitingForUs.isEmpty()) { + for (final Iterator> iterator = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + final Reference2ObjectMap.Entry entry = iterator.next(); + + final NewChunkHolder chunkHolder = entry.getKey(); + final ChunkStatus toStatus = entry.getValue(); + + if (!requestedGenStatus.isOrAfter(toStatus)) { + // if we were cancelled, we are responsible for removing the waiter + if (!chunkHolder.neighboursBlockingGenTask.remove(this)) { + throw new IllegalStateException("Corrupt state"); + } + if (chunkHolder.neighboursBlockingGenTask.isEmpty()) { + chunkHolder.checkUnload(); + } + iterator.remove(); + continue; + } + } + } + + // note: only after generationTask -> null, generationTaskStatus -> null, and requestedGenStatus -> null + this.scheduler.schedule( + this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList + ); + + // return, can't do anything further + return; + } + + if (!this.neighboursWaitingForUs.isEmpty()) { + for (final NewChunkHolder chunkHolder : this.neighboursWaitingForUs.keySet()) { + if (!chunkHolder.neighboursBlockingGenTask.remove(this)) { + throw new IllegalStateException("Corrupt state"); + } + if (chunkHolder.neighboursBlockingGenTask.isEmpty()) { + chunkHolder.checkUnload(); + } + } + this.neighboursWaitingForUs.clear(); + } + // reset priority, we have nothing left to generate to + this.setPriority(null); + this.checkUnload(); + return; + } + + this.currentChunk = newChunk; + this.currentGenStatus = newStatus; + final ChunkCompletion completion = new ChunkCompletion(newChunk, newStatus); + CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, newStatus.getIndex(), completion); + this.lastChunkCompletion = completion; + + final ChunkStatus requestedGenStatus = this.requestedGenStatus; + + List needsScheduling = null; + boolean recalculatePriority = false; + for (final Iterator> iterator + = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + final Reference2ObjectMap.Entry entry = iterator.next(); + final NewChunkHolder neighbour = entry.getKey(); + final ChunkStatus requiredStatus = entry.getValue(); + + if (!newStatus.isOrAfter(requiredStatus)) { + if (requestedGenStatus == null || !requestedGenStatus.isOrAfter(requiredStatus)) { + // if we're cancelled, still need to clear this map + if (!neighbour.neighboursBlockingGenTask.remove(this)) { + throw new IllegalStateException("Neighbour is not waiting for us?"); + } + if (neighbour.neighboursBlockingGenTask.isEmpty()) { + neighbour.checkUnload(); + } + + iterator.remove(); + } + continue; + } + + // doesn't matter what isCancelled is here, we need to schedule if we can + + recalculatePriority = true; + if (!neighbour.neighboursBlockingGenTask.remove(this)) { + throw new IllegalStateException("Neighbour is not waiting for us?"); + } + + if (neighbour.neighboursBlockingGenTask.isEmpty()) { + if (neighbour.requestedGenStatus != null) { + if (needsScheduling == null) { + needsScheduling = new ArrayList<>(); + } + needsScheduling.add(neighbour); + } else { + neighbour.checkUnload(); + } + } + + // remove last; access to entry will throw if removed + iterator.remove(); + } + + if (newStatus == ChunkStatus.FULL) { + this.lockPriority(); + // try to push pending to FULL + if (this.updatePendingStatus()) { + changedLoadStatus.add(this); + } + } + + if (recalculatePriority) { + this.recalculateNeighbourRequestedPriority(); + } + + if (requestedGenStatus != null && !newStatus.isOrAfter(requestedGenStatus)) { + this.scheduleNeighbours(needsScheduling, scheduleList); + + // we need to schedule more tasks now + this.scheduler.schedule( + this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList + ); + } else { + // we're done now + if (requestedGenStatus != null) { + this.requestedGenStatus = null; + } + // reached final stage, so stop scheduling now + this.setPriority(null); + this.checkUnload(); + + this.scheduleNeighbours(needsScheduling, scheduleList); + } + } + + private void scheduleNeighbours(final List needsScheduling, final List scheduleList) { + if (needsScheduling != null) { + for (int i = 0, len = needsScheduling.size(); i < len; ++i) { + final NewChunkHolder neighbour = needsScheduling.get(i); + + this.scheduler.schedule( + neighbour.chunkX, neighbour.chunkZ, neighbour.requestedGenStatus, neighbour, scheduleList + ); + } + } + } + + public void setGenerationTask(final ChunkProgressionTask generationTask, final ChunkStatus taskStatus, + final List neighbours) { + if (this.generationTask != null || (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(taskStatus))) { + throw new IllegalStateException("Currently generating or provided task is trying to generate to a level we are already at!"); + } + if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(taskStatus)) { + throw new IllegalStateException("Cannot schedule generation task when not requested"); + } + this.generationTask = generationTask; + this.generationTaskStatus = taskStatus; + + for (int i = 0, len = neighbours.size(); i < len; ++i) { + neighbours.get(i).addNeighbourUsingChunk(); + } + + this.checkUnload(); + + generationTask.onComplete((final ChunkAccess access, final Throwable thr) -> { + if (generationTask != this.generationTask) { + throw new IllegalStateException( + "Cannot complete generation task '" + generationTask + "' because we are waiting on '" + this.generationTask + "' instead!" + ); + } + if (thr != null) { + if (this.genTaskException != null) { + LOGGER.warn("Ignoring exception for " + this.toString(), thr); + return; + } + // don't set generation task to null, so that scheduling will not attempt to create another task and it + // will automatically block any further scheduling usage of this chunk as it will wait forever for a failed + // task to complete + this.genTaskException = thr; + this.failedGenStatus = taskStatus; + this.genTaskFailedThread = Thread.currentThread(); + + this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( + "Generation task", ChunkTaskScheduler.stringIfNull(generationTask), + "Task to status", ChunkTaskScheduler.stringIfNull(taskStatus) + ), thr); + return; + } + + final boolean scheduleTasks; + List tasks = ChunkHolderManager.getCurrentTicketUpdateScheduling(); + if (tasks == null) { + scheduleTasks = true; + tasks = new ArrayList<>(); + } else { + scheduleTasks = false; + // we are currently updating ticket levels, so we already hold the schedule lock + // this means we have to leave the ticket level update to handle the scheduling + } + final List changedLoadStatus = new ArrayList<>(); + // theoretically, we could schedule a chunk at the max radius which performs another max radius access. So we need to double the radius. + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, 2 * ChunkTaskScheduler.getMaxAccessRadius()); + try { + for (int i = 0, len = neighbours.size(); i < len; ++i) { + neighbours.get(i).removeNeighbourUsingChunk(); + } + this.onChunkGenComplete(access, taskStatus, tasks, changedLoadStatus); + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + this.scheduler.chunkHolderManager.addChangedStatuses(changedLoadStatus); + + if (scheduleTasks) { + // can't hold the lock while scheduling, so we have to build the tasks and then schedule after + for (int i = 0, len = tasks.size(); i < len; ++i) { + tasks.get(i).schedule(); + } + } + }); + } + + public PoiChunk getPoiChunk() { + return this.poiChunk; + } + + public ChunkEntitySlices getEntityChunk() { + return this.entityChunk; + } + + public long lastAutoSave; + + public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {} + + private static final MoonriseRegionFileIO.RegionFileType[] REGION_FILE_TYPES = MoonriseRegionFileIO.RegionFileType.values(); + + public SaveStat save(final boolean shutdown) { + TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main"); + + ChunkAccess chunk = this.getCurrentChunk(); + PoiChunk poi = this.getPoiChunk(); + ChunkEntitySlices entities = this.getEntityChunk(); + boolean executedUnloadTask = false; + final boolean[] executedUnloadTasks = new boolean[REGION_FILE_TYPES.length]; + + if (shutdown) { + // make sure that the async unloads complete + if (this.unloadState != null) { + // must have errored during unload + chunk = this.unloadState.chunk(); + poi = this.unloadState.poiChunk(); + entities = this.unloadState.entityChunk(); + } + for (final MoonriseRegionFileIO.RegionFileType regionFileType : REGION_FILE_TYPES) { + final UnloadTask unloadTask = this.getUnloadTask(regionFileType); + if (unloadTask == null) { + continue; + } + + final PrioritisedExecutor.PrioritisedTask task = unloadTask.task(); + if (task != null && task.isQueued()) { + final boolean executed = task.execute(); + executedUnloadTask |= executed; + executedUnloadTasks[regionFileType.ordinal()] = executed; + } + } + } + + final boolean forceNoSaveChunk = PlatformHooks.get().forceNoSave(chunk); + + // can only synchronously save worldgen chunks during shutdown + boolean canSaveChunk = !forceNoSaveChunk && (chunk != null && ((shutdown || chunk instanceof LevelChunk) && chunk.isUnsaved())); + boolean canSavePOI = !forceNoSaveChunk && (poi != null && poi.isDirty()); + boolean canSaveEntities = entities != null; + + if (canSaveChunk) { + canSaveChunk = this.saveChunk(chunk, false); + } + if (canSavePOI) { + canSavePOI = this.savePOI(poi, false); + } + if (canSaveEntities) { + // on shutdown, we need to force transient entity chunks to save + canSaveEntities = this.saveEntities(entities, shutdown); + if (shutdown) { + this.lastEntityUnload = null; + } + } + + return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI ? + new SaveStat( + canSaveChunk | executedUnloadTasks[MoonriseRegionFileIO.RegionFileType.CHUNK_DATA.ordinal()], + canSaveEntities | executedUnloadTasks[MoonriseRegionFileIO.RegionFileType.ENTITY_DATA.ordinal()], + canSavePOI | executedUnloadTasks[MoonriseRegionFileIO.RegionFileType.POI_DATA.ordinal()] + ) + : null; + } + + private boolean saveChunk(final ChunkAccess chunk, final boolean unloading) { + if (!chunk.isUnsaved()) { + if (unloading) { + this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, null); + } + return false; + } + try { + final SerializableChunkData chunkData = SerializableChunkData.copyOf(this.world, chunk); + PlatformHooks.get().chunkSyncSave(this.world, chunk, chunkData); + + chunk.tryMarkSaved(); + + final CallbackCompletable completable = new CallbackCompletable<>(); + + final Runnable run = () -> { + final CompoundTag data = chunkData.write(); + + completable.complete(data); + + if (unloading) { + NewChunkHolder.this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, data); + } + }; + + final PrioritisedExecutor.PrioritisedTask task; + if (unloading) { + this.chunkDataUnload.toRun().setRunnable(run); + task = this.chunkDataUnload.task(); + } else { + task = this.scheduler.saveExecutor.createTask(run); + } + + task.queue(); + + MoonriseRegionFileIO.scheduleSave( + this.world, this.chunkX, this.chunkZ, completable, task, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, Priority.NORMAL + ); + } catch (final Throwable thr) { + LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr); + } + + return true; + } + + private boolean lastEntitySaveNull; + private CompoundTag lastEntityUnload; + private boolean saveEntities(final ChunkEntitySlices entities, final boolean unloading) { + try { + CompoundTag mergeFrom = null; + if (entities.isTransient()) { + if (!unloading) { + // if we're a transient chunk, we cannot save until unloading because otherwise a double save will + // result in double adding the entities + return false; + } + try { + mergeFrom = MoonriseRegionFileIO.loadData(this.world, this.chunkX, this.chunkZ, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, Priority.BLOCKING); + } catch (final Exception ex) { + LOGGER.error("Cannot merge transient entities for chunk (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', data on disk will be replaced", ex); + } + } + + final CompoundTag save = entities.save(); + if (mergeFrom != null) { + if (save == null) { + // don't override the data on disk with nothing + return false; + } else { + ChunkEntitySlices.copyEntities(mergeFrom, save); + } + } + if (save == null && this.lastEntitySaveNull) { + return false; + } + + MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, save, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA); + this.lastEntitySaveNull = save == null; + if (unloading) { + this.lastEntityUnload = save; + } + } catch (final Throwable thr) { + LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr); + } + + return true; + } + + private boolean lastPoiSaveNull; + private boolean savePOI(final PoiChunk poi, final boolean unloading) { + try { + final CompoundTag save = poi.save(); + poi.setDirty(false); + if (save == null && this.lastPoiSaveNull) { + if (unloading) { + this.poiDataUnload.completable().complete(null); + } + return false; + } + + MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, save, MoonriseRegionFileIO.RegionFileType.POI_DATA); + this.lastPoiSaveNull = save == null; + if (unloading) { + this.poiDataUnload.completable().complete(save); + } + } catch (final Throwable thr) { + LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr); + } + + return true; + } + + @Override + public String toString() { + final ChunkCompletion lastCompletion = this.lastChunkCompletion; + final ChunkEntitySlices entityChunk = this.entityChunk; + final FullChunkStatus pendingFullStatus = this.pendingFullChunkStatus; + final FullChunkStatus currentFullStatus = this.currentFullChunkStatus; + return "NewChunkHolder{" + + "world=" + WorldUtil.getWorldName(this.world) + + ", chunkX=" + this.chunkX + + ", chunkZ=" + this.chunkZ + + ", entityChunkFromDisk=" + (entityChunk != null && !entityChunk.isTransient()) + + ", lastChunkCompletion={chunk_class=" + (lastCompletion == null || lastCompletion.chunk() == null ? "null" : lastCompletion.chunk().getClass().getName()) + ",status=" + (lastCompletion == null ? "null" : lastCompletion.genStatus()) + "}" + + ", currentGenStatus=" + this.currentGenStatus + + ", requestedGenStatus=" + this.requestedGenStatus + + ", generationTask=" + this.generationTask + + ", generationTaskStatus=" + this.generationTaskStatus + + ", priority=" + this.priority + + ", priorityLocked=" + this.priorityLocked + + ", neighbourRequestedPriority=" + this.neighbourRequestedPriority + + ", effective_priority=" + this.getEffectivePriority(null) + + ", oldTicketLevel=" + this.oldTicketLevel + + ", currentTicketLevel=" + this.currentTicketLevel + + ", totalNeighboursUsingThisChunk=" + this.totalNeighboursUsingThisChunk + + ", fullNeighbourChunksLoadedBitset=" + this.fullNeighbourChunksLoadedBitset + + ", currentChunkStatus=" + currentFullStatus + + ", pendingChunkStatus=" + pendingFullStatus + + ", is_unload_safe=" + this.isSafeToUnload() + + ", killed=" + this.unloaded + + '}'; + } + + private static JsonElement serializeStacktraceElement(final StackTraceElement element) { + return element == null ? JsonNull.INSTANCE : new JsonPrimitive(element.toString()); + } + + private static JsonObject serializeCompletable(final CallbackCompletable completable) { + final JsonObject ret = new JsonObject(); + + if (completable == null) { + return ret; + } + + ret.addProperty("valid", Boolean.TRUE); + + final boolean isCompleted = completable.isCompleted(); + ret.addProperty("completed", Boolean.valueOf(isCompleted)); + + if (isCompleted) { + final Throwable throwable = completable.getThrowable(); + if (throwable != null) { + final JsonArray throwableJson = new JsonArray(); + ret.add("throwable", throwableJson); + + for (final StackTraceElement element : throwable.getStackTrace()) { + throwableJson.add(serializeStacktraceElement(element)); + } + } else { + final Object result = completable.getResult(); + ret.add("result_class", result == null ? JsonNull.INSTANCE : new JsonPrimitive(result.getClass().getName())); + } + } + + return ret; + } + + // (probably) holds ticket and scheduling lock + public JsonObject getDebugJson() { + final JsonObject ret = new JsonObject(); + + final ChunkCompletion lastCompletion = this.lastChunkCompletion; + final ChunkEntitySlices slices = this.entityChunk; + final PoiChunk poiChunk = this.poiChunk; + + ret.addProperty("chunkX", Integer.valueOf(this.chunkX)); + ret.addProperty("chunkZ", Integer.valueOf(this.chunkZ)); + ret.addProperty("entity_chunk", slices == null ? "null" : "transient=" + slices.isTransient()); + ret.addProperty("poi_chunk", "null=" + (poiChunk == null)); + ret.addProperty("completed_chunk_class", lastCompletion == null ? "null" : lastCompletion.chunk().getClass().getName()); + ret.addProperty("completed_gen_status", lastCompletion == null ? "null" : lastCompletion.genStatus().toString()); + ret.addProperty("priority", Objects.toString(this.priority)); + ret.addProperty("neighbour_requested_priority", Objects.toString(this.neighbourRequestedPriority)); + ret.addProperty("generation_task", Objects.toString(this.generationTask)); + ret.addProperty("is_safe_unload", Objects.toString(this.isSafeToUnload())); + ret.addProperty("old_ticket_level", Integer.valueOf(this.oldTicketLevel)); + ret.addProperty("current_ticket_level", Integer.valueOf(this.currentTicketLevel)); + ret.addProperty("neighbours_using_chunk", Integer.valueOf(this.totalNeighboursUsingThisChunk)); + + final JsonObject neighbourWaitState = new JsonObject(); + ret.add("neighbour_state", neighbourWaitState); + + final JsonArray blockingGenNeighbours = new JsonArray(); + neighbourWaitState.add("blocking_gen_task", blockingGenNeighbours); + for (final NewChunkHolder blockingGenNeighbour : this.neighboursBlockingGenTask) { + final JsonObject neighbour = new JsonObject(); + blockingGenNeighbours.add(neighbour); + + neighbour.addProperty("chunkX", Integer.valueOf(blockingGenNeighbour.chunkX)); + neighbour.addProperty("chunkZ", Integer.valueOf(blockingGenNeighbour.chunkZ)); + } + + final JsonArray neighboursWaitingForUs = new JsonArray(); + neighbourWaitState.add("neighbours_waiting_on_us", neighboursWaitingForUs); + for (final Reference2ObjectMap.Entry entry : this.neighboursWaitingForUs.reference2ObjectEntrySet()) { + final NewChunkHolder holder = entry.getKey(); + final ChunkStatus status = entry.getValue(); + + final JsonObject neighbour = new JsonObject(); + neighboursWaitingForUs.add(neighbour); + + + neighbour.addProperty("chunkX", Integer.valueOf(holder.chunkX)); + neighbour.addProperty("chunkZ", Integer.valueOf(holder.chunkZ)); + neighbour.addProperty("waiting_for", Objects.toString(status)); + } + + ret.addProperty("pending_chunk_full_status", Objects.toString(this.pendingFullChunkStatus)); + ret.addProperty("current_chunk_full_status", Objects.toString(this.currentFullChunkStatus)); + ret.addProperty("generation_task", Objects.toString(this.generationTask)); + ret.addProperty("requested_generation", Objects.toString(this.requestedGenStatus)); + ret.addProperty("has_entity_load_task", Boolean.valueOf(this.entityDataLoadTask != null)); + ret.addProperty("has_poi_load_task", Boolean.valueOf(this.poiDataLoadTask != null)); + + final UnloadTask entityDataUnload = this.entityDataUnload; + final UnloadTask poiDataUnload = this.poiDataUnload; + final UnloadTask chunkDataUnload = this.chunkDataUnload; + + ret.add("entity_unload_completable", serializeCompletable(entityDataUnload == null ? null : entityDataUnload.completable())); + ret.add("poi_unload_completable", serializeCompletable(poiDataUnload == null ? null : poiDataUnload.completable())); + ret.add("chunk_unload_completable", serializeCompletable(chunkDataUnload == null ? null : chunkDataUnload.completable())); + + final PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task(); + if (unloadTask == null) { + ret.addProperty("unload_task_priority", "null"); + ret.addProperty("unload_task_suborder", Long.valueOf(0L)); + } else { + ret.addProperty("unload_task_priority", Objects.toString(unloadTask.getPriority())); + ret.addProperty("unload_task_suborder", Long.valueOf(unloadTask.getSubOrder())); + } + + ret.addProperty("killed", Boolean.valueOf(this.unloaded)); + + return ret; + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java new file mode 100644 index 0000000000000000000000000000000000000000..6b468c621b74449a6218391f6477cf63cfc98c7c --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java @@ -0,0 +1,215 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Priority; +import java.lang.invoke.VarHandle; + +public abstract class PriorityHolder { + + protected volatile int priority; + protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(PriorityHolder.class, "priority", int.class); + + protected static final int PRIORITY_SCHEDULED = Integer.MIN_VALUE >>> 0; + protected static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 1; + + protected final int getPriorityVolatile() { + return (int)PRIORITY_HANDLE.getVolatile((PriorityHolder)this); + } + + protected final int compareAndExchangePriorityVolatile(final int expect, final int update) { + return (int)PRIORITY_HANDLE.compareAndExchange((PriorityHolder)this, (int)expect, (int)update); + } + + protected final int getAndOrPriorityVolatile(final int val) { + return (int)PRIORITY_HANDLE.getAndBitwiseOr((PriorityHolder)this, (int)val); + } + + protected final void setPriorityPlain(final int val) { + PRIORITY_HANDLE.set((PriorityHolder)this, (int)val); + } + + protected PriorityHolder(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.setPriorityPlain(priority.priority); + } + + // used only for debug json + public boolean isScheduled() { + return (this.getPriorityVolatile() & PRIORITY_SCHEDULED) != 0; + } + + // returns false if cancelled + public boolean markExecuting() { + return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0; + } + + public boolean isMarkedExecuted() { + return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0; + } + + public void cancel() { + if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) { + // cancelled already + return; + } + this.cancelScheduled(); + } + + public void schedule() { + int priority = this.getPriorityVolatile(); + + if ((priority & PRIORITY_SCHEDULED) != 0) { + throw new IllegalStateException("schedule() called twice"); + } + + if ((priority & PRIORITY_EXECUTED) != 0) { + // cancelled + return; + } + + this.scheduleTask(Priority.getPriority(priority)); + + int failures = 0; + for (;;) { + if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SCHEDULED))) { + return; + } + + if ((priority & PRIORITY_SCHEDULED) != 0) { + throw new IllegalStateException("schedule() called twice"); + } + + if ((priority & PRIORITY_EXECUTED) != 0) { + // cancelled or executed + return; + } + + this.setPriorityScheduled(Priority.getPriority(priority)); + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public final Priority getPriority() { + final int ret = this.getPriorityVolatile(); + if ((ret & PRIORITY_EXECUTED) != 0) { + return Priority.COMPLETING; + } + if ((ret & PRIORITY_SCHEDULED) != 0) { + return this.getScheduledPriority(); + } + return Priority.getPriority(ret); + } + + public final void lowerPriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_EXECUTED) != 0) { + return; + } + + if ((curr & PRIORITY_SCHEDULED) != 0) { + this.lowerPriorityScheduled(priority); + return; + } + + if (!priority.isLowerPriority(curr)) { + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public final void setPriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_EXECUTED) != 0) { + return; + } + + if ((curr & PRIORITY_SCHEDULED) != 0) { + this.setPriorityScheduled(priority); + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public final void raisePriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_EXECUTED) != 0) { + return; + } + + if ((curr & PRIORITY_SCHEDULED) != 0) { + this.raisePriorityScheduled(priority); + return; + } + + if (!priority.isHigherPriority(curr)) { + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + protected abstract void cancelScheduled(); + + protected abstract Priority getScheduledPriority(); + + protected abstract void scheduleTask(final Priority priority); + + protected abstract void lowerPriorityScheduled(final Priority priority); + + protected abstract void setPriorityScheduled(final Priority priority); + + protected abstract void raisePriorityScheduled(final Priority priority); +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java new file mode 100644 index 0000000000000000000000000000000000000000..310a8f80debadd64c2d962ebf83b7d0505ce6e42 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java @@ -0,0 +1,1457 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask; +import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap; +import it.unimi.dsi.fastutil.shorts.Short2ByteLinkedOpenHashMap; +import it.unimi.dsi.fastutil.shorts.Short2ByteMap; +import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; +import java.lang.invoke.VarHandle; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.locks.LockSupport; + +public abstract class ThreadedTicketLevelPropagator { + + // sections are 64 in length + public static final int SECTION_SHIFT = 6; + public static final int SECTION_SIZE = 1 << SECTION_SHIFT; + private static final int LEVEL_BITS = SECTION_SHIFT; + private static final int LEVEL_COUNT = 1 << LEVEL_BITS; + private static final int MIN_SOURCE_LEVEL = 1; + // we limit the max source to 62 because the de-propagation code _must_ attempt to de-propagate + // a 1 level to 0; and if a source was 63 then it may cross more than 2 sections in de-propagation + private static final int MAX_SOURCE_LEVEL = 62; + + private static int getMaxSchedulingRadius() { + return 2 * ChunkTaskScheduler.getMaxAccessRadius(); + } + + private final UpdateQueue updateQueue; + private final ConcurrentLong2ReferenceChainedHashTable
    sections; + + public ThreadedTicketLevelPropagator() { + this.updateQueue = new UpdateQueue(); + this.sections = new ConcurrentLong2ReferenceChainedHashTable<>(); + } + + // must hold ticket lock for: + // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1)) + public void setSource(final int posX, final int posZ, final int to) { + if (to < 1 || to > MAX_SOURCE_LEVEL) { + throw new IllegalArgumentException("Source: " + to); + } + + final int sectionX = posX >> SECTION_SHIFT; + final int sectionZ = posZ >> SECTION_SHIFT; + + final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); + Section section = this.sections.get(coordinate); + if (section == null) { + if (null != this.sections.putIfAbsent(coordinate, section = new Section(sectionX, sectionZ))) { + throw new IllegalStateException("Race condition while creating new section"); + } + } + + final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); + final short sLocalIdx = (short)localIdx; + + final short sourceAndLevel = section.levels[localIdx]; + final int currentSource = (sourceAndLevel >>> 8) & 0xFF; + + if (currentSource == to) { + // nothing to do + // make sure to kill the current update, if any + section.queuedSources.replace(sLocalIdx, (byte)to); + return; + } + + if (section.queuedSources.put(sLocalIdx, (byte)to) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) { + this.queueSectionUpdate(section); + } + } + + // must hold ticket lock for: + // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1)) + public void removeSource(final int posX, final int posZ) { + final int sectionX = posX >> SECTION_SHIFT; + final int sectionZ = posZ >> SECTION_SHIFT; + + final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); + final Section section = this.sections.get(coordinate); + + if (section == null) { + return; + } + + final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); + final short sLocalIdx = (short)localIdx; + + final int currentSource = (section.levels[localIdx] >>> 8) & 0xFF; + + if (currentSource == 0) { + // we use replace here so that we do not possibly multi-queue a section for an update + section.queuedSources.replace(sLocalIdx, (byte)0); + return; + } + + if (section.queuedSources.put(sLocalIdx, (byte)0) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) { + this.queueSectionUpdate(section); + } + } + + private void queueSectionUpdate(final Section section) { + this.updateQueue.append(new UpdateQueue.UpdateQueueNode(section, null)); + } + + public boolean hasPendingUpdates() { + return !this.updateQueue.isEmpty(); + } + + // holds ticket lock for every chunk section represented by any position in the key set + // updates is modifiable and passed to processSchedulingUpdates after this call + protected abstract void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates); + + // holds ticket lock for every chunk section represented by any position in the key set + // holds scheduling lock in max access radius for every position held by the ticket lock + // updates is cleared after this call + protected abstract void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List scheduledTasks, + final List changedFullStatus); + + // must hold ticket lock for every position in the sections in one radius around sectionX,sectionZ + public boolean performUpdate(final int sectionX, final int sectionZ, final ReentrantAreaLock schedulingLock, + final List scheduledTasks, final List changedFullStatus) { + if (!this.hasPendingUpdates()) { + return false; + } + + final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); + final Section section = this.sections.get(coordinate); + + if (section == null || section.queuedSources.isEmpty()) { + // no section or no updates + return false; + } + + final Propagator propagator = Propagator.acquirePropagator(); + final boolean ret = this.performUpdate(section, null, propagator, + null, schedulingLock, scheduledTasks, changedFullStatus + ); + Propagator.returnPropagator(propagator); + return ret; + } + + private boolean performUpdate(final Section section, final UpdateQueue.UpdateQueueNode node, final Propagator propagator, + final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock, + final List scheduledTasks, final List changedFullStatus) { + final int sectionX = section.sectionX; + final int sectionZ = section.sectionZ; + + final int rad1MinX = (sectionX - 1) << SECTION_SHIFT; + final int rad1MinZ = (sectionZ - 1) << SECTION_SHIFT; + final int rad1MaxX = ((sectionX + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1); + final int rad1MaxZ = ((sectionZ + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1); + + // set up encode offset first as we need to queue level changes _before_ + propagator.setupEncodeOffset(sectionX, sectionZ); + + final int coordinateOffset = propagator.coordinateOffset; + + final ReentrantAreaLock.Node ticketNode = ticketLock == null ? null : ticketLock.lock(rad1MinX, rad1MinZ, rad1MaxX, rad1MaxZ); + final boolean ret; + try { + // first, check if this update was stolen + if (section != this.sections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ))) { + // occurs when a stolen update deletes this section + // it is possible that another update is scheduled, but that one will have the correct section + if (node != null) { + this.updateQueue.remove(node); + } + return false; + } + + final int oldSourceSize = section.sources.size(); + + // process pending sources + for (final Iterator iterator = section.queuedSources.short2ByteEntrySet().fastIterator(); iterator.hasNext();) { + final Short2ByteMap.Entry entry = iterator.next(); + final int pos = (int)entry.getShortKey(); + final int posX = (pos & (SECTION_SIZE - 1)) | (sectionX << SECTION_SHIFT); + final int posZ = ((pos >> SECTION_SHIFT) & (SECTION_SIZE - 1)) | (sectionZ << SECTION_SHIFT); + final int newSource = (int)entry.getByteValue(); + + final short currentEncoded = section.levels[pos]; + final int currLevel = currentEncoded & 0xFF; + final int prevSource = (currentEncoded >>> 8) & 0xFF; + + if (prevSource == newSource) { + // nothing changed + continue; + } + + if ((prevSource < currLevel && newSource <= currLevel) || newSource == currLevel) { + // just update the source, don't need to propagate change + section.levels[pos] = (short)(currLevel | (newSource << 8)); + // level is unchanged, don't add to changed positions + } else { + // set current level and current source to new source + section.levels[pos] = (short)(newSource | (newSource << 8)); + // must add to updated positions in case this is final + propagator.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)newSource); + if (newSource != 0) { + // queue increase with new source level + propagator.appendToIncreaseQueue( + ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) | + ((newSource & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) | + (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS)) + ); + } + // queue decrease with previous level + if (newSource < currLevel) { + propagator.appendToDecreaseQueue( + ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) | + ((currLevel & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) | + (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS)) + ); + } + } + + if (newSource == 0) { + // prevSource != newSource, so we are removing this source + section.sources.remove((short)pos); + } else if (prevSource == 0) { + // prevSource != newSource, so we are adding this source + section.sources.add((short)pos); + } + } + + section.queuedSources.clear(); + + final int newSourceSize = section.sources.size(); + + if (oldSourceSize == 0 && newSourceSize != 0) { + // need to make sure the sections in 1 radius are initialised + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + if ((dx | dz) == 0) { + continue; + } + final int offX = dx + sectionX; + final int offZ = dz + sectionZ; + final long coordinate = CoordinateUtils.getChunkKey(offX, offZ); + final Section neighbour = this.sections.computeIfAbsent(coordinate, (final long keyInMap) -> { + return new Section(CoordinateUtils.getChunkX(keyInMap), CoordinateUtils.getChunkZ(keyInMap)); + }); + + // increase ref count + ++neighbour.oneRadNeighboursWithSources; + if (neighbour.oneRadNeighboursWithSources <= 0 || neighbour.oneRadNeighboursWithSources > 8) { + throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources)); + } + } + } + } + + if (propagator.hasUpdates()) { + propagator.setupCaches(this, sectionX, sectionZ, 1); + propagator.performDecrease(); + // don't need try-finally, as any exception will cause the propagator to not be returned + propagator.destroyCaches(); + } + + if (newSourceSize == 0) { + final boolean decrementRef = oldSourceSize != 0; + // check for section de-init + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + final int offX = dx + sectionX; + final int offZ = dz + sectionZ; + final long coordinate = CoordinateUtils.getChunkKey(offX, offZ); + final Section neighbour = this.sections.get(coordinate); + + if (neighbour == null) { + if (oldSourceSize == 0 && (dx | dz) != 0) { + // since we don't have sources, this section is allowed to be null + continue; + } + throw new IllegalStateException("??"); + } + + if (decrementRef && (dx | dz) != 0) { + // decrease ref count, but only for neighbours + --neighbour.oneRadNeighboursWithSources; + } + + // we need to check the current section for de-init as well + if (neighbour.oneRadNeighboursWithSources == 0) { + if (neighbour.queuedSources.isEmpty() && neighbour.sources.isEmpty()) { + // need to de-init + this.sections.remove(coordinate); + } // else: neighbour is queued for an update, and it will de-init itself + } else if (neighbour.oneRadNeighboursWithSources < 0 || neighbour.oneRadNeighboursWithSources > 8) { + throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources)); + } + } + } + } + + + ret = !propagator.updatedPositions.isEmpty(); + + if (ret) { + this.processLevelUpdates(propagator.updatedPositions); + + if (!propagator.updatedPositions.isEmpty()) { + // now we can actually update the ticket levels in the chunk holders + final int maxScheduleRadius = getMaxSchedulingRadius(); + + // allow the chunkholders to process ticket level updates without needing to acquire the schedule lock every time + final ReentrantAreaLock.Node schedulingNode = schedulingLock.lock( + rad1MinX - maxScheduleRadius, rad1MinZ - maxScheduleRadius, + rad1MaxX + maxScheduleRadius, rad1MaxZ + maxScheduleRadius + ); + try { + this.processSchedulingUpdates(propagator.updatedPositions, scheduledTasks, changedFullStatus); + } finally { + schedulingLock.unlock(schedulingNode); + } + } + + propagator.updatedPositions.clear(); + } + } finally { + if (ticketLock != null) { + ticketLock.unlock(ticketNode); + } + } + + // finished + if (node != null) { + this.updateQueue.remove(node); + } + + return ret; + } + + public boolean performUpdates(final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock, + final List scheduledTasks, final List changedFullStatus) { + if (this.updateQueue.isEmpty()) { + return false; + } + + final long maxOrder = this.updateQueue.getLastOrder(); + + boolean updated = false; + Propagator propagator = null; + + for (;;) { + final UpdateQueue.UpdateQueueNode toUpdate = this.updateQueue.acquireNextOrWait(maxOrder); + if (toUpdate == null) { + if (!this.updateQueue.hasRemainingUpdates(maxOrder)) { + if (propagator != null) { + Propagator.returnPropagator(propagator); + } + return updated; + } + + continue; + } + + if (propagator == null) { + propagator = Propagator.acquirePropagator(); + } + + updated |= this.performUpdate(toUpdate.section, toUpdate, propagator, ticketLock, schedulingLock, scheduledTasks, changedFullStatus); + } + } + + // Similar implementation of concurrent FIFO queue (See MTQ in ConcurrentUtil) which has an additional node pointer + // for the last update node being handled + private static final class UpdateQueue { + + private volatile UpdateQueueNode head; + private volatile UpdateQueueNode tail; + + private static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "head", UpdateQueueNode.class); + private static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "tail", UpdateQueueNode.class); + + /* head */ + + private final void setHeadPlain(final UpdateQueueNode newHead) { + HEAD_HANDLE.set(this, newHead); + } + + private final void setHeadOpaque(final UpdateQueueNode newHead) { + HEAD_HANDLE.setOpaque(this, newHead); + } + + private final UpdateQueueNode getHeadPlain() { + return (UpdateQueueNode)HEAD_HANDLE.get(this); + } + + private final UpdateQueueNode getHeadOpaque() { + return (UpdateQueueNode)HEAD_HANDLE.getOpaque(this); + } + + private final UpdateQueueNode getHeadAcquire() { + return (UpdateQueueNode)HEAD_HANDLE.getAcquire(this); + } + + /* tail */ + + private final void setTailPlain(final UpdateQueueNode newTail) { + TAIL_HANDLE.set(this, newTail); + } + + private final void setTailOpaque(final UpdateQueueNode newTail) { + TAIL_HANDLE.setOpaque(this, newTail); + } + + private final UpdateQueueNode getTailPlain() { + return (UpdateQueueNode)TAIL_HANDLE.get(this); + } + + private final UpdateQueueNode getTailOpaque() { + return (UpdateQueueNode)TAIL_HANDLE.getOpaque(this); + } + + public UpdateQueue() { + final UpdateQueueNode dummy = new UpdateQueueNode(null, null); + dummy.order = -1L; + dummy.preventAdds(); + + this.setHeadPlain(dummy); + this.setTailPlain(dummy); + } + + public boolean isEmpty() { + return this.peek() == null; + } + + public boolean hasRemainingUpdates(final long maxUpdate) { + final UpdateQueueNode node = this.peek(); + return node != null && node.order <= maxUpdate; + } + + public long getLastOrder() { + for (UpdateQueueNode tail = this.getTailOpaque(), curr = tail;;) { + final UpdateQueueNode next = curr.getNextVolatile(); + if (next == null) { + // try to update stale tail + if (this.getTailOpaque() == tail && curr != tail) { + this.setTailOpaque(curr); + } + return curr.order; + } + curr = next; + } + } + + private static void await(final UpdateQueueNode node) { + final Thread currThread = Thread.currentThread(); + // we do not use add-blocking because we use the nullability of the section to block + // remove() does not begin to poll from the wait queue until the section is null'd, + // and so provided we check the nullability before parking there is no ordering of these operations + // such that remove() finishes polling from the wait queue while section is not null + node.add(currThread); + + // wait until completed + while (node.getSectionVolatile() != null) { + LockSupport.park(); + } + } + + public UpdateQueueNode acquireNextOrWait(final long maxOrder) { + final List blocking = new ArrayList<>(); + + node_search: + for (UpdateQueueNode curr = this.peek(); curr != null && curr.order <= maxOrder; curr = curr.getNextVolatile()) { + if (curr.getSectionVolatile() == null) { + continue; + } + + if (curr.getUpdatingVolatile()) { + blocking.add(curr); + continue; + } + + for (int i = 0, len = blocking.size(); i < len; ++i) { + final UpdateQueueNode node = blocking.get(i); + + if (node.intersects(curr)) { + continue node_search; + } + } + + if (curr.getAndSetUpdatingVolatile(true)) { + blocking.add(curr); + continue; + } + + return curr; + } + + if (!blocking.isEmpty()) { + await(blocking.get(0)); + } + + return null; + } + + public UpdateQueueNode peek() { + for (UpdateQueueNode head = this.getHeadOpaque(), curr = head;;) { + final UpdateQueueNode next = curr.getNextVolatile(); + final Section element = curr.getSectionVolatile(); /* Likely in sync */ + + if (element != null) { + if (this.getHeadOpaque() == head && curr != head) { + this.setHeadOpaque(curr); + } + return curr; + } + + if (next == null) { + if (this.getHeadOpaque() == head && curr != head) { + this.setHeadOpaque(curr); + } + return null; + } + curr = next; + } + } + + public void remove(final UpdateQueueNode node) { + // mark as removed + node.setSectionVolatile(null); + + // use peek to advance head + this.peek(); + + // unpark any waiters / block the wait queue + Thread unpark; + while ((unpark = node.poll()) != null) { + LockSupport.unpark(unpark); + } + } + + public void append(final UpdateQueueNode node) { + int failures = 0; + + for (UpdateQueueNode currTail = this.getTailOpaque(), curr = currTail;;) { + /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */ + /* It is likely due to a cache miss caused by another write to the next field */ + final UpdateQueueNode next = curr.getNextVolatile(); + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (next == null) { + node.order = curr.order + 1L; + final UpdateQueueNode compared = curr.compareExchangeNextVolatile(null, node); + + if (compared == null) { + /* Added */ + /* Avoid CASing on tail more than we need to */ + /* CAS to avoid setting an out-of-date tail */ + if (this.getTailOpaque() == currTail) { + this.setTailOpaque(node); + } + return; + } + + ++failures; + curr = compared; + continue; + } + + if (curr == currTail) { + /* Tail is likely not up-to-date */ + curr = next; + } else { + /* Try to update to tail */ + if (currTail == (currTail = this.getTailOpaque())) { + curr = next; + } else { + curr = currTail; + } + } + } + } + + // each node also represents a set of waiters, represented by the MTQ + // if the queue is add-blocked, then the update is complete + private static final class UpdateQueueNode extends MultiThreadedQueue { + private final int sectionX; + private final int sectionZ; + + private long order; + private volatile Section section; + private volatile UpdateQueueNode next; + private volatile boolean updating; + + private static final VarHandle SECTION_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "section", Section.class); + private static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "next", UpdateQueueNode.class); + private static final VarHandle UPDATING_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "updating", boolean.class); + + public UpdateQueueNode(final Section section, final UpdateQueueNode next) { + if (section == null) { + this.sectionX = this.sectionZ = 0; + } else { + this.sectionX = section.sectionX; + this.sectionZ = section.sectionZ; + } + + SECTION_HANDLE.set(this, section); + NEXT_HANDLE.set(this, next); + } + + public boolean intersects(final UpdateQueueNode other) { + final int dist = Math.max(Math.abs(this.sectionX - other.sectionX), Math.abs(this.sectionZ - other.sectionZ)); + + // intersection radius is ticket update radius (1) + scheduling radius + return dist <= (1 + ((getMaxSchedulingRadius() + (SECTION_SIZE - 1)) >> SECTION_SHIFT)); + } + + /* section */ + + private final Section getSectionPlain() { + return (Section)SECTION_HANDLE.get(this); + } + + private final Section getSectionVolatile() { + return (Section)SECTION_HANDLE.getVolatile(this); + } + + private final void setSectionPlain(final Section update) { + SECTION_HANDLE.set(this, update); + } + + private final void setSectionOpaque(final Section update) { + SECTION_HANDLE.setOpaque(this, update); + } + + private final void setSectionVolatile(final Section update) { + SECTION_HANDLE.setVolatile(this, update); + } + + private final Section getAndSetSectionVolatile(final Section update) { + return (Section)SECTION_HANDLE.getAndSet(this, update); + } + + private final Section compareExchangeSectionVolatile(final Section expect, final Section update) { + return (Section)SECTION_HANDLE.compareAndExchange(this, expect, update); + } + + /* next */ + + private final UpdateQueueNode getNextPlain() { + return (UpdateQueueNode)NEXT_HANDLE.get(this); + } + + private final UpdateQueueNode getNextOpaque() { + return (UpdateQueueNode)NEXT_HANDLE.getOpaque(this); + } + + private final UpdateQueueNode getNextAcquire() { + return (UpdateQueueNode)NEXT_HANDLE.getAcquire(this); + } + + private final UpdateQueueNode getNextVolatile() { + return (UpdateQueueNode)NEXT_HANDLE.getVolatile(this); + } + + private final void setNextPlain(final UpdateQueueNode next) { + NEXT_HANDLE.set(this, next); + } + + private final void setNextVolatile(final UpdateQueueNode next) { + NEXT_HANDLE.setVolatile(this, next); + } + + private final UpdateQueueNode compareExchangeNextVolatile(final UpdateQueueNode expect, final UpdateQueueNode set) { + return (UpdateQueueNode)NEXT_HANDLE.compareAndExchange(this, expect, set); + } + + /* updating */ + + private final boolean getUpdatingVolatile() { + return (boolean)UPDATING_HANDLE.getVolatile(this); + } + + private final boolean getAndSetUpdatingVolatile(final boolean value) { + return (boolean)UPDATING_HANDLE.getAndSet(this, value); + } + } + } + + private static final class Section { + + // upper 8 bits: sources, lower 8 bits: level + // if we REALLY wanted to get crazy, we could make the increase propagator use MethodHandles#byteArrayViewVarHandle + // to read and write the lower 8 bits of this array directly rather than reading, updating the bits, then writing back. + private final short[] levels = new short[SECTION_SIZE * SECTION_SIZE]; + // set of local positions that represent sources + private final ShortOpenHashSet sources = new ShortOpenHashSet(); + // map of local index to new source level + // the source level _cannot_ be updated in the backing storage immediately since the update + private static final byte NO_QUEUED_UPDATE = (byte)-1; + private final Short2ByteLinkedOpenHashMap queuedSources = new Short2ByteLinkedOpenHashMap(); + { + this.queuedSources.defaultReturnValue(NO_QUEUED_UPDATE); + } + private int oneRadNeighboursWithSources = 0; + + public final int sectionX; + public final int sectionZ; + + public Section(final int sectionX, final int sectionZ) { + this.sectionX = sectionX; + this.sectionZ = sectionZ; + } + + public boolean isZero() { + for (final short val : this.levels) { + if (val != 0) { + return false; + } + } + return true; + } + + @Override + public String toString() { + final StringBuilder ret = new StringBuilder(); + + for (int x = 0; x < SECTION_SIZE; ++x) { + ret.append("levels x=").append(x).append("\n"); + for (int z = 0; z < SECTION_SIZE; ++z) { + final short v = this.levels[x | (z << SECTION_SHIFT)]; + ret.append(v & 0xFF).append("."); + } + ret.append("\n"); + ret.append("sources x=").append(x).append("\n"); + for (int z = 0; z < SECTION_SIZE; ++z) { + final short v = this.levels[x | (z << SECTION_SHIFT)]; + ret.append((v >>> 8) & 0xFF).append("."); + } + ret.append("\n\n"); + } + + return ret.toString(); + } + } + + + private static final class Propagator { + + private static final ArrayDeque CACHED_PROPAGATORS = new ArrayDeque<>(); + private static final int MAX_PROPAGATORS = Runtime.getRuntime().availableProcessors() * 2; + + private static Propagator acquirePropagator() { + synchronized (CACHED_PROPAGATORS) { + final Propagator ret = CACHED_PROPAGATORS.pollFirst(); + if (ret != null) { + return ret; + } + } + return new Propagator(); + } + + private static void returnPropagator(final Propagator propagator) { + synchronized (CACHED_PROPAGATORS) { + if (CACHED_PROPAGATORS.size() < MAX_PROPAGATORS) { + CACHED_PROPAGATORS.add(propagator); + } + } + } + + private static final int SECTION_RADIUS = 2; + private static final int SECTION_CACHE_WIDTH = 2 * SECTION_RADIUS + 1; + // minimum number of bits to represent [0, SECTION_SIZE * SECTION_CACHE_WIDTH) + private static final int COORDINATE_BITS = 9; + private static final int COORDINATE_SIZE = 1 << COORDINATE_BITS; + static { + if ((SECTION_SIZE * SECTION_CACHE_WIDTH) > (1 << COORDINATE_BITS)) { + throw new IllegalStateException("Adjust COORDINATE_BITS"); + } + } + // index = x + (z * SECTION_CACHE_WIDTH) + // (this requires x >= 0 and z >= 0) + private final Section[] sections = new Section[SECTION_CACHE_WIDTH * SECTION_CACHE_WIDTH]; + + private int encodeOffsetX; + private int encodeOffsetZ; + + private int coordinateOffset; + + private int encodeSectionOffsetX; + private int encodeSectionOffsetZ; + + private int sectionIndexOffset; + + public final boolean hasUpdates() { + return this.decreaseQueueInitialLength != 0 || this.increaseQueueInitialLength != 0; + } + + private final void setupEncodeOffset(final int centerSectionX, final int centerSectionZ) { + final int maxCoordinate = (SECTION_RADIUS * SECTION_SIZE - 1); + // must have that encoded >= 0 + // coordinates can range from [-maxCoordinate + centerSection*SECTION_SIZE, maxCoordinate + centerSection*SECTION_SIZE] + // we want a range of [0, maxCoordinate*2] + // so, 0 = -maxCoordinate + centerSection*SECTION_SIZE + offset + this.encodeOffsetX = maxCoordinate - (centerSectionX << SECTION_SHIFT); + this.encodeOffsetZ = maxCoordinate - (centerSectionZ << SECTION_SHIFT); + + // encoded coordinates range from [0, SECTION_SIZE * SECTION_CACHE_WIDTH) + // coordinate index = (x + encodeOffsetX) + ((z + encodeOffsetZ) << COORDINATE_BITS) + this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << COORDINATE_BITS); + + // need encoded values to be >= 0 + // so, 0 = (-SECTION_RADIUS + centerSectionX) + encodeOffset + this.encodeSectionOffsetX = SECTION_RADIUS - centerSectionX; + this.encodeSectionOffsetZ = SECTION_RADIUS - centerSectionZ; + + // section index = (secX + encodeSectionOffsetX) + ((secZ + encodeSectionOffsetZ) * SECTION_CACHE_WIDTH) + this.sectionIndexOffset = this.encodeSectionOffsetX + (this.encodeSectionOffsetZ * SECTION_CACHE_WIDTH); + } + + // must hold ticket lock for (centerSectionX,centerSectionZ) in radius rad + // must call setupEncodeOffset + private final void setupCaches(final ThreadedTicketLevelPropagator propagator, + final int centerSectionX, final int centerSectionZ, + final int rad) { + for (int dz = -rad; dz <= rad; ++dz) { + for (int dx = -rad; dx <= rad; ++dx) { + final int sectionX = centerSectionX + dx; + final int sectionZ = centerSectionZ + dz; + final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); + final Section section = propagator.sections.get(coordinate); + + if (section == null) { + throw new IllegalStateException("Section at " + coordinate + " should not be null"); + } + + this.setSectionInCache(sectionX, sectionZ, section); + } + } + } + + private final void setSectionInCache(final int sectionX, final int sectionZ, final Section section) { + this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset] = section; + } + + private final Section getSection(final int sectionX, final int sectionZ) { + return this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset]; + } + + private final int getLevel(final int posX, final int posZ) { + final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset]; + if (section != null) { + return (int)section.levels[(posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT)] & 0xFF; + } + + return 0; + } + + private final void setLevel(final int posX, final int posZ, final int to) { + final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset]; + if (section != null) { + final int index = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); + final short level = section.levels[index]; + section.levels[index] = (short)((level & ~0xFF) | (to & 0xFF)); + this.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)to); + } + } + + private final void destroyCaches() { + Arrays.fill(this.sections, null); + } + + // contains: + // lower (COORDINATE_BITS(9) + COORDINATE_BITS(9) = 18) bits encoded position: (x | (z << COORDINATE_BITS)) + // next LEVEL_BITS (6) bits: propagated level [0, 63] + // propagation directions bitset (16 bits): + private static final long ALL_DIRECTIONS_BITSET = ( + // z = -1 + (1L << ((1 - 1) | ((1 - 1) << 2))) | + (1L << ((1 + 0) | ((1 - 1) << 2))) | + (1L << ((1 + 1) | ((1 - 1) << 2))) | + + // z = 0 + (1L << ((1 - 1) | ((1 + 0) << 2))) | + //(1L << ((1 + 0) | ((1 + 0) << 2))) | // exclude (0,0) + (1L << ((1 + 1) | ((1 + 0) << 2))) | + + // z = 1 + (1L << ((1 - 1) | ((1 + 1) << 2))) | + (1L << ((1 + 0) | ((1 + 1) << 2))) | + (1L << ((1 + 1) | ((1 + 1) << 2))) + ); + + private void ex(int bitset) { + for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) { + final int set = Integer.numberOfTrailingZeros(bitset); + final int tailingBit = (-bitset) & bitset; + // XOR to remove the trailing bit + bitset ^= tailingBit; + + // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits + // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the + // index of the set bit is the encoded value + // the encoded coordinate has 3 valid states: + // 0b00 (0) -> -1 + // 0b01 (1) -> 0 + // 0b10 (2) -> 1 + // the decode operation then is val - 1, and the encode operation is val + 1 + final int xOff = (set & 3) - 1; + final int zOff = ((set >>> 2) & 3) - 1; + System.out.println("Encoded: (" + xOff + "," + zOff + ")"); + } + } + + private void ch(long bs, int shift) { + int bitset = (int)(bs >>> shift); + for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) { + final int set = Integer.numberOfTrailingZeros(bitset); + final int tailingBit = (-bitset) & bitset; + // XOR to remove the trailing bit + bitset ^= tailingBit; + + // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits + // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the + // index of the set bit is the encoded value + // the encoded coordinate has 3 valid states: + // 0b00 (0) -> -1 + // 0b01 (1) -> 0 + // 0b10 (2) -> 1 + // the decode operation then is val - 1, and the encode operation is val + 1 + final int xOff = (set & 3) - 1; + final int zOff = ((set >>> 2) & 3) - 1; + if (Math.abs(xOff) > 1 || Math.abs(zOff) > 1 || (xOff | zOff) == 0) { + throw new IllegalStateException(); + } + } + } + + // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading + // updates for sources + private static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 1; + // whether the propagation needs to check if its current level is equal to the expected level + // used only in increase propagation + private static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 0; + + private long[] increaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2]; + private int increaseQueueInitialLength; + private long[] decreaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2]; + private int decreaseQueueInitialLength; + + private final Long2ByteLinkedOpenHashMap updatedPositions = new Long2ByteLinkedOpenHashMap(); + + private final long[] resizeIncreaseQueue() { + return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2); + } + + private final long[] resizeDecreaseQueue() { + return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2); + } + + private final void appendToIncreaseQueue(final long value) { + final int idx = this.increaseQueueInitialLength++; + long[] queue = this.increaseQueue; + if (idx >= queue.length) { + queue = this.resizeIncreaseQueue(); + queue[idx] = value; + return; + } else { + queue[idx] = value; + return; + } + } + + private final void appendToDecreaseQueue(final long value) { + final int idx = this.decreaseQueueInitialLength++; + long[] queue = this.decreaseQueue; + if (idx >= queue.length) { + queue = this.resizeDecreaseQueue(); + queue[idx] = value; + return; + } else { + queue[idx] = value; + return; + } + } + + private final void performIncrease() { + long[] queue = this.increaseQueue; + int queueReadIndex = 0; + int queueLength = this.increaseQueueInitialLength; + this.increaseQueueInitialLength = 0; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetZ = -this.encodeOffsetZ; + final int encodeOffset = this.coordinateOffset; + final int sectionOffset = this.sectionIndexOffset; + + final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions; + + while (queueReadIndex < queueLength) { + final long queueValue = queue[queueReadIndex++]; + + final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX; + final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ; + final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1); + // note: the above code requires coordinate bits * 2 < 32 + // bitset is 16 bits + int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1); + + if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) { + if (this.getLevel(posX, posZ) != propagatedLevel) { + // not at the level we expect, so something changed. + continue; + } + } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) { + // these are used to restore sources after a propagation decrease + this.setLevel(posX, posZ, propagatedLevel); + } + + // this bitset represents the values that we have not propagated to + // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases + // significantly reducing the total number of ops + // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need + // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead + // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits) + // to make things easy, we use positions [0, 4] in the bitset, with current position being 2 + // index = x | (z << 3) + + // to start, we eliminate everything 1 radius from the current position as the previous propagator + // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius + // but the rest not propagated are already handled + long currentPropagation = ~( + // z = -1 + (1L << ((2 - 1) | ((2 - 1) << 3))) | + (1L << ((2 + 0) | ((2 - 1) << 3))) | + (1L << ((2 + 1) | ((2 - 1) << 3))) | + + // z = 0 + (1L << ((2 - 1) | ((2 + 0) << 3))) | + (1L << ((2 + 0) | ((2 + 0) << 3))) | + (1L << ((2 + 1) | ((2 + 0) << 3))) | + + // z = 1 + (1L << ((2 - 1) | ((2 + 1) << 3))) | + (1L << ((2 + 0) | ((2 + 1) << 3))) | + (1L << ((2 + 1) | ((2 + 1) << 3))) + ); + + final int toPropagate = propagatedLevel - 1; + + // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting + // the bits, the cpu loop predictor should perfectly predict the loop. + for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) { + final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset); + final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset; + propagateDirectionBitset ^= tailingBit; + + // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset + // it has been split to save some cycles via parallelism + final int pDecodeX = (set & 3); + final int pDecodeZ = ((set >>> 2) & 3); + + // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX + final int offX = (posX - 1) + pDecodeX; + final int offZ = (posZ - 1) + pDecodeZ; + + final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset; + final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); + + // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset + // bitset idx = x | (z << 3) + + // read three bits, so we need 7L + // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1 + // nstartidx1 = x rel -1 for z rel -1 + // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3) + // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3) + // = pDecodeX | (pDecodeZ << 3) = start + final int start = pDecodeX | (pDecodeZ << 3); + final long bitsetLine1 = currentPropagation & (7L << (start)); + + // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset) + final long bitsetLine2 = currentPropagation & (7L << (start + 8)); + + // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset) + final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8))); + + // remove ("take") lines from bitset + currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3); + + // now try to propagate + final Section section = this.sections[sectionIndex]; + + // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag + final short currentStoredLevel = section.levels[localIndex]; + final int currentLevel = currentStoredLevel & 0xFF; + + if (currentLevel >= toPropagate) { + continue; // already at the level we want + } + + // update level + section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF) | (toPropagate & 0xFF)); + updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)toPropagate); + + // queue next + if (toPropagate > 1) { + // now combine into one bitset to pass to child + // the child bitset is 4x4, so we just shift each line by 4 + // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value + final long childPropagation = + ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1 + ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0 + ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1 + + // don't queue update if toPropagate cannot propagate anything to neighbours + // (for increase, propagating 0 to neighbours is useless) + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | + ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | + childPropagation; //(ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); + continue; + } + continue; + } + } + } + + private final void performDecrease() { + long[] queue = this.decreaseQueue; + long[] increaseQueue = this.increaseQueue; + int queueReadIndex = 0; + int queueLength = this.decreaseQueueInitialLength; + this.decreaseQueueInitialLength = 0; + int increaseQueueLength = this.increaseQueueInitialLength; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetZ = -this.encodeOffsetZ; + final int encodeOffset = this.coordinateOffset; + final int sectionOffset = this.sectionIndexOffset; + + final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions; + + while (queueReadIndex < queueLength) { + final long queueValue = queue[queueReadIndex++]; + + final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX; + final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ; + final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1); + // note: the above code requires coordinate bits * 2 < 32 + // bitset is 16 bits + int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1); + + // this bitset represents the values that we have not propagated to + // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases + // significantly reducing the total number of ops + // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need + // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead + // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits) + // to make things easy, we use positions [0, 4] in the bitset, with current position being 2 + // index = x | (z << 3) + + // to start, we eliminate everything 1 radius from the current position as the previous propagator + // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius + // but the rest not propagated are already handled + long currentPropagation = ~( + // z = -1 + (1L << ((2 - 1) | ((2 - 1) << 3))) | + (1L << ((2 + 0) | ((2 - 1) << 3))) | + (1L << ((2 + 1) | ((2 - 1) << 3))) | + + // z = 0 + (1L << ((2 - 1) | ((2 + 0) << 3))) | + (1L << ((2 + 0) | ((2 + 0) << 3))) | + (1L << ((2 + 1) | ((2 + 0) << 3))) | + + // z = 1 + (1L << ((2 - 1) | ((2 + 1) << 3))) | + (1L << ((2 + 0) | ((2 + 1) << 3))) | + (1L << ((2 + 1) | ((2 + 1) << 3))) + ); + + final int toPropagate = propagatedLevel - 1; + + // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting + // the bits, the cpu loop predictor should perfectly predict the loop. + for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) { + final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset); + final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset; + propagateDirectionBitset ^= tailingBit; + + + // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset + // it has been split to save some cycles via parallelism + final int pDecodeX = (set & 3); + final int pDecodeZ = ((set >>> 2) & 3); + + // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX + final int offX = (posX - 1) + pDecodeX; + final int offZ = (posZ - 1) + pDecodeZ; + + final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset; + final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); + + // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset + // bitset idx = x | (z << 3) + + // read three bits, so we need 7L + // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1 + // nstartidx1 = x rel -1 for z rel -1 + // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3) + // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3) + // = pDecodeX | (pDecodeZ << 3) = start + final int start = pDecodeX | (pDecodeZ << 3); + final long bitsetLine1 = currentPropagation & (7L << (start)); + + // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset) + final long bitsetLine2 = currentPropagation & (7L << (start + 8)); + + // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset) + final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8))); + + // now try to propagate + final Section section = this.sections[sectionIndex]; + + // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag + final short currentStoredLevel = section.levels[localIndex]; + final int currentLevel = currentStoredLevel & 0xFF; + final int sourceLevel = (currentStoredLevel >>> 8) & 0xFF; + + if (currentLevel == 0) { + continue; // already at the level we want + } + + if (currentLevel > toPropagate) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | + ((currentLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | + (FLAG_RECHECK_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS))); + continue; + } + + // remove ("take") lines from bitset + // can't do this during decrease, TODO WHY? + //currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3); + + // update level + section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF)); + updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)0); + + if (sourceLevel != 0) { + // re-propagate source + // note: do not set recheck level, or else the propagation will fail + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | + ((sourceLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | + (FLAG_WRITE_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS))); + } + + // queue next + // note: targetLevel > 0 here, since toPropagate >= currentLevel and currentLevel > 0 + // now combine into one bitset to pass to child + // the child bitset is 4x4, so we just shift each line by 4 + // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value + final long childPropagation = + ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1 + ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0 + ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1 + + // don't queue update if toPropagate cannot propagate anything to neighbours + // (for increase, propagating 0 to neighbours is useless) + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | + ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | + (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); //childPropagation; + continue; + } + } + + // propagate sources we clobbered + this.increaseQueueInitialLength = increaseQueueLength; + this.performIncrease(); + } + } + + /* + private static final java.util.Random random = new java.util.Random(4L); + private static final List> walkers = + new java.util.ArrayList<>(); + static final int PLAYERS = 0; + static final int RAD_BLOCKS = 10000; + static final int RAD = RAD_BLOCKS >> 4; + static final int RAD_BIG_BLOCKS = 100_000; + static final int RAD_BIG = RAD_BIG_BLOCKS >> 4; + static final int VD = 4; + static final int BIG_PLAYERS = 50; + static final double WALK_CHANCE = 0.10; + static final double TP_CHANCE = 0.01; + static final int TP_BACK_PLAYERS = 200; + static final double TP_BACK_CHANCE = 0.25; + static final double TP_STEAL_CHANCE = 0.25; + private static final List> tpBack = + new java.util.ArrayList<>(); + + public static void main(final String[] args) { + final ReentrantAreaLock ticketLock = new ReentrantAreaLock(SECTION_SHIFT); + final ReentrantAreaLock schedulingLock = new ReentrantAreaLock(SECTION_SHIFT); + final Long2ByteLinkedOpenHashMap levelMap = new Long2ByteLinkedOpenHashMap(); + final Long2ByteLinkedOpenHashMap refMap = new Long2ByteLinkedOpenHashMap(); + final io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D ref = new io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D((final long coordinate, final byte oldLevel, final byte newLevel) -> { + if (newLevel == 0) { + refMap.remove(coordinate); + } else { + refMap.put(coordinate, newLevel); + } + }); + final ThreadedTicketLevelPropagator propagator = new ThreadedTicketLevelPropagator() { + @Override + protected void processLevelUpdates(Long2ByteLinkedOpenHashMap updates) { + for (final long key : updates.keySet()) { + final byte val = updates.get(key); + if (val == 0) { + levelMap.remove(key); + } else { + levelMap.put(key, val); + } + } + } + + @Override + protected void processSchedulingUpdates(Long2ByteLinkedOpenHashMap updates, List scheduledTasks, List changedFullStatus) {} + }; + + for (;;) { + if (walkers.isEmpty() && tpBack.isEmpty()) { + for (int i = 0; i < PLAYERS; ++i) { + int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; + int posX = random.nextInt(-rad, rad + 1); + int posZ = random.nextInt(-rad, rad + 1); + + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { + @Override + protected void addCallback(Void parameter, int chunkX, int chunkZ) { + int src = 45 - 31 + 1; + ref.setSource(chunkX, chunkZ, src); + propagator.setSource(chunkX, chunkZ, src); + } + + @Override + protected void removeCallback(Void parameter, int chunkX, int chunkZ) { + ref.removeSource(chunkX, chunkZ); + propagator.removeSource(chunkX, chunkZ); + } + }; + + map.add(posX, posZ, VD); + + walkers.add(map); + } + for (int i = 0; i < TP_BACK_PLAYERS; ++i) { + int rad = RAD_BIG; + int posX = random.nextInt(-rad, rad + 1); + int posZ = random.nextInt(-rad, rad + 1); + + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { + @Override + protected void addCallback(Void parameter, int chunkX, int chunkZ) { + int src = 45 - 31 + 1; + ref.setSource(chunkX, chunkZ, src); + propagator.setSource(chunkX, chunkZ, src); + } + + @Override + protected void removeCallback(Void parameter, int chunkX, int chunkZ) { + ref.removeSource(chunkX, chunkZ); + propagator.removeSource(chunkX, chunkZ); + } + }; + + map.add(posX, posZ, random.nextInt(1, 63)); + + tpBack.add(map); + } + } else { + for (int i = 0; i < PLAYERS; ++i) { + if (random.nextDouble() > WALK_CHANCE) { + continue; + } + + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = walkers.get(i); + + int updateX = random.nextInt(-1, 2); + int updateZ = random.nextInt(-1, 2); + + map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD); + } + + for (int i = 0; i < PLAYERS; ++i) { + if (random.nextDouble() > TP_CHANCE) { + continue; + } + + int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; + int posX = random.nextInt(-rad, rad + 1); + int posZ = random.nextInt(-rad, rad + 1); + + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = walkers.get(i); + + map.update(posX, posZ, VD); + } + + for (int i = 0; i < TP_BACK_PLAYERS; ++i) { + if (random.nextDouble() > TP_BACK_CHANCE) { + continue; + } + + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = tpBack.get(i); + + map.update(-map.lastChunkX, -map.lastChunkZ, random.nextInt(1, 63)); + + if (random.nextDouble() > TP_STEAL_CHANCE) { + propagator.performUpdate( + map.lastChunkX >> SECTION_SHIFT, map.lastChunkZ >> SECTION_SHIFT, schedulingLock, null, null + ); + propagator.performUpdate( + (-map.lastChunkX >> SECTION_SHIFT), (-map.lastChunkZ >> SECTION_SHIFT), schedulingLock, null, null + ); + } + } + } + + ref.propagateUpdates(); + propagator.performUpdates(ticketLock, schedulingLock, null, null); + + if (!refMap.equals(levelMap)) { + throw new IllegalStateException("Error!"); + } + } + } + */ +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..5f4b99d8c5453f8ad2e600a57ea4e7dafa2d45f8 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java @@ -0,0 +1,729 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor; + +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.PriorityQueue; + +public class RadiusAwarePrioritisedExecutor { + + private static final Comparator DEPENDENCY_NODE_COMPARATOR = (final DependencyNode t1, final DependencyNode t2) -> { + return Long.compare(t1.id, t2.id); + }; + + private final PrioritisedExecutor executor; + private final DependencyTree[] queues = new DependencyTree[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; + private static final int NO_TASKS_QUEUED = -1; + private int selectedQueue = NO_TASKS_QUEUED; + private boolean canQueueTasks = true; + + public RadiusAwarePrioritisedExecutor(final PrioritisedExecutor executor, final int maxToSchedule) { + this.executor = executor; + + for (int i = 0; i < this.queues.length; ++i) { + this.queues[i] = new DependencyTree(this, executor, maxToSchedule); + } + } + + public void setMaxToSchedule(final int maxToSchedule) { + final List tasks; + + synchronized (this) { + for (final DependencyTree dependencyTree : this.queues) { + dependencyTree.maxToSchedule = maxToSchedule; + } + + if (this.selectedQueue == NO_TASKS_QUEUED || !this.canQueueTasks) { + return; + } + + tasks = this.queues[this.selectedQueue].tryPushTasks(); + } + + scheduleTasks(tasks); + } + + private boolean canQueueTasks() { + return this.canQueueTasks; + } + + private List treeFinished() { + this.canQueueTasks = true; + for (int priority = 0; priority < this.queues.length; ++priority) { + final DependencyTree queue = this.queues[priority]; + if (queue.hasWaitingTasks()) { + final List ret = queue.tryPushTasks(); + + if (ret == null || ret.isEmpty()) { + // this happens when the tasks in the wait queue were purged + // in this case, the queue was actually empty, we just had to purge it + // if we set the selected queue without scheduling any tasks, the queue will never be unselected + // as that requires a scheduled task completing... + continue; + } + + this.selectedQueue = priority; + return ret; + } + } + + this.selectedQueue = NO_TASKS_QUEUED; + + return null; + } + + private List queue(final Task task, final Priority priority) { + final int priorityId = priority.priority; + final DependencyTree queue = this.queues[priorityId]; + + final DependencyNode node = new DependencyNode(task, queue); + + if (task.dependencyNode != null) { + throw new IllegalStateException(); + } + task.dependencyNode = node; + + queue.pushNode(node); + + if (this.selectedQueue == NO_TASKS_QUEUED) { + this.canQueueTasks = true; + this.selectedQueue = priorityId; + return queue.tryPushTasks(); + } + + if (!this.canQueueTasks) { + return null; + } + + if (Priority.isHigherPriority(priorityId, this.selectedQueue)) { + // prevent the lower priority tree from queueing more tasks + this.canQueueTasks = false; + return null; + } + + // priorityId != selectedQueue: lower priority, don't care - treeFinished will pick it up + return priorityId == this.selectedQueue ? queue.tryPushTasks() : null; + } + + public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius, + final Runnable run, final Priority priority) { + if (radius < 0) { + throw new IllegalArgumentException("Radius must be > 0: " + radius); + } + return new Task(this, chunkX, chunkZ, radius, run, priority); + } + + public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius, + final Runnable run) { + return this.createTask(chunkX, chunkZ, radius, run, Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius, + final Runnable run, final Priority priority) { + final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run, priority); + + ret.queue(); + + return ret; + } + + public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius, + final Runnable run) { + final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run); + + ret.queue(); + + return ret; + } + + public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run, final Priority priority) { + return new Task(this, 0, 0, -1, run, priority); + } + + public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run) { + return this.createInfiniteRadiusTask(run, Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final Priority priority) { + final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, priority); + + ret.queue(); + + return ret; + } + + public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run) { + final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, Priority.NORMAL); + + ret.queue(); + + return ret; + } + + private static void scheduleTasks(final List toSchedule) { + if (toSchedule != null) { + for (int i = 0, len = toSchedule.size(); i < len; ++i) { + toSchedule.get(i).queue(); + } + } + } + + // all accesses must be synchronised by the radius aware object + private static final class DependencyTree { + + private final RadiusAwarePrioritisedExecutor scheduler; + private final PrioritisedExecutor executor; + private int maxToSchedule; + + private int currentlyExecuting; + private long idGenerator; + + private final PriorityQueue awaiting = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); + + private final PriorityQueue infiniteRadius = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); + private boolean isInfiniteRadiusScheduled; + + private final Long2ReferenceOpenHashMap nodeByPosition = new Long2ReferenceOpenHashMap<>(); + + public DependencyTree(final RadiusAwarePrioritisedExecutor scheduler, final PrioritisedExecutor executor, + final int maxToSchedule) { + this.scheduler = scheduler; + this.executor = executor; + this.maxToSchedule = maxToSchedule; + } + + public boolean hasWaitingTasks() { + return !this.awaiting.isEmpty() || !this.infiniteRadius.isEmpty(); + } + + private long nextId() { + return this.idGenerator++; + } + + private boolean isExecutingAnyTasks() { + return this.currentlyExecuting != 0; + } + + private void pushNode(final DependencyNode node) { + if (!node.task.isFiniteRadius()) { + this.infiniteRadius.add(node); + return; + } + + // set up dependency for node + final Task task = node.task; + + final int centerX = task.chunkX; + final int centerZ = task.chunkZ; + final int radius = task.radius; + + final int minX = centerX - radius; + final int maxX = centerX + radius; + + final int minZ = centerZ - radius; + final int maxZ = centerZ + radius; + + ReferenceOpenHashSet parents = null; + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final DependencyNode dependency = this.nodeByPosition.put(CoordinateUtils.getChunkKey(currX, currZ), node); + if (dependency != null) { + if (parents == null) { + parents = new ReferenceOpenHashSet<>(); + } + if (parents.add(dependency)) { + // added a dependency, so we need to add as a child to the dependency + if (dependency.children == null) { + dependency.children = new ArrayList<>(); + } + dependency.children.add(node); + } + } + } + } + + if (parents == null) { + // no dependencies, add straight to awaiting + this.awaiting.add(node); + } else { + node.parents = parents.size(); + // we will be added to awaiting once we have no parents + } + } + + // called only when a node is returned after being executed + private List returnNode(final DependencyNode node) { + final Task task = node.task; + + // now that the task is completed, we can push its children to the awaiting queue + this.pushChildren(node); + + if (task.isFiniteRadius()) { + // remove from dependency map + this.removeNodeFromMap(node); + } else { + // mark as no longer executing infinite radius + if (!this.isInfiniteRadiusScheduled) { + throw new IllegalStateException(); + } + this.isInfiniteRadiusScheduled = false; + } + + // decrement executing count, we are done executing this task + --this.currentlyExecuting; + + if (this.currentlyExecuting == 0) { + return this.scheduler.treeFinished(); + } + + return this.scheduler.canQueueTasks() ? this.tryPushTasks() : null; + } + + private List tryPushTasks() { + // tasks are not queued, but only created here - we do hold the lock for the map + List ret = null; + PrioritisedExecutor.PrioritisedTask pushedTask; + while ((pushedTask = this.tryPushTask()) != null) { + if (ret == null) { + ret = new ArrayList<>(); + } + ret.add(pushedTask); + } + + return ret; + } + + private void removeNodeFromMap(final DependencyNode node) { + final Task task = node.task; + + final int centerX = task.chunkX; + final int centerZ = task.chunkZ; + final int radius = task.radius; + + final int minX = centerX - radius; + final int maxX = centerX + radius; + + final int minZ = centerZ - radius; + final int maxZ = centerZ + radius; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + this.nodeByPosition.remove(CoordinateUtils.getChunkKey(currX, currZ), node); + } + } + } + + private void pushChildren(final DependencyNode node) { + // add all the children that we can into awaiting + final List children = node.children; + if (children != null) { + for (int i = 0, len = children.size(); i < len; ++i) { + final DependencyNode child = children.get(i); + int newParents = --child.parents; + if (newParents == 0) { + // no more dependents, we can push to awaiting + // even if the child is purged, we need to push it so that its children will be pushed + this.awaiting.add(child); + } else if (newParents < 0) { + throw new IllegalStateException(); + } + } + } + } + + private DependencyNode pollAwaiting() { + final DependencyNode ret = this.awaiting.poll(); + if (ret == null) { + return ret; + } + + if (ret.parents != 0) { + throw new IllegalStateException(); + } + + if (ret.purged) { + // need to manually remove from state here + this.pushChildren(ret); + this.removeNodeFromMap(ret); + } // else: delay children push until the task has finished + + return ret; + } + + private DependencyNode pollInfinite() { + return this.infiniteRadius.poll(); + } + + public PrioritisedExecutor.PrioritisedTask tryPushTask() { + if (this.currentlyExecuting >= this.maxToSchedule || this.isInfiniteRadiusScheduled) { + return null; + } + + DependencyNode firstInfinite; + while ((firstInfinite = this.infiniteRadius.peek()) != null && firstInfinite.purged) { + this.pollInfinite(); + } + + DependencyNode firstAwaiting; + while ((firstAwaiting = this.awaiting.peek()) != null && firstAwaiting.purged) { + this.pollAwaiting(); + } + + if (firstInfinite == null && firstAwaiting == null) { + return null; + } + + // firstAwaiting compared to firstInfinite + final int compare; + + if (firstAwaiting == null) { + // we choose first infinite, or infinite < awaiting + compare = 1; + } else if (firstInfinite == null) { + // we choose first awaiting, or awaiting < infinite + compare = -1; + } else { + compare = DEPENDENCY_NODE_COMPARATOR.compare(firstAwaiting, firstInfinite); + } + + if (compare >= 0) { + if (this.currentlyExecuting != 0) { + // don't queue infinite task while other tasks are executing in parallel + return null; + } + ++this.currentlyExecuting; + this.pollInfinite(); + this.isInfiniteRadiusScheduled = true; + return firstInfinite.task.pushTask(this.executor); + } else { + ++this.currentlyExecuting; + this.pollAwaiting(); + return firstAwaiting.task.pushTask(this.executor); + } + } + } + + private static final class DependencyNode { + + private final Task task; + private final DependencyTree tree; + + // dependency tree fields + // (must hold lock on the scheduler to use) + // null is the same as empty, we just use it so that we don't allocate the set unless we need to + private List children; + // 0 indicates that this task is considered "awaiting" + private int parents; + // false -> scheduled and not cancelled + // true -> scheduled but cancelled + private boolean purged; + private final long id; + + public DependencyNode(final Task task, final DependencyTree tree) { + this.task = task; + this.id = tree.nextId(); + this.tree = tree; + } + } + + private static final class Task implements PrioritisedExecutor.PrioritisedTask, Runnable { + + // task specific fields + private final RadiusAwarePrioritisedExecutor scheduler; + private final int chunkX; + private final int chunkZ; + private final int radius; + private Runnable run; + private Priority priority; + + private DependencyNode dependencyNode; + private PrioritisedExecutor.PrioritisedTask queuedTask; + + private Task(final RadiusAwarePrioritisedExecutor scheduler, final int chunkX, final int chunkZ, final int radius, + final Runnable run, final Priority priority) { + this.scheduler = scheduler; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.radius = radius; + this.run = run; + this.priority = priority; + } + + private boolean isFiniteRadius() { + return this.radius >= 0; + } + + private PrioritisedExecutor.PrioritisedTask pushTask(final PrioritisedExecutor executor) { + return this.queuedTask = executor.createTask(this, this.priority); + } + + private void executeTask() { + final Runnable run = this.run; + this.run = null; + run.run(); + } + + private void returnNode() { + final List toSchedule; + synchronized (this.scheduler) { + final DependencyNode node = this.dependencyNode; + this.dependencyNode = null; + toSchedule = node.tree.returnNode(node); + } + + scheduleTasks(toSchedule); + } + + @Override + public PrioritisedExecutor getExecutor() { + return this.scheduler.executor; + } + + @Override + public void run() { + final Runnable run = this.run; + this.run = null; + try { + run.run(); + } finally { + this.returnNode(); + } + } + + @Override + public boolean queue() { + final List toSchedule; + synchronized (this.scheduler) { + if (this.queuedTask != null || this.dependencyNode != null || this.priority == Priority.COMPLETING) { + return false; + } + + toSchedule = this.scheduler.queue(this, this.priority); + } + + scheduleTasks(toSchedule); + return true; + } + + @Override + public boolean isQueued() { + synchronized (this.scheduler) { + return (this.queuedTask != null || this.dependencyNode != null) && this.priority != Priority.COMPLETING; + } + } + + @Override + public boolean cancel() { + final PrioritisedExecutor.PrioritisedTask task; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { + if (this.priority == Priority.COMPLETING) { + return false; + } + + this.priority = Priority.COMPLETING; + if (this.dependencyNode != null) { + this.dependencyNode.purged = true; + this.dependencyNode = null; + } + + return true; + } + } + + if (task.cancel()) { + // must manually return the node + this.run = null; + this.returnNode(); + return true; + } + return false; + } + + @Override + public boolean execute() { + final PrioritisedExecutor.PrioritisedTask task; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { + if (this.priority == Priority.COMPLETING) { + return false; + } + + this.priority = Priority.COMPLETING; + if (this.dependencyNode != null) { + this.dependencyNode.purged = true; + this.dependencyNode = null; + } + // fall through to execution logic + } + } + + if (task != null) { + // will run the return node logic automatically + return task.execute(); + } else { + // don't run node removal/insertion logic, we aren't actually removed from the dependency tree + this.executeTask(); + return true; + } + } + + @Override + public Priority getPriority() { + final PrioritisedExecutor.PrioritisedTask task; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { + return this.priority; + } + } + + return task.getPriority(); + } + + @Override + public boolean setPriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + final PrioritisedExecutor.PrioritisedTask task; + List toSchedule = null; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { + if (this.priority == Priority.COMPLETING) { + return false; + } + + if (this.priority == priority) { + return true; + } + + this.priority = priority; + if (this.dependencyNode != null) { + // need to re-insert node + this.dependencyNode.purged = true; + this.dependencyNode = null; + toSchedule = this.scheduler.queue(this, priority); + } + } + } + + if (task != null) { + return task.setPriority(priority); + } + + scheduleTasks(toSchedule); + + return true; + } + + @Override + public boolean raisePriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + final PrioritisedExecutor.PrioritisedTask task; + List toSchedule = null; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { + if (this.priority == Priority.COMPLETING) { + return false; + } + + if (this.priority.isHigherOrEqualPriority(priority)) { + return true; + } + + this.priority = priority; + if (this.dependencyNode != null) { + // need to re-insert node + this.dependencyNode.purged = true; + this.dependencyNode = null; + toSchedule = this.scheduler.queue(this, priority); + } + } + } + + if (task != null) { + return task.raisePriority(priority); + } + + scheduleTasks(toSchedule); + + return true; + } + + @Override + public boolean lowerPriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + final PrioritisedExecutor.PrioritisedTask task; + List toSchedule = null; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { + if (this.priority == Priority.COMPLETING) { + return false; + } + + if (this.priority.isLowerOrEqualPriority(priority)) { + return true; + } + + this.priority = priority; + if (this.dependencyNode != null) { + // need to re-insert node + this.dependencyNode.purged = true; + this.dependencyNode = null; + toSchedule = this.scheduler.queue(this, priority); + } + } + } + + if (task != null) { + return task.lowerPriority(priority); + } + + scheduleTasks(toSchedule); + + return true; + } + + @Override + public long getSubOrder() { + // TODO implement + return 0; + } + + @Override + public boolean setSubOrder(final long subOrder) { + // TODO implement + return false; + } + + @Override + public boolean raiseSubOrder(final long subOrder) { + // TODO implement + return false; + } + + @Override + public boolean lowerSubOrder(final long subOrder) { + // TODO implement + return false; + } + + @Override + public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) { + // TODO implement + return this.setPriority(priority); + } + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java new file mode 100644 index 0000000000000000000000000000000000000000..6ab353b0d2465c3680bb3c8d0852ba0f65c00fd2 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java @@ -0,0 +1,151 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.PlatformHooks; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk; +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager; +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ImposterProtoChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.status.ChunkStatusTasks; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.lang.invoke.VarHandle; + +public final class ChunkFullTask extends ChunkProgressionTask implements Runnable { + + private static final Logger LOGGER = LoggerFactory.getLogger(ChunkFullTask.class); + + private final NewChunkHolder chunkHolder; + private final ChunkAccess fromChunk; + private final PrioritisedExecutor.PrioritisedTask convertToFullTask; + + public ChunkFullTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, + final NewChunkHolder chunkHolder, final ChunkAccess fromChunk, final Priority priority) { + super(scheduler, world, chunkX, chunkZ); + this.chunkHolder = chunkHolder; + this.fromChunk = fromChunk; + this.convertToFullTask = scheduler.createChunkTask(chunkX, chunkZ, this, priority); + } + + @Override + public ChunkStatus getTargetStatus() { + return ChunkStatus.FULL; + } + + @Override + public void run() { + final PlatformHooks platformHooks = PlatformHooks.get(); + + // See Vanilla ChunkPyramid#LOADING_PYRAMID.FULL for what this function should be doing + final LevelChunk chunk; + try { + // moved from the load from nbt stage into here + final PoiChunk poiChunk = this.chunkHolder.getPoiChunk(); + if (poiChunk == null) { + LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString()); + } else { + poiChunk.load(); + ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$checkConsistency(this.fromChunk); + } + + if (this.fromChunk instanceof ImposterProtoChunk wrappedFull) { + chunk = wrappedFull.getWrapped(); + } else { + final ServerLevel world = this.world; + final ProtoChunk protoChunk = (ProtoChunk)this.fromChunk; + chunk = new LevelChunk(this.world, protoChunk, (final LevelChunk unused) -> { + PlatformHooks.get().postLoadProtoChunk(world, protoChunk); + }); + this.chunkHolder.replaceProtoChunk(new ImposterProtoChunk(chunk, false)); + } + + ((ChunkSystemLevelChunk)chunk).moonrise$setChunkAndHolder(new ServerChunkCache.ChunkAndHolder(chunk, this.chunkHolder.vanillaChunkHolder)); + + final NewChunkHolder chunkHolder = this.chunkHolder; + + chunk.setFullStatus(chunkHolder::getChunkStatus); + try { + platformHooks.setCurrentlyLoading(this.chunkHolder.vanillaChunkHolder, chunk); + chunk.runPostLoad(); + // Unlike Vanilla, we load the entity chunk here, as we load the NBT in empty status (unlike Vanilla) + // This brings entity addition back in line with older versions of the game + // Since we load the NBT in the empty status, this will never block for I/O + ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getOrCreateEntityChunk(this.chunkX, this.chunkZ, false); + chunk.setLoaded(true); + chunk.registerAllBlockEntitiesAfterLevelLoad(); + chunk.registerTickContainerInLevel(this.world); + chunk.setUnsavedListener(this.world.getChunkSource().chunkMap.worldGenContext.unsavedListener()); + platformHooks.chunkFullStatusComplete(chunk, (ProtoChunk)this.fromChunk); + } finally { + platformHooks.setCurrentlyLoading(this.chunkHolder.vanillaChunkHolder, null); + } + } catch (final Throwable throwable) { + this.complete(null, throwable); + return; + } + this.complete(chunk, null); + } + + protected volatile boolean scheduled; + protected static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkFullTask.class, "scheduled", boolean.class); + + @Override + public boolean isScheduled() { + return this.scheduled; + } + + @Override + public void schedule() { + if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkFullTask)this, true)) { + throw new IllegalStateException("Cannot double call schedule()"); + } + this.convertToFullTask.queue(); + } + + @Override + public void cancel() { + if (this.convertToFullTask.cancel()) { + this.complete(null, null); + } + } + + @Override + public Priority getPriority() { + return this.convertToFullTask.getPriority(); + } + + @Override + public void lowerPriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.convertToFullTask.lowerPriority(priority); + } + + @Override + public void setPriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.convertToFullTask.setPriority(priority); + } + + @Override + public void raisePriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.convertToFullTask.raisePriority(priority); + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java new file mode 100644 index 0000000000000000000000000000000000000000..4538ccfaea83d217ed85eaf16e82393c7f286489 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java @@ -0,0 +1,181 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.PriorityHolder; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import java.util.function.BooleanSupplier; + +public final class ChunkLightTask extends ChunkProgressionTask { + + private static final Logger LOGGER = LogManager.getLogger(); + + private final ChunkAccess fromChunk; + + private final LightTaskPriorityHolder priorityHolder; + + public ChunkLightTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, + final ChunkAccess chunk, final Priority priority) { + super(scheduler, world, chunkX, chunkZ); + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.priorityHolder = new LightTaskPriorityHolder(priority, this); + this.fromChunk = chunk; + } + + @Override + public boolean isScheduled() { + return this.priorityHolder.isScheduled(); + } + + @Override + public ChunkStatus getTargetStatus() { + return ChunkStatus.LIGHT; + } + + @Override + public void schedule() { + this.priorityHolder.schedule(); + } + + @Override + public void cancel() { + this.priorityHolder.cancel(); + } + + @Override + public Priority getPriority() { + return this.priorityHolder.getPriority(); + } + + @Override + public void lowerPriority(final Priority priority) { + this.priorityHolder.raisePriority(priority); + } + + @Override + public void setPriority(final Priority priority) { + this.priorityHolder.setPriority(priority); + } + + @Override + public void raisePriority(final Priority priority) { + this.priorityHolder.raisePriority(priority); + } + + private static final class LightTaskPriorityHolder extends PriorityHolder { + + private final ChunkLightTask task; + + private LightTaskPriorityHolder(final Priority priority, final ChunkLightTask task) { + super(priority); + this.task = task; + } + + @Override + protected void cancelScheduled() { + final ChunkLightTask task = this.task; + task.complete(null, null); + } + + @Override + protected Priority getScheduledPriority() { + final ChunkLightTask task = this.task; + return ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine().getServerLightQueue().getPriority(task.chunkX, task.chunkZ); + } + + @Override + protected void scheduleTask(final Priority priority) { + final ChunkLightTask task = this.task; + final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); + final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); + lightQueue.queueChunkLightTask(new ChunkPos(task.chunkX, task.chunkZ), new LightTask(starLightInterface, task), priority); + lightQueue.setPriority(task.chunkX, task.chunkZ, priority); + } + + @Override + protected void lowerPriorityScheduled(final Priority priority) { + final ChunkLightTask task = this.task; + final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); + final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); + lightQueue.lowerPriority(task.chunkX, task.chunkZ, priority); + } + + @Override + protected void setPriorityScheduled(final Priority priority) { + final ChunkLightTask task = this.task; + final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); + final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); + lightQueue.setPriority(task.chunkX, task.chunkZ, priority); + } + + @Override + protected void raisePriorityScheduled(final Priority priority) { + final ChunkLightTask task = this.task; + final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); + final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); + lightQueue.raisePriority(task.chunkX, task.chunkZ, priority); + } + } + + private static final class LightTask implements BooleanSupplier { + + private final StarLightInterface lightEngine; + private final ChunkLightTask task; + + public LightTask(final StarLightInterface lightEngine, final ChunkLightTask task) { + this.lightEngine = lightEngine; + this.task = task; + } + + @Override + public boolean getAsBoolean() { + final ChunkLightTask task = this.task; + // executed on light thread + if (!task.priorityHolder.markExecuting()) { + // cancelled + return false; + } + + try { + final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(task.fromChunk); + + if (task.fromChunk.isLightCorrect() && task.fromChunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) { + this.lightEngine.forceLoadInChunk(task.fromChunk, emptySections); + this.lightEngine.checkChunkEdges(task.chunkX, task.chunkZ); + } else { + task.fromChunk.setLightCorrect(false); + this.lightEngine.lightChunk(task.fromChunk, emptySections); + task.fromChunk.setLightCorrect(true); + } + // we need to advance status + if (task.fromChunk instanceof ProtoChunk chunk && chunk.getPersistedStatus() == ChunkStatus.LIGHT.getParent()) { + chunk.setPersistedStatus(ChunkStatus.LIGHT); + } + } catch (final Throwable thr) { + LOGGER.fatal( + "Failed to light chunk " + task.fromChunk.getPos().toString() + + " in world '" + WorldUtil.getWorldName(this.lightEngine.getWorld()) + "'", thr + ); + + task.complete(null, thr); + + return true; + } + + task.complete(task.fromChunk, null); + return true; + } + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java new file mode 100644 index 0000000000000000000000000000000000000000..1440c9e2b106616884edcb20201113320817ed9f --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java @@ -0,0 +1,494 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.PlatformHooks; +import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemConverters; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.storage.SerializableChunkData; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.lang.invoke.VarHandle; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +public final class ChunkLoadTask extends ChunkProgressionTask { + + private static final Logger LOGGER = LoggerFactory.getLogger(ChunkLoadTask.class); + + private final NewChunkHolder chunkHolder; + private final ChunkDataLoadTask loadTask; + + private volatile boolean cancelled; + private NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; + private NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask; + private GenericDataLoadTask.TaskResult loadResult; + private final AtomicInteger taskCountToComplete = new AtomicInteger(3); // one for poi, one for entity, and one for chunk data + + public ChunkLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, + final NewChunkHolder chunkHolder, final Priority priority) { + super(scheduler, world, chunkX, chunkZ); + this.chunkHolder = chunkHolder; + this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority); + this.loadTask.addCallback((final GenericDataLoadTask.TaskResult result) -> { + ChunkLoadTask.this.loadResult = result; // must be before getAndDecrement + ChunkLoadTask.this.tryCompleteLoad(); + }); + } + + private void tryCompleteLoad() { + final int count = this.taskCountToComplete.decrementAndGet(); + if (count == 0) { + final GenericDataLoadTask.TaskResult result = this.cancelled ? null : this.loadResult; // only after the getAndDecrement + ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right()); + } else if (count < 0) { + throw new IllegalStateException("Called tryCompleteLoad() too many times"); + } + } + + @Override + public ChunkStatus getTargetStatus() { + return ChunkStatus.EMPTY; + } + + private boolean scheduled; + + @Override + public boolean isScheduled() { + return this.scheduled; + } + + @Override + public void schedule() { + final NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; + final NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask; + + final Consumer> scheduleLoadTask = (final GenericDataLoadTask.TaskResult result) -> { + ChunkLoadTask.this.tryCompleteLoad(); + }; + + // NOTE: it is IMPOSSIBLE for getOrLoadEntityData/getOrLoadPoiData to complete synchronously, because + // they must schedule a task to off main or to on main to complete + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + if (this.scheduled) { + throw new IllegalStateException("schedule() called twice"); + } + this.scheduled = true; + if (this.cancelled) { + return; + } + if (!this.chunkHolder.isEntityChunkNBTLoaded()) { + entityLoadTask = this.chunkHolder.getOrLoadEntityData((Consumer)scheduleLoadTask); + } else { + entityLoadTask = null; + this.tryCompleteLoad(); + } + + if (!this.chunkHolder.isPoiChunkLoaded()) { + poiLoadTask = this.chunkHolder.getOrLoadPoiData((Consumer)scheduleLoadTask); + } else { + poiLoadTask = null; + this.tryCompleteLoad(); + } + + this.entityLoadTask = entityLoadTask; + this.poiLoadTask = poiLoadTask; + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + + if (entityLoadTask != null) { + entityLoadTask.schedule(); + } + + if (poiLoadTask != null) { + poiLoadTask.schedule(); + } + + this.loadTask.schedule(false); + } + + @Override + public void cancel() { + // must be before load task access, so we can synchronise with the writes to the fields + final boolean scheduled; + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + // must read field here, as it may be written later conucrrently - + // we need to know if we scheduled _before_ cancellation + scheduled = this.scheduled; + this.cancelled = true; + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + + /* + Note: The entityLoadTask/poiLoadTask do not complete when cancelled, + so we need to manually try to complete in those cases + It is also important to note that we set the cancelled field first, just in case + the chunk load task attempts to complete with a non-null value + */ + + if (scheduled) { + // since we scheduled, we need to cancel the tasks + if (this.entityLoadTask != null) { + if (this.entityLoadTask.cancel()) { + this.tryCompleteLoad(); + } + } + if (this.poiLoadTask != null) { + if (this.poiLoadTask.cancel()) { + this.tryCompleteLoad(); + } + } + } else { + // since nothing was scheduled, we need to decrement the task count here ourselves + + // for entity load task + this.tryCompleteLoad(); + + // for poi load task + this.tryCompleteLoad(); + } + this.loadTask.cancel(); + } + + @Override + public Priority getPriority() { + return this.loadTask.getPriority(); + } + + @Override + public void lowerPriority(final Priority priority) { + final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); + if (entityLoad != null) { + entityLoad.lowerPriority(priority); + } + + final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); + + if (poiLoad != null) { + poiLoad.lowerPriority(priority); + } + + this.loadTask.lowerPriority(priority); + } + + @Override + public void setPriority(final Priority priority) { + final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); + if (entityLoad != null) { + entityLoad.setPriority(priority); + } + + final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); + + if (poiLoad != null) { + poiLoad.setPriority(priority); + } + + this.loadTask.setPriority(priority); + } + + @Override + public void raisePriority(final Priority priority) { + final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); + if (entityLoad != null) { + entityLoad.raisePriority(priority); + } + + final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); + + if (poiLoad != null) { + poiLoad.raisePriority(priority); + } + + this.loadTask.raisePriority(priority); + } + + protected static abstract class CallbackDataLoadTask extends GenericDataLoadTask { + + private TaskResult result; + private final MultiThreadedQueue>> waiters = new MultiThreadedQueue<>(); + + protected volatile boolean completed; + protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(CallbackDataLoadTask.class, "completed", boolean.class); + + protected CallbackDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, + final int chunkZ, final MoonriseRegionFileIO.RegionFileType type, + final Priority priority) { + super(scheduler, world, chunkX, chunkZ, type, priority); + } + + public void addCallback(final Consumer> consumer) { + if (!this.waiters.add(consumer)) { + try { + consumer.accept(this.result); + } catch (final Throwable throwable) { + this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( + "Consumer", ChunkTaskScheduler.stringIfNull(consumer), + "Completed throwable", ChunkTaskScheduler.stringIfNull(this.result.right()), + "CallbackDataLoadTask impl", this.getClass().getName() + ), throwable); + } + } + } + + @Override + protected void onComplete(final TaskResult result) { + if ((boolean)COMPLETED_HANDLE.getAndSet((CallbackDataLoadTask)this, (boolean)true)) { + throw new IllegalStateException("Already completed"); + } + this.result = result; + Consumer> consumer; + while ((consumer = this.waiters.pollOrBlockAdds()) != null) { + try { + consumer.accept(result); + } catch (final Throwable throwable) { + this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( + "Consumer", ChunkTaskScheduler.stringIfNull(consumer), + "Completed throwable", ChunkTaskScheduler.stringIfNull(result.right()), + "CallbackDataLoadTask impl", this.getClass().getName() + ), throwable); + return; + } + } + } + } + + + private static record ReadChunk(ProtoChunk protoChunk, SerializableChunkData chunkData) {} + + private static final class ChunkDataLoadTask extends CallbackDataLoadTask { + private ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, + final int chunkZ, final Priority priority) { + super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, priority); + } + + @Override + protected boolean hasOffMain() { + return true; + } + + @Override + protected boolean hasOnMain() { + return true; + } + + @Override + protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority) { + return this.scheduler.loadExecutor.createTask(run, priority); + } + + @Override + protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) { + return this.scheduler.createChunkTask(this.chunkX, this.chunkZ, run, priority); + } + + @Override + protected TaskResult completeOnMainOffMain(final ReadChunk data, final Throwable throwable) { + if (throwable != null) { + return new TaskResult<>(null, throwable); + } + + if (data == null || data.protoChunk() == null) { + return new TaskResult<>(this.getEmptyChunk(), null); + } + + if (!PlatformHooks.get().hasMainChunkLoadHook()) { + return new TaskResult<>(data.protoChunk(), null); + } + + // need to invoke the callback for loading on the main thread + return null; + } + + private ProtoChunk getEmptyChunk() { + return new ProtoChunk( + new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, this.world, + this.world.registryAccess().lookupOrThrow(Registries.BIOME), (BlendingData)null + ); + } + + @Override + protected TaskResult runOffMain(final CompoundTag data, final Throwable throwable) { + if (throwable != null) { + LOGGER.error("Failed to load chunk data for task: " + this.toString() + ", chunk data will be lost", throwable); + return new TaskResult<>(null, null); + } + + if (data == null) { + return new TaskResult<>(null, null); + } + + try { + // run converters + final CompoundTag converted = this.world.getChunkSource().chunkMap.upgradeChunkTag(data, new ChunkPos(this.chunkX, this.chunkZ)); // Paper + + // unpack the data + final SerializableChunkData chunkData = SerializableChunkData.parse( + this.world, this.world.registryAccess(), converted + ); + + if (chunkData == null) { + LOGGER.error("Deserialized chunk for task: " + this.toString() + " produced null, chunk data will be lost?"); + } + + // read into ProtoChunk + final ProtoChunk chunk = chunkData == null ? null : chunkData.read( + this.world, this.world.getPoiManager(), this.world.getChunkSource().chunkMap.storageInfo(), + new ChunkPos(this.chunkX, this.chunkZ) + ); + + return new TaskResult<>(new ReadChunk(chunk, chunkData), null); + } catch (final Throwable thr2) { + LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2); + return new TaskResult<>(null, null); + } + } + + @Override + protected TaskResult runOnMain(final ReadChunk data, final Throwable throwable) { + PlatformHooks.get().mainChunkLoad(data.protoChunk(), data.chunkData()); + + return new TaskResult<>(data.protoChunk(), null); + } + } + + public static final class PoiDataLoadTask extends CallbackDataLoadTask { + + public PoiDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, + final int chunkZ, final Priority priority) { + super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.POI_DATA, priority); + } + + @Override + protected boolean hasOffMain() { + return true; + } + + @Override + protected boolean hasOnMain() { + return false; + } + + @Override + protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority) { + return this.scheduler.loadExecutor.createTask(run, priority); + } + + @Override + protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) { + throw new UnsupportedOperationException(); + } + + @Override + protected TaskResult completeOnMainOffMain(final PoiChunk data, final Throwable throwable) { + throw new UnsupportedOperationException(); + } + + @Override + protected TaskResult runOffMain(final CompoundTag data, final Throwable throwable) { + if (throwable != null) { + LOGGER.error("Failed to load poi data for task: " + this.toString() + ", poi data will be lost", throwable); + return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); + } + + if (data == null || data.isEmpty()) { + // nothing to do + return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); + } + + try { + // run converters + final CompoundTag converted = ChunkSystemConverters.convertPoiCompoundTag(data, this.world); + + // now we need to parse it + return new TaskResult<>(PoiChunk.parse(this.world, this.chunkX, this.chunkZ, converted), null); + } catch (final Throwable thr2) { + LOGGER.error("Failed to run parse poi data for task: " + this.toString() + ", poi data will be lost", thr2); + return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); + } + } + + @Override + protected TaskResult runOnMain(final PoiChunk data, final Throwable throwable) { + throw new UnsupportedOperationException(); + } + } + + public static final class EntityDataLoadTask extends CallbackDataLoadTask { + + public EntityDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, + final int chunkZ, final Priority priority) { + super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, priority); + } + + @Override + protected boolean hasOffMain() { + return true; + } + + @Override + protected boolean hasOnMain() { + return false; + } + + @Override + protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority) { + return this.scheduler.loadExecutor.createTask(run, priority); + } + + @Override + protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) { + throw new UnsupportedOperationException(); + } + + @Override + protected TaskResult completeOnMainOffMain(final CompoundTag data, final Throwable throwable) { + throw new UnsupportedOperationException(); + } + + @Override + protected TaskResult runOffMain(final CompoundTag data, final Throwable throwable) { + if (throwable != null) { + LOGGER.error("Failed to load entity data for task: " + this.toString() + ", entity data will be lost", throwable); + return new TaskResult<>(null, null); + } + + if (data == null || data.isEmpty()) { + // nothing to do + return new TaskResult<>(null, null); + } + + try { + return new TaskResult<>(ChunkSystemConverters.convertEntityChunkCompoundTag(data, this.world), null); + } catch (final Throwable thr2) { + LOGGER.error("Failed to run converters for entity data for task: " + this.toString() + ", entity data will be lost", thr2); + return new TaskResult<>(null, thr2); + } + } + + @Override + protected TaskResult runOnMain(final CompoundTag data, final Throwable throwable) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java new file mode 100644 index 0000000000000000000000000000000000000000..002ee365aa70d8e6a6e6bd5c95988bd17db4395a --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java @@ -0,0 +1,101 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import java.lang.invoke.VarHandle; +import java.util.Map; +import java.util.function.BiConsumer; + +public abstract class ChunkProgressionTask { + + private final MultiThreadedQueue> waiters = new MultiThreadedQueue<>(); + private ChunkAccess completedChunk; + private Throwable completedThrowable; + + protected final ChunkTaskScheduler scheduler; + protected final ServerLevel world; + protected final int chunkX; + protected final int chunkZ; + + protected volatile boolean completed; + protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(ChunkProgressionTask.class, "completed", boolean.class); + + protected ChunkProgressionTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ) { + this.scheduler = scheduler; + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + } + + // Used only for debug json + public abstract boolean isScheduled(); + + // Note: It is the responsibility of the task to set the chunk's status once it has completed + public abstract ChunkStatus getTargetStatus(); + + /* Only executed once */ + /* Implementations must be prepared to handle cases where cancel() is called before schedule() */ + public abstract void schedule(); + + /* May be called multiple times */ + public abstract void cancel(); + + public abstract Priority getPriority(); + + /* Schedule lock is always held for the priority update calls */ + + public abstract void lowerPriority(final Priority priority); + + public abstract void setPriority(final Priority priority); + + public abstract void raisePriority(final Priority priority); + + public final void onComplete(final BiConsumer onComplete) { + if (!this.waiters.add(onComplete)) { + try { + onComplete.accept(this.completedChunk, this.completedThrowable); + } catch (final Throwable throwable) { + this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( + "Consumer", ChunkTaskScheduler.stringIfNull(onComplete), + "Completed throwable", ChunkTaskScheduler.stringIfNull(this.completedThrowable) + ), throwable); + } + } + } + + protected final void complete(final ChunkAccess chunk, final Throwable throwable) { + try { + this.complete0(chunk, throwable); + } catch (final Throwable thr2) { + this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( + "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable) + ), thr2); + } + } + + private void complete0(final ChunkAccess chunk, final Throwable throwable) { + if ((boolean)COMPLETED_HANDLE.getAndSet((ChunkProgressionTask)this, (boolean)true)) { + throw new IllegalStateException("Already completed"); + } + this.completedChunk = chunk; + this.completedThrowable = throwable; + + BiConsumer consumer; + while ((consumer = this.waiters.pollOrBlockAdds()) != null) { + consumer.accept(chunk, throwable); + } + } + + @Override + public String toString() { + return "ChunkProgressionTask{class: " + this.getClass().getName() + ", for world: " + WorldUtil.getWorldName(this.world) + + ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() + + ", status: " + this.getTargetStatus().toString() + ", scheduled: " + this.isScheduled() + "}"; + } +} \ No newline at end of file diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java new file mode 100644 index 0000000000000000000000000000000000000000..25d8da4773dcee5096053e7e3788bfc224d705a7 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java @@ -0,0 +1,218 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.GenerationChunkHolder; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.StaticCache2D; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.status.ChunkPyramid; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.status.WorldGenContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.lang.invoke.VarHandle; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public final class ChunkUpgradeGenericStatusTask extends ChunkProgressionTask implements Runnable { + + private static final Logger LOGGER = LoggerFactory.getLogger(ChunkUpgradeGenericStatusTask.class); + + private final ChunkAccess fromChunk; + private final ChunkStatus fromStatus; + private final ChunkStatus toStatus; + private final StaticCache2D neighbours; + + private final PrioritisedExecutor.PrioritisedTask generateTask; + + public ChunkUpgradeGenericStatusTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, + final int chunkZ, final ChunkAccess chunk, final StaticCache2D neighbours, + final ChunkStatus toStatus, final Priority priority) { + super(scheduler, world, chunkX, chunkZ); + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.fromChunk = chunk; + this.fromStatus = chunk.getPersistedStatus(); + this.toStatus = toStatus; + this.neighbours = neighbours; + if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isParallelCapable()) { + this.generateTask = this.scheduler.parallelGenExecutor.createTask(this, priority); + } else { + final int writeRadius = ((ChunkSystemChunkStatus)this.toStatus).moonrise$getWriteRadius(); + if (writeRadius < 0) { + this.generateTask = this.scheduler.radiusAwareScheduler.createInfiniteRadiusTask(this, priority); + } else { + this.generateTask = this.scheduler.radiusAwareScheduler.createTask(chunkX, chunkZ, writeRadius, this, priority); + } + } + } + + @Override + public ChunkStatus getTargetStatus() { + return this.toStatus; + } + + private boolean isEmptyTask() { + // must use fromStatus here to avoid any race condition with run() overwriting the status + final boolean generation = !this.fromStatus.isOrAfter(this.toStatus); + return (generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) || (!generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus()); + } + + @Override + public void run() { + final ChunkAccess chunk = this.fromChunk; + + final ServerChunkCache serverChunkCache = this.world.getChunkSource(); + final ChunkMap chunkMap = serverChunkCache.chunkMap; + + final CompletableFuture completeFuture; + + final boolean generation; + boolean completing = false; + + // note: should optimise the case where the chunk does not need to execute the status, because + // schedule() calls this synchronously if it will run through that path + + final WorldGenContext ctx = chunkMap.worldGenContext; + try { + generation = !chunk.getPersistedStatus().isOrAfter(this.toStatus); + if (generation) { + if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) { + if (chunk instanceof ProtoChunk) { + ((ProtoChunk)chunk).setPersistedStatus(this.toStatus); + } + completing = true; + this.complete(chunk, null); + return; + } + completeFuture = ChunkPyramid.GENERATION_PYRAMID.getStepTo(this.toStatus).apply(ctx, this.neighbours, this.fromChunk) + .whenComplete((final ChunkAccess either, final Throwable throwable) -> { + if (either instanceof ProtoChunk proto) { + proto.setPersistedStatus(ChunkUpgradeGenericStatusTask.this.toStatus); + } + } + ); + } else { + if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus()) { + completing = true; + this.complete(chunk, null); + return; + } + completeFuture = ChunkPyramid.LOADING_PYRAMID.getStepTo(this.toStatus).apply(ctx, this.neighbours, this.fromChunk); + } + } catch (final Throwable throwable) { + if (!completing) { + this.complete(null, throwable); + return; + } + + this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( + "Target status", ChunkTaskScheduler.stringIfNull(this.toStatus), + "From status", ChunkTaskScheduler.stringIfNull(this.fromStatus), + "Generation task", this + ), throwable); + + LOGGER.error( + "Failed to complete status for chunk: status:" + this.toStatus + ", chunk: (" + this.chunkX + + "," + this.chunkZ + "), world: " + WorldUtil.getWorldName(this.world), + throwable + ); + + return; + } + + if (!completeFuture.isDone() && !((ChunkSystemChunkStatus)this.toStatus).moonrise$getWarnedAboutNoImmediateComplete().getAndSet(true)) { + LOGGER.warn("Future status not complete after scheduling: " + this.toStatus.toString() + ", generate: " + generation); + } + + final ChunkAccess newChunk; + + try { + newChunk = completeFuture.join(); + } catch (final Throwable throwable) { + this.complete(null, throwable); + return; + } + + if (newChunk == null) { + this.complete(null, + new IllegalStateException( + "Chunk for status: " + ChunkUpgradeGenericStatusTask.this.toStatus.toString() + + ", generation: " + generation + " should not be null! Future: " + completeFuture + ).fillInStackTrace() + ); + return; + } + + this.complete(newChunk, null); + } + + private volatile boolean scheduled; + private static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkUpgradeGenericStatusTask.class, "scheduled", boolean.class); + + @Override + public boolean isScheduled() { + return this.scheduled; + } + + @Override + public void schedule() { + if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkUpgradeGenericStatusTask)this, true)) { + throw new IllegalStateException("Cannot double call schedule()"); + } + if (this.isEmptyTask()) { + if (this.generateTask.cancel()) { + this.run(); + } + } else { + this.generateTask.queue(); + } + } + + @Override + public void cancel() { + if (this.generateTask.cancel()) { + this.complete(null, null); + } + } + + @Override + public Priority getPriority() { + return this.generateTask.getPriority(); + } + + @Override + public void lowerPriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.generateTask.lowerPriority(priority); + } + + @Override + public void setPriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.generateTask.setPriority(priority); + } + + @Override + public void raisePriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.generateTask.raisePriority(priority); + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java new file mode 100644 index 0000000000000000000000000000000000000000..bdcd1879457bafcca4e76523aac0555968f37c0b --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java @@ -0,0 +1,674 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + +import ca.spottedleaf.concurrentutil.completable.CallbackCompletable; +import ca.spottedleaf.concurrentutil.completable.Completable; +import ca.spottedleaf.concurrentutil.executor.Cancellable; +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.lang.invoke.VarHandle; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; + +public abstract class GenericDataLoadTask { + + private static final Logger LOGGER = LoggerFactory.getLogger(GenericDataLoadTask.class); + + protected static final CompoundTag CANCELLED_DATA = new CompoundTag(); + + // reference count is the upper 32 bits + protected final AtomicLong stageAndReferenceCount = new AtomicLong(STAGE_NOT_STARTED); + + protected static final long STAGE_MASK = 0xFFFFFFFFL; + protected static final long STAGE_CANCELLED = 0xFFFFFFFFL; + protected static final long STAGE_NOT_STARTED = 0L; + protected static final long STAGE_LOADING = 1L; + protected static final long STAGE_PROCESSING = 2L; + protected static final long STAGE_COMPLETED = 3L; + + // for loading data off disk + protected final LoadDataFromDiskTask loadDataFromDiskTask; + // processing off-main + protected final PrioritisedExecutor.PrioritisedTask processOffMain; + // processing on-main + protected final PrioritisedExecutor.PrioritisedTask processOnMain; + + protected final ChunkTaskScheduler scheduler; + protected final ServerLevel world; + protected final int chunkX; + protected final int chunkZ; + protected final MoonriseRegionFileIO.RegionFileType type; + + public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, + final int chunkZ, final MoonriseRegionFileIO.RegionFileType type, + final Priority priority) { + this.scheduler = scheduler; + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.type = type; + + final ProcessOnMainTask mainTask; + if (this.hasOnMain()) { + mainTask = new ProcessOnMainTask(); + this.processOnMain = this.createOnMain(mainTask, priority); + } else { + mainTask = null; + this.processOnMain = null; + } + + final ProcessOffMainTask offMainTask; + if (this.hasOffMain()) { + offMainTask = new ProcessOffMainTask(mainTask); + this.processOffMain = this.createOffMain(offMainTask, priority); + } else { + offMainTask = null; + this.processOffMain = null; + } + + if (this.processOffMain == null && this.processOnMain == null) { + throw new IllegalStateException("Illegal class implementation: " + this.getClass().getName() + ", should be able to schedule at least one task!"); + } + + this.loadDataFromDiskTask = new LoadDataFromDiskTask(world, chunkX, chunkZ, type, new DataLoadCallback(offMainTask, mainTask), priority); + } + + public static final record TaskResult(L left, R right) {} + + protected abstract boolean hasOffMain(); + + protected abstract boolean hasOnMain(); + + protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority); + + protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority); + + protected abstract TaskResult runOffMain(final CompoundTag data, final Throwable throwable); + + protected abstract TaskResult runOnMain(final OnMain data, final Throwable throwable); + + protected abstract void onComplete(final TaskResult result); + + protected abstract TaskResult completeOnMainOffMain(final OnMain data, final Throwable throwable); + + @Override + public String toString() { + return "GenericDataLoadTask{class: " + this.getClass().getName() + ", world: " + WorldUtil.getWorldName(this.world) + + ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() + + ", type: " + this.type.toString() + "}"; + } + + public Priority getPriority() { + if (this.processOnMain != null) { + return this.processOnMain.getPriority(); + } else { + return this.processOffMain.getPriority(); + } + } + + public void lowerPriority(final Priority priority) { + // can't lower I/O tasks, we don't know what they affect + if (this.processOffMain != null) { + this.processOffMain.lowerPriority(priority); + } + if (this.processOnMain != null) { + this.processOnMain.lowerPriority(priority); + } + } + + public void setPriority(final Priority priority) { + // can't lower I/O tasks, we don't know what they affect + this.loadDataFromDiskTask.raisePriority(priority); + if (this.processOffMain != null) { + this.processOffMain.setPriority(priority); + } + if (this.processOnMain != null) { + this.processOnMain.setPriority(priority); + } + } + + public void raisePriority(final Priority priority) { + // can't lower I/O tasks, we don't know what they affect + this.loadDataFromDiskTask.raisePriority(priority); + if (this.processOffMain != null) { + this.processOffMain.raisePriority(priority); + } + if (this.processOnMain != null) { + this.processOnMain.raisePriority(priority); + } + } + + // returns whether scheduleNow() needs to be called + public boolean schedule(final boolean delay) { + if (this.stageAndReferenceCount.get() != STAGE_NOT_STARTED || + !this.stageAndReferenceCount.compareAndSet(STAGE_NOT_STARTED, (1L << 32) | STAGE_LOADING)) { + // try and increment reference count + int failures = 0; + for (long curr = this.stageAndReferenceCount.get();;) { + if ((curr & STAGE_MASK) == STAGE_CANCELLED || (curr & STAGE_MASK) == STAGE_COMPLETED) { + // cancelled or completed, nothing to do here + return false; + } + + if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, curr + (1L << 32)))) { + // successful + return false; + } + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + if (!delay) { + this.scheduleNow(); + return false; + } + return true; + } + + public void scheduleNow() { + this.loadDataFromDiskTask.schedule(); // will schedule the rest + } + + // assumes the current stage cannot be completed + // returns false if cancelled, returns true if can proceed + private boolean advanceStage(final long expect, final long to) { + int failures = 0; + for (long curr = this.stageAndReferenceCount.get();;) { + if ((curr & STAGE_MASK) != expect) { + // must be cancelled + return false; + } + + final long newVal = (curr & ~STAGE_MASK) | to; + if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { + return true; + } + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public boolean cancel() { + int failures = 0; + for (long curr = this.stageAndReferenceCount.get();;) { + if ((curr & STAGE_MASK) == STAGE_COMPLETED || (curr & STAGE_MASK) == STAGE_CANCELLED) { + return false; + } + + if ((curr & STAGE_MASK) == STAGE_NOT_STARTED || (curr & ~STAGE_MASK) == (1L << 32)) { + // no other references, so we can cancel + final long newVal = STAGE_CANCELLED; + if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { + this.loadDataFromDiskTask.cancel(); + if (this.processOffMain != null) { + this.processOffMain.cancel(); + } + if (this.processOnMain != null) { + this.processOnMain.cancel(); + } + this.onComplete(null); + return true; + } + } else { + if ((curr & ~STAGE_MASK) == (0L << 32)) { + throw new IllegalStateException("Reference count cannot be zero here"); + } + // just decrease the reference count + final long newVal = curr - (1L << 32); + if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { + return false; + } + } + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + private final class DataLoadCallback implements BiConsumer { + + private final ProcessOffMainTask offMainTask; + private final ProcessOnMainTask onMainTask; + + public DataLoadCallback(final ProcessOffMainTask offMainTask, final ProcessOnMainTask onMainTask) { + this.offMainTask = offMainTask; + this.onMainTask = onMainTask; + } + + @Override + public void accept(final CompoundTag compoundTag, final Throwable throwable) { + if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) { + // don't try to schedule further + return; + } + + try { + if (compoundTag == CANCELLED_DATA) { + // cancelled, except this isn't possible + LOGGER.error("Data callback says cancelled, but stage does not?"); + return; + } + + // get off of the regionfile callback ASAP, no clue what locks are held right now... + if (GenericDataLoadTask.this.processOffMain != null) { + this.offMainTask.data = compoundTag; + this.offMainTask.throwable = throwable; + GenericDataLoadTask.this.processOffMain.queue(); + return; + } else { + // no off-main task, so go straight to main + this.onMainTask.data = (OnMain)compoundTag; + this.onMainTask.throwable = throwable; + GenericDataLoadTask.this.processOnMain.queue(); + } + } catch (final Throwable thr2) { + LOGGER.error("Failed I/O callback for task: " + GenericDataLoadTask.this.toString(), thr2); + GenericDataLoadTask.this.scheduler.unrecoverableChunkSystemFailure( + GenericDataLoadTask.this.chunkX, GenericDataLoadTask.this.chunkZ, Map.of( + "Callback throwable", ChunkTaskScheduler.stringIfNull(throwable) + ), thr2 + ); + } + } + } + + private final class ProcessOffMainTask implements Runnable { + + private CompoundTag data; + private Throwable throwable; + private final ProcessOnMainTask schedule; + + public ProcessOffMainTask(final ProcessOnMainTask schedule) { + this.schedule = schedule; + } + + @Override + public void run() { + if (!GenericDataLoadTask.this.advanceStage(STAGE_LOADING, this.schedule == null ? STAGE_COMPLETED : STAGE_PROCESSING)) { + // cancelled + return; + } + final TaskResult newData = GenericDataLoadTask.this.runOffMain(this.data, this.throwable); + + if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) { + // don't try to schedule further + return; + } + + if (this.schedule != null) { + final TaskResult syncComplete = GenericDataLoadTask.this.completeOnMainOffMain(newData.left, newData.right); + + if (syncComplete != null) { + if (GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) { + GenericDataLoadTask.this.onComplete(syncComplete); + } // else: cancelled + return; + } + + this.schedule.data = newData.left; + this.schedule.throwable = newData.right; + + GenericDataLoadTask.this.processOnMain.queue(); + } else { + GenericDataLoadTask.this.onComplete((TaskResult)newData); + } + } + } + + private final class ProcessOnMainTask implements Runnable { + + private OnMain data; + private Throwable throwable; + + @Override + public void run() { + if (!GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) { + // cancelled + return; + } + final TaskResult result = GenericDataLoadTask.this.runOnMain(this.data, this.throwable); + + GenericDataLoadTask.this.onComplete(result); + } + } + + protected static final class LoadDataFromDiskTask { + + private volatile int priority; + private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(LoadDataFromDiskTask.class, "priority", int.class); + + private static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 0; + private static final int PRIORITY_LOAD_SCHEDULED = Integer.MIN_VALUE >>> 1; + private static final int PRIORITY_UNLOAD_SCHEDULED = Integer.MIN_VALUE >>> 2; + + private static final int PRIORITY_FLAGS = ~Character.MAX_VALUE; + + private final int getPriorityVolatile() { + return (int)PRIORITY_HANDLE.getVolatile((LoadDataFromDiskTask)this); + } + + private final int compareAndExchangePriorityVolatile(final int expect, final int update) { + return (int)PRIORITY_HANDLE.compareAndExchange((LoadDataFromDiskTask)this, (int)expect, (int)update); + } + + private final int getAndOrPriorityVolatile(final int val) { + return (int)PRIORITY_HANDLE.getAndBitwiseOr((LoadDataFromDiskTask)this, (int)val); + } + + private final void setPriorityPlain(final int val) { + PRIORITY_HANDLE.set((LoadDataFromDiskTask)this, (int)val); + } + + private final ServerLevel world; + private final int chunkX; + private final int chunkZ; + + private final MoonriseRegionFileIO.RegionFileType type; + private Cancellable dataLoadTask; + private Cancellable dataUnloadCancellable; + private PrioritisedExecutor.PrioritisedTask dataUnloadTask; + + private final BiConsumer onComplete; + private final AtomicBoolean scheduled = new AtomicBoolean(); + + // onComplete should be caller sensitive, it may complete synchronously with schedule() - which does + // hold a priority lock. + public LoadDataFromDiskTask(final ServerLevel world, final int chunkX, final int chunkZ, + final MoonriseRegionFileIO.RegionFileType type, + final BiConsumer onComplete, + final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.type = type; + this.onComplete = onComplete; + this.setPriorityPlain(priority.priority); + } + + private void complete(final CompoundTag data, final Throwable throwable) { + try { + this.onComplete.accept(data, throwable); + } catch (final Throwable thr2) { + ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( + "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable), + "Regionfile type", ChunkTaskScheduler.stringIfNull(this.type) + ), thr2); + } + } + + private boolean markExecuting() { + return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0; + } + + private boolean isMarkedExecuted() { + return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0; + } + + public void lowerPriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_EXECUTED) != 0) { + // cancelled or executed + return; + } + + if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { + MoonriseRegionFileIO.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); + return; + } + + if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { + if (this.dataUnloadTask != null) { + this.dataUnloadTask.lowerPriority(priority); + } + // no return - we need to propagate priority + } + + if (!priority.isHigherPriority(curr & ~PRIORITY_FLAGS)) { + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public void setPriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_EXECUTED) != 0) { + // cancelled or executed + return; + } + + if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { + MoonriseRegionFileIO.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); + return; + } + + if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { + if (this.dataUnloadTask != null) { + this.dataUnloadTask.setPriority(priority); + } + // no return - we need to propagate priority + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public void raisePriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_EXECUTED) != 0) { + // cancelled or executed + return; + } + + if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { + MoonriseRegionFileIO.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority); + return; + } + + if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { + if (this.dataUnloadTask != null) { + this.dataUnloadTask.raisePriority(priority); + } + // no return - we need to propagate priority + } + + if (!priority.isLowerPriority(curr & ~PRIORITY_FLAGS)) { + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public void cancel() { + if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) { + // cancelled or executed already + return; + } + + // OK if we miss the field read, the task cannot complete if the cancelled bit is set and + // the write to dataLoadTask will check for the cancelled bit + if (this.dataUnloadCancellable != null) { + this.dataUnloadCancellable.cancel(); + } + + if (this.dataLoadTask != null) { + this.dataLoadTask.cancel(); + } + + this.complete(CANCELLED_DATA, null); + } + + public void schedule() { + if (this.scheduled.getAndSet(true)) { + throw new IllegalStateException("schedule() called twice"); + } + int priority = this.getPriorityVolatile(); + + if ((priority & PRIORITY_EXECUTED) != 0) { + // cancelled + return; + } + + final BiConsumer consumer = (final CompoundTag data, final Throwable thr) -> { + // because cancelScheduled() cannot actually stop this task from executing in every case, we need + // to mark complete here to ensure we do not double complete + if (LoadDataFromDiskTask.this.markExecuting()) { + LoadDataFromDiskTask.this.complete(data, thr); + } // else: cancelled + }; + + final Priority initialPriority = Priority.getPriority(priority); + boolean scheduledUnload = false; + + final NewChunkHolder holder = ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ); + if (holder != null) { + final BiConsumer unloadConsumer = (final CompoundTag data, final Throwable thr) -> { + if (data != null) { + consumer.accept(data, null); + } else { + // need to schedule task + LoadDataFromDiskTask.this.schedule(false, consumer, Priority.getPriority(LoadDataFromDiskTask.this.getPriorityVolatile() & ~PRIORITY_FLAGS)); + } + }; + Cancellable unloadCancellable = null; + CompoundTag syncComplete = null; + final NewChunkHolder.UnloadTask unloadTask = holder.getUnloadTask(this.type); // can be null if no task exists + final CallbackCompletable unloadCompletable = unloadTask == null ? null : unloadTask.completable(); + if (unloadCompletable != null) { + unloadCancellable = unloadCompletable.addAsynchronousWaiter(unloadConsumer); + if (unloadCancellable == null) { + syncComplete = unloadCompletable.getResult(); + } + } + + if (syncComplete != null) { + consumer.accept(syncComplete, null); + return; + } + + if (unloadCancellable != null) { + scheduledUnload = true; + this.dataUnloadCancellable = unloadCancellable; + this.dataUnloadTask = unloadTask.task(); + } + } + + this.schedule(scheduledUnload, consumer, initialPriority); + } + + private void schedule(final boolean scheduledUnload, final BiConsumer consumer, final Priority initialPriority) { + int priority = this.getPriorityVolatile(); + + if ((priority & PRIORITY_EXECUTED) != 0) { + // cancelled + return; + } + + if (!scheduledUnload) { + this.dataLoadTask = MoonriseRegionFileIO.loadDataAsync( + this.world, this.chunkX, this.chunkZ, this.type, consumer, + initialPriority.isHigherPriority(Priority.NORMAL), initialPriority + ); + } + + int failures = 0; + for (;;) { + if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | (scheduledUnload ? PRIORITY_UNLOAD_SCHEDULED : PRIORITY_LOAD_SCHEDULED)))) { + return; + } + + if ((priority & PRIORITY_EXECUTED) != 0) { + // cancelled or executed + if (this.dataUnloadCancellable != null) { + this.dataUnloadCancellable.cancel(); + } + + if (this.dataLoadTask != null) { + this.dataLoadTask.cancel(); + } + return; + } + + if (scheduledUnload) { + if (this.dataUnloadTask != null) { + this.dataUnloadTask.setPriority(Priority.getPriority(priority & ~PRIORITY_FLAGS)); + } + } else { + MoonriseRegionFileIO.setPriority(this.world, this.chunkX, this.chunkZ, this.type, Priority.getPriority(priority & ~PRIORITY_FLAGS)); + } + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java b/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java new file mode 100644 index 0000000000000000000000000000000000000000..cb6af3712bf9f6f6b8f7a459c309c75dabe83a50 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.server; + +public interface ChunkSystemMinecraftServer { + + public void moonrise$setChunkSystemCrash(final Throwable throwable); + + public void moonrise$executeMidTickTasks(); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java b/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java new file mode 100644 index 0000000000000000000000000000000000000000..ea759ce6f10f2a5a4e107ab7528030fe931ba223 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.status; + +import net.minecraft.world.level.chunk.status.ChunkStatus; + +public interface ChunkSystemChunkStep { + + public ChunkStatus moonrise$getRequiredStatusAtRadius(final int radius); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java new file mode 100644 index 0000000000000000000000000000000000000000..51c126735ace8fdde89ad97b5cab62f244212db0 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java @@ -0,0 +1,12 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.storage; + +import net.minecraft.world.level.chunk.storage.RegionFile; +import java.io.IOException; + +public interface ChunkSystemChunkBuffer { + public boolean moonrise$getWriteOnClose(); + + public void moonrise$setWriteOnClose(final boolean value); + + public void moonrise$write(final RegionFile regionFile) throws IOException; +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..129a35ff2db5b3bb6736810fc180796ce55e1875 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.storage; + +import net.minecraft.world.level.chunk.storage.RegionFileStorage; + +public interface ChunkSystemChunkStorage { + + public RegionFileStorage moonrise$getRegionStorage(); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java new file mode 100644 index 0000000000000000000000000000000000000000..3bd1b59250dbab15097a64d515999b278636795a --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java @@ -0,0 +1,12 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.storage; + +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.ChunkPos; +import java.io.IOException; + +public interface ChunkSystemRegionFile { + + public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(final CompoundTag data, final ChunkPos pos) throws IOException; + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java b/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java new file mode 100644 index 0000000000000000000000000000000000000000..786e6ad17cd6216ef0aadaa7cf10044a0c19c933 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.ticket; + +public interface ChunkSystemTicket { + + public long moonrise$getRemoveDelay(); + + public void moonrise$setRemoveDelay(final long removeDelay); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java b/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java new file mode 100644 index 0000000000000000000000000000000000000000..2add7fd15a2210286aeb9af5024263333340d34c --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.ticks; + +public interface ChunkSystemLevelChunkTicks { + + public boolean moonrise$isDirty(final long tick); + + public void moonrise$clearDirty(); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java b/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java new file mode 100644 index 0000000000000000000000000000000000000000..ce3bb903c9ccb7efa0f004cf79b291dcb1cb7a23 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java @@ -0,0 +1,15 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.util; + +import net.minecraft.util.SortedArraySet; + +public interface ChunkSystemSortedArraySet { + + public SortedArraySet moonrise$copy(); + + public Object[] moonrise$copyBackingArray(); + + public T moonrise$replace(final T object); + + public T moonrise$removeAndGet(final T object); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java b/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java new file mode 100644 index 0000000000000000000000000000000000000000..93fd23027c00cef76562098306737272fda1350a --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java @@ -0,0 +1,321 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.util; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.MoonriseConstants; +import it.unimi.dsi.fastutil.HashCommon; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import java.util.Arrays; +import java.util.Objects; + +public final class ParallelSearchRadiusIteration { + + // expected that this list returns for a given radius, the set of chunks ordered + // by manhattan distance + private static final long[][] SEARCH_RADIUS_ITERATION_LIST = new long[MoonriseConstants.MAX_VIEW_DISTANCE+2+1][]; + static { + for (int i = 0; i < SEARCH_RADIUS_ITERATION_LIST.length; ++i) { + // a BFS around -x, -z, +x, +z will give increasing manhatten distance + SEARCH_RADIUS_ITERATION_LIST[i] = generateBFSOrder(i); + } + } + + public static long[] getSearchIteration(final int radius) { + return SEARCH_RADIUS_ITERATION_LIST[radius]; + } + + private static class CustomLongArray extends LongArrayList { + + public CustomLongArray() { + super(); + } + + public CustomLongArray(final int expected) { + super(expected); + } + + public boolean addAll(final CustomLongArray list) { + this.addElements(this.size, list.a, 0, list.size); + return list.size != 0; + } + + public void addUnchecked(final long value) { + this.a[this.size++] = value; + } + + public void forceSize(final int to) { + this.size = to; + } + + @Override + public int hashCode() { + long h = 1L; + + Objects.checkFromToIndex(0, this.size, this.a.length); + + for (int i = 0; i < this.size; ++i) { + h = HashCommon.mix(h + this.a[i]); + } + + return (int)h; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof CustomLongArray other)) { + return false; + } + + return this.size == other.size && Arrays.equals(this.a, 0, this.size, other.a, 0, this.size); + } + } + + private static int getDistanceSize(final int radius, final int max) { + if (radius == 0) { + return 1; + } + final int diff = radius - max; + if (diff <= 0) { + return 4*radius; + } + return 4*(max - Math.max(0, diff - 1)); + } + + private static int getQ1DistanceSize(final int radius, final int max) { + if (radius == 0) { + return 1; + } + final int diff = radius - max; + if (diff <= 0) { + return radius+1; + } + return max - diff + 1; + } + + private static final class BasicFIFOLQueue { + + private final long[] values; + private int head, tail; + + public BasicFIFOLQueue(final int cap) { + if (cap <= 1) { + throw new IllegalArgumentException(); + } + this.values = new long[cap]; + } + + public boolean isEmpty() { + return this.head == this.tail; + } + + public long removeFirst() { + final long ret = this.values[this.head]; + + if (this.head == this.tail) { + throw new IllegalStateException(); + } + + ++this.head; + if (this.head == this.values.length) { + this.head = 0; + } + + return ret; + } + + public void addLast(final long value) { + this.values[this.tail++] = value; + + if (this.tail == this.head) { + throw new IllegalStateException(); + } + + if (this.tail == this.values.length) { + this.tail = 0; + } + } + } + + private static CustomLongArray[] makeQ1BFS(final int radius) { + final CustomLongArray[] ret = new CustomLongArray[2 * radius + 1]; + final BasicFIFOLQueue queue = new BasicFIFOLQueue(Math.max(1, 4 * radius) + 1); + final LongOpenHashSet seen = new LongOpenHashSet((radius + 1) * (radius + 1)); + + seen.add(CoordinateUtils.getChunkKey(0, 0)); + queue.addLast(CoordinateUtils.getChunkKey(0, 0)); + while (!queue.isEmpty()) { + final long chunk = queue.removeFirst(); + final int chunkX = CoordinateUtils.getChunkX(chunk); + final int chunkZ = CoordinateUtils.getChunkZ(chunk); + + final int index = Math.abs(chunkX) + Math.abs(chunkZ); + final CustomLongArray list = ret[index]; + if (list != null) { + list.addUnchecked(chunk); + } else { + (ret[index] = new CustomLongArray(getQ1DistanceSize(index, radius))).addUnchecked(chunk); + } + + for (int i = 0; i < 4; ++i) { + // 0 -> -1, 0 + // 1 -> 0, -1 + // 2 -> 1, 0 + // 3 -> 0, 1 + + final int signInv = -(i >>> 1); // 2/3 -> -(1), 0/1 -> -(0) + // note: -n = (~n) + 1 + // (n ^ signInv) - signInv = signInv == 0 ? ((n ^ 0) - 0 = n) : ((n ^ -1) - (-1) = ~n + 1) + + final int axis = i & 1; // 0/2 -> 0, 1/3 -> 1 + final int dx = ((axis - 1) ^ signInv) - signInv; // 0 -> -1, 1 -> 0 + final int dz = (-axis ^ signInv) - signInv; // 0 -> 0, 1 -> -1 + + final int neighbourX = chunkX + dx; + final int neighbourZ = chunkZ + dz; + final long neighbour = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); + + if ((neighbourX | neighbourZ) < 0 || Math.max(Math.abs(neighbourX), Math.abs(neighbourZ)) > radius) { + // don't enqueue out of range + continue; + } + + if (!seen.add(neighbour)) { + continue; + } + + queue.addLast(neighbour); + } + } + + return ret; + } + + // doesn't appear worth optimising this function now, even though it's 70% of the call + private static CustomLongArray spread(final CustomLongArray input, final int size) { + final LongLinkedOpenHashSet notAdded = new LongLinkedOpenHashSet(input); + final CustomLongArray added = new CustomLongArray(size); + + while (!notAdded.isEmpty()) { + if (added.isEmpty()) { + added.addUnchecked(notAdded.removeLastLong()); + continue; + } + + long maxChunk = -1L; + int maxDist = 0; + + // select the chunk from the not yet added set that has the largest minimum distance from + // the current set of added chunks + + for (final LongIterator iterator = notAdded.iterator(); iterator.hasNext();) { + final long chunkKey = iterator.nextLong(); + final int chunkX = CoordinateUtils.getChunkX(chunkKey); + final int chunkZ = CoordinateUtils.getChunkZ(chunkKey); + + int minDist = Integer.MAX_VALUE; + + final int len = added.size(); + final long[] addedArr = added.elements(); + Objects.checkFromToIndex(0, len, addedArr.length); + for (int i = 0; i < len; ++i) { + final long addedKey = addedArr[i]; + final int addedX = CoordinateUtils.getChunkX(addedKey); + final int addedZ = CoordinateUtils.getChunkZ(addedKey); + + // here we use square distance because chunk generation uses neighbours in a square radius + final int dist = Math.max(Math.abs(addedX - chunkX), Math.abs(addedZ - chunkZ)); + + minDist = Math.min(dist, minDist); + } + + if (minDist > maxDist) { + maxDist = minDist; + maxChunk = chunkKey; + } + } + + // move the selected chunk from the not added set to the added set + + if (!notAdded.remove(maxChunk)) { + throw new IllegalStateException(); + } + + added.addUnchecked(maxChunk); + } + + return added; + } + + private static void expandQuadrants(final CustomLongArray input, final int size) { + final int len = input.size(); + final long[] array = input.elements(); + + int writeIndex = size - 1; + for (int i = len - 1; i >= 0; --i) { + final long key = array[i]; + final int chunkX = CoordinateUtils.getChunkX(key); + final int chunkZ = CoordinateUtils.getChunkZ(key); + + if ((chunkX | chunkZ) < 0 || (i != 0 && chunkX == 0 && chunkZ == 0)) { + throw new IllegalStateException(); + } + + // Q4 + if (chunkZ != 0) { + array[writeIndex--] = CoordinateUtils.getChunkKey(chunkX, -chunkZ); + } + // Q3 + if (chunkX != 0 && chunkZ != 0) { + array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, -chunkZ); + } + // Q2 + if (chunkX != 0) { + array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, chunkZ); + } + + array[writeIndex--] = key; + } + + input.forceSize(size); + + if (writeIndex != -1) { + throw new IllegalStateException(); + } + } + + private static long[] generateBFSOrder(final int radius) { + // by using only the first quadrant, we can reduce the total element size by 4 when spreading + final CustomLongArray[] byDistance = makeQ1BFS(radius); + + // to increase generation parallelism, we want to space the chunks out so that they are not nearby when generating + // this also means we are minimising locality + // but, we need to maintain sorted order by manhatten distance + + // per manhatten distance we transform the chunk list so that each element is maximally spaced out from each other + for (int i = 0, len = byDistance.length; i < len; ++i) { + final CustomLongArray points = byDistance[i]; + final int expectedSize = getDistanceSize(i, radius); + + final CustomLongArray spread = spread(points, expectedSize); + // add in Q2, Q3, Q4 + expandQuadrants(spread, expectedSize); + + byDistance[i] = spread; + } + + // now, rebuild the list so that it still maintains manhatten distance order + final CustomLongArray ret = new CustomLongArray((2 * radius + 1) * (2 * radius + 1)); + + for (final CustomLongArray dist : byDistance) { + ret.addAll(dist); + } + + return ret.elements(); + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java b/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java new file mode 100644 index 0000000000000000000000000000000000000000..7ef3dcca89ed7578c6c0f5565131889110063056 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java @@ -0,0 +1,37 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.util.stream; + +import java.io.DataInputStream; +import java.io.FilterInputStream; +import java.io.InputStream; +import java.lang.reflect.Field; + +/** + * Used to mark chunk data streams that are on external files + */ +public class ExternalChunkStreamMarker extends DataInputStream { + + private static final Field IN_FIELD; + static { + Field field; + try { + field = FilterInputStream.class.getDeclaredField("in"); + field.setAccessible(true); + } catch (final Throwable throwable) { + field = null; + } + + IN_FIELD = field; + } + + private static InputStream getWrapped(final FilterInputStream in) { + try { + return (InputStream)IN_FIELD.get(in); + } catch (final Throwable throwable) { + return in; + } + } + + public ExternalChunkStreamMarker(final DataInputStream in) { + super(getWrapped(in)); + } +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java new file mode 100644 index 0000000000000000000000000000000000000000..ea6b6ed27b212719feb31610faac974899688839 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java @@ -0,0 +1,12 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.world; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import java.util.List; +import java.util.function.Predicate; + +public interface ChunkSystemEntityGetter { + + public List moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate predicate); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java new file mode 100644 index 0000000000000000000000000000000000000000..4b9e2fa963c14f65f15407c1814c543c2999ea32 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java @@ -0,0 +1,11 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.world; + +import net.minecraft.world.level.chunk.LevelChunk; + +public interface ChunkSystemServerChunkCache { + + public void moonrise$setFullChunk(final int chunkX, final int chunkZ, final LevelChunk chunk); + + public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..e97e7d276faf055c89207385d3820debffb06463 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java @@ -0,0 +1,7 @@ +package ca.spottedleaf.moonrise.patches.chunk_tick_iteration; + +public final class ChunkTickConstants { + + public static final int PLAYER_SPAWN_TRACK_RANGE = 8; + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java new file mode 100644 index 0000000000000000000000000000000000000000..f28fd0e01e2bdda0daf9d775e514a7253d32d8d0 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java @@ -0,0 +1,16 @@ +package ca.spottedleaf.moonrise.patches.chunk_tick_iteration; + +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ServerPlayer; + +public interface ChunkTickDistanceManager { + + public void moonrise$addPlayer(final ServerPlayer player, final SectionPos pos); + + public void moonrise$removePlayer(final ServerPlayer player, final SectionPos pos); + + public void moonrise$updatePlayer(final ServerPlayer player, + final SectionPos oldPos, final SectionPos newPos, + final boolean oldIgnore, final boolean newIgnore); + +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java new file mode 100644 index 0000000000000000000000000000000000000000..6af03fd7807d4c71dbf85028d18dc850978ef429 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java @@ -0,0 +1,19 @@ +package ca.spottedleaf.moonrise.patches.chunk_tick_iteration; + +import ca.spottedleaf.moonrise.common.list.ReferenceList; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.world.level.chunk.LevelChunk; + +public interface ChunkTickServerLevel { + + public ReferenceList moonrise$getPlayerTickingChunks(); + + public void moonrise$markChunkForPlayerTicking(final LevelChunk chunk); + + public void moonrise$removeChunkForPlayerTicking(final LevelChunk chunk); + + public void moonrise$addPlayerTickingRequest(final int chunkX, final int chunkZ); + + public void moonrise$removePlayerTickingRequest(final int chunkX, final int chunkZ); + +} diff --git a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..e04bd54744335fb5398c6e4f7ce8b981f35bfb7d --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java @@ -0,0 +1,2183 @@ +package ca.spottedleaf.moonrise.patches.collisions; + +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter; +import ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState; +import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity; +import ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData; +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape; +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; +import ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection; +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.vehicle.AbstractMinecart; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.CollisionGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkSource; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.ArrayVoxelShape; +import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape; +import net.minecraft.world.phys.shapes.BooleanOp; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.DiscreteVoxelShape; +import net.minecraft.world.phys.shapes.EntityCollisionContext; +import net.minecraft.world.phys.shapes.OffsetDoubleList; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.SliceShape; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +public final class CollisionUtil { + + public static final double COLLISION_EPSILON = 1.0E-7; + public static final DoubleArrayList ZERO_ONE = DoubleArrayList.wrap(new double[] { 0.0, 1.0 }); + + public static boolean isSpecialCollidingBlock(final net.minecraft.world.level.block.state.BlockBehaviour.BlockStateBase block) { + return block.hasLargeCollisionShape() || block.getBlock() == Blocks.MOVING_PISTON; + } + + public static boolean isEmpty(final AABB aabb) { + return (aabb.maxX - aabb.minX) < COLLISION_EPSILON || (aabb.maxY - aabb.minY) < COLLISION_EPSILON || (aabb.maxZ - aabb.minZ) < COLLISION_EPSILON; + } + + public static boolean isEmpty(final double minX, final double minY, final double minZ, + final double maxX, final double maxY, final double maxZ) { + return (maxX - minX) < COLLISION_EPSILON || (maxY - minY) < COLLISION_EPSILON || (maxZ - minZ) < COLLISION_EPSILON; + } + + public static AABB getBoxForChunk(final int chunkX, final int chunkZ) { + double x = (double)(chunkX << 4); + double z = (double)(chunkZ << 4); + // use a bounding box bigger than the chunk to prevent entities from entering it on move + return new AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON, + x + (16.0 + 3*COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*COLLISION_EPSILON)); + } + + /* + A couple of rules for VoxelShape collisions: + Two shapes only intersect if they are actually more than EPSILON units into each other. This also applies to movement + checks. + If the two shapes strictly collide, then the return value of a collide call will return a value in the opposite + direction of the source move. However, this value will not be greater in magnitude than EPSILON. Collision code + will automatically round it to 0. + */ + + public static boolean voxelShapeIntersect(final double minX1, final double minY1, final double minZ1, final double maxX1, + final double maxY1, final double maxZ1, final double minX2, final double minY2, + final double minZ2, final double maxX2, final double maxY2, final double maxZ2) { + return (minX1 - maxX2) < -COLLISION_EPSILON && (maxX1 - minX2) > COLLISION_EPSILON && + (minY1 - maxY2) < -COLLISION_EPSILON && (maxY1 - minY2) > COLLISION_EPSILON && + (minZ1 - maxZ2) < -COLLISION_EPSILON && (maxZ1 - minZ2) > COLLISION_EPSILON; + } + + public static boolean voxelShapeIntersect(final AABB box, final double minX, final double minY, final double minZ, + final double maxX, final double maxY, final double maxZ) { + return (box.minX - maxX) < -COLLISION_EPSILON && (box.maxX - minX) > COLLISION_EPSILON && + (box.minY - maxY) < -COLLISION_EPSILON && (box.maxY - minY) > COLLISION_EPSILON && + (box.minZ - maxZ) < -COLLISION_EPSILON && (box.maxZ - minZ) > COLLISION_EPSILON; + } + + public static boolean voxelShapeIntersect(final AABB box1, final AABB box2) { + return (box1.minX - box2.maxX) < -COLLISION_EPSILON && (box1.maxX - box2.minX) > COLLISION_EPSILON && + (box1.minY - box2.maxY) < -COLLISION_EPSILON && (box1.maxY - box2.minY) > COLLISION_EPSILON && + (box1.minZ - box2.maxZ) < -COLLISION_EPSILON && (box1.maxZ - box2.minZ) > COLLISION_EPSILON; + } + + // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON + public static double collideX(final AABB target, final AABB source, final double source_move) { + if ((source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON && + (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { + if (source_move >= 0.0) { + final double max_move = target.minX - source.maxX; // < 0.0 if no strict collision + if (max_move < -COLLISION_EPSILON) { + return source_move; + } + return Math.min(max_move, source_move); + } else { + final double max_move = target.maxX - source.minX; // > 0.0 if no strict collision + if (max_move > COLLISION_EPSILON) { + return source_move; + } + return Math.max(max_move, source_move); + } + } + return source_move; + } + + // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON + public static double collideY(final AABB target, final AABB source, final double source_move) { + if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && + (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { + if (source_move >= 0.0) { + final double max_move = target.minY - source.maxY; // < 0.0 if no strict collision + if (max_move < -COLLISION_EPSILON) { + return source_move; + } + return Math.min(max_move, source_move); + } else { + final double max_move = target.maxY - source.minY; // > 0.0 if no strict collision + if (max_move > COLLISION_EPSILON) { + return source_move; + } + return Math.max(max_move, source_move); + } + } + return source_move; + } + + // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON + public static double collideZ(final AABB target, final AABB source, final double source_move) { + if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && + (source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON) { + if (source_move >= 0.0) { + final double max_move = target.minZ - source.maxZ; // < 0.0 if no strict collision + if (max_move < -COLLISION_EPSILON) { + return source_move; + } + return Math.min(max_move, source_move); + } else { + final double max_move = target.maxZ - source.minZ; // > 0.0 if no strict collision + if (max_move > COLLISION_EPSILON) { + return source_move; + } + return Math.max(max_move, source_move); + } + } + return source_move; + } + + // startIndex and endIndex inclusive + // assumes indices are in range of array + public static int findFloor(final double[] values, final double offset, final double value, int startIndex, int endIndex) { + Objects.checkFromToIndex(startIndex, endIndex + 1, values.length); + do { + final int middle = (startIndex + endIndex) >>> 1; + final double middleVal = (values[middle] + offset); + + if (value < middleVal) { + endIndex = middle - 1; + } else { + startIndex = middle + 1; + } + } while (startIndex <= endIndex); + + return startIndex - 1; + } + + private static VoxelShape sliceShapeVanilla(final VoxelShape src, final Direction.Axis axis, + final int index) { + return new SliceShape(src, axis, index); + } + + private static DoubleList offsetList(final double[] src, final double by) { + final DoubleArrayList wrap = DoubleArrayList.wrap(src); + if (by == 0.0) { + return wrap; + } + return new OffsetDoubleList(wrap, by); + } + + private static VoxelShape sliceShapeOptimised(final VoxelShape src, final Direction.Axis axis, + final int index) { + // assume index in range + final double off_x = ((CollisionVoxelShape)src).moonrise$offsetX(); + final double off_y = ((CollisionVoxelShape)src).moonrise$offsetY(); + final double off_z = ((CollisionVoxelShape)src).moonrise$offsetZ(); + + final double[] coords_x = ((CollisionVoxelShape)src).moonrise$rootCoordinatesX(); + final double[] coords_y = ((CollisionVoxelShape)src).moonrise$rootCoordinatesY(); + final double[] coords_z = ((CollisionVoxelShape)src).moonrise$rootCoordinatesZ(); + + final CachedShapeData cached_shape_data = ((CollisionVoxelShape)src).moonrise$getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); + final int size_y = cached_shape_data.sizeY(); + final int size_z = cached_shape_data.sizeZ(); + + final long[] bitset = cached_shape_data.voxelSet(); + + final DoubleList list_x; + final DoubleList list_y; + final DoubleList list_z; + final int shape_sx; + final int shape_ex; + final int shape_sy; + final int shape_ey; + final int shape_sz; + final int shape_ez; + + switch (axis) { + case X: { + // validate index + if (index < 0 || index >= size_x) { + return Shapes.empty(); + } + + // test if input is already "sliced" + if (coords_x.length == 2 && (coords_x[0] + off_x) == 0.0 && (coords_x[1] + off_x) == 1.0) { + return src; + } + + // test if result would be full box + if (coords_y.length == 2 && coords_z.length == 2 && + (coords_y[0] + off_y) == 0.0 && (coords_y[1] + off_y) == 1.0 && + (coords_z[0] + off_z) == 0.0 && (coords_z[1] + off_z) == 1.0) { + // note: size_y == size_z == 1 + final int bitIdx = 0 + 0*size_z + index*(size_z*size_y); + return (bitset[bitIdx >>> 6] & (1L << bitIdx)) == 0L ? Shapes.empty() : Shapes.block(); + } + + list_x = ZERO_ONE; + list_y = offsetList(coords_y, off_y); + list_z = offsetList(coords_z, off_z); + shape_sx = index; + shape_ex = index + 1; + shape_sy = 0; + shape_ey = size_y; + shape_sz = 0; + shape_ez = size_z; + + break; + } + case Y: { + // validate index + if (index < 0 || index >= size_y) { + return Shapes.empty(); + } + + // test if input is already "sliced" + if (coords_y.length == 2 && (coords_y[0] + off_y) == 0.0 && (coords_y[1] + off_y) == 1.0) { + return src; + } + + // test if result would be full box + if (coords_x.length == 2 && coords_z.length == 2 && + (coords_x[0] + off_x) == 0.0 && (coords_x[1] + off_x) == 1.0 && + (coords_z[0] + off_z) == 0.0 && (coords_z[1] + off_z) == 1.0) { + // note: size_x == size_z == 1 + final int bitIdx = 0 + index*size_z + 0*(size_z*size_y); + return (bitset[bitIdx >>> 6] & (1L << bitIdx)) == 0L ? Shapes.empty() : Shapes.block(); + } + + list_x = offsetList(coords_x, off_x); + list_y = ZERO_ONE; + list_z = offsetList(coords_z, off_z); + shape_sx = 0; + shape_ex = size_x; + shape_sy = index; + shape_ey = index + 1; + shape_sz = 0; + shape_ez = size_z; + + break; + } + case Z: { + // validate index + if (index < 0 || index >= size_z) { + return Shapes.empty(); + } + + // test if input is already "sliced" + if (coords_z.length == 2 && (coords_z[0] + off_z) == 0.0 && (coords_z[1] + off_z) == 1.0) { + return src; + } + + // test if result would be full box + if (coords_x.length == 2 && coords_y.length == 2 && + (coords_x[0] + off_x) == 0.0 && (coords_x[1] + off_x) == 1.0 && + (coords_y[0] + off_y) == 0.0 && (coords_y[1] + off_y) == 1.0) { + // note: size_x == size_y == 1 + final int bitIdx = index + 0*size_z + 0*(size_z*size_y); + return (bitset[bitIdx >>> 6] & (1L << bitIdx)) == 0L ? Shapes.empty() : Shapes.block(); + } + + list_x = offsetList(coords_x, off_x); + list_y = offsetList(coords_y, off_y); + list_z = ZERO_ONE; + shape_sx = 0; + shape_ex = size_x; + shape_sy = 0; + shape_ey = size_y; + shape_sz = index; + shape_ez = index + 1; + + break; + } + default: { + throw new IllegalStateException("Unknown axis: " + axis); + } + } + + final int local_len_x = shape_ex - shape_sx; + final int local_len_y = shape_ey - shape_sy; + final int local_len_z = shape_ez - shape_sz; + + final BitSetDiscreteVoxelShape shape = new BitSetDiscreteVoxelShape(local_len_x, local_len_y, local_len_z); + + final int bitset_mul_x = size_z*size_y; + final int idx_off = shape_sz + shape_sy*size_z + shape_sx*bitset_mul_x; + final int shape_mul_x = local_len_y*local_len_z; + for (int x = 0; x < local_len_x; ++x) { + boolean setX = false; + for (int y = 0; y < local_len_y; ++y) { + boolean setY = false; + for (int z = 0; z < local_len_z; ++z) { + final int unslicedIdx = idx_off + z + y*size_z + x*bitset_mul_x; + if ((bitset[unslicedIdx >>> 6] & (1L << unslicedIdx)) == 0L) { + continue; + } + + setY = true; + setX = true; + shape.zMin = Math.min(shape.zMin, z); + shape.zMax = Math.max(shape.zMax, z + 1); + + shape.storage.set( + z + y*local_len_z + x*shape_mul_x + ); + } + + if (setY) { + shape.yMin = Math.min(shape.yMin, y); + shape.yMax = Math.max(shape.yMax, y + 1); + } + } + if (setX) { + shape.xMin = Math.min(shape.xMin, x); + shape.xMax = Math.max(shape.xMax, x + 1); + } + } + + return shape.isEmpty() ? Shapes.empty() : new ArrayVoxelShape( + shape, list_x, list_y, list_z + ); + } + + private static final boolean DEBUG_SLICE_SHAPE = false; + + public static VoxelShape sliceShape(final VoxelShape src, final Direction.Axis axis, + final int index) { + final VoxelShape ret = sliceShapeOptimised(src, axis, index); + if (DEBUG_SLICE_SHAPE) { + final VoxelShape vanilla = sliceShapeVanilla(src, axis, index); + if (!equals(ret, vanilla)) { + // special case: SliceShape is not empty when it should be! + if (areAnyFull(ret.shape) || areAnyFull(vanilla.shape)) { + equals(ret, vanilla); + sliceShapeOptimised(src, axis, index); + throw new IllegalStateException("Slice shape mismatch"); + } + } + } + + return ret; + } + + public static boolean voxelShapeIntersectNoEmpty(final VoxelShape voxel, final AABB aabb) { + if (voxel.isEmpty()) { + return false; + } + + // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true + + // offsets that should be applied to coords + final double off_x = ((CollisionVoxelShape)voxel).moonrise$offsetX(); + final double off_y = ((CollisionVoxelShape)voxel).moonrise$offsetY(); + final double off_z = ((CollisionVoxelShape)voxel).moonrise$offsetZ(); + + final double[] coords_x = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesX(); + final double[] coords_y = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesY(); + final double[] coords_z = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ(); + + final CachedShapeData cached_shape_data = ((CollisionVoxelShape)voxel).moonrise$getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); + final int size_y = cached_shape_data.sizeY(); + final int size_z = cached_shape_data.sizeZ(); + + // note: voxel bitset with set index (x, y, z) indicates that + // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) + // is collidable. this is the fundamental principle of operation for the voxel collision operation + + // note: for intersection, one we find the floor of the min we can use that as the start index + // for the next check as source max >= source min + // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, + // as this implies that coords[coords.length - 1] < source min + // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max + + final int floor_min_x = Math.max( + 0, + findFloor(coords_x, off_x, aabb.minX + COLLISION_EPSILON, 0, size_x) + ); + if (floor_min_x >= size_x) { + // cannot intersect + return false; + } + + final int ceil_max_x = Math.min( + size_x, + findFloor(coords_x, off_x, aabb.maxX - COLLISION_EPSILON, floor_min_x, size_x) + 1 + ); + if (floor_min_x >= ceil_max_x) { + // cannot intersect + return false; + } + + final int floor_min_y = Math.max( + 0, + findFloor(coords_y, off_y, aabb.minY + COLLISION_EPSILON, 0, size_y) + ); + if (floor_min_y >= size_y) { + // cannot intersect + return false; + } + + final int ceil_max_y = Math.min( + size_y, + findFloor(coords_y, off_y, aabb.maxY - COLLISION_EPSILON, floor_min_y, size_y) + 1 + ); + if (floor_min_y >= ceil_max_y) { + // cannot intersect + return false; + } + + final int floor_min_z = Math.max( + 0, + findFloor(coords_z, off_z, aabb.minZ + COLLISION_EPSILON, 0, size_z) + ); + if (floor_min_z >= size_z) { + // cannot intersect + return false; + } + + final int ceil_max_z = Math.min( + size_z, + findFloor(coords_z, off_z, aabb.maxZ - COLLISION_EPSILON, floor_min_z, size_z) + 1 + ); + if (floor_min_z >= ceil_max_z) { + // cannot intersect + return false; + } + + final long[] bitset = cached_shape_data.voxelSet(); + + // check bitset to check if any shapes in range are full + + final int mul_x = size_y*size_z; + for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { + for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { + for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { + final int index = curr_z + curr_y*size_z + curr_x*mul_x; + // note: JLS states long shift operators ANDS shift by 63 + if ((bitset[index >>> 6] & (1L << index)) != 0L) { + return true; + } + } + } + } + + return false; + } + + // assume !target.isEmpty() && abs(source_move) >= COLLISION_EPSILON + public static double collideX(final VoxelShape target, final AABB source, final double source_move) { + final AABB single_aabb = ((CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation(); + if (single_aabb != null) { + return collideX(single_aabb, source, source_move); + } + // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true + + // offsets that should be applied to coords + final double off_x = ((CollisionVoxelShape)target).moonrise$offsetX(); + final double off_y = ((CollisionVoxelShape)target).moonrise$offsetY(); + final double off_z = ((CollisionVoxelShape)target).moonrise$offsetZ(); + + final double[] coords_x = ((CollisionVoxelShape)target).moonrise$rootCoordinatesX(); + final double[] coords_y = ((CollisionVoxelShape)target).moonrise$rootCoordinatesY(); + final double[] coords_z = ((CollisionVoxelShape)target).moonrise$rootCoordinatesZ(); + + final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).moonrise$getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); + final int size_y = cached_shape_data.sizeY(); + final int size_z = cached_shape_data.sizeZ(); + + // note: voxel bitset with set index (x, y, z) indicates that + // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) + // is collidable. this is the fundamental principle of operation for the voxel collision operation + + + // note: for intersection, one we find the floor of the min we can use that as the start index + // for the next check as source max >= source min + // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, + // as this implies that coords[coords.length - 1] < source min + // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max + + final int floor_min_y = Math.max( + 0, + findFloor(coords_y, off_y, source.minY + COLLISION_EPSILON, 0, size_y) + ); + if (floor_min_y >= size_y) { + // cannot intersect + return source_move; + } + + final int ceil_max_y = Math.min( + size_y, + findFloor(coords_y, off_y, source.maxY - COLLISION_EPSILON, floor_min_y, size_y) + 1 + ); + if (floor_min_y >= ceil_max_y) { + // cannot intersect + return source_move; + } + + final int floor_min_z = Math.max( + 0, + findFloor(coords_z, off_z, source.minZ + COLLISION_EPSILON, 0, size_z) + ); + if (floor_min_z >= size_z) { + // cannot intersect + return source_move; + } + + final int ceil_max_z = Math.min( + size_z, + findFloor(coords_z, off_z, source.maxZ - COLLISION_EPSILON, floor_min_z, size_z) + 1 + ); + if (floor_min_z >= ceil_max_z) { + // cannot intersect + return source_move; + } + + // index = z + y*size_z + x*(size_z*size_y) + + final long[] bitset = cached_shape_data.voxelSet(); + + if (source_move > 0.0) { + final double source_max = source.maxX; + final int ceil_max_x = findFloor( + coords_x, off_x, source_max - COLLISION_EPSILON, 0, size_x + ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max + + // note: only the order of the first loop matters + + // note: we cannot collide with the face at index size on the collision axis for forward movement + + final int mul_x = size_y*size_z; + for (int curr_x = ceil_max_x; curr_x < size_x; ++curr_x) { + double max_dist = (coords_x[curr_x] + off_x) - source_max; + if (max_dist >= source_move) { + // if we reach here, then we will never have a case where + // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1] + // thus, we can return immediately + + // this optimization is important since this loop is bounded by size, and _not_ by + // a calculated max index based off of source_move - so it would be possible to check + // the whole intersected shape for collisions when we didn't need to! + return source_move; + } + if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON + max_dist = Math.min(max_dist, source_move); + } + for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { + for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { + final int index = curr_z + curr_y*size_z + curr_x*mul_x; + // note: JLS states long shift operators ANDS shift by 63 + if ((bitset[index >>> 6] & (1L << index)) != 0L) { + return max_dist; + } + } + } + } + + return source_move; + } else { + final double source_min = source.minX; + final int floor_min_x = findFloor( + coords_x, off_x, source_min + COLLISION_EPSILON, 0, size_x + ); + + // note: only the order of the first loop matters + + // note: we cannot collide with the face at index 0 on the collision axis for backwards movement + + // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the + // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1] + // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid + final int mul_x = size_y*size_z; + for (int curr_x = floor_min_x - 1; curr_x >= 0; --curr_x) { + double max_dist = (coords_x[curr_x + 1] + off_x) - source_min; + if (max_dist <= source_move) { + // if we reach here, then we will never have a case where + // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1] + // thus, we can return immediately + + // this optimization is important since this loop is possibly bounded by size, and _not_ by + // a calculated max index based off of source_move - so it would be possible to check + // the whole intersected shape for collisions when we didn't need to! + return source_move; + } + if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON + max_dist = Math.max(max_dist, source_move); + } + for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { + for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { + final int index = curr_z + curr_y*size_z + curr_x*mul_x; + // note: JLS states long shift operators ANDS shift by 63 + if ((bitset[index >>> 6] & (1L << index)) != 0L) { + return max_dist; + } + } + } + } + + return source_move; + } + } + + public static double collideY(final VoxelShape target, final AABB source, final double source_move) { + final AABB single_aabb = ((CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation(); + if (single_aabb != null) { + return collideY(single_aabb, source, source_move); + } + // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true + + // offsets that should be applied to coords + final double off_x = ((CollisionVoxelShape)target).moonrise$offsetX(); + final double off_y = ((CollisionVoxelShape)target).moonrise$offsetY(); + final double off_z = ((CollisionVoxelShape)target).moonrise$offsetZ(); + + final double[] coords_x = ((CollisionVoxelShape)target).moonrise$rootCoordinatesX(); + final double[] coords_y = ((CollisionVoxelShape)target).moonrise$rootCoordinatesY(); + final double[] coords_z = ((CollisionVoxelShape)target).moonrise$rootCoordinatesZ(); + + final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).moonrise$getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); + final int size_y = cached_shape_data.sizeY(); + final int size_z = cached_shape_data.sizeZ(); + + // note: voxel bitset with set index (x, y, z) indicates that + // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) + // is collidable. this is the fundamental principle of operation for the voxel collision operation + + + // note: for intersection, one we find the floor of the min we can use that as the start index + // for the next check as source max >= source min + // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, + // as this implies that coords[coords.length - 1] < source min + // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max + + final int floor_min_x = Math.max( + 0, + findFloor(coords_x, off_x, source.minX + COLLISION_EPSILON, 0, size_x) + ); + if (floor_min_x >= size_x) { + // cannot intersect + return source_move; + } + + final int ceil_max_x = Math.min( + size_x, + findFloor(coords_x, off_x, source.maxX - COLLISION_EPSILON, floor_min_x, size_x) + 1 + ); + if (floor_min_x >= ceil_max_x) { + // cannot intersect + return source_move; + } + + final int floor_min_z = Math.max( + 0, + findFloor(coords_z, off_z, source.minZ + COLLISION_EPSILON, 0, size_z) + ); + if (floor_min_z >= size_z) { + // cannot intersect + return source_move; + } + + final int ceil_max_z = Math.min( + size_z, + findFloor(coords_z, off_z, source.maxZ - COLLISION_EPSILON, floor_min_z, size_z) + 1 + ); + if (floor_min_z >= ceil_max_z) { + // cannot intersect + return source_move; + } + + // index = z + y*size_z + x*(size_z*size_y) + + final long[] bitset = cached_shape_data.voxelSet(); + + if (source_move > 0.0) { + final double source_max = source.maxY; + final int ceil_max_y = findFloor( + coords_y, off_y, source_max - COLLISION_EPSILON, 0, size_y + ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max + + // note: only the order of the first loop matters + + // note: we cannot collide with the face at index size on the collision axis for forward movement + + final int mul_x = size_y*size_z; + for (int curr_y = ceil_max_y; curr_y < size_y; ++curr_y) { + double max_dist = (coords_y[curr_y] + off_y) - source_max; + if (max_dist >= source_move) { + // if we reach here, then we will never have a case where + // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1] + // thus, we can return immediately + + // this optimization is important since this loop is bounded by size, and _not_ by + // a calculated max index based off of source_move - so it would be possible to check + // the whole intersected shape for collisions when we didn't need to! + return source_move; + } + if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON + max_dist = Math.min(max_dist, source_move); + } + for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { + for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { + final int index = curr_z + curr_y*size_z + curr_x*mul_x; + // note: JLS states long shift operators ANDS shift by 63 + if ((bitset[index >>> 6] & (1L << index)) != 0L) { + return max_dist; + } + } + } + } + + return source_move; + } else { + final double source_min = source.minY; + final int floor_min_y = findFloor( + coords_y, off_y, source_min + COLLISION_EPSILON, 0, size_y + ); + + // note: only the order of the first loop matters + + // note: we cannot collide with the face at index 0 on the collision axis for backwards movement + + // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the + // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1] + // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid + final int mul_x = size_y*size_z; + for (int curr_y = floor_min_y - 1; curr_y >= 0; --curr_y) { + double max_dist = (coords_y[curr_y + 1] + off_y) - source_min; + if (max_dist <= source_move) { + // if we reach here, then we will never have a case where + // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1] + // thus, we can return immediately + + // this optimization is important since this loop is possibly bounded by size, and _not_ by + // a calculated max index based off of source_move - so it would be possible to check + // the whole intersected shape for collisions when we didn't need to! + return source_move; + } + if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON + max_dist = Math.max(max_dist, source_move); + } + for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { + for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { + final int index = curr_z + curr_y*size_z + curr_x*mul_x; + // note: JLS states long shift operators ANDS shift by 63 + if ((bitset[index >>> 6] & (1L << index)) != 0L) { + return max_dist; + } + } + } + } + + return source_move; + } + } + + public static double collideZ(final VoxelShape target, final AABB source, final double source_move) { + final AABB single_aabb = ((CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation(); + if (single_aabb != null) { + return collideZ(single_aabb, source, source_move); + } + // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true + + // offsets that should be applied to coords + final double off_x = ((CollisionVoxelShape)target).moonrise$offsetX(); + final double off_y = ((CollisionVoxelShape)target).moonrise$offsetY(); + final double off_z = ((CollisionVoxelShape)target).moonrise$offsetZ(); + + final double[] coords_x = ((CollisionVoxelShape)target).moonrise$rootCoordinatesX(); + final double[] coords_y = ((CollisionVoxelShape)target).moonrise$rootCoordinatesY(); + final double[] coords_z = ((CollisionVoxelShape)target).moonrise$rootCoordinatesZ(); + + final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).moonrise$getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); + final int size_y = cached_shape_data.sizeY(); + final int size_z = cached_shape_data.sizeZ(); + + // note: voxel bitset with set index (x, y, z) indicates that + // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) + // is collidable. this is the fundamental principle of operation for the voxel collision operation + + + // note: for intersection, one we find the floor of the min we can use that as the start index + // for the next check as source max >= source min + // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, + // as this implies that coords[coords.length - 1] < source min + // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max + + final int floor_min_x = Math.max( + 0, + findFloor(coords_x, off_x, source.minX + COLLISION_EPSILON, 0, size_x) + ); + if (floor_min_x >= size_x) { + // cannot intersect + return source_move; + } + + final int ceil_max_x = Math.min( + size_x, + findFloor(coords_x, off_x, source.maxX - COLLISION_EPSILON, floor_min_x, size_x) + 1 + ); + if (floor_min_x >= ceil_max_x) { + // cannot intersect + return source_move; + } + + final int floor_min_y = Math.max( + 0, + findFloor(coords_y, off_y, source.minY + COLLISION_EPSILON, 0, size_y) + ); + if (floor_min_y >= size_y) { + // cannot intersect + return source_move; + } + + final int ceil_max_y = Math.min( + size_y, + findFloor(coords_y, off_y, source.maxY - COLLISION_EPSILON, floor_min_y, size_y) + 1 + ); + if (floor_min_y >= ceil_max_y) { + // cannot intersect + return source_move; + } + + // index = z + y*size_z + x*(size_z*size_y) + + final long[] bitset = cached_shape_data.voxelSet(); + + if (source_move > 0.0) { + final double source_max = source.maxZ; + final int ceil_max_z = findFloor( + coords_z, off_z, source_max - COLLISION_EPSILON, 0, size_z + ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max + + // note: only the order of the first loop matters + + // note: we cannot collide with the face at index size on the collision axis for forward movement + + final int mul_x = size_y*size_z; + for (int curr_z = ceil_max_z; curr_z < size_z; ++curr_z) { + double max_dist = (coords_z[curr_z] + off_z) - source_max; + if (max_dist >= source_move) { + // if we reach here, then we will never have a case where + // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1] + // thus, we can return immediately + + // this optimization is important since this loop is bounded by size, and _not_ by + // a calculated max index based off of source_move - so it would be possible to check + // the whole intersected shape for collisions when we didn't need to! + return source_move; + } + if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON + max_dist = Math.min(max_dist, source_move); + } + for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { + for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { + final int index = curr_z + curr_y*size_z + curr_x*mul_x; + // note: JLS states long shift operators ANDS shift by 63 + if ((bitset[index >>> 6] & (1L << index)) != 0L) { + return max_dist; + } + } + } + } + + return source_move; + } else { + final double source_min = source.minZ; + final int floor_min_z = findFloor( + coords_z, off_z, source_min + COLLISION_EPSILON, 0, size_z + ); + + // note: only the order of the first loop matters + + // note: we cannot collide with the face at index 0 on the collision axis for backwards movement + + // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the + // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1] + // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid + final int mul_x = size_y*size_z; + for (int curr_z = floor_min_z - 1; curr_z >= 0; --curr_z) { + double max_dist = (coords_z[curr_z + 1] + off_z) - source_min; + if (max_dist <= source_move) { + // if we reach here, then we will never have a case where + // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1] + // thus, we can return immediately + + // this optimization is important since this loop is possibly bounded by size, and _not_ by + // a calculated max index based off of source_move - so it would be possible to check + // the whole intersected shape for collisions when we didn't need to! + return source_move; + } + if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON + max_dist = Math.max(max_dist, source_move); + } + for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { + for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { + final int index = curr_z + curr_y*size_z + curr_x*mul_x; + // note: JLS states long shift operators ANDS shift by 63 + if ((bitset[index >>> 6] & (1L << index)) != 0L) { + return max_dist; + } + } + } + } + + return source_move; + } + } + + // does not use epsilon + public static boolean strictlyContains(final VoxelShape voxel, final Vec3 point) { + return strictlyContains(voxel, point.x, point.y, point.z); + } + + // does not use epsilon + public static boolean strictlyContains(final VoxelShape voxel, final double x, final double y, final double z) { + final AABB single_aabb = ((CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation(); + if (single_aabb != null) { + return single_aabb.contains(x, y, z); + } + + if (voxel.isEmpty()) { + // bitset is clear, no point in searching + return false; + } + + final double off_x = ((CollisionVoxelShape)voxel).moonrise$offsetX(); + final double off_y = ((CollisionVoxelShape)voxel).moonrise$offsetY(); + final double off_z = ((CollisionVoxelShape)voxel).moonrise$offsetZ(); + + final double[] coords_x = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesX(); + final double[] coords_y = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesY(); + final double[] coords_z = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ(); + + final CachedShapeData cached_shape_data = ((CollisionVoxelShape)voxel).moonrise$getCachedVoxelData(); + + // note: size = coords.length - 1 + final int size_x = cached_shape_data.sizeX(); + final int size_y = cached_shape_data.sizeY(); + final int size_z = cached_shape_data.sizeZ(); + + // note: should mirror AABB#contains, which is that for any point X that X >= min and X < max. + // specifically, it cannot collide on the max bounds of the shape + + final int index_x = findFloor(coords_x, off_x, x, 0, size_x); + if (index_x < 0 || index_x >= size_x) { + return false; + } + + final int index_y = findFloor(coords_y, off_y, y, 0, size_y); + if (index_y < 0 || index_y >= size_y) { + return false; + } + + final int index_z = findFloor(coords_z, off_z, z, 0, size_z); + if (index_z < 0 || index_z >= size_z) { + return false; + } + + // index = z + y*size_z + x*(size_z*size_y) + + final int index = index_z + index_y*size_z + index_x*(size_z*size_y); + + final long[] bitset = cached_shape_data.voxelSet(); + + return (bitset[index >>> 6] & (1L << index)) != 0L; + } + + private static int makeBitset(final boolean ft, final boolean tf, final boolean tt) { + // idx ff -> 0 + // idx ft -> 1 + // idx tf -> 2 + // idx tt -> 3 + return ((ft ? 1 : 0) << 1) | ((tf ? 1 : 0) << 2) | ((tt ? 1 : 0) << 3); + } + + private static BitSetDiscreteVoxelShape merge(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond, + final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY, + final MergedVoxelCoordinateList mergedZ, + final int booleanOp) { + final int sizeX = mergedX.voxels; + final int sizeY = mergedY.voxels; + final int sizeZ = mergedZ.voxels; + + final long[] s1Voxels = shapeDataFirst.voxelSet(); + final long[] s2Voxels = shapeDataSecond.voxelSet(); + + final int s1Mul1 = shapeDataFirst.sizeZ(); + final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY(); + + final int s2Mul1 = shapeDataSecond.sizeZ(); + final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY(); + + // note: indices may contain -1, but nothing > size + final BitSetDiscreteVoxelShape ret = new BitSetDiscreteVoxelShape(sizeX, sizeY, sizeZ); + + boolean empty = true; + + int mergedIdx = 0; + for (int idxX = 0; idxX < sizeX; ++idxX) { + final int s1x = mergedX.firstIndices[idxX]; + final int s2x = mergedX.secondIndices[idxX]; + boolean setX = false; + for (int idxY = 0; idxY < sizeY; ++idxY) { + final int s1y = mergedY.firstIndices[idxY]; + final int s2y = mergedY.secondIndices[idxY]; + boolean setY = false; + for (int idxZ = 0; idxZ < sizeZ; ++idxZ) { + final int s1z = mergedZ.firstIndices[idxZ]; + final int s2z = mergedZ.secondIndices[idxZ]; + + int idx1; + int idx2; + + final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx1 = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx1) & 1L); + final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx2 = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx2) & 1L); + + // idx ff -> 0 + // idx ft -> 1 + // idx tf -> 2 + // idx tt -> 3 + + final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0; + setY |= res; + setX |= res; + + if (res) { + empty = false; + // inline and optimize fill operation + ret.zMin = Math.min(ret.zMin, idxZ); + ret.zMax = Math.max(ret.zMax, idxZ + 1); + ret.storage.set(mergedIdx); + } + + ++mergedIdx; + } + if (setY) { + ret.yMin = Math.min(ret.yMin, idxY); + ret.yMax = Math.max(ret.yMax, idxY + 1); + } + } + if (setX) { + ret.xMin = Math.min(ret.xMin, idxX); + ret.xMax = Math.max(ret.xMax, idxX + 1); + } + } + + return empty ? null : ret; + } + + private static boolean isMergeEmpty(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond, + final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY, + final MergedVoxelCoordinateList mergedZ, + final int booleanOp) { + final int sizeX = mergedX.voxels; + final int sizeY = mergedY.voxels; + final int sizeZ = mergedZ.voxels; + + final long[] s1Voxels = shapeDataFirst.voxelSet(); + final long[] s2Voxels = shapeDataSecond.voxelSet(); + + final int s1Mul1 = shapeDataFirst.sizeZ(); + final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY(); + + final int s2Mul1 = shapeDataSecond.sizeZ(); + final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY(); + + // note: indices may contain -1, but nothing > size + for (int idxX = 0; idxX < sizeX; ++idxX) { + final int s1x = mergedX.firstIndices[idxX]; + final int s2x = mergedX.secondIndices[idxX]; + for (int idxY = 0; idxY < sizeY; ++idxY) { + final int s1y = mergedY.firstIndices[idxY]; + final int s2y = mergedY.secondIndices[idxY]; + for (int idxZ = 0; idxZ < sizeZ; ++idxZ) { + final int s1z = mergedZ.firstIndices[idxZ]; + final int s2z = mergedZ.secondIndices[idxZ]; + + int idx1; + int idx2; + + final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx1 = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx1) & 1L); + final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx2 = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx2) & 1L); + + // idx ff -> 0 + // idx ft -> 1 + // idx tf -> 2 + // idx tt -> 3 + + final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0; + + if (res) { + return false; + } + } + } + } + + return true; + } + + public static VoxelShape joinOptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { + return joinUnoptimized(first, second, operator).optimize(); + } + + public static VoxelShape joinUnoptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { + final boolean ff = operator.apply(false, false); + if (ff) { + // technically, should be an infinite box but that's clearly an error + throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true"); + } + + final boolean tt = operator.apply(true, true); + + if (first == second) { + return tt ? first : Shapes.empty(); + } + + final boolean ft = operator.apply(false, true); + final boolean tf = operator.apply(true, false); + + if (first.isEmpty()) { + return ft ? second : Shapes.empty(); + } + if (second.isEmpty()) { + return tf ? first : Shapes.empty(); + } + + if (!tt) { + // try to check for no intersection, since tt = false + final AABB aabbF = ((CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation(); + final AABB aabbS = ((CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation(); + + final boolean intersect; + + final boolean hasAABBF = aabbF != null; + final boolean hasAABBS = aabbS != null; + if (hasAABBF | hasAABBS) { + if (hasAABBF & hasAABBS) { + intersect = voxelShapeIntersect(aabbF, aabbS); + } else if (hasAABBF) { + intersect = voxelShapeIntersectNoEmpty(second, aabbF); + } else { + intersect = voxelShapeIntersectNoEmpty(first, aabbS); + } + } else { + // expect cached bounds + intersect = voxelShapeIntersect(first.bounds(), second.bounds()); + } + + if (!intersect) { + if (!tf & !ft) { + return Shapes.empty(); + } + if (!tf | !ft) { + return tf ? first : second; + } + } + } + + final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge( + ((CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)first).moonrise$offsetX(), + ((CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)second).moonrise$offsetX(), + ft, tf + ); + if (mergedX == null) { + return Shapes.empty(); + } + final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge( + ((CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)first).moonrise$offsetY(), + ((CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)second).moonrise$offsetY(), + ft, tf + ); + if (mergedY == null) { + return Shapes.empty(); + } + final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge( + ((CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)first).moonrise$offsetZ(), + ((CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)second).moonrise$offsetZ(), + ft, tf + ); + if (mergedZ == null) { + return Shapes.empty(); + } + + final CachedShapeData shapeDataFirst = ((CollisionVoxelShape)first).moonrise$getCachedVoxelData(); + final CachedShapeData shapeDataSecond = ((CollisionVoxelShape)second).moonrise$getCachedVoxelData(); + + final BitSetDiscreteVoxelShape mergedShape = merge( + shapeDataFirst, shapeDataSecond, + mergedX, mergedY, mergedZ, + makeBitset(ft, tf, tt) + ); + + if (mergedShape == null) { + return Shapes.empty(); + } + + return new ArrayVoxelShape( + mergedShape, mergedX.wrapCoords(), mergedY.wrapCoords(), mergedZ.wrapCoords() + ); + } + + public static boolean isJoinNonEmpty(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { + final boolean ff = operator.apply(false, false); + if (ff) { + // technically, should be an infinite box but that's clearly an error + throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true"); + } + final boolean firstEmpty = first.isEmpty(); + final boolean secondEmpty = second.isEmpty(); + if (firstEmpty | secondEmpty) { + return operator.apply(!firstEmpty, !secondEmpty); + } + + final boolean tt = operator.apply(true, true); + + if (first == second) { + return tt; + } + + final boolean ft = operator.apply(false, true); + final boolean tf = operator.apply(true, false); + + // try to check intersection + final AABB aabbF = ((CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation(); + final AABB aabbS = ((CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation(); + + final boolean intersect; + + final boolean hasAABBF = aabbF != null; + final boolean hasAABBS = aabbS != null; + if (hasAABBF | hasAABBS) { + if (hasAABBF & hasAABBS) { + intersect = voxelShapeIntersect(aabbF, aabbS); + } else if (hasAABBF) { + intersect = voxelShapeIntersectNoEmpty(second, aabbF); + } else { + // hasAABBS -> true + intersect = voxelShapeIntersectNoEmpty(first, aabbS); + } + + if (!intersect) { + // is only non-empty if we take from first or second, as there is no overlap AND both shapes are non-empty + return tf | ft; + } else if (tt) { + // intersect = true && tt = true -> non-empty merged shape + return true; + } + } else { + // expect cached bounds + intersect = voxelShapeIntersect(first.bounds(), second.bounds()); + if (!intersect) { + // is only non-empty if we take from first or second, as there is no intersection + return tf | ft; + } + } + + final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge( + ((CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)first).moonrise$offsetX(), + ((CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)second).moonrise$offsetX(), + ft, tf + ); + if (mergedX == null) { + return false; + } + final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge( + ((CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)first).moonrise$offsetY(), + ((CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)second).moonrise$offsetY(), + ft, tf + ); + if (mergedY == null) { + return false; + } + final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge( + ((CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)first).moonrise$offsetZ(), + ((CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)second).moonrise$offsetZ(), + ft, tf + ); + if (mergedZ == null) { + return false; + } + + final CachedShapeData shapeDataFirst = ((CollisionVoxelShape)first).moonrise$getCachedVoxelData(); + final CachedShapeData shapeDataSecond = ((CollisionVoxelShape)second).moonrise$getCachedVoxelData(); + + return !isMergeEmpty( + shapeDataFirst, shapeDataSecond, + mergedX, mergedY, mergedZ, + makeBitset(ft, tf, tt) + ); + } + + private static final class MergedVoxelCoordinateList { + + private static final int[][] SIMPLE_INDICES_CACHE = new int[64][]; + static { + for (int i = 0; i < SIMPLE_INDICES_CACHE.length; ++i) { + SIMPLE_INDICES_CACHE[i] = getIndices(i); + } + } + + private static int[] getIndices(final int length) { + final int[] ret = new int[length]; + + for (int i = 1; i < length; ++i) { + ret[i] = i; + } + + return ret; + } + + // indices above voxel size are always set to -1 + public final double[] coordinates; + public final double coordinateOffset; + public final int[] firstIndices; + public final int[] secondIndices; + public final int voxels; + + private MergedVoxelCoordinateList(final double[] coordinates, final double coordinateOffset, + final int[] firstIndices, final int[] secondIndices, final int voxels) { + this.coordinates = coordinates; + this.coordinateOffset = coordinateOffset; + this.firstIndices = firstIndices; + this.secondIndices = secondIndices; + this.voxels = voxels; + } + + public DoubleList wrapCoords() { + if (this.coordinateOffset == 0.0) { + return DoubleArrayList.wrap(this.coordinates, this.voxels + 1); + } + return new OffsetDoubleList(DoubleArrayList.wrap(this.coordinates, this.voxels + 1), this.coordinateOffset); + } + + // assume coordinates.length > 1 + public static MergedVoxelCoordinateList getForSingle(final double[] coordinates, final double offset) { + final int voxels = coordinates.length - 1; + final int[] indices = voxels < SIMPLE_INDICES_CACHE.length ? SIMPLE_INDICES_CACHE[voxels] : getIndices(voxels); + + return new MergedVoxelCoordinateList(coordinates, offset, indices, indices, voxels); + } + + // assume coordinates.length > 1 + public static MergedVoxelCoordinateList merge(final double[] firstCoordinates, final double firstOffset, + final double[] secondCoordinates, final double secondOffset, + final boolean ft, final boolean tf) { + if (firstCoordinates == secondCoordinates && firstOffset == secondOffset) { + return getForSingle(firstCoordinates, firstOffset); + } + + final int firstCount = firstCoordinates.length; + final int secondCount = secondCoordinates.length; + + final int voxelsFirst = firstCount - 1; + final int voxelsSecond = secondCount - 1; + + final int maxCount = firstCount + secondCount; + + final double[] coordinates = new double[maxCount]; + final int[] firstIndices = new int[maxCount]; + final int[] secondIndices = new int[maxCount]; + + final boolean notTF = !tf; + final boolean notFT = !ft; + + int firstIndex = 0; + int secondIndex = 0; + int resultSize = 0; + + // note: operations on NaN are false + double last = Double.NaN; + + for (;;) { + final boolean noneLeftFirst = firstIndex >= firstCount; + final boolean noneLeftSecond = secondIndex >= secondCount; + + if ((noneLeftFirst & noneLeftSecond) | (noneLeftSecond & notTF) | (noneLeftFirst & notFT)) { + break; + } + + final boolean firstZero = firstIndex == 0; + final boolean secondZero = secondIndex == 0; + + final double select; + + if (noneLeftFirst) { + // noneLeftSecond -> false + // notFT -> false + select = secondCoordinates[secondIndex] + secondOffset; + ++secondIndex; + } else if (noneLeftSecond) { + // noneLeftFirst -> false + // notTF -> false + select = firstCoordinates[firstIndex] + firstOffset; + ++firstIndex; + } else { + // noneLeftFirst | noneLeftSecond -> false + // notTF -> ?? + // notFT -> ?? + final boolean breakFirst = notTF & secondZero; + final boolean breakSecond = notFT & firstZero; + + final double first = firstCoordinates[firstIndex] + firstOffset; + final double second = secondCoordinates[secondIndex] + secondOffset; + final boolean useFirst = first < (second + COLLISION_EPSILON); + final boolean cont = (useFirst & breakFirst) | (!useFirst & breakSecond); + + select = useFirst ? first : second; + firstIndex += useFirst ? 1 : 0; + secondIndex += 1 ^ (useFirst ? 1 : 0); + + if (cont) { + continue; + } + } + + int prevFirst = firstIndex - 1; + prevFirst = prevFirst >= voxelsFirst ? -1 : prevFirst; + int prevSecond = secondIndex - 1; + prevSecond = prevSecond >= voxelsSecond ? -1 : prevSecond; + + if (last >= (select - COLLISION_EPSILON)) { + // note: any operations on NaN is false + firstIndices[resultSize - 1] = prevFirst; + secondIndices[resultSize - 1] = prevSecond; + } else { + firstIndices[resultSize] = prevFirst; + secondIndices[resultSize] = prevSecond; + coordinates[resultSize] = select; + + ++resultSize; + last = select; + } + } + + return resultSize <= 1 ? null : new MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1); + } + } + + public static boolean equals(final DiscreteVoxelShape shape1, final DiscreteVoxelShape shape2) { + final CachedShapeData cachedShapeData1 = ((CollisionDiscreteVoxelShape)shape1).moonrise$getOrCreateCachedShapeData(); + final CachedShapeData cachedShapeData2 = ((CollisionDiscreteVoxelShape)shape2).moonrise$getOrCreateCachedShapeData(); + + final boolean isEmpty1 = cachedShapeData1.isEmpty(); + final boolean isEmpty2 = cachedShapeData2.isEmpty(); + + if (isEmpty1 & isEmpty2) { + return true; + } else if (isEmpty1 ^ isEmpty2) { + return false; + } // else: isEmpty1 = isEmpty2 = false + + if (cachedShapeData1.hasSingleAABB() != cachedShapeData2.hasSingleAABB()) { + return false; + } + + if (cachedShapeData1.sizeX() != cachedShapeData2.sizeX()) { + return false; + } + if (cachedShapeData1.sizeY() != cachedShapeData2.sizeY()) { + return false; + } + if (cachedShapeData1.sizeZ() != cachedShapeData2.sizeZ()) { + return false; + } + + return Arrays.equals(cachedShapeData1.voxelSet(), cachedShapeData2.voxelSet()); + } + + // useful only for testing + public static boolean equals(final VoxelShape shape1, final VoxelShape shape2) { + if (shape1.isEmpty() & shape2.isEmpty()) { + return true; + } else if (shape1.isEmpty() ^ shape2.isEmpty()) { + return false; + } + + if (!equals(shape1.shape, shape2.shape)) { + return false; + } + + return shape1.getCoords(Direction.Axis.X).equals(shape2.getCoords(Direction.Axis.X)) && + shape1.getCoords(Direction.Axis.Y).equals(shape2.getCoords(Direction.Axis.Y)) && + shape1.getCoords(Direction.Axis.Z).equals(shape2.getCoords(Direction.Axis.Z)); + } + + public static boolean areAnyFull(final DiscreteVoxelShape shape) { + if (shape.isEmpty()) { + return false; + } + + final int sizeX = shape.getXSize(); + final int sizeY = shape.getYSize(); + final int sizeZ = shape.getZSize(); + + for (int x = 0; x < sizeX; ++x) { + for (int y = 0; y < sizeY; ++y) { + for (int z = 0; z < sizeZ; ++z) { + if (shape.isFull(x, y, z)) { + return true; + } + } + } + } + + return false; + } + + public static String shapeMismatch(final DiscreteVoxelShape shape1, final DiscreteVoxelShape shape2) { + final CachedShapeData cachedShapeData1 = ((CollisionDiscreteVoxelShape)shape1).moonrise$getOrCreateCachedShapeData(); + final CachedShapeData cachedShapeData2 = ((CollisionDiscreteVoxelShape)shape2).moonrise$getOrCreateCachedShapeData(); + + final boolean isEmpty1 = cachedShapeData1.isEmpty(); + final boolean isEmpty2 = cachedShapeData2.isEmpty(); + + if (isEmpty1 & isEmpty2) { + return null; + } else if (isEmpty1 ^ isEmpty2) { + return null; + } // else: isEmpty1 = isEmpty2 = false + + if (cachedShapeData1.sizeX() != cachedShapeData2.sizeX()) { + return "size x: " + cachedShapeData1.sizeX() + " != " + cachedShapeData2.sizeX(); + } + if (cachedShapeData1.sizeY() != cachedShapeData2.sizeY()) { + return "size y: " + cachedShapeData1.sizeY() + " != " + cachedShapeData2.sizeY(); + } + if (cachedShapeData1.sizeZ() != cachedShapeData2.sizeZ()) { + return "size z: " + cachedShapeData1.sizeZ() + " != " + cachedShapeData2.sizeZ(); + } + + final StringBuilder ret = new StringBuilder(); + + final int sizeX = cachedShapeData1.sizeX();; + final int sizeY = cachedShapeData1.sizeY(); + final int sizeZ = cachedShapeData1.sizeZ(); + + boolean first = true; + + for (int x = 0; x < sizeX; ++x) { + for (int y = 0; y < sizeY; ++y) { + for (int z = 0; z < sizeZ; ++z) { + final boolean isFull1 = shape1.isFull(x, y, z); + final boolean isFull2 = shape2.isFull(x, y, z); + + if (isFull1 == isFull2) { + continue; + } + + if (first) { + first = false; + } else { + ret.append(", "); + } + + ret.append("(").append(x).append(",").append(y).append(",").append(z) + .append("): shape1: ").append(isFull1).append(", shape2: ").append(isFull2); + } + } + } + + return ret.isEmpty() ? null : ret.toString(); + } + + public static AABB offsetX(final AABB box, final double dx) { + return new AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); + } + + public static AABB offsetY(final AABB box, final double dy) { + return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ); + } + + public static AABB offsetZ(final AABB box, final double dz) { + return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz); + } + + public static AABB expandRight(final AABB box, final double dx) { // dx > 0.0 + return new AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); + } + + public static AABB expandLeft(final AABB box, final double dx) { // dx < 0.0 + return new AABB(box.minX - dx, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ); + } + + public static AABB expandUpwards(final AABB box, final double dy) { // dy > 0.0 + return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, box.maxZ); + } + + public static AABB expandDownwards(final AABB box, final double dy) { // dy < 0.0 + return new AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, box.maxZ); + } + + public static AABB expandForwards(final AABB box, final double dz) { // dz > 0.0 + return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz); + } + + public static AABB expandBackwards(final AABB box, final double dz) { // dz < 0.0 + return new AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ); + } + + public static AABB cutRight(final AABB box, final double dx) { // dx > 0.0 + return new AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); + } + + public static AABB cutLeft(final AABB box, final double dx) { // dx < 0.0 + return new AABB(box.minX + dx, box.minY, box.minZ, box.minX, box.maxY, box.maxZ); + } + + public static AABB cutUpwards(final AABB box, final double dy) { // dy > 0.0 + return new AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, box.maxZ); + } + + public static AABB cutDownwards(final AABB box, final double dy) { // dy < 0.0 + return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, box.maxZ); + } + + public static AABB cutForwards(final AABB box, final double dz) { // dz > 0.0 + return new AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz); + } + + public static AABB cutBackwards(final AABB box, final double dz) { // dz < 0.0 + return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ); + } + + public static double performAABBCollisionsX(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + final AABB target = potentialCollisions.get(i); + value = collideX(target, currentBoundingBox, value); + } + + return value; + } + + public static double performAABBCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + final AABB target = potentialCollisions.get(i); + value = collideY(target, currentBoundingBox, value); + } + + return value; + } + + public static double performAABBCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + final AABB target = potentialCollisions.get(i); + value = collideZ(target, currentBoundingBox, value); + } + + return value; + } + + public static double performVoxelCollisionsX(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + final VoxelShape target = potentialCollisions.get(i); + value = collideX(target, currentBoundingBox, value); + } + + return value; + } + + public static double performVoxelCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + final VoxelShape target = potentialCollisions.get(i); + value = collideY(target, currentBoundingBox, value); + } + + return value; + } + + public static double performVoxelCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + final VoxelShape target = potentialCollisions.get(i); + value = collideZ(target, currentBoundingBox, value); + } + + return value; + } + + public static Vec3 performVoxelCollisions(final Vec3 moveVector, AABB axisalignedbb, final List potentialCollisions) { + double x = moveVector.x; + double y = moveVector.y; + double z = moveVector.z; + + if (y != 0.0) { + y = performVoxelCollisionsY(axisalignedbb, y, potentialCollisions); + if (y != 0.0) { + axisalignedbb = offsetY(axisalignedbb, y); + } + } + + final boolean xSmaller = Math.abs(x) < Math.abs(z); + + if (xSmaller && z != 0.0) { + z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions); + if (z != 0.0) { + axisalignedbb = offsetZ(axisalignedbb, z); + } + } + + if (x != 0.0) { + x = performVoxelCollisionsX(axisalignedbb, x, potentialCollisions); + if (!xSmaller && x != 0.0) { + axisalignedbb = offsetX(axisalignedbb, x); + } + } + + if (!xSmaller && z != 0.0) { + z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions); + } + + return new Vec3(x, y, z); + } + + public static Vec3 performAABBCollisions(final Vec3 moveVector, AABB axisalignedbb, final List potentialCollisions) { + double x = moveVector.x; + double y = moveVector.y; + double z = moveVector.z; + + if (y != 0.0) { + y = performAABBCollisionsY(axisalignedbb, y, potentialCollisions); + if (y != 0.0) { + axisalignedbb = offsetY(axisalignedbb, y); + } + } + + final boolean xSmaller = Math.abs(x) < Math.abs(z); + + if (xSmaller && z != 0.0) { + z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions); + if (z != 0.0) { + axisalignedbb = offsetZ(axisalignedbb, z); + } + } + + if (x != 0.0) { + x = performAABBCollisionsX(axisalignedbb, x, potentialCollisions); + if (!xSmaller && x != 0.0) { + axisalignedbb = offsetX(axisalignedbb, x); + } + } + + if (!xSmaller && z != 0.0) { + z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions); + } + + return new Vec3(x, y, z); + } + + public static Vec3 performCollisions(final Vec3 moveVector, AABB axisalignedbb, + final List voxels, + final List aabbs) { + if (voxels.isEmpty()) { + // fast track only AABBs + return performAABBCollisions(moveVector, axisalignedbb, aabbs); + } + + double x = moveVector.x; + double y = moveVector.y; + double z = moveVector.z; + + if (y != 0.0) { + y = performAABBCollisionsY(axisalignedbb, y, aabbs); + y = performVoxelCollisionsY(axisalignedbb, y, voxels); + if (y != 0.0) { + axisalignedbb = offsetY(axisalignedbb, y); + } + } + + final boolean xSmaller = Math.abs(x) < Math.abs(z); + + if (xSmaller && z != 0.0) { + z = performAABBCollisionsZ(axisalignedbb, z, aabbs); + z = performVoxelCollisionsZ(axisalignedbb, z, voxels); + if (z != 0.0) { + axisalignedbb = offsetZ(axisalignedbb, z); + } + } + + if (x != 0.0) { + x = performAABBCollisionsX(axisalignedbb, x, aabbs); + x = performVoxelCollisionsX(axisalignedbb, x, voxels); + if (!xSmaller && x != 0.0) { + axisalignedbb = offsetX(axisalignedbb, x); + } + } + + if (!xSmaller && z != 0.0) { + z = performAABBCollisionsZ(axisalignedbb, z, aabbs); + z = performVoxelCollisionsZ(axisalignedbb, z, voxels); + } + + return new Vec3(x, y, z); + } + + public static boolean isCollidingWithBorder(final WorldBorder worldborder, final AABB boundingBox) { + return isCollidingWithBorder(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); + } + + public static boolean isCollidingWithBorder(final WorldBorder worldborder, + final double boxMinX, final double boxMaxX, + final double boxMinZ, final double boxMaxZ) { + final double borderMinX = Math.floor(worldborder.getMinX()); // -X + final double borderMaxX = Math.ceil(worldborder.getMaxX()); // +X + + final double borderMinZ = Math.floor(worldborder.getMinZ()); // -Z + final double borderMaxZ = Math.ceil(worldborder.getMaxZ()); // +Z + + // inverted check for world border enclosing the specified box expanded by -EPSILON + return (borderMinX - boxMinX) > CollisionUtil.COLLISION_EPSILON || (borderMaxX - boxMaxX) < -CollisionUtil.COLLISION_EPSILON || + (borderMinZ - boxMinZ) > CollisionUtil.COLLISION_EPSILON || (borderMaxZ - boxMaxZ) < -CollisionUtil.COLLISION_EPSILON; + } + + /* Math.max/min specify that any NaN argument results in a NaN return, unlike these functions */ + private static double min(final double x, final double y) { + return x < y ? x : y; + } + + private static double max(final double x, final double y) { + return x > y ? x : y; + } + + public static final int COLLISION_FLAG_LOAD_CHUNKS = 1 << 0; + public static final int COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS = 1 << 1; + public static final int COLLISION_FLAG_CHECK_BORDER = 1 << 2; + public static final int COLLISION_FLAG_CHECK_ONLY = 1 << 3; + + public static boolean getCollisionsForBlocksOrWorldBorder(final Level world, final Entity entity, final AABB aabb, + final List intoVoxel, final List intoAABB, + final int collisionFlags, final BiPredicate predicate) { + final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; + boolean ret = false; + + if ((collisionFlags & COLLISION_FLAG_CHECK_BORDER) != 0) { + final WorldBorder worldBorder = world.getWorldBorder(); + if (CollisionUtil.isCollidingWithBorder(worldBorder, aabb) && entity != null && worldBorder.isInsideCloseToBorder(entity, aabb)) { + if (checkOnly) { + return true; + } else { + final VoxelShape borderShape = worldBorder.getCollisionShape(); + intoVoxel.add(borderShape); + ret = true; + } + } + } + + final int minSection = WorldUtil.getMinSection(world); + + final int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1; + final int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1; + + final int minBlockY = Math.max((minSection << 4) - 1, Mth.floor(aabb.minY - COLLISION_EPSILON) - 1); + final int maxBlockY = Math.min((WorldUtil.getMaxSection(world) << 4) + 16, Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1); + + final int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1; + final int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1; + + final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); + final CollisionContext collisionShape = new LazyEntityCollisionContext(entity); + final boolean useEntityCollisionShape = LazyEntityCollisionContext.useEntityCollisionShape(world, entity); + + // special cases: + if (minBlockY > maxBlockY) { + // no point in checking + return ret; + } + + final int minChunkX = minBlockX >> 4; + final int maxChunkX = maxBlockX >> 4; + + final int minChunkY = minBlockY >> 4; + final int maxChunkY = maxBlockY >> 4; + + final int minChunkZ = minBlockZ >> 4; + final int maxChunkZ = maxBlockZ >> 4; + + final boolean loadChunks = (collisionFlags & COLLISION_FLAG_LOAD_CHUNKS) != 0; + final ChunkSource chunkSource = world.getChunkSource(); + + for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { + for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { + final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks); + + if (chunk == null) { + if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) { + if (checkOnly) { + return true; + } else { + intoAABB.add(getBoxForChunk(currChunkX, currChunkZ)); + ret = true; + } + } + continue; + } + + final LevelChunkSection[] sections = chunk.getSections(); + + // bound y + for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { + final int sectionIdx = currChunkY - minSection; + if (sectionIdx < 0 || sectionIdx >= sections.length) { + continue; + } + final LevelChunkSection section = sections[sectionIdx]; + if (section.hasOnlyAir()) { + // empty + continue; + } + + final boolean hasSpecial = ((BlockCountingChunkSection)section).moonrise$hasSpecialCollidingBlocks(); + final int sectionAdjust = !hasSpecial ? 1 : 0; + + final PalettedContainer blocks = section.states; + + final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0; + final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15; + final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) + sectionAdjust : 0; + final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) - sectionAdjust : 15; + final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) + sectionAdjust : 0; + final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) - sectionAdjust : 15; + + for (int currY = minYIterate; currY <= maxYIterate; ++currY) { + final int blockY = currY | (currChunkY << 4); + for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { + final int blockZ = currZ | (currChunkZ << 4); + for (int currX = minXIterate; currX <= maxXIterate; ++currX) { + final int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8); + final int blockX = currX | (currChunkX << 4); + + final int edgeCount = hasSpecial ? ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + + ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + + ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0) : 0; + if (edgeCount == 3) { + continue; + } + + final BlockState blockData = blocks.get(localBlockIndex); + + if (((CollisionBlockState)blockData).moonrise$emptyContextCollisionShape()) { + continue; + } + + VoxelShape blockCollision = ((CollisionBlockState)blockData).moonrise$getConstantContextCollisionShape(); + + if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) { + if (useEntityCollisionShape) { + mutablePos.set(blockX, blockY, blockZ); + blockCollision = collisionShape.getCollisionShape(blockData, world, mutablePos); + } else if (blockCollision == null) { + mutablePos.set(blockX, blockY, blockZ); + blockCollision = blockData.getCollisionShape(world, mutablePos, collisionShape); + } + + AABB singleAABB = ((CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation(); + if (singleAABB != null) { + singleAABB = singleAABB.move((double)blockX, (double)blockY, (double)blockZ); + if (!voxelShapeIntersect(aabb, singleAABB)) { + continue; + } + + if (predicate != null) { + mutablePos.set(blockX, blockY, blockZ); + if (!predicate.test(blockData, mutablePos)) { + continue; + } + } + + if (checkOnly) { + return true; + } else { + ret = true; + intoAABB.add(singleAABB); + continue; + } + } + + if (blockCollision.isEmpty()) { + continue; + } + + final VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ); + + if (!voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) { + continue; + } + + if (predicate != null) { + mutablePos.set(blockX, blockY, blockZ); + if (!predicate.test(blockData, mutablePos)) { + continue; + } + } + + if (checkOnly) { + return true; + } else { + ret = true; + intoVoxel.add(blockCollisionOffset); + continue; + } + } + } + } + } + } + } + } + + return ret; + } + + public static boolean getEntityHardCollisions(final Level world, final Entity entity, AABB aabb, + final List into, final int collisionFlags, final Predicate predicate) { + final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; + + boolean ret = false; + + // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with. + // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems + // specifically with boat collisions. + aabb = aabb.inflate(-COLLISION_EPSILON, -COLLISION_EPSILON, -COLLISION_EPSILON); + final List entities; + if (entity != null && ((ChunkSystemEntity)entity).moonrise$isHardColliding()) { + entities = world.getEntities(entity, aabb, predicate); + } else { + entities = ((ChunkSystemEntityGetter)world).moonrise$getHardCollidingEntities(entity, aabb, predicate); + } + + for (int i = 0, len = entities.size(); i < len; ++i) { + final Entity otherEntity = entities.get(i); + + if (otherEntity.isSpectator()) { + continue; + } + + if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) { + if (checkOnly) { + return true; + } else { + into.add(otherEntity.getBoundingBox()); + ret = true; + } + } + } + + return ret; + } + + public static boolean getCollisions(final Level world, final Entity entity, final AABB aabb, + final List intoVoxel, final List intoAABB, final int collisionFlags, + final BiPredicate blockPredicate, + final Predicate entityPredicate) { + if ((collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0) { + return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate) + || getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate); + } else { + return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate) + | getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate); + } + } + + public static final class LazyEntityCollisionContext extends EntityCollisionContext { + + private CollisionContext delegate; + private boolean delegated; + + public LazyEntityCollisionContext(final Entity entity) { + super(false, 0.0, null, null, entity); + } + + public static boolean useEntityCollisionShape(final Level world, final Entity entity) { + return entity instanceof AbstractMinecart && AbstractMinecart.useExperimentalMovement(world); + } + + public boolean isDelegated() { + final boolean delegated = this.delegated; + this.delegated = false; + return delegated; + } + + public CollisionContext getDelegate() { + this.delegated = true; + final Entity entity = super.getEntity(); + return this.delegate == null ? this.delegate = (entity == null ? CollisionContext.empty() : CollisionContext.of(entity)) : this.delegate; + } + + @Override + public Entity getEntity() { + this.getDelegate(); + return super.getEntity(); + } + + @Override + public boolean isDescending() { + return this.getDelegate().isDescending(); + } + + @Override + public boolean isAbove(final VoxelShape shape, final BlockPos pos, final boolean defaultValue) { + return this.getDelegate().isAbove(shape, pos, defaultValue); + } + + @Override + public boolean isHoldingItem(final Item item) { + return this.getDelegate().isHoldingItem(item); + } + + @Override + public boolean canStandOnFluid(final FluidState state, final FluidState fluidState) { + return this.getDelegate().canStandOnFluid(state, fluidState); + } + + @Override + public VoxelShape getCollisionShape(final BlockState blockState, final CollisionGetter collisionGetter, final BlockPos blockPos) { + return this.getDelegate().getCollisionShape(blockState, collisionGetter, blockPos); + } + } + + private CollisionUtil() { + throw new RuntimeException(); + } +} diff --git a/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java b/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java new file mode 100644 index 0000000000000000000000000000000000000000..35c8aaf0bfa42717f45eed1d1072e1614874de91 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java @@ -0,0 +1,28 @@ +package ca.spottedleaf.moonrise.patches.collisions; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.shapes.VoxelShape; + +public final class ExplosionBlockCache { + + public final long key; + public final BlockPos immutablePos; + public final BlockState blockState; + public final FluidState fluidState; + public final float resistance; + public final boolean outOfWorld; + public Boolean shouldExplode; // null -> not called yet + public VoxelShape cachedCollisionShape; + + public ExplosionBlockCache(final long key, final BlockPos immutablePos, final BlockState blockState, + final FluidState fluidState, final float resistance, final boolean outOfWorld) { + this.key = key; + this.immutablePos = immutablePos; + this.blockState = blockState; + this.fluidState = fluidState; + this.resistance = resistance; + this.outOfWorld = outOfWorld; + } +} diff --git a/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java b/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java new file mode 100644 index 0000000000000000000000000000000000000000..a38ab583200ebf68ca68fdddf2d12077720b72b7 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java @@ -0,0 +1,29 @@ +package ca.spottedleaf.moonrise.patches.collisions.block; + +import net.minecraft.world.phys.shapes.VoxelShape; + +public interface CollisionBlockState { + + // note: this does not consider canOcclude, it is only based on the cached collision shape (i.e hasCache()) + // and whether Shapes.faceShapeOccludes(EMPTY, cached shape) is true + public boolean moonrise$occludesFullBlock(); + + // whether the cached collision shape exists and is empty + public boolean moonrise$emptyCollisionShape(); + + // whether the context-sensitive shape is constant and is empty + public boolean moonrise$emptyContextCollisionShape(); + + // indicates that occludesFullBlock is cached for the collision shape + public boolean moonrise$hasCache(); + + // note: this is HashCommon#murmurHash3(incremental id); and since murmurHash3 has an inverse function the returned + // value is still unique + public int moonrise$uniqueId1(); + + // note: this is HashCommon#murmurHash3(incremental id); and since murmurHash3 has an inverse function the returned + // value is still unique + public int moonrise$uniqueId2(); + + public VoxelShape moonrise$getConstantContextCollisionShape(); +} diff --git a/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java new file mode 100644 index 0000000000000000000000000000000000000000..5a6b16be4b8c0cc92d017bc592bc4818dba17da7 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java @@ -0,0 +1,10 @@ +package ca.spottedleaf.moonrise.patches.collisions.shape; + +public record CachedShapeData( + int sizeX, int sizeY, int sizeZ, + long[] voxelSet, + int minFullX, int minFullY, int minFullZ, + int maxFullX, int maxFullY, int maxFullZ, + boolean isEmpty, boolean hasSingleAABB +) { +} diff --git a/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java new file mode 100644 index 0000000000000000000000000000000000000000..9d33ead3a97d86b371e4d9ad9fed80d789bed844 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java @@ -0,0 +1,39 @@ +package ca.spottedleaf.moonrise.patches.collisions.shape; + +import net.minecraft.world.phys.AABB; +import java.util.ArrayList; +import java.util.List; + +public record CachedToAABBs( + List aabbs, + boolean isOffset, + double offX, double offY, double offZ +) { + + public CachedToAABBs removeOffset() { + final List toOffset = this.aabbs; + final double offX = this.offX; + final double offY = this.offY; + final double offZ = this.offZ; + + final List ret = new ArrayList<>(toOffset.size()); + + for (int i = 0, len = toOffset.size(); i < len; ++i) { + ret.add(toOffset.get(i).move(offX, offY, offZ)); + } + + return new CachedToAABBs(ret, false, 0.0, 0.0, 0.0); + } + + public static CachedToAABBs offset(final CachedToAABBs cache, final double offX, final double offY, final double offZ) { + if (offX == 0.0 && offY == 0.0 && offZ == 0.0) { + return cache; + } + + final double resX = cache.offX + offX; + final double resY = cache.offY + offY; + final double resZ = cache.offZ + offZ; + + return new CachedToAABBs(cache.aabbs, true, resX, resY, resZ); + } +} diff --git a/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java new file mode 100644 index 0000000000000000000000000000000000000000..07fe5e02c2d0a27d2fe37bb45761654dc2d02e5d --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java @@ -0,0 +1,7 @@ +package ca.spottedleaf.moonrise.patches.collisions.shape; + +public interface CollisionDiscreteVoxelShape { + + public CachedShapeData moonrise$getOrCreateCachedShapeData(); + +} diff --git a/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java new file mode 100644 index 0000000000000000000000000000000000000000..05d7b3f9d8659c259f3ed0537c57e6e43eb6e288 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java @@ -0,0 +1,40 @@ +package ca.spottedleaf.moonrise.patches.collisions.shape; + +import net.minecraft.core.Direction; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.VoxelShape; + +public interface CollisionVoxelShape { + + public double moonrise$offsetX(); + + public double moonrise$offsetY(); + + public double moonrise$offsetZ(); + + public double[] moonrise$rootCoordinatesX(); + + public double[] moonrise$rootCoordinatesY(); + + public double[] moonrise$rootCoordinatesZ(); + + public CachedShapeData moonrise$getCachedVoxelData(); + + // rets null if not possible to represent this shape as one AABB + public AABB moonrise$getSingleAABBRepresentation(); + + // ONLY USE INTERNALLY, ONLY FOR INITIALISING IN CONSTRUCTOR: VOXELSHAPES ARE STATIC + public void moonrise$initCache(); + + // this returns empty if not clamped to 1.0 or 0.0 depending on direction + public VoxelShape moonrise$getFaceShapeClamped(final Direction direction); + + public boolean moonrise$isFullBlock(); + + public boolean moonrise$occludesFullBlock(); + + public boolean moonrise$occludesFullBlockIfCached(); + + // uses a cache internally + public VoxelShape moonrise$orUnoptimized(final VoxelShape other); +} diff --git a/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java b/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java new file mode 100644 index 0000000000000000000000000000000000000000..44831fc18efb7534dc6e4822f3c9b5cdc4dcc33e --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java @@ -0,0 +1,10 @@ +package ca.spottedleaf.moonrise.patches.collisions.shape; + +import net.minecraft.world.phys.shapes.VoxelShape; + +public record MergedORCache( + VoxelShape key, + VoxelShape result +) { + +} diff --git a/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java b/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java new file mode 100644 index 0000000000000000000000000000000000000000..f62359e5d6aa9a9cdb015441dbdb6182dc302f02 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.collisions.util; + +public interface CollisionDirection { + + // note: this is HashCommon#murmurHash3(some unique id) and since murmurHash3 has an inverse function the returned + // value is still unique + public int moonrise$uniqueId(); + +} diff --git a/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java b/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java new file mode 100644 index 0000000000000000000000000000000000000000..cf9ffdeff6bf0b62a45f7a44dbfe0dd7d17dc4f4 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java @@ -0,0 +1,7 @@ +package ca.spottedleaf.moonrise.patches.collisions.util; + +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.state.BlockState; + +public record FluidOcclusionCacheKey(BlockState first, BlockState second, Direction direction, boolean result) { +} diff --git a/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..5f5734c00ce8245a1ff69b2d4c3036579d5392e0 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java @@ -0,0 +1,11 @@ +package ca.spottedleaf.moonrise.patches.entity_tracker; + +import net.minecraft.server.level.ChunkMap; + +public interface EntityTrackerEntity { + + public ChunkMap.TrackedEntity moonrise$getTrackedEntity(); + + public void moonrise$setTrackedEntity(final ChunkMap.TrackedEntity trackedEntity); + +} diff --git a/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..8e7472157a98de607c03769a91f64c8369fd3ea6 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java @@ -0,0 +1,15 @@ +package ca.spottedleaf.moonrise.patches.entity_tracker; + +import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; + +public interface EntityTrackerTrackedEntity { + + public void moonrise$tick(final NearbyPlayers.TrackedChunk chunk); + + public void moonrise$removeNonTickThreadPlayers(); + + public void moonrise$clearPlayers(); + + public boolean moonrise$hasPlayers(); + +} diff --git a/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java b/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java new file mode 100644 index 0000000000000000000000000000000000000000..4a7abd239a9c59aa98947e7993962d75e9051902 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.fast_palette; + +public interface FastPalette { + + public default T[] moonrise$getRawPalette(final FastPaletteData src) { + return null; + } + +} diff --git a/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java b/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java new file mode 100644 index 0000000000000000000000000000000000000000..4503f3495846a7d7ed082b9e24636044e4fbccd1 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.fast_palette; + +public interface FastPaletteData { + + public T[] moonrise$getPalette(); + + public void moonrise$setPalette(final T[] palette); + +} diff --git a/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java b/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java new file mode 100644 index 0000000000000000000000000000000000000000..107c97089354edd35f330582f5e0c8a18e792a6e --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java @@ -0,0 +1,5 @@ +package ca.spottedleaf.moonrise.patches.fluid; + +public interface FluidFluidState { + public void moonrise$initCaches(); +} diff --git a/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java b/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java new file mode 100644 index 0000000000000000000000000000000000000000..540c14a6d2c216cd3ef2a9c4056e15712bf8cb8c --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.getblock; + +import net.minecraft.world.level.block.state.BlockState; + +public interface GetBlockChunk { + + public BlockState moonrise$getBlock(final int x, final int y, final int z); + +} diff --git a/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java b/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java new file mode 100644 index 0000000000000000000000000000000000000000..8e6d79b7c10ef25f5478b72c53c555423d615a2f --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java @@ -0,0 +1,7 @@ +package ca.spottedleaf.moonrise.patches.starlight.blockstate; + +public interface StarlightAbstractBlockState { + + public boolean starlight$isConditionallyFullOpaque(); + +} diff --git a/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java b/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java new file mode 100644 index 0000000000000000000000000000000000000000..ed80017c8f257b981d626a37ffc5480d9b326558 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java @@ -0,0 +1,18 @@ +package ca.spottedleaf.moonrise.patches.starlight.chunk; + +import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray; + +public interface StarlightChunk { + + public SWMRNibbleArray[] starlight$getBlockNibbles(); + public void starlight$setBlockNibbles(final SWMRNibbleArray[] nibbles); + + public SWMRNibbleArray[] starlight$getSkyNibbles(); + public void starlight$setSkyNibbles(final SWMRNibbleArray[] nibbles); + + public boolean[] starlight$getSkyEmptinessMap(); + public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap); + + public boolean[] starlight$getBlockEmptinessMap(); + public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap); +} diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java b/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..fa7b784a89626e8528c249d7889a598bd7ee3d49 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java @@ -0,0 +1,280 @@ +package ca.spottedleaf.moonrise.patches.starlight.light; + +import ca.spottedleaf.moonrise.common.PlatformHooks; +import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState; +import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public final class BlockStarLightEngine extends StarLightEngine { + + public BlockStarLightEngine(final Level world) { + super(false, world); + } + + @Override + protected boolean[] getEmptinessMap(final ChunkAccess chunk) { + return ((StarlightChunk)chunk).starlight$getBlockEmptinessMap(); + } + + @Override + protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { + ((StarlightChunk)chunk).starlight$setBlockEmptinessMap(to); + } + + @Override + protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { + return ((StarlightChunk)chunk).starlight$getBlockNibbles(); + } + + @Override + protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { + ((StarlightChunk)chunk).starlight$setBlockNibbles(to); + } + + @Override + protected boolean canUseChunk(final ChunkAccess chunk) { + return chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); + } + + @Override + protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble != null) { + // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically + // because a block was removed - which can decrease light. with sky data, block breaking can only result + // in increases, and thus the existing sky block check will actually correctly propagate light through + // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove + // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running + // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence + // of vanilla data management we "hide" them. + nibble.setHidden(); + } + } + + @Override + protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) { + if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) { + return; + } + + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble == null) { + if (!initRemovedNibbles) { + throw new IllegalStateException(); + } else { + this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray()); + } + } else { + nibble.setNonNull(); + } + } + + @Override + protected final void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) { + // blocks can change opacity + // blocks can change emitted light + // blocks can change direction of propagation + + final int encodeOffset = this.coordinateOffset; + final int emittedMask = this.emittedLightMask; + + final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); + final BlockState blockState = this.getBlockState(worldX, worldY, worldZ); + final int emittedLevel = (PlatformHooks.get().getLightEmission(blockState, lightAccess.getLevel(), this.lightEmissionPos.set(worldX, worldY, worldZ))) & emittedMask; + + this.setLightLevel(worldX, worldY, worldZ, emittedLevel); + // this accounts for change in emitted light that would cause an increase + if (emittedLevel != 0) { + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (emittedLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) + ); + } + // this also accounts for a change in emitted light that would cause a decrease + // this also accounts for the change of direction of propagation (i.e old block was full transparent, new block is full opaque or vice versa) + // as it checks all neighbours (even if current level is 0) + this.appendToDecreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (currentLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + // always keep sided transparent false here, new block might be conditionally transparent which would + // prevent us from decreasing sources in the directions where the new block is opaque + // if it turns out we were wrong to de-propagate the source, the re-propagate logic WILL always + // catch that and fix it. + ); + // re-propagating neighbours (done by the decrease queue) will also account for opacity changes in this block + } + + protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); + + @Override + protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect) { + this.recalcCenterPos.set(worldX, worldY, worldZ); + + final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); + final BlockGetter world = lightAccess.getLevel(); + int level = (PlatformHooks.get().getLightEmission(centerState, world, this.recalcCenterPos)) & this.emittedLightMask; + + if (level >= (15 - 1) || level > expect) { + return level; + } + + final int opacity = Math.max(1, centerState.getLightBlock()); + if (opacity >= 15) { + return level; + } + final BlockState conditionallyOpaqueState; + if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) { + conditionallyOpaqueState = centerState; + } else { + conditionallyOpaqueState = null; + } + + final int sectionOffset = this.chunkSectionIndexOffset; + for (final AxisDirection direction : AXIS_DIRECTIONS) { + final int offX = worldX + direction.x; + final int offY = worldY + direction.y; + final int offZ = worldZ + direction.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + + final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8)); + + if ((neighbourLevel - 1) <= level) { + // don't need to test transparency, we know it wont affect the result. + continue; + } + + final BlockState neighbourState = this.getBlockState(offX, offY, offZ); + if (((StarlightAbstractBlockState)neighbourState).starlight$isConditionallyFullOpaque()) { + // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that + // we don't read the blockstate because most of the time this is false, so using the faster + // known transparency lookup results in a net win + final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(direction.opposite.nms); + final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(direction.nms); + if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { + // not allowed to propagate + continue; + } + } + + // passed transparency, + + final int calculated = neighbourLevel - opacity; + level = Math.max(calculated, level); + if (level > expect) { + return level; + } + } + + return level; + } + + @Override + protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) { + for (final BlockPos pos : positions) { + this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); + } + + this.performLightDecrease(lightAccess); + } + + protected List getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) { + final List sources = new ArrayList<>(); + + final int offX = chunk.getPos().x << 4; + final int offZ = chunk.getPos().z << 4; + + final PlatformHooks platformHooks = PlatformHooks.get(); + + final BlockGetter world = lightAccess.getLevel(); + final LevelChunkSection[] sections = chunk.getSections(); + for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { + final LevelChunkSection section = sections[sectionY - this.minSection]; + if (section.hasOnlyAir()) { + // no sources in empty sections + continue; + } + if (!section.maybeHas(platformHooks.maybeHasLightEmission())) { + // no light sources in palette + continue; + } + final PalettedContainer states = section.states; + final int offY = sectionY << 4; + + final BlockPos.MutableBlockPos mutablePos = this.lightEmissionPos; + for (int index = 0; index < (16 * 16 * 16); ++index) { + final BlockState state = states.get(index); + mutablePos.set(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15)); + + if ((platformHooks.getLightEmission(state, world, mutablePos)) == 0) { + continue; + } + + // index = x | (z << 4) | (y << 8) + sources.add(mutablePos.immutable()); + } + } + + return sources; + } + + @Override + public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { + // setup sources + final BlockGetter world = lightAccess.getLevel(); + final PlatformHooks platformHooks = PlatformHooks.get(); + + final int emittedMask = this.emittedLightMask; + final List positions = this.getSources(lightAccess, chunk); + for (int i = 0, len = positions.size(); i < len; ++i) { + final BlockPos pos = positions.get(i); + final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ()); + final int emittedLight = platformHooks.getLightEmission(blockState, world, pos) & emittedMask; + + if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) { + // some other source is brighter + continue; + } + + this.appendToIncreaseQueue( + ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (emittedLight & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) + ); + + + // propagation wont set this for us + this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight); + } + + if (needsEdgeChecks) { + // not required to propagate here, but this will reduce the hit of the edge checks + this.performLightIncrease(lightAccess); + + // verify neighbour edges + this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); + } else { + this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection); + + this.performLightIncrease(lightAccess); + } + } +} diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java b/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java new file mode 100644 index 0000000000000000000000000000000000000000..4ca68a903e67606fc4ef0bfa9862a73797121c8b --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java @@ -0,0 +1,440 @@ +package ca.spottedleaf.moonrise.patches.starlight.light; + +import net.minecraft.world.level.chunk.DataLayer; +import java.util.ArrayDeque; +import java.util.Arrays; + +// SWMR -> Single Writer Multi Reader Nibble Array +public final class SWMRNibbleArray { + + /* + * Null nibble - nibble does not exist, and should not be written to. Just like vanilla - null + * nibbles are always 0 - and they are never written to directly. Only initialised/uninitialised + * nibbles can be written to. + * + * Uninitialised nibble - They are all 0, but the backing array isn't initialised. + * + * Initialised nibble - Has light data. + */ + + protected static final int INIT_STATE_NULL = 0; // null + protected static final int INIT_STATE_UNINIT = 1; // uninitialised + protected static final int INIT_STATE_INIT = 2; // initialised + protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL + + public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block + // this allows us to maintain only 1 byte array when we're not updating + static final ThreadLocal> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new); + + private static byte[] allocateBytes() { + final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst(); + if (inPool != null) { + return inPool; + } + + return new byte[ARRAY_SIZE]; + } + + private static void freeBytes(final byte[] bytes) { + WORKING_BYTES_POOL.get().addFirst(bytes); + } + + public static SWMRNibbleArray fromVanilla(final DataLayer nibble) { + if (nibble == null) { + return new SWMRNibbleArray(null, true); + } else if (nibble.isEmpty()) { + return new SWMRNibbleArray(); + } else { + return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later + } + } + + protected int stateUpdating; + protected volatile int stateVisible; + + protected byte[] storageUpdating; + protected boolean updatingDirty; // only returns whether storageUpdating is dirty + protected volatile byte[] storageVisible; + + public SWMRNibbleArray() { + this(null, false); // lazy init + } + + public SWMRNibbleArray(final byte[] bytes) { + this(bytes, false); + } + + public SWMRNibbleArray(final byte[] bytes, final boolean isNullNibble) { + if (bytes != null && bytes.length != ARRAY_SIZE) { + throw new IllegalArgumentException("Data of wrong length: " + bytes.length); + } + this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT; + this.storageUpdating = this.storageVisible = bytes; + } + + public SWMRNibbleArray(final byte[] bytes, final int state) { + if (bytes != null && bytes.length != ARRAY_SIZE) { + throw new IllegalArgumentException("Data of wrong length: " + bytes.length); + } + if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) { + throw new IllegalArgumentException("Data cannot be null and have state be initialised"); + } + this.stateUpdating = this.stateVisible = state; + this.storageUpdating = this.storageVisible = bytes; + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("State: "); + switch (this.stateVisible) { + case INIT_STATE_NULL: + stringBuilder.append("null"); + break; + case INIT_STATE_UNINIT: + stringBuilder.append("uninitialised"); + break; + case INIT_STATE_INIT: + stringBuilder.append("initialised"); + break; + case INIT_STATE_HIDDEN: + stringBuilder.append("hidden"); + break; + default: + stringBuilder.append("unknown"); + break; + } + stringBuilder.append("\nData:\n"); + + final byte[] data = this.storageVisible; + if (data != null) { + for (int i = 0; i < 4096; ++i) { + // Copied from NibbleArray#toString + final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF); + + stringBuilder.append(Integer.toHexString(level)); + if ((i & 15) == 15) { + stringBuilder.append("\n"); + } + + if ((i & 255) == 255) { + stringBuilder.append("\n"); + } + } + } else { + stringBuilder.append("null"); + } + + return stringBuilder.toString(); + } + + public SaveState getSaveState() { + synchronized (this) { + final int state = this.stateVisible; + final byte[] data = this.storageVisible; + if (state == INIT_STATE_NULL) { + return null; + } + if (state == INIT_STATE_UNINIT) { + return new SaveState(null, state); + } + final boolean zero = isAllZero(data); + if (zero) { + return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null; + } else { + return new SaveState(data.clone(), state); + } + } + } + + protected static boolean isAllZero(final byte[] data) { + for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) { + byte whole = data[i << 4]; + + for (int k = 1; k < (1 << 4); ++k) { + whole |= data[(i << 4) | k]; + } + + if (whole != 0) { + return false; + } + } + + return true; + } + + // operation type: updating on src, updating on other + public void extrudeLower(final SWMRNibbleArray other) { + if (other.stateUpdating == INIT_STATE_NULL) { + throw new IllegalArgumentException(); + } + + if (other.storageUpdating == null) { + this.setUninitialised(); + return; + } + + final byte[] src = other.storageUpdating; + final byte[] into; + + if (!this.updatingDirty) { + if (this.storageUpdating != null) { + into = this.storageUpdating = allocateBytes(); + } else { + this.storageUpdating = into = allocateBytes(); + this.stateUpdating = INIT_STATE_INIT; + } + this.updatingDirty = true; + } else { + into = this.storageUpdating; + } + + final int start = 0; + final int end = (15 | (15 << 4)) >>> 1; + + /* x | (z << 4) | (y << 8) */ + for (int y = 0; y <= 15; ++y) { + System.arraycopy(src, start, into, y << (8 - 1), end - start + 1); + } + } + + // operation type: updating + public void setFull() { + if (this.stateUpdating != INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + } + Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1); + this.updatingDirty = true; + } + + // operation type: updating + public void setZero() { + if (this.stateUpdating != INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + } + Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0); + this.updatingDirty = true; + } + + // operation type: updating + public void setNonNull() { + if (this.stateUpdating == INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + return; + } + if (this.stateUpdating != INIT_STATE_NULL) { + return; + } + this.stateUpdating = INIT_STATE_UNINIT; + } + + // operation type: updating + public void setNull() { + this.stateUpdating = INIT_STATE_NULL; + if (this.updatingDirty && this.storageUpdating != null) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = null; + this.updatingDirty = false; + } + + // operation type: updating + public void setUninitialised() { + this.stateUpdating = INIT_STATE_UNINIT; + if (this.storageUpdating != null && this.updatingDirty) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = null; + this.updatingDirty = false; + } + + // operation type: updating + public void setHidden() { + if (this.stateUpdating == INIT_STATE_HIDDEN) { + return; + } + if (this.stateUpdating != INIT_STATE_INIT) { + this.setNull(); + } else { + this.stateUpdating = INIT_STATE_HIDDEN; + } + } + + // operation type: updating + public boolean isDirty() { + return this.stateUpdating != this.stateVisible || this.updatingDirty; + } + + // operation type: updating + public boolean isNullNibbleUpdating() { + return this.stateUpdating == INIT_STATE_NULL; + } + + // operation type: visible + public boolean isNullNibbleVisible() { + return this.stateVisible == INIT_STATE_NULL; + } + + // opeartion type: updating + public boolean isUninitialisedUpdating() { + return this.stateUpdating == INIT_STATE_UNINIT; + } + + // operation type: visible + public boolean isUninitialisedVisible() { + return this.stateVisible == INIT_STATE_UNINIT; + } + + // operation type: updating + public boolean isInitialisedUpdating() { + return this.stateUpdating == INIT_STATE_INIT; + } + + // operation type: visible + public boolean isInitialisedVisible() { + return this.stateVisible == INIT_STATE_INIT; + } + + // operation type: updating + public boolean isHiddenUpdating() { + return this.stateUpdating == INIT_STATE_HIDDEN; + } + + // operation type: updating + public boolean isHiddenVisible() { + return this.stateVisible == INIT_STATE_HIDDEN; + } + + // operation type: updating + protected void swapUpdatingAndMarkDirty() { + if (this.updatingDirty) { + return; + } + + if (this.storageUpdating == null) { + this.storageUpdating = allocateBytes(); + Arrays.fill(this.storageUpdating, (byte)0); + } else { + System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE); + } + + if (this.stateUpdating != INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + } + this.updatingDirty = true; + } + + // operation type: updating + public boolean updateVisible() { + if (!this.isDirty()) { + return false; + } + + synchronized (this) { + if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) { + this.storageVisible = null; + } else { + if (this.storageVisible == null) { + this.storageVisible = this.storageUpdating.clone(); + } else { + if (this.storageUpdating != this.storageVisible) { + System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE); + } + } + + if (this.storageUpdating != this.storageVisible) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = this.storageVisible; + } + this.updatingDirty = false; + this.stateVisible = this.stateUpdating; + } + + return true; + } + + // operation type: visible + public DataLayer toVanillaNibble() { + synchronized (this) { + switch (this.stateVisible) { + case INIT_STATE_HIDDEN: + case INIT_STATE_NULL: + return null; + case INIT_STATE_UNINIT: + return new DataLayer(); + case INIT_STATE_INIT: + return new DataLayer(this.storageVisible.clone()); + default: + throw new IllegalStateException(); + } + } + } + + /* x | (z << 4) | (y << 8) */ + + // operation type: updating + public int getUpdating(final int x, final int y, final int z) { + return this.getUpdating((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); + } + + // operation type: updating + public int getUpdating(final int index) { + // indices range from 0 -> 4096 + final byte[] bytes = this.storageUpdating; + if (bytes == null) { + return 0; + } + final byte value = bytes[index >>> 1]; + + // if we are an even index, we want lower 4 bits + // if we are an odd index, we want upper 4 bits + return ((value >>> ((index & 1) << 2)) & 0xF); + } + + // operation type: visible + public int getVisible(final int x, final int y, final int z) { + return this.getVisible((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); + } + + // operation type: visible + public int getVisible(final int index) { + // indices range from 0 -> 4096 + final byte[] visibleBytes = this.storageVisible; + if (visibleBytes == null) { + return 0; + } + final byte value = visibleBytes[index >>> 1]; + + // if we are an even index, we want lower 4 bits + // if we are an odd index, we want upper 4 bits + return ((value >>> ((index & 1) << 2)) & 0xF); + } + + // operation type: updating + public void set(final int x, final int y, final int z, final int value) { + this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value); + } + + // operation type: updating + public void set(final int index, final int value) { + if (!this.updatingDirty) { + this.swapUpdatingAndMarkDirty(); + } + final int shift = (index & 1) << 2; + final int i = index >>> 1; + + this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift)); + } + + public static final class SaveState { + + public final byte[] data; + public final int state; + + public SaveState(final byte[] data, final int state) { + this.data = data; + this.state = state; + } + } +} diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java b/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..f9aef289e9a2d6f63c98c72c56ef32b8793f57f4 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java @@ -0,0 +1,681 @@ +package ca.spottedleaf.moonrise.patches.starlight.light; + +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState; +import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortIterator; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.Arrays; +import java.util.Set; + +public final class SkyStarLightEngine extends StarLightEngine { + + /* + Specification for managing the initialisation and de-initialisation of skylight nibble arrays: + + Skylight nibble initialisation requires that non-empty chunk sections have 1 radius nibbles non-null. + + This presents some problems, as vanilla is only guaranteed to have 0 radius neighbours loaded when editing blocks. + However starlight fixes this so that it has 1 radius loaded. Still, we don't actually have guarantees + that we have the necessary chunks loaded to de-initialise neighbour sections (but we do have enough to de-initialise + our own) - we need a radius of 2 to de-initialise neighbour nibbles. + How do we solve this? + + Each chunk will store the last known "emptiness" of sections for each of their 1 radius neighbour chunk sections. + If the chunk does not have full data, then its nibbles are NOT de-initialised. This is because obviously the + chunk did not go through the light stage yet - or its neighbours are not lit. In either case, once the last + known "emptiness" of neighbouring sections is filled with data, the chunk will run a full check of the data + to see if any of its nibbles need to be de-initialised. + + The emptiness map allows us to de-initialise neighbour nibbles if the neighbour has it filled with data, + and if it doesn't have data then we know it will correctly de-initialise once it fills up. + + Unlike vanilla, we store whether nibbles are uninitialised on disk - so we don't need any dumb hacking + around those. + */ + + protected final int[] heightMapBlockChange = new int[16 * 16]; + { + Arrays.fill(this.heightMapBlockChange, Integer.MIN_VALUE); // clear heightmap + } + + protected final boolean[] nullPropagationCheckCache; + + public SkyStarLightEngine(final Level world) { + super(true, world); + this.nullPropagationCheckCache = new boolean[WorldUtil.getTotalLightSections(world)]; + } + + @Override + protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) { + if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) { + return; + } + SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble == null) { + if (!initRemovedNibbles) { + throw new IllegalStateException(); + } else { + this.setNibbleInCache(chunkX, chunkY, chunkZ, nibble = new SWMRNibbleArray(null, true)); + } + } + this.initNibble(nibble, chunkX, chunkY, chunkZ, extrude); + } + + @Override + protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble != null) { + nibble.setNull(); + } + } + + protected final void initNibble(final SWMRNibbleArray currNibble, final int chunkX, final int chunkY, final int chunkZ, final boolean extrude) { + if (!currNibble.isNullNibbleUpdating()) { + // already initialised + return; + } + + final boolean[] emptinessMap = this.getEmptinessMap(chunkX, chunkZ); + + // are we above this chunk's lowest empty section? + int lowestY = this.minLightSection - 1; + for (int currY = this.maxSection; currY >= this.minSection; --currY) { + if (emptinessMap == null) { + // cannot delay nibble init for lit chunks, as we need to init to propagate into them. + final LevelChunkSection current = this.getChunkSection(chunkX, currY, chunkZ); + if (current == null || current.hasOnlyAir()) { + continue; + } + } else { + if (emptinessMap[currY - this.minSection]) { + continue; + } + } + + // should always be full lit here + lowestY = currY; + break; + } + + if (chunkY > lowestY) { + // we need to set this one to full + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + nibble.setNonNull(); + nibble.setFull(); + return; + } + + if (extrude) { + // this nibble is going to depend solely on the skylight data above it + // find first non-null data above (there does exist one, as we just found it above) + for (int currY = chunkY + 1; currY <= this.maxLightSection; ++currY) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, currY, chunkZ); + if (nibble != null && !nibble.isNullNibbleUpdating()) { + currNibble.setNonNull(); + currNibble.extrudeLower(nibble); + break; + } + } + } else { + currNibble.setNonNull(); + } + } + + protected final void rewriteNibbleCacheForSkylight(final ChunkAccess chunk) { + for (int index = 0, max = this.nibbleCache.length; index < max; ++index) { + final SWMRNibbleArray nibble = this.nibbleCache[index]; + if (nibble != null && nibble.isNullNibbleUpdating()) { + // stop propagation in these areas + this.nibbleCache[index] = null; + nibble.updateVisible(); + } + } + } + + // rets whether neighbours were init'd + + protected final boolean checkNullSection(final int chunkX, final int chunkY, final int chunkZ, + final boolean extrudeInitialised) { + // null chunk sections may have nibble neighbours in the horizontal 1 radius that are + // non-null. Propagation to these neighbours is necessary. + // What makes this easy is we know none of these neighbours are non-empty (otherwise + // this nibble would be initialised). So, we don't have to initialise + // the neighbours in the full 1 radius, because there's no worry that any "paths" + // to the neighbours on this horizontal plane are blocked. + if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.nullPropagationCheckCache[chunkY - this.minLightSection]) { + return false; + } + this.nullPropagationCheckCache[chunkY - this.minLightSection] = true; + + // check horizontal neighbours + boolean needInitNeighbours = false; + neighbour_search: + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, chunkY, dz + chunkZ); + if (nibble != null && !nibble.isNullNibbleUpdating()) { + needInitNeighbours = true; + break neighbour_search; + } + } + } + + if (needInitNeighbours) { + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + this.initNibble(dx + chunkX, chunkY, dz + chunkZ, (dx | dz) == 0 ? extrudeInitialised : true, true); + } + } + } + + return needInitNeighbours; + } + + protected final int getLightLevelExtruded(final int worldX, final int worldY, final int worldZ) { + final int chunkX = worldX >> 4; + int chunkY = worldY >> 4; + final int chunkZ = worldZ >> 4; + + SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble != null) { + return nibble.getUpdating(worldX, worldY, worldZ); + } + + for (;;) { + if (++chunkY > this.maxLightSection) { + return 15; + } + + nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + + if (nibble != null) { + return nibble.getUpdating(worldX, 0, worldZ); + } + } + } + + @Override + protected boolean[] getEmptinessMap(final ChunkAccess chunk) { + return ((StarlightChunk)chunk).starlight$getSkyEmptinessMap(); + } + + @Override + protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { + ((StarlightChunk)chunk).starlight$setSkyEmptinessMap(to); + } + + @Override + protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { + return ((StarlightChunk)chunk).starlight$getSkyNibbles(); + } + + @Override + protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { + ((StarlightChunk)chunk).starlight$setSkyNibbles(to); + } + + @Override + protected boolean canUseChunk(final ChunkAccess chunk) { + // can only use chunks for sky stuff if their sections have been init'd + return chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); + } + + @Override + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, + final int toSection) { + Arrays.fill(this.nullPropagationCheckCache, false); + this.rewriteNibbleCacheForSkylight(chunk); + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + for (int y = toSection; y >= fromSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, true); + } + + super.checkChunkEdges(lightAccess, chunk, fromSection, toSection); + } + + @Override + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) { + Arrays.fill(this.nullPropagationCheckCache, false); + this.rewriteNibbleCacheForSkylight(chunk); + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) { + final int y = (int)iterator.nextShort(); + this.checkNullSection(chunkX, y, chunkZ, true); + } + + super.checkChunkEdges(lightAccess, chunk, sections); + } + + @Override + protected void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) { + // blocks can change opacity + // blocks can change direction of propagation + + // same logic applies from BlockStarLightEngine#checkBlock + + final int encodeOffset = this.coordinateOffset; + + final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); + + if (currentLevel == 15) { + // must re-propagate clobbered source + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (currentLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the block is conditionally transparent + ); + } else { + this.setLightLevel(worldX, worldY, worldZ, 0); + } + + this.appendToDecreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (currentLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + ); + } + + @Override + protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect) { + if (expect == 15) { + return expect; + } + + final int sectionOffset = this.chunkSectionIndexOffset; + final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); + + final BlockState conditionallyOpaqueState; + final int opacity = Math.max(1, centerState.getLightBlock()); + if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) { + conditionallyOpaqueState = centerState; + } else { + conditionallyOpaqueState = null; + } + + int level = 0; + + for (final AxisDirection direction : AXIS_DIRECTIONS) { + final int offX = worldX + direction.x; + final int offY = worldY + direction.y; + final int offZ = worldZ + direction.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + + final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8)); + + if ((neighbourLevel - 1) <= level) { + // don't need to test transparency, we know it wont affect the result. + continue; + } + + final BlockState neighbourState = this.getBlockState(offX, offY, offZ); + + if (((StarlightAbstractBlockState)neighbourState).starlight$isConditionallyFullOpaque()) { + // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that + // we don't read the blockstate because most of the time this is false, so using the faster + // known transparency lookup results in a net win + final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(direction.opposite.nms); + final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(direction.nms); + if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { + // not allowed to propagate + continue; + } + } + + final int calculated = neighbourLevel - opacity; + level = Math.max(calculated, level); + if (level > expect) { + return level; + } + } + + return level; + } + + @Override + protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) { + this.rewriteNibbleCacheForSkylight(atChunk); + Arrays.fill(this.nullPropagationCheckCache, false); + + final BlockGetter world = lightAccess.getLevel(); + final int chunkX = atChunk.getPos().x; + final int chunkZ = atChunk.getPos().z; + final int heightMapOffset = chunkX * -16 + (chunkZ * (-16 * 16)); + + // setup heightmap for changes + for (final BlockPos pos : positions) { + final int index = pos.getX() + (pos.getZ() << 4) + heightMapOffset; + final int curr = this.heightMapBlockChange[index]; + if (pos.getY() > curr) { + this.heightMapBlockChange[index] = pos.getY(); + } + } + + // note: light sets are delayed while processing skylight source changes due to how + // nibbles are initialised, as we want to avoid clobbering nibble values so what when + // below nibbles are initialised they aren't reading from partially modified nibbles + + // now we can recalculate the sources for the changed columns + for (int index = 0; index < (16 * 16); ++index) { + final int maxY = this.heightMapBlockChange[index]; + if (maxY == Integer.MIN_VALUE) { + // not changed + continue; + } + this.heightMapBlockChange[index] = Integer.MIN_VALUE; // restore default for next caller + + final int columnX = (index & 15) | (chunkX << 4); + final int columnZ = (index >>> 4) | (chunkZ << 4); + + // try and propagate from the above y + // delay light set until after processing all sources to setup + final int maxPropagationY = this.tryPropagateSkylight(world, columnX, maxY, columnZ, true, true); + + // maxPropagationY is now the highest block that could not be propagated to + + // remove all sources below that are 15 + final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; + final int encodeOffset = this.coordinateOffset; + + if (this.getLightLevelExtruded(columnX, maxPropagationY, columnZ) == 15) { + // ensure section is checked + this.checkNullSection(columnX >> 4, maxPropagationY >> 4, columnZ >> 4, true); + + for (int currY = maxPropagationY; currY >= (this.minLightSection << 4); --currY) { + if ((currY & 15) == 15) { + // ensure section is checked + this.checkNullSection(columnX >> 4, (currY >> 4), columnZ >> 4, true); + } + + // ensure section below is always checked + final SWMRNibbleArray nibble = this.getNibbleFromCache(columnX >> 4, currY >> 4, columnZ >> 4); + if (nibble == null) { + // advance currY to the the top of the section below + currY = (currY) & (~15); + // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually + // end up there + continue; + } + + if (nibble.getUpdating(columnX, currY, columnZ) != 15) { + break; + } + + // delay light set until after processing all sources to setup + this.appendToDecreaseQueue( + ((columnX + (columnZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) + | (propagateDirection << (6 + 6 + 16 + 4)) + // do not set transparent blocks for the same reason we don't in the checkBlock method + ); + } + } + } + + // delayed light sets are processed here, and must be processed before checkBlock as checkBlock reads + // immediate light value + this.processDelayedIncreases(); + this.processDelayedDecreases(); + + for (final BlockPos pos : positions) { + this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); + } + + this.performLightDecrease(lightAccess); + } + + protected final int[] heightMapGen = new int[32 * 32]; + + @Override + protected void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { + this.rewriteNibbleCacheForSkylight(chunk); + Arrays.fill(this.nullPropagationCheckCache, false); + + final BlockGetter world = lightAccess.getLevel(); + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + final LevelChunkSection[] sections = chunk.getSections(); + + int highestNonEmptySection = this.maxSection; + while (highestNonEmptySection == (this.minSection - 1) || + sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].hasOnlyAir()) { + this.checkNullSection(chunkX, highestNonEmptySection, chunkZ, false); + // try propagate FULL to neighbours + + // check neighbours to see if we need to propagate into them + for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { + final int neighbourX = chunkX + direction.x; + final int neighbourZ = chunkZ + direction.z; + final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(neighbourX, highestNonEmptySection, neighbourZ); + if (neighbourNibble == null) { + // unloaded neighbour + // most of the time we fall here + continue; + } + + // it looks like we need to propagate into the neighbour + + final int incX; + final int incZ; + final int startX; + final int startZ; + + if (direction.x != 0) { + // x direction + incX = 0; + incZ = 1; + + if (direction.x < 0) { + // negative + startX = chunkX << 4; + } else { + startX = chunkX << 4 | 15; + } + startZ = chunkZ << 4; + } else { + // z direction + incX = 1; + incZ = 0; + + if (direction.z < 0) { + // negative + startZ = chunkZ << 4; + } else { + startZ = chunkZ << 4 | 15; + } + startX = chunkX << 4; + } + + final int encodeOffset = this.coordinateOffset; + final long propagateDirection = 1L << direction.ordinal(); // we only want to check in this direction + + for (int currY = highestNonEmptySection << 4, maxY = currY | 15; currY <= maxY; ++currY) { + for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { + this.appendToIncreaseQueue( + ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) // we know we're at full lit here + | (propagateDirection << (6 + 6 + 16 + 4)) + // no transparent flag, we know for a fact there are no blocks here that could be directionally transparent (as the section is EMPTY) + ); + } + } + } + + if (highestNonEmptySection-- == (this.minSection - 1)) { + break; + } + } + + if (highestNonEmptySection >= this.minSection) { + // fill out our other sources + final int minX = chunkPos.x << 4; + final int maxX = chunkPos.x << 4 | 15; + final int minZ = chunkPos.z << 4; + final int maxZ = chunkPos.z << 4 | 15; + final int startY = highestNonEmptySection << 4 | 15; + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + this.tryPropagateSkylight(world, currX, startY + 1, currZ, false, false); + } + } + } // else: apparently the chunk is empty + + if (needsEdgeChecks) { + // not required to propagate here, but this will reduce the hit of the edge checks + this.performLightIncrease(lightAccess); + + for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, false); + } + // no need to rewrite the nibble cache again + super.checkChunkEdges(lightAccess, chunk, this.minLightSection, highestNonEmptySection); + } else { + for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, false); + } + this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, highestNonEmptySection); + + this.performLightIncrease(lightAccess); + } + } + + protected final void processDelayedIncreases() { + // copied from performLightIncrease + final long[] queue = this.increaseQueue; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + + for (int i = 0, len = this.increaseQueueInitialLength; i < len; ++i) { + final long queueValue = queue[i]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); + + this.setLightLevel(posX, posY, posZ, propagatedLightLevel); + } + } + + protected final void processDelayedDecreases() { + // copied from performLightDecrease + final long[] queue = this.decreaseQueue; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + + for (int i = 0, len = this.decreaseQueueInitialLength; i < len; ++i) { + final long queueValue = queue[i]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + + this.setLightLevel(posX, posY, posZ, 0); + } + } + + // delaying the light set is useful for block changes since they need to worry about initialising nibblearrays + // while also queueing light at the same time (initialising nibblearrays might depend on nibbles above, so + // clobbering the light values will result in broken propagation) + protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ, + final boolean extrudeInitialised, final boolean delayLightSet) { + final int encodeOffset = this.coordinateOffset; + final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards. + + if (this.getLightLevelExtruded(worldX, startY + 1, worldZ) != 15) { + return startY; + } + + // ensure this section is always checked + this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised); + + BlockState above = this.getBlockState(worldX, startY + 1, worldZ); + + for (;startY >= (this.minLightSection << 4); --startY) { + if ((startY & 15) == 15) { + // ensure this section is always checked + this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised); + } + final BlockState current = this.getBlockState(worldX, startY, worldZ); + + final VoxelShape fromShape; + if (((StarlightAbstractBlockState)above).starlight$isConditionallyFullOpaque()) { + fromShape = above.getFaceOcclusionShape(AxisDirection.NEGATIVE_Y.nms); + if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { + // above wont let us propagate + break; + } + } else { + fromShape = Shapes.empty(); + } + + // does light propagate from the top down? + long flags = 0L; + if (((StarlightAbstractBlockState)current).starlight$isConditionallyFullOpaque()) { + final VoxelShape cullingFace = current.getFaceOcclusionShape(AxisDirection.POSITIVE_Y.nms); + + if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { + // can't propagate here, we're done on this column. + break; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = current.getLightBlock(); + if (opacity > 0) { + // let the queued value (if any) handle it from here. + break; + } + + // light set delayed until we determine if this nibble section is null + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) // we know we're at full lit here + | (propagateDirection << (6 + 6 + 16 + 4)) + | flags + ); + + above = current; + + if (this.getNibbleFromCache(worldX >> 4, startY >> 4, worldZ >> 4) == null) { + // we skip empty sections here, as this is just an easy way of making sure the above block + // can propagate through air. + + // nothing can propagate in null sections, remove the queue entry for it + --this.increaseQueueInitialLength; + + // advance currY to the the top of the section below + startY = (startY) & (~15); + // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually + // end up there + + // make sure this is marked as AIR + above = AIR_BLOCK_STATE; + } else if (!delayLightSet) { + this.setLightLevel(worldX, startY, worldZ, 15); + } + } + + return startY; + } +} diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..8aeb5fb87f94a35659347a09a638420699b52a6f --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java @@ -0,0 +1,1438 @@ +package ca.spottedleaf.moonrise.patches.starlight.light; + +import ca.spottedleaf.concurrentutil.util.IntegerUtil; +import ca.spottedleaf.moonrise.common.PlatformHooks; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortIterator; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public abstract class StarLightEngine { + + protected static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState(); + + protected static final AxisDirection[] DIRECTIONS = AxisDirection.values(); + protected static final AxisDirection[] AXIS_DIRECTIONS = DIRECTIONS; + protected static final AxisDirection[] ONLY_HORIZONTAL_DIRECTIONS = new AxisDirection[] { + AxisDirection.POSITIVE_X, AxisDirection.NEGATIVE_X, + AxisDirection.POSITIVE_Z, AxisDirection.NEGATIVE_Z + }; + + protected static enum AxisDirection { + + // Declaration order is important and relied upon. Do not change without modifying propagation code. + POSITIVE_X(1, 0, 0, Direction.EAST) , NEGATIVE_X(-1, 0, 0, Direction.WEST), + POSITIVE_Z(0, 0, 1, Direction.SOUTH), NEGATIVE_Z(0, 0, -1, Direction.NORTH), + POSITIVE_Y(0, 1, 0, Direction.UP) , NEGATIVE_Y(0, -1, 0, Direction.DOWN); + + static { + POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X; + POSITIVE_Z.opposite = NEGATIVE_Z; NEGATIVE_Z.opposite = POSITIVE_Z; + POSITIVE_Y.opposite = NEGATIVE_Y; NEGATIVE_Y.opposite = POSITIVE_Y; + } + + protected AxisDirection opposite; + + public final int x; + public final int y; + public final int z; + public final Direction nms; + public final long everythingButThisDirection; + public final long everythingButTheOppositeDirection; + + AxisDirection(final int x, final int y, final int z, final Direction nms) { + this.x = x; + this.y = y; + this.z = z; + this.nms = nms; + this.everythingButThisDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << this.ordinal())); + // positive is always even, negative is always odd. Flip the 1 bit to get the negative direction. + this.everythingButTheOppositeDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << (this.ordinal() ^ 1))); + } + + public AxisDirection getOpposite() { + return this.opposite; + } + } + + // I'd like to thank https://www.seedofandromeda.com/blogs/29-fast-flood-fill-lighting-in-a-blocky-voxel-game-pt-1 + // for explaining how light propagates via breadth-first search + + // While the above is a good start to understanding the general idea of what the general principles are, it's not + // exactly how the vanilla light engine should behave for minecraft. + + // similar to the above, except the chunk section indices vary from [-1, 1], or [0, 2] + // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] + // index = x + (z * 5) + (y * 25) + // null index indicates the chunk section doesn't exist (empty or out of bounds) + protected final LevelChunkSection[] sectionCache; + + // the exact same as above, except for storing fast access to SWMRNibbleArray + // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] + // index = x + (z * 5) + (y * 25) + protected final SWMRNibbleArray[] nibbleCache; + + // the exact same as above, except for storing fast access to nibbles to call change callbacks for + // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] + // index = x + (z * 5) + (y * 25) + protected final boolean[] notifyUpdateCache; + + // always initialsed during start of lighting. + // index = x + (z * 5) + protected final ChunkAccess[] chunkCache = new ChunkAccess[5 * 5]; + + // index = x + (z * 5) + protected final boolean[][] emptinessMapCache = new boolean[5 * 5][]; + + protected final BlockPos.MutableBlockPos lightEmissionPos = new BlockPos.MutableBlockPos(); + + protected int encodeOffsetX; + protected int encodeOffsetY; + protected int encodeOffsetZ; + + protected int coordinateOffset; + + protected int chunkOffsetX; + protected int chunkOffsetY; + protected int chunkOffsetZ; + + protected int chunkIndexOffset; + protected int chunkSectionIndexOffset; + + protected final boolean skylightPropagator; + protected final int emittedLightMask; + protected final boolean isClientSide; + + protected final Level world; + protected final int minLightSection; + protected final int maxLightSection; + protected final int minSection; + protected final int maxSection; + + protected StarLightEngine(final boolean skylightPropagator, final Level world) { + this.skylightPropagator = skylightPropagator; + this.emittedLightMask = skylightPropagator ? 0 : 0xF; + this.isClientSide = world.isClientSide; + this.world = world; + this.minLightSection = WorldUtil.getMinLightSection(world); + this.maxLightSection = WorldUtil.getMaxLightSection(world); + this.minSection = WorldUtil.getMinSection(world); + this.maxSection = WorldUtil.getMaxSection(world); + + this.sectionCache = new LevelChunkSection[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer + this.nibbleCache = new SWMRNibbleArray[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer + this.notifyUpdateCache = new boolean[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer + } + + protected final void setupEncodeOffset(final int centerX, final int centerY, final int centerZ) { + // 31 = center + encodeOffset + this.encodeOffsetX = 31 - centerX; + this.encodeOffsetY = (-(this.minLightSection - 1) << 4); // we want 0 to be the smallest encoded value + this.encodeOffsetZ = 31 - centerZ; + + // coordinateIndex = x | (z << 6) | (y << 12) + this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << 6) + (this.encodeOffsetY << 12); + + // 2 = (centerX >> 4) + chunkOffset + this.chunkOffsetX = 2 - (centerX >> 4); + this.chunkOffsetY = -(this.minLightSection - 1); // lowest should be 0 + this.chunkOffsetZ = 2 - (centerZ >> 4); + + // chunk index = x + (5 * z) + this.chunkIndexOffset = this.chunkOffsetX + (5 * this.chunkOffsetZ); + + // chunk section index = x + (5 * z) + ((5*5) * y) + this.chunkSectionIndexOffset = this.chunkIndexOffset + ((5 * 5) * this.chunkOffsetY); + } + + protected final void setupCaches(final LightChunkGetter chunkProvider, final int centerX, final int centerY, final int centerZ, + final boolean relaxed, final boolean tryToLoadChunksFor2Radius) { + final int centerChunkX = centerX >> 4; + final int centerChunkY = centerY >> 4; + final int centerChunkZ = centerZ >> 4; + + this.setupEncodeOffset(centerChunkX * 16 + 7, centerChunkY * 16 + 7, centerChunkZ * 16 + 7); + + final int radius = tryToLoadChunksFor2Radius ? 2 : 1; + + for (int dz = -radius; dz <= radius; ++dz) { + for (int dx = -radius; dx <= radius; ++dx) { + final int cx = centerChunkX + dx; + final int cz = centerChunkZ + dz; + final boolean isTwoRadius = Math.max(IntegerUtil.branchlessAbs(dx), IntegerUtil.branchlessAbs(dz)) == 2; + final ChunkAccess chunk = (ChunkAccess)chunkProvider.getChunkForLighting(cx, cz); + + if (chunk == null) { + if (relaxed | isTwoRadius) { + continue; + } + throw new IllegalArgumentException("Trying to propagate light update before 1 radius neighbours ready"); + } + + if (!this.canUseChunk(chunk)) { + continue; + } + + this.setChunkInCache(cx, cz, chunk); + this.setEmptinessMapCache(cx, cz, this.getEmptinessMap(chunk)); + if (!isTwoRadius) { + this.setBlocksForChunkInCache(cx, cz, chunk.getSections()); + this.setNibblesForChunkInCache(cx, cz, this.getNibblesOnChunk(chunk)); + } + } + } + } + + protected final ChunkAccess getChunkInCache(final int chunkX, final int chunkZ) { + return this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset]; + } + + protected final void setChunkInCache(final int chunkX, final int chunkZ, final ChunkAccess chunk) { + this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk; + } + + protected final LevelChunkSection getChunkSection(final int chunkX, final int chunkY, final int chunkZ) { + return this.sectionCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset]; + } + + protected final void setChunkSectionInCache(final int chunkX, final int chunkY, final int chunkZ, final LevelChunkSection section) { + this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section; + } + + protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final LevelChunkSection[] sections) { + for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { + this.setChunkSectionInCache(chunkX, cy, chunkZ, + sections == null ? null : (cy >= this.minSection && cy <= this.maxSection ? sections[cy - this.minSection] : null)); + } + } + + protected final SWMRNibbleArray getNibbleFromCache(final int chunkX, final int chunkY, final int chunkZ) { + return this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset]; + } + + protected final SWMRNibbleArray[] getNibblesForChunkFromCache(final int chunkX, final int chunkZ) { + final SWMRNibbleArray[] ret = new SWMRNibbleArray[this.maxLightSection - this.minLightSection + 1]; + + for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { + ret[cy - this.minLightSection] = this.nibbleCache[chunkX + 5*chunkZ + (cy * (5 * 5)) + this.chunkSectionIndexOffset]; + } + + return ret; + } + + protected final void setNibbleInCache(final int chunkX, final int chunkY, final int chunkZ, final SWMRNibbleArray nibble) { + this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset] = nibble; + } + + protected final void setNibblesForChunkInCache(final int chunkX, final int chunkZ, final SWMRNibbleArray[] nibbles) { + for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { + this.setNibbleInCache(chunkX, cy, chunkZ, nibbles == null ? null : nibbles[cy - this.minLightSection]); + } + } + + protected final void updateVisible(final LightChunkGetter lightAccess) { + for (int index = 0, max = this.nibbleCache.length; index < max; ++index) { + final SWMRNibbleArray nibble = this.nibbleCache[index]; + if (!this.notifyUpdateCache[index] && (nibble == null || !nibble.isDirty())) { + continue; + } + + final int chunkX = (index % 5) - this.chunkOffsetX; + final int chunkZ = ((index / 5) % 5) - this.chunkOffsetZ; + final int ySections = this.maxSection - this.minSection + 1; + final int chunkY = ((index / (5*5)) % (ySections + 2 + 2)) - this.chunkOffsetY; + if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) { + lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, chunkY, chunkZ)); + } + } + } + + protected final void destroyCaches() { + Arrays.fill(this.sectionCache, null); + Arrays.fill(this.nibbleCache, null); + Arrays.fill(this.chunkCache, null); + Arrays.fill(this.emptinessMapCache, null); + if (this.isClientSide) { + Arrays.fill(this.notifyUpdateCache, false); + } + } + + protected final BlockState getBlockState(final int worldX, final int worldY, final int worldZ) { + final LevelChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset]; + + if (section != null) { + return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15); + } + + return AIR_BLOCK_STATE; + } + + protected final BlockState getBlockState(final int sectionIndex, final int localIndex) { + final LevelChunkSection section = this.sectionCache[sectionIndex]; + + if (section != null) { + return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.states.get(localIndex); + } + + return AIR_BLOCK_STATE; + } + + protected final int getLightLevel(final int worldX, final int worldY, final int worldZ) { + final SWMRNibbleArray nibble = this.nibbleCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset]; + + return nibble == null ? 0 : nibble.getUpdating((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8)); + } + + protected final int getLightLevel(final int sectionIndex, final int localIndex) { + final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; + + return nibble == null ? 0 : nibble.getUpdating(localIndex); + } + + protected final void setLightLevel(final int worldX, final int worldY, final int worldZ, final int level) { + final int sectionIndex = (worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset; + final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; + + if (nibble != null) { + nibble.set((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8), level); + if (this.isClientSide) { + int cx1 = (worldX - 1) >> 4; + int cx2 = (worldX + 1) >> 4; + int cy1 = (worldY - 1) >> 4; + int cy2 = (worldY + 1) >> 4; + int cz1 = (worldZ - 1) >> 4; + int cz2 = (worldZ + 1) >> 4; + for (int x = cx1; x <= cx2; ++x) { + for (int y = cy1; y <= cy2; ++y) { + for (int z = cz1; z <= cz2; ++z) { + this.notifyUpdateCache[x + 5 * z + (5 * 5) * y + this.chunkSectionIndexOffset] = true; + } + } + } + } + } + } + + protected final void postLightUpdate(final int worldX, final int worldY, final int worldZ) { + if (this.isClientSide) { + int cx1 = (worldX - 1) >> 4; + int cx2 = (worldX + 1) >> 4; + int cy1 = (worldY - 1) >> 4; + int cy2 = (worldY + 1) >> 4; + int cz1 = (worldZ - 1) >> 4; + int cz2 = (worldZ + 1) >> 4; + for (int x = cx1; x <= cx2; ++x) { + for (int y = cy1; y <= cy2; ++y) { + for (int z = cz1; z <= cz2; ++z) { + this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true; + } + } + } + } + } + + protected final void setLightLevel(final int sectionIndex, final int localIndex, final int worldX, final int worldY, final int worldZ, final int level) { + final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; + + if (nibble != null) { + nibble.set(localIndex, level); + if (this.isClientSide) { + int cx1 = (worldX - 1) >> 4; + int cx2 = (worldX + 1) >> 4; + int cy1 = (worldY - 1) >> 4; + int cy2 = (worldY + 1) >> 4; + int cz1 = (worldZ - 1) >> 4; + int cz2 = (worldZ + 1) >> 4; + for (int x = cx1; x <= cx2; ++x) { + for (int y = cy1; y <= cy2; ++y) { + for (int z = cz1; z <= cz2; ++z) { + this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true; + } + } + } + } + } + } + + protected final boolean[] getEmptinessMap(final int chunkX, final int chunkZ) { + return this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset]; + } + + protected final void setEmptinessMapCache(final int chunkX, final int chunkZ, final boolean[] emptinessMap) { + this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = emptinessMap; + } + + public static SWMRNibbleArray[] getFilledEmptyLight(final LevelHeightAccessor world) { + return getFilledEmptyLight(WorldUtil.getTotalLightSections(world)); + } + + private static SWMRNibbleArray[] getFilledEmptyLight(final int totalLightSections) { + final SWMRNibbleArray[] ret = new SWMRNibbleArray[totalLightSections]; + + for (int i = 0, len = ret.length; i < len; ++i) { + ret[i] = new SWMRNibbleArray(null, true); + } + + return ret; + } + + protected abstract boolean[] getEmptinessMap(final ChunkAccess chunk); + + protected abstract void setEmptinessMap(final ChunkAccess chunk, final boolean[] to); + + protected abstract SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk); + + protected abstract void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to); + + protected abstract boolean canUseChunk(final ChunkAccess chunk); + + public final void blocksChangedInChunk(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, + final Set positions, final Boolean[] changedSections) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + if (changedSections != null) { + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, changedSections, false); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + } + if (!positions.isEmpty()) { + this.propagateBlockChanges(lightAccess, chunk, positions); + } + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + protected abstract void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions); + + protected abstract void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ); + + // if ret > expect, then the real value is at least ret (early returns if ret > expect, rather than calculating actual) + // if ret == expect, then expect is the correct light value for pos + // if ret < expect, then ret is the real light value + protected abstract int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect); + + protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16]; + protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16]; + + protected void checkChunkEdge(final LightChunkGetter lightAccess, final ChunkAccess chunk, + final int chunkX, final int chunkY, final int chunkZ) { + final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (currNibble == null) { + return; + } + + for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { + final int neighbourOffX = direction.x; + final int neighbourOffZ = direction.z; + + final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX, + chunkY, chunkZ + neighbourOffZ); + + if (neighbourNibble == null) { + continue; + } + + if (!currNibble.isInitialisedUpdating() && !neighbourNibble.isInitialisedUpdating()) { + // both are zero, nothing to check. + continue; + } + + // this chunk + final int incX; + final int incZ; + final int startX; + final int startZ; + + if (neighbourOffX != 0) { + // x direction + incX = 0; + incZ = 1; + + if (direction.x < 0) { + // negative + startX = chunkX << 4; + } else { + startX = chunkX << 4 | 15; + } + startZ = chunkZ << 4; + } else { + // z direction + incX = 1; + incZ = 0; + + if (neighbourOffZ < 0) { + // negative + startZ = chunkZ << 4; + } else { + startZ = chunkZ << 4 | 15; + } + startX = chunkX << 4; + } + + int centerDelayedChecks = 0; + int neighbourDelayedChecks = 0; + for (int currY = chunkY << 4, maxY = currY | 15; currY <= maxY; ++currY) { + for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { + final int neighbourX = currX + neighbourOffX; + final int neighbourZ = currZ + neighbourOffZ; + + final int currentIndex = (currX & 15) | + ((currZ & 15)) << 4 | + ((currY & 15) << 8); + final int currentLevel = currNibble.getUpdating(currentIndex); + + final int neighbourIndex = + (neighbourX & 15) | + ((neighbourZ & 15)) << 4 | + ((currY & 15) << 8); + final int neighbourLevel = neighbourNibble.getUpdating(neighbourIndex); + + // the checks are delayed because the checkBlock method clobbers light values - which then + // affect later calculate light value operations. While they don't affect it in a behaviourly significant + // way, they do have a negative performance impact due to simply queueing more values + + if (this.calculateLightValue(lightAccess, currX, currY, currZ, currentLevel) != currentLevel) { + this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex; + } + + if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel) != neighbourLevel) { + this.chunkCheckDelayedUpdatesNeighbour[neighbourDelayedChecks++] = neighbourIndex; + } + } + } + + final int currentChunkOffX = chunkX << 4; + final int currentChunkOffZ = chunkZ << 4; + final int neighbourChunkOffX = (chunkX + direction.x) << 4; + final int neighbourChunkOffZ = (chunkZ + direction.z) << 4; + final int chunkOffY = chunkY << 4; + for (int i = 0, len = Math.max(centerDelayedChecks, neighbourDelayedChecks); i < len; ++i) { + // try to queue neighbouring data together + // index = x | (z << 4) | (y << 8) + if (i < centerDelayedChecks) { + final int value = this.chunkCheckDelayedUpdatesCenter[i]; + this.checkBlock(lightAccess, currentChunkOffX | (value & 15), + chunkOffY | (value >>> 8), + currentChunkOffZ | ((value >>> 4) & 0xF)); + } + if (i < neighbourDelayedChecks) { + final int value = this.chunkCheckDelayedUpdatesNeighbour[i]; + this.checkBlock(lightAccess, neighbourChunkOffX | (value & 15), + chunkOffY | (value >>> 8), + neighbourChunkOffZ | ((value >>> 4) & 0xF)); + } + } + } + } + + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) { + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) { + this.checkChunkEdge(lightAccess, chunk, chunkX, iterator.nextShort(), chunkZ); + } + + this.performLightDecrease(lightAccess); + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + // verifies that light levels on this chunks edges are consistent with this chunk's neighbours + // edges. if they are not, they are decreased (effectively performing the logic in checkBlock). + // This does not resolve skylight source problems. + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) { + this.checkChunkEdge(lightAccess, chunk, chunkX, currSectionY, chunkZ); + } + + this.performLightDecrease(lightAccess); + } + + // pulls light from neighbours, and adds them into the increase queue. does not actually propagate. + protected final void propagateNeighbourLevels(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) { + final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, currSectionY, chunkZ); + if (currNibble == null) { + continue; + } + for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { + final int neighbourOffX = direction.x; + final int neighbourOffZ = direction.z; + + final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX, + currSectionY, chunkZ + neighbourOffZ); + + if (neighbourNibble == null || !neighbourNibble.isInitialisedUpdating()) { + // can't pull from 0 + continue; + } + + // neighbour chunk + final int incX; + final int incZ; + final int startX; + final int startZ; + + if (neighbourOffX != 0) { + // x direction + incX = 0; + incZ = 1; + + if (direction.x < 0) { + // negative + startX = (chunkX << 4) - 1; + } else { + startX = (chunkX << 4) + 16; + } + startZ = chunkZ << 4; + } else { + // z direction + incX = 1; + incZ = 0; + + if (neighbourOffZ < 0) { + // negative + startZ = (chunkZ << 4) - 1; + } else { + startZ = (chunkZ << 4) + 16; + } + startX = chunkX << 4; + } + + final long propagateDirection = 1L << direction.getOpposite().ordinal(); // we only want to check in this direction towards this chunk + final int encodeOffset = this.coordinateOffset; + + for (int currY = currSectionY << 4, maxY = currY | 15; currY <= maxY; ++currY) { + for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { + final int level = neighbourNibble.getUpdating( + (currX & 15) + | ((currZ & 15) << 4) + | ((currY & 15) << 8) + ); + + if (level <= 1) { + // nothing to propagate + continue; + } + + this.appendToIncreaseQueue( + ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((level & 0xFL) << (6 + 6 + 16)) + | (propagateDirection << (6 + 6 + 16 + 4)) + | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the current block is transparent, must check. + ); + } + } + } + } + } + + public static Boolean[] getEmptySectionsForChunk(final ChunkAccess chunk) { + final LevelChunkSection[] sections = chunk.getSections(); + final Boolean[] ret = new Boolean[sections.length]; + + for (int i = 0; i < sections.length; ++i) { + if (sections[i] == null || sections[i].hasOnlyAir()) { + ret[i] = Boolean.TRUE; + } else { + ret[i] = Boolean.FALSE; + } + } + + return ret; + } + + public final void forceHandleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptinessChanges) { + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + try { + // force current chunk into cache + this.setChunkInCache(chunkX, chunkZ, chunk); + this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections()); + this.setNibblesForChunkInCache(chunkX, chunkZ, this.getNibblesOnChunk(chunk)); + this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk)); + + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + public final void handleEmptySectionChanges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, + final Boolean[] emptinessChanges) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + protected abstract void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles); + + protected abstract void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ); + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + // subclasses are guaranteed that this is always called before a changed block set + // newChunk specifies whether the changes describe a "first load" of a chunk or changes to existing, already loaded chunks + // rets non-null when the emptiness map changed and needs to be updated + protected final boolean[] handleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, + final Boolean[] emptinessChanges, final boolean unlit) { + final Level world = (Level)lightAccess.getLevel(); + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + + boolean[] chunkEmptinessMap = this.getEmptinessMap(chunkX, chunkZ); + boolean[] ret = null; + final boolean needsInit = unlit || chunkEmptinessMap == null; + if (needsInit) { + this.setEmptinessMapCache(chunkX, chunkZ, ret = chunkEmptinessMap = new boolean[WorldUtil.getTotalSections(world)]); + } + + // update emptiness map + for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) { + Boolean valueBoxed = emptinessChanges[sectionIndex]; + if (valueBoxed == null) { + if (!needsInit) { + continue; + } + final LevelChunkSection section = this.getChunkSection(chunkX, sectionIndex + this.minSection, chunkZ); + emptinessChanges[sectionIndex] = valueBoxed = section == null || section.hasOnlyAir() ? Boolean.TRUE : Boolean.FALSE; + } + chunkEmptinessMap[sectionIndex] = valueBoxed.booleanValue(); + } + + // now init neighbour nibbles + for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) { + final Boolean valueBoxed = emptinessChanges[sectionIndex]; + final int sectionY = sectionIndex + this.minSection; + if (valueBoxed == null) { + continue; + } + + final boolean empty = valueBoxed.booleanValue(); + + if (empty) { + continue; + } + + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + // if we're not empty, we also need to initialise nibbles + // note: if we're unlit, we absolutely do not want to extrude, as light data isn't set up + final boolean extrude = (dx | dz) != 0 || !unlit; + for (int dy = 1; dy >= -1; --dy) { + this.initNibble(dx + chunkX, dy + sectionY, dz + chunkZ, extrude, false); + } + } + } + } + + // check for de-init and lazy-init + // lazy init is when chunks are being lit, so at the time they weren't loaded when their neighbours were running + // init checks. + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + // does this neighbour have 1 radius loaded? + boolean neighboursLoaded = true; + neighbour_loaded_search: + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + if (this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ) == null) { + neighboursLoaded = false; + break neighbour_loaded_search; + } + } + } + + for (int sectionY = this.maxLightSection; sectionY >= this.minLightSection; --sectionY) { + // check neighbours to see if we need to de-init this one + boolean allEmpty = true; + neighbour_search: + for (int dy2 = -1; dy2 <= 1; ++dy2) { + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + final int y = sectionY + dy2; + if (y < this.minSection || y > this.maxSection) { + // empty + continue; + } + final boolean[] emptinessMap = this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ); + if (emptinessMap != null) { + if (!emptinessMap[y - this.minSection]) { + allEmpty = false; + break neighbour_search; + } + } else { + final LevelChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ); + if (section != null && !section.hasOnlyAir()) { + allEmpty = false; + break neighbour_search; + } + } + } + } + } + + if (allEmpty & neighboursLoaded) { + // can only de-init when neighbours are loaded + // de-init is fine to delay, as de-init is just an optimisation - it's not required for lighting + // to be correct + + // all were empty, so de-init + this.setNibbleNull(dx + chunkX, sectionY, dz + chunkZ); + } else if (!allEmpty) { + // must init + final boolean extrude = (dx | dz) != 0 || !unlit; + this.initNibble(dx + chunkX, sectionY, dz + chunkZ, extrude, false); + } + } + } + } + + return ret; + } + + public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + this.checkChunkEdges(lightAccess, chunk, sections); + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + // needsEdgeChecks applies when possibly loading vanilla data, which means we need to validate the current + // chunks light values with respect to neighbours + // subclasses should note that the emptiness changes are propagated BEFORE this is called, so this function + // does not need to detect empty chunks itself (and it should do no handling for them either!) + protected abstract void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks); + + public final void light(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptySections) { + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + + try { + final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.maxLightSection - this.minLightSection + 1); + // force current chunk into cache + this.setChunkInCache(chunkX, chunkZ, chunk); + this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections()); + this.setNibblesForChunkInCache(chunkX, chunkZ, nibbles); + this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk)); + + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptySections, true); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + this.lightChunk(lightAccess, chunk, true); + this.setNibbles(chunk, nibbles); + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + public final void relightChunks(final LightChunkGetter lightAccess, final Set chunks, + final Consumer chunkLightCallback, final IntConsumer onComplete) { + // it's recommended for maximum performance that the set is ordered according to a BFS from the center of + // the region of chunks to relight + // it's required that tickets are added for each chunk to keep them loaded + final Long2ObjectOpenHashMap nibblesByChunk = new Long2ObjectOpenHashMap<>(); + final Long2ObjectOpenHashMap emptinessMapByChunk = new Long2ObjectOpenHashMap<>(); + + final int[] neighbourLightOrder = new int[] { + // d = 0 + 0, 0, + // d = 1 + -1, 0, + 0, -1, + 1, 0, + 0, 1, + // d = 2 + -1, 1, + 1, 1, + -1, -1, + 1, -1, + }; + + int lightCalls = 0; + + for (final ChunkPos chunkPos : chunks) { + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + final ChunkAccess chunk = (ChunkAccess)lightAccess.getChunkForLighting(chunkX, chunkZ); + if (chunk == null || !this.canUseChunk(chunk)) { + throw new IllegalStateException(); + } + + for (int i = 0, len = neighbourLightOrder.length; i < len; i += 2) { + final int dx = neighbourLightOrder[i]; + final int dz = neighbourLightOrder[i + 1]; + final int neighbourX = dx + chunkX; + final int neighbourZ = dz + chunkZ; + + final ChunkAccess neighbour = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX, neighbourZ); + if (neighbour == null || !this.canUseChunk(neighbour)) { + continue; + } + + if (nibblesByChunk.get(CoordinateUtils.getChunkKey(neighbourX, neighbourZ)) != null) { + // lit already called for neighbour, no need to light it now + continue; + } + + // light neighbour chunk + this.setupEncodeOffset(neighbourX * 16 + 7, 128, neighbourZ * 16 + 7); + try { + // insert all neighbouring chunks for this neighbour that we have data for + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + final int neighbourX2 = neighbourX + dx2; + final int neighbourZ2 = neighbourZ + dz2; + final long key = CoordinateUtils.getChunkKey(neighbourX2, neighbourZ2); + final ChunkAccess neighbour2 = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX2, neighbourZ2); + if (neighbour2 == null || !this.canUseChunk(neighbour2)) { + continue; + } + + final SWMRNibbleArray[] nibbles = nibblesByChunk.get(key); + if (nibbles == null) { + // we haven't lit this chunk + continue; + } + + this.setChunkInCache(neighbourX2, neighbourZ2, neighbour2); + this.setBlocksForChunkInCache(neighbourX2, neighbourZ2, neighbour2.getSections()); + this.setNibblesForChunkInCache(neighbourX2, neighbourZ2, nibbles); + this.setEmptinessMapCache(neighbourX2, neighbourZ2, emptinessMapByChunk.get(key)); + } + } + + final long key = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); + + // now insert the neighbour chunk and light it + final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.world); + nibblesByChunk.put(key, nibbles); + + this.setChunkInCache(neighbourX, neighbourZ, neighbour); + this.setBlocksForChunkInCache(neighbourX, neighbourZ, neighbour.getSections()); + this.setNibblesForChunkInCache(neighbourX, neighbourZ, nibbles); + + final boolean[] neighbourEmptiness = this.handleEmptySectionChanges(lightAccess, neighbour, getEmptySectionsForChunk(neighbour), true); + emptinessMapByChunk.put(key, neighbourEmptiness); + if (chunks.contains(new ChunkPos(neighbourX, neighbourZ))) { + this.setEmptinessMap(neighbour, neighbourEmptiness); + } + + this.lightChunk(lightAccess, neighbour, false); + } finally { + this.destroyCaches(); + } + } + + // done lighting all neighbours, so the chunk is now fully lit + + // make sure nibbles are fully updated before calling back + final SWMRNibbleArray[] nibbles = nibblesByChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + for (final SWMRNibbleArray nibble : nibbles) { + nibble.updateVisible(); + } + + this.setNibbles(chunk, nibbles); + + for (int y = this.minLightSection; y <= this.maxLightSection; ++y) { + lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkZ)); + } + + // now do callback + if (chunkLightCallback != null) { + chunkLightCallback.accept(chunkPos); + } + ++lightCalls; + } + + if (onComplete != null) { + onComplete.accept(lightCalls); + } + } + + // contains: + // lower (6 + 6 + 16) = 28 bits: encoded coordinate position (x | (z << 6) | (y << (6 + 6)))) + // next 4 bits: propagated light level (0, 15] + // next 6 bits: propagation direction bitset + // next 24 bits: unused + // last 3 bits: state flags + // state flags: + // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading light + // updates for block sources + protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 2; + // whether the propagation needs to check if its current level is equal to the expected level + // used only in increase propagation + protected static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 1; + // whether the propagation needs to consider if its block is conditionally transparent + protected static final long FLAG_HAS_SIDED_TRANSPARENT_BLOCKS = Long.MIN_VALUE; + + protected long[] increaseQueue = new long[16 * 16 * 16]; + protected int increaseQueueInitialLength; + protected long[] decreaseQueue = new long[16 * 16 * 16]; + protected int decreaseQueueInitialLength; + + protected final long[] resizeIncreaseQueue() { + return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2); + } + + protected final long[] resizeDecreaseQueue() { + return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2); + } + + protected final void appendToIncreaseQueue(final long value) { + final int idx = this.increaseQueueInitialLength++; + long[] queue = this.increaseQueue; + if (idx >= queue.length) { + queue = this.resizeIncreaseQueue(); + queue[idx] = value; + } else { + queue[idx] = value; + } + } + + protected final void appendToDecreaseQueue(final long value) { + final int idx = this.decreaseQueueInitialLength++; + long[] queue = this.decreaseQueue; + if (idx >= queue.length) { + queue = this.resizeDecreaseQueue(); + queue[idx] = value; + } else { + queue[idx] = value; + } + } + + protected static final AxisDirection[][] OLD_CHECK_DIRECTIONS = new AxisDirection[1 << 6][]; + protected static final int ALL_DIRECTIONS_BITSET = (1 << 6) - 1; + static { + for (int i = 0; i < OLD_CHECK_DIRECTIONS.length; ++i) { + final List directions = new ArrayList<>(); + for (int bitset = i, len = Integer.bitCount(i), index = 0; index < len; ++index, bitset ^= IntegerUtil.getTrailingBit(bitset)) { + directions.add(AXIS_DIRECTIONS[IntegerUtil.trailingZeros(bitset)]); + } + OLD_CHECK_DIRECTIONS[i] = directions.toArray(new AxisDirection[0]); + } + } + + protected final void performLightIncrease(final LightChunkGetter lightAccess) { + final BlockGetter world = lightAccess.getLevel(); + long[] queue = this.increaseQueue; + int queueReadIndex = 0; + int queueLength = this.increaseQueueInitialLength; + this.increaseQueueInitialLength = 0; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + final int encodeOffset = this.coordinateOffset; + final int sectionOffset = this.chunkSectionIndexOffset; + + while (queueReadIndex < queueLength) { + final long queueValue = queue[queueReadIndex++]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xFL); + final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63L)]; + + if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) { + if (this.getLightLevel(posX, posY, posZ) != propagatedLightLevel) { + // not at the level we expect, so something changed. + continue; + } + } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) { + // these are used to restore block sources after a propagation decrease + this.setLightLevel(posX, posY, posZ, propagatedLightLevel); + } + + if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) { + // we don't need to worry about our state here. + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int currentLevel; + if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) { + continue; // already at the level we want or unloaded + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + long flags = 0; + if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(); + final int targetLevel = propagatedLightLevel - Math.max(1, opacity); + if (targetLevel <= currentLevel) { + continue; + } + + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) + | (flags); + } + continue; + } + } else { + // we actually need to worry about our state here + final BlockState fromBlock = this.getBlockState(posX, posY, posZ); + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(propagate.nms) : Shapes.empty(); + + if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { + continue; + } + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int currentLevel; + + if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) { + continue; // already at the level we want + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + long flags = 0; + if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(); + final int targetLevel = propagatedLightLevel - Math.max(1, opacity); + if (targetLevel <= currentLevel) { + continue; + } + + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) + | (flags); + } + continue; + } + } + } + } + + protected final void performLightDecrease(final LightChunkGetter lightAccess) { + final BlockGetter world = lightAccess.getLevel(); + long[] queue = this.decreaseQueue; + long[] increaseQueue = this.increaseQueue; + int queueReadIndex = 0; + int queueLength = this.decreaseQueueInitialLength; + this.decreaseQueueInitialLength = 0; + int increaseQueueLength = this.increaseQueueInitialLength; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + final int encodeOffset = this.coordinateOffset; + final int sectionOffset = this.chunkSectionIndexOffset; + final int emittedMask = this.emittedLightMask; + + final PlatformHooks platformHooks = PlatformHooks.get(); + + while (queueReadIndex < queueLength) { + final long queueValue = queue[queueReadIndex++]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); + final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63)]; + + if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) { + // we don't need to worry about our state here. + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int lightLevel; + + if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { + // already at lowest (or unloaded), nothing we can do + continue; + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + this.lightEmissionPos.set(offX, offY, offZ); + long flags = 0; + if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(); + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (FLAG_RECHECK_LEVEL | flags); + continue; + } + final int emittedLight = (platformHooks.getLightEmission(blockState, world, this.lightEmissionPos)) & emittedMask; + if (emittedLight != 0) { + // re-propagate source + // note: do not set recheck level, or else the propagation will fail + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (flags | FLAG_WRITE_LEVEL); + } + + currentNibble.set(localIndex, 0); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) + | flags; + } + continue; + } + } else { + // we actually need to worry about our state here + final BlockState fromBlock = this.getBlockState(posX, posY, posZ); + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(propagate.nms) : Shapes.empty(); + + if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { + continue; + } + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int lightLevel; + + if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { + // already at lowest (or unloaded), nothing we can do + continue; + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + this.lightEmissionPos.set(offX, offY, offZ); + long flags = 0; + if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(); + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (FLAG_RECHECK_LEVEL | flags); + continue; + } + final int emittedLight = (platformHooks.getLightEmission(blockState, world, this.lightEmissionPos)) & emittedMask; + if (emittedLight != 0) { + // re-propagate source + // note: do not set recheck level, or else the propagation will fail + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (flags | FLAG_WRITE_LEVEL); + } + + currentNibble.set(localIndex, 0); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) + | flags; + } + continue; + } + } + } + + // propagate sources we clobbered + this.increaseQueueInitialLength = increaseQueueLength; + this.performLightIncrease(lightAccess); + } +} diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java new file mode 100644 index 0000000000000000000000000000000000000000..571db5f9bf94745a8afe2cd313e593fb15db5e37 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java @@ -0,0 +1,931 @@ +package ca.spottedleaf.moonrise.patches.starlight.light; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; +import ca.spottedleaf.concurrentutil.util.Priority; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; +import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.DataLayer; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.lighting.LayerLightEventListener; +import net.minecraft.world.level.lighting.LevelLightEngine; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public final class StarLightInterface { + + public static final TicketType CHUNK_WORK_TICKET = TicketType.create("starlight:chunk_work_ticket", Long::compareTo); + public static final int LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(ChunkStatus.LIGHT); + // ticket level = ChunkLevel.byStatus(FullChunkStatus.FULL) - input + public static final int REGION_LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.FULL) - LIGHT_TICKET_LEVEL; + + /** + * Can be {@code null}, indicating the light is all empty. + */ + public final Level world; + public final LightChunkGetter lightAccess; + + private final ArrayDeque cachedSkyPropagators; + private final ArrayDeque cachedBlockPropagators; + + private final LightQueue lightQueue; + + private final LayerLightEventListener skyReader; + private final LayerLightEventListener blockReader; + private final boolean isClientSide; + + public final int minSection; + public final int maxSection; + public final int minLightSection; + public final int maxLightSection; + + public final LevelLightEngine lightEngine; + + private final boolean hasBlockLight; + private final boolean hasSkyLight; + + public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) { + this.lightAccess = lightAccess; + this.world = lightAccess == null ? null : (Level)lightAccess.getLevel(); + this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null; + this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null; + this.isClientSide = !(this.world instanceof ServerLevel); + if (this.world == null) { + this.minSection = -4; + this.maxSection = 19; + this.minLightSection = -5; + this.maxLightSection = 20; + } else { + this.minSection = WorldUtil.getMinSection(this.world); + this.maxSection = WorldUtil.getMaxSection(this.world); + this.minLightSection = WorldUtil.getMinLightSection(this.world); + this.maxLightSection = WorldUtil.getMaxLightSection(this.world); + } + + if (this.world instanceof ServerLevel) { + this.lightQueue = new ServerLightQueue(this); + } else { + this.lightQueue = new ClientLightQueue(this); + } + + this.lightEngine = lightEngine; + this.hasBlockLight = hasBlockLight; + this.hasSkyLight = hasSkyLight; + this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { + @Override + public void checkBlock(final BlockPos blockPos) { + StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); + } + + @Override + public void propagateLightSources(final ChunkPos chunkPos) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasLightWork() { + // not really correct... + return StarLightInterface.this.hasUpdates(); + } + + @Override + public int runLightUpdates() { + throw new UnsupportedOperationException(); + } + + @Override + public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) { + throw new UnsupportedOperationException(); + } + + @Override + public DataLayer getDataLayerData(final SectionPos pos) { + final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); + if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) { + return null; + } + + final int sectionY = pos.getY(); + + if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) { + return null; + } + + if (((StarlightChunk)chunk).starlight$getSkyEmptinessMap() == null) { + return null; + } + + return ((StarlightChunk)chunk).starlight$getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble(); + } + + @Override + public int getLightValue(final BlockPos blockPos) { + return StarLightInterface.this.getSkyLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4)); + } + + @Override + public void updateSectionStatus(final SectionPos pos, final boolean notReady) { + StarLightInterface.this.sectionChange(pos, notReady); + } + }; + this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { + @Override + public void checkBlock(final BlockPos blockPos) { + StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); + } + + @Override + public void propagateLightSources(final ChunkPos chunkPos) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasLightWork() { + // not really correct... + return StarLightInterface.this.hasUpdates(); + } + + @Override + public int runLightUpdates() { + throw new UnsupportedOperationException(); + } + + @Override + public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) { + throw new UnsupportedOperationException(); + } + + @Override + public DataLayer getDataLayerData(final SectionPos pos) { + final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); + + if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) { + return null; + } + + return ((StarlightChunk)chunk).starlight$getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble(); + } + + @Override + public int getLightValue(final BlockPos blockPos) { + return StarLightInterface.this.getBlockLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4)); + } + + @Override + public void updateSectionStatus(final SectionPos pos, final boolean notReady) { + StarLightInterface.this.sectionChange(pos, notReady); + } + }; + } + + public ClientLightQueue getClientLightQueue() { + if (this.lightQueue instanceof ClientLightQueue clientLightQueue) { + return clientLightQueue; + } + return null; + } + + public ServerLightQueue getServerLightQueue() { + if (this.lightQueue instanceof ServerLightQueue serverLightQueue) { + return serverLightQueue; + } + return null; + } + + public boolean hasSkyLight() { + return this.hasSkyLight; + } + + public boolean hasBlockLight() { + return this.hasBlockLight; + } + + public int getSkyLightValue(final BlockPos blockPos, final ChunkAccess chunk) { + if (!this.hasSkyLight) { + return 0; + } + final int x = blockPos.getX(); + int y = blockPos.getY(); + final int z = blockPos.getZ(); + + final int minSection = this.minSection; + final int maxSection = this.maxSection; + final int minLightSection = this.minLightSection; + final int maxLightSection = this.maxLightSection; + + if (chunk == null || (!this.isClientSide && !chunk.isLightCorrect()) || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) { + return 15; + } + + int sectionY = y >> 4; + + if (sectionY > maxLightSection) { + return 15; + } + + if (sectionY < minLightSection) { + sectionY = minLightSection; + y = sectionY << 4; + } + + final SWMRNibbleArray[] nibbles = ((StarlightChunk)chunk).starlight$getSkyNibbles(); + final SWMRNibbleArray immediate = nibbles[sectionY - minLightSection]; + + if (!immediate.isNullNibbleVisible()) { + return immediate.getVisible(x, y, z); + } + + final boolean[] emptinessMap = ((StarlightChunk)chunk).starlight$getSkyEmptinessMap(); + + if (emptinessMap == null) { + return 15; + } + + // are we above this chunk's lowest empty section? + int lowestY = minLightSection - 1; + for (int currY = maxSection; currY >= minSection; --currY) { + if (emptinessMap[currY - minSection]) { + continue; + } + + // should always be full lit here + lowestY = currY; + break; + } + + if (sectionY > lowestY) { + return 15; + } + + // this nibble is going to depend solely on the skylight data above it + // find first non-null data above (there does exist one, as we just found it above) + for (int currY = sectionY + 1; currY <= maxLightSection; ++currY) { + final SWMRNibbleArray nibble = nibbles[currY - minLightSection]; + if (!nibble.isNullNibbleVisible()) { + return nibble.getVisible(x, 0, z); + } + } + + // should never reach here + return 15; + } + + public int getBlockLightValue(final BlockPos blockPos, final ChunkAccess chunk) { + if (!this.hasBlockLight) { + return 0; + } + final int y = blockPos.getY(); + final int cy = y >> 4; + + final int minLightSection = this.minLightSection; + final int maxLightSection = this.maxLightSection; + + if (cy < minLightSection || cy > maxLightSection) { + return 0; + } + + if (chunk == null) { + return 0; + } + + final SWMRNibbleArray nibble = ((StarlightChunk)chunk).starlight$getBlockNibbles()[cy - minLightSection]; + return nibble.getVisible(blockPos.getX(), y, blockPos.getZ()); + } + + public int getRawBrightness(final BlockPos pos, final int ambientDarkness) { + final ChunkAccess chunk = this.getAnyChunkNow(pos.getX() >> 4, pos.getZ() >> 4); + + final int sky = this.getSkyLightValue(pos, chunk) - ambientDarkness; + // Don't fetch the block light level if the skylight level is 15, since the value will never be higher. + if (sky == 15) { + return 15; + } + final int block = this.getBlockLightValue(pos, chunk); + return Math.max(sky, block); + } + + public LayerLightEventListener getSkyReader() { + return this.skyReader; + } + + public LayerLightEventListener getBlockReader() { + return this.blockReader; + } + + public boolean isClientSide() { + return this.isClientSide; + } + + public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) { + if (this.world == null) { + // empty world + return null; + } + return ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ); + } + + public boolean hasUpdates() { + return !this.lightQueue.isEmpty(); + } + + public Level getWorld() { + return this.world; + } + + public LightChunkGetter getLightAccess() { + return this.lightAccess; + } + + public SkyStarLightEngine getSkyLightEngine() { + if (this.cachedSkyPropagators == null) { + return null; + } + final SkyStarLightEngine ret; + synchronized (this.cachedSkyPropagators) { + ret = this.cachedSkyPropagators.pollFirst(); + } + + if (ret == null) { + return new SkyStarLightEngine(this.world); + } + return ret; + } + + public void releaseSkyLightEngine(final SkyStarLightEngine engine) { + if (this.cachedSkyPropagators == null) { + return; + } + synchronized (this.cachedSkyPropagators) { + this.cachedSkyPropagators.addFirst(engine); + } + } + + public BlockStarLightEngine getBlockLightEngine() { + if (this.cachedBlockPropagators == null) { + return null; + } + final BlockStarLightEngine ret; + synchronized (this.cachedBlockPropagators) { + ret = this.cachedBlockPropagators.pollFirst(); + } + + if (ret == null) { + return new BlockStarLightEngine(this.world); + } + return ret; + } + + public void releaseBlockLightEngine(final BlockStarLightEngine engine) { + if (this.cachedBlockPropagators == null) { + return; + } + synchronized (this.cachedBlockPropagators) { + this.cachedBlockPropagators.addFirst(engine); + } + } + + public LightQueue.ChunkTasks blockChange(final BlockPos pos) { + if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world + return null; + } + + return this.lightQueue.queueBlockChange(pos); + } + + public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) { + if (this.world == null) { // empty world + return null; + } + + return this.lightQueue.queueSectionChange(pos, newEmptyValue); + } + + public void forceLoadInChunk(final ChunkAccess chunk, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); + } + if (blockEngine != null) { + blockEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void loadInChunk(final int chunkX, final int chunkZ, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); + } + if (blockEngine != null) { + blockEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void lightChunk(final ChunkAccess chunk, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.light(this.lightAccess, chunk, emptySections); + } + if (blockEngine != null) { + blockEngine.light(this.lightAccess, chunk, emptySections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void relightChunks(final Set chunks, final Consumer chunkLightCallback, + final IntConsumer onComplete) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.relightChunks(this.lightAccess, chunks, blockEngine == null ? chunkLightCallback : null, + blockEngine == null ? onComplete : null); + } + if (blockEngine != null) { + blockEngine.relightChunks(this.lightAccess, chunks, chunkLightCallback, onComplete); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void checkChunkEdges(final int chunkX, final int chunkZ) { + this.checkSkyEdges(chunkX, chunkZ); + this.checkBlockEdges(chunkX, chunkZ); + } + + public void checkSkyEdges(final int chunkX, final int chunkZ) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + } + } + + public void checkBlockEdges(final int chunkX, final int chunkZ) { + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + try { + if (blockEngine != null) { + blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); + } + } finally { + this.releaseBlockLightEngine(blockEngine); + } + } + + public void propagateChanges() { + final LightQueue lightQueue = this.lightQueue; + if (lightQueue instanceof ClientLightQueue clientLightQueue) { + clientLightQueue.drainTasks(); + } // else: invalid usage, although we won't throw because mods... + } + + public static abstract class LightQueue { + + protected final StarLightInterface lightInterface; + + public LightQueue(final StarLightInterface lightInterface) { + this.lightInterface = lightInterface; + } + + public abstract boolean isEmpty(); + + public abstract ChunkTasks queueBlockChange(final BlockPos pos); + + public abstract ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue); + + public abstract ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections); + + public abstract ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections); + + public static abstract class ChunkTasks implements Runnable { + + public final long chunkCoordinate; + + protected final StarLightInterface lightEngine; + protected final LightQueue queue; + protected final MultiThreadedQueue onComplete = new MultiThreadedQueue<>(); + protected final Set changedPositions = new HashSet<>(); + protected Boolean[] changedSectionSet; + protected ShortOpenHashSet queuedEdgeChecksSky; + protected ShortOpenHashSet queuedEdgeChecksBlock; + protected List lightTasks; + + public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue) { + this.chunkCoordinate = chunkCoordinate; + this.lightEngine = lightEngine; + this.queue = queue; + } + + @Override + public abstract void run(); + + public void queueOrRunTask(final Runnable run) { + if (!this.onComplete.add(run)) { + run.run(); + } + } + + protected void addChangedPosition(final BlockPos pos) { + this.changedPositions.add(pos.immutable()); + } + + protected void setChangedSection(final int y, final Boolean newEmptyValue) { + if (this.changedSectionSet == null) { + this.changedSectionSet = new Boolean[this.lightEngine.maxSection - this.lightEngine.minSection + 1]; + } + this.changedSectionSet[y - this.lightEngine.minSection] = newEmptyValue; + } + + protected void addLightTask(final BooleanSupplier lightTask) { + if (this.lightTasks == null) { + this.lightTasks = new ArrayList<>(); + } + this.lightTasks.add(lightTask); + } + + protected void addEdgeChecksSky(final ShortCollection values) { + if (this.queuedEdgeChecksSky == null) { + this.queuedEdgeChecksSky = new ShortOpenHashSet(Math.max(8, values.size())); + } + this.queuedEdgeChecksSky.addAll(values); + } + + protected void addEdgeChecksBlock(final ShortCollection values) { + if (this.queuedEdgeChecksBlock == null) { + this.queuedEdgeChecksBlock = new ShortOpenHashSet(Math.max(8, values.size())); + } + this.queuedEdgeChecksBlock.addAll(values); + } + + protected final void runTasks() { + boolean litChunk = false; + if (this.lightTasks != null) { + for (final BooleanSupplier run : this.lightTasks) { + if (run.getAsBoolean()) { + litChunk = true; + break; + } + } + } + + if (!litChunk) { + final SkyStarLightEngine skyEngine = this.lightEngine.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.lightEngine.getBlockLightEngine(); + try { + final long coordinate = this.chunkCoordinate; + final int chunkX = CoordinateUtils.getChunkX(coordinate); + final int chunkZ = CoordinateUtils.getChunkZ(coordinate); + + final Set positions = this.changedPositions; + final Boolean[] sectionChanges = this.changedSectionSet; + + if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { + skyEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges); + } + if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { + blockEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges); + } + + if (skyEngine != null && this.queuedEdgeChecksSky != null) { + skyEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksSky); + } + if (blockEngine != null && this.queuedEdgeChecksBlock != null) { + blockEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksBlock); + } + } finally { + this.lightEngine.releaseSkyLightEngine(skyEngine); + this.lightEngine.releaseBlockLightEngine(blockEngine); + } + } + + Runnable run; + while ((run = this.onComplete.pollOrBlockAdds()) != null) { + run.run(); + } + } + } + } + + public static final class ClientLightQueue extends LightQueue { + + private final Long2ObjectLinkedOpenHashMap chunkTasks = new Long2ObjectLinkedOpenHashMap<>(); + + public ClientLightQueue(final StarLightInterface lightInterface) { + super(lightInterface); + } + + @Override + public synchronized boolean isEmpty() { + return this.chunkTasks.isEmpty(); + } + + // must hold synchronized lock on this object + private ClientChunkTasks getOrCreate(final long key) { + return this.chunkTasks.computeIfAbsent(key, (final long keyInMap) -> { + return new ClientChunkTasks(keyInMap, ClientLightQueue.this.lightInterface, ClientLightQueue.this); + }); + } + + @Override + public synchronized ClientChunkTasks queueBlockChange(final BlockPos pos) { + final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); + tasks.addChangedPosition(pos); + return tasks; + } + + @Override + public synchronized ClientChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { + final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); + + tasks.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue)); + + return tasks; + } + + @Override + public synchronized ClientChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { + final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); + + tasks.addEdgeChecksSky(sections); + + return tasks; + } + + @Override + public synchronized ClientChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { + final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); + + tasks.addEdgeChecksBlock(sections); + + return tasks; + } + + public synchronized ClientChunkTasks removeFirstTask() { + if (this.chunkTasks.isEmpty()) { + return null; + } + return this.chunkTasks.removeFirst(); + } + + public void drainTasks() { + ClientChunkTasks task; + while ((task = this.removeFirstTask()) != null) { + task.runTasks(); + } + } + + public static final class ClientChunkTasks extends ChunkTasks { + + public ClientChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final ClientLightQueue queue) { + super(chunkCoordinate, lightEngine, queue); + } + + @Override + public void run() { + this.runTasks(); + } + } + } + + public static final class ServerLightQueue extends LightQueue { + + private final ConcurrentLong2ReferenceChainedHashTable chunkTasks = new ConcurrentLong2ReferenceChainedHashTable<>(); + + public ServerLightQueue(final StarLightInterface lightInterface) { + super(lightInterface); + } + + public void lowerPriority(final int chunkX, final int chunkZ, final Priority priority) { + final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (task != null) { + task.lowerPriority(priority); + } + } + + public void setPriority(final int chunkX, final int chunkZ, final Priority priority) { + final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (task != null) { + task.setPriority(priority); + } + } + + public void raisePriority(final int chunkX, final int chunkZ, final Priority priority) { + final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (task != null) { + task.raisePriority(priority); + } + } + + public Priority getPriority(final int chunkX, final int chunkZ) { + final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (task != null) { + return task.getPriority(); + } + + return Priority.COMPLETING; + } + + @Override + public boolean isEmpty() { + return this.chunkTasks.isEmpty(); + } + + @Override + public ServerChunkTasks queueBlockChange(final BlockPos pos) { + final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { + if (valueInMap == null) { + valueInMap = new ServerChunkTasks( + keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this + ); + } + valueInMap.addChangedPosition(pos); + return valueInMap; + }); + + ret.schedule(); + + return ret; + } + + @Override + public ServerChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { + final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { + if (valueInMap == null) { + valueInMap = new ServerChunkTasks( + keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this + ); + } + + valueInMap.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue)); + + return valueInMap; + }); + + ret.schedule(); + + return ret; + } + + public ServerChunkTasks queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final Priority priority) { + final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { + if (valueInMap == null) { + valueInMap = new ServerChunkTasks( + keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this, priority + ); + } + + valueInMap.addLightTask(lightTask); + + return valueInMap; + }); + + ret.schedule(); + + return ret; + } + + @Override + public ServerChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { + final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { + if (valueInMap == null) { + valueInMap = new ServerChunkTasks( + keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this + ); + } + + valueInMap.addEdgeChecksSky(sections); + + return valueInMap; + }); + + ret.schedule(); + + return ret; + } + + @Override + public ServerChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { + final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { + if (valueInMap == null) { + valueInMap = new ServerChunkTasks( + keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this + ); + } + + valueInMap.addEdgeChecksBlock(sections); + + return valueInMap; + }); + + ret.schedule(); + + return ret; + } + + public static final class ServerChunkTasks extends ChunkTasks { + + private final AtomicBoolean ticketAdded = new AtomicBoolean(); + private final PrioritisedExecutor.PrioritisedTask task; + + public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, + final ServerLightQueue queue) { + this(chunkCoordinate, lightEngine, queue, Priority.NORMAL); + } + + public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, + final ServerLightQueue queue, final Priority priority) { + super(chunkCoordinate, lightEngine, queue); + this.task = ((ChunkSystemServerLevel)(ServerLevel)lightEngine.getWorld()).moonrise$getChunkTaskScheduler().radiusAwareScheduler.createTask( + CoordinateUtils.getChunkX(chunkCoordinate), CoordinateUtils.getChunkZ(chunkCoordinate), + ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$getWriteRadius(), this, priority + ); + } + + public boolean markTicketAdded() { + return !this.ticketAdded.get() && !this.ticketAdded.getAndSet(true); + } + + public void schedule() { + this.task.queue(); + } + + public boolean cancel() { + return this.task.cancel(); + } + + public Priority getPriority() { + return this.task.getPriority(); + } + + public void lowerPriority(final Priority priority) { + this.task.lowerPriority(priority); + } + + public void setPriority(final Priority priority) { + this.task.setPriority(priority); + } + + public void raisePriority(final Priority priority) { + this.task.raisePriority(priority); + } + + @Override + public void run() { + ((ServerLightQueue)this.queue).chunkTasks.remove(this.chunkCoordinate, this); + + this.runTasks(); + } + } + } +} diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..7fe59ab70557aa6a484a02db2b2007fdd9e4bbb8 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java @@ -0,0 +1,29 @@ +package ca.spottedleaf.moonrise.patches.starlight.light; + +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.chunk.DataLayer; +import net.minecraft.world.level.chunk.LevelChunk; +import java.util.Collection; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public interface StarLightLightingProvider { + + public StarLightInterface starlight$getLightEngine(); + + public void starlight$clientUpdateLight(final LightLayer lightType, final SectionPos pos, + final DataLayer nibble, final boolean trustEdges); + + public void starlight$clientRemoveLightData(final ChunkPos chunkPos); + + public void starlight$clientChunkLoad(final ChunkPos pos, final LevelChunk chunk); + + public default int starlight$serverRelightChunks(final Collection chunks, + final Consumer chunkLightCallback, + final IntConsumer onComplete) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + +} diff --git a/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java b/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java new file mode 100644 index 0000000000000000000000000000000000000000..40d004afdc6449530f5bb2d7c7638b8ee3e3a577 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java @@ -0,0 +1,13 @@ +package ca.spottedleaf.moonrise.patches.starlight.storage; + +public interface StarlightSectionData { + + public int starlight$getBlockLightState(); + + public void starlight$setBlockLightState(final int state); + + public int starlight$getSkyLightState(); + + public void starlight$setSkyLightState(final int state); + +} diff --git a/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java b/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..689ce367164e79e0426eeecb81dbbc521d4bc742 --- /dev/null +++ b/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java @@ -0,0 +1,189 @@ +package ca.spottedleaf.moonrise.patches.starlight.util; + +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; +import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine; +import com.mojang.logging.LogUtils; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.slf4j.Logger; + +// note: keep in-sync with SerializableChunkDataMixin +public final class SaveUtil { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public static final int STARLIGHT_LIGHT_VERSION = 9; + + public static int getLightVersion() { + return STARLIGHT_LIGHT_VERSION; + } + + public static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state"; + public static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; + public static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; + + public static void saveLightHook(final Level world, final ChunkAccess chunk, final CompoundTag nbt) { + try { + saveLightHookReal(world, chunk, nbt); + } catch (final Throwable ex) { + // failing to inject is not fatal so we catch anything here. if it fails, it will have correctly set lit to false + // for Vanilla to relight on load and it will not set our lit tag so we will relight on load + LOGGER.warn("Failed to inject light data into save data for chunk " + chunk.getPos() + ", chunk light will be recalculated on its next load", ex); + } + } + + private static void saveLightHookReal(final Level world, final ChunkAccess chunk, final CompoundTag tag) { + if (tag == null) { + return; + } + + final int minSection = WorldUtil.getMinLightSection(world); + final int maxSection = WorldUtil.getMaxLightSection(world); + + SWMRNibbleArray[] blockNibbles = ((StarlightChunk)chunk).starlight$getBlockNibbles(); + SWMRNibbleArray[] skyNibbles = ((StarlightChunk)chunk).starlight$getSkyNibbles(); + + boolean lit = chunk.isLightCorrect() || !(world instanceof ServerLevel); + // diff start - store our tag for whether light data is init'd + if (lit) { + tag.putBoolean("isLightOn", false); + } + // diff end - store our tag for whether light data is init'd + ChunkStatus status = ChunkStatus.byName(tag.getString("Status")); + + CompoundTag[] sections = new CompoundTag[maxSection - minSection + 1]; + + ListTag sectionsStored = tag.getList("sections", 10); + + for (int i = 0; i < sectionsStored.size(); ++i) { + CompoundTag sectionStored = sectionsStored.getCompound(i); + int k = sectionStored.getByte("Y"); + + // strip light data + sectionStored.remove("BlockLight"); + sectionStored.remove("SkyLight"); + + if (!sectionStored.isEmpty()) { + sections[k - minSection] = sectionStored; + } + } + + if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { + for (int i = minSection; i <= maxSection; ++i) { + SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState(); + SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState(); + if (blockNibble != null || skyNibble != null) { + CompoundTag section = sections[i - minSection]; + if (section == null) { + section = new CompoundTag(); + section.putByte("Y", (byte)i); + sections[i - minSection] = section; + } + + // we store under the same key so mod programs editing nbt + // can still read the data, hopefully. + // however, for compatibility we store chunks as unlit so vanilla + // is forced to re-light them if it encounters our data. It's too much of a burden + // to try and maintain compatibility with a broken and inferior skylight management system. + + if (blockNibble != null) { + if (blockNibble.data != null) { + section.putByteArray("BlockLight", blockNibble.data); + } + section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state); + } + + if (skyNibble != null) { + if (skyNibble.data != null) { + section.putByteArray("SkyLight", skyNibble.data); + } + section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state); + } + } + } + } + + // rewrite section list + sectionsStored.clear(); + for (CompoundTag section : sections) { + if (section != null) { + sectionsStored.add(section); + } + } + tag.put("sections", sectionsStored); + if (lit) { + tag.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // only mark as fully lit after we have successfully injected our data + } + } + + public static void loadLightHook(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) { + try { + loadLightHookReal(world, pos, tag, into); + } catch (final Throwable ex) { + // failing to inject is not fatal so we catch anything here. if it fails, then we simply relight. Not a problem, we get correct + // lighting in both cases. + LOGGER.warn("Failed to load light for chunk " + pos + ", light will be recalculated", ex); + } + } + + private static void loadLightHookReal(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) { + if (into == null) { + return; + } + final int minSection = WorldUtil.getMinLightSection(world); + final int maxSection = WorldUtil.getMaxLightSection(world); + + into.setLightCorrect(false); // mark as unlit in case we fail parsing + + SWMRNibbleArray[] blockNibbles = StarLightEngine.getFilledEmptyLight(world); + SWMRNibbleArray[] skyNibbles = StarLightEngine.getFilledEmptyLight(world); + + + // start copy from the original method + boolean lit = tag.get("isLightOn") != null && tag.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; + boolean canReadSky = world.dimensionType().hasSkyLight(); + ChunkStatus status = ChunkStatus.byName(tag.getString("Status")); + if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { // diff - we add the status check here + ListTag sections = tag.getList("sections", 10); + + for (int i = 0; i < sections.size(); ++i) { + CompoundTag sectionData = sections.getCompound(i); + int y = sectionData.getByte("Y"); + + if (sectionData.contains("BlockLight", 7)) { + // this is where our diff is + blockNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety + } else { + blockNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG)); + } + + if (canReadSky) { + if (sectionData.contains("SkyLight", 7)) { + // we store under the same key so mod programs editing nbt + // can still read the data, hopefully. + // however, for compatibility we store chunks as unlit so vanilla + // is forced to re-light them if it encounters our data. It's too much of a burden + // to try and maintain compatibility with a broken and inferior skylight management system. + skyNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety + } else { + skyNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG)); + } + } + } + } + // end copy from vanilla + + ((StarlightChunk)into).starlight$setBlockNibbles(blockNibbles); + ((StarlightChunk)into).starlight$setSkyNibbles(skyNibbles); + into.setLightCorrect(lit); // now we set lit here, only after we've correctly parsed data + } + + private SaveUtil() {} +} diff --git a/io/papermc/paper/FeatureHooks.java b/io/papermc/paper/FeatureHooks.java index 184e6c6fe2ba522d0ea0774604839320c4152371..460bb584db04b582f3297ae419183f430aff1ec0 100644 --- a/io/papermc/paper/FeatureHooks.java +++ b/io/papermc/paper/FeatureHooks.java @@ -1,6 +1,9 @@ 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.LongIterator; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import it.unimi.dsi.fastutil.longs.LongSet; import it.unimi.dsi.fastutil.longs.LongSets; @@ -31,9 +34,12 @@ import org.bukkit.World; public final class FeatureHooks { public static void initChunkTaskScheduler(final boolean useParallelGen) { + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.init(useParallelGen); // Paper - Chunk system } public static void registerPaperCommands(final Map, PaperSubcommand> commands) { + commands.put(Set.of("fixlight"), new FixLightCommand()); // Paper - rewrite chunk system + commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand()); // Paper - rewrite chunk system } public static LevelChunkSection createSection(final Registry biomeRegistry, final Level level, final ChunkPos chunkPos, final int chunkSection) { @@ -59,18 +65,19 @@ public final class FeatureHooks { } public static Set getSentChunkKeys(final ServerPlayer player) { - final LongSet keys = new LongOpenHashSet(); - player.getChunkTrackingView().forEach(pos -> keys.add(pos.longKey)); - return LongSets.unmodifiable(keys); + return LongSets.unmodifiable(player.moonrise$getChunkLoader().getSentChunksRaw().clone()); // Paper - rewrite chunk system } public static Set getSentChunks(final ServerPlayer player) { - final ObjectSet chunks = new ObjectOpenHashSet<>(); + // Paper start - rewrite chunk system + final LongOpenHashSet rawChunkKeys = player.moonrise$getChunkLoader().getSentChunksRaw(); + final ObjectSet chunks = new ObjectOpenHashSet<>(rawChunkKeys.size()); final World world = player.serverLevel().getWorld(); - player.getChunkTrackingView().forEach(pos -> { - final org.bukkit.Chunk chunk = world.getChunkAt(pos.longKey); - chunks.add(chunk); - }); + final LongIterator iter = player.moonrise$getChunkLoader().getSentChunksRaw().longIterator(); + while (iter.hasNext()) { + chunks.add(world.getChunkAt(iter.nextLong(), false)); + } + // Paper end - rewrite chunk system return ObjectSets.unmodifiable(chunks); } @@ -79,89 +86,30 @@ public final class FeatureHooks { } public static boolean isSpiderCollidingWithWorldBorder(final Spider spider) { - return true; // ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isCollidingWithBorder(spider.level().getWorldBorder(), spider.getBoundingBox().inflate(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) + return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isCollidingWithBorder(spider.level().getWorldBorder(), spider.getBoundingBox().inflate(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)); // Paper - rewrite collision system } public static void dumpAllChunkLoadInfo(net.minecraft.server.MinecraftServer server, boolean isLongTimeout) { + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(server, isLongTimeout); // Paper - rewrite chunk system } private static void dumpEntity(final Entity entity) { } public static org.bukkit.entity.Entity[] getChunkEntities(net.minecraft.server.level.ServerLevel world, int chunkX, int chunkZ) { - world.getChunk(chunkX, chunkZ); // ensure full loaded - - net.minecraft.world.level.entity.PersistentEntitySectionManager entityManager = world.entityManager; - long pair = ChunkPos.asLong(chunkX, chunkZ); - - if (entityManager.areEntitiesLoaded(pair)) { - return entityManager.getEntities(new ChunkPos(chunkX, chunkZ)).stream() - .map(net.minecraft.world.entity.Entity::getBukkitEntity) - .filter(java.util.Objects::nonNull).toArray(org.bukkit.entity.Entity[]::new); - } - - entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading - - // SPIGOT-6772: Use entity mailbox and re-schedule entities if they get unloaded - net.minecraft.util.thread.ConsecutiveExecutor mailbox = ((net.minecraft.world.level.chunk.storage.EntityStorage) entityManager.permanentStorage).entityDeserializerQueue; - java.util.function.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(); - java.util.concurrent.locks.LockSupport.parkNanos("waiting for entity loading", 100000L); - } - } - - return entityManager.getEntities(new ChunkPos(chunkX, chunkZ)).stream() - .map(net.minecraft.world.entity.Entity::getBukkitEntity) - .filter(java.util.Objects::nonNull).toArray(org.bukkit.entity.Entity[]::new); + return world.getChunkEntities(chunkX, chunkZ); // Paper - rewrite chunk system } public static java.util.Collection getPluginChunkTickets(net.minecraft.server.level.ServerLevel world, int x, int z) { - net.minecraft.server.level.DistanceManager chunkDistanceManager = world.getChunkSource().chunkMap.distanceManager; - net.minecraft.util.SortedArraySet> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z)); - - if (tickets == null) { - return java.util.Collections.emptyList(); - } - - com.google.common.collect.ImmutableList.Builder ret = com.google.common.collect.ImmutableList.builder(); - for (net.minecraft.server.level.Ticket ticket : tickets) { - if (ticket.getType() == net.minecraft.server.level.TicketType.PLUGIN_TICKET) { - ret.add((org.bukkit.plugin.Plugin) ticket.key); - } - } - - return ret.build(); + return world.moonrise$getChunkTaskScheduler().chunkHolderManager.getPluginChunkTickets(x, z); // Paper - rewrite chunk system } public static Map> getPluginChunkTickets(net.minecraft.server.level.ServerLevel world) { Map> ret = new HashMap<>(); net.minecraft.server.level.DistanceManager chunkDistanceManager = world.getChunkSource().chunkMap.distanceManager; - for (it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) { + for (it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry>> chunkTickets : chunkDistanceManager.moonrise$getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { // Paper - rewrite chunk system long chunkKey = chunkTickets.getLongKey(); net.minecraft.util.SortedArraySet> tickets = chunkTickets.getValue(); @@ -183,15 +131,15 @@ public final class FeatureHooks { } public static int getViewDistance(net.minecraft.server.level.ServerLevel world) { - return world.getChunkSource().chunkMap.serverViewDistance; + return world.moonrise$getPlayerChunkLoader().getAPIViewDistance(); // Paper - rewrite chunk system } public static int getSimulationDistance(net.minecraft.server.level.ServerLevel world) { - return world.getChunkSource().chunkMap.getDistanceManager().simulationDistance; + return world.moonrise$getPlayerChunkLoader().getAPITickDistance(); // Paper - rewrite chunk system } public static int getSendViewDistance(net.minecraft.server.level.ServerLevel world) { - return getViewDistance(world); + return world.moonrise$getPlayerChunkLoader().getAPISendViewDistance(); // Paper - rewrite chunk system } public static void setViewDistance(net.minecraft.server.level.ServerLevel world, int distance) { @@ -209,31 +157,31 @@ public final class FeatureHooks { } public static void setSendViewDistance(net.minecraft.server.level.ServerLevel world, int distance) { - throw new UnsupportedOperationException("Not implemented yet"); + world.chunkSource.setSendViewDistance(distance); // Paper - rewrite chunk system } public static void tickEntityManager(net.minecraft.server.level.ServerLevel world) { - world.entityManager.tick(); + // Paper - rewrite chunk system } public static void closeEntityManager(net.minecraft.server.level.ServerLevel world, boolean save) { - world.entityManager.close(save); + // Paper - rewrite chunk system } public static java.util.concurrent.Executor getWorldgenExecutor() { - return net.minecraft.Util.backgroundExecutor(); + return Runnable::run; // Paper - rewrite chunk system } public static void setViewDistance(ServerPlayer player, int distance) { - throw new UnsupportedOperationException("Not implemented yet"); + ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)player).moonrise$getViewDistanceHolder().setLoadViewDistance(distance == -1 ? distance : distance + 1); // Paper - rewrite chunk system } public static void setSimulationDistance(ServerPlayer player, int distance) { - throw new UnsupportedOperationException("Not implemented yet"); + ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)player).moonrise$getViewDistanceHolder().setTickViewDistance(distance); // Paper - rewrite chunk system } public static void setSendViewDistance(ServerPlayer player, int distance) { - throw new UnsupportedOperationException("Not implemented yet"); + ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)player).moonrise$getViewDistanceHolder().setSendViewDistance(distance); // Paper - rewrite chunk system } } \ No newline at end of file diff --git a/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/io/papermc/paper/command/subcommands/ChunkDebugCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd01fba0fc --- /dev/null +++ b/io/papermc/paper/command/subcommands/ChunkDebugCommand.java @@ -0,0 +1,277 @@ +package io.papermc.paper.command.subcommands; + +import ca.spottedleaf.moonrise.common.util.JsonUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import io.papermc.paper.command.CommandUtil; +import io.papermc.paper.command.PaperSubcommand; +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ImposterProtoChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.ProtoChunk; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.craftbukkit.CraftWorld; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.BLUE; +import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA; +import static net.kyori.adventure.text.format.NamedTextColor.GREEN; +import static net.kyori.adventure.text.format.NamedTextColor.RED; + +@DefaultQualifier(NonNull.class) +public final class ChunkDebugCommand implements PaperSubcommand { + @Override + public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { + switch (subCommand) { + case "debug" -> this.doDebug(sender, args); + case "chunkinfo" -> this.doChunkInfo(sender, args); + case "holderinfo" -> this.doHolderInfo(sender, args); + } + return true; + } + + @Override + public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { + switch (subCommand) { + case "debug" -> { + if (args.length == 1) { + return CommandUtil.getListMatchingLast(sender, args, "help", "chunks"); + } + } + case "holderinfo" -> { + List worldNames = new ArrayList<>(); + worldNames.add("*"); + for (org.bukkit.World world : Bukkit.getWorlds()) { + worldNames.add(world.getName()); + } + if (args.length == 1) { + return CommandUtil.getListMatchingLast(sender, args, worldNames); + } + } + case "chunkinfo" -> { + List worldNames = new ArrayList<>(); + worldNames.add("*"); + for (org.bukkit.World world : Bukkit.getWorlds()) { + worldNames.add(world.getName()); + } + if (args.length == 1) { + return CommandUtil.getListMatchingLast(sender, args, worldNames); + } + } + } + return Collections.emptyList(); + } + + private void doChunkInfo(final CommandSender sender, final String[] args) { + List worlds; + if (args.length < 1 || args[0].equals("*")) { + worlds = Bukkit.getWorlds(); + } else { + worlds = new ArrayList<>(args.length); + for (final String arg : args) { + org.bukkit.@Nullable World world = Bukkit.getWorld(arg); + if (world == null) { + sender.sendMessage(text("World '" + arg + "' is invalid", RED)); + return; + } + worlds.add(world); + } + } + + int accumulatedTotal = 0; + int accumulatedInactive = 0; + int accumulatedBorder = 0; + int accumulatedTicking = 0; + int accumulatedEntityTicking = 0; + + for (final org.bukkit.World bukkitWorld : worlds) { + final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); + + int total = 0; + int inactive = 0; + int full = 0; + int blockTicking = 0; + int entityTicking = 0; + + for (final NewChunkHolder holder : ((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders()) { + final NewChunkHolder.ChunkCompletion completion = holder.getLastChunkCompletion(); + final ChunkAccess chunk = completion == null ? null : completion.chunk(); + + if (!(chunk instanceof LevelChunk fullChunk)) { + continue; + } + + ++total; + + switch (holder.getChunkStatus()) { + case INACCESSIBLE: { + ++inactive; + break; + } + case FULL: { + ++full; + break; + } + case BLOCK_TICKING: { + ++blockTicking; + break; + } + case ENTITY_TICKING: { + ++entityTicking; + break; + } + } + } + + accumulatedTotal += total; + accumulatedInactive += inactive; + accumulatedBorder += full; + accumulatedTicking += blockTicking; + accumulatedEntityTicking += entityTicking; + + sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":"))); + sender.sendMessage(text().color(DARK_AQUA).append( + text("Total: ", BLUE), text(total), + text(" Inactive: ", BLUE), text(inactive), + text(" Full: ", BLUE), text(full), + text(" Block Ticking: ", BLUE), text(blockTicking), + text(" Entity Ticking: ", BLUE), text(entityTicking) + )); + } + if (worlds.size() > 1) { + sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA))); + sender.sendMessage(text().color(DARK_AQUA).append( + text("Total: ", BLUE), text(accumulatedTotal), + text(" Inactive: ", BLUE), text(accumulatedInactive), + text(" Full: ", BLUE), text(accumulatedBorder), + text(" Block Ticking: ", BLUE), text(accumulatedTicking), + text(" Entity Ticking: ", BLUE), text(accumulatedEntityTicking) + )); + } + } + + private void doHolderInfo(final CommandSender sender, final String[] args) { + List worlds; + if (args.length < 1 || args[0].equals("*")) { + worlds = Bukkit.getWorlds(); + } else { + worlds = new ArrayList<>(args.length); + for (final String arg : args) { + org.bukkit.@Nullable World world = Bukkit.getWorld(arg); + if (world == null) { + sender.sendMessage(text("World '" + arg + "' is invalid", RED)); + return; + } + worlds.add(world); + } + } + + int accumulatedTotal = 0; + int accumulatedCanUnload = 0; + int accumulatedNull = 0; + int accumulatedReadOnly = 0; + int accumulatedProtoChunk = 0; + int accumulatedFullChunk = 0; + + for (final org.bukkit.World bukkitWorld : worlds) { + final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); + + int total = 0; + int canUnload = 0; + int nullChunks = 0; + int readOnly = 0; + int protoChunk = 0; + int fullChunk = 0; + + for (final NewChunkHolder holder : ((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders()) { + final NewChunkHolder.ChunkCompletion completion = holder.getLastChunkCompletion(); + final ChunkAccess chunk = completion == null ? null : completion.chunk(); + + ++total; + + if (chunk == null) { + ++nullChunks; + } else if (chunk instanceof ImposterProtoChunk) { + ++readOnly; + } else if (chunk instanceof ProtoChunk) { + ++protoChunk; + } else if (chunk instanceof LevelChunk) { + ++fullChunk; + } + + if (holder.isSafeToUnload() == null) { + ++canUnload; + } + } + + accumulatedTotal += total; + accumulatedCanUnload += canUnload; + accumulatedNull += nullChunks; + accumulatedReadOnly += readOnly; + accumulatedProtoChunk += protoChunk; + accumulatedFullChunk += fullChunk; + + sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":"))); + sender.sendMessage(text().color(DARK_AQUA).append( + text("Total: ", BLUE), text(total), + text(" Unloadable: ", BLUE), text(canUnload), + text(" Null: ", BLUE), text(nullChunks), + text(" ReadOnly: ", BLUE), text(readOnly), + text(" Proto: ", BLUE), text(protoChunk), + text(" Full: ", BLUE), text(fullChunk) + )); + } + if (worlds.size() > 1) { + sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA))); + sender.sendMessage(text().color(DARK_AQUA).append( + text("Total: ", BLUE), text(accumulatedTotal), + text(" Unloadable: ", BLUE), text(accumulatedCanUnload), + text(" Null: ", BLUE), text(accumulatedNull), + text(" ReadOnly: ", BLUE), text(accumulatedReadOnly), + text(" Proto: ", BLUE), text(accumulatedProtoChunk), + text(" Full: ", BLUE), text(accumulatedFullChunk) + )); + } + } + + private void doDebug(final CommandSender sender, final String[] args) { + if (args.length < 1) { + sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED)); + return; + } + + final String debugType = args[0].toLowerCase(Locale.ROOT); + switch (debugType) { + case "chunks" -> { + if (args.length >= 2 && args[1].toLowerCase(Locale.ROOT).equals("help")) { + sender.sendMessage(text("Use /paper debug chunks to dump loaded chunk information to a file", RED)); + break; + } + final File file = ChunkTaskScheduler.getChunkDebugFile(); + sender.sendMessage(text("Writing chunk information dump to " + file, GREEN)); + try { + JsonUtil.writeJson(ChunkTaskScheduler.debugAllWorlds(MinecraftServer.getServer()), file); + sender.sendMessage(text("Successfully written chunk information!", GREEN)); + } catch (Throwable thr) { + MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr); + sender.sendMessage(text("Failed to dump chunk information, see console", RED)); + } + } + // "help" & default + default -> sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED)); + } + } + +} diff --git a/io/papermc/paper/command/subcommands/FixLightCommand.java b/io/papermc/paper/command/subcommands/FixLightCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..85950a1aa732ab8c01ad28bec9e0de140e1a172e --- /dev/null +++ b/io/papermc/paper/command/subcommands/FixLightCommand.java @@ -0,0 +1,116 @@ +package io.papermc.paper.command.subcommands; + +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider; +import io.papermc.paper.command.PaperSubcommand; +import io.papermc.paper.util.MCUtil; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ThreadedLevelLightEngine; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import org.bukkit.command.CommandSender; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; + +import java.text.DecimalFormat; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.BLUE; +import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA; +import static net.kyori.adventure.text.format.NamedTextColor.RED; + +@DefaultQualifier(NonNull.class) +public final class FixLightCommand implements PaperSubcommand { + + private static final ThreadLocal ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { + return new DecimalFormat("#,##0.0"); + }); + + @Override + public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { + this.doFixLight(sender, args); + return true; + } + + private void doFixLight(final CommandSender sender, final String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage(text("Only players can use this command", RED)); + return; + } + @Nullable Runnable post = null; + int radius = 2; + if (args.length > 0) { + try { + final int parsed = Integer.parseInt(args[0]); + if (parsed < 0) { + sender.sendMessage(text("Radius cannot be negative!", RED)); + return; + } + final int maxRadius = 32; + radius = Math.min(maxRadius, parsed); + if (radius != parsed) { + post = () -> sender.sendMessage(text("Radius '" + parsed + "' was not in the required range [0, " + maxRadius + "], it was lowered to the maximum (" + maxRadius + " chunks).", RED)); + } + } catch (final Exception e) { + sender.sendMessage(text("'" + args[0] + "' is not a valid number.", RED)); + return; + } + } + + CraftPlayer player = (CraftPlayer) sender; + ServerPlayer handle = player.getHandle(); + ServerLevel world = (ServerLevel) handle.level(); + ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine(); + this.starlightFixLight(handle, world, lightengine, radius, post); + } + + private void starlightFixLight( + final ServerPlayer sender, + final ServerLevel world, + final ThreadedLevelLightEngine lightengine, + final int radius, + final @Nullable Runnable done + ) { + final long start = System.nanoTime(); + final java.util.LinkedHashSet chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos + + final int[] pending = new int[1]; + for (java.util.Iterator iterator = chunks.iterator(); iterator.hasNext(); ) { + final ChunkPos chunkPos = iterator.next(); + + final @Nullable ChunkAccess chunk = (ChunkAccess) world.getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z); + if (chunk == null || !chunk.isLightCorrect() || !chunk.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) { + // cannot relight this chunk + iterator.remove(); + continue; + } + + ++pending[0]; + } + + final int[] relitChunks = new int[1]; + ((StarLightLightingProvider)lightengine).starlight$serverRelightChunks(chunks, + (final ChunkPos chunkPos) -> { + ++relitChunks[0]; + sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append( + text("Relit chunk ", BLUE), text(chunkPos.toString()), + text(", progress: ", BLUE), text(ONE_DECIMAL_PLACES.get().format(100.0 * (double) (relitChunks[0]) / (double) pending[0]) + "%") + )); + }, + (final int totalRelit) -> { + final long end = System.nanoTime(); + sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append( + text("Relit ", BLUE), text(totalRelit), + text(" chunks. Took ", BLUE), text(ONE_DECIMAL_PLACES.get().format(1.0e-6 * (end - start)) + "ms") + )); + if (done != null) { + done.run(); + } + } + ); + sender.getBukkitEntity().sendMessage(text().color(BLUE).append(text("Relighting "), text(pending[0], DARK_AQUA), text(" chunks"))); + } +} diff --git a/io/papermc/paper/threadedregions/TickRegions.java b/io/papermc/paper/threadedregions/TickRegions.java new file mode 100644 index 0000000000000000000000000000000000000000..8424cf9d4617b4732d44cc460d25b04481068989 --- /dev/null +++ b/io/papermc/paper/threadedregions/TickRegions.java @@ -0,0 +1,10 @@ +package io.papermc.paper.threadedregions; + +// placeholder class for Folia +public class TickRegions { + + public static int getRegionChunkShift() { + return ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ThreadedTicketLevelPropagator.SECTION_SHIFT; + } + +} diff --git a/net/minecraft/core/Direction.java b/net/minecraft/core/Direction.java index 3d3eec1db91cb47395f40c4f47aa77164ad42175..216f97207dac88cc1dc3df59c6ee8a62c7614b4a 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 data3d, @@ -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 47b1fafd91b39e73c4e9134b0b8048000fba108a..76994c1491221c06cca5405ba239e6ff642b19ed 100644 --- a/net/minecraft/core/MappedRegistry.java +++ b/net/minecraft/core/MappedRegistry.java @@ -50,6 +50,19 @@ public class MappedRegistry implements WritableRegistry { return this.getTags(); } + // Paper start - fluid method optimisations + private void injectFluidRegister( + final ResourceKey resourceKey, + final T object + ) { + if (resourceKey.registryKey() == (Object)net.minecraft.core.registries.Registries.FLUID) { + for (final net.minecraft.world.level.material.FluidState possibleState : ((net.minecraft.world.level.material.Fluid)object).getStateDefinition().getPossibleStates()) { + ((ca.spottedleaf.moonrise.patches.fluid.FluidFluidState)(Object)possibleState).moonrise$initCaches(); + } + } + } + // Paper end - fluid method optimisations + public MappedRegistry(ResourceKey> key, Lifecycle registryLifecycle) { this(key, registryLifecycle, false); } @@ -114,6 +127,7 @@ public class MappedRegistry implements WritableRegistry { this.toId.put(value, size); this.registrationInfos.put(key, registrationInfo); this.registryLifecycle = this.registryLifecycle.add(registrationInfo.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 47c62090b421ebea1253ee3f1c896ed84119cea6..e738405e5112584e02e01df2d5ede2676fa1bffb 100644 --- a/net/minecraft/server/Main.java +++ b/net/minecraft/server/Main.java @@ -320,6 +320,7 @@ public class Main { WorldData worldData = worldStem.worldData(); levelStorageAccess.saveDataTag(frozen, worldData); */ + Class.forName(net.minecraft.world.entity.npc.VillagerTrades.class.getName()); // Paper - load this sync so it won't fail later async final DedicatedServer dedicatedServer = MinecraftServer.spin( thread1 -> { DedicatedServer dedicatedServer1 = new DedicatedServer( diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java index 768d287be8040797130499b021b2cfd3970f44e5..9c5305b4542483efeeeab19a79eb8eae6d15ef2c 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java @@ -173,7 +173,7 @@ import net.minecraft.world.phys.Vec2; import net.minecraft.world.phys.Vec3; import org.slf4j.Logger; -public abstract class MinecraftServer extends ReentrantBlockableEventLoop implements ServerInfo, ChunkIOErrorReporter, CommandSource { +public abstract class MinecraftServer extends ReentrantBlockableEventLoop implements ServerInfo, ChunkIOErrorReporter, CommandSource, ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer { // Paper - rewrite chunk system private static MinecraftServer SERVER; // Paper public static final Logger LOGGER = LogUtils.getLogger(); public static final net.kyori.adventure.text.logger.slf4j.ComponentLogger COMPONENT_LOGGER = net.kyori.adventure.text.logger.slf4j.ComponentLogger.logger(LOGGER.getName()); // Paper @@ -318,6 +318,77 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= MAX_CHUNK_EXEC_TIME) { + if (!moreTasks) { + this.lastMidTickExecuteFailure = currTime; + } + + // note: negative values reduce the time + long overuse = diff - MAX_CHUNK_EXEC_TIME; + if (overuse >= (10L * 1000L * 1000L)) { // 10ms + // make sure something like a GC or dumb plugin doesn't screw us over... + overuse = 10L * 1000L * 1000L; // 10ms + } + + final double overuseCount = (double)overuse/(double)MAX_CHUNK_EXEC_TIME; + final long extraSleep = (long)Math.round(overuseCount*CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME); + + this.lastMidTickExecute = currTime + extraSleep; + return; + } + } + } + // Paper end - rewrite chunk system + public MinecraftServer( // CraftBukkit start joptsimple.OptionSet options, @@ -648,7 +719,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop level.getChunkSource().chunkMap.hasWork())) { + while (false && this.levels.values().stream().anyMatch(level -> level.getChunkSource().chunkMap.hasWork())) { // Paper - rewrite chunk system this.nextTickTimeNanos = Util.getNanos() + TimeUtil.NANOSECONDS_PER_MILLISECOND; for (ServerLevel serverLevelx : this.getAllLevels()) { @@ -956,17 +1032,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 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(); profilerFiller.popPush("nextTickWait"); this.mayHaveDelayedTasks = true; @@ -1322,6 +1403,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + LOGGER.info("Async debug chunks executing"); + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(this, false); + org.bukkit.command.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(msg, source)); // Paper - Perf: use proper queue } diff --git a/net/minecraft/server/level/ChunkHolder.java b/net/minecraft/server/level/ChunkHolder.java index b95de132c53f82d270de396787dda3be8bc6c910..656041c9539b6834b4d37b353eb6b810a7763ff4 100644 --- a/net/minecraft/server/level/ChunkHolder.java +++ b/net/minecraft/server/level/ChunkHolder.java @@ -29,27 +29,112 @@ import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.lighting.LevelLightEngine; -public class ChunkHolder extends GenerationChunkHolder { +public class ChunkHolder extends GenerationChunkHolder implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder { // Paper - rewrite chunk system public static final ChunkResult UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk"); private static final CompletableFuture> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(UNLOADED_LEVEL_CHUNK); private final LevelHeightAccessor levelHeightAccessor; - private volatile CompletableFuture> fullChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage - private volatile CompletableFuture> tickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage - private volatile CompletableFuture> entityTickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; 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 = new BitSet(); private final BitSet skyChangedLightSectionFilter = new BitSet(); private final LevelLightEngine lightEngine; - private final ChunkHolder.LevelChangeListener onLevelChange; + // Paper - rewrite chunk system public final ChunkHolder.PlayerProvider playerProvider; - private boolean wasAccessibleSinceLastSave; - private CompletableFuture pendingFullStateConfirmation = CompletableFuture.completedFuture(null); - private CompletableFuture sendSync = CompletableFuture.completedFuture(null); - private CompletableFuture saveSync = CompletableFuture.completedFuture(null); + // Paper - rewrite chunk system + + // Paper start - rewrite chunk system + private ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder; + + private static final ServerPlayer[] EMPTY_PLAYER_ARRAY = new ServerPlayer[0]; + private final ca.spottedleaf.moonrise.common.list.ReferenceList playersSentChunkTo = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_ARRAY); + + private ChunkMap getChunkMap() { + return (ChunkMap)this.playerProvider; + } + + @Override + public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder moonrise$getRealChunkHolder() { + return this.newChunkHolder; + } + + @Override + public final void moonrise$setRealChunkHolder(final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder) { + this.newChunkHolder = newChunkHolder; + } + + @Override + public final void moonrise$addReceivedChunk(final ServerPlayer player) { + if (!this.playersSentChunkTo.add(player)) { + throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player); + } + } + + @Override + public final void moonrise$removeReceivedChunk(final ServerPlayer player) { + if (!this.playersSentChunkTo.remove(player)) { + throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player); + } + } + + @Override + public final boolean moonrise$hasChunkBeenSent() { + return this.playersSentChunkTo.size() != 0; + } + + @Override + public final boolean moonrise$hasChunkBeenSent(final ServerPlayer to) { + return this.playersSentChunkTo.contains(to); + } + + @Override + public final List moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge) { + final List ret = new java.util.ArrayList<>(); + final ServerPlayer[] raw = this.playersSentChunkTo.getRawDataUnchecked(); + for (int i = 0, len = this.playersSentChunkTo.size(); i < len; ++i) { + final ServerPlayer player = raw[i]; + if (onlyOnWatchDistanceEdge && !((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getPlayerChunkLoader().isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) { + continue; + } + ret.add(player); + } + + return ret; + } + + @Override + public final LevelChunk moonrise$getFullChunk() { + if (this.newChunkHolder.isFullChunkReady()) { + if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) { + return levelChunk; + } // else: race condition: chunk unload + } + return null; + } + + private boolean isRadiusLoaded(final int radius) { + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getChunkTaskScheduler() + .chunkHolderManager; + final ChunkPos pos = this.pos; + final int chunkX = pos.x; + final int chunkZ = pos.z; + for (int dz = -radius; dz <= radius; ++dz) { + for (int dx = -radius; dx <= radius; ++dx) { + if ((dx | dz) == 0) { + continue; + } + + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = manager.getChunkHolder(dx + chunkX, dz + chunkZ); + + if (holder == null || !holder.isFullChunkReady()) { + return false; + } + } + } + + return true; + } + // Paper end - rewrite chunk system public ChunkHolder( ChunkPos pos, @@ -62,11 +147,9 @@ public class ChunkHolder extends GenerationChunkHolder { super(pos); this.levelHeightAccessor = levelHeightAccessor; this.lightEngine = lightEngine; - this.onLevelChange = onLevelChange; + // Paper - rewrite chunk system this.playerProvider = playerProvider; - this.oldTicketLevel = ChunkLevel.MAX_LEVEL + 1; - this.ticketLevel = this.oldTicketLevel; - this.queueLevel = this.oldTicketLevel; + // Paper - rewrite chunk system this.setTicketLevel(ticketLevel); this.changedBlocksPerSection = new ShortSet[levelHeightAccessor.getSectionsCount()]; } @@ -74,7 +157,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(); } @@ -84,58 +167,63 @@ public class ChunkHolder extends GenerationChunkHolder { // CraftBukkit end public CompletableFuture> getTickingChunkFuture() { - return this.tickingChunkFuture; + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } public CompletableFuture> getEntityTickingChunkFuture() { - return this.entityTickingChunkFuture; + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } public CompletableFuture> getFullChunkFuture() { - return this.fullChunkFuture; + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } @Nullable public final LevelChunk getTickingChunk() { // Paper - final for inline - return this.getTickingChunkFuture().getNow(UNLOADED_LEVEL_CHUNK).orElse(null); + // 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 dependency) { - if (this.sendSync.isDone()) { - this.sendSync = dependency; - } else { - this.sendSync = this.sendSync.thenCombine((CompletionStage)dependency, (object, object1) -> 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 dependency) { - if (this.saveSync.isDone()) { - this.saveSync = dependency; - } else { - this.saveSync = this.saveSync.thenCombine((CompletionStage)dependency, (object, object1) -> null); - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } public boolean blockChanged(BlockPos pos) { - LevelChunk tickingChunk = this.getTickingChunk(); + LevelChunk tickingChunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system if (tickingChunk == null) { return false; } else { @@ -158,7 +246,7 @@ public class ChunkHolder extends GenerationChunkHolder { return false; } else { chunkIfPresent.markUnsaved(); - LevelChunk tickingChunk = this.getTickingChunk(); + LevelChunk tickingChunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system if (tickingChunk == null) { return false; } else { @@ -188,7 +276,7 @@ public class ChunkHolder extends GenerationChunkHolder { if (this.hasChangesToBroadcast()) { Level level = chunk.getLevel(); if (!this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) { - List players = this.playerProvider.getPlayers(this.pos, true); + List players = this.moonrise$getPlayers(true); // Paper - rewrite chunk system if (!players.isEmpty()) { ClientboundLightUpdatePacket clientboundLightUpdatePacket = new ClientboundLightUpdatePacket( chunk.getPos(), this.lightEngine, this.skyChangedLightSectionFilter, this.blockChangedLightSectionFilter @@ -201,7 +289,7 @@ public class ChunkHolder extends GenerationChunkHolder { } if (this.hasChangedSections) { - List players = this.playerProvider.getPlayers(this.pos, false); + List players = this.moonrise$getPlayers(false); // Paper - rewrite chunk system for (int i = 0; i < this.changedBlocksPerSection.length; i++) { ShortSet set = this.changedBlocksPerSection[i]; @@ -256,193 +344,50 @@ 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 queueLevel) { - this.queueLevel = queueLevel; + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } public void setTicketLevel(int level) { - this.ticketLevel = level; + // Paper - rewrite chunk system } private void scheduleFullChunkPromotion( ChunkMap chunkMap, CompletableFuture> future, Executor executor, FullChunkStatus fullChunkStatus ) { - this.pendingFullStateConfirmation.cancel(false); - CompletableFuture completableFuture = new CompletableFuture<>(); - completableFuture.thenRunAsync(() -> chunkMap.onFullChunkStatusChange(this.pos, fullChunkStatus), executor); - this.pendingFullStateConfirmation = completableFuture; - future.thenAccept(chunkResult -> chunkResult.ifSuccess(levelChunk -> completableFuture.complete(null))); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } private void demoteFullChunk(ChunkMap chunkMap, FullChunkStatus fullChunkStatus) { - this.pendingFullStateConfirmation.cancel(false); - chunkMap.onFullChunkStatusChange(this.pos, fullChunkStatus); + 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 chunkMap) { - 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 = either.orElse(null); - if (chunk != null) { - chunkMap.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 - net.minecraft.server.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 - chunkMap.callbackExecutor.run(); - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } // CraftBukkit end protected void updateFutures(ChunkMap chunkMap, Executor executor) { - FullChunkStatus fullChunkStatus = ChunkLevel.fullStatus(this.oldTicketLevel); - FullChunkStatus fullChunkStatus1 = ChunkLevel.fullStatus(this.ticketLevel); - boolean isOrAfter = fullChunkStatus.isOrAfter(FullChunkStatus.FULL); - boolean isOrAfter1 = fullChunkStatus1.isOrAfter(FullChunkStatus.FULL); - this.wasAccessibleSinceLastSave |= isOrAfter1; - if (!isOrAfter && isOrAfter1) { - int expectCreateCount = ++this.fullChunkCreateCount; // Paper - this.fullChunkFuture = chunkMap.prepareAccessibleChunk(this); - this.scheduleFullChunkPromotion(chunkMap, 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.PlatformHooks.get().onChunkBorder(chunk, this); - } - }); - }); - // Paper end - cache ticking ready status - this.addSaveDependency(this.fullChunkFuture); - } - - if (isOrAfter && !isOrAfter1) { - // Paper start - if (this.isFullChunkReady) { - ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper - } - // Paper end - this.fullChunkFuture.complete(UNLOADED_LEVEL_CHUNK); - this.fullChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; - } - - boolean isOrAfter2 = fullChunkStatus.isOrAfter(FullChunkStatus.BLOCK_TICKING); - boolean isOrAfter3 = fullChunkStatus1.isOrAfter(FullChunkStatus.BLOCK_TICKING); - if (!isOrAfter2 && isOrAfter3) { - this.tickingChunkFuture = chunkMap.prepareTickingChunk(this); - this.scheduleFullChunkPromotion(chunkMap, 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.PlatformHooks.get().onChunkTicking(chunk, this); - }); - }); - // Paper end - this.addSaveDependency(this.tickingChunkFuture); - } - - if (isOrAfter2 && !isOrAfter3) { - // Paper start - if (this.isTickingReady) { - 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 - this.tickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; - } - - boolean isOrAfter4 = fullChunkStatus.isOrAfter(FullChunkStatus.ENTITY_TICKING); - boolean isOrAfter5 = fullChunkStatus1.isOrAfter(FullChunkStatus.ENTITY_TICKING); - if (!isOrAfter4 && isOrAfter5) { - if (this.entityTickingChunkFuture != UNLOADED_LEVEL_CHUNK_FUTURE) { - throw (IllegalStateException)Util.pauseInIde(new IllegalStateException()); - } - - this.entityTickingChunkFuture = chunkMap.prepareEntityTickingChunk(this); - this.scheduleFullChunkPromotion(chunkMap, 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.PlatformHooks.get().onChunkEntityTicking(chunk, this); - }); - }); - // Paper end - this.addSaveDependency(this.entityTickingChunkFuture); - } - - if (isOrAfter4 && !isOrAfter5) { - // Paper start - if (this.isEntityTickingReady) { - 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 - this.entityTickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; - } - - if (!fullChunkStatus1.isOrAfter(fullChunkStatus)) { - this.demoteFullChunk(chunkMap, 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) { - chunkMap.callbackExecutor.execute(() -> { - chunk.loadCallback(); - }); - } - }).exceptionally((throwable) -> { - // ensure exceptions are printed, by default this is not the case - net.minecraft.server.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 - chunkMap.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 e823b8aac00158892538083bc877ccf99895909a..7d871318065f19540748363809de82652613e733 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 ad665c7535c615d2b03a3e7864be435f933235dd..3dff97f13586be3b52bbe786852c185f6753a019 100644 --- a/net/minecraft/server/level/ChunkMap.java +++ b/net/minecraft/server/level/ChunkMap.java @@ -96,7 +96,7 @@ import net.minecraft.world.level.storage.LevelStorageSource; import org.apache.commons.lang3.mutable.MutableBoolean; import org.slf4j.Logger; -public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider, GeneratingChunkMap { +public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider, GeneratingChunkMap, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemChunkMap { // Paper - rewrite chunk system private static final ChunkResult> UNLOADED_CHUNK_LIST_RESULT = ChunkResult.error("Unloaded chunks found in range"); private static final CompletableFuture>> UNLOADED_CHUNK_LIST_FUTURE = CompletableFuture.completedFuture( UNLOADED_CHUNK_LIST_RESULT @@ -112,10 +112,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider public static final int MIN_VIEW_DISTANCE = 2; public static final int MAX_VIEW_DISTANCE = 32; public static final int FORCED_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING); - public final Long2ObjectLinkedOpenHashMap updatingChunkMap = new Long2ObjectLinkedOpenHashMap<>(); - public volatile Long2ObjectLinkedOpenHashMap visibleChunkMap = this.updatingChunkMap.clone(); - private final Long2ObjectLinkedOpenHashMap pendingUnloads = new Long2ObjectLinkedOpenHashMap<>(); - private final List pendingGenerationTasks = new ArrayList<>(); + // Paper - rewrite chunk system public final ServerLevel level; private final ThreadedLevelLightEngine lightEngine; private final BlockableEventLoop mainThreadExecutor; @@ -125,22 +122,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider private final PoiManager poiManager; public final LongSet toDrop = new LongOpenHashSet(); 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.DistanceManager distanceManager; - private final AtomicInteger tickingGenerated = new AtomicInteger(); + public final AtomicInteger tickingGenerated = new AtomicInteger(); // Paper - public private final String storageName; private final PlayerMap playerMap = new PlayerMap(); public final Int2ObjectMap entityMap = new Int2ObjectOpenHashMap<>(); private final Long2ByteMap chunkTypeCache = new Long2ByteOpenHashMap(); - private final Long2LongMap nextChunkSaveTime = new Long2LongOpenHashMap(); - private final LongSet chunksToEagerlySave = new LongLinkedOpenHashSet(); - private final Queue unloadQueue = Queues.newConcurrentLinkedQueue(); - private final AtomicInteger activeChunkWrites = new AtomicInteger(); + // 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(); @@ -165,9 +158,16 @@ 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 level, @@ -213,10 +213,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.progressListener = progressListener; this.chunkStatusListener = chunkStatusListener; ConsecutiveExecutor consecutiveExecutor1 = new ConsecutiveExecutor(dispatcher, "light"); - this.worldgenTaskDispatcher = new ChunkTaskDispatcher(consecutiveExecutor, dispatcher); - this.lightTaskDispatcher = new ChunkTaskDispatcher(consecutiveExecutor1, dispatcher); + // Paper - rewrite chunk system this.lightEngine = new ThreadedLevelLightEngine( - lightChunk, this, this.level.dimensionType().hasSkyLight(), consecutiveExecutor1, this.lightTaskDispatcher + lightChunk, this, this.level.dimensionType().hasSkyLight(), consecutiveExecutor1, null // Paper - rewrite chunk system ); this.distanceManager = new ChunkMap.DistanceManager(dispatcher, mainThreadExecutor); this.overworldDataStorage = overworldDataStorage; @@ -230,11 +229,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider level ); this.setServerViewDistance(viewDistance); - this.worldGenContext = new WorldGenContext(level, generator, structureManager, this.lightEngine, mainThreadExecutor, this::setChunkUnsaved); + this.worldGenContext = new WorldGenContext(level, generator, structureManager, this.lightEngine, null, this::setChunkUnsaved); // Paper - rewrite chunk system } private void setChunkUnsaved(ChunkPos chunkPos) { - this.chunksToEagerlySave.add(chunkPos.toLong()); + // Paper - rewrite chunk system } // Paper start @@ -264,23 +263,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } boolean isChunkTracked(ServerPlayer player, int x, int z) { - return player.getChunkTrackingView().contains(x, z) && !player.connection.chunkSender.isPending(ChunkPos.asLong(x, z)); + return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, x, z); // Paper - rewrite chunk system } private boolean isChunkOnTrackedBorder(ServerPlayer player, int x, int z) { - if (!this.isChunkTracked(player, x, z)) { - return false; - } else { - for (int i = -1; i <= 1; i++) { - for (int i1 = -1; i1 <= 1; i1++) { - if ((i != 0 || i1 != 0) && !this.isChunkTracked(player, x + i, z + i1)) { - return true; - } - } - } - - return false; - } + return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, x, z, true); // Paper - rewrite chunk system } protected ThreadedLevelLightEngine getLightEngine() { @@ -289,21 +276,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider @Nullable protected ChunkHolder getUpdatingChunkIfPresent(long chunkPos) { - return this.updatingChunkMap.get(chunkPos); + // 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(chunkPos); + return holder == null ? null : holder.vanillaChunkHolder; + // Paper end - rewrite chunk system } @Nullable public ChunkHolder getVisibleChunkIfPresent(long chunkPos) { - return this.visibleChunkMap.get(chunkPos); + // 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(chunkPos); + return holder == null ? null : holder.vanillaChunkHolder; + // Paper end - rewrite chunk system } protected IntSupplier getChunkQueueLevel(long chunkPos) { - return () -> { - ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(chunkPos); - return visibleChunkIfPresent == null - ? ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1 - : Math.min(visibleChunkIfPresent.getQueueLevel(), ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1); - }; + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } public String getChunkDebugData(ChunkPos pos) { @@ -329,47 +317,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } private CompletableFuture>> getChunkRangeFuture(ChunkHolder chunkHolder, int range, IntFunction statusGetter) { - if (range == 0) { - ChunkStatus chunkStatus = statusGetter.apply(0); - return chunkHolder.scheduleChunkGenerationTask(chunkStatus, this).thenApply(chunkResult -> chunkResult.map(List::of)); - } else { - int squared = Mth.square(range * 2 + 1); - List>> list = new ArrayList<>(squared); - ChunkPos pos = chunkHolder.getPos(); - - for (int i = -range; i <= range; i++) { - for (int i1 = -range; i1 <= range; i1++) { - int max = Math.max(Math.abs(i1), Math.abs(i)); - long packedChunkPos = ChunkPos.asLong(pos.x + i1, pos.z + i); - ChunkHolder updatingChunkIfPresent = this.getUpdatingChunkIfPresent(packedChunkPos); - if (updatingChunkIfPresent == null) { - return UNLOADED_CHUNK_LIST_FUTURE; - } - - ChunkStatus chunkStatus1 = statusGetter.apply(max); - list.add(updatingChunkIfPresent.scheduleChunkGenerationTask(chunkStatus1, this)); - } - } - - return Util.sequence(list).thenApply(list1 -> { - List list2 = new ArrayList<>(list1.size()); - - for (ChunkResult chunkResult : list1) { - if (chunkResult == null) { - throw this.debugFuturesAndCreateReportedException(new IllegalStateException("At least one of the chunk futures were null"), "n/a"); - } - - ChunkAccess chunkAccess = chunkResult.orElse(null); - if (chunkAccess == null) { - return UNLOADED_CHUNK_LIST_RESULT; - } - - list2.add(chunkAccess); - } - - return ChunkResult.of(list2); - }); - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } public ReportedException debugFuturesAndCreateReportedException(IllegalStateException exception, String details) { @@ -401,95 +349,29 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } public CompletableFuture> prepareEntityTickingChunk(ChunkHolder chunk) { - return this.getChunkRangeFuture(chunk, 2, i -> ChunkStatus.FULL) - .thenApply(chunkResult -> chunkResult.map(list -> (LevelChunk)list.get(list.size() / 2))); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } @Nullable ChunkHolder updateChunkScheduling(long chunkPos, int newLevel, @Nullable ChunkHolder holder, int oldLevel) { - if (!ChunkLevel.isLoaded(oldLevel) && !ChunkLevel.isLoaded(newLevel)) { - return holder; - } else { - if (holder != null) { - holder.setTicketLevel(newLevel); - } - - if (holder != null) { - if (!ChunkLevel.isLoaded(newLevel)) { - this.toDrop.add(chunkPos); - } else { - this.toDrop.remove(chunkPos); - } - } - - if (ChunkLevel.isLoaded(newLevel) && holder == null) { - holder = this.pendingUnloads.remove(chunkPos); - if (holder != null) { - holder.setTicketLevel(newLevel); - } else { - holder = new ChunkHolder(new ChunkPos(chunkPos), newLevel, this.level, this.lightEngine, this::onLevelChange, this); - // Paper start - ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderCreate(this.level, holder); - // Paper end - } - - this.updatingChunkMap.put(chunkPos, holder); - this.modified = true; - } - - return holder; - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } private void onLevelChange(ChunkPos chunkPos, IntSupplier queueLevelGetter, int ticketLevel, IntConsumer queueLevelSetter) { - this.worldgenTaskDispatcher.onLevelChange(chunkPos, queueLevelGetter, ticketLevel, queueLevelSetter); - this.lightTaskDispatcher.onLevelChange(chunkPos, queueLevelGetter, ticketLevel, queueLevelSetter); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } @Override public void close() throws IOException { - try { - this.worldgenTaskDispatcher.close(); - this.lightTaskDispatcher.close(); - this.poiManager.close(); - } finally { - super.close(); - } + throw new UnsupportedOperationException("Use ServerChunkCache#close"); // Paper - rewrite chunk system } protected void saveAllChunks(boolean flush) { - if (flush) { - List list = ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level) // Paper - moonrise - //.values() // Paper - moonrise - .stream() - .filter(ChunkHolder::wasAccessibleSinceLastSave) - .peek(ChunkHolder::refreshAccessibility) - .toList(); - MutableBoolean mutableBoolean = new MutableBoolean(); - - do { - mutableBoolean.setFalse(); - list.stream() - .map(chunk -> { - this.mainThreadExecutor.managedBlock(chunk::isReadyForSaving); - return chunk.getLatestChunk(); - }) - .filter(chunk -> chunk instanceof ImposterProtoChunk || chunk instanceof LevelChunk) - .filter(this::save) - .forEach(chunk -> mutableBoolean.setTrue()); - } while (mutableBoolean.isTrue()); - - this.poiManager.flushAll(); - this.processUnloads(() -> true); - this.flushWorker(); - } else { - this.nextChunkSaveTime.clear(); - long millis = Util.getMillis(); - - for (ChunkHolder chunkHolder : ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level)) { // Paper - this.saveChunkIfNeeded(chunkHolder, millis); - } - } + // 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 } protected void tick(BooleanSupplier hasMoreTime) { @@ -505,130 +387,28 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } public boolean hasWork() { - return this.lightEngine.hasLightWork() - || !this.pendingUnloads.isEmpty() - || ca.spottedleaf.moonrise.common.PlatformHooks.get().hasAnyChunkHolders(this.level) // Paper - moonrise - || !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 hasMoreTime) { - for (LongIterator longIterator = this.toDrop.iterator(); longIterator.hasNext(); longIterator.remove()) { - long l = longIterator.nextLong(); - ChunkHolder chunkHolder = this.updatingChunkMap.get(l); - if (chunkHolder != null) { - this.updatingChunkMap.remove(l); - this.pendingUnloads.put(l, chunkHolder); - this.modified = true; - this.scheduleUnload(l, chunkHolder); - } - } - - int max = Math.max(0, this.unloadQueue.size() - 2000); - - Runnable runnable; - while ((max > 0 || hasMoreTime.getAsBoolean()) && (runnable = this.unloadQueue.poll()) != null) { - max--; - runnable.run(); - } - - this.saveChunksEagerly(hasMoreTime); + ((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 hasMoreTime) { - long millis = Util.getMillis(); - int i = 0; - LongIterator longIterator = this.chunksToEagerlySave.iterator(); - - while (i < 20 && this.activeChunkWrites.get() < 128 && hasMoreTime.getAsBoolean() && longIterator.hasNext()) { - long l = longIterator.nextLong(); - ChunkHolder chunkHolder = this.visibleChunkMap.get(l); - ChunkAccess chunkAccess = chunkHolder != null ? chunkHolder.getLatestChunk() : null; - if (chunkAccess == null || !chunkAccess.isUnsaved()) { - longIterator.remove(); - } else if (this.saveChunkIfNeeded(chunkHolder, millis)) { - i++; - longIterator.remove(); - } - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } private void scheduleUnload(long chunkPos, ChunkHolder chunkHolder) { - CompletableFuture saveSyncFuture = chunkHolder.getSaveSyncFuture(); - saveSyncFuture.thenRunAsync(() -> { - CompletableFuture saveSyncFuture1 = chunkHolder.getSaveSyncFuture(); - if (saveSyncFuture1 != saveSyncFuture) { - this.scheduleUnload(chunkPos, chunkHolder); - } else { - ChunkAccess latestChunk = chunkHolder.getLatestChunk(); - // Paper start - boolean removed; - if ((removed = this.pendingUnloads.remove(chunkPos, chunkHolder)) && latestChunk != null) { - ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderDelete(this.level, chunkHolder); - // Paper end - if (latestChunk instanceof LevelChunk levelChunk) { - levelChunk.setLoaded(false); - } - - this.save(latestChunk); - if (latestChunk instanceof LevelChunk levelChunk) { - this.level.unload(levelChunk); - } - - this.lightEngine.updateChunkStatus(latestChunk.getPos()); - this.lightEngine.tryScheduleUpdate(); - this.progressListener.onStatusChange(latestChunk.getPos(), null); - this.nextChunkSaveTime.remove(latestChunk.getPos().toLong()); - } else if (removed) { // Paper start - ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderDelete(this.level, chunkHolder); - } // Paper end - } - }, this.unloadQueue::add).whenComplete((_void, error) -> { - if (error != null) { - LOGGER.error("Failed to save chunk {}", chunkHolder.getPos(), error); - } - }); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } protected boolean promoteChunkMap() { - if (!this.modified) { - return false; - } else { - this.visibleChunkMap = this.updatingChunkMap.clone(); - this.modified = false; - return true; - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } private CompletableFuture scheduleChunkLoad(ChunkPos chunkPos) { - CompletableFuture> completableFuture = this.readChunk(chunkPos).thenApplyAsync(optional -> optional.map(tag -> { - SerializableChunkData serializableChunkData = SerializableChunkData.parse(this.level, this.level.registryAccess(), tag); - if (serializableChunkData == null) { - LOGGER.error("Chunk file at {} is missing level data, skipping", chunkPos); - } - - return serializableChunkData; - }), Util.backgroundExecutor().forName("parseChunk")); - CompletableFuture completableFuture1 = this.poiManager.prefetch(chunkPos); - return completableFuture.>thenCombine( - (CompletionStage)completableFuture1, (optional, object) -> optional - ) - .thenApplyAsync(optional -> { - Profiler.get().incrementCounter("chunkLoad"); - if (optional.isPresent()) { - ChunkAccess chunkAccess = optional.get().read(this.level, this.poiManager, this.storageInfo(), chunkPos); - this.markPosition(chunkPos, chunkAccess.getPersistedStatus().getChunkType()); - return chunkAccess; - } else { - return this.createEmptyChunk(chunkPos); - } - }, this.mainThreadExecutor) - .exceptionallyAsync(throwable -> this.handleChunkLoadFailure(throwable, chunkPos), this.mainThreadExecutor); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } private ChunkAccess handleChunkLoadFailure(Throwable exception, ChunkPos chunkPos) { @@ -666,108 +446,43 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider @Override public GenerationChunkHolder acquireGeneration(long chunkPos) { - ChunkHolder chunkHolder = this.updatingChunkMap.get(chunkPos); - chunkHolder.increaseGenerationRefCount(); - return chunkHolder; + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } @Override public void releaseGeneration(GenerationChunkHolder chunk) { - chunk.decreaseGenerationRefCount(); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } @Override public CompletableFuture applyStep(GenerationChunkHolder chunk, ChunkStep step, StaticCache2D cache) { - ChunkPos pos = chunk.getPos(); - if (step.targetStatus() == ChunkStatus.EMPTY) { - return this.scheduleChunkLoad(pos); - } else { - try { - GenerationChunkHolder generationChunkHolder = cache.get(pos.x, pos.z); - ChunkAccess chunkIfPresentUnchecked = generationChunkHolder.getChunkIfPresentUnchecked(step.targetStatus().getParent()); - if (chunkIfPresentUnchecked == null) { - throw new IllegalStateException("Parent chunk missing"); - } else { - CompletableFuture completableFuture = step.apply(this.worldGenContext, cache, chunkIfPresentUnchecked); - this.progressListener.onStatusChange(pos, step.targetStatus()); - return completableFuture; - } - } catch (Exception var8) { - var8.getStackTrace(); - CrashReport crashReport = CrashReport.forThrowable(var8, "Exception generating new chunk"); - CrashReportCategory crashReportCategory = crashReport.addCategory("Chunk to be generated"); - crashReportCategory.setDetail("Status being generated", () -> step.targetStatus().getName()); - crashReportCategory.setDetail("Location", String.format(Locale.ROOT, "%d,%d", pos.x, pos.z)); - crashReportCategory.setDetail("Position hash", ChunkPos.asLong(pos.x, pos.z)); - crashReportCategory.setDetail("Generator", 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 targetStatus, ChunkPos pos) { - ChunkGenerationTask chunkGenerationTask = ChunkGenerationTask.create(this, targetStatus, pos); - this.pendingGenerationTasks.add(chunkGenerationTask); - return chunkGenerationTask; + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } private void runGenerationTask(ChunkGenerationTask task) { - GenerationChunkHolder center = task.getCenter(); - this.worldgenTaskDispatcher.submit(() -> { - CompletableFuture completableFuture = task.runUntilWait(); - if (completableFuture != null) { - completableFuture.thenRun(() -> this.runGenerationTask(task)); - } - }, center.getPos().toLong(), center::getQueueLevel); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } @Override public void runGenerationTasks() { - this.pendingGenerationTasks.forEach(this::runGenerationTask); - this.pendingGenerationTasks.clear(); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } public CompletableFuture> prepareTickingChunk(ChunkHolder holder) { - CompletableFuture>> chunkRangeFuture = this.getChunkRangeFuture(holder, 1, i -> ChunkStatus.FULL); - CompletableFuture> completableFuture = chunkRangeFuture.thenApplyAsync(chunk -> chunk.map(list -> { - LevelChunk levelChunk = (LevelChunk)list.get(list.size() / 2); - levelChunk.postProcessGeneration(this.level); - this.level.startTickingChunk(levelChunk); - CompletableFuture sendSyncFuture = holder.getSendSyncFuture(); - if (sendSyncFuture.isDone()) { - this.onChunkReadyToSend(holder, levelChunk); - } else { - sendSyncFuture.thenAcceptAsync(object -> this.onChunkReadyToSend(holder, levelChunk), this.mainThreadExecutor); - } - - return levelChunk; - }), this.mainThreadExecutor); - completableFuture.handle((chunk, exception) -> { - this.tickingGenerated.getAndIncrement(); - return null; - }); - return completableFuture; + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } private void onChunkReadyToSend(ChunkHolder chunkHolder, LevelChunk chunk) { - ChunkPos pos = chunk.getPos(); - - for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) { - if (serverPlayer.getChunkTrackingView().contains(pos)) { - markChunkPendingToSend(serverPlayer, chunk); - } - } - - this.level.getChunkSource().onChunkReadyToSend(chunkHolder); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } public CompletableFuture> prepareAccessibleChunk(ChunkHolder chunk) { - return this.getChunkRangeFuture(chunk, 1, ChunkLevel::getStatusAroundFullChunk) - .thenApply(chunkResult -> chunkResult.map(list -> (LevelChunk)list.get(list.size() / 2))); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } public int getTickingGenerated() { @@ -775,125 +490,78 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } private boolean saveChunkIfNeeded(ChunkHolder chunk, long gametime) { - if (chunk.wasAccessibleSinceLastSave() && chunk.isReadyForSaving()) { - ChunkAccess latestChunk = chunk.getLatestChunk(); - if (!(latestChunk instanceof ImposterProtoChunk) && !(latestChunk instanceof LevelChunk)) { - return false; - } else if (!latestChunk.isUnsaved()) { - return false; - } else { - long packedChunkPos = latestChunk.getPos().toLong(); - long orDefault = this.nextChunkSaveTime.getOrDefault(packedChunkPos, -1L); - if (gametime < orDefault) { - return false; - } else { - boolean flag = this.save(latestChunk); - chunk.refreshAccessibility(); - if (flag) { - this.nextChunkSaveTime.put(packedChunkPos, gametime + 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 pos = chunk.getPos(); - - try { - ChunkStatus persistedStatus = chunk.getPersistedStatus(); - if (persistedStatus.getChunkType() != ChunkType.LEVELCHUNK) { - if (this.isExistingChunkFull(pos)) { - return false; - } - - if (persistedStatus == 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); - CompletableFuture completableFuture = CompletableFuture.supplyAsync(serializableChunkData::write, Util.backgroundExecutor()); - this.write(pos, completableFuture::join).handle((_void, exception1) -> { - if (exception1 != null) { - this.level.getServer().reportChunkSaveFailure(exception1, this.storageInfo(), pos); - } - - this.activeChunkWrites.decrementAndGet(); - return null; - }); - this.markPosition(pos, persistedStatus.getChunkType()); - return true; - } catch (Exception var6) { - this.level.getServer().reportChunkSaveFailure(var6, this.storageInfo(), pos); - return false; - } - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } private boolean isExistingChunkFull(ChunkPos chunkPos) { - byte b = this.chunkTypeCache.get(chunkPos.toLong()); - if (b != 0) { - return b == 1; - } else { - CompoundTag compoundTag; - try { - compoundTag = this.readChunk(chunkPos).join().orElse(null); - if (compoundTag == null) { - this.markPositionReplaceable(chunkPos); - return false; - } - } catch (Exception var5) { - LOGGER.error("Failed to read chunk {}", chunkPos, var5); - this.markPositionReplaceable(chunkPos); - return false; - } - - ChunkType chunkTypeFromTag = SerializableChunkData.getChunkTypeFromTag(compoundTag); - return this.markPosition(chunkPos, chunkTypeFromTag) == 1; - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } public void setServerViewDistance(int viewDistance) { - int i = Mth.clamp(viewDistance, 2, 32); - if (i != this.serverViewDistance) { - this.serverViewDistance = i; - this.distanceManager.updatePlayerTickets(this.serverViewDistance); - - for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) { - this.updateChunkTracking(serverPlayer); - } + // Paper start - rewrite chunk system + final int clamped = Mth.clamp(viewDistance, 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 } int getPlayerViewDistance(ServerPlayer player) { - return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance); + return ca.spottedleaf.moonrise.common.PlatformHooks.get().getSendViewDistance(player); // Paper - rewrite chunk system } private void markChunkPendingToSend(ServerPlayer player, ChunkPos chunkPos) { - LevelChunk chunkToSend = this.getChunkToSend(chunkPos.toLong()); - if (chunkToSend != null) { - markChunkPendingToSend(player, chunkToSend); - } + 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 chunkPos) { - player.connection.chunkSender.dropChunk(player, chunkPos); + // Paper - rewrite chunk system + } + + // Paper start - rewrite chunk system + @Override + public CompletableFuture> read(final ChunkPos pos) { + final CompletableFuture> ret = new CompletableFuture<>(); + + ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.loadDataAsync( + this.level, pos.x, pos.z, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, + (final CompoundTag data, final Throwable thr) -> { + if (thr != null) { + ret.completeExceptionally(thr); + } else { + ret.complete(Optional.ofNullable(data)); + } + }, false + ); + + return ret; + } + + @Override + public CompletableFuture write(final ChunkPos pos, final Supplier tag) { + ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.scheduleSave( + this.level, pos.x, pos.z, tag.get(), + ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionFileType.CHUNK_DATA + ); + return null; + } + + @Override + public void flushWorker() { + ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.flush(this.level); } + // Paper end - rewrite chunk system @Nullable public LevelChunk getChunkToSend(long chunkPos) { @@ -981,7 +649,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } // CraftBukkit start - private CompoundTag upgradeChunkTag(CompoundTag tag, ChunkPos pos) { + public CompoundTag upgradeChunkTag(CompoundTag tag, ChunkPos pos) { // Paper - public return this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, tag, this.generator().getTypeNameForDataFixer(), pos, this.level); // CraftBukkit end } @@ -991,7 +659,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider while (spawnCandidateChunks.hasNext()) { long l = spawnCandidateChunks.nextLong(); - ChunkHolder chunkHolder = this.visibleChunkMap.get(l); + ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(l); // Paper - rewrite chunk system if (chunkHolder != null && this.anyPlayerCloseEnoughForSpawningInternal(chunkHolder.getPos())) { action.accept(chunkHolder); } @@ -1004,7 +672,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } boolean anyPlayerCloseEnoughForSpawning(ChunkPos chunkPos, boolean reducedRange) { - return this.distanceManager.hasPlayersNearby(chunkPos.toLong()) && this.anyPlayerCloseEnoughForSpawningInternal(chunkPos, reducedRange); + return this.anyPlayerCloseEnoughForSpawningInternal(chunkPos, reducedRange); // Paper - chunk tick iteration optimisation // Spigot end } @@ -1016,7 +684,20 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos chunkPos, boolean reducedRange) { double blockRange; // Paper - use from event // Spigot end - for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) { + // Paper start - chunk tick iteration optimisation + final ca.spottedleaf.moonrise.common.list.ReferenceList players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers().getPlayers( + chunkPos, 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) { + final ServerPlayer serverPlayer = raw[i]; // Paper start - PlayerNaturallySpawnCreaturesEvent com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event; blockRange = 16384.0D; @@ -1032,26 +713,41 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } return false; + // Paper end - chunk tick iteration optimisation } public List getPlayersCloseForSpawning(ChunkPos chunkPos) { - long packedChunkPos = chunkPos.toLong(); - if (!this.distanceManager.hasPlayersNearby(packedChunkPos)) { - return List.of(); - } else { - Builder builder = ImmutableList.builder(); + // Paper start - chunk tick iteration optimisation + final ca.spottedleaf.moonrise.common.list.ReferenceList players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers().getPlayers( + chunkPos, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.SPAWN_RANGE + ); + if (players == null) { + return new ArrayList<>(); + } + + List ret = null; + + final ServerPlayer[] raw = players.getRawDataUnchecked(); + final int len = players.size(); - for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) { - if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos, 16384.0D)) { // Spigot - builder.add(serverPlayer); + Objects.checkFromIndexSize(0, len, raw.length); + for (int i = 0; i < len; ++i) { + final ServerPlayer player = raw[i]; + if (this.playerIsCloseEnoughForSpawning(player, chunkPos, 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 player, ChunkPos chunkPos, double range) { // Spigot + public boolean playerIsCloseEnoughForSpawning(ServerPlayer player, ChunkPos chunkPos, double range) { // Spigot // Paper - chunk tick iteration optimisation - public if (player.isSpectator()) { return false; } else { @@ -1072,18 +768,20 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.updatePlayerPos(player); if (!flag) { this.distanceManager.addPlayer(SectionPos.of(player), player); + ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$addPlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation } player.setChunkTrackingView(ChunkTrackingView.EMPTY); - this.updateChunkTracking(player); + ca.spottedleaf.moonrise.common.PlatformHooks.get().addPlayerToDistanceMaps(this.level, player); // Paper - rewrite chunk system } else { SectionPos lastSectionPos = player.getLastSectionPos(); this.playerMap.removePlayer(player); if (!flag1) { this.distanceManager.removePlayer(lastSectionPos, player); + ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$removePlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation } - this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY); + ca.spottedleaf.moonrise.common.PlatformHooks.get().removePlayerFromDistanceMaps(this.level, player); // Paper - rewrite chunk system } } @@ -1093,13 +791,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } public void move(ServerPlayer player) { - for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) { - if (trackedEntity.entity == player) { - trackedEntity.updatePlayers(this.level.players()); - } else { - trackedEntity.updatePlayer(player); - } - } + // Paper - optimise entity tracker SectionPos lastSectionPos = player.getLastSectionPos(); SectionPos sectionPos = SectionPos.of(player); @@ -1108,6 +800,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider boolean flag2 = lastSectionPos.asLong() != sectionPos.asLong(); if (flag2 || flag != flag1) { this.updatePlayerPos(player); + ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, lastSectionPos, sectionPos, flag, flag1); // Paper - chunk tick iteration optimisation if (!flag) { this.distanceManager.removePlayer(lastSectionPos, player); } @@ -1124,49 +817,29 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.playerMap.unIgnorePlayer(player); } - this.updateChunkTracking(player); + // Paper - rewrite chunk system } + ca.spottedleaf.moonrise.common.PlatformHooks.get().updateMaps(this.level, player); // Paper - rewrite chunk system } private void updateChunkTracking(ServerPlayer player) { - ChunkPos chunkPos = player.chunkPosition(); - int playerViewDistance = this.getPlayerViewDistance(player); - if (!( - player.getChunkTrackingView() instanceof ChunkTrackingView.Positioned positioned - && positioned.center().equals(chunkPos) - && positioned.viewDistance() == playerViewDistance - )) { - this.applyChunkTrackingView(player, ChunkTrackingView.of(chunkPos, playerViewDistance)); - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } private void applyChunkTrackingView(ServerPlayer player, ChunkTrackingView chunkTrackingView) { - if (player.level() == this.level) { - ChunkTrackingView chunkTrackingView1 = player.getChunkTrackingView(); - if (chunkTrackingView instanceof ChunkTrackingView.Positioned positioned - && !(chunkTrackingView1 instanceof ChunkTrackingView.Positioned positioned1 && positioned1.center().equals(positioned.center()))) { - player.connection.send(new ClientboundSetChunkCacheCenterPacket(positioned.center().x, positioned.center().z)); - } - - ChunkTrackingView.difference( - chunkTrackingView1, chunkTrackingView, chunkPos -> this.markChunkPendingToSend(player, chunkPos), chunkPos -> dropChunk(player, chunkPos) - ); - player.setChunkTrackingView(chunkTrackingView); - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } @Override public List getPlayers(ChunkPos pos, boolean boundaryOnly) { - Set allPlayers = this.playerMap.getAllPlayers(); - Builder builder = ImmutableList.builder(); - - for (ServerPlayer serverPlayer : allPlayers) { - if (boundaryOnly && this.isChunkOnTrackedBorder(serverPlayer, pos.x, pos.z) || !boundaryOnly && this.isChunkTracked(serverPlayer, pos.x, pos.z)) { - builder.add(serverPlayer); - } + // Paper start - rewrite chunk system + final ChunkHolder holder = this.getVisibleChunkIfPresent(pos.toLong()); + if (holder == null) { + return new ArrayList<>(); + } else { + return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)holder).moonrise$getPlayers(boundaryOnly); } - - return builder.build(); + // Paper end - rewrite chunk system } public void addEntity(Entity entity) { @@ -1190,6 +863,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } else { ChunkMap.TrackedEntity trackedEntity = new ChunkMap.TrackedEntity(entity, i, updateInterval, type.trackDeltas()); this.entityMap.put(entity.getId(), trackedEntity); + // 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(trackedEntity); + // Paper end - optimise entity tracker trackedEntity.updatePlayers(this.level.players()); if (entity instanceof ServerPlayer serverPlayer) { this.updatePlayerStatus(serverPlayer, true); @@ -1219,12 +898,38 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider if (trackedEntity1 != null) { trackedEntity1.broadcastRemoved(); } + ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(null); // Paper - optimise entity tracker } + // 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();; + + final ca.spottedleaf.moonrise.common.list.ReferenceList trackerEntities = entityLookup.trackerEntities; + final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked(); + for (int i = 0, len = trackerEntities.size(); i < len; ++i) { + final Entity entity = trackerEntitiesRaw[i]; + final ChunkMap.TrackedEntity tracker = ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity(); + if (tracker == null) { + continue; + } + ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$tick(((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$getChunkData().nearbyPlayers); + if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$hasPlayers() + || ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING)) { + tracker.serverEntity.sendChanges(); + } + } + } + // Paper end - optimise entity tracker + protected void tick() { - for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) { - this.updateChunkTracking(serverPlayer); + // Paper start - optimise entity tracker + if (true) { + this.newTrackerTick(); + return; } + // Paper end - optimise entity tracker + // Paper - rewrite chunk system List list = Lists.newArrayList(); List list1 = this.level.players(); @@ -1302,23 +1007,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } public void waitForLightBeforeSending(ChunkPos chunkPos, int range) { - int i = range + 1; - ChunkPos.rangeClosed(chunkPos, i).forEach(pos -> { - ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(pos.toLong()); - if (visibleChunkIfPresent != null) { - visibleChunkIfPresent.addSendDependency(this.lightEngine.waitForPendingTasks(pos.x, pos.z)); - } - }); + // Paper - rewrite chunk system } - public class DistanceManager extends net.minecraft.server.level.DistanceManager { // Paper - public + public class DistanceManager extends net.minecraft.server.level.DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager { // Paper - public // Paper - rewrite chunk system protected DistanceManager(final Executor dispatcher, final Executor mainThreadExecutor) { super(dispatcher, 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 chunkPos) { - return ChunkMap.this.toDrop.contains(chunkPos); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } @Nullable @@ -1334,13 +1040,96 @@ 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; private final int range; SectionPos lastSectionPos; public final Set seenBy = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Paper - Perf: optimise map impl + // Paper start - optimise entity tracker + private long lastChunkUpdate = -1L; + private ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk lastTrackedChunk; + + @Override + public final void moonrise$tick(final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk chunk) { + if (chunk == null) { + this.moonrise$clearPlayers(); + return; + } + + final ca.spottedleaf.moonrise.common.list.ReferenceList players = chunk.getPlayers(ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.VIEW_DISTANCE); + + if (players == null) { + this.moonrise$clearPlayers(); + return; + } + + final long lastChunkUpdate = this.lastChunkUpdate; + final long currChunkUpdate = chunk.getUpdateCount(); + final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk lastTrackedChunk = this.lastTrackedChunk; + this.lastChunkUpdate = currChunkUpdate; + this.lastTrackedChunk = chunk; + + final ServerPlayer[] playersRaw = players.getRawDataUnchecked(); + + for (int i = 0, len = players.size(); i < len; ++i) { + final ServerPlayer player = playersRaw[i]; + this.updatePlayer(player); + } + + if (lastChunkUpdate != currChunkUpdate || lastTrackedChunk != chunk) { + // need to purge any players possible not in the chunk list + for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) { + final ServerPlayer player = conn.getPlayer(); + if (!players.contains(player)) { + this.removePlayer(player); + } + } + } + } + + @Override + public final void moonrise$removeNonTickThreadPlayers() { + boolean foundToRemove = false; + for (final ServerPlayerConnection conn : this.seenBy) { + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(conn.getPlayer())) { + foundToRemove = true; + break; + } + } + + if (!foundToRemove) { + return; + } + + for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) { + ServerPlayer player = conn.getPlayer(); + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) { + this.removePlayer(player); + } + } + } + + @Override + public final void moonrise$clearPlayers() { + this.lastChunkUpdate = -1; + this.lastTrackedChunk = null; + if (this.seenBy.isEmpty()) { + return; + } + for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) { + ServerPlayer player = conn.getPlayer(); + this.removePlayer(player); + } + } + + @Override + public final boolean moonrise$hasPlayers() { + return !this.seenBy.isEmpty(); + } + // Paper end - optimise entity tracker + public TrackedEntity(final Entity entity, final int range, final int updateInterval, final boolean trackDelta) { this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, updateInterval, trackDelta, this::broadcast, this.seenBy); // CraftBukkit this.entity = entity; @@ -1431,17 +1220,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } private int getEffectiveRange() { - int i = this.range; + // Paper start - optimise entity tracker + final Entity entity = this.entity; + int range = this.range; - for (Entity entity : this.entity.getIndirectPassengers()) { - int i1 = entity.getType().clientTrackingRange() * 16; - i1 = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, i1); // Paper - if (i1 > i) { - i = i1; - } + if (entity.getPassengers() == ImmutableList.of()) { + return this.scaledRange(range); + } + + // note: we change to List + final List passengers = (List)entity.getIndirectPassengers(); + for (int i = 0, len = passengers.size(); i < len; ++i) { + final Entity passenger = passengers.get(i); + // note: max should be branchless + range = Math.max(range, ca.spottedleaf.moonrise.common.PlatformHooks.get().modifyEntityTrackingRange(passenger, passenger.getType().clientTrackingRange() << 4)); } - return this.scaledRange(i); + return this.scaledRange(range); + // Paper end - optimise entity tracker } public void updatePlayers(List playersList) { diff --git a/net/minecraft/server/level/DistanceManager.java b/net/minecraft/server/level/DistanceManager.java index 8ec20e372570d5eb720cdcdaed9c92946033be9b..5eab6179ce3913cb4e4d424f910ba423faf21c85 100644 --- a/net/minecraft/server/level/DistanceManager.java +++ b/net/minecraft/server/level/DistanceManager.java @@ -34,56 +34,56 @@ import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.chunk.LevelChunk; import org.slf4j.Logger; -public abstract class DistanceManager { +public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager, ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager { // Paper - rewrite chunk system // Paper - chunk tick iteration optimisation static final Logger LOGGER = LogUtils.getLogger(); static final int PLAYER_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING); private static final int INITIAL_TICKET_LIST_CAPACITY = 4; final Long2ObjectMap> playersPerChunk = new Long2ObjectOpenHashMap<>(); - public final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap<>(); - private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); - private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8); - private final TickingTracker tickingTicketsTracker = new TickingTracker(); - private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(32); - final Set chunksToUpdateFutures = new ReferenceOpenHashSet<>(); - final ThrottlingChunkTaskDispatcher ticketDispatcher; - final LongSet ticketsToRelease = new LongOpenHashSet(); - final Executor mainThreadExecutor; + // Paper - rewrite chunk system + // Paper - chunk tick iteration optimisation + // Paper - rewrite chunk system private long ticketTickCounter; - public int simulationDistance = 10; + // Paper - rewrite chunk system protected DistanceManager(Executor dispatcher, Executor mainThreadExecutor) { TaskScheduler taskScheduler = TaskScheduler.wrapExecutor("player ticket throttler", mainThreadExecutor); - this.ticketDispatcher = new ThrottlingChunkTaskDispatcher(taskScheduler, dispatcher, 4); - this.mainThreadExecutor = mainThreadExecutor; + // Paper - rewrite chunk system } - protected void purgeStaleTickets() { - this.ticketTickCounter++; - ObjectIterator>>> objectIterator = this.tickets.long2ObjectEntrySet().fastIterator(); - - while (objectIterator.hasNext()) { - Entry>> entry = objectIterator.next(); - Iterator> iterator = entry.getValue().iterator(); - boolean flag = false; - - while (iterator.hasNext()) { - Ticket ticket = iterator.next(); - if (ticket.timedOut(this.ticketTickCounter)) { - iterator.remove(); - flag = true; - this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); - } - } + // Paper start - rewrite chunk system + @Override + public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager moonrise$getChunkHolderManager() { + return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getChunkTaskScheduler().chunkHolderManager; + } + // Paper end - rewrite chunk system + // Paper start - chunk tick iteration optimisation + private final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>(); - if (flag) { - this.ticketTracker.update(entry.getLongKey(), getTicketLevelAt(entry.getValue()), false); - } + @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 (entry.getValue().isEmpty()) { - objectIterator.remove(); - } + @Override + public final void moonrise$removePlayer(final ServerPlayer player, final SectionPos pos) { + this.spawnChunkTracker.remove(player); + } + + @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 + } private static int getTicketLevelAt(SortedArraySet> tickets) { return !tickets.isEmpty() ? tickets.first().getTicketLevel() : ChunkLevel.MAX_LEVEL + 1; @@ -98,77 +98,15 @@ public abstract class DistanceManager { protected abstract ChunkHolder updateChunkScheduling(long chunkPos, int i, @Nullable ChunkHolder newLevel, int holder); public boolean runAllUpdates(ChunkMap chunkMap) { - 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()) { - // CraftBukkit start - SPIGOT-7780: Call chunk unload events before updateHighestAllowedStatus - for (final ChunkHolder chunkHolder : this.chunksToUpdateFutures) { - chunkHolder.callEventIfUnloading(chunkMap); - } - // CraftBukkit end - SPIGOT-7780: Call chunk unload events before updateHighestAllowedStatus - - for (ChunkHolder chunkHolder : this.chunksToUpdateFutures) { - chunkHolder.updateHighestAllowedStatus(chunkMap); - } - - for (ChunkHolder chunkHolder : this.chunksToUpdateFutures) { - chunkHolder.updateFutures(chunkMap, this.mainThreadExecutor); - } - - this.chunksToUpdateFutures.clear(); - return true; - } else { - if (!this.ticketsToRelease.isEmpty()) { - LongIterator longIterator = this.ticketsToRelease.iterator(); - - while (longIterator.hasNext()) { - long l = longIterator.nextLong(); - if (this.getTickets(l).stream().anyMatch(ticket -> ticket.getType() == TicketType.PLAYER)) { - ChunkHolder updatingChunkIfPresent = chunkMap.getUpdatingChunkIfPresent(l); - if (updatingChunkIfPresent == null) { - throw new IllegalStateException(); - } - - CompletableFuture> entityTickingChunkFuture = updatingChunkIfPresent.getEntityTickingChunkFuture(); - entityTickingChunkFuture.thenAccept( - chunkResult -> this.mainThreadExecutor.execute(() -> this.ticketDispatcher.release(l, () -> {}, false)) - ); - } - } - - this.ticketsToRelease.clear(); - } - - return flag; - } + return this.moonrise$getChunkHolderManager().processTicketUpdates(); // Paper - rewrite chunk system } void addTicket(long chunkPos, Ticket ticket) { - SortedArraySet> tickets = this.getTickets(chunkPos); - int ticketLevelAt = getTicketLevelAt(tickets); - Ticket ticket1 = tickets.addOrGet(ticket); - ticket1.setCreatedTick(this.ticketTickCounter); - if (ticket.getTicketLevel() < ticketLevelAt) { - this.ticketTracker.update(chunkPos, ticket.getTicketLevel(), true); - } + this.moonrise$getChunkHolderManager().addTicketAtLevel((TicketType)ticket.getType(), chunkPos, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system } void removeTicket(long chunkPos, Ticket ticket) { - SortedArraySet> tickets = this.getTickets(chunkPos); - if (tickets.remove(ticket)) { - } - - if (tickets.isEmpty()) { - this.tickets.remove(chunkPos); - } - - this.ticketTracker.update(chunkPos, getTicketLevelAt(tickets), false); + this.moonrise$getChunkHolderManager().removeTicketAtLevel((TicketType)ticket.getType(), chunkPos, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system } public void addTicket(TicketType type, ChunkPos pos, int level, T value) { @@ -181,68 +119,43 @@ public abstract class DistanceManager { } public void addRegionTicket(TicketType type, ChunkPos pos, int distance, T value) { - Ticket ticket = new Ticket<>(type, ChunkLevel.byStatus(FullChunkStatus.FULL) - distance, value); - long packedChunkPos = pos.toLong(); - this.addTicket(packedChunkPos, ticket); // Paper - diff on change above - this.tickingTicketsTracker.addTicket(packedChunkPos, ticket); + this.moonrise$getChunkHolderManager().addTicketAtLevel(type, pos, ChunkLevel.byStatus(FullChunkStatus.FULL) - distance, value); // Paper - rewrite chunk system } public void removeRegionTicket(TicketType type, ChunkPos pos, int distance, T value) { - Ticket ticket = new Ticket<>(type, ChunkLevel.byStatus(FullChunkStatus.FULL) - distance, value); - long packedChunkPos = pos.toLong(); - this.removeTicket(packedChunkPos, ticket); // Paper - diff on change above - this.tickingTicketsTracker.removeTicket(packedChunkPos, ticket); + this.moonrise$getChunkHolderManager().removeTicketAtLevel(type, pos, ChunkLevel.byStatus(FullChunkStatus.FULL) - distance, value); // Paper - rewrite chunk system } // Paper start public boolean addPluginRegionTicket(final ChunkPos pos, final org.bukkit.plugin.Plugin value) { - Ticket ticket = new Ticket<>(TicketType.PLUGIN_TICKET, ChunkLevel.byStatus(FullChunkStatus.FULL) - 2, value); // Copied from below and keep in-line with force loading, add at level 31 - final long packedChunkPos = pos.toLong(); - final Set> tickets = this.getTickets(packedChunkPos); - if (tickets.contains(ticket)) { - return false; - } - this.addTicket(packedChunkPos, ticket); - this.tickingTicketsTracker.addTicket(packedChunkPos, ticket); - return true; + return this.moonrise$getChunkHolderManager().addTicketAtLevel(TicketType.PLUGIN_TICKET, pos, ChunkLevel.byStatus(FullChunkStatus.FULL) - 2, value); // Paper - rewrite chunk system } public boolean removePluginRegionTicket(final ChunkPos pos, final org.bukkit.plugin.Plugin value) { - Ticket ticket = new Ticket<>(TicketType.PLUGIN_TICKET, ChunkLevel.byStatus(FullChunkStatus.FULL) - 2, value); // Copied from below and keep in-line with force loading, add at level 31 - final long packedChunkPos = pos.toLong(); - final Set> tickets = this.tickets.get(packedChunkPos); // Don't use getTickets, we don't want to create a new set - if (tickets == null || !tickets.contains(ticket)) { - return false; - } - this.removeTicket(packedChunkPos, ticket); - this.tickingTicketsTracker.removeTicket(packedChunkPos, ticket); - return true; + return this.moonrise$getChunkHolderManager().removeTicketAtLevel(TicketType.PLUGIN_TICKET, pos, ChunkLevel.byStatus(FullChunkStatus.FULL) - 2, value); // Paper - rewrite chunk system } // Paper end private SortedArraySet> getTickets(long chunkPos) { - return this.tickets.computeIfAbsent(chunkPos, l -> SortedArraySet.create(4)); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } protected void updateChunkForced(ChunkPos pos, boolean add) { - Ticket ticket = new Ticket<>(TicketType.FORCED, ChunkMap.FORCED_TICKET_LEVEL, pos); - long packedChunkPos = pos.toLong(); + // Paper start - rewrite chunk system if (add) { - this.addTicket(packedChunkPos, ticket); - this.tickingTicketsTracker.addTicket(packedChunkPos, ticket); + this.moonrise$getChunkHolderManager().addTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos); } else { - this.removeTicket(packedChunkPos, ticket); - this.tickingTicketsTracker.removeTicket(packedChunkPos, ticket); + this.moonrise$getChunkHolderManager().removeTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos); } + // Paper end - rewrite chunk system } public void addPlayer(SectionPos sectionPos, ServerPlayer player) { ChunkPos chunkPos = sectionPos.chunk(); long packedChunkPos = chunkPos.toLong(); this.playersPerChunk.computeIfAbsent(packedChunkPos, l -> new ObjectOpenHashSet<>()).add(player); - this.naturalSpawnChunkCounter.update(packedChunkPos, 0, true); - this.playerTicketManager.update(packedChunkPos, 0, true); - this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkPos, this.getPlayerTicketLevel(), chunkPos); + // Paper - chunk tick iteration optimisation + // Paper - rewrite chunk system } public void removePlayer(SectionPos sectionPos, ServerPlayer player) { @@ -254,136 +167,90 @@ public abstract class DistanceManager { if (set == null || set.isEmpty()) { // Paper end - some state corruption happens here, don't crash, clean up gracefully this.playersPerChunk.remove(packedChunkPos); - this.naturalSpawnChunkCounter.update(packedChunkPos, Integer.MAX_VALUE, false); - this.playerTicketManager.update(packedChunkPos, Integer.MAX_VALUE, false); - this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkPos, this.getPlayerTicketLevel(), chunkPos); + // 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 chunkPos) { - SortedArraySet> set = this.tickets.get(chunkPos); - return set != null && !set.isEmpty() ? set.first().toString() : "no_ticket"; + return this.moonrise$getChunkHolderManager().getTicketDebugString(chunkPos); // 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 filename) { - try (FileOutputStream fileOutputStream = new FileOutputStream(new File(filename))) { - for (Entry>> entry : this.tickets.long2ObjectEntrySet()) { - ChunkPos chunkPos = new ChunkPos(entry.getLongKey()); - - for (Ticket ticket : entry.getValue()) { - fileOutputStream.write( - (chunkPos.x + "\t" + chunkPos.z + "\t" + ticket.getType() + "\t" + ticket.getTicketLevel() + "\t\n").getBytes(StandardCharsets.UTF_8) - ); - } - } - } catch (IOException var10) { - LOGGER.error("Failed to dump tickets to {}", filename, var10); - } + 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> set = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve - ObjectIterator>>> objectIterator = this.tickets.long2ObjectEntrySet().fastIterator(); - - while (objectIterator.hasNext()) { - Entry>> entry = objectIterator.next(); - Iterator> iterator = entry.getValue().iterator(); - boolean flag = false; - - while (iterator.hasNext()) { - Ticket ticket = iterator.next(); - if (!set.contains(ticket.getType())) { - iterator.remove(); - flag = true; - this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); - } - } - - if (flag) { - this.ticketTracker.update(entry.getLongKey(), getTicketLevelAt(entry.getValue()), false); - } - - if (entry.getValue().isEmpty()) { - objectIterator.remove(); - } - } + // Paper - rewrite chunk system } public boolean hasTickets() { - return !this.tickets.isEmpty(); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } // CraftBukkit start public void removeAllTicketsFor(TicketType ticketType, int ticketLevel, T ticketIdentifier) { - Ticket target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier); - - for (java.util.Iterator>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) { - Entry>> entry = iterator.next(); - SortedArraySet> tickets = entry.getValue(); - if (tickets.remove(target)) { - // copied from removeTicket - this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false); - - // can't use entry after it's removed - if (tickets.isEmpty()) { - iterator.remove(); - } - } - } + this.moonrise$getChunkHolderManager().removeAllTicketsFor(ticketType, ticketLevel, ticketIdentifier); // Paper - rewrite chunk system } // CraftBukkit end +/* // Paper - rewrite chunk system class ChunkTicketTracker extends ChunkTracker { private static final int MAX_LEVEL = ChunkLevel.MAX_LEVEL + 1; @@ -428,7 +295,7 @@ public abstract class DistanceManager { public int runDistanceUpdates(int toUpdateCount) { return this.runUpdates(toUpdateCount); } - } + }*/ // Paper - rewrite chunk system class FixedPlayerDistanceChunkTracker extends ChunkTracker { protected final Long2ByteMap chunks = new Long2ByteOpenHashMap(); @@ -487,6 +354,7 @@ public abstract class DistanceManager { } } +/* // Paper - rewrite chunk system class PlayerTicketTracker extends DistanceManager.FixedPlayerDistanceChunkTracker { private int viewDistance; private final Long2IntMap queueLevels = Long2IntMaps.synchronize(new Long2IntOpenHashMap()); @@ -563,5 +431,5 @@ public abstract class DistanceManager { private boolean haveTicketFor(int level) { return level <= this.viewDistance; } - } + }*/ // Paper - rewrite chunk system } diff --git a/net/minecraft/server/level/GenerationChunkHolder.java b/net/minecraft/server/level/GenerationChunkHolder.java index cb66209c64b855dedf2e4e114a7716da13bc4587..da1366fdc4889d6a3befd43d81a19a816ed4cbe9 100644 --- a/net/minecraft/server/level/GenerationChunkHolder.java +++ b/net/minecraft/server/level/GenerationChunkHolder.java @@ -27,13 +27,7 @@ public abstract class GenerationChunkHolder { public static final ChunkResult UNLOADED_CHUNK = ChunkResult.error("Unloaded chunk"); public static final CompletableFuture> UNLOADED_CHUNK_FUTURE = CompletableFuture.completedFuture(UNLOADED_CHUNK); protected final ChunkPos pos; - @Nullable - private volatile ChunkStatus highestAllowedStatus; - private final AtomicReference startedWork = new AtomicReference<>(); - private final AtomicReferenceArray>> futures = new AtomicReferenceArray<>(CHUNK_STATUSES.size()); - private final AtomicReference task = new AtomicReference<>(); - private final AtomicInteger generationRefCount = new AtomicInteger(); - private volatile CompletableFuture generationSaveSyncFuture = CompletableFuture.completedFuture(null); + // Paper - rewrite chunk system public GenerationChunkHolder(ChunkPos pos) { this.pos = pos; @@ -43,243 +37,96 @@ public abstract class GenerationChunkHolder { } public CompletableFuture> scheduleChunkGenerationTask(ChunkStatus targetStatus, ChunkMap chunkMap) { - if (this.isStatusDisallowed(targetStatus)) { - return UNLOADED_CHUNK_FUTURE; - } else { - CompletableFuture> future = this.getOrCreateFuture(targetStatus); - if (future.isDone()) { - return future; - } else { - ChunkGenerationTask chunkGenerationTask = this.task.get(); - if (chunkGenerationTask == null || targetStatus.isAfter(chunkGenerationTask.targetStatus)) { - this.rescheduleChunkTask(chunkMap, targetStatus); - } - - return future; - } - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } CompletableFuture> applyStep(ChunkStep step, GeneratingChunkMap chunkMap, StaticCache2D cache) { - if (this.isStatusDisallowed(step.targetStatus())) { - return UNLOADED_CHUNK_FUTURE; - } else { - return this.acquireStatusBump(step.targetStatus()) ? chunkMap.applyStep(this, step, cache).handle((chunkAccess, throwable) -> { - if (throwable != null) { - CrashReport crashReport = CrashReport.forThrowable(throwable, "Exception chunk generation/loading"); - MinecraftServer.setFatalException(new ReportedException(crashReport)); - } else { - this.completeFuture(step.targetStatus(), chunkAccess); - } - - return ChunkResult.of(chunkAccess); - }) : this.getOrCreateFuture(step.targetStatus()); - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } protected void updateHighestAllowedStatus(ChunkMap chunkMap) { - ChunkStatus chunkStatus = this.highestAllowedStatus; - ChunkStatus chunkStatus1 = ChunkLevel.generationStatus(this.getTicketLevel()); - this.highestAllowedStatus = chunkStatus1; - boolean flag = chunkStatus != null && (chunkStatus1 == null || chunkStatus1.isBefore(chunkStatus)); - if (flag) { - this.failAndClearPendingFuturesBetween(chunkStatus1, chunkStatus); - if (this.task.get() != null) { - this.rescheduleChunkTask(chunkMap, this.findHighestStatusWithPendingFuture(chunkStatus1)); - } - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } public void replaceProtoChunk(ImposterProtoChunk chunk) { - CompletableFuture> completableFuture = CompletableFuture.completedFuture(ChunkResult.of(chunk)); - - for (int i = 0; i < this.futures.length() - 1; i++) { - CompletableFuture> completableFuture1 = this.futures.get(i); - Objects.requireNonNull(completableFuture1); - ChunkAccess chunkAccess = completableFuture1.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, completableFuture1, completableFuture)) { - throw new IllegalStateException("Future changed by other thread while trying to replace it"); - } - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } void removeTask(ChunkGenerationTask task) { - this.task.compareAndSet(task, null); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } private void rescheduleChunkTask(ChunkMap chunkMap, @Nullable ChunkStatus targetStatus) { - ChunkGenerationTask chunkGenerationTask; - if (targetStatus != null) { - chunkGenerationTask = chunkMap.scheduleGenerationTask(targetStatus, this.getPos()); - } else { - chunkGenerationTask = null; - } - - ChunkGenerationTask chunkGenerationTask1 = this.task.getAndSet(chunkGenerationTask); - if (chunkGenerationTask1 != null) { - chunkGenerationTask1.markForCancellation(); - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } private CompletableFuture> getOrCreateFuture(ChunkStatus targetStatus) { - if (this.isStatusDisallowed(targetStatus)) { - return UNLOADED_CHUNK_FUTURE; - } else { - int index = targetStatus.getIndex(); - CompletableFuture> completableFuture = this.futures.get(index); - - while (completableFuture == null) { - CompletableFuture> completableFuture1 = new CompletableFuture<>(); - completableFuture = this.futures.compareAndExchange(index, null, completableFuture1); - if (completableFuture == null) { - if (this.isStatusDisallowed(targetStatus)) { - this.failAndClearPendingFuture(index, completableFuture1); - return UNLOADED_CHUNK_FUTURE; - } - - return completableFuture1; - } - } - - return completableFuture; - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } private void failAndClearPendingFuturesBetween(@Nullable ChunkStatus highestAllowableStatus, ChunkStatus currentStatus) { - int i = highestAllowableStatus == null ? 0 : highestAllowableStatus.getIndex() + 1; - int index = currentStatus.getIndex(); - - for (int i1 = i; i1 <= index; i1++) { - CompletableFuture> completableFuture = this.futures.get(i1); - if (completableFuture != null) { - this.failAndClearPendingFuture(i1, completableFuture); - } - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } private void failAndClearPendingFuture(int status, CompletableFuture> future) { - if (future.complete(UNLOADED_CHUNK) && !this.futures.compareAndSet(status, future, null)) { - throw new IllegalStateException("Nothing else should replace the future here"); - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } private void completeFuture(ChunkStatus targetStatus, ChunkAccess chunkAccess) { - ChunkResult chunkResult = ChunkResult.of(chunkAccess); - int index = targetStatus.getIndex(); - - while (true) { - CompletableFuture> completableFuture = this.futures.get(index); - if (completableFuture == null) { - if (this.futures.compareAndSet(index, 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 generationStatus) { - if (generationStatus == null) { - return null; - } else { - ChunkStatus chunkStatus = generationStatus; - - for (ChunkStatus chunkStatus1 = this.startedWork.get(); - chunkStatus1 == null || chunkStatus.isAfter(chunkStatus1); - 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 status) { - ChunkStatus chunkStatus = status == ChunkStatus.EMPTY ? null : status.getParent(); - ChunkStatus chunkStatus1 = this.startedWork.compareAndExchange(chunkStatus, status); - if (chunkStatus1 == chunkStatus) { - return true; - } else if (chunkStatus1 != null && !status.isAfter(chunkStatus1)) { - return false; - } else { - throw new IllegalStateException("Unexpected last startedWork status: " + chunkStatus1 + " while trying to start: " + status); - } + 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 saveDependency); public void increaseGenerationRefCount() { - if (this.generationRefCount.getAndIncrement() == 0) { - this.generationSaveSyncFuture = new CompletableFuture<>(); - this.addSaveDependency(this.generationSaveSyncFuture); - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } public void decreaseGenerationRefCount() { - CompletableFuture completableFuture = this.generationSaveSyncFuture; - int i = this.generationRefCount.decrementAndGet(); - if (i == 0) { - completableFuture.complete(null); - } - - if (i < 0) { - throw new IllegalStateException("More releases than claims. Count: " + i); - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } @Nullable public ChunkAccess getChunkIfPresentUnchecked(ChunkStatus status) { - CompletableFuture> completableFuture = this.futures.get(status.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(status); + // Paper end - rewrite chunk system } @Nullable public ChunkAccess getChunkIfPresent(ChunkStatus status) { - return this.isStatusDisallowed(status) ? null : this.getChunkIfPresentUnchecked(status); + // Paper start - rewrite chunk system + return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkIfPresent(status); + // Paper end - rewrite chunk system } @Nullable public ChunkAccess getLatestChunk() { - ChunkStatus chunkStatus = this.startedWork.get(); - if (chunkStatus == null) { - return null; - } else { - ChunkAccess chunkIfPresentUnchecked = this.getChunkIfPresentUnchecked(chunkStatus); - return chunkIfPresentUnchecked != null ? chunkIfPresentUnchecked : this.getChunkIfPresentUnchecked(chunkStatus.getParent()); - } + // Paper start - rewrite chunk system + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion(); + return lastCompletion == null ? null : lastCompletion.chunk(); + // Paper end - rewrite chunk system } @Nullable public ChunkStatus getPersistedStatus() { - CompletableFuture> completableFuture = this.futures.get(ChunkStatus.EMPTY.getIndex()); - ChunkAccess chunkAccess = completableFuture == null ? null : completableFuture.getNow(NOT_DONE_YET).orElse(null); - return chunkAccess == null ? null : chunkAccess.getPersistedStatus(); + // Paper start - rewrite chunk system + final ChunkAccess chunk = this.getLatestChunk(); + return chunk == null ? null : chunk.getPersistedStatus(); + // Paper end - rewrite chunk system } public ChunkPos getPos() { @@ -287,7 +134,7 @@ public abstract class GenerationChunkHolder { } public FullChunkStatus getFullStatus() { - return ChunkLevel.fullStatus(this.getTicketLevel()); + return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkStatus(); // Paper - rewrite chunk system } public abstract int getTicketLevel(); @@ -296,26 +143,15 @@ public abstract class GenerationChunkHolder { @VisibleForDebug public List>>> getAllFutures() { - List>>> list = new ArrayList<>(); - - for (int i = 0; i < CHUNK_STATUSES.size(); i++) { - list.add(Pair.of(CHUNK_STATUSES.get(i), this.futures.get(i))); - } - - return list; + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } @Nullable @VisibleForDebug public ChunkStatus getLatestStatus() { - for (int i = CHUNK_STATUSES.size() - 1; i >= 0; i--) { - ChunkStatus chunkStatus = CHUNK_STATUSES.get(i); - ChunkAccess chunkIfPresentUnchecked = this.getChunkIfPresentUnchecked(chunkStatus); - if (chunkIfPresentUnchecked != 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 2f49dbc919f7f5eea9abce6106723c72f5ae45fb..87d4291a3944f706a694536da6de0f28c548ab8d 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; private final ServerLevel level; @@ -80,6 +80,107 @@ public class ServerChunkCache extends ChunkSource { } long chunkFutureAwaitCounter; // Paper end + // Paper start - rewrite chunk system + + @Override + public final void moonrise$setFullChunk(final int chunkX, final int chunkZ, final LevelChunk chunk) { + final long key = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); + if (chunk == null) { + this.fullChunks.remove(key); + } else { + this.fullChunks.put(key, chunk); + } + } + + @Override + public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) { + return this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } + + private ChunkAccess syncLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus) { + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler(); + final CompletableFuture completable = new CompletableFuture<>(); + chunkTaskScheduler.scheduleChunkLoad( + chunkX, chunkZ, toStatus, true, ca.spottedleaf.concurrentutil.util.Priority.BLOCKING, + completable::complete + ); + + if (!completable.isDone() && chunkTaskScheduler.hasShutdown()) { + throw new IllegalStateException( + "Chunk system has shut down, cannot process chunk requests in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.level) + "' at " + + "(" + chunkX + "," + chunkZ + ") status: " + toStatus + ); + } + + if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, chunkX, chunkZ)) { + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.pushChunkWait(this.level, chunkX, chunkZ); + this.mainThreadProcessor.managedBlock(completable::isDone); + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.popChunkWait(); + } + + final ChunkAccess ret = completable.join(); + if (ret == null) { + throw new IllegalStateException("Chunk not loaded when requested"); + } + + return ret; + } + + private ChunkAccess getChunkFallback(final int chunkX, final int chunkZ, final ChunkStatus toStatus, + final boolean load) { + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler(); + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager; + + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder currentChunk = chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + final ChunkAccess ifPresent = currentChunk == null ? null : currentChunk.getChunkIfPresent(toStatus); + + if (ifPresent != null && (toStatus != ChunkStatus.FULL || currentChunk.isFullChunkReady())) { + return ifPresent; + } + + final ca.spottedleaf.moonrise.common.PlatformHooks platformHooks = ca.spottedleaf.moonrise.common.PlatformHooks.get(); + + if (platformHooks.hasCurrentlyLoadingChunk() && currentChunk != null) { + final ChunkAccess loading = platformHooks.getCurrentlyLoadingChunk(currentChunk.vanillaChunkHolder); + if (loading != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { + return loading; + } + } + + return load ? this.syncLoad(chunkX, chunkZ, toStatus) : null; + } + // Paper end - rewrite chunk system + // Paper start - chunk tick iteration optimisations + private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom shuffleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(0L); + private boolean isChunkNearPlayer(final ChunkMap chunkMap, final ChunkPos chunkPos, final LevelChunk levelChunk) { + final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)levelChunk).moonrise$getChunkAndHolder().holder()) + .moonrise$getRealChunkHolder().holderData; + final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk nearbyPlayers = chunkData.nearbyPlayers; + if (nearbyPlayers == null) { + return false; + } + + final ca.spottedleaf.moonrise.common.list.ReferenceList players = nearbyPlayers.getPlayers(ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.SPAWN_RANGE); + + if (players == null) { + return false; + } + + final ServerPlayer[] raw = players.getRawDataUnchecked(); + final int len = players.size(); + + java.util.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 level, @@ -138,13 +239,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) { @@ -215,51 +310,42 @@ public class ServerChunkCache extends ChunkSource { @Nullable @Override public ChunkAccess getChunk(int x, int z, ChunkStatus chunkStatus, boolean requireChunk) { - if (Thread.currentThread() != this.mainThread) { - return CompletableFuture.supplyAsync(() -> this.getChunk(x, z, chunkStatus, requireChunk), this.mainThreadProcessor).join(); - } else { - // Paper start - Perf: Optimise getChunkAt calls for loaded chunks - LevelChunk ifLoaded = this.getChunkAtIfCachedImmediately(x, z); - if (ifLoaded != null) { - return ifLoaded; - } - // Paper end - Perf: Optimise getChunkAt calls for loaded chunks - ProfilerFiller profilerFiller = Profiler.get(); - profilerFiller.incrementCounter("getChunk"); - long packedChunkPos = ChunkPos.asLong(x, z); - - for (int i = 0; i < 4; i++) { - if (packedChunkPos == this.lastChunkPos[i] && chunkStatus == this.lastChunkStatus[i]) { - ChunkAccess chunkAccess = this.lastChunk[i]; - if (chunkAccess != 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 chunkAccess; - } - } - } + // Paper start - rewrite chunk system + if (chunkStatus == ChunkStatus.FULL) { + final LevelChunk ret = this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x, z)); - profilerFiller.incrementCounter("getChunkCacheMiss"); - CompletableFuture> chunkFutureMainThread = this.getChunkFutureMainThread(x, z, chunkStatus, requireChunk); - this.mainThreadProcessor.managedBlock(chunkFutureMainThread::isDone); - // com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads - ChunkResult chunkResult = chunkFutureMainThread.join(); - ChunkAccess chunkAccess1 = chunkResult.orElse(null); - if (chunkAccess1 == null && requireChunk) { - throw (IllegalStateException)Util.pauseInIde(new IllegalStateException("Chunk not there when requested: " + chunkResult.getError())); - } else { - this.storeInCache(packedChunkPos, chunkAccess1, chunkStatus); - return chunkAccess1; + if (ret != null) { + return ret; } + + return requireChunk ? this.getChunkFallback(x, z, chunkStatus, requireChunk) : null; } + + return this.getChunkFallback(x, z, chunkStatus, requireChunk); + // Paper end - rewrite chunk system } @Nullable @Override public LevelChunk getChunkNow(int chunkX, int chunkZ) { - if (Thread.currentThread() != this.mainThread) { - return null; - } else { - return this.getChunkAtIfCachedImmediately(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() { @@ -285,54 +371,59 @@ public class ServerChunkCache extends ChunkSource { } private CompletableFuture> getChunkFutureMainThread(int x, int z, ChunkStatus chunkStatus, boolean requireChunk) { - ChunkPos chunkPos = new ChunkPos(x, z); - long packedChunkPos = chunkPos.toLong(); - int i = ChunkLevel.byStatus(chunkStatus); - ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(packedChunkPos); - // CraftBukkit start - don't add new ticket for currently unloading chunk - boolean currentlyUnloading = false; - if (visibleChunkIfPresent != null) { - FullChunkStatus oldChunkState = ChunkLevel.fullStatus(visibleChunkIfPresent.oldTicketLevel); - FullChunkStatus currentChunkState = ChunkLevel.fullStatus(visibleChunkIfPresent.getTicketLevel()); - currentlyUnloading = (oldChunkState.isOrAfter(FullChunkStatus.FULL) && !currentChunkState.isOrAfter(FullChunkStatus.FULL)); - } - if (requireChunk && !currentlyUnloading) { - // CraftBukkit end - this.distanceManager.addTicket(TicketType.UNKNOWN, chunkPos, i, chunkPos); - if (this.chunkAbsent(visibleChunkIfPresent, i)) { - ProfilerFiller profilerFiller = Profiler.get(); - profilerFiller.push("chunkLoad"); - this.runDistanceManagerUpdates(); - visibleChunkIfPresent = this.getVisibleChunkIfPresent(packedChunkPos); - profilerFiller.pop(); - if (this.chunkAbsent(visibleChunkIfPresent, i)) { - 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, x, z, "Scheduling chunk load off-main"); + + final int minLevel = ChunkLevel.byStatus(chunkStatus); + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(x, z); + + final boolean needsFullScheduling = chunkStatus == ChunkStatus.FULL && (chunkHolder == null || !chunkHolder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)); + + if ((chunkHolder == null || chunkHolder.getTicketLevel() > minLevel || needsFullScheduling) && !requireChunk) { + return ChunkHolder.UNLOADED_CHUNK_FUTURE; } - return this.chunkAbsent(visibleChunkIfPresent, i) - ? GenerationChunkHolder.UNLOADED_CHUNK_FUTURE - : visibleChunkIfPresent.scheduleChunkGenerationTask(chunkStatus, this.chunkMap); - } + final ChunkAccess ifPresent = chunkHolder == null ? null : chunkHolder.getChunkIfPresent(chunkStatus); + if (needsFullScheduling || ifPresent == null) { + // schedule + final CompletableFuture> ret = new CompletableFuture<>(); + final Consumer complete = (ChunkAccess chunk) -> { + if (chunk == null) { + ret.complete(ChunkHolder.UNLOADED_CHUNK); + } else { + ret.complete(ChunkResult.of(chunk)); + } + }; - private boolean chunkAbsent(@Nullable ChunkHolder chunkHolder, int status) { - return chunkHolder == null || chunkHolder.oldTicketLevel > status; // CraftBukkit using oldTicketLevel for isLoaded checks + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().scheduleChunkLoad( + x, z, chunkStatus, 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 visibleChunkIfPresent = this.getVisibleChunkIfPresent(new ChunkPos(x, z).toLong()); - int i = ChunkLevel.byStatus(ChunkStatus.FULL); - return !this.chunkAbsent(visibleChunkIfPresent, i); + return this.getChunkNow(x, z) != null; // Paper - rewrite chunk system } @Nullable @Override public LightChunk getChunkForLighting(int chunkX, int chunkZ) { - long packedChunkPos = ChunkPos.asLong(chunkX, chunkZ); - ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(packedChunkPos); - return visibleChunkIfPresent == null ? null : visibleChunkIfPresent.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 @@ -345,28 +436,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 chunkPos) { - if (!this.level.shouldTickBlocksAt(chunkPos)) { - return false; - } else { - ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(chunkPos); - return visibleChunkIfPresent != null && visibleChunkIfPresent.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(chunkPos); + 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); } @@ -377,17 +458,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"); @@ -411,6 +490,7 @@ public class ServerChunkCache extends ChunkSource { this.runDistanceManagerUpdates(); profilerFiller.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(); } @@ -435,7 +515,10 @@ public class ServerChunkCache extends ChunkSource { profilerFiller.push("filteringTickingChunks"); this.collectTickingChunks(list); profilerFiller.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(profilerFiller, l, list); profilerFiller.pop(); } finally { @@ -452,7 +535,7 @@ public class ServerChunkCache extends ChunkSource { profiler.push("broadcast"); for (ChunkHolder chunkHolder : this.chunkHoldersToBroadcast) { - LevelChunk tickingChunk = chunkHolder.getTickingChunk(); + LevelChunk tickingChunk = chunkHolder.getChunkToSend(); // Paper - rewrite chunk system if (tickingChunk != null) { chunkHolder.broadcastChanges(tickingChunk); } @@ -463,12 +546,26 @@ public class ServerChunkCache extends ChunkSource { } private void collectTickingChunks(List output) { - this.chunkMap.forEachSpawnCandidateChunk(chunk -> { - LevelChunk tickingChunk = chunk.getTickingChunk(); - if (tickingChunk != null && this.level.isNaturalSpawningAllowed(chunk.getPos())) { - output.add(tickingChunk); + // Paper start - chunk tick iteration optimisation + final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = + ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)this.level).moonrise$getPlayerTickingChunks(); + + final ServerChunkCache.ChunkAndHolder[] raw = tickingChunks.getRawDataUnchecked(); + final int size = tickingChunks.size(); + + 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; } - }); + + output.add(levelChunk); + } + // Paper end - chunk tick iteration optimisation } private void tickChunks(ProfilerFiller profiler, long timeInhabited, List chunks) { @@ -504,7 +601,7 @@ public class ServerChunkCache extends ChunkSource { NaturalSpawner.spawnForChunk(this.level, levelChunk, spawnState, filteredSpawningCategories); } - if (this.level.shouldTickBlocksAt(pos.toLong())) { + if (true) { // Paper - rewrite chunk system this.level.tickChunk(levelChunk, _int); } } @@ -516,10 +613,13 @@ public class ServerChunkCache extends ChunkSource { } private void getFullChunk(long chunkPos, Consumer fullChunkGetter) { - ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(chunkPos); - if (visibleChunkIfPresent != null) { - visibleChunkIfPresent.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).ifSuccess(fullChunkGetter); + // Paper start - rewrite chunk system + // note: bypass currentlyLoaded from getChunkNow + final LevelChunk fullChunk = this.fullChunks.get(chunkPos); + if (fullChunk != null) { + fullChunkGetter.accept(fullChunk); } + // Paper end - rewrite chunk system } @Override @@ -607,6 +707,12 @@ public class ServerChunkCache extends ChunkSource { this.chunkMap.setServerViewDistance(viewDistance); } + // 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); } @@ -654,7 +760,7 @@ public class ServerChunkCache extends ChunkSource { } } - record ChunkAndHolder(LevelChunk chunk, ChunkHolder holder) { + public record ChunkAndHolder(LevelChunk chunk, ChunkHolder holder) { // Paper - public } public final class MainThreadExecutor extends BlockableEventLoop { @@ -695,18 +801,14 @@ public class ServerChunkCache extends ChunkSource { @Override public boolean pollTask() { - try { // CraftBukkit - process pending Chunk loadCallback() and unloadCallback() after each run task - 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(); - } - // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task - } finally { - ServerChunkCache.this.chunkMap.callbackExecutor.run(); + return super.pollTask() | ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)serverChunkCache.level).moonrise$getChunkTaskScheduler().executeMainThreadTask(); } - // CraftBukkit end - process pending Chunk loadCallback() and unloadCallback() after each run task + // Paper end - rewrite chunk system } } } diff --git a/net/minecraft/server/level/ServerEntity.java b/net/minecraft/server/level/ServerEntity.java index 70f6d068b3f3665b282d9750310c883839120ab2..870b9efd445ddadb3725e88351555ad986ce7c72 100644 --- a/net/minecraft/server/level/ServerEntity.java +++ b/net/minecraft/server/level/ServerEntity.java @@ -91,6 +91,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 passengers = this.entity.getPassengers(); if (!passengers.equals(this.lastPassengers)) { this.broadcastAndSend(new ClientboundSetPassengersPacket(this.entity)); // CraftBukkit diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java index cdda7f6272cfc48638df4e0e51b496e91ed77ba5..bbb4bb0940765a12c45a99c8234ca82ef1934903 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -170,7 +170,7 @@ import net.minecraft.world.phys.shapes.VoxelShape; import net.minecraft.world.ticks.LevelTicks; import org.slf4j.Logger; -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); public static final IntProvider RAIN_DURATION = UniformInt.of(12000, 24000); @@ -185,7 +185,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe public final net.minecraft.world.level.storage.PrimaryLevelData serverLevelData; // CraftBukkit - type private int lastSpawnChunkRadius; final EntityTickList entityTickList = new EntityTickList(); - public final PersistentEntitySectionManager entityManager; + // Paper - rewrite chunk system private final GameEventDispatcher gameEventDispatcher; public boolean noSave; private final SleepStatus sleepStatus; @@ -256,12 +256,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.util.Priority priority, java.util.function.Consumer> onLoad) { - if (Thread.currentThread() != this.thread) { - this.getChunkSource().mainThreadProcessor.execute(() -> { - this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad); - }); - return; - } + // Paper - rewrite chunk system int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; @@ -280,32 +275,159 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe public final void loadChunks(int minChunkX, int minChunkZ, int maxChunkX, int maxChunkZ, ca.spottedleaf.concurrentutil.util.Priority priority, java.util.function.Consumer> onLoad) { - List ret = new java.util.ArrayList<>(); - it.unimi.dsi.fastutil.ints.IntArrayList ticketLevels = new it.unimi.dsi.fastutil.ints.IntArrayList(); - ServerChunkCache chunkProvider = this.getChunkSource(); + this.moonrise$loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, priority, onLoad); // Paper - rewrite chunk system + } + // Paper end + + // Paper start - optimise getPlayerByUUID + @Nullable + @Override + public Player getPlayerByUUID(UUID uuid) { + final Player player = this.getServer().getPlayerList().getPlayer(uuid); + return player != null && player.level() == this ? player : null; + } + // Paper end - optimise getPlayerByUUID + // Paper start - rewrite chunk system + private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder(); + private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader chunkLoader = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader((ServerLevel)(Object)this); + private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController entityDataController; + private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.PoiDataController poiDataController; + private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController chunkDataController; + private final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler; + private long lastMidTickFailure; + private long tickedBlocksOrFluids; + private final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = new ca.spottedleaf.moonrise.common.misc.NearbyPlayers((ServerLevel)(Object)this); + private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDERS = new ServerChunkCache.ChunkAndHolder[0]; + private final ca.spottedleaf.moonrise.common.list.ReferenceList loadedChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); + private final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); + private final ca.spottedleaf.moonrise.common.list.ReferenceList entityTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); + + @Override + public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) { + return this.chunkSource.getChunkNow(chunkX, chunkZ); + } + + @Override + public final ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) { + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (newChunkHolder == null) { + return null; + } + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion(); + return lastCompletion == null ? null : lastCompletion.chunk(); + } + + @Override + public final ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus leastStatus) { + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ); + if (newChunkHolder == null) { + return null; + } + return newChunkHolder.getChunkIfPresentUnchecked(leastStatus); + } + + @Override + public final void moonrise$midTickTasks() { + ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks(); + } + + @Override + public final ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus status) { + return this.moonrise$getChunkTaskScheduler().syncLoadNonFull(chunkX, chunkZ, status); + } + + @Override + public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler moonrise$getChunkTaskScheduler() { + return this.chunkTaskScheduler; + } + + @Override + public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController moonrise$getChunkDataController() { + return this.chunkDataController; + } + + @Override + public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController moonrise$getPoiChunkDataController() { + return this.poiDataController; + } + + @Override + public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController moonrise$getEntityChunkDataController() { + return this.entityDataController; + } + + @Override + public final int moonrise$getRegionChunkShift() { + return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(); + } + + @Override + public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader() { + return this.chunkLoader; + } + + @Override + public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, + final ca.spottedleaf.concurrentutil.util.Priority priority, + final java.util.function.Consumer> onLoad) { + this.moonrise$loadChunksAsync( + (pos.getX() - radiusBlocks) >> 4, + (pos.getX() + radiusBlocks) >> 4, + (pos.getZ() - radiusBlocks) >> 4, + (pos.getZ() + radiusBlocks) >> 4, + priority, onLoad + ); + } + + @Override + public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, + final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.util.Priority priority, + final java.util.function.Consumer> onLoad) { + this.moonrise$loadChunksAsync( + (pos.getX() - radiusBlocks) >> 4, + (pos.getX() + radiusBlocks) >> 4, + (pos.getZ() - radiusBlocks) >> 4, + (pos.getZ() + radiusBlocks) >> 4, + chunkStatus, priority, onLoad + ); + } + + @Override + public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, + final ca.spottedleaf.concurrentutil.util.Priority priority, + final java.util.function.Consumer> onLoad) { + this.moonrise$loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, priority, onLoad); + } + + @Override + public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, + final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.util.Priority priority, + final java.util.function.Consumer> onLoad) { + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = this.moonrise$getChunkTaskScheduler(); + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager; - int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1); - int[] loadedChunks = new int[1]; + 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); - Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++); + final List ret = new ArrayList<>(requiredChunks); - java.util.function.Consumer consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> { + final java.util.function.Consumer consumer = (final ChunkAccess chunk) -> { if (chunk != null) { - int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel()); - ret.add(chunk); - ticketLevels.add(ticketLevel); - chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier); + synchronized (ret) { + ret.add(chunk); + } + chunkHolderManager.addTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunk.getPos(), ticketLevel, holderIdentifier); } - if (++loadedChunks[0] == requiredChunks) { + if (loadedChunks.incrementAndGet() == requiredChunks) { try { onLoad.accept(java.util.Collections.unmodifiableList(ret)); } finally { for (int i = 0, len = ret.size(); i < len; ++i) { - ChunkPos chunkPos = ret.get(i).getPos(); - int ticketLevel = ticketLevels.getInt(i); + final ChunkPos chunkPos = ret.get(i).getPos(); - chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); - chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier); + chunkHolderManager.removeTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunkPos, ticketLevel, holderIdentifier); } } } @@ -319,16 +441,133 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } } } - // 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; + } + + @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; } - // Paper end - optimise getPlayerByUUID + + @Override + public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getLoadedChunks() { + return this.loadedChunks; + } + + @Override + public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getTickingChunks() { + return this.tickingChunks; + } + + @Override + public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getEntityTickingChunks() { + return this.entityTickingChunks; + } + + @Override + public final boolean moonrise$areChunksLoaded(final int fromX, final int fromZ, final int toX, final int toZ) { + final ServerChunkCache chunkSource = this.chunkSource; + + for (int currZ = fromZ; currZ <= toZ; ++currZ) { + for (int currX = fromX; currX <= toX; ++currX) { + if (!chunkSource.hasChunk(currX, currZ)) { + return false; + } + } + } + + return true; + } + // Paper end - rewrite chunk system + // Paper start - chunk tick iteration + private static final ServerChunkCache.ChunkAndHolder[] EMPTY_PLAYER_CHUNK_HOLDERS = new ServerChunkCache.ChunkAndHolder[0]; + private final ca.spottedleaf.moonrise.common.list.ReferenceList playerTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_CHUNK_HOLDERS); + private final it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap playerTickingRequests = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(); + + @Override + public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getPlayerTickingChunks() { + return this.playerTickingChunks; + } + + @Override + public final void moonrise$markChunkForPlayerTicking(final LevelChunk chunk) { + final ChunkPos pos = chunk.getPos(); + if (!this.playerTickingRequests.containsKey(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos))) { + return; + } + + this.playerTickingChunks.add(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()); + } + + @Override + public final void moonrise$removeChunkForPlayerTicking(final LevelChunk chunk) { + this.playerTickingChunks.remove(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()); + } + + @Override + public final void moonrise$addPlayerTickingRequest(final int chunkX, final int chunkZ) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot add ticking request async"); + + final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); + + if (this.playerTickingRequests.addTo(chunkKey, 1) != 0) { + // already added + return; + } + + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler() + .chunkHolderManager.getChunkHolder(chunkKey); + + if (chunkHolder == null || !chunkHolder.isTickingReady()) { + return; + } + + this.playerTickingChunks.add( + ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder() + ); + } + + @Override + public final void moonrise$removePlayerTickingRequest(final int chunkX, final int chunkZ) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot remove ticking request async"); + + final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); + final int val = this.playerTickingRequests.addTo(chunkKey, -1); + + if (val <= 0) { + throw new IllegalStateException("Negative counter"); + } + + if (val != 1) { + // still has at least one request + return; + } + + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler() + .chunkHolderManager.getChunkHolder(chunkKey); + + if (chunkHolder == null || !chunkHolder.isTickingReady()) { + return; + } + + this.playerTickingChunks.remove( + ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder() + ); + } + // Paper end - chunk tick iteration public ServerLevel( MinecraftServer server, @@ -376,18 +615,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // CraftBukkit end boolean flag = server.forceSynchronousWrites(); DataFixer fixerUpper = server.getFixerUpper(); - EntityPersistentStorage entityPersistentStorage = new EntityStorage( - new SimpleRegionStorage( - new RegionStorageInfo(levelStorageAccess.getLevelId(), dimension, "entities"), - levelStorageAccess.getDimensionPath(dimension).resolve("entities"), - fixerUpper, - flag, - DataFixTypes.ENTITY_CHUNK - ), - this, - server - ); - this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entityPersistentStorage); + // Paper - rewrite chunk system this.chunkSource = new ServerChunkCache( this, levelStorageAccess, @@ -399,7 +627,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe this.spigotConfig.simulationDistance, // Spigot flag, progressListener, - this.entityManager::updateChunkStatus, + null, // Paper - rewrite chunk system () -> server.overworld().getDataStorage() ); this.chunkSource.getGeneratorState().ensureStructuresGenerated(); @@ -437,6 +665,20 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe this.randomSequences = Objects.requireNonNullElseGet( randomSequences, () -> this.getDataStorage().computeIfAbsent(RandomSequences.factory(seed), "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(levelStorageAccess.getLevelId(), dimension, "entities"), + levelStorageAccess.getDimensionPath(dimension).resolve("entities"), + server.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 } @@ -560,8 +802,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe profilerFiller.push("checkDespawn"); entity.checkDespawn(); profilerFiller.pop(); - if (entity instanceof ServerPlayer - || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { + if (true) { // Paper - rewrite chunk system Entity vehicle = entity.getVehicle(); if (vehicle != null) { if (!vehicle.isRemoved() && vehicle.hasPassenger(entity)) { @@ -584,13 +825,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } profilerFiller.push("entityManagement"); - this.entityManager.tick(); + // Paper - rewrite chunk system profilerFiller.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() { @@ -621,14 +865,67 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList()).forEach(player -> player.stopSleepInBed(false, false)); } + // Paper start - optimise random ticking + private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); + + private void optimiseRandomTick(final LevelChunk chunk, final int tickSpeed) { + final LevelChunkSection[] sections = chunk.getSections(); + final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection((ServerLevel)(Object)this); + final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; + final boolean doubleTickFluids = !ca.spottedleaf.moonrise.common.PlatformHooks.get().configFixMC224294(); + + final ChunkPos cpos = chunk.getPos(); + final int offsetX = cpos.x << 4; + final int offsetZ = cpos.z << 4; + + for (int sectionIndex = 0, sectionsLen = sections.length; sectionIndex < sectionsLen; sectionIndex++) { + final int offsetY = (sectionIndex + minSection) << 4; + final LevelChunkSection section = sections[sectionIndex]; + final net.minecraft.world.level.chunk.PalettedContainer states = section.states; + if (!section.isRandomlyTickingBlocks()) { + continue; + } + + final ca.spottedleaf.moonrise.common.list.ShortList tickList = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getTickingBlockList(); + + for (int i = 0; i < tickSpeed; ++i) { + final int tickingBlocks = tickList.size(); + final int index = simpleRandom.nextInt() & ((16 * 16 * 16) - 1); + + if (index >= tickingBlocks) { + // most of the time we fall here + continue; + } + + final int location = (int)tickList.getRaw(index) & 0xFFFF; + final BlockState state = states.get(location); + + // do not use a mutable pos, as some random tick implementations store the input without calling immutable()! + final BlockPos pos = new BlockPos((location & 15) | offsetX, ((location >>> (4 + 4)) & 15) | offsetY, ((location >>> 4) & 15) | offsetZ); + + state.randomTick((ServerLevel)(Object)this, pos, simpleRandom); + if (doubleTickFluids) { + final FluidState fluidState = state.getFluidState(); + if (fluidState.isRandomlyTicking()) { + fluidState.randomTick((ServerLevel)(Object)this, pos, simpleRandom); + } + } + } + } + + return; + } + // Paper end - optimise random ticking + public void tickChunk(LevelChunk chunk, int randomTickSpeed) { + final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking ChunkPos pos = chunk.getPos(); boolean isRaining = this.isRaining(); int minBlockX = pos.getMinBlockX(); int minBlockZ = pos.getMinBlockZ(); ProfilerFiller profilerFiller = Profiler.get(); profilerFiller.push("thunder"); - if (!this.paperConfig().environment.disableThunder && isRaining && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder + if (!this.paperConfig().environment.disableThunder && isRaining && this.isThundering() && this.spigotConfig.thunderChance > 0 && simpleRandom.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder // Paper - optimise random ticking BlockPos blockPos = this.findLightningTargetAround(this.getBlockRandomPos(minBlockX, 0, minBlockZ, 15)); if (this.isRainingAt(blockPos)) { DifficultyInstance currentDifficultyAt = this.getCurrentDifficultyAt(blockPos); @@ -658,7 +955,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe if (!this.paperConfig().environment.disableIceAndSnow) { // Paper - Option to disable ice and snow for (int i = 0; i < randomTickSpeed; i++) { - if (this.random.nextInt(48) == 0) { + if (simpleRandom.nextInt(48) == 0) { // Paper - optimise random ticking this.tickPrecipitation(this.getBlockRandomPos(minBlockX, 0, minBlockZ, 15)); } } @@ -666,33 +963,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe profilerFiller.popPush("tickBlocks"); if (randomTickSpeed > 0) { - LevelChunkSection[] sections = chunk.getSections(); - - for (int i1 = 0; i1 < sections.length; i1++) { - LevelChunkSection levelChunkSection = sections[i1]; - if (levelChunkSection.isRandomlyTicking()) { - int sectionYFromSectionIndex = chunk.getSectionYFromSectionIndex(i1); - int blockPosCoord = SectionPos.sectionToBlockCoord(sectionYFromSectionIndex); - - for (int i2 = 0; i2 < randomTickSpeed; i2++) { - BlockPos blockRandomPos = this.getBlockRandomPos(minBlockX, blockPosCoord, minBlockZ, 15); - profilerFiller.push("randomTick"); - BlockState blockState = levelChunkSection.getBlockState( - blockRandomPos.getX() - minBlockX, blockRandomPos.getY() - blockPosCoord, blockRandomPos.getZ() - minBlockZ - ); - if (blockState.isRandomlyTicking()) { - blockState.randomTick(this, blockRandomPos, this.random); - } - - FluidState fluidState = blockState.getFluidState(); - if (fluidState.isRandomlyTicking()) { - fluidState.randomTick(this, blockRandomPos, this.random); - } - - profilerFiller.pop(); - } - } - } + this.optimiseRandomTick(chunk, randomTickSpeed); // Paper - optimise random ticking } profilerFiller.pop(); @@ -946,6 +1217,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe if (fluidState.is(fluid)) { fluidState.tick(this, pos, blockState); } + // 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 + } private void tickBlock(BlockPos pos, Block block) { @@ -953,6 +1230,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe if (blockState.is(block)) { blockState.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 + } public void tickNonPassenger(Entity entity) { @@ -1007,6 +1290,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } public void save(@Nullable ProgressListener progress, boolean flush, boolean skipSave) { + // Paper start - add close param + this.save(progress, flush, skipSave, false); + } + public void save(@Nullable ProgressListener progress, boolean flush, boolean skipSave, boolean close) { + // Paper end - add close param ServerChunkCache chunkSource = this.getChunkSource(); if (!skipSave) { org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(this.getWorld())); // CraftBukkit @@ -1019,13 +1307,18 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe progress.progressStage(Component.translatable("menu.savingChunks")); } - chunkSource.save(flush); - if (flush) { - this.entityManager.saveAll(); - } else { - this.entityManager.autoSave(); + if (!close) { chunkSource.save(flush); } // Paper - add close param + // Paper - rewrite chunk system + } + // Paper start - add close param + if (close) { + try { + chunkSource.close(!skipSave); + } catch (IOException never) { + throw new RuntimeException(never); } } + // Paper end - add close param // CraftBukkit start - moved from MinecraftServer.saveChunks ServerLevel worldserver1 = this; @@ -1156,7 +1449,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 @@ -1187,7 +1480,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 } } @@ -1198,7 +1491,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe public boolean tryAddFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) { // CraftBukkit end - if (entity.getSelfAndPassengers().map(Entity::getUUID).anyMatch(this.entityManager::isLoaded)) { + if (entity.getSelfAndPassengers().map(Entity::getUUID).anyMatch(this.moonrise$getEntityLookup()::hasEntity)) { // Paper - rewrite chunk system return false; } else { this.addFreshEntityWithPassengers(entity, reason); // CraftBukkit @@ -1933,7 +2226,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())); @@ -1951,13 +2244,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe Path path1 = path.resolve("chunks.csv"); try (Writer bufferedWriter2 = Files.newBufferedWriter(path1)) { - chunkMap.dumpChunks(bufferedWriter2); + //chunkMap.dumpChunks(bufferedWriter2); // Paper - rewrite chunk system } Path path2 = path.resolve("entity_chunks.csv"); try (Writer bufferedWriter3 = Files.newBufferedWriter(path2)) { - this.entityManager.dumpSections(bufferedWriter3); + //this.entityManager.dumpSections(bufferedWriter3); // Paper - rewrite chunk system } Path path3 = path.resolve("entities.csv"); @@ -2066,8 +2359,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe 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(), - getTypeCount(this.entityManager.getEntityGetter().getAll(), entity -> BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString()), + this.moonrise$getEntityLookup().getDebugInfo(), // Paper - rewrite chunk system + getTypeCount(this.moonrise$getEntityLookup().getAll(), entity -> BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString()), // Paper - rewrite chunk system this.blockEntityTickers.size(), getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), this.getBlockTicks().count(), @@ -2099,15 +2392,25 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Override public LevelEntityGetter getEntities() { org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot - return this.entityManager.getEntityGetter(); + return this.moonrise$getEntityLookup(); // Paper - rewrite chunk system } public void addLegacyChunkEntities(Stream entities) { - this.entityManager.addLegacyChunkEntities(entities); + // Paper start - add chunkpos param + this.addLegacyChunkEntities(entities, null); + } + public void addLegacyChunkEntities(Stream entities, ChunkPos chunkPos) { + // Paper end - add chunkpos param + this.moonrise$getEntityLookup().addLegacyChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system } public void addWorldGenChunkEntities(Stream entities) { - this.entityManager.addWorldGenChunkEntities(entities); + // Paper start - add chunkpos param + this.addWorldGenChunkEntities(entities, null); + } + public void addWorldGenChunkEntities(Stream entities, ChunkPos chunkPos) { + // Paper end - add chunkpos param + this.moonrise$getEntityLookup().addWorldGenChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system } public void startTickingChunk(LevelChunk chunk) { @@ -2125,32 +2428,45 @@ 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() { - return "Chunks[S] W: " + this.chunkSource.gatherStats() + " E: " + this.entityManager.gatherStats(); + return "Chunks[S] W: " + this.chunkSource.gatherStats() + " 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 chunkPos) { - return this.entityManager.canPositionTick(chunkPos); + // 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(chunkPos)); + return chunkHolder != null && chunkHolder.isEntityTickingReady(); + // Paper end - rewrite chunk system } @Override @@ -2204,7 +2520,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Override public CrashReportCategory fillReportDetails(CrashReport report) { CrashReportCategory crashReportCategory = super.fillReportDetails(report); - crashReportCategory.setDetail("Loaded entity count", () -> String.valueOf(this.entityManager.count())); + crashReportCategory.setDetail("Loaded entity count", () -> String.valueOf(this.moonrise$getEntityLookup().getEntityCount())); // Paper - rewrite chunk system return crashReportCategory; } diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java index ff5889f8fed0707a6654d9d21862e32e2ebc866d..e61fe83479f095e8addbd3e8f1d5179c998ae1eb 100644 --- a/net/minecraft/server/level/ServerPlayer.java +++ b/net/minecraft/server/level/ServerPlayer.java @@ -178,7 +178,7 @@ import net.minecraft.world.scores.Team; import net.minecraft.world.scores.criteria.ObjectiveCriteria; import org.slf4j.Logger; -public class ServerPlayer extends Player { +public class ServerPlayer extends 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; private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10; @@ -388,6 +388,36 @@ public class ServerPlayer extends 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 level, GameProfile gameProfile, ClientInformation clientInformation) { super(level, level.getSharedSpawnPos(), level.getSharedSpawnAngle(), gameProfile); this.textFilter = server.createTextFilterForPlayer(this); diff --git a/net/minecraft/server/level/ThreadedLevelLightEngine.java b/net/minecraft/server/level/ThreadedLevelLightEngine.java index 11a264ef2f43c2b00741397c9c9ea5393afad6ab..5c9ac44a3b4bc8e047feaf61a94eb163761498a2 100644 --- a/net/minecraft/server/level/ThreadedLevelLightEngine.java +++ b/net/minecraft/server/level/ThreadedLevelLightEngine.java @@ -22,23 +22,134 @@ import net.minecraft.world.level.chunk.LightChunkGetter; import net.minecraft.world.level.lighting.LevelLightEngine; import org.slf4j.Logger; -public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable { +public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable, ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider { // Paper - rewrite chunk system public static final int DEFAULT_BATCH_SIZE = 1000; private static final Logger LOGGER = LogUtils.getLogger(); - private final ConsecutiveExecutor consecutiveExecutor; - private final ObjectList> lightTasks = new ObjectArrayList<>(); + // Paper - rewrite chunk sytem private final ChunkMap chunkMap; - private final ChunkTaskDispatcher taskDispatcher; + // Paper - rewrite chunk sytem private final int taskPerBatch = 1000; - private final AtomicBoolean scheduled = new AtomicBoolean(); + // Paper - rewrite chunk sytem + + // Paper start - rewrite chunk system + private final java.util.concurrent.atomic.AtomicLong chunkWorkCounter = new java.util.concurrent.atomic.AtomicLong(); + private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, + final java.util.function.Supplier supplier) { + final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld(); + + final ChunkAccess center = this.starlight$getLightEngine().getAnyChunkNow(chunkX, chunkZ); + if (center == null || !center.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) { + // do not accept updates in unlit chunks, unless we might be generating a chunk + return; + } + + final ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.ServerLightQueue.ServerChunkTasks scheduledTask = (ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.ServerLightQueue.ServerChunkTasks)supplier.get(); + + if (scheduledTask == null) { + // not scheduled + return; + } + + if (!scheduledTask.markTicketAdded()) { + // ticket already added + return; + } + + final Long ticketId = Long.valueOf(this.chunkWorkCounter.getAndIncrement()); + final ChunkPos pos = new ChunkPos(chunkX, chunkZ); + world.getChunkSource().addRegionTicket(ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId); + + scheduledTask.queueOrRunTask(() -> { + world.getChunkSource().removeRegionTicket(ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId); + }); + } + + @Override + public final int starlight$serverRelightChunks(final java.util.Collection chunks0, + final java.util.function.Consumer chunkLightCallback, + final java.util.function.IntConsumer onComplete) { + final java.util.Set chunks = new java.util.LinkedHashSet<>(chunks0); + final java.util.Map ticketIds = new java.util.HashMap<>(); + final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld(); + + for (final java.util.Iterator iterator = chunks.iterator(); iterator.hasNext();) { + final ChunkPos pos = iterator.next(); + + final Long id = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getNextChunkRelightId(); + world.getChunkSource().addRegionTicket(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, id); + ticketIds.put(pos, id); + + final ChunkAccess chunk = (ChunkAccess)world.getChunkSource().getChunkForLighting(pos.x, pos.z); + if (chunk == null || !chunk.isLightCorrect() || !chunk.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) { + // cannot relight this chunk + iterator.remove(); + ticketIds.remove(pos); + world.getChunkSource().removeRegionTicket(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, id); + continue; + } + } + + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().radiusAwareScheduler.queueInfiniteRadiusTask(() -> { + ThreadedLevelLightEngine.this.starlight$getLightEngine().relightChunks( + chunks, + (final ChunkPos pos) -> { + if (chunkLightCallback != null) { + chunkLightCallback.accept(pos); + } + + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().scheduleChunkTask(pos.x, pos.z, () -> { + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder( + pos.x, pos.z + ); + + if (chunkHolder == null) { + return; + } + + final java.util.List players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)chunkHolder.vanillaChunkHolder).moonrise$getPlayers(false); + + if (players.isEmpty()) { + return; + } + + final net.minecraft.network.protocol.Packet relightPacket = new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket( + pos, (ThreadedLevelLightEngine)(Object)ThreadedLevelLightEngine.this, + null, null + ); + + for (final ServerPlayer player : players) { + final net.minecraft.server.network.ServerGamePacketListenerImpl conn = player.connection; + if (conn != null) { + conn.send(relightPacket); + } + } + }); + }, + (final int relight) -> { + if (onComplete != null) { + onComplete.accept(relight); + } + + for (final java.util.Map.Entry entry : ticketIds.entrySet()) { + world.getChunkSource().removeRegionTicket( + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, entry.getKey(), + ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, entry.getValue() + ); + } + } + ); + }); + + return chunks.size(); + } + // Paper end - rewrite chunk system public ThreadedLevelLightEngine( LightChunkGetter lightChunkGetter, ChunkMap chunkMap, boolean skyLight, ConsecutiveExecutor consecutiveExecutor, ChunkTaskDispatcher taskDispatcher ) { super(lightChunkGetter, true, skyLight); this.chunkMap = chunkMap; - this.taskDispatcher = taskDispatcher; - this.consecutiveExecutor = consecutiveExecutor; + // Paper - rewrite chunk system } @Override @@ -52,163 +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 chunkPos) { - this.addTask(chunkPos.x, chunkPos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { - super.retainData(chunkPos, false); - super.setLightEnabled(chunkPos, false); - - for (int lightSection = this.getMinLightSection(); lightSection < this.getMaxLightSection(); lightSection++) { - super.queueSectionData(LightLayer.BLOCK, SectionPos.of(chunkPos, lightSection), null); - super.queueSectionData(LightLayer.SKY, SectionPos.of(chunkPos, lightSection), null); - } - - for (int lightSection = this.levelHeightAccessor.getMinSectionY(); lightSection <= this.levelHeightAccessor.getMaxSectionY(); lightSection++) { - super.updateSectionStatus(SectionPos.of(chunkPos, lightSection), true); - } - }, () -> "updateChunkStatus " + chunkPos + " true")); + // Paper - rewrite chunk system } @Override public void updateSectionStatus(SectionPos pos, boolean isEmpty) { - this.addTask( - pos.x(), - pos.z(), - () -> 0, - ThreadedLevelLightEngine.TaskType.PRE_UPDATE, - Util.name(() -> super.updateSectionStatus(pos, isEmpty), () -> "updateSectionStatus " + pos + " " + isEmpty) - ); + // Paper start - rewrite chunk system + this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> { + return ThreadedLevelLightEngine.this.starlight$getLightEngine().sectionChange(pos, isEmpty); + }); + // 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 chunkPos, boolean lightEnabled) { - this.addTask( - chunkPos.x, - chunkPos.z, - ThreadedLevelLightEngine.TaskType.PRE_UPDATE, - Util.name(() -> super.setLightEnabled(chunkPos, lightEnabled), () -> "enableLight " + chunkPos + " " + lightEnabled) - ); + // Paper start - rewrite chunk system } @Override public void queueSectionData(LightLayer lightLayer, SectionPos sectionPos, @Nullable DataLayer dataLayer) { - this.addTask( - sectionPos.x(), - sectionPos.z(), - () -> 0, - ThreadedLevelLightEngine.TaskType.PRE_UPDATE, - Util.name(() -> super.queueSectionData(lightLayer, sectionPos, dataLayer), () -> "queueData " + sectionPos) - ); + // Paper start - rewrite chunk system } private void addTask(int chunkX, int chunkZ, ThreadedLevelLightEngine.TaskType type, Runnable task) { - this.addTask(chunkX, chunkZ, this.chunkMap.getChunkQueueLevel(ChunkPos.asLong(chunkX, chunkZ)), type, task); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } private void addTask(int chunkX, int chunkZ, IntSupplier queueLevelSupplier, ThreadedLevelLightEngine.TaskType type, Runnable task) { - this.taskDispatcher.submit(() -> { - this.lightTasks.add(Pair.of(type, task)); - if (this.lightTasks.size() >= 1000) { - this.runUpdate(); - } - }, ChunkPos.asLong(chunkX, chunkZ), queueLevelSupplier); + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } @Override public void retainData(ChunkPos pos, boolean retain) { - this.addTask( - pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> super.retainData(pos, retain), () -> "retainData " + pos) - ); + // Paper start - rewrite chunk system } public CompletableFuture initializeLight(ChunkAccess chunk, boolean lightEnabled) { - ChunkPos pos = chunk.getPos(); - this.addTask(pos.x, pos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { - LevelChunkSection[] sections = chunk.getSections(); - - for (int i = 0; i < chunk.getSectionsCount(); i++) { - LevelChunkSection levelChunkSection = sections[i]; - if (!levelChunkSection.hasOnlyAir()) { - int sectionYFromSectionIndex = this.levelHeightAccessor.getSectionYFromSectionIndex(i); - super.updateSectionStatus(SectionPos.of(pos, sectionYFromSectionIndex), false); - } - } - }, () -> "initializeLight: " + pos)); - return CompletableFuture.supplyAsync(() -> { - super.setLightEnabled(pos, lightEnabled); - super.retainData(pos, false); - return chunk; - }, task -> this.addTask(pos.x, pos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task)); + return CompletableFuture.completedFuture(chunk); // Paper start - rewrite chunk system } public CompletableFuture lightChunk(ChunkAccess chunk, boolean isLighted) { - ChunkPos pos = chunk.getPos(); - chunk.setLightCorrect(false); - this.addTask(pos.x, pos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { - if (!isLighted) { - super.propagateLightSources(pos); - } - }, () -> "lightChunk " + pos + " " + isLighted)); - return CompletableFuture.supplyAsync(() -> { - chunk.setLightCorrect(true); - return chunk; - }, task -> this.addTask(pos.x, pos.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 min = Math.min(this.lightTasks.size(), 1000); - ObjectListIterator> objectListIterator = this.lightTasks.iterator(); - - int i; - for (i = 0; objectListIterator.hasNext() && i < min; i++) { - Pair pair = objectListIterator.next(); - if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.PRE_UPDATE) { - pair.getSecond().run(); - } - } - - objectListIterator.back(i); - super.runLightUpdates(); - - for (int var5 = 0; objectListIterator.hasNext() && var5 < min; var5++) { - Pair pair = objectListIterator.next(); - if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.POST_UPDATE) { - pair.getSecond().run(); - } - - objectListIterator.remove(); - } + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } public CompletableFuture waitForPendingTasks(int x, int z) { - return CompletableFuture.runAsync(() -> {}, task -> this.addTask(x, z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task)); + 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 e574f217386d677400b4c093d50261045df06d5c..ed8a3d5bd25909ee4648b1ec2ee66878198a1d8a 100644 --- a/net/minecraft/server/level/Ticket.java +++ b/net/minecraft/server/level/Ticket.java @@ -2,13 +2,25 @@ package net.minecraft.server.level; import java.util.Objects; -public final class Ticket implements Comparable> { +public final class Ticket implements Comparable>, ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket { // Paper - rewrite chunk system private final TicketType type; private final int ticketLevel; public final T key; - private long createdTick; + // Paper start - rewrite chunk system + private long removeDelay; - protected Ticket(TicketType type, int ticketLevel, T key) { + @Override + public final long moonrise$getRemoveDelay() { + return this.removeDelay; + } + + @Override + public final void moonrise$setRemoveDelay(final long removeDelay) { + this.removeDelay = removeDelay; + } + // Paper end - rewrite chunk system + + public Ticket(TicketType type, int ticketLevel, T key) { // Paper - public this.type = type; this.ticketLevel = ticketLevel; this.key = key; @@ -41,7 +53,7 @@ public final class Ticket implements Comparable> { @Override public String toString() { - return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] at " + this.createdTick; + return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] to die in " + this.removeDelay; // Paper - rewrite chunk system } public TicketType getType() { @@ -53,11 +65,10 @@ public final class Ticket implements Comparable> { } protected void setCreatedTick(long timestamp) { - this.createdTick = timestamp; + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } protected boolean timedOut(long currentTime) { - long timeout = this.type.timeout(); - return timeout != 0L && currentTime - this.createdTick > timeout; + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } } diff --git a/net/minecraft/server/level/WorldGenRegion.java b/net/minecraft/server/level/WorldGenRegion.java index 4eb040006f5d41b47e5ac9df5d9f19c4315d6343..7fa41dea184b01891f45d8e404bc1cba19cf1bcf 100644 --- a/net/minecraft/server/level/WorldGenRegion.java +++ b/net/minecraft/server/level/WorldGenRegion.java @@ -78,6 +78,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 level, StaticCache2D cache, ChunkStep generatingStep, ChunkAccess center) { this.generatingStep = generatingStep; this.cache = cache; diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java index b0df94fb5f933491af8b024cc8ecf8b98e448f16..0a2f4af548561144607d24d3bd92270ce630fc95 100644 --- a/net/minecraft/server/players/PlayerList.java +++ b/net/minecraft/server/players/PlayerList.java @@ -1317,7 +1317,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 for (ServerLevel serverLevel : this.server.getAllLevels()) { if (serverLevel != null) { @@ -1328,7 +1328,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 for (ServerLevel serverLevel : this.server.getAllLevels()) { if (serverLevel != null) { diff --git a/net/minecraft/util/BitStorage.java b/net/minecraft/util/BitStorage.java index 32fe9b22e1d3a422dd80c64d61156dbc7241ba20..02502d50f0255f5bbcc0ecb965abb48cc1a112da 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[] array); BitStorage copy(); + + // Paper start - block counting + // provide default impl in case mods implement this... + @Override + public default it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap moonrise$countEntries() { + final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(); + + final int size = this.getSize(); + for (int index = 0; index < size; ++index) { + final int paletteIdx = this.get(index); + ret.computeIfAbsent(paletteIdx, (final int key) -> { + return new it.unimi.dsi.fastutil.shorts.ShortArrayList(); + }).add((short)index); + } + + return ret; + } + // Paper end - block counting } diff --git a/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java b/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java index 4a7c83c56dfbff59af71c3cd2fa4205c9a22bdc7..f28fbf81a417a678726d3f77b3999054676d522e 100644 --- a/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java +++ b/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java @@ -7,7 +7,7 @@ import java.util.Iterator; import javax.annotation.Nullable; import net.minecraft.core.IdMap; -public class CrudeIncrementalIntIdentityHashBiMap implements IdMap { +public class CrudeIncrementalIntIdentityHashBiMap implements IdMap, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // Paper - optimise palette reads private static final int NOT_FOUND = -1; private static final Object EMPTY_SLOT = null; private static final float LOADFACTOR = 0.8F; @@ -17,6 +17,16 @@ public class CrudeIncrementalIntIdentityHashBiMap implements IdMap { private int nextId; private int size; + // Paper start - optimise palette reads + private ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData reference; + + @Override + public final K[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData src) { + this.reference = src; + return this.byId; + } + // Paper end - optimise palette reads + private CrudeIncrementalIntIdentityHashBiMap(int size) { this.keys = (K[])(new Object[size]); this.values = new int[size]; @@ -88,6 +98,12 @@ public class CrudeIncrementalIntIdentityHashBiMap implements IdMap { this.byId = crudeIncrementalIntIdentityHashBiMap.byId; this.nextId = crudeIncrementalIntIdentityHashBiMap.nextId; this.size = crudeIncrementalIntIdentityHashBiMap.size; + // Paper start - optimise palette reads + final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData ref = this.reference; + if (ref != null) { + ref.moonrise$setPalette(this.byId); + } + // Paper end - optimise palette reads } public void addMapping(K object, int intKey) { diff --git a/net/minecraft/util/SimpleBitStorage.java b/net/minecraft/util/SimpleBitStorage.java index 6fb3a3f167d8cbaa78135af0c180b592661e2c1d..e6306a68c8652d4c5d22d5ecb1416f5f931f76ee 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 bits, int size, int[] data) { this(bits, size); int i = 0; @@ -261,6 +275,13 @@ public class SimpleBitStorage implements BitStorage { } else { this.data = new long[i1]; } + // 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) { @@ -269,28 +290,51 @@ public class SimpleBitStorage implements BitStorage { @Override public final int getAndSet(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage - int i = this.cellIndex(index); - long l = this.data[i]; - int i1 = (index - i * this.valuesPerLong) * this.bits; - int i2 = (int)(l >> i1 & this.mask); - this.data[i] = l & ~(this.mask << i1) | (value & this.mask) << i1; - return i2; + // 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 - int i = this.cellIndex(index); - long l = this.data[i]; - int i1 = (index - i * this.valuesPerLong) * this.bits; - this.data[i] = l & ~(this.mask << i1) | (value & this.mask) << i1; + // 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 - int i = this.cellIndex(index); - long l = this.data[i]; - int i1 = (index - i * this.valuesPerLong) * this.bits; - return (int)(l >> i1 & 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 @@ -355,6 +399,67 @@ public class SimpleBitStorage implements BitStorage { return new SimpleBitStorage(this.bits, this.size, (long[])this.data.clone()); } + // Paper start - block counting + @Override + public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap moonrise$countEntries() { + final int valuesPerLong = this.valuesPerLong; + final int bits = this.bits; + final long mask = (1L << bits) - 1L; + final int size = this.size; + + if (bits <= 6) { + final it.unimi.dsi.fastutil.shorts.ShortArrayList[] byId = new it.unimi.dsi.fastutil.shorts.ShortArrayList[1 << bits]; + final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1 << bits); + + int index = 0; + + for (long value : this.data) { + int li = 0; + do { + final int paletteIdx = (int)(value & mask); + value >>= bits; + ++li; + + final it.unimi.dsi.fastutil.shorts.ShortArrayList coords = byId[paletteIdx]; + if (coords != null) { + coords.add((short)index++); + continue; + } else { + final it.unimi.dsi.fastutil.shorts.ShortArrayList newCoords = new it.unimi.dsi.fastutil.shorts.ShortArrayList(64); + byId[paletteIdx] = newCoords; + newCoords.add((short)index++); + ret.put(paletteIdx, newCoords); + continue; + } + } while (li < valuesPerLong && index < size); + } + + return ret; + } else { + final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>( + 1 << 6 + ); + + int index = 0; + + for (long value : this.data) { + int li = 0; + do { + final int paletteIdx = (int)(value & mask); + value >>= bits; + ++li; + + ret.computeIfAbsent(paletteIdx, (final int key) -> { + return new it.unimi.dsi.fastutil.shorts.ShortArrayList(64); + }).add((short)index++); + } while (li < valuesPerLong && index < size); + } + + return ret; + } + } + // Paper end - block counting + public static class InitializationException extends RuntimeException { InitializationException(String message) { super(message); diff --git a/net/minecraft/util/SortedArraySet.java b/net/minecraft/util/SortedArraySet.java index 2c6b35b86eed9002016b8228c3195f8033d219ca..339b19e88567be382e550ed54477fabd58d51faa 100644 --- a/net/minecraft/util/SortedArraySet.java +++ b/net/minecraft/util/SortedArraySet.java @@ -8,12 +8,89 @@ import java.util.Iterator; import java.util.NoSuchElementException; import javax.annotation.Nullable; -public class SortedArraySet extends AbstractSet { +public class SortedArraySet extends AbstractSet implements ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet { // Paper - rewrite chunk system private static final int DEFAULT_INITIAL_CAPACITY = 10; private final Comparator comparator; T[] contents; int size; + // Paper start - rewrite chunk system + @Override + public final boolean removeIf(final java.util.function.Predicate filter) { + // prev. impl used an iterator, which could be n^2 and creates garbage + int i = 0; + final int len = this.size; + final T[] backingArray = this.contents; + + for (;;) { + if (i >= len) { + return false; + } + if (!filter.test(backingArray[i])) { + ++i; + continue; + } + break; + } + + // we only want to write back to backingArray if we really need to + + int lastIndex = i; // this is where new elements are shifted to + + for (; i < len; ++i) { + final T curr = backingArray[i]; + if (!filter.test(curr)) { // if test throws we're screwed + backingArray[lastIndex++] = curr; + } + } + + // cleanup end + Arrays.fill(backingArray, lastIndex, len, null); + this.size = lastIndex; + return true; + } + + @Override + public final T moonrise$replace(final T object) { + final int index = this.findIndex(object); + if (index >= 0) { + final T old = this.contents[index]; + this.contents[index] = object; + return old; + } else { + this.addInternal(object, getInsertionPosition(index)); + return object; + } + } + + @Override + public final T moonrise$removeAndGet(final T object) { + int i = this.findIndex(object); + if (i >= 0) { + final T ret = this.contents[i]; + this.removeInternal(i); + return ret; + } else { + return null; + } + } + + @Override + public final SortedArraySet moonrise$copy() { + final SortedArraySet ret = SortedArraySet.create(this.comparator, 0); + + ret.size = this.size; + ret.contents = Arrays.copyOf(this.contents, this.size); + + return ret; + } + + @Override + public Object[] moonrise$copyBackingArray() { + return this.contents.clone(); + } + // Paper end - rewrite chunk system + private SortedArraySet(int initialCapacity, Comparator comparator) { this.comparator = comparator; if (initialCapacity < 0) { diff --git a/net/minecraft/util/ZeroBitStorage.java b/net/minecraft/util/ZeroBitStorage.java index 8cc5c0716392ba06501542ff5cbe71ee43979e5d..09fd99c9cbd23b5f3c899bfb00c9b89651948ed8 100644 --- a/net/minecraft/util/ZeroBitStorage.java +++ b/net/minecraft/util/ZeroBitStorage.java @@ -62,4 +62,22 @@ public class ZeroBitStorage implements BitStorage { public BitStorage copy() { return this; } + + // Paper start - block counting + @Override + public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap moonrise$countEntries() { + final int size = this.size; + + final short[] raw = new short[size]; + for (int i = 0; i < size; ++i) { + raw[i] = (short)i; + } + + final it.unimi.dsi.fastutil.shorts.ShortArrayList coordinates = it.unimi.dsi.fastutil.shorts.ShortArrayList.wrap(raw, size); + + final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1); + ret.put(0, coordinates); + return ret; + } + // Paper end - block counting } diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java index 2b53c5f8ff2ce1097e38ea7cb6f83fbb1d661831..c166a0ec56ff2d4885459d789ddcd5f0ebdfc56a 100644 --- a/net/minecraft/world/entity/Entity.java +++ b/net/minecraft/world/entity/Entity.java @@ -135,7 +135,7 @@ import net.minecraft.world.scores.ScoreHolder; import net.minecraft.world.scores.Team; import org.slf4j.Logger; -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; @@ -146,7 +146,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 @@ -159,61 +169,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 @@ -419,6 +375,156 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess return this.dimensions.makeBoundingBox(x, y, z); } // Paper end + // Paper start - rewrite chunk system + private final boolean isHardColliding = this.moonrise$isHardCollidingUncached(); + private net.minecraft.server.level.FullChunkStatus chunkStatus; + private ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData; + private int sectionX = Integer.MIN_VALUE; + private int sectionY = Integer.MIN_VALUE; + private int sectionZ = Integer.MIN_VALUE; + private boolean updatingSectionStatus; + + @Override + public final boolean moonrise$isHardColliding() { + return this.isHardColliding; + } + + @Override + public final net.minecraft.server.level.FullChunkStatus moonrise$getChunkStatus() { + return this.chunkStatus; + } + + @Override + public final void moonrise$setChunkStatus(final net.minecraft.server.level.FullChunkStatus status) { + this.chunkStatus = status; + } + + @Override + public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$getChunkData() { + return this.chunkData; + } + + @Override + public final void moonrise$setChunkData(final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData) { + this.chunkData = chunkData; + } + + @Override + public final int moonrise$getSectionX() { + return this.sectionX; + } + + @Override + public final void moonrise$setSectionX(final int x) { + this.sectionX = x; + } + + @Override + public final int moonrise$getSectionY() { + return this.sectionY; + } + + @Override + public final void moonrise$setSectionY(final int y) { + this.sectionY = y; + } + + @Override + public final int moonrise$getSectionZ() { + return this.sectionZ; + } + + @Override + public final void moonrise$setSectionZ(final int z) { + this.sectionZ = z; + } + + @Override + public final boolean moonrise$isUpdatingSectionStatus() { + return this.updatingSectionStatus; + } + + @Override + public final void moonrise$setUpdatingSectionStatus(final boolean to) { + this.updatingSectionStatus = to; + } + + @Override + public final boolean moonrise$hasAnyPlayerPassengers() { + if (this.passengers.isEmpty()) { + return false; + } + return this.getIndirectPassengersStream().anyMatch((entity) -> entity instanceof Player); + } + // Paper end - rewrite chunk system + // Paper start - optimise collisions + private static float[] calculateStepHeights(final AABB box, final List voxels, final List aabbs, final float stepHeight, + final float collidedY) { + final FloatArraySet ret = new FloatArraySet(); + + for (int i = 0, len = voxels.size(); i < len; ++i) { + final VoxelShape shape = voxels.get(i); + + final double[] yCoords = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$rootCoordinatesY(); + final double yOffset = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$offsetY(); + + for (final double yUnoffset : yCoords) { + final double y = yUnoffset + yOffset; + + final float step = (float)(y - box.minY); + + if (step > stepHeight) { + break; + } + + if (step < 0.0f || !(step != collidedY)) { + continue; + } + + ret.add(step); + } + } + + for (int i = 0, len = aabbs.size(); i < len; ++i) { + final AABB shape = aabbs.get(i); + + final float step1 = (float)(shape.minY - box.minY); + final float step2 = (float)(shape.maxY - box.minY); + + if (!(step1 < 0.0f) && step1 != collidedY && !(step1 > stepHeight)) { + ret.add(step1); + } + + if (!(step2 < 0.0f) && step2 != collidedY && !(step2 > stepHeight)) { + ret.add(step2); + } + } + + final float[] steps = ret.toFloatArray(); + FloatArrays.unstableSort(steps); + return steps; + } + // Paper end - optimise collisions + // Paper start - optimise entity tracker + private net.minecraft.server.level.ChunkMap.TrackedEntity trackedEntity; + + @Override + public final net.minecraft.server.level.ChunkMap.TrackedEntity moonrise$getTrackedEntity() { + return this.trackedEntity; + } + + @Override + public final void moonrise$setTrackedEntity(final net.minecraft.server.level.ChunkMap.TrackedEntity trackedEntity) { + this.trackedEntity = trackedEntity; + } + + private static void collectIndirectPassengers(final List into, final List from) { + for (final Entity passenger : from) { + into.add(passenger); + collectIndirectPassengers(into, ((Entity)(Object)passenger).passengers); + } + } + // Paper end - optimise entity tracker public Entity(EntityType entityType, Level level) { this.type = entityType; @@ -1285,35 +1391,77 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess return distance; } - private Vec3 collide(Vec3 vec) { - AABB boundingBox = this.getBoundingBox(); - List entityCollisions = this.level().getEntityCollisions(this, boundingBox.expandTowards(vec)); - Vec3 vec3 = vec.lengthSqr() == 0.0 ? vec : collideBoundingBox(this, vec, boundingBox, this.level(), entityCollisions); - boolean flag = vec.x != vec3.x; - boolean flag1 = vec.y != vec3.y; - boolean flag2 = vec.z != vec3.z; - boolean flag3 = flag1 && vec.y < 0.0; - if (this.maxUpStep() > 0.0F && (flag3 || this.onGround()) && (flag || flag2)) { - AABB aabb = flag3 ? boundingBox.move(0.0, vec3.y, 0.0) : boundingBox; - AABB aabb1 = aabb.expandTowards(vec.x, this.maxUpStep(), vec.z); - if (!flag3) { - aabb1 = aabb1.expandTowards(0.0, -1.0E-5F, 0.0); - } + // Paper start - optimise collisions + private Vec3 collide(Vec3 movement) { + final boolean xZero = movement.x == 0.0; + final boolean yZero = movement.y == 0.0; + final boolean zZero = movement.z == 0.0; + if (xZero & yZero & zZero) { + return movement; + } - List list = collectColliders(this, this.level, entityCollisions, aabb1); - float f = (float)vec3.y; - float[] floats = collectCandidateStepUpHeights(aabb, list, this.maxUpStep(), f); + final AABB currentBox = this.getBoundingBox(); - for (float f1 : floats) { - Vec3 vec31 = collideWithShapes(new Vec3(vec.x, f1, vec.z), aabb, list); - if (vec31.horizontalDistanceSqr() > vec3.horizontalDistanceSqr()) { - double d = boundingBox.minY - aabb.minY; - return vec31.add(0.0, -d, 0.0); - } + final List potentialCollisionsVoxel = new ArrayList<>(); + final List potentialCollisionsBB = new ArrayList<>(); + + 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); + } + + final List entityAABBs = new ArrayList<>(); + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getEntityHardCollisions( + this.level, (Entity)(Object)this, initialCollisionBox, entityAABBs, 0, null + ); + + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder( + this.level, (Entity)(Object)this, initialCollisionBox, potentialCollisionsVoxel, potentialCollisionsBB, + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, null + ); + potentialCollisionsBB.addAll(entityAABBs); + final Vec3 collided = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(movement, currentBox, potentialCollisionsVoxel, potentialCollisionsBB); + + final boolean collidedX = collided.x != movement.x; + final boolean collidedY = collided.y != movement.y; + final boolean collidedZ = collided.z != movement.z; + + final boolean collidedDownwards = collidedY && movement.y < 0.0; + + final double stepHeight; + + if ((!collidedDownwards && !this.onGround) || (!collidedX && !collidedZ) || (stepHeight = (double)this.maxUpStep()) <= 0.0) { + return collided; + } + + final AABB collidedYBox = collidedDownwards ? currentBox.move(0.0, collided.y, 0.0) : currentBox; + AABB stepRetrievalBox = collidedYBox.expandTowards(movement.x, stepHeight, movement.z); + if (!collidedDownwards) { + stepRetrievalBox = stepRetrievalBox.expandTowards(0.0, (double)-1.0E-5F, 0.0); + } + + final List stepVoxels = new ArrayList<>(); + final List stepAABBs = entityAABBs; + + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder( + this.level, (Entity)(Object)this, stepRetrievalBox, stepVoxels, stepAABBs, + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, null + ); + + for (final float step : calculateStepHeights(collidedYBox, stepVoxels, stepAABBs, (float)stepHeight, (float)collided.y)) { + final Vec3 stepResult = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(movement.x, (double)step, movement.z), collidedYBox, stepVoxels, stepAABBs); + if (stepResult.horizontalDistanceSqr() > collided.horizontalDistanceSqr()) { + return stepResult.add(0.0, collidedYBox.minY - currentBox.minY, 0.0); } } - return vec3; + return collided; + // Paper end - optimise collisions } private static float[] collectCandidateStepUpHeights(AABB box, List colliders, float deltaY, float maxUpStep) { @@ -2622,23 +2770,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 aabb = AABB.ofSize(this.getEyePosition(), f, 1.0E-6, f); - return BlockPos.betweenClosedStream(aabb) - .anyMatch( - pos -> { - BlockState blockState = this.level().getBlockState(pos); - return !blockState.isAir() - && blockState.isSuffocating(this.level(), pos) - && Shapes.joinIsNotEmpty( - blockState.getCollisionShape(this.level(), pos).move(pos.getX(), pos.getY(), pos.getZ()), Shapes.create(aabb), BooleanOp.AND - ); + } + + final double reducedWith = (double)(this.dimensions.width() * 0.8F); + final AABB boundingBox = AABB.ofSize(this.getEyePosition(), reducedWith, 1.0E-6D, reducedWith); + final Level world = this.level; + + if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(boundingBox)) { + return false; + } + + final int minBlockX = Mth.floor(boundingBox.minX); + final int minBlockY = Mth.floor(boundingBox.minY); + final int minBlockZ = Mth.floor(boundingBox.minZ); + + final int maxBlockX = Mth.floor(boundingBox.maxX); + final int maxBlockY = Mth.floor(boundingBox.maxY); + final int maxBlockZ = Mth.floor(boundingBox.maxZ); + + final int minChunkX = minBlockX >> 4; + final int minChunkY = minBlockY >> 4; + final int minChunkZ = minBlockZ >> 4; + + final int maxChunkX = maxBlockX >> 4; + final int maxChunkY = maxBlockY >> 4; + final int maxChunkZ = maxBlockZ >> 4; + + final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(world); + final net.minecraft.world.level.chunk.ChunkSource chunkSource = world.getChunkSource(); + final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); + + for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { + for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { + final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true).getSections(); + + for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { + final int sectionIdx = currChunkY - minSection; + if (sectionIdx < 0 || sectionIdx >= sections.length) { + continue; } - ); + final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx]; + if (section.hasOnlyAir()) { + // empty + continue; + } + + final net.minecraft.world.level.chunk.PalettedContainer blocks = section.states; + + final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) : 0; + final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) : 15; + final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) : 0; + final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) : 15; + final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) : 0; + final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) : 15; + + for (int currY = minYIterate; currY <= maxYIterate; ++currY) { + final int blockY = currY | (currChunkY << 4); + mutablePos.setY(blockY); + for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { + final int blockZ = currZ | (currChunkZ << 4); + mutablePos.setZ(blockZ); + for (int currX = minXIterate; currX <= maxXIterate; ++currX) { + final int blockX = currX | (currChunkX << 4); + mutablePos.setX(blockX); + + final BlockState blockState = blocks.get((currX) | (currZ << 4) | ((currY) << 8)); + + if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$emptyCollisionShape() + || !blockState.isSuffocating(world, mutablePos)) { + continue; + } + + // Yes, it does not use the Entity context stuff. + final VoxelShape collisionShape = blockState.getCollisionShape(world, mutablePos); + + if (collisionShape.isEmpty()) { + continue; + } + + final AABB toCollide = boundingBox.move(-(double)blockX, -(double)blockY, -(double)blockZ); + + final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)collisionShape).moonrise$getSingleAABBRepresentation(); + if (singleAABB != null) { + if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(singleAABB, toCollide)) { + return true; + } + continue; + } + + if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(collisionShape, toCollide)) { + return true; + } + continue; + } + } + } + } + } } + + return false; + // Paper end - optimise collisions } public InteractionResult interact(Player player, InteractionHand hand) { @@ -4062,15 +4297,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } public Iterable getIndirectPassengers() { - // Paper start - Optimize indirect passenger iteration - if (this.passengers.isEmpty()) { return ImmutableList.of(); } - ImmutableList.Builder indirectPassengers = ImmutableList.builder(); - for (Entity passenger : this.passengers) { - indirectPassengers.add(passenger); - indirectPassengers.addAll(passenger.getIndirectPassengers()); + // Paper start - optimise entity tracker + final List ret = new ArrayList<>(); + + if (this.passengers.isEmpty()) { + return ret; } - return indirectPassengers.build(); - // Paper end - Optimize indirect passenger iteration + + collectIndirectPassengers(ret, this.passengers); + + return ret; + // Paper end - optimise entity tracker } public int countPlayerPassengers() { @@ -4208,77 +4445,136 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess return Mth.lerp(partialTick, this.yRotO, this.yRot); } - public boolean updateFluidHeightAndDoFluidPushing(TagKey fluidTag, double motionScale) { + // Paper start - optimise collisions + public boolean updateFluidHeightAndDoFluidPushing(final TagKey fluid, final double flowScale) { if (this.touchingUnloadedChunk()) { return false; - } else { - AABB aabb = this.getBoundingBox().deflate(0.001); - int floor = Mth.floor(aabb.minX); - int ceil = Mth.ceil(aabb.maxX); - int floor1 = Mth.floor(aabb.minY); - int ceil1 = Mth.ceil(aabb.maxY); - int floor2 = Mth.floor(aabb.minZ); - int ceil2 = Mth.ceil(aabb.maxZ); - double d = 0.0; - boolean isPushedByFluid = this.isPushedByFluid(); - boolean flag = false; - Vec3 vec3 = Vec3.ZERO; - int i = 0; - BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos(); - - for (int i1 = floor; i1 < ceil; i1++) { - for (int i2 = floor1; i2 < ceil1; i2++) { - for (int i3 = floor2; i3 < ceil2; i3++) { - mutableBlockPos.set(i1, i2, i3); - FluidState fluidState = this.level().getFluidState(mutableBlockPos); - if (fluidState.is(fluidTag)) { - double d1 = i2 + fluidState.getHeight(this.level(), mutableBlockPos); - if (d1 >= aabb.minY) { - flag = true; - d = Math.max(d1 - aabb.minY, d); - if (isPushedByFluid) { - Vec3 flow = fluidState.getFlow(this.level(), mutableBlockPos); - if (d < 0.4) { - flow = flow.scale(d); - } + } + + final AABB boundingBox = this.getBoundingBox().deflate(1.0E-3); + + final Level world = this.level; + final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(world); + + final int minBlockX = Mth.floor(boundingBox.minX); + final int minBlockY = Math.max((minSection << 4), Mth.floor(boundingBox.minY)); + final int minBlockZ = Mth.floor(boundingBox.minZ); + + // note: bounds are exclusive in Vanilla, so we subtract 1 - our loop expects bounds to be inclusive + final int maxBlockX = Mth.ceil(boundingBox.maxX) - 1; + final int maxBlockY = Math.min((ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(world) << 4) | 15, Mth.ceil(boundingBox.maxY) - 1); + final int maxBlockZ = Mth.ceil(boundingBox.maxZ) - 1; + + final boolean isPushable = this.isPushedByFluid(); + final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); + + Vec3 pushVector = Vec3.ZERO; + double totalPushes = 0.0; + double maxHeightDiff = 0.0; + boolean inFluid = false; + + final int minChunkX = minBlockX >> 4; + final int maxChunkX = maxBlockX >> 4; + + final int minChunkY = minBlockY >> 4; + final int maxChunkY = maxBlockY >> 4; + + final int minChunkZ = minBlockZ >> 4; + final int maxChunkZ = maxBlockZ >> 4; + + final net.minecraft.world.level.chunk.ChunkSource chunkSource = world.getChunkSource(); + + for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { + for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { + final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, false).getSections(); + + // bound y + for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { + final int sectionIdx = currChunkY - minSection; + if (sectionIdx < 0 || sectionIdx >= sections.length) { + continue; + } + final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx]; + if (section.hasOnlyAir()) { + // empty + continue; + } + + final net.minecraft.world.level.chunk.PalettedContainer blocks = section.states; + + final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) : 0; + final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) : 15; + final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) : 0; + final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) : 15; + final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) : 0; + final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) : 15; - vec3 = vec3.add(flow); - i++; + 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 (fluidTag == FluidTags.LAVA) { - this.lastLavaContact = mutableBlockPos.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 (vec3.length() > 0.0) { - if (i > 0) { - vec3 = vec3.scale(1.0 / i); - } + this.fluidHeight.put(fluid, maxHeightDiff); - if (!(this instanceof Player)) { - vec3 = vec3.normalize(); - } + if (pushVector.lengthSqr() == 0.0) { + return inFluid; + } - Vec3 deltaMovement = this.getDeltaMovement(); - vec3 = vec3.scale(motionScale); - double d2 = 0.003; - if (Math.abs(deltaMovement.x) < 0.003 && Math.abs(deltaMovement.z) < 0.003 && vec3.length() < 0.0045000000000000005) { - vec3 = vec3.normalize().scale(0.0045000000000000005); - } + // note: totalPushes != 0 as pushVector != 0 + pushVector = pushVector.scale(1.0 / totalPushes); + final Vec3 currMovement = this.getDeltaMovement(); - this.setDeltaMovement(this.getDeltaMovement().add(vec3)); - } + if (!((Entity)(Object)this instanceof Player)) { + pushVector = pushVector.normalize(); + } - this.fluidHeight.put(fluidTag, d); - return flag; + 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(currMovement.add(pushVector)); + + // note: inFluid = true here as pushVector != 0 + return true; } + // Paper end - optimise collisions public boolean touchingUnloadedChunk() { AABB aabb = this.getBoundingBox().inflate(1.0); @@ -4429,6 +4725,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; } @@ -4557,6 +4862,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess @Override public final void setRemoved(Entity.RemovalReason removalReason, org.bukkit.event.entity.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 org.bukkit.craftbukkit.event.CraftEventFactory.callEntityRemoveEvent(this, cause); // CraftBukkit end final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers @@ -4568,7 +4879,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(removalReason); this.onRemoval(removalReason); // Paper start - Folia schedulers @@ -4602,7 +4913,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess public boolean shouldBeSaved() { return (this.removalReason == null || this.removalReason.shouldSave()) && !this.isPassenger() - && (!this.isVehicle() || !this.hasExactlyOnePlayerPassenger()); + && (!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 7d590dd06cc69c0925d22708425520c38e3cda25..5c5724f5e3ad640f55aecbc1d8f71d1f59ecdc62 100644 --- a/net/minecraft/world/entity/ai/village/poi/PoiManager.java +++ b/net/minecraft/world/entity/ai/village/poi/PoiManager.java @@ -38,12 +38,137 @@ import net.minecraft.world.level.chunk.storage.RegionStorageInfo; import net.minecraft.world.level.chunk.storage.SectionStorage; import net.minecraft.world.level.chunk.storage.SimpleRegionStorage; -public class PoiManager extends SectionStorage { +public class PoiManager extends SectionStorage implements ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager { // Paper - rewrite chunk system public static final int MAX_VILLAGE_DISTANCE = 6; public static final int VILLAGE_SECTION_SIZE = 1; private final PoiManager.DistanceTracker distanceTracker; private final LongSet loadedChunks = new LongOpenHashSet(); + // Paper start - rewrite chunk system + private final net.minecraft.server.level.ServerLevel world; + + // the vanilla tracker needs to be replaced because it does not support level removes, and we need level removes + // to support poi unloading + private final ca.spottedleaf.moonrise.common.misc.Delayed26WayDistancePropagator3D villageDistanceTracker = new ca.spottedleaf.moonrise.common.misc.Delayed26WayDistancePropagator3D(); + + private static final int POI_DATA_SOURCE = 7; + + private static int convertBetweenLevels(final int level) { + return POI_DATA_SOURCE - level; + } + + private void updateDistanceTracking(long section) { + if (this.isVillageCenter(section)) { + this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE); + } else { + this.villageDistanceTracker.removeSource(section); + } + } + + @Override + public Optional get(final long pos) { + final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos); + final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos); + final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos); + + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); + + final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getPoiChunkIfLoaded(chunkX, chunkZ, true); + + return ret == null ? Optional.empty() : ret.getSectionForVanilla(chunkY); + } + + @Override + public Optional getOrLoad(final long pos) { + final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos); + final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos); + final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos); + + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); + + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager; + + if (chunkY >= ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world) && chunkY <= ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world)) { + final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); + if (ret != null) { + return ret.getSectionForVanilla(chunkY); + } else { + return manager.loadPoiChunk(chunkX, chunkZ).getSectionForVanilla(chunkY); + } + } + // retain vanilla behavior: do not load section if out of bounds! + return Optional.empty(); + } + + @Override + protected PoiSection getOrCreate(final long pos) { + final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos); + final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos); + final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos); + + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); + + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager; + + final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); + if (ret != null) { + return ret.getOrCreateSection(chunkY); + } else { + return manager.loadPoiChunk(chunkX, chunkZ).getOrCreateSection(chunkY); + } + } + + @Override + public final net.minecraft.server.level.ServerLevel moonrise$getWorld() { + return this.world; + } + + @Override + public final void moonrise$onUnload(final long coordinate) { // Paper - rewrite chunk system + final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(coordinate); + final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(coordinate); + + final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world); + final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world); + + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Unloading poi chunk off-main"); + for (int sectionY = minY; sectionY <= maxY; ++sectionY) { + final long sectionPos = SectionPos.asLong(chunkX, sectionY, chunkZ); + this.updateDistanceTracking(sectionPos); + } + } + + @Override + public final void moonrise$loadInPoiChunk(final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk poiChunk) { + final int chunkX = poiChunk.chunkX; + final int chunkZ = poiChunk.chunkZ; + + final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world); + final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world); + + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Loading poi chunk off-main"); + for (int sectionY = minY; sectionY <= maxY; ++sectionY) { + final PoiSection section = poiChunk.getSection(sectionY); + if (section != null && !((ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection)section).moonrise$isEmpty()) { + this.onSectionLoad(SectionPos.asLong(chunkX, sectionY, chunkZ)); + } + } + } + + @Override + public final void moonrise$checkConsistency(final net.minecraft.world.level.chunk.ChunkAccess chunk) { + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + + final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(chunk); + final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(chunk); + final LevelChunkSection[] sections = chunk.getSections(); + for (int section = minY; section <= maxY; ++section) { + this.checkConsistencyWithBlocks(SectionPos.of(chunkX, section, chunkZ), sections[section - minY]); + } + } + // Paper end - rewrite chunk system + public PoiManager( RegionStorageInfo info, Path folder, @@ -64,6 +189,7 @@ public class PoiManager extends SectionStorage { levelHeightAccessor ); this.distanceTracker = new PoiManager.DistanceTracker(); + this.world = (net.minecraft.server.level.ServerLevel)levelHeightAccessor; // Paper - rewrite chunk system } public void add(BlockPos pos, Holder type) { @@ -197,8 +323,10 @@ public class PoiManager extends SectionStorage { } public int sectionsToVillage(SectionPos sectionPos) { - this.distanceTracker.runAllUpdates(); - return this.distanceTracker.getLevel(sectionPos.asLong()); + // Paper start - rewrite chunk system + this.villageDistanceTracker.propagateUpdates(); + return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(sectionPos))); + // Paper end - rewrite chunk system } boolean isVillageCenter(long chunkPos) { @@ -212,19 +340,26 @@ public class PoiManager extends SectionStorage { @Override public void tick(BooleanSupplier aheadOfTime) { - super.tick(aheadOfTime); - this.distanceTracker.runAllUpdates(); + this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system } @Override - protected void setDirty(long sectionPos) { - super.setDirty(sectionPos); - this.distanceTracker.update(sectionPos, this.distanceTracker.getLevelFromSource(sectionPos), false); + public void setDirty(long sectionPos) { // Paper - public + // Paper start - rewrite chunk system + final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(sectionPos); + final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(sectionPos); + 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(sectionPos); + // Paper end - rewrite chunk system } @Override protected void onSectionLoad(long sectionKey) { - this.distanceTracker.update(sectionKey, this.distanceTracker.getLevelFromSource(sectionKey), false); + this.updateDistanceTracking(sectionKey); // Paper - rewrite chunk system } public void checkConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection levelChunkSection) { @@ -263,7 +398,7 @@ public class PoiManager extends SectionStorage { .map(sectionPos -> Pair.of(sectionPos, this.getOrLoad(sectionPos.asLong()))) .filter(pair -> !pair.getSecond().map(PoiSection::isValid).orElse(false)) .map(pair -> pair.getFirst().chunk()) - .filter(chunkPos -> this.loadedChunks.add(chunkPos.toLong())) + // Paper - rewrite chunk system .forEach(chunkPos -> levelReader.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 324cc0686f0f5b1371b2bbea5b8c8fdb1f363006..39cd1e3d8192d7077d6b7864d33933097cc6b986 100644 --- a/net/minecraft/world/entity/ai/village/poi/PoiSection.java +++ b/net/minecraft/world/entity/ai/village/poi/PoiSection.java @@ -23,13 +23,27 @@ import net.minecraft.core.SectionPos; import net.minecraft.util.VisibleForDebug; import org.slf4j.Logger; -public class PoiSection { +public class PoiSection implements ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection { // Paper - rewrite chunk system private static final Logger LOGGER = LogUtils.getLogger(); private final Short2ObjectMap records = new Short2ObjectOpenHashMap<>(); private final Map, Set> byType = Maps.newHashMap(); private final Runnable setDirty; private boolean isValid; + // Paper start - rewrite chunk system + private final Optional noAllocOptional = Optional.of((PoiSection)(Object)this); + + @Override + public final boolean moonrise$isEmpty() { + return this.isValid && this.records.isEmpty() && this.byType.isEmpty(); + } + + @Override + public final Optional moonrise$asOptional() { + return this.noAllocOptional; + } + // Paper end - rewrite chunk system + public PoiSection(Runnable setDirty) { this(setDirty, true, ImmutableList.of()); } diff --git a/net/minecraft/world/entity/decoration/ArmorStand.java b/net/minecraft/world/entity/decoration/ArmorStand.java index 33f6d6862731d22d6d3eeb7cf39a4a42049afae3..a3cc0001a949597e345d7919c3f6109fa4a949ad 100644 --- a/net/minecraft/world/entity/decoration/ArmorStand.java +++ b/net/minecraft/world/entity/decoration/ArmorStand.java @@ -316,7 +316,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 - for (Entity entity : this.level().getEntities(this, this.getBoundingBox(), RIDABLE_MINECARTS)) { + for (Entity entity : this.level().getEntitiesOfClass(AbstractMinecart.class, this.getBoundingBox(), RIDABLE_MINECARTS)) { // Paper - optimise collisions if (this.distanceToSqr(entity) <= 0.2) { entity.push(this); } diff --git a/net/minecraft/world/level/ClipContext.java b/net/minecraft/world/level/ClipContext.java index 9f34fc4278860dd7bcfa1fd79b15e588b0cc3973..a7ebd624652cb6f0edc735bf6b9760e7b443594f 100644 --- a/net/minecraft/world/level/ClipContext.java +++ b/net/minecraft/world/level/ClipContext.java @@ -17,7 +17,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 from, Vec3 to, ClipContext.Block block, ClipContext.Fluid fluid, Entity entity) { diff --git a/net/minecraft/world/level/EntityGetter.java b/net/minecraft/world/level/EntityGetter.java index 300f3ed58109219d97846082941b860585f66fed..e81195df621159da67136f020fa7a6d39d1ee5ed 100644 --- a/net/minecraft/world/level/EntityGetter.java +++ b/net/minecraft/world/level/EntityGetter.java @@ -15,7 +15,7 @@ import net.minecraft.world.phys.shapes.BooleanOp; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; -public interface EntityGetter { +public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter { // Paper - rewrite chunk system List getEntities(@Nullable Entity entity, AABB area, Predicate predicate); List getEntities(EntityTypeTest entityTypeTest, AABB bounds, Predicate predicate); @@ -30,21 +30,44 @@ public interface EntityGetter { return this.getEntities(entity, area, EntitySelector.NO_SPECTATORS); } - default boolean isUnobstructed(@Nullable Entity entity, VoxelShape shape) { - if (shape.isEmpty()) { - return true; - } else { - for (Entity entity1 : this.getEntities(entity, shape.bounds())) { - if (!entity1.isRemoved() - && entity1.blocksBuilding - && (entity == null || !entity1.isPassengerOfSameVehicle(entity)) - && Shapes.joinIsNotEmpty(shape, Shapes.create(entity1.getBoundingBox()), BooleanOp.AND)) { - return false; + // Paper start - rewrite chunk system + @Override + default List moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate predicate) { + return this.getEntities(entity, box, predicate); + } + // Paper end - rewrite chunk system + + // Paper start - optimise collisions + default boolean isUnobstructed(@Nullable Entity entity, VoxelShape voxel) { + if (voxel.isEmpty()) { + return false; + } + + final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation(); + final List entities = this.getEntities( + entity, + singleAABB == null ? voxel.bounds() : singleAABB.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + ); + + for (int i = 0, len = entities.size(); i < len; ++i) { + final Entity otherEntity = entities.get(i); + + if (otherEntity.isRemoved() || !otherEntity.blocksBuilding || (entity != null && otherEntity.isPassengerOfSameVehicle(entity))) { + continue; + } + + if (singleAABB == null) { + final AABB entityBB = otherEntity.getBoundingBox(); + if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(entityBB) || !ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(voxel, entityBB)) { + continue; } } - return true; + return false; } + + return true; + // Paper end - optimise collisions } default List getEntitiesOfClass(Class entityClass, AABB area) { @@ -52,23 +75,41 @@ public interface EntityGetter { } default List getEntityCollisions(@Nullable Entity entity, AABB collisionBox) { - if (collisionBox.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(collisionBox)) { + // 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. + collisionBox = collisionBox.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON); + + final List entities; + if (entity != null && ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity) entity).moonrise$isHardColliding()) { + entities = this.getEntities(entity, collisionBox, null); } else { - Predicate predicate = entity == null ? EntitySelector.CAN_BE_COLLIDED_WITH : EntitySelector.NO_SPECTATORS.and(entity::canCollideWith); - List entities = this.getEntities(entity, collisionBox.inflate(1.0E-7), predicate); - if (entities.isEmpty()) { - return List.of(); - } else { - Builder builder = ImmutableList.builderWithExpectedSize(entities.size()); - - for (Entity entity1 : entities) { - builder.add(Shapes.create(entity1.getBoundingBox())); - } + entities = ((ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter) this).moonrise$getHardCollidingEntities(entity, collisionBox, null); + } - return builder.build(); + final List ret = new java.util.ArrayList<>(Math.min(25, entities.size())); + + for (int i = 0, len = entities.size(); i < len; ++i) { + final Entity otherEntity = entities.get(i); + + if (otherEntity.isSpectator()) { + continue; + } + + if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) { + ret.add(Shapes.create(otherEntity.getBoundingBox())); } } + + return ret; + // Paper end - optimise collisions } // Paper start - Affects Spawning API diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java index 0e4ab448755632696c4326f1df9f3855cd38a64d..aff78499b73a98f7405aead0886c51e8ac262884 100644 --- a/net/minecraft/world/level/Level.java +++ b/net/minecraft/world/level/Level.java @@ -79,6 +79,7 @@ import net.minecraft.world.level.storage.LevelData; import net.minecraft.world.level.storage.WritableLevelData; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.VoxelShape; import net.minecraft.world.scores.Scoreboard; // CraftBukkit start @@ -102,7 +103,7 @@ import org.bukkit.entity.SpawnCategory; import org.bukkit.event.block.BlockPhysicsEvent; // CraftBukkit end -public abstract class Level implements LevelAccessor, AutoCloseable { +public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel, ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter { // Paper - rewrite chunk system // Paper - optimise collisions public static final Codec> RESOURCE_KEY_CODEC = ResourceKey.codec(Registries.DIMENSION); public static final ResourceKey OVERWORLD = ResourceKey.create(Registries.DIMENSION, ResourceLocation.withDefaultNamespace("overworld")); public static final ResourceKey NETHER = ResourceKey.create(Registries.DIMENSION, ResourceLocation.withDefaultNamespace("the_nether")); @@ -127,7 +128,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 private final RandomSource threadSafeRandom = RandomSource.createThreadSafe(); private final Holder dimensionTypeRegistration; @@ -202,6 +203,629 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public abstract ResourceKey getTypeKey(); + // Paper start - rewrite chunk system + private ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup; + private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable chunkData = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>(); + + @Override + public final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup moonrise$getEntityLookup() { + return this.entityLookup; + } + + @Override + public final void moonrise$setEntityLookup(final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup) { + if (this.entityLookup != null && !(this.entityLookup instanceof ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl.DefaultEntityLookup)) { + throw new IllegalStateException("Entity lookup already initialised"); + } + this.entityLookup = entityLookup; + } + + @Override + public final List getEntitiesOfClass(final Class entityClass, final AABB boundingBox, final Predicate predicate) { + Profiler.get().incrementCounter("getEntities"); + final List ret = new java.util.ArrayList<>(); + + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(entityClass, null, boundingBox, ret, predicate); + + return ret; + } + + @Override + public final List moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate predicate) { + Profiler.get().incrementCounter("getEntities"); + final List ret = new java.util.ArrayList<>(); + + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getHardCollidingEntities(entity, box, ret, predicate); + + return ret; + } + + @Override + public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) { + return (LevelChunk)this.getChunkSource().getChunk(chunkX, chunkZ, ChunkStatus.FULL, false); + } + + @Override + public ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) { + return this.getChunkSource().getChunk(chunkX, chunkZ, ChunkStatus.EMPTY, false); + } + + @Override + public ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus) { + return this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, false); + } + + @Override + public void moonrise$midTickTasks() { + // no-op on ClientLevel + } + + @Override + public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$getChunkData(final long chunkKey) { + return this.chunkData.get(chunkKey); + } + + @Override + public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$getChunkData(final int chunkX, final int chunkZ) { + return this.chunkData.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } + + @Override + public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$requestChunkData(final long chunkKey) { + return this.chunkData.compute(chunkKey, (final long keyInMap, final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData valueInMap) -> { + if (valueInMap == null) { + final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData ret = new ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData(); + ret.increaseRef(); + return ret; + } + + valueInMap.increaseRef(); + return valueInMap; + }); + } + + @Override + public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$releaseChunkData(final long chunkKey) { + return this.chunkData.compute(chunkKey, (final long keyInMap, final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData) -> { + return chunkData.decreaseRef() == 0 ? null : chunkData; + }); + } + + @Override + public boolean moonrise$areChunksLoaded(final int fromX, final int fromZ, final int toX, final int toZ) { + final net.minecraft.world.level.chunk.ChunkSource chunkSource = this.getChunkSource(); + + for (int currZ = fromZ; currZ <= toZ; ++currZ) { + for (int currX = fromX; currX <= toX; ++currX) { + if (!chunkSource.hasChunk(currX, currZ)) { + return false; + } + } + } + + return true; + } + + @Override + public boolean hasChunksAt(final int minBlockX, final int minBlockZ, final int maxBlockX, final int maxBlockZ) { + return this.moonrise$areChunksLoaded( + minBlockX >> 4, minBlockZ >> 4, maxBlockX >> 4, maxBlockZ >> 4 + ); + } + + /** + * @reason Turn all getChunk(x, z, status) calls into virtual invokes, instead of interface invokes: + * 1. The interface invoke is expensive + * 2. The method makes other interface invokes (again, expensive) + * Instead, we just directly call getChunk(x, z, status, true) which avoids the interface invokes entirely. + * @author Spottedleaf + */ + @Override + public ChunkAccess getChunk(final int x, final int z, final ChunkStatus status) { + return ((Level)(Object)this).getChunk(x, z, status, true); + } + + @Override + public BlockPos getHeightmapPos(Heightmap.Types types, BlockPos blockPos) { + return new BlockPos(blockPos.getX(), this.getHeight(types, blockPos.getX(), blockPos.getZ()), blockPos.getZ()); + } + // Paper end - rewrite chunk system + // Paper start - optimise collisions + /** + * Route to faster lookup. + * See {@link EntityGetter#isUnobstructed(Entity, VoxelShape)} for expected behavior + * @author Spottedleaf + */ + @Override + public boolean isUnobstructed(final Entity entity) { + final AABB boundingBox = entity.getBoundingBox(); + if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(boundingBox)) { + return false; + } + + final List entities = this.getEntities( + entity, + boundingBox.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON), + null + ); + + for (int i = 0, len = entities.size(); i < len; ++i) { + final Entity otherEntity = entities.get(i); + + if (otherEntity.isSpectator() || otherEntity.isRemoved() || !otherEntity.blocksBuilding || otherEntity.isPassengerOfSameVehicle(entity)) { + continue; + } + + return false; + } + + return true; + } + + + private static net.minecraft.world.phys.BlockHitResult miss(final ClipContext clipContext) { + final Vec3 to = clipContext.getTo(); + final Vec3 from = clipContext.getFrom(); + + return net.minecraft.world.phys.BlockHitResult.miss(to, Direction.getApproximateNearest(from.x - to.x, from.y - to.y, from.z - to.z), BlockPos.containing(to.x, to.y, to.z)); + } + + private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState(); + + private static net.minecraft.world.phys.BlockHitResult fastClip(final Vec3 from, final Vec3 to, final Level level, + final ClipContext clipContext) { + final double adjX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.x - to.x); + final double adjY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.y - to.y); + final double adjZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.z - to.z); + + if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) { + return miss(clipContext); + } + + final double toXAdj = to.x - adjX; + final double toYAdj = to.y - adjY; + final double toZAdj = to.z - adjZ; + final double fromXAdj = from.x + adjX; + final double fromYAdj = from.y + adjY; + final double fromZAdj = from.z + adjZ; + + int currX = Mth.floor(fromXAdj); + int currY = Mth.floor(fromYAdj); + int currZ = Mth.floor(fromZAdj); + + final BlockPos.MutableBlockPos currPos = new BlockPos.MutableBlockPos(); + + final double diffX = toXAdj - fromXAdj; + final double diffY = toYAdj - fromYAdj; + final double diffZ = toZAdj - fromZAdj; + + final double dxDouble = Math.signum(diffX); + final double dyDouble = Math.signum(diffY); + final double dzDouble = Math.signum(diffZ); + + final int dx = (int)dxDouble; + final int dy = (int)dyDouble; + final int dz = (int)dzDouble; + + final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX; + final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY; + final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ; + + double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj)); + double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj)); + double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj)); + + net.minecraft.world.level.chunk.LevelChunkSection[] lastChunk = null; + net.minecraft.world.level.chunk.PalettedContainer lastSection = null; + int lastChunkX = Integer.MIN_VALUE; + int lastChunkY = Integer.MIN_VALUE; + int lastChunkZ = Integer.MIN_VALUE; + + final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(level); + + for (;;) { + currPos.set(currX, currY, currZ); + + final int newChunkX = currX >> 4; + final int newChunkY = currY >> 4; + final int newChunkZ = currZ >> 4; + + final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); + final int chunkYDiff = newChunkY ^ lastChunkY; + + if ((chunkDiff | chunkYDiff) != 0) { + if (chunkDiff != 0) { + lastChunk = level.getChunk(newChunkX, newChunkZ).getSections(); + } + final int sectionY = newChunkY - minSection; + lastSection = sectionY >= 0 && sectionY < lastChunk.length ? lastChunk[sectionY].states : null; + + lastChunkX = newChunkX; + lastChunkY = newChunkY; + lastChunkZ = newChunkZ; + } + + final BlockState blockState; + if (lastSection != null && !(blockState = lastSection.get((currX & 15) | ((currZ & 15) << 4) | ((currY & 15) << (4+4)))).isAir()) { + final VoxelShape blockCollision = clipContext.getBlockShape(blockState, level, currPos); + + final net.minecraft.world.phys.BlockHitResult blockHit = blockCollision.isEmpty() ? null : level.clipWithInteractionOverride(from, to, currPos, blockCollision, blockState); + + final VoxelShape fluidCollision; + final FluidState fluidState; + if (clipContext.fluid != ClipContext.Fluid.NONE && (fluidState = blockState.getFluidState()) != AIR_FLUIDSTATE) { + fluidCollision = clipContext.getFluidShape(fluidState, level, currPos); + + final net.minecraft.world.phys.BlockHitResult fluidHit = fluidCollision.clip(from, to, currPos); + + if (fluidHit != null) { + if (blockHit == null) { + return fluidHit; + } + + return from.distanceToSqr(blockHit.getLocation()) <= from.distanceToSqr(fluidHit.getLocation()) ? blockHit : fluidHit; + } + } + + if (blockHit != null) { + return blockHit; + } + } // else: usually fall here + + if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) { + return miss(clipContext); + } + + // inc the smallest normalized coordinate + + if (normalizedCurrX < normalizedCurrY) { + if (normalizedCurrX < normalizedCurrZ) { + currX += dx; + normalizedCurrX += normalizedDiffX; + } else { + // x < y && x >= z <--> z < y && z <= x + currZ += dz; + normalizedCurrZ += normalizedDiffZ; + } + } else if (normalizedCurrY < normalizedCurrZ) { + // y <= x && y < z + currY += dy; + normalizedCurrY += normalizedDiffY; + } else { + // y <= x && z <= y <--> z <= y && z <= x + currZ += dz; + normalizedCurrZ += normalizedDiffZ; + } + } + } + + /** + * @reason Route to optimized call + * @author Spottedleaf + */ + @Override + public net.minecraft.world.phys.BlockHitResult clip(final ClipContext clipContext) { + // can only do this in this class, as not everything that implements BlockGetter can retrieve chunks + return fastClip(clipContext.getFrom(), clipContext.getTo(), (Level)(Object)this, clipContext); + } + + /** + * @reason Route to faster logic + * @author Spottedleaf + */ + @Override + public boolean collidesWithSuffocatingBlock(final Entity entity, final AABB box) { + return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder((Level)(Object)this, entity, box, null, null, + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_ONLY, + (final BlockState state, final BlockPos pos) -> { + return state.isSuffocating((Level)(Object)Level.this, pos); + } + ); + } + + private static VoxelShape inflateAABBToVoxel(final AABB aabb, final double x, final double y, final double z) { + return net.minecraft.world.phys.shapes.Shapes.create( + aabb.minX - x, + aabb.minY - y, + aabb.minZ - z, + + aabb.maxX + x, + aabb.maxY + y, + aabb.maxZ + z + ); + } + + /** + * @reason Use optimised OR operator join strategy, avoid streams + * @author Spottedleaf + */ + @Override + public java.util.Optional findFreePosition(final Entity entity, final VoxelShape boundsShape, final Vec3 fromPosition, + final double rangeX, final double rangeY, final double rangeZ) { + if (boundsShape.isEmpty()) { + return java.util.Optional.empty(); + } + + final double expandByX = rangeX * 0.5; + final double expandByY = rangeY * 0.5; + final double expandByZ = rangeZ * 0.5; + + // note: it is useless to look at shapes outside of range / 2.0 + final AABB collectionVolume = boundsShape.bounds().inflate(expandByX, expandByY, expandByZ); + + final List aabbs = new java.util.ArrayList<>(); + final List voxels = new java.util.ArrayList<>(); + + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder( + (Level)(Object)this, entity, collectionVolume, voxels, aabbs, + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, + null + ); + + final WorldBorder worldBorder = this.getWorldBorder(); + if (worldBorder != null) { + aabbs.removeIf((final AABB aabb) -> { + return !worldBorder.isWithinBounds(aabb); + }); + voxels.removeIf((final VoxelShape shape) -> { + return !worldBorder.isWithinBounds(shape.bounds()); + }); + } + + // push voxels into aabbs + for (int i = 0, len = voxels.size(); i < len; ++i) { + aabbs.addAll(voxels.get(i).toAabbs()); + } + + // expand AABBs + final VoxelShape first = aabbs.isEmpty() ? net.minecraft.world.phys.shapes.Shapes.empty() : inflateAABBToVoxel(aabbs.get(0), expandByX, expandByY, expandByZ); + final VoxelShape[] rest = new VoxelShape[Math.max(0, aabbs.size() - 1)]; + + for (int i = 1, len = aabbs.size(); i < len; ++i) { + rest[i - 1] = inflateAABBToVoxel(aabbs.get(i), expandByX, expandByY, expandByZ); + } + + // use optimized implementation of ORing the shapes together + final VoxelShape joined = net.minecraft.world.phys.shapes.Shapes.or(first, rest); + + // find free space + // can use unoptimized join here (instead of join()), as closestPointTo uses toAabbs() + final VoxelShape freeSpace = net.minecraft.world.phys.shapes.Shapes.joinUnoptimized(boundsShape, joined, net.minecraft.world.phys.shapes.BooleanOp.ONLY_FIRST); + + return freeSpace.closestPointTo(fromPosition); + } + + /** + * @reason Route to faster logic + * @author Spottedleaf + */ + @Override + public java.util.Optional findSupportingBlock(final Entity entity, final AABB aabb) { + final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection((Level)(Object)this); + + final int minBlockX = Mth.floor(aabb.minX - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1; + final int maxBlockX = Mth.floor(aabb.maxX + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1; + + final int minBlockY = Math.max((minSection << 4) - 1, Mth.floor(aabb.minY - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1); + final int maxBlockY = Math.min((ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection((Level)(Object)this) << 4) + 16, Mth.floor(aabb.maxY + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1); + + final int minBlockZ = Mth.floor(aabb.minZ - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1; + final int maxBlockZ = Mth.floor(aabb.maxZ + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1; + + final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); + final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext collisionShape = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(entity); + BlockPos selected = null; + double selectedDistance = Double.MAX_VALUE; + final Vec3 entityPos = entity.position(); + + // special cases: + if (minBlockY > maxBlockY) { + // no point in checking + return java.util.Optional.empty(); + } + + final int minChunkX = minBlockX >> 4; + final int maxChunkX = maxBlockX >> 4; + + final int minChunkY = minBlockY >> 4; + final int maxChunkY = maxBlockY >> 4; + + final int minChunkZ = minBlockZ >> 4; + final int maxChunkZ = maxBlockZ >> 4; + + final net.minecraft.world.level.chunk.ChunkSource chunkSource = this.getChunkSource(); + + for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { + for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { + final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, false); + + if (chunk == null) { + continue; + } + + final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections(); + + // bound y + for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { + final int sectionIdx = currChunkY - minSection; + if (sectionIdx < 0 || sectionIdx >= sections.length) { + continue; + } + final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx]; + if (section.hasOnlyAir()) { + // empty + continue; + } + + final boolean hasSpecial = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$hasSpecialCollidingBlocks(); + final int sectionAdjust = !hasSpecial ? 1 : 0; + + final net.minecraft.world.level.chunk.PalettedContainer blocks = section.states; + + final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0; + final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15; + final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) + sectionAdjust : 0; + final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) - sectionAdjust : 15; + final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) + sectionAdjust : 0; + final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) - sectionAdjust : 15; + + for (int currY = minYIterate; currY <= maxYIterate; ++currY) { + final int blockY = currY | (currChunkY << 4); + mutablePos.setY(blockY); + for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { + final int blockZ = currZ | (currChunkZ << 4); + mutablePos.setZ(blockZ); + for (int currX = minXIterate; currX <= maxXIterate; ++currX) { + final int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8); + final int blockX = currX | (currChunkX << 4); + mutablePos.setX(blockX); + + final int edgeCount = hasSpecial ? ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + + ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + + ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0) : 0; + if (edgeCount == 3) { + continue; + } + + final double distance = mutablePos.distToCenterSqr(entityPos); + if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(mutablePos) >= 0)) { + continue; + } + + final BlockState blockData = blocks.get(localBlockIndex); + + if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$emptyContextCollisionShape()) { + continue; + } + + VoxelShape blockCollision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$getConstantContextCollisionShape(); + + if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) { + if (blockCollision == null) { + blockCollision = blockData.getCollisionShape((Level)(Object)this, mutablePos, collisionShape); + + if (blockCollision.isEmpty()) { + continue; + } + } + + // avoid VoxelShape#move by shifting the entity collision shape instead + final AABB shiftedAABB = aabb.move(-(double)blockX, -(double)blockY, -(double)blockZ); + + final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation(); + if (singleAABB != null) { + if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(singleAABB, shiftedAABB)) { + continue; + } + + selected = mutablePos.immutable(); + selectedDistance = distance; + continue; + } + + if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(blockCollision, shiftedAABB)) { + continue; + } + + selected = mutablePos.immutable(); + selectedDistance = distance; + continue; + } + } + } + } + } + } + } + + return java.util.Optional.ofNullable(selected); + } + // Paper end - optimise collisions + // Paper start - getblock optimisations - cache world height/sections + private final int minY; + private final int height; + private final int maxY; + private final int minSectionY; + private final int maxSectionY; + private final int sectionsCount; + + @Override + public int getMinY() { + return this.minY; + } + + @Override + public int getHeight() { + return this.height; + } + + @Override + public int getMaxY() { + return this.maxY; + } + + @Override + public int getSectionsCount() { + return this.sectionsCount; + } + + @Override + public int getMinSectionY() { + return this.minSectionY; + } + + @Override + public int getMaxSectionY() { + return this.maxSectionY; + } + + @Override + public boolean isInsideBuildHeight(final int blockY) { + return blockY >= this.minY && blockY <= this.maxY; + } + + @Override + public boolean isOutsideBuildHeight(final BlockPos pos) { + return this.isOutsideBuildHeight(pos.getY()); + } + + @Override + public boolean isOutsideBuildHeight(final int blockY) { + return blockY < this.minY || blockY > this.maxY; + } + + @Override + public int getSectionIndex(final int blockY) { + return (blockY >> 4) - this.minSectionY; + } + + @Override + public int getSectionIndexFromSectionY(final int sectionY) { + return sectionY - this.minSectionY; + } + + @Override + public int getSectionYFromSectionIndex(final int sectionIdx) { + return sectionIdx + this.minSectionY; + } + // Paper end - getblock optimisations - cache world height/sections + // Paper start - optimise random ticking + @Override + public abstract Holder getUncachedNoiseBiome(final int x, final int y, final int z); + + /** + * @reason Make getChunk and getUncachedNoiseBiome virtual calls instead of interface calls + * by implementing the superclass method in this class. + * @author Spottedleaf + */ + @Override + public Holder getNoiseBiome(final int x, final int y, final int z) { + final ChunkAccess chunk = this.getChunk(x >> 2, z >> 2, ChunkStatus.BIOMES, false); + + return chunk != null ? chunk.getNoiseBiome(x, y, z) : this.getUncachedNoiseBiome(x, y, z); + } + // Paper end - optimise random ticking + protected Level( WritableLevelData levelData, ResourceKey dimension, @@ -218,6 +842,15 @@ public abstract class Level implements LevelAccessor, AutoCloseable { io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator, // Paper - create paper world config java.util.concurrent.Executor executor // Paper - Anti-Xray ) { + // Paper start - getblock optimisations - cache world height/sections + final DimensionType dimType = dimensionTypeRegistration.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) levelData).getLevelName()); // Spigot this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config this.generator = gen; @@ -298,6 +931,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 io.papermc.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : io.papermc.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 @@ -567,7 +1201,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); } @@ -835,6 +1469,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { // Spigot start boolean runsNormally = this.tickRateManager().runsNormally(); + int tickedEntities = 0; // Paper - rewrite chunk system var toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet(); // Paper - Fix MC-117075; use removeAll toRemove.add(null); // Paper - Fix MC-117075 for (tileTickPosition = 0; tileTickPosition < this.blockEntityTickers.size(); tileTickPosition++) { // Paper - Disable tick limiters @@ -845,6 +1480,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable { toRemove.add(tickingBlockEntity); // Paper - Fix MC-117075; use removeAll } else if (runsNormally && 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 @@ -865,6 +1505,7 @@ 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 @@ -872,7 +1513,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable { 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 @@ -1010,7 +1658,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { if (this.isOutsideBuildHeight(pos)) { return null; } else { - return !this.isClientSide && Thread.currentThread() != this.thread + return !this.isClientSide && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThread() // Paper - rewrite chunk system ? null : this.getChunkAt(pos).getBlockEntity(pos, LevelChunk.EntityCreationType.IMMEDIATE); } @@ -1103,22 +1751,16 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public List getEntities(@Nullable Entity entity, AABB boundingBox, Predicate predicate) { Profiler.get().incrementCounter("getEntities"); List list = Lists.newArrayList(); - this.getEntities().get(boundingBox, entity1 -> { - if (entity1 != entity && predicate.test(entity1)) { - list.add(entity1); - } - }); - for (EnderDragonPart enderDragonPart : this.dragonParts()) { - if (enderDragonPart != entity - && enderDragonPart.parentMob != entity - && predicate.test(enderDragonPart) - && boundingBox.intersects(enderDragonPart.getBoundingBox())) { - list.add(enderDragonPart); - } - } + // Paper start - rewrite chunk system + final List ret = new java.util.ArrayList<>(); - return list; + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(entity, boundingBox, ret, predicate); + + ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entity, boundingBox, predicate, ret); + + return ret; + // Paper end - rewrite chunk system } @Override @@ -1132,33 +1774,94 @@ public abstract class Level implements LevelAccessor, AutoCloseable { this.getEntities(entityTypeTest, bounds, predicate, output, Integer.MAX_VALUE); } - public void getEntities( - EntityTypeTest entityTypeTest, AABB bounds, Predicate predicate, List output, int maxResults - ) { + // Paper start - rewrite chunk system + public void getEntities(final EntityTypeTest entityTypeTest, + final AABB boundingBox, final Predicate predicate, + final List into, final int maxCount) { Profiler.get().incrementCounter("getEntities"); - this.getEntities().get(entityTypeTest, bounds, entity -> { - if (predicate.test(entity)) { - output.add(entity); - if (output.size() >= maxResults) { - return AbortableIterationConsumer.Continuation.ABORT; - } + + if (entityTypeTest instanceof net.minecraft.world.entity.EntityType byType) { + if (maxCount != Integer.MAX_VALUE) { + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(byType, boundingBox, into, predicate, maxCount); + ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); + return; + } else { + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(byType, boundingBox, into, predicate); + ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); + return; } + } - if (entity instanceof EnderDragon enderDragon) { - for (EnderDragonPart enderDragonPart : enderDragon.getSubEntities()) { - T entity1 = entityTypeTest.tryCast(enderDragonPart); - if (entity1 != null && predicate.test(entity1)) { - output.add(entity1); - if (output.size() >= maxResults) { - return AbortableIterationConsumer.Continuation.ABORT; - } - } + 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; + } + } + + final Class base = entityTypeTest.getBaseClass(); + + final Predicate modifiedPredicate; + if (predicate == null) { + modifiedPredicate = (final T obj) -> { + return entityTypeTest.tryCast(obj) != null; + }; + } else { + modifiedPredicate = (final Entity obj) -> { + final T casted = entityTypeTest.tryCast(obj); + if (casted == null) { + return false; } + + return predicate.test(casted); + }; + } + + if (base == null || base == Entity.class) { + if (maxCount != Integer.MAX_VALUE) { + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount); + ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); + return; + } else { + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate); + ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); + return; + } + } else { + if (maxCount != Integer.MAX_VALUE) { + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount); + ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); + return; + } else { + ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate); + ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount); + return; } + } + } - return AbortableIterationConsumer.Continuation.CONTINUE; - }); + public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) { + ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices slices = ((ServerLevel)this).moonrise$getEntityLookup().getChunk(chunkX, chunkZ); + if (slices == null) { + return new org.bukkit.entity.Entity[0]; + } + + List ret = new java.util.ArrayList<>(); + for (Entity entity : slices.getAllEntities()) { + org.bukkit.entity.Entity bukkit = entity.getBukkitEntity(); + if (bukkit != null && bukkit.isValid()) { + ret.add(bukkit); + } + } + + return ret.toArray(new org.bukkit.entity.Entity[0]); } + // Paper end - rewrite chunk system @Nullable public abstract Entity getEntity(int id); diff --git a/net/minecraft/world/level/LevelReader.java b/net/minecraft/world/level/LevelReader.java index 2709803b9266ff4a2034d83321cd0ba4e30fc0aa..26c8c1e5598daf3550aef05b12218c47bda6618b 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 x, int z, ChunkStatus chunkStatus, boolean requireChunk); diff --git a/net/minecraft/world/level/ServerExplosion.java b/net/minecraft/world/level/ServerExplosion.java index 3619509d51ebd2e5e36fe4b67e76c94a8d272d1b..7b132c55caf9d3c3df3b0a123f4b5bfc7ae35984 100644 --- a/net/minecraft/world/level/ServerExplosion.java +++ b/net/minecraft/world/level/ServerExplosion.java @@ -63,6 +63,249 @@ public class ServerExplosion implements Explosion { public float yield; // CraftBukkit end public boolean excludeSourceFromDamage = true; // Paper - Allow explosions to damage source + // Paper start - collisions optimisations + private static final double[] CACHED_RAYS; + static { + final it.unimi.dsi.fastutil.doubles.DoubleArrayList rayCoords = new it.unimi.dsi.fastutil.doubles.DoubleArrayList(); + + for (int x = 0; x <= 15; ++x) { + for (int y = 0; y <= 15; ++y) { + for (int z = 0; z <= 15; ++z) { + if ((x == 0 || x == 15) || (y == 0 || y == 15) || (z == 0 || z == 15)) { + double xDir = (double)((float)x / 15.0F * 2.0F - 1.0F); + double yDir = (double)((float)y / 15.0F * 2.0F - 1.0F); + double zDir = (double)((float)z / 15.0F * 2.0F - 1.0F); + + double mag = Math.sqrt( + xDir * xDir + yDir * yDir + zDir * zDir + ); + + rayCoords.add((xDir / mag) * (double)0.3F); + rayCoords.add((yDir / mag) * (double)0.3F); + rayCoords.add((zDir / mag) * (double)0.3F); + } + } + } + } + + CACHED_RAYS = rayCoords.toDoubleArray(); + } + + private static final int CHUNK_CACHE_SHIFT = 2; + private static final int CHUNK_CACHE_MASK = (1 << CHUNK_CACHE_SHIFT) - 1; + private static final int CHUNK_CACHE_WIDTH = 1 << CHUNK_CACHE_SHIFT; + + private static final int BLOCK_EXPLOSION_CACHE_SHIFT = 3; + private static final int BLOCK_EXPLOSION_CACHE_MASK = (1 << BLOCK_EXPLOSION_CACHE_SHIFT) - 1; + private static final int BLOCK_EXPLOSION_CACHE_WIDTH = 1 << BLOCK_EXPLOSION_CACHE_SHIFT; + + // resistance = (res + 0.3F) * 0.3F; + // so for resistance = 0, we need res = -0.3F + private static final Float ZERO_RESISTANCE = Float.valueOf(-0.3f); + private it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap blockCache = null; + private long[] chunkPosCache = null; + private net.minecraft.world.level.chunk.LevelChunk[] chunkCache = null; + private ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] directMappedBlockCache; + private BlockPos.MutableBlockPos mutablePos; + + private ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache getOrCacheExplosionBlock(final int x, final int y, final int z, + final long key, final boolean calculateResistance) { + ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache ret = this.blockCache.get(key); + if (ret != null) { + return ret; + } + + BlockPos pos = new BlockPos(x, y, z); + + if (!this.level.isInWorldBounds(pos)) { + ret = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache(key, pos, null, null, 0.0f, true); + } else { + net.minecraft.world.level.chunk.LevelChunk chunk; + long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x >> 4, z >> 4); + int chunkCacheKey = ((x >> 4) & CHUNK_CACHE_MASK) | (((z >> 4) << CHUNK_CACHE_SHIFT) & (CHUNK_CACHE_MASK << CHUNK_CACHE_SHIFT)); + if (this.chunkPosCache[chunkCacheKey] == chunkKey) { + chunk = this.chunkCache[chunkCacheKey]; + } else { + this.chunkPosCache[chunkCacheKey] = chunkKey; + this.chunkCache[chunkCacheKey] = chunk = this.level.getChunk(x >> 4, z >> 4); + } + + BlockState blockState = ((ca.spottedleaf.moonrise.patches.getblock.GetBlockChunk)chunk).moonrise$getBlock(x, y, z); + FluidState fluidState = blockState.getFluidState(); + + Optional resistance = !calculateResistance ? Optional.empty() : this.damageCalculator.getBlockExplosionResistance((Explosion)(Object)this, this.level, pos, blockState, fluidState); + + ret = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache( + key, pos, blockState, fluidState, + (resistance.orElse(ZERO_RESISTANCE).floatValue() + 0.3f) * 0.3f, + false + ); + } + + this.blockCache.put(key, ret); + + return ret; + } + + private boolean clipsAnything(final Vec3 from, final Vec3 to, + final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext context, + final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache, + final BlockPos.MutableBlockPos currPos) { + // assume that context.delegated = false + final double adjX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.x - to.x); + final double adjY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.y - to.y); + final double adjZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.z - to.z); + + if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) { + return false; + } + + final double toXAdj = to.x - adjX; + final double toYAdj = to.y - adjY; + final double toZAdj = to.z - adjZ; + final double fromXAdj = from.x + adjX; + final double fromYAdj = from.y + adjY; + final double fromZAdj = from.z + adjZ; + + int currX = Mth.floor(fromXAdj); + int currY = Mth.floor(fromYAdj); + int currZ = Mth.floor(fromZAdj); + + final double diffX = toXAdj - fromXAdj; + final double diffY = toYAdj - fromYAdj; + final double diffZ = toZAdj - fromZAdj; + + final double dxDouble = Math.signum(diffX); + final double dyDouble = Math.signum(diffY); + final double dzDouble = Math.signum(diffZ); + + final int dx = (int)dxDouble; + final int dy = (int)dyDouble; + final int dz = (int)dzDouble; + + final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX; + final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY; + final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ; + + double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj)); + double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj)); + double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj)); + + for (;;) { + currPos.set(currX, currY, currZ); + + // ClipContext.Block.COLLIDER -> BlockBehaviour.BlockStateBase::getCollisionShape + // ClipContext.Fluid.NONE -> ignore fluids + + // read block from cache + final long key = BlockPos.asLong(currX, currY, currZ); + + final int cacheKey = + (currX & BLOCK_EXPLOSION_CACHE_MASK) | + (currY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | + (currZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); + ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache cachedBlock = blockCache[cacheKey]; + if (cachedBlock == null || cachedBlock.key != key) { + blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(currX, currY, currZ, key, false); + } + + final BlockState blockState = cachedBlock.blockState; + if (blockState != null && !((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$emptyContextCollisionShape()) { + net.minecraft.world.phys.shapes.VoxelShape collision = cachedBlock.cachedCollisionShape; + if (collision == null) { + collision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$getConstantContextCollisionShape(); + if (collision == null) { + collision = blockState.getCollisionShape(this.level, currPos, context); + if (!context.isDelegated()) { + // if it was not delegated during this call, assume that for any future ones it will not be delegated + // again, and cache the result + cachedBlock.cachedCollisionShape = collision; + } + } else { + cachedBlock.cachedCollisionShape = collision; + } + } + + if (!collision.isEmpty() && collision.clip(from, to, currPos) != null) { + return true; + } + } + + if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) { + return false; + } + + // inc the smallest normalized coordinate + + if (normalizedCurrX < normalizedCurrY) { + if (normalizedCurrX < normalizedCurrZ) { + currX += dx; + normalizedCurrX += normalizedDiffX; + } else { + // x < y && x >= z <--> z < y && z <= x + currZ += dz; + normalizedCurrZ += normalizedDiffZ; + } + } else if (normalizedCurrY < normalizedCurrZ) { + // y <= x && y < z + currY += dy; + normalizedCurrY += normalizedDiffY; + } else { + // y <= x && z <= y <--> z <= y && z <= x + currZ += dz; + normalizedCurrZ += normalizedDiffZ; + } + } + } + + private float getSeenFraction(final Vec3 source, final Entity target, + final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache, + final BlockPos.MutableBlockPos blockPos) { + final AABB boundingBox = target.getBoundingBox(); + final double diffX = boundingBox.maxX - boundingBox.minX; + final double diffY = boundingBox.maxY - boundingBox.minY; + final double diffZ = boundingBox.maxZ - boundingBox.minZ; + + final double incX = 1.0 / (diffX * 2.0 + 1.0); + final double incY = 1.0 / (diffY * 2.0 + 1.0); + final double incZ = 1.0 / (diffZ * 2.0 + 1.0); + + if (incX < 0.0 || incY < 0.0 || incZ < 0.0) { + return 0.0f; + } + + final double offX = (1.0 - Math.floor(1.0 / incX) * incX) * 0.5 + boundingBox.minX; + final double offY = boundingBox.minY; + final double offZ = (1.0 - Math.floor(1.0 / incZ) * incZ) * 0.5 + boundingBox.minZ; + + final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext context = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(target); + + int totalRays = 0; + int missedRays = 0; + + for (double dx = 0.0; dx <= 1.0; dx += incX) { + final double fromX = Math.fma(dx, diffX, offX); + for (double dy = 0.0; dy <= 1.0; dy += incY) { + final double fromY = Math.fma(dy, diffY, offY); + for (double dz = 0.0; dz <= 1.0; dz += incZ) { + ++totalRays; + + final Vec3 from = new Vec3( + fromX, + fromY, + Math.fma(dz, diffZ, offZ) + ); + + if (!this.clipsAnything(from, source, context, blockCache, blockPos)) { + ++missedRays; + } + } + } + } + + return (float)missedRays / (float)totalRays; + } + // Paper end - collisions optimisations public ServerExplosion( ServerLevel level, @@ -134,63 +377,102 @@ public class ServerExplosion implements Explosion { } private List calculateExplodedPositions() { - Set set = new HashSet<>(); - int i = 16; - - for (int i1 = 0; i1 < 16; i1++) { - for (int i2 = 0; i2 < 16; i2++) { - for (int i3 = 0; i3 < 16; i3++) { - if (i1 == 0 || i1 == 15 || i2 == 0 || i2 == 15 || i3 == 0 || i3 == 15) { - double d = i1 / 15.0F * 2.0F - 1.0F; - double d1 = i2 / 15.0F * 2.0F - 1.0F; - double d2 = i3 / 15.0F * 2.0F - 1.0F; - double squareRoot = Math.sqrt(d * d + d1 * d1 + d2 * d2); - d /= squareRoot; - d1 /= squareRoot; - d2 /= squareRoot; - float f = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F); - double d3 = this.center.x; - double d4 = this.center.y; - double d5 = this.center.z; - - for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) { - BlockPos blockPos = BlockPos.containing(d3, d4, d5); - BlockState blockState = this.level.getBlockState(blockPos); - if (!blockState.isDestroyable()) continue; // Paper - Protect Bedrock and End Portal/Frames from being destroyed - FluidState fluidState = blockState.getFluidState(); // Paper - Perf: Optimize call to getFluid for explosions - if (!this.level.isInWorldBounds(blockPos)) { - break; - } + // Paper start - collision optimisations + final ObjectArrayList ret = new ObjectArrayList<>(); - Optional blockExplosionResistance = this.damageCalculator - .getBlockExplosionResistance(this, this.level, blockPos, blockState, fluidState); - if (blockExplosionResistance.isPresent()) { - f -= (blockExplosionResistance.get() + 0.3F) * 0.3F; - } + final Vec3 center = this.center; - if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockPos, blockState, f)) { - set.add(blockPos); - // Paper start - prevent headless pistons from forming - if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && blockState.is(Blocks.MOVING_PISTON)) { - net.minecraft.world.level.block.entity.BlockEntity extension = this.level.getBlockEntity(blockPos); - if (extension instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity blockEntity && blockEntity.isSourcePiston()) { - net.minecraft.core.Direction direction = blockState.getValue(net.minecraft.world.level.block.piston.PistonHeadBlock.FACING); - set.add(blockPos.relative(direction.getOpposite())); - } + final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache = this.directMappedBlockCache; + + // 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 } - d3 += d * 0.3F; - d4 += d1 * 0.3F; - d5 += d2 * 0.3F; + // 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() { @@ -371,6 +653,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, GameEvent.EXPLODE, this.center); List list = this.calculateExplodedPositions(); this.hurtEntities(); @@ -384,6 +674,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 } private static void addOrAppendStack(List stackCollectors, ItemStack stack, BlockPos pos) { @@ -474,12 +771,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 ea521e1d636b8ffdeb22882dfc8875b2ddbd8da1..7b666bbeefe296e7fdbadcc72dbf9e602f73e925 100644 --- a/net/minecraft/world/level/biome/Biome.java +++ b/net/minecraft/world/level/biome/Biome.java @@ -117,20 +117,7 @@ public final class Biome { @Deprecated public float getTemperature(BlockPos pos, int seaLevel) { - long packedBlockPos = pos.asLong(); - Long2FloatLinkedOpenHashMap map = this.temperatureCache.get(); - float f = map.get(packedBlockPos); - if (!Float.isNaN(f)) { - return f; - } else { - float heightAdjustedTemperature = this.getHeightAdjustedTemperature(pos, seaLevel); - if (map.size() == 1024) { - map.removeFirstFloat(); - } - - map.put(packedBlockPos, heightAdjustedTemperature); - return heightAdjustedTemperature; - } + return this.getHeightAdjustedTemperature(pos, seaLevel); // Paper - optimise random ticking } public boolean shouldFreeze(LevelReader level, BlockPos pos) { diff --git a/net/minecraft/world/level/biome/BiomeManager.java b/net/minecraft/world/level/biome/BiomeManager.java index 8d98cba3830dc5dfb5cae9a6f5fedfffee0d2cd8..73962e79a0f3d892e3155443a1b84508b0f4042e 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 seed) { - double d = Math.floorMod(seed >> 24, 1024) / 1024.0; - return (d - 0.5) * 0.9; + return (double)(((seed >> 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 91d7d250f7c3de8a71aef26e23c12764b06b322b..0d36b1ac7d54283af71f2494accded11c059dba5 100644 --- a/net/minecraft/world/level/block/Block.java +++ b/net/minecraft/world/level/block/Block.java @@ -259,7 +259,7 @@ public class Block extends BlockBehaviour implements ItemLike { } public static boolean isShapeFullBlock(VoxelShape shape) { - return 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 level, 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 25e49a24cedfa8ad04245d59fcac3231bcd62103..061d94a35d957ca72a01bae959d38aab59b1a64d 100644 --- a/net/minecraft/world/level/block/state/BlockBehaviour.java +++ b/net/minecraft/world/level/block/state/BlockBehaviour.java @@ -416,7 +416,7 @@ public abstract class BlockBehaviour implements FeatureElement { return this.properties.destroyTime; } - public abstract static class BlockStateBase extends StateHolder { + public abstract static class BlockStateBase extends StateHolder implements ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState, ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState { // Paper - rewrite chunk system // Paper - optimise collisions private static final Direction[] DIRECTIONS = Direction.values(); private static final VoxelShape[] EMPTY_OCCLUSION_SHAPES = Util.make(new VoxelShape[DIRECTIONS.length], shape -> Arrays.fill(shape, Shapes.empty())); private static final VoxelShape[] FULL_BLOCK_OCCLUSION_SHAPES = Util.make( @@ -455,6 +455,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 owner, Reference2ObjectArrayMap, Comparable> values, MapCodec propertiesCodec) { super(owner, values, propertiesCodec); BlockBehaviour.Properties properties = owner.properties; @@ -533,6 +603,41 @@ public abstract class BlockBehaviour implements FeatureElement { this.propagatesSkylightDown = this.owner.propagatesSkylightDown(this.asState()); this.lightBlock = 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 2f2dbf02a9732a7e640a6c730d4fc1443e723933..098518383d2c07491e047749ce3a834e98b85b1d 100644 --- a/net/minecraft/world/level/block/state/StateHolder.java +++ b/net/minecraft/world/level/block/state/StateHolder.java @@ -15,7 +15,7 @@ import java.util.stream.Collectors; import javax.annotation.Nullable; import net.minecraft.world.level.block.state.properties.Property; -public abstract class StateHolder { +public abstract class StateHolder implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccessStateHolder { // Paper - optimise blockstate property access public static final String NAME_TAG = "Name"; public static final String PROPERTIES_TAG = "Properties"; public static final Function, Comparable>, String> PROPERTY_ENTRY_TO_STRING_FUNCTION = new Function, Comparable>, String>() { @@ -34,14 +34,28 @@ public abstract class StateHolder { } }; protected final O owner; - private final Reference2ObjectArrayMap, Comparable> values; + private Reference2ObjectArrayMap, Comparable> values; // Paper - optimise blockstate property access - remove final private Map, S[]> neighbours; protected final MapCodec propertiesCodec; + // Paper start - optimise blockstate property access + protected ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable optimisedTable; + protected final long tableIndex; + + @Override + public final long moonrise$getTableIndex() { + return this.tableIndex; + } + // Paper end - optimise blockstate property access + protected StateHolder(O owner, Reference2ObjectArrayMap, Comparable> values, MapCodec propertiesCodec) { this.owner = owner; this.values = values; this.propertiesCodec = propertiesCodec; + // Paper start - optimise blockstate property access + this.optimisedTable = new ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<>(this.values.keySet()); + this.tableIndex = this.optimisedTable.getIndex((StateHolder)(Object)this); + // Paper end - optimise blockstate property access } public > S cycle(Property property) { @@ -67,20 +81,21 @@ public abstract class StateHolder { } public Collection> getProperties() { - return Collections.unmodifiableCollection(this.values.keySet()); + return this.optimisedTable.getProperties(); // Paper - optimise blockstate property access } public > boolean hasProperty(Property property) { - return this.values.containsKey(property); + return property != null && this.optimisedTable.hasProperty(property); // Paper - optimise blockstate property access } public > T getValue(Property property) { - Comparable comparable = this.values.get(property); - if (comparable == null) { - throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner); - } else { - return property.getValueClass().cast(comparable); + // Paper start - optimise blockstate property access + final T ret = this.optimisedTable.get(this.tableIndex, property); + if (ret != null) { + return ret; } + throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner); + // Paper end - optimise blockstate property access } public > Optional getOptionalValue(Property property) { @@ -93,22 +108,30 @@ public abstract class StateHolder { @Nullable public > T getNullableValue(Property property) { - Comparable comparable = this.values.get(property); - return comparable == null ? null : property.getValueClass().cast(comparable); + return property == null ? null : this.optimisedTable.get(this.tableIndex, property); // Paper - optimise blockstate property access } public , V extends T> S setValue(Property property, V value) { - Comparable comparable = this.values.get(property); - if (comparable == null) { - throw new IllegalArgumentException("Cannot set property " + property + " as it does not exist in " + this.owner); - } else { - return this.setValueInternal(property, value, comparable); + // Paper start - optimise blockstate property access + final S ret = this.optimisedTable.set(this.tableIndex, property, value); + if (ret != null) { + return ret; } + throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner); + // Paper end - optimise blockstate property access } public , V extends T> S trySetValue(Property property, V value) { - Comparable comparable = this.values.get(property); - return (S)(comparable == null ? this : this.setValueInternal(property, value, comparable)); + // Paper start - optimise blockstate property access + if (property == null) { + return (S)(StateHolder)(Object)this; + } + final S ret = this.optimisedTable.trySet(this.tableIndex, property, value, (S)(StateHolder)(Object)this); + if (ret != null) { + return ret; + } + throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner); + // Paper end - optimise blockstate property access } private , V extends T> S setValueInternal(Property property, V value, Comparable comparable) { @@ -125,21 +148,27 @@ public abstract class StateHolder { } public void populateNeighbours(Map, Comparable>, S> possibleStateMap) { - if (this.neighbours != null) { - throw new IllegalStateException(); - } else { - Map, S[]> map = new Reference2ObjectArrayMap<>(this.values.size()); - - for (Entry, Comparable> entry : this.values.entrySet()) { - Property property = entry.getKey(); - map.put( - property, - (S[]) property.getPossibleValues().stream().map(comparable -> possibleStateMap.get(this.makeNeighbourValues(property, comparable))).toArray() - ); - } + // Paper start - optimise blockstate property access + final Map, Comparable>, S> map = possibleStateMap; + if (this.optimisedTable.isLoaded()) { + return; + } + this.optimisedTable.loadInTable(map); - this.neighbours = map; + // de-duplicate the tables + for (final Map.Entry, Comparable>, S> entry : map.entrySet()) { + final S value = entry.getValue(); + ((StateHolder)value).optimisedTable = this.optimisedTable; } + + // remove values arrays + for (final Map.Entry, Comparable>, S> entry : map.entrySet()) { + final S value = entry.getValue(); + ((StateHolder)value).values = null; + } + + return; + // Paper end optimise blockstate property access } private Map, Comparable> makeNeighbourValues(Property property, Comparable value) { @@ -149,7 +178,11 @@ public abstract class StateHolder { } public Map, Comparable> getValues() { - return this.values; + // Paper start - optimise blockstate property access + ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable table = this.optimisedTable; + // We have to use this.values until the table is loaded + return table.isLoaded() ? table.getMapView(this.tableIndex) : this.values; + // Paper end - optimise blockstate property access } protected static > Codec codec(Codec propertyMap, Function holderFunction) { diff --git a/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/net/minecraft/world/level/block/state/properties/BooleanProperty.java index 40c83ff614169be8ab988f3ab476eca93acee28d..654f14e0fd1920ec94300649719c2460918899e2 100644 --- a/net/minecraft/world/level/block/state/properties/BooleanProperty.java +++ b/net/minecraft/world/level/block/state/properties/BooleanProperty.java @@ -3,13 +3,23 @@ package net.minecraft.world.level.block.state.properties; import java.util.List; import java.util.Optional; -public final class BooleanProperty extends Property { +public final class BooleanProperty extends Property implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess { // Paper - optimise blockstate property access private static final List VALUES = List.of(true, false); private static final int TRUE_INDEX = 0; private static final int FALSE_INDEX = 1; + // Paper start - optimise blockstate property access + private static final Boolean[] BY_ID = new Boolean[]{ Boolean.FALSE, Boolean.TRUE }; + + @Override + public final int moonrise$getIdFor(final Boolean value) { + return value.booleanValue() ? 1 : 0; + } + // Paper end - optimise blockstate property access + private BooleanProperty(String name) { super(name, Boolean.class); + this.moonrise$setById(BY_ID); // Paper - optimise blockstate property access } @Override diff --git a/net/minecraft/world/level/block/state/properties/EnumProperty.java b/net/minecraft/world/level/block/state/properties/EnumProperty.java index c56728863a084a5e1f6e6d9489d00bb0c83af168..1d785b7bb046ef291342efa3ede6cdeb460f12fb 100644 --- a/net/minecraft/world/level/block/state/properties/EnumProperty.java +++ b/net/minecraft/world/level/block/state/properties/EnumProperty.java @@ -10,11 +10,39 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import net.minecraft.util.StringRepresentable; -public final class EnumProperty & StringRepresentable> extends Property { +public final class EnumProperty & StringRepresentable> extends Property implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess { // Paper - optimise blockstate property access private final List values; private final Map names; private final int[] ordinalToIndex; + // Paper start - optimise blockstate property access + private int[] idLookupTable; + + @Override + public final int moonrise$getIdFor(final T value) { + final Class target = this.getValueClass(); + return ((value.getClass() != target && value.getDeclaringClass() != target)) ? -1 : this.idLookupTable[value.ordinal()]; + } + + private void init() { + final java.util.Collection values = this.getPossibleValues(); + final Class clazz = this.getValueClass(); + + int id = 0; + this.idLookupTable = new int[clazz.getEnumConstants().length]; + Arrays.fill(this.idLookupTable, -1); + final T[] byId = (T[])java.lang.reflect.Array.newInstance(clazz, values.size()); + + for (final T value : values) { + final int valueId = id++; + this.idLookupTable[value.ordinal()] = valueId; + byId[valueId] = value; + } + + this.moonrise$setById(byId); + } + // Paper end - optimise blockstate property access + private EnumProperty(String name, Class clazz, List values) { super(name, clazz); if (values.isEmpty()) { @@ -37,6 +65,7 @@ public final class EnumProperty & StringRepresentable> extends this.names = builder.buildOrThrow(); } + this.init(); // Paper - optimise blockstate property access } @Override diff --git a/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/net/minecraft/world/level/block/state/properties/IntegerProperty.java index 28a15908420cb239c317d58f7e3a1df3c6278b33..b7543eb5a8f87bc7bd275ed9d46a68072c1e57b5 100644 --- a/net/minecraft/world/level/block/state/properties/IntegerProperty.java +++ b/net/minecraft/world/level/block/state/properties/IntegerProperty.java @@ -5,11 +5,33 @@ import java.util.List; import java.util.Optional; import java.util.stream.IntStream; -public final class IntegerProperty extends Property { +public final class IntegerProperty extends Property implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess { // Paper - optimise blockstate property access private final IntImmutableList values; public final int min; public final int max; + // Paper start - optimise blockstate property access + @Override + public final int moonrise$getIdFor(final Integer value) { + final int val = value.intValue(); + final int ret = val - this.min; + + return ret | ((this.max - ret) >> 31); + } + + private void init() { + final int min = this.min; + final int max = this.max; + + final Integer[] byId = new Integer[max - min + 1]; + for (int i = min; i <= max; ++i) { + byId[i - min] = Integer.valueOf(i); + } + + this.moonrise$setById(byId); + } + // Paper end - optimise blockstate property access + private IntegerProperty(String name, int min, int max) { super(name, Integer.class); if (min < 0) { @@ -21,6 +43,7 @@ public final class IntegerProperty extends Property { this.max = max; this.values = IntImmutableList.toList(IntStream.range(min, max + 1)); } + this.init(); // Paper - optimise blockstate property access } @Override diff --git a/net/minecraft/world/level/block/state/properties/Property.java b/net/minecraft/world/level/block/state/properties/Property.java index 92350434746f06bbf4a161c6bc42602de7b45220..1c24f38d21da1be9740512981f219924c5d3cf76 100644 --- a/net/minecraft/world/level/block/state/properties/Property.java +++ b/net/minecraft/world/level/block/state/properties/Property.java @@ -10,7 +10,7 @@ import java.util.stream.Stream; import javax.annotation.Nullable; import net.minecraft.world.level.block.state.StateHolder; -public abstract class Property> { +public abstract class Property> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess { // Paper - optimise blockstate property access private final Class clazz; private final String name; @Nullable @@ -24,9 +24,38 @@ public abstract class Property> { ); private final Codec> valueCodec = this.codec.xmap(this::value, Property.Value::value); + // Paper start - optimise blockstate property access + private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger(); + private final int id; + private T[] byId; + + @Override + public final int moonrise$getId() { + return this.id; + } + + @Override + public final T moonrise$getById(final int id) { + final T[] byId = this.byId; + return id < 0 || id >= byId.length ? null : this.byId[id]; + } + + @Override + public final void moonrise$setById(final T[] byId) { + if (this.byId != null) { + throw new IllegalStateException(); + } + this.byId = byId; + } + + @Override + public abstract int moonrise$getIdFor(final T value); + // Paper end - optimise blockstate property access + protected Property(String name, Class clazz) { this.clazz = clazz; this.name = name; + this.id = ID_GENERATOR.getAndIncrement(); // Paper - optimise blockstate property access } public Property.Value value(T value) { diff --git a/net/minecraft/world/level/chunk/ChunkAccess.java b/net/minecraft/world/level/chunk/ChunkAccess.java index 860d1c9729c4ee97ec6f40f7aa969829070b27c0..94de1217d18e1a7a0fb7b83f21436eaf0a5998c6 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(); private static final LongSet EMPTY_REFERENCE_SET = new LongOpenHashSet(); @@ -75,7 +75,7 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh @Nullable protected BlendingData blendingData; public final Map heightmaps = Maps.newEnumMap(Heightmap.Types.class); - protected ChunkSkyLightSources skyLightSources; + // Paper - rewrite chunk system private final Map structureStarts = Maps.newHashMap(); private final Map structuresRefences = Maps.newHashMap(); protected final Map pendingBlockEntities = Maps.newHashMap(); @@ -88,6 +88,57 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh // CraftBukkit end public final Registry biomeRegistry; // CraftBukkit + // 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 chunkPos, UpgradeData upgradeData, @@ -105,7 +156,7 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh this.inhabitedTime = inhabitedTime; this.postProcessing = new ShortList[levelHeightAccessor.getSectionsCount()]; this.blendingData = blendingData; - this.skyLightSources = new ChunkSkyLightSources(levelHeightAccessor); + // Paper - rewrite chunk system if (sections != null) { if (this.sections.length == sections.length) { System.arraycopy(sections, 0, this.sections, 0, this.sections.length); @@ -116,6 +167,16 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh this.replaceMissingSections(biomeRegistry, this.sections); // Paper - Anti-Xray - make it a non-static method this.biomeRegistry = biomeRegistry; // CraftBukkit + // Paper start - rewrite chunk system + if (!((Object)this instanceof ImposterProtoChunk)) { + this.starlight$setBlockNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(levelHeightAccessor)); + this.starlight$setSkyNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(levelHeightAccessor)); + } + // 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 } private void replaceMissingSections(Registry biomeRegistry, LevelChunkSection[] sections) { // Paper - Anti-Xray - make it a non-static method @@ -442,18 +503,22 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh @Override public Holder getNoiseBiome(int x, int y, int z) { - try { - int quartPosMinY = QuartPos.fromBlock(this.getMinY()); - int i = quartPosMinY + QuartPos.fromBlock(this.getHeight()) - 1; - int i1 = Mth.clamp(y, quartPosMinY, i); - int sectionIndex = this.getSectionIndex(QuartPos.toBlock(i1)); - return this.sections[sectionIndex].getNoiseBiome(x & 3, i1 & 3, z & 3); - } catch (Throwable var8) { - CrashReport crashReport = CrashReport.forThrowable(var8, "Getting biome"); - CrashReportCategory crashReportCategory = crashReport.addCategory("Biome being got"); - crashReportCategory.setDetail("Location", () -> CrashReportCategory.formatLocation(this, x, y, z)); - throw new ReportedException(crashReport); + // Paper start - get block chunk optimisation + int sectionY = (y >> 2) - this.minSection; + int rel = y & 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(x & 3, rel, z & 3); + // Paper end - get block chunk optimisation } // CraftBukkit start public void setBiome(int i, int j, int k, Holder biome) { @@ -507,12 +572,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 record PackedTicks(List> blocks, List> fluids) { diff --git a/net/minecraft/world/level/chunk/ChunkGenerator.java b/net/minecraft/world/level/chunk/ChunkGenerator.java index 65117a9cb9d1b8684cae8d36ea3b8e2050fb928c..a9d65e28b849c9660a14ef7c16ed17bd5182bd7e 100644 --- a/net/minecraft/world/level/chunk/ChunkGenerator.java +++ b/net/minecraft/world/level/chunk/ChunkGenerator.java @@ -116,7 +116,7 @@ public abstract class ChunkGenerator { return CompletableFuture.supplyAsync(() -> { chunk.fillBiomesFromNoise(this.biomeSource, randomState.sampler()); return chunk; - }, Util.backgroundExecutor().forName("init_biomes")); + }, Runnable::run); // Paper - rewrite chunk system } public abstract void applyCarvers( @@ -315,7 +315,7 @@ public abstract class ChunkGenerator { return Pair.of(placement.getLocatePos(chunkPos), holder); } - ChunkAccess chunk = level.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.STRUCTURE_STARTS); + ChunkAccess chunk = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader)level).moonrise$syncLoadNonFull(chunkPos.x, chunkPos.z, ChunkStatus.STRUCTURE_STARTS); // Paper - rewrite chunk system StructureStart startForStructure = structureManager.getStartForStructure(SectionPos.bottomOf(chunk), holder.value(), chunk); if (startForStructure != null && startForStructure.isValid() && (!skipKnownStructures || tryAddReference(structureManager, startForStructure))) { return Pair.of(placement.getLocatePos(startForStructure.getChunkPos()), holder); diff --git a/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/net/minecraft/world/level/chunk/EmptyLevelChunk.java index ec128412e4a0d3d21e3b6abea8cd06c03656f00c..07b7e82c7d24f52c0251e09195451841d47883c9 100644 --- a/net/minecraft/world/level/chunk/EmptyLevelChunk.java +++ b/net/minecraft/world/level/chunk/EmptyLevelChunk.java @@ -13,7 +13,7 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.material.FluidState; import net.minecraft.world.level.material.Fluids; -public class EmptyLevelChunk extends LevelChunk { +public class EmptyLevelChunk extends LevelChunk implements ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system private final Holder biome; public EmptyLevelChunk(Level level, ChunkPos pos, Holder biome) { @@ -21,6 +21,40 @@ public class EmptyLevelChunk extends LevelChunk { this.biome = biome; } + // 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 7cd5d42e0c28033ee80f18bd0031ed1241fb7aae..718d00a386f32423db9f6d6c95b4a20698b976f5 100644 --- a/net/minecraft/world/level/chunk/HashMapPalette.java +++ b/net/minecraft/world/level/chunk/HashMapPalette.java @@ -8,12 +8,19 @@ import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.VarInt; import net.minecraft.util.CrudeIncrementalIntIdentityHashBiMap; -public class HashMapPalette implements Palette { +public class HashMapPalette implements Palette, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // Paper - optimise palette reads private final IdMap registry; private final CrudeIncrementalIntIdentityHashBiMap values; private final PaletteResize resizeHandler; private final int bits; + // Paper start - optimise palette reads + @Override + public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData container) { + return ((ca.spottedleaf.moonrise.patches.fast_palette.FastPalette)this.values).moonrise$getRawPalette(container); + } + // Paper end - optimise palette reads + public HashMapPalette(IdMap registry, int bits, PaletteResize resizeHandler, List values) { this(registry, bits, resizeHandler); values.forEach(this.values::add); diff --git a/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/net/minecraft/world/level/chunk/ImposterProtoChunk.java index e7c0f4da8508fbca467326f475668d66454d7b77..41856c98d97e7eb0782f8e441b9a269a47ed1914 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 = allowWrites; } + // 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 96b0342ab7b922aa16d07b6c00542e6cb66c974a..c1ae7755e8d6fa8501d2210dab7605d993c55722 100644 --- a/net/minecraft/world/level/chunk/LevelChunk.java +++ b/net/minecraft/world/level/chunk/LevelChunk.java @@ -52,7 +52,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() { @Override @@ -93,6 +93,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(Level level, ChunkPos pos) { this(level, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, null, null, null); @@ -122,6 +155,14 @@ public class LevelChunk extends ChunkAccess { this.postLoad = postLoad; this.blockTicks = blockTicks; this.fluidTicks = fluidTicks; + // 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 } public LevelChunk(ServerLevel level, ProtoChunk chunk, @Nullable LevelChunk.PostLoadProcessor postLoad) { @@ -159,13 +200,19 @@ public class LevelChunk extends ChunkAccess { } } - this.skyLightSources = chunk.skyLightSources; + // Paper - rewrite chunk system this.setLightCorrect(chunk.isLightCorrect()); this.markUnsaved(); this.needsDecoration = true; // CraftBukkit // CraftBukkit start this.persistentDataContainer = chunk.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)chunk).starlight$getBlockNibbles()); + this.starlight$setSkyNibbles(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)chunk).starlight$getSkyNibbles()); + this.starlight$setSkyEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)chunk).starlight$getSkyEmptinessMap()); + this.starlight$setBlockEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)chunk).starlight$getBlockEmptinessMap()); + // Paper end - rewrite chunk system } public void setUnsavedListener(LevelChunk.UnsavedListener unsavedListener) { @@ -341,7 +388,7 @@ public class LevelChunk extends ChunkAccess { if (LightEngine.hasDifferentLightProperties(blockState, state)) { ProfilerFiller profilerFiller = Profiler.get(); profilerFiller.push("updateSkyLightSources"); - this.skyLightSources.update(this, i, y, i2); + // Paper - rewrite chunk system profilerFiller.popPush("queueCheckLight"); this.level.getChunkSource().getLightEngine().checkBlock(pos); profilerFiller.pop(); @@ -573,11 +620,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 @@ -586,6 +634,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; @@ -612,13 +661,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 @@ -626,8 +677,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() { @@ -706,6 +780,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 fc21c3268c4b4fda2933d71f0913db28e3796653..2ff691bcd32f587e8add2d8eda7e7339ccbde6e8 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; public static final int SECTION_SIZE = 4096; @@ -24,6 +24,30 @@ public class LevelChunkSection { public final PalettedContainer states; private PalettedContainer> biomes; // CraftBukkit - read/write + // Paper start - block counting + private static final it.unimi.dsi.fastutil.shorts.ShortArrayList FULL_LIST = new it.unimi.dsi.fastutil.shorts.ShortArrayList(16*16*16); + static { + for (short i = 0; i < (16*16*16); ++i) { + FULL_LIST.add(i); + } + } + + private boolean isClient; + private static final short CLIENT_FORCED_SPECIAL_COLLIDING_BLOCKS = (short)9999; + private short specialCollidingBlocks; + private final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = new ca.spottedleaf.moonrise.common.list.ShortList(); + + @Override + public final boolean moonrise$hasSpecialCollidingBlocks() { + return this.specialCollidingBlocks != 0; + } + + @Override + public final ca.spottedleaf.moonrise.common.list.ShortList moonrise$getTickingBlockList() { + return this.tickingBlocks; + } + // Paper end - block counting + private LevelChunkSection(LevelChunkSection section) { this.nonEmptyBlockCount = section.nonEmptyBlockCount; this.tickingBlockCount = section.tickingBlockCount; @@ -69,6 +93,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 useLocks) { BlockState blockState; if (useLocks) { @@ -86,7 +149,7 @@ public class LevelChunkSection { } } - if (!fluidState.isEmpty()) { + if (!!fluidState.isRandomlyTicking()) { // Paper - block counting this.tickingFluidCount--; } @@ -97,10 +160,12 @@ public class LevelChunkSection { } } - if (!fluidState1.isEmpty()) { + if (!!fluidState1.isRandomlyTicking()) { // Paper - block counting this.tickingFluidCount++; } + this.updateBlockCallback(x, y, z, state, blockState); // Paper - block counting + return blockState; } @@ -121,35 +186,70 @@ public class LevelChunkSection { } public void recalcBlockCounts() { - class BlockCounter implements PalettedContainer.CountConsumer { - public int nonEmptyBlockCount; - public int tickingBlockCount; - public int tickingFluidCount; - - @Override - public void accept(BlockState state, int count) { - FluidState fluidState = state.getFluidState(); - if (!state.isAir()) { - this.nonEmptyBlockCount += count; - if (state.isRandomlyTicking()) { - this.tickingBlockCount += count; + // Paper start - block counting + // reset, then recalculate + this.nonEmptyBlockCount = (short)0; + this.tickingBlockCount = (short)0; + this.tickingFluidCount = (short)0; + this.specialCollidingBlocks = (short)0; + this.tickingBlocks.clear(); + + if (this.maybeHas((final BlockState state) -> !state.isAir())) { + final PalettedContainer.Data data = this.states.data; + final Palette palette = data.palette(); + final int paletteSize = palette.getSize(); + final net.minecraft.util.BitStorage storage = data.storage(); + + final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap counts; + if (paletteSize == 1) { + counts = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1); + counts.put(0, FULL_LIST); + } else { + counts = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingBitStorage)storage).moonrise$countEntries(); + } + + for (final java.util.Iterator> iterator = counts.int2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + final it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry entry = iterator.next(); + final int paletteIdx = entry.getIntKey(); + final it.unimi.dsi.fastutil.shorts.ShortArrayList coordinates = entry.getValue(); + final int paletteCount = coordinates.size(); + + final BlockState state = palette.valueFor(paletteIdx); + + 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; + + tickingBlocks.setMinCapacity(Math.min((rawLen + tickingBlocks.size()) * 3 / 2, 16*16*16)); + + java.util.Objects.checkFromToIndex(0, paletteCount, raw.length); + for (int i = 0; i < paletteCount; ++i) { + tickingBlocks.add(raw[i]); } } - if (!fluidState.isEmpty()) { - this.nonEmptyBlockCount += count; - if (fluidState.isRandomlyTicking()) { - this.tickingFluidCount += count; + final FluidState fluid = state.getFluidState(); + + if (!fluid.isEmpty()) { + //this.nonEmptyBlockCount += count; // fix vanilla bug: make non-empty block count correct + if (fluid.isRandomlyTicking()) { + this.tickingFluidCount += (short)paletteCount; } } } } - - BlockCounter blockCounter = new BlockCounter(); - this.states.count(blockCounter); - this.nonEmptyBlockCount = (short)blockCounter.nonEmptyBlockCount; - this.tickingBlockCount = (short)blockCounter.tickingBlockCount; - this.tickingFluidCount = (short)blockCounter.tickingFluidCount; + // Paper end - block counting } public PalettedContainer getStates() { @@ -166,6 +266,11 @@ public class LevelChunkSection { PalettedContainer> palettedContainer = this.biomes.recreate(); palettedContainer.read(buffer); this.biomes = palettedContainer; + // 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 buffer) { diff --git a/net/minecraft/world/level/chunk/LinearPalette.java b/net/minecraft/world/level/chunk/LinearPalette.java index 5ae2f38dc613ac6129af49084980d064f14ff153..2073f6ff41aa570102621d183ee890b076267d54 100644 --- a/net/minecraft/world/level/chunk/LinearPalette.java +++ b/net/minecraft/world/level/chunk/LinearPalette.java @@ -7,13 +7,20 @@ import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.VarInt; import org.apache.commons.lang3.Validate; -public class LinearPalette implements Palette { +public class LinearPalette implements Palette, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // Paper - optimise palette reads private final IdMap registry; private final T[] values; private final PaletteResize resizeHandler; private final int bits; private int size; + // Paper start - optimise palette reads + @Override + public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData container) { + return this.values; + } + // Paper end - optimise palette reads + private LinearPalette(IdMap registry, int bits, PaletteResize resizeHandler, List values) { this.registry = registry; 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 b4b973e453a093dcc04a6b7257883aa0065e2a89..a80b2e9dceea423180a9c390d1970317dff4f1b0 100644 --- a/net/minecraft/world/level/chunk/Palette.java +++ b/net/minecraft/world/level/chunk/Palette.java @@ -5,7 +5,7 @@ import java.util.function.Predicate; import net.minecraft.core.IdMap; import net.minecraft.network.FriendlyByteBuf; -public interface Palette { +public interface Palette extends ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // Paper - optimise palette reads int idFor(T state); boolean maybeHas(Predicate filter); diff --git a/net/minecraft/world/level/chunk/PalettedContainer.java b/net/minecraft/world/level/chunk/PalettedContainer.java index a6028a54c75de068515e95913b21160ab4326985..f5da433050fd3060e0335d4002d520ebe8cd691f 100644 --- a/net/minecraft/world/level/chunk/PalettedContainer.java +++ b/net/minecraft/world/level/chunk/PalettedContainer.java @@ -29,7 +29,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer private final PaletteResize dummyPaletteResize = (bits, objectAdded) -> 0; public final IdMap registry; private final T @org.jetbrains.annotations.Nullable [] presetValues; // Paper - Anti-Xray - Add preset values - private volatile PalettedContainer.Data data; + public volatile PalettedContainer.Data data; // Paper - optimise collisions - public private final PalettedContainer.Strategy strategy; //private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused @@ -75,6 +75,33 @@ public class PalettedContainer implements PaletteResize, PalettedContainer ); } + // Paper start - optimise palette reads + private void updateData(final PalettedContainer.Data data) { + if (data != null) { + ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData)(Object)data).moonrise$setPalette( + ((ca.spottedleaf.moonrise.patches.fast_palette.FastPalette)data.palette).moonrise$getRawPalette((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData)(Object)data) + ); + } + } + + private T readPaletteSlow(final PalettedContainer.Data data, final int paletteIdx) { + return data.palette.valueFor(paletteIdx); + } + + private T readPalette(final PalettedContainer.Data data, final int paletteIdx) { + final T[] palette = ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData)(Object)data).moonrise$getPalette(); + if (palette == null) { + return this.readPaletteSlow(data, paletteIdx); + } + + final T ret = palette[paletteIdx]; + if (ret == null) { + throw new IllegalArgumentException("Palette index out of bounds"); + } + return ret; + } + // Paper end - optimise palette reads + // Paper start - Anti-Xray - Add preset values @Deprecated @io.papermc.paper.annotation.DoNotUse public PalettedContainer(IdMap registry, PalettedContainer.Strategy strategy, PalettedContainer.Configuration configuration, BitStorage storage, List values) { @@ -109,6 +136,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer } } // Paper end + this.updateData(this.data); // Paper - optimise palette reads } // Paper start - Anti-Xray - Add preset values @@ -118,6 +146,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer this.registry = registry; this.strategy = strategy; this.data = data; + this.updateData(this.data); // Paper - optimise palette reads } private PalettedContainer(PalettedContainer other, T @org.jetbrains.annotations.Nullable [] presetValues) { // Paper - Anti-Xray - Add preset values @@ -139,6 +168,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer this.registry = registry; this.data = this.createOrReuseData(null, 0); this.data.palette.idFor(palette); + this.updateData(this.data); // Paper - optimise palette reads } private PalettedContainer.Data createOrReuseData(@Nullable PalettedContainer.Data data, int id) { @@ -163,6 +193,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer this.data = data1; // Paper start - Anti-Xray this.addPresetValues(); + this.updateData(this.data); // Paper - optimise palette reads return objectAdded == null ? -1 : data1.palette.idFor(objectAdded); } private void addPresetValues() { @@ -192,9 +223,12 @@ public class PalettedContainer implements PaletteResize, PalettedContainer } private T getAndSet(int index, T state) { - int i = this.data.palette.idFor(state); - int andSet = this.data.storage.getAndSet(index, i); - return this.data.palette.valueFor(andSet); + // Paper start - optimise palette reads + final int paletteIdx = this.data.palette.idFor(state); + final PalettedContainer.Data data = this.data; + final int prev = data.storage.getAndSet(index, paletteIdx); + return this.readPalette(data, prev); + // Paper end - optimise palette reads } public synchronized void set(int x, int y, int z, T state) { // Paper - synchronize @@ -217,9 +251,11 @@ public class PalettedContainer implements PaletteResize, PalettedContainer return this.get(this.strategy.getIndex(x, y, z)); } - protected T get(int index) { - PalettedContainer.Data data = this.data; - return data.palette.valueFor(data.storage.get(index)); + public T get(int index) { // Paper - public + // Paper start - optimise palette reads + final PalettedContainer.Data data = this.data; + return this.readPalette(data, data.storage.get(index)); + // Paper end - optimise palette reads } @Override @@ -240,6 +276,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer buffer.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(); } @@ -390,7 +427,44 @@ public class PalettedContainer implements PaletteResize, PalettedContainer void accept(T state, int count); } - record Data(PalettedContainer.Configuration configuration, BitStorage storage, Palette palette) { + // Paper start - optimise palette reads + public static final class Data implements ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData { + + private final PalettedContainer.Configuration configuration; + private final BitStorage storage; + private final Palette palette; + + private T[] moonrise$palette; + + public Data(final PalettedContainer.Configuration configuration, final BitStorage storage, final Palette palette) { + this.configuration = configuration; + this.storage = storage; + this.palette = palette; + } + + public PalettedContainer.Configuration configuration() { + return this.configuration; + } + + public BitStorage storage() { + return this.storage; + } + + public Palette palette() { + return this.palette; + } + + @Override + public final T[] moonrise$getPalette() { + return this.moonrise$palette; + } + + @Override + public final void moonrise$setPalette(final T[] palette) { + this.moonrise$palette = palette; + } + // Paper end - optimise palette reads + public void copyFrom(Palette palette, BitStorage bitStorage) { for (int i = 0; i < bitStorage.getSize(); i++) { T object = palette.valueFor(bitStorage.get(i)); diff --git a/net/minecraft/world/level/chunk/ProtoChunk.java b/net/minecraft/world/level/chunk/ProtoChunk.java index 8c333d7f390d823a7c7f303e2f444f52ec16f799..e66239e2da91bd3ddf358d239be796719c0da327 100644 --- a/net/minecraft/world/level/chunk/ProtoChunk.java +++ b/net/minecraft/world/level/chunk/ProtoChunk.java @@ -151,7 +151,7 @@ public class ProtoChunk extends ChunkAccess { } if (LightEngine.hasDifferentLightProperties(blockState, state)) { - this.skyLightSources.update(this, relativeBlockPosX, y, relativeBlockPosZ); + // 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 bc84910dbc688331efaea76972a6625014ff76f5..2ffae24b0cb1a20c7d5a8520f1b5197c2cedea11 100644 --- a/net/minecraft/world/level/chunk/SingleValuePalette.java +++ b/net/minecraft/world/level/chunk/SingleValuePalette.java @@ -8,12 +8,24 @@ import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.VarInt; import org.apache.commons.lang3.Validate; -public class SingleValuePalette implements Palette { +public class SingleValuePalette implements Palette, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // Paper - optimise palette reads private final IdMap registry; @Nullable private T value; private final PaletteResize resizeHandler; + // Paper start - optimise palette reads + private T[] rawPalette; + + @Override + public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData container) { + if (this.rawPalette != null) { + return this.rawPalette; + } + return this.rawPalette = (T[])new Object[] { this.value }; + } + // Paper end - optimise palette reads + public SingleValuePalette(IdMap registry, PaletteResize resizeHandler, List value) { this.registry = registry; this.resizeHandler = resizeHandler; @@ -33,6 +45,11 @@ public class SingleValuePalette implements Palette { return this.resizeHandler.onResize(1, state); } else { this.value = state; + // Paper start - optimise palette reads + if (this.rawPalette != null) { + this.rawPalette[0] = state; + } + // Paper end - optimise palette reads return 0; } } @@ -58,6 +75,11 @@ public class SingleValuePalette implements Palette { @Override public void read(FriendlyByteBuf buffer) { this.value = this.registry.byIdOrThrow(buffer.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 9c6f4aa173fa25f9c8a3852d91a4585e069236b6..b14001afe0bf841dac7d0a1d1568fd10f6086237 100644 --- a/net/minecraft/world/level/chunk/status/ChunkPyramid.java +++ b/net/minecraft/world/level/chunk/status/ChunkPyramid.java @@ -54,7 +54,7 @@ public record ChunkPyramid(ImmutableList steps) { .step(ChunkStatus.CARVERS, builder -> builder) .step(ChunkStatus.FEATURES, builder -> builder) .step(ChunkStatus.INITIALIZE_LIGHT, builder -> builder.setTask(ChunkStatusTasks::initializeLight)) - .step(ChunkStatus.LIGHT, builder -> builder.addRequirement(ChunkStatus.INITIALIZE_LIGHT, 1).setTask(ChunkStatusTasks::light)) + .step(ChunkStatus.LIGHT, builder -> builder.setTask(ChunkStatusTasks::light)) // Paper - rewrite chunk system - starlight does not need neighbours .step(ChunkStatus.SPAWN, builder -> builder) .step(ChunkStatus.FULL, builder -> builder.setTask(ChunkStatusTasks::full)) .build(); diff --git a/net/minecraft/world/level/chunk/status/ChunkStatus.java b/net/minecraft/world/level/chunk/status/ChunkStatus.java index 7a64b00ff31d1273d0b0b9a3cfd43808c88ef46a..c9d8a1c0a75c34ccd9f5cead02cccd776276f3cb 100644 --- a/net/minecraft/world/level/chunk/status/ChunkStatus.java +++ b/net/minecraft/world/level/chunk/status/ChunkStatus.java @@ -11,7 +11,7 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.levelgen.Heightmap; import org.jetbrains.annotations.VisibleForTesting; -public class ChunkStatus { +public class ChunkStatus implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus { // Paper - rewrite chunk system public static final int MAX_STRUCTURE_DISTANCE = 8; private static final EnumSet WORLDGEN_HEIGHTMAPS = EnumSet.of(Heightmap.Types.OCEAN_FLOOR_WG, Heightmap.Types.WORLD_SURFACE_WG); public static final EnumSet FINAL_HEIGHTMAPS = EnumSet.of( @@ -51,8 +51,70 @@ 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 parent, EnumSet heightmapsAfter, ChunkType chunkType) { + // Paper start - rewrite chunk system + this.isParallelCapable = false; + this.writeRadius = -1; + this.nextStatus = (ChunkStatus)(Object)this; + if (parent != null) { + parent.nextStatus = (ChunkStatus)(Object)this; + } + this.warnedAboutNoImmediateComplete = new java.util.concurrent.atomic.AtomicBoolean(); + // Paper end - rewrite chunk system this.parent = parent == null ? this : parent; this.chunkType = chunkType; this.heightmapsAfter = heightmapsAfter; diff --git a/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java b/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java index c953bc93de8a42bcc12b7e8f46b3ae804e54964e..2ccbdfdcf81556306e098277ecf119d5fd02138c 100644 --- a/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java +++ b/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java @@ -182,7 +182,7 @@ public class ChunkStatusTasks { if (protoChunk instanceof ImposterProtoChunk imposterProtoChunk) { wrapped = imposterProtoChunk.getWrapped(); } else { - wrapped = new LevelChunk(serverLevel, protoChunk, chunk1 -> postLoadProtoChunk(serverLevel, protoChunk.getEntities())); + wrapped = new LevelChunk(serverLevel, protoChunk, chunk1 -> postLoadProtoChunk(serverLevel, protoChunk.getEntities(), protoChunk.getPos())); // Paper - rewrite chunk system generationChunkHolder.replaceProtoChunk(new ImposterProtoChunk(wrapped, false)); } @@ -196,7 +196,7 @@ public class ChunkStatusTasks { }, worldGenContext.mainThreadExecutor()); } - public static void postLoadProtoChunk(ServerLevel level, List entityTags) { // Paper - public + public static void postLoadProtoChunk(ServerLevel level, List entityTags, ChunkPos pos) { // Paper - public // Paper - rewrite chunk system - add ChunkPos param if (!entityTags.isEmpty()) { // CraftBukkit start - these are spawned serialized (DefinedStructure) and we don't call an add event below at the moment due to ordering complexities level.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(entityTags, level, EntitySpawnReason.LOAD).filter((entity) -> { @@ -208,7 +208,7 @@ public class ChunkStatusTasks { } checkDupeUUID(level, 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 7a4d299d2ce36982204e30de9278ddfd5b37c3df..b8348976e80578d9eff64eea68c04c603fed49ad 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); } @@ -40,6 +81,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 80bc7ad9ad076968d06279dedd845d5946cf2501..433feab7f7c1931f79836164a0b8c4a1c3b75ba6 100644 --- a/net/minecraft/world/level/chunk/storage/ChunkStorage.java +++ b/net/minecraft/world/level/chunk/storage/ChunkStorage.java @@ -22,20 +22,30 @@ import net.minecraft.world.level.chunk.ChunkGenerator; 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 info, Path folder, DataFixer fixerUpper, boolean sync) { this.fixerUpper = fixerUpper; - this.worker = new IOWorker(info, folder, sync); + this.storage = new IOWorker(info, folder, sync).storage; // Paper - rewrite chunk system } public boolean isOldChunkAround(ChunkPos pos, int radius) { - return this.worker.isOldChunkAround(pos, radius); + return true; // Paper - rewrite chunk system } // CraftBukkit start @@ -99,7 +109,9 @@ public class ChunkStorage implements AutoCloseable { chunkData = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, chunkData, version, 1493); // Paper - replace chunk converter if (chunkData.getCompound("Level").getBoolean("hasLegacyStructureData")) { LegacyStructureDataHandler legacyStructureHandler = this.getLegacyStructureHandler(levelKey, storage); + synchronized (legacyStructureHandler) { // Paper - rewrite chunk system chunkData = legacyStructureHandler.updateFromLegacy(chunkData); + } } } @@ -163,7 +175,13 @@ public class ChunkStorage implements AutoCloseable { } public CompletableFuture> read(ChunkPos chunkPos) { - return this.worker.loadAsync(chunkPos); + // Paper start - rewrite chunk system + try { + return CompletableFuture.completedFuture(Optional.ofNullable(this.storage.read(chunkPos))); + } catch (final Throwable throwable) { + return CompletableFuture.failedFuture(throwable); + } + // Paper end - rewrite chunk system } public CompletableFuture write(ChunkPos pos, Supplier tagSupplier) { @@ -179,29 +197,54 @@ public class ChunkStorage implements AutoCloseable { }; // Paper end - guard against possible chunk pos desync this.handleLegacyStructureIndex(pos); - return this.worker.store(pos, guardedPosCheck); // Paper - guard against possible chunk pos desync + // Paper start - rewrite chunk system + try { + this.storage.write(pos, 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 } @Override 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 c3c9771138cb1712ea429d8c45596220830314eb..da05fb780c55381a7a08ced51d01764a645740b2 100644 --- a/net/minecraft/world/level/chunk/storage/EntityStorage.java +++ b/net/minecraft/world/level/chunk/storage/EntityStorage.java @@ -71,12 +71,12 @@ public class EntityStorage implements EntityPersistentStorage { } } - private static ChunkPos readChunkPos(CompoundTag tag) { + public static ChunkPos readChunkPos(CompoundTag tag) { // Paper - public int[] intArray = tag.getIntArray("Position"); return new ChunkPos(intArray[0], intArray[1]); } - private static void writeChunkPos(CompoundTag tag, ChunkPos pos) { + public static void writeChunkPos(CompoundTag tag, ChunkPos pos) { // Paper - public tag.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 889e188e920edb284f04b264bcdd06146f54a4cb..2199a9e2a0141c646d108f2687a27f1d165453c5 100644 --- a/net/minecraft/world/level/chunk/storage/IOWorker.java +++ b/net/minecraft/world/level/chunk/storage/IOWorker.java @@ -30,7 +30,7 @@ public class IOWorker implements ChunkScanAccess, AutoCloseable { private static final Logger LOGGER = LogUtils.getLogger(); private final AtomicBoolean shutdownRequested = new AtomicBoolean(); private final PriorityConsecutiveExecutor consecutiveExecutor; - private final RegionFileStorage storage; + public final RegionFileStorage storage; // Paper - public private final SequencedMap pendingWrites = new LinkedHashMap<>(); private final Long2ObjectLinkedOpenHashMap> regionCacheForBlender = new Long2ObjectLinkedOpenHashMap<>(); private static final int REGION_CACHE_SIZE = 1024; diff --git a/net/minecraft/world/level/chunk/storage/RegionFile.java b/net/minecraft/world/level/chunk/storage/RegionFile.java index 783a2d80f6197dd0af0dc81909f0353a8ea2ecf4..7da388ffab162c282cad0f297bb7304f3c2abbaf 100644 --- a/net/minecraft/world/level/chunk/storage/RegionFile.java +++ b/net/minecraft/world/level/chunk/storage/RegionFile.java @@ -22,7 +22,7 @@ import net.minecraft.util.profiling.jfr.JvmProfiler; 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; @VisibleForTesting @@ -45,6 +45,21 @@ public class RegionFile implements AutoCloseable { @VisibleForTesting protected final RegionBitmap usedSectors = new RegionBitmap(); + // 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 info, Path path, Path externalFileDir, boolean sync) throws IOException { this(info, path, externalFileDir, RegionFileVersion.getCompressionFormat(), sync); // Paper - Configurable region compression format } @@ -204,6 +219,16 @@ public class RegionFile implements AutoCloseable { @Nullable private DataInputStream createExternalChunkInputStream(ChunkPos chunkPos, byte versionByte) throws IOException { + // Paper start - rewrite chunk system + final DataInputStream is = this.createExternalChunkInputStream0(chunkPos, versionByte); + if (is == null) { + return is; + } + return new ca.spottedleaf.moonrise.patches.chunk_system.util.stream.ExternalChunkStreamMarker(is); + } + @Nullable + private DataInputStream createExternalChunkInputStream0(ChunkPos chunkPos, byte versionByte) throws IOException { + // Paper end - rewrite chunk system Path externalChunkPath = this.getExternalChunkPath(chunkPos); if (!Files.isRegularFile(externalChunkPath)) { LOGGER.error("External chunk path {} is not file", externalChunkPath); @@ -398,9 +423,28 @@ public class RegionFile implements AutoCloseable { } } - class ChunkBuffer extends ByteArrayOutputStream { + 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 pos) { super(8096); super.write(0); @@ -417,7 +461,7 @@ public class RegionFile implements AutoCloseable { int i = this.count - 5 + 1; 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 51bf310423013d0ae9d3202d66e36a053a767197..e35bb5534e2fbd2e30154a15ff6d39baa121608f 100644 --- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java @@ -14,7 +14,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; public final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap<>(); @@ -22,29 +22,218 @@ public final class RegionFileStorage implements AutoCloseable { private final Path folder; private final boolean sync; - RegionFileStorage(RegionStorageInfo info, Path folder, boolean sync) { + // 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 + // Paper start - rewrite chunk system + public RegionFile getRegionFile(ChunkPos chunkcoordintpair) throws IOException { + return this.getRegionFile(chunkcoordintpair, false); + } + // Paper end - rewrite chunk system + + protected RegionFileStorage(RegionStorageInfo info, Path folder, boolean sync) { // Paper - protected this.folder = folder; this.sync = sync; this.info = info; } @org.jetbrains.annotations.Contract("_, false -> !null") @Nullable private RegionFile getRegionFile(ChunkPos chunkPos, boolean existingOnly) throws IOException { // CraftBukkit - long packedChunkPos = ChunkPos.asLong(chunkPos.getRegionX(), chunkPos.getRegionZ()); - RegionFile regionFile = this.regionCache.getAndMoveToFirst(packedChunkPos); - if (regionFile != null) { - return regionFile; - } else { - if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - Sanitise RegionFileCache and make configurable + // Paper start - rewrite chunk system + if (existingOnly) { + return this.moonrise$getRegionFileIfExists(chunkPos.x, chunkPos.z); + } + synchronized (this) { + final long key = ChunkPos.asLong(chunkPos.x >> REGION_SHIFT, chunkPos.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(chunkPos.x, chunkPos.z)); + + this.createRegionFile(key); + FileUtil.createDirectoriesSafe(this.folder); - Path path = this.folder.resolve("r." + chunkPos.getRegionX() + "." + chunkPos.getRegionZ() + ".mca"); - if (existingOnly && !java.nio.file.Files.exists(path)) return null; // CraftBukkit - RegionFile regionFile1 = new RegionFile(this.info, path, this.folder, this.sync); - this.regionCache.putAndMoveToFirst(packedChunkPos, 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 @@ -126,8 +315,14 @@ public final class RegionFileStorage implements AutoCloseable { } } - protected void write(ChunkPos chunkPos, @Nullable CompoundTag chunkData) throws IOException { - RegionFile regionFile = this.getRegionFile(chunkPos, false); // CraftBukkit + public void write(ChunkPos chunkPos, @Nullable CompoundTag chunkData) throws IOException { // Paper - rewrite chunk system - public + RegionFile regionFile = this.getRegionFile(chunkPos, chunkData == 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 (chunkData == null) { regionFile.clear(chunkPos); } else { @@ -140,23 +335,36 @@ public final class RegionFileStorage implements AutoCloseable { @Override public void close() throws IOException { - ExceptionCollector exceptionCollector = new ExceptionCollector<>(); - - for (RegionFile regionFile : this.regionCache.values()) { - try { - regionFile.close(); - } catch (IOException var5) { - exceptionCollector.add(var5); + // Paper start - rewrite chunk system + synchronized (this) { + final ExceptionCollector exceptionCollector = new ExceptionCollector<>(); + for (final RegionFile regionFile : this.regionCache.values()) { + try { + regionFile.close(); + } catch (final IOException ex) { + exceptionCollector.add(ex); + } } + exceptionCollector.throwIfPresent(); } - - exceptionCollector.throwIfPresent(); + // Paper end - rewrite chunk system } public void flush() throws IOException { - for (RegionFile regionFile : this.regionCache.values()) { - regionFile.flush(); + // Paper start - rewrite chunk system + synchronized (this) { + final ExceptionCollector exceptionCollector = new ExceptionCollector<>(); + for (final RegionFile regionFile : this.regionCache.values()) { + try { + regionFile.flush(); + } catch (final IOException ex) { + exceptionCollector.add(ex); + } + } + + exceptionCollector.throwIfPresent(); } + // Paper end - rewrite chunk system } public RegionStorageInfo info() { diff --git a/net/minecraft/world/level/chunk/storage/SectionStorage.java b/net/minecraft/world/level/chunk/storage/SectionStorage.java index 7dc1ffffd9d0fec54dbc254c154ee85ee750174d..778bd73a938c94ecb85ca0f8b686ff4e1baee040 100644 --- a/net/minecraft/world/level/chunk/storage/SectionStorage.java +++ b/net/minecraft/world/level/chunk/storage/SectionStorage.java @@ -40,10 +40,10 @@ import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.LevelHeightAccessor; import org.slf4j.Logger; -public class SectionStorage implements AutoCloseable { +public class SectionStorage implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage { // Paper - rewrite chunk system static final Logger LOGGER = LogUtils.getLogger(); private static final String SECTIONS_TAG = "Sections"; - private final SimpleRegionStorage simpleRegionStorage; + // Paper - rewrite chunk system private final Long2ObjectMap> storage = new Long2ObjectOpenHashMap<>(); private final LongLinkedOpenHashSet dirtyChunks = new LongLinkedOpenHashSet(); private final Codec

    codec; @@ -57,6 +57,18 @@ public class SectionStorage implements AutoCloseable { private final Long2ObjectMap>>> pendingLoads = new Long2ObjectOpenHashMap<>(); private final Object loadLock = new Object(); + // Paper start - rewrite chunk system + private final RegionFileStorage regionStorage; + + @Override + public final RegionFileStorage moonrise$getRegionStorage() { + return this.regionStorage; + } + + @Override + public void moonrise$close() throws IOException {} + // Paper end - rewrite chunk system + public SectionStorage( SimpleRegionStorage simpleRegionStorage, Codec

    codec, @@ -67,7 +79,7 @@ public class SectionStorage implements AutoCloseable { ChunkIOErrorReporter errorReporter, LevelHeightAccessor levelHeightAccessor ) { - this.simpleRegionStorage = simpleRegionStorage; + // Paper - rewrite chunk system this.codec = codec; this.packer = packer; this.unpacker = unpacker; @@ -75,6 +87,7 @@ public class SectionStorage implements AutoCloseable { this.registryAccess = registryAccess; this.errorReporter = errorReporter; this.levelHeightAccessor = levelHeightAccessor; + this.regionStorage = simpleRegionStorage.worker.storage; // Paper - rewrite chunk system } protected void tick(BooleanSupplier aheadOfTime) { @@ -188,65 +201,15 @@ public class SectionStorage implements AutoCloseable { } private CompletableFuture>> tryRead(ChunkPos chunkPos) { - RegistryOps registryOps = this.registryAccess.createSerializationContext(NbtOps.INSTANCE); - return this.simpleRegionStorage - .read(chunkPos) - .thenApplyAsync( - optional -> optional.map( - compoundTag -> SectionStorage.PackedChunk.parse(this.codec, registryOps, compoundTag, this.simpleRegionStorage, this.levelHeightAccessor) - ), - Util.backgroundExecutor().forName("parseSection") - ) - .exceptionally(cause -> { - if (cause instanceof CompletionException) { - cause = cause.getCause(); - } - - if (cause 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(cause); - } - }); + throw new IllegalStateException("Only chunk system can write state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system } private void unpackChunk(ChunkPos pos, @Nullable SectionStorage.PackedChunk

    packedChunk) { - if (packedChunk == null) { - for (int sectionY = this.levelHeightAccessor.getMinSectionY(); sectionY <= this.levelHeightAccessor.getMaxSectionY(); sectionY++) { - this.storage.put(getKey(pos, sectionY), Optional.empty()); - } - } else { - boolean versionChanged = packedChunk.versionChanged(); - - for (int sectionY1 = this.levelHeightAccessor.getMinSectionY(); sectionY1 <= this.levelHeightAccessor.getMaxSectionY(); sectionY1++) { - long key = getKey(pos, sectionY1); - Optional optional = Optional.ofNullable(packedChunk.sectionsByY.get(sectionY1)) - .map(object -> this.unpacker.apply((P)object, () -> this.setDirty(key))); - this.storage.put(key, optional); - optional.ifPresent(object -> { - this.onSectionLoad(key); - if (versionChanged) { - this.setDirty(key); - } - }); - } - } + throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system } private void writeChunk(ChunkPos pos) { - RegistryOps registryOps = this.registryAccess.createSerializationContext(NbtOps.INSTANCE); - Dynamic dynamic = this.writeChunk(pos, registryOps); - Tag tag = dynamic.getValue(); - if (tag instanceof CompoundTag) { - this.simpleRegionStorage.write(pos, (CompoundTag)tag).exceptionally(throwable -> { - this.errorReporter.reportChunkSaveFailure(throwable, this.simpleRegionStorage.storageInfo(), pos); - return null; - }); - } else { - LOGGER.error("Expected compound tag, got {}", tag); - } + throw new IllegalStateException("Only chunk system can write state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system } private Dynamic writeChunk(ChunkPos pos, DynamicOps ops) { @@ -282,7 +245,7 @@ public class SectionStorage implements AutoCloseable { protected void onSectionLoad(long sectionKey) { } - protected void setDirty(long sectionPos) { + public void setDirty(long sectionPos) { // Paper - public Optional optional = this.storage.get(sectionPos); if (optional != null && !optional.isEmpty()) { this.dirtyChunks.add(ChunkPos.asLong(SectionPos.x(sectionPos), SectionPos.z(sectionPos))); @@ -303,7 +266,7 @@ public class SectionStorage implements AutoCloseable { @Override public void close() throws IOException { - this.simpleRegionStorage.close(); + this.moonrise$close(); // Paper - rewrite chunk system } record PackedChunk(Int2ObjectMap sectionsByY, boolean versionChanged) { diff --git a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java index cf6e2053d81f7b0f8c8e58b9c0fad3285ebc047d..70a9972252576e039ac126f6057a6ed66b80cdfc 100644 --- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java @@ -148,7 +148,7 @@ public record SerializableChunkData( UpgradeData upgradeData = tag.contains("UpgradeData", 10) ? new UpgradeData(tag.getCompound("UpgradeData"), levelHeightAccessor) : UpgradeData.EMPTY; - boolean _boolean = tag.getBoolean("isLightOn"); + boolean _boolean = chunkStatus.isOrAfter(ChunkStatus.LIGHT) && (tag.get("isLightOn") != null && tag.getInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_VERSION_TAG) == ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_LIGHT_VERSION); // Paper - starlight BlendingData.Packed packed; if (tag.contains("blending_data", 10)) { packed = BlendingData.Packed.CODEC.parse(NbtOps.INSTANCE, tag.getCompound("blending_data")).resultOrPartial(LOGGER::error).orElse(null); @@ -249,7 +249,17 @@ public record SerializableChunkData( DataLayer dataLayer = compound2.contains("BlockLight", 7) ? new DataLayer(compound2.getByteArray("BlockLight")) : null; DataLayer dataLayer1 = compound2.contains("SkyLight", 7) ? new DataLayer(compound2.getByteArray("SkyLight")) : null; - list8.add(new SerializableChunkData.SectionData(_byte, levelChunkSection, dataLayer, dataLayer1)); + // Paper start - starlight + SerializableChunkData.SectionData serializableChunkData = new SerializableChunkData.SectionData(_byte, levelChunkSection, dataLayer, dataLayer1); + if (sectionData.contains(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.BLOCKLIGHT_STATE_TAG, net.minecraft.nbt.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, net.minecraft.nbt.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)); + } + list8.add(serializableChunkData); + // Paper end - starlight } return new SerializableChunkData( @@ -276,6 +286,59 @@ public record SerializableChunkData( } } + // 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 level, PoiManager poiManager, RegionStorageInfo regionStorageInfo, ChunkPos pos) { if (!Objects.equals(pos, this.chunkPos)) { LOGGER.error("Chunk file at {} is in the wrong location; relocating. (Expected {}, got {})", pos, pos, this.chunkPos); @@ -294,7 +357,7 @@ public record SerializableChunkData( SectionPos sectionPos = SectionPos.of(pos, sectionData.y); if (sectionData.chunkSection != null) { levelChunkSections[level.getSectionIndexFromSectionY(sectionData.y)] = sectionData.chunkSection; - poiManager.checkConsistencyWithBlocks(sectionPos, sectionData.chunkSection); + //poiManager.checkConsistencyWithBlocks(sectionPos, sectionData.chunkSection); // Paper - rewrite chunk system } boolean flag1 = sectionData.blockLight != null; @@ -376,7 +439,7 @@ public record SerializableChunkData( } if (chunkType == ChunkType.LEVELCHUNK) { - return new ImposterProtoChunk((LevelChunk)chunkAccess, false); + return this.loadStarlightLightData(level, new ImposterProtoChunk((LevelChunk)chunkAccess, false)); // Paper - starlight } else { ProtoChunk protoChunk1 = (ProtoChunk)chunkAccess; @@ -399,7 +462,7 @@ public record SerializableChunkData( protoChunk1.setCarvingMask(new CarvingMask(this.carvingMask, chunkAccess.getMinY())); } - return protoChunk1; + return this.loadStarlightLightData(level, protoChunk1); // Paper - starlight } } @@ -427,22 +490,48 @@ public record SerializableChunkData( throw new IllegalArgumentException("Chunk can't be serialized: " + chunk); } else { ChunkPos pos = chunk.getPos(); - List list = new ArrayList<>(); + List list = new ArrayList<>(); final List sectionsList = list; // Paper - starlight - OBFHELPER LevelChunkSection[] sections = chunk.getSections(); LevelLightEngine lightEngine = level.getChunkSource().getLightEngine(); - for (int lightSection = lightEngine.getMinLightSection(); lightSection < lightEngine.getMaxLightSection(); lightSection++) { - int sectionIndexFromSectionY = chunk.getSectionIndexFromSectionY(lightSection); - boolean flag = sectionIndexFromSectionY >= 0 && sectionIndexFromSectionY < sections.length; - DataLayer dataLayerData = lightEngine.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(pos, lightSection)); - DataLayer dataLayerData1 = lightEngine.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(pos, lightSection)); - DataLayer dataLayer = dataLayerData != null && !dataLayerData.isEmpty() ? dataLayerData.copy() : null; - DataLayer dataLayer1 = dataLayerData1 != null && !dataLayerData1.isEmpty() ? dataLayerData1.copy() : null; - if (flag || dataLayer != null || dataLayer1 != null) { - LevelChunkSection levelChunkSection = flag ? sections[sectionIndexFromSectionY].copy() : null; - list.add(new SerializableChunkData.SectionData(lightSection, levelChunkSection, dataLayer, dataLayer1)); + // Paper start - starlight + final int minLightSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinLightSection(level); + final int maxLightSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxLightSection(level); + final int minBlockSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(level); + + 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; + + 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(); + + 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); } + + sectionsList.add(sectionData); } + // Paper end - starlight List list1 = new ArrayList<>(chunk.getBlockEntitiesPos().size()); @@ -540,7 +629,7 @@ public record SerializableChunkData( Codec>> codec = makeBiomeCodec(this.biomeRegistry); for (SerializableChunkData.SectionData sectionData : this.sectionData) { - CompoundTag compoundTag1 = new CompoundTag(); + CompoundTag compoundTag1 = new CompoundTag(); final CompoundTag sectionNBT = compoundTag1; // Paper - starlight - OBFHELPER LevelChunkSection levelChunkSection = sectionData.chunkSection; if (levelChunkSection != null) { compoundTag1.put("block_states", BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, levelChunkSection.getStates()).getOrThrow()); @@ -555,6 +644,19 @@ public record SerializableChunkData( compoundTag1.putByteArray("SkyLight", sectionData.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 (!compoundTag1.isEmpty()) { compoundTag1.putByte("Y", (byte)sectionData.y); listTag.add(compoundTag1); @@ -589,6 +691,14 @@ public record SerializableChunkData( compoundTag.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 + compoundTag.putBoolean("isLightOn", false); + // store our light version + compoundTag.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 compoundTag; } @@ -747,6 +857,66 @@ public record SerializableChunkData( } } - public 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 41ddaceb7485626b1f2ee258c2142eb3114c106e..f883c6400281788982403d0af3ee28613e9a29b1 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 342c83309b19c64d86e0dd97c1756c96be52772b..423779a2b690f387a4f0bd07b97b50e0baefda76 100644 --- a/net/minecraft/world/level/entity/EntityTickList.java +++ b/net/minecraft/world/level/entity/EntityTickList.java @@ -9,52 +9,38 @@ import javax.annotation.Nullable; import net.minecraft.world.entity.Entity; public class EntityTickList { - private Int2ObjectMap active = new Int2ObjectLinkedOpenHashMap<>(); - private Int2ObjectMap passive = new Int2ObjectLinkedOpenHashMap<>(); - @Nullable - private Int2ObjectMap iterated; + private final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet entities = new ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<>(); // Paper - rewrite chunk system private void ensureActiveIsNotIterated() { - if (this.iterated == this.active) { - this.passive.clear(); - - for (Entry entry : Int2ObjectMaps.fastIterable(this.active)) { - this.passive.put(entry.getIntKey(), entry.getValue()); - } - - Int2ObjectMap map = this.active; - this.active = this.passive; - this.passive = map; - } + // 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) { - if (this.iterated != null) { - throw new UnsupportedOperationException("Only one concurrent iteration supported"); - } else { - this.iterated = this.active; - - try { - for (Entity entity1 : this.active.values()) { - entity.accept(entity1); - } - } finally { - this.iterated = null; + // Paper start - rewrite chunk system + // To ensure nothing weird happens with dimension travelling, do not iterate over new entries... + // (by dfl iterator() is configured to not iterate over new entries) + final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet.Iterator iterator = this.entities.iterator(); + try { + while (iterator.hasNext()) { + entity.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 29d9f6e54421c539e9e55ab9f51b4c872da3fbb8..d77016287f5f9a0964d56f05d2d5256ef2e6e86c 100644 --- a/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java +++ b/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java @@ -78,7 +78,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator { return CompletableFuture.supplyAsync(() -> { this.doCreateBiomes(blender, randomState, structureManager, chunk); return chunk; - }, Util.backgroundExecutor().forName("init_biomes")); + }, Runnable::run); // Paper - rewrite chunk system } private void doCreateBiomes(Blender blender, RandomState random, StructureManager structureManager, ChunkAccess chunk) { @@ -318,7 +318,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator { } return var20; - }, Util.backgroundExecutor().forName("wgen_fill_noise")); + }, Runnable::run); // Paper - rewrite chunk system } private ChunkAccess doFill(Blender blender, StructureManager structureManager, RandomState random, ChunkAccess chunk, int minCellY, int cellCountY) { diff --git a/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/net/minecraft/world/level/levelgen/structure/StructureCheck.java index 06b54c0bec4031689d5c2da5cfea4ef28dbd16bc..f7dc4957b38878ddd3bfc7546be8a4e0af65c807 100644 --- a/net/minecraft/world/level/levelgen/structure/StructureCheck.java +++ b/net/minecraft/world/level/levelgen/structure/StructureCheck.java @@ -47,8 +47,13 @@ public class StructureCheck { private final BiomeSource biomeSource; private final long seed; private final DataFixer fixerUpper; - private final Long2ObjectMap> loadedChunks = new Long2ObjectOpenHashMap<>(); - private final Map featureChecks = new HashMap<>(); + // Paper start - rewrite chunk system + // make sure to purge entries from the maps to prevent memory leaks + private static final int CHUNK_TOTAL_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups + private static final int PER_FEATURE_CHECK_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups + private final ca.spottedleaf.moonrise.common.map.SynchronisedLong2ObjectMap> loadedChunksSafe = new ca.spottedleaf.moonrise.common.map.SynchronisedLong2ObjectMap<>(CHUNK_TOTAL_LIMIT); + private final java.util.concurrent.ConcurrentHashMap featureChecksSafe = new java.util.concurrent.ConcurrentHashMap<>(); + // Paper end - rewrite chunk system public StructureCheck( ChunkScanAccess storageAccess, @@ -90,7 +95,7 @@ public class StructureCheck { public StructureCheckResult checkStart(ChunkPos chunkPos, Structure structure, StructurePlacement placement, boolean skipKnownStructures) { long packedChunkPos = chunkPos.toLong(); - Object2IntMap map = this.loadedChunks.get(packedChunkPos); + Object2IntMap map = this.loadedChunksSafe.get(packedChunkPos); // Paper - rewrite chunk system if (map != null) { return this.checkStructureInfo(map, structure, skipKnownStructures); } else { @@ -100,9 +105,11 @@ public class StructureCheck { } else if (!placement.applyAdditionalChunkRestrictions(chunkPos.x, chunkPos.z, this.seed, this.getSaltOverride(structure))) { // Paper - add missing structure seed configs return StructureCheckResult.START_NOT_PRESENT; } else { - boolean flag = this.featureChecks - .computeIfAbsent(structure, structure1 -> new Long2BooleanOpenHashMap()) - .computeIfAbsent(packedChunkPos, l -> this.canCreateStructure(chunkPos, structure)); + // Paper start - rewrite chunk system + boolean flag = this.featureChecksSafe + .computeIfAbsent(structure, structure1 -> new ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap(PER_FEATURE_CHECK_LIMIT)) + .getOrCompute(packedChunkPos, l -> this.canCreateStructure(chunkPos, structure)); + // Paper end - rewrite chunk system return !flag ? StructureCheckResult.START_NOT_PRESENT : StructureCheckResult.CHUNK_LOAD_NEEDED; } } @@ -228,15 +235,25 @@ public class StructureCheck { } private void storeFullResults(long chunkPos, Object2IntMap structureChunks) { - this.loadedChunks.put(chunkPos, deduplicateEmptyMap(structureChunks)); - this.featureChecks.values().forEach(map -> map.remove(chunkPos)); + // Paper start - rewrite chunk system + this.loadedChunksSafe.put(chunkPos, deduplicateEmptyMap(structureChunks)); + // 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(chunkPos); + } + // Paper end - rewrite chunk system } public void incrementReference(ChunkPos pos, Structure structure) { - this.loadedChunks.compute(pos.toLong(), (_long, map) -> { - if (map == null || map.isEmpty()) { + this.loadedChunksSafe.compute(pos.toLong(), (_long, map) -> { // Paper start - rewrite chunk system + if (map == null) { map = new Object2IntOpenHashMap<>(); + } else { + map = map instanceof Object2IntOpenHashMap fastClone ? fastClone.clone() : new Object2IntOpenHashMap<>(map); } + // Paper end - rewrite chunk system map.computeInt(structure, (structure1, integer) -> integer == null ? 1 : integer + 1); return map; diff --git a/net/minecraft/world/level/lighting/LevelLightEngine.java b/net/minecraft/world/level/lighting/LevelLightEngine.java index ca23af013967b50420ebee178878ea79333de53b..d41b9266625ca6c5e32c5126f35a1f7733159cfc 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 lightChunkGetter, boolean blockLight, boolean skyLight) { this.levelHeightAccessor = lightChunkGetter.getLevel(); - this.blockEngine = blockLight ? new BlockLightEngine(lightChunkGetter) : null; - this.skyEngine = skyLight ? new SkyLightEngine(lightChunkGetter) : null; + // Paper start - rewrite chunk system + if (lightChunkGetter.getLevel() instanceof net.minecraft.world.level.Level) { + this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(lightChunkGetter, skyLight, blockLight, (LevelLightEngine)(Object)this); + } else { + this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(null, skyLight, blockLight, (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 isEmpty) { - if (this.blockEngine != null) { - this.blockEngine.updateSectionStatus(pos, isEmpty); - } - - if (this.skyEngine != null) { - this.skyEngine.updateSectionStatus(pos, isEmpty); - } + this.lightEngine.sectionChange(pos, isEmpty); // Paper - rewrite chunk system } @Override public void setLightEnabled(ChunkPos chunkPos, boolean lightEnabled) { - if (this.blockEngine != null) { - this.blockEngine.setLightEnabled(chunkPos, lightEnabled); - } - - if (this.skyEngine != null) { - this.skyEngine.setLightEnabled(chunkPos, lightEnabled); - } + // 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 type) { - if (type == LightLayer.BLOCK) { - return (LayerLightEventListener)(this.blockEngine == null ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : this.blockEngine); - } else { - return (LayerLightEventListener)(this.skyEngine == null ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : this.skyEngine); - } + return type == LightLayer.BLOCK ? this.lightEngine.getBlockReader() : this.lightEngine.getSkyReader(); // Paper - rewrite chunk system } public String getDebugData(LightLayer lightLayer, SectionPos sectionPos) { - if (lightLayer == LightLayer.BLOCK) { - if (this.blockEngine != null) { - return this.blockEngine.getDebugData(sectionPos.asLong()); - } - } else if (this.skyEngine != null) { - return this.skyEngine.getDebugData(sectionPos.asLong()); - } - - return "n/a"; + return "n/a"; // Paper - rewrite chunk system } public LayerLightSectionStorage.SectionType getDebugSectionType(LightLayer lightLayer, SectionPos sectionPos) { - if (lightLayer == LightLayer.BLOCK) { - if (this.blockEngine != null) { - return this.blockEngine.getDebugSectionType(sectionPos.asLong()); - } - } else if (this.skyEngine != null) { - return this.skyEngine.getDebugSectionType(sectionPos.asLong()); - } - - return LayerLightSectionStorage.SectionType.EMPTY; + throw new UnsupportedOperationException(); // Paper - rewrite chunk system } public void queueSectionData(LightLayer lightLayer, SectionPos sectionPos, @Nullable DataLayer dataLayer) { - if (lightLayer == LightLayer.BLOCK) { - if (this.blockEngine != null) { - this.blockEngine.queueSectionData(sectionPos.asLong(), dataLayer); - } - } else if (this.skyEngine != null) { - this.skyEngine.queueSectionData(sectionPos.asLong(), dataLayer); - } + // Paper - rewrite chunk system } public void retainData(ChunkPos pos, boolean retain) { - if (this.blockEngine != null) { - this.blockEngine.retainData(pos, retain); - } - - if (this.skyEngine != null) { - this.skyEngine.retainData(pos, retain); - } + // Paper - rewrite chunk system } public int getRawBrightness(BlockPos blockPos, int amount) { - int i = this.skyEngine == null ? 0 : this.skyEngine.getLightValue(blockPos) - amount; - int i1 = this.blockEngine == null ? 0 : this.blockEngine.getLightValue(blockPos); - return Math.max(i1, i); + return this.lightEngine.getRawBrightness(blockPos, amount); // Paper - rewrite chunk system } public boolean lightOnInColumn(long columnPos) { - return this.blockEngine == null - || this.blockEngine.storage.lightOnInColumn(columnPos) && (this.skyEngine == null || this.skyEngine.storage.lightOnInColumn(columnPos)); + 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 130ef38a50f1df1faa26b433b0c605a4507f71af..f6daca279788c3d983a9ee213df85d5d93fc6eed 100644 --- a/net/minecraft/world/level/material/FlowingFluid.java +++ b/net/minecraft/world/level/material/FlowingFluid.java @@ -45,6 +45,48 @@ public abstract class FlowingFluid extends Fluid { }); private final Map shapes = Maps.newIdentityHashMap(); + // Paper start - fluid method optimisations + private FluidState sourceFalling; + private FluidState sourceNotFalling; + + private static final int TOTAL_FLOWING_STATES = FALLING.getPossibleValues().size() * LEVEL.getPossibleValues().size(); + private static final int MIN_LEVEL = LEVEL.getPossibleValues().stream().sorted().findFirst().get().intValue(); + + // index = (falling ? 1 : 0) + level*2 + private FluidState[] flowingLookUp; + private volatile boolean init; + + private static final int COLLISION_OCCLUSION_CACHE_SIZE = 2048; + private static final ThreadLocal COLLISION_OCCLUSION_CACHE = ThreadLocal.withInitial(() -> new ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey[COLLISION_OCCLUSION_CACHE_SIZE]); + + + /** + * Due to init order, we need to use callbacks to initialise our state + */ + private void init() { + synchronized (this) { + if (this.init) { + return; + } + this.flowingLookUp = new FluidState[TOTAL_FLOWING_STATES]; + final FluidState defaultFlowState = this.getFlowing().defaultFluidState(); + for (int i = 0; i < TOTAL_FLOWING_STATES; ++i) { + final int falling = i & 1; + final int level = (i >>> 1) + MIN_LEVEL; + + this.flowingLookUp[i] = defaultFlowState.setValue(FALLING, falling == 1 ? Boolean.TRUE : Boolean.FALSE) + .setValue(LEVEL, Integer.valueOf(level)); + } + + final FluidState defaultFallState = this.getSource().defaultFluidState(); + this.sourceFalling = defaultFallState.setValue(FALLING, Boolean.TRUE); + this.sourceNotFalling = defaultFallState.setValue(FALLING, Boolean.FALSE); + + this.init = true; + } + } + // Paper end - fluid method optimisations + @Override protected void createFluidStateDefinition(StateDefinition.Builder builder) { builder.add(FALLING); @@ -209,61 +251,71 @@ public abstract class FlowingFluid extends Fluid { } } - private static boolean canPassThroughWall( - Direction direction, BlockGetter level, BlockPos pos, BlockState state, BlockPos spreadPos, BlockState spreadState - ) { - VoxelShape collisionShape = spreadState.getCollisionShape(level, spreadPos); - if (collisionShape == Shapes.block()) { - return false; - } else { - VoxelShape collisionShape1 = state.getCollisionShape(level, pos); - if (collisionShape1 == Shapes.block()) { - return false; - } else if (collisionShape1 == Shapes.empty() && collisionShape == Shapes.empty()) { - return true; - } else { - Object2ByteLinkedOpenHashMap map; - if (!state.getBlock().hasDynamicShape() && !spreadState.getBlock().hasDynamicShape()) { - map = OCCLUSION_CACHE.get(); - } else { - map = null; - } + // 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; + } - FlowingFluid.BlockStatePairKey blockStatePairKey; - if (map != null) { - blockStatePairKey = new FlowingFluid.BlockStatePairKey(state, spreadState, direction); - byte andMoveToFirst = map.getAndMoveToFirst(blockStatePairKey); - if (andMoveToFirst != 127) { - return andMoveToFirst != 0; - } - } else { - blockStatePairKey = null; - } + if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$occludesFullBlock() | ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$occludesFullBlock()) { + // don't even try to cache simple cases + return false; + } - boolean flag = !Shapes.mergedFaceOccludes(collisionShape1, collisionShape, direction); - if (map != null) { - if (map.size() == 200) { - map.removeLastByte(); - } + 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; - map.putAndMoveToFirst(blockStatePairKey, (byte)(flag ? 1 : 0)); - } + 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); - return flag; + 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(); } } + + final VoxelShape shape1 = fromState.getCollisionShape(level, fromPos); + final VoxelShape shape2 = toState.getCollisionShape(level, toPos); + + final boolean result = !Shapes.mergedFaceOccludes(shape1, shape2, direction); + + 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 this.getFlowing().defaultFluidState().setValue(LEVEL, Integer.valueOf(level)).setValue(FALLING, Boolean.valueOf(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 this.getSource().defaultFluidState().setValue(FALLING, Boolean.valueOf(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 level); diff --git a/net/minecraft/world/level/material/FluidState.java b/net/minecraft/world/level/material/FluidState.java index d2d71b22666639c003d86a6b6403fcbd2912c5af..481cb46973acb9785fdee5732e98aac560c6ec08 100644 --- a/net/minecraft/world/level/material/FluidState.java +++ b/net/minecraft/world/level/material/FluidState.java @@ -22,12 +22,30 @@ import net.minecraft.world.level.block.state.properties.Property; import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.shapes.VoxelShape; -public final class FluidState extends StateHolder { +public final class FluidState extends StateHolder implements ca.spottedleaf.moonrise.patches.fluid.FluidFluidState { // Paper - fluid method optimisations public static final Codec CODEC = codec(BuiltInRegistries.FLUID.byNameCodec(), Fluid::defaultFluidState).stable(); public static final int AMOUNT_MAX = 9; public static final int AMOUNT_FULL = 8; protected final boolean isEmpty; // Paper - Perf: moved from isEmpty() + // Paper start - fluid method optimisations + private int amount; + //private boolean isEmpty; + private boolean isSource; + private float ownHeight; + private boolean isRandomlyTicking; + private BlockState legacyBlock; + + @Override + public final void moonrise$initCaches() { + this.amount = this.getType().getAmount((FluidState)(Object)this); + //this.isEmpty = this.getType().isEmpty(); + this.isSource = this.getType().isSource((FluidState)(Object)this); + this.ownHeight = this.getType().getOwnHeight((FluidState)(Object)this); + this.isRandomlyTicking = this.getType().isRandomlyTicking(); + } + // Paper end - fluid method optimisations + public FluidState(Fluid owner, Reference2ObjectArrayMap, Comparable> values, MapCodec propertiesCodec) { super(owner, values, propertiesCodec); this.isEmpty = owner.isEmpty(); // Paper - Perf: moved from isEmpty() @@ -38,11 +56,11 @@ public final class FluidState extends StateHolder { } public boolean isSource() { - return this.getType().isSource(this); + return this.isSource; // Paper - fluid method optimisations } public boolean isSourceOfType(Fluid fluid) { - return this.owner == fluid && this.owner.isSource(this); + return this.isSource && this.owner == fluid; // Paper - fluid method optimisations } public boolean isEmpty() { @@ -54,11 +72,11 @@ public final class FluidState extends StateHolder { } public float getOwnHeight() { - return this.getType().getOwnHeight(this); + return this.ownHeight; // Paper - fluid method optimisations } public int getAmount() { - return this.getType().getAmount(this); + return this.amount; // Paper - fluid method optimisations } public boolean shouldRenderBackwardUpFace(BlockGetter level, BlockPos pos) { @@ -84,7 +102,7 @@ public final class FluidState extends StateHolder { } public boolean isRandomlyTicking() { - return this.getType().isRandomlyTicking(); + return this.isRandomlyTicking; // Paper - fluid method optimisations } public void randomTick(ServerLevel level, BlockPos pos, RandomSource random) { @@ -96,7 +114,12 @@ public final class FluidState extends StateHolder { } public BlockState createLegacyBlock() { - return this.getType().createLegacyBlock(this); + // Paper start - fluid method optimisations + if (this.legacyBlock != null) { + return this.legacyBlock; + } + return this.legacyBlock = this.getType().createLegacyBlock((FluidState)(Object)this); + // Paper end - fluid method optimisations } @Nullable diff --git a/net/minecraft/world/phys/AABB.java b/net/minecraft/world/phys/AABB.java index 047e1fd078d7f49a2547daeca9eec31306d25dd0..85148858db1fd5e9da8bbdde4b0d84110d80e373 100644 --- a/net/minecraft/world/phys/AABB.java +++ b/net/minecraft/world/phys/AABB.java @@ -314,7 +314,7 @@ public class AABB { } @Nullable - private static Direction getDirection(AABB aabb, Vec3 start, double[] minDistance, @Nullable Direction facing, double deltaX, double deltaY, double deltaZ) { + public static Direction getDirection(AABB aabb, Vec3 start, double[] minDistance, @Nullable Direction facing, double deltaX, double deltaY, double deltaZ) { // Paper - optimise collisions - public return getDirection(aabb.minX, aabb.minY, aabb.minZ, aabb.maxX, aabb.maxY, aabb.maxZ, start, minDistance, facing, deltaX, deltaY, deltaZ); } diff --git a/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/net/minecraft/world/phys/shapes/ArrayVoxelShape.java index adb5f1be35d3a712499076719a1bb819ef52b9a8..39a634e1392239e17818a11750ba869ea7d195ce 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 xs, DoubleList ys, DoubleList zs) { + public ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xs, DoubleList ys, DoubleList zs) { // Paper - optimise collisions - public super(shape); int i = shape.getXSize() + 1; int i1 = 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 14a12bdaa428556fa7b0c43e37b79699ae2fcb92..3a56e4ad9b3cba0cdf4bc373f7d0457d8643fdc4 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 xSize, int ySize, int zSize) { super(xSize, ySize, zSize); @@ -150,47 +150,109 @@ public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape { return bitSetDiscreteVoxelShape; } - protected static void forAllBoxes(DiscreteVoxelShape shape, DiscreteVoxelShape.IntLineConsumer consumer, boolean combine) { - BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = new BitSetDiscreteVoxelShape(shape); + // 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 i1 = 0; i1 < bitSetDiscreteVoxelShape.xSize; i1++) { - int i2 = -1; + // only clone when we may write to it + bitset = ca.spottedleaf.moonrise.common.util.MixinWorkarounds.clone(bitset); - for (int i3 = 0; i3 <= bitSetDiscreteVoxelShape.zSize; i3++) { - if (bitSetDiscreteVoxelShape.isFullWide(i1, i, i3)) { - if (combine) { - if (i2 == -1) { - i2 = i3; - } - } else { - consumer.consume(i1, i, i3, i1 + 1, i + 1, i3 + 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 (i2 != -1) { - int i4 = i1; - int i5 = i; - bitSetDiscreteVoxelShape.clearZStrip(i2, i3, i1, i); - - while (bitSetDiscreteVoxelShape.isZStripFull(i2, i3, i4 + 1, i)) { - bitSetDiscreteVoxelShape.clearZStrip(i2, i3, i4 + 1, i); - i4++; + + 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(i1, i4 + 1, i2, i3, i5 + 1)) { - for (int i6 = i1; i6 <= i4; i6++) { - bitSetDiscreteVoxelShape.clearZStrip(i2, i3, i6, i5 + 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; + } } - i5++; + ++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); + } } - consumer.consume(i1, i, i2, i4 + 1, i5 + 1, i3); - i2 = -1; + consumer.consume(x, y, firstSetZ - indexX, endX, endY, lastSetZ - indexX); + zIdx = lastSetZ; } } } } } + // Paper end - optimise collisions private boolean isZStripFull(int zMin, int zMax, int x, int y) { return x < this.xSize && y < this.ySize && this.storage.nextClearBit(this.getIndex(x, y, zMin)) >= this.getIndex(x, y, zMax); diff --git a/net/minecraft/world/phys/shapes/CubeVoxelShape.java b/net/minecraft/world/phys/shapes/CubeVoxelShape.java index f6b6481591e009de80f6b6318d35f193aabb7df3..e9b5069dcd572966b2f5aa220cef30e7a328fa2c 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 shape) { super(shape); + ((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 4fc61b329ccb7c9aeb6105dc53d71545a3baea89..309a34f192f7737204ce7a5c3b4004bdd83842f2 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 xSize, int ySize, int zSize) { if (xSize >= 0 && ySize >= 0 && zSize >= 0) { this.xSize = xSize; diff --git a/net/minecraft/world/phys/shapes/OffsetDoubleList.java b/net/minecraft/world/phys/shapes/OffsetDoubleList.java index ac1488875537421b74f0c491c9b7a40e75539c92..9eb27eb8d6dcaad6ce02f8ce4546acc224c4196f 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 delegate, double offset) { this.delegate = delegate; diff --git a/net/minecraft/world/phys/shapes/Shapes.java b/net/minecraft/world/phys/shapes/Shapes.java index e759221fb54aa510d2d8bbba47e1d794367aec6d..5665cfaae4fc9e72b77fd41e16e7f64460b099b0 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 i1 = findBits(minY, maxY); - int i2 = findBits(minZ, maxZ); - if (i < 0 || i1 < 0 || i2 < 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 && i1 == 0 && i2 == 0) { - return block(); - } else { - int i3 = 1 << i; - int i4 = 1 << i1; - int i5 = 1 << i2; - BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.withFilledBounds( - i3, - i4, - i5, - (int)Math.round(minX * i3), - (int)Math.round(minY * i4), - (int)Math.round(minZ * i5), - (int)Math.round(maxX * i3), - (int)Math.round(maxY * i4), - (int)Math.round(maxZ * i5) - ); - return new CubeVoxelShape(bitSetDiscreteVoxelShape); } } else { - return empty(); + return EMPTY; } + // Paper end - optimise collisions } public static VoxelShape create(AABB aabb) { @@ -120,85 +153,52 @@ public final class Shapes { } public static VoxelShape or(VoxelShape shape1, VoxelShape... others) { - return Arrays.stream(others).reduce(shape1, Shapes::or); + int size = others.length; + if (size == 0) { + return shape1; + } + + // 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] = shape1; + + 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 shape1, VoxelShape shape2, BooleanOp function) { - return joinUnoptimized(shape1, shape2, function).optimize(); + return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.joinOptimized(shape1, shape2, function); // Paper - optimise collisions } public static VoxelShape joinUnoptimized(VoxelShape shape1, VoxelShape shape2, BooleanOp function) { - if (function.apply(false, false)) { - throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException()); - } else if (shape1 == shape2) { - return function.apply(true, true) ? shape1 : empty(); - } else { - boolean flag = function.apply(true, false); - boolean flag1 = function.apply(false, true); - if (shape1.isEmpty()) { - return flag1 ? shape2 : empty(); - } else if (shape2.isEmpty()) { - return flag ? shape1 : empty(); - } else { - IndexMerger indexMerger = createIndexMerger(1, shape1.getCoords(Direction.Axis.X), shape2.getCoords(Direction.Axis.X), flag, flag1); - IndexMerger indexMerger1 = createIndexMerger( - indexMerger.size() - 1, shape1.getCoords(Direction.Axis.Y), shape2.getCoords(Direction.Axis.Y), flag, flag1 - ); - IndexMerger indexMerger2 = createIndexMerger( - (indexMerger.size() - 1) * (indexMerger1.size() - 1), shape1.getCoords(Direction.Axis.Z), shape2.getCoords(Direction.Axis.Z), flag, flag1 - ); - BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.join( - shape1.shape, shape2.shape, indexMerger, indexMerger1, indexMerger2, function - ); - return (VoxelShape)(indexMerger instanceof DiscreteCubeMerger - && indexMerger1 instanceof DiscreteCubeMerger - && indexMerger2 instanceof DiscreteCubeMerger - ? new CubeVoxelShape(bitSetDiscreteVoxelShape) - : new ArrayVoxelShape(bitSetDiscreteVoxelShape, indexMerger.getList(), indexMerger1.getList(), indexMerger2.getList())); - } - } + return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.joinUnoptimized(shape1, shape2, function); // Paper - optimise collisions } public static boolean joinIsNotEmpty(VoxelShape shape1, VoxelShape shape2, BooleanOp resultOperator) { - if (resultOperator.apply(false, false)) { - throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException()); - } else { - boolean isEmpty = shape1.isEmpty(); - boolean isEmpty1 = shape2.isEmpty(); - if (!isEmpty && !isEmpty1) { - if (shape1 == shape2) { - return resultOperator.apply(true, true); - } else { - boolean flag = resultOperator.apply(true, false); - boolean flag1 = resultOperator.apply(false, true); - - for (Direction.Axis axis : AxisCycle.AXIS_VALUES) { - if (shape1.max(axis) < shape2.min(axis) - 1.0E-7) { - return flag || flag1; - } - - if (shape2.max(axis) < shape1.min(axis) - 1.0E-7) { - return flag || flag1; - } - } - - IndexMerger indexMerger = createIndexMerger(1, shape1.getCoords(Direction.Axis.X), shape2.getCoords(Direction.Axis.X), flag, flag1); - IndexMerger indexMerger1 = createIndexMerger( - indexMerger.size() - 1, shape1.getCoords(Direction.Axis.Y), shape2.getCoords(Direction.Axis.Y), flag, flag1 - ); - IndexMerger indexMerger2 = createIndexMerger( - (indexMerger.size() - 1) * (indexMerger1.size() - 1), - shape1.getCoords(Direction.Axis.Z), - shape2.getCoords(Direction.Axis.Z), - flag, - flag1 - ); - return joinIsNotEmpty(indexMerger, indexMerger1, indexMerger2, shape1.shape, shape2.shape, resultOperator); - } - } else { - return resultOperator.apply(!isEmpty, !isEmpty1); - } - } + return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isJoinNonEmpty(shape1, shape2, resultOperator); // Paper - optimise collisions } private static boolean joinIsNotEmpty( @@ -230,52 +230,116 @@ public final class Shapes { return desiredOffset; } - public static boolean blockOccudes(VoxelShape shape, VoxelShape adjacentShape, Direction side) { - if (shape == block() && adjacentShape == 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 (adjacentShape.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 = side.getAxis(); - Direction.AxisDirection axisDirection = side.getAxisDirection(); - VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? shape : adjacentShape; - VoxelShape voxelShape1 = axisDirection == Direction.AxisDirection.POSITIVE ? adjacentShape : 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(voxelShape1.min(axis), 0.0, 1.0E-7) - && !joinIsNotEmpty(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape1, 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 shape, VoxelShape adjacentShape, Direction side) { - if (shape != block() && adjacentShape != block()) { - Direction.Axis axis = side.getAxis(); - Direction.AxisDirection axisDirection = side.getAxisDirection(); - VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? shape : adjacentShape; - VoxelShape voxelShape1 = axisDirection == Direction.AxisDirection.POSITIVE ? adjacentShape : shape; - 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(voxelShape1.min(axis), 0.0, 1.0E-7)) { - voxelShape1 = 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(voxelShape1, 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 voxelShape1, VoxelShape voxelShape2) { - return voxelShape1 == block() - || voxelShape2 == block() - || (!voxelShape1.isEmpty() || !voxelShape2.isEmpty()) - && !joinIsNotEmpty(block(), joinUnoptimized(voxelShape1, voxelShape2, 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 79f7f04207891dd98cc0b2d93ecb2e07c8baa7b6..7ca12213c10f962ff597a8d51413a17b1827bbb4 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(delegate.shape, axis, index)); this.delegate = delegate; this.axis = axis; + ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this).moonrise$initCache(); // Paper - optimise collisions } private static DiscreteVoxelShape makeSlice(DiscreteVoxelShape shape, Direction.Axis axis, int index) { diff --git a/net/minecraft/world/phys/shapes/VoxelShape.java b/net/minecraft/world/phys/shapes/VoxelShape.java index 006065c32baf3b1ddc5647196cb9f863c7969064..2c7e70675b62cb753447d2acebf2f36cdac74973 100644 --- a/net/minecraft/world/phys/shapes/VoxelShape.java +++ b/net/minecraft/world/phys/shapes/VoxelShape.java @@ -15,61 +15,546 @@ import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.Vec3; -public abstract class VoxelShape { - protected final DiscreteVoxelShape shape; +public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape { // Paper - optimise collisions + public final DiscreteVoxelShape shape; // Paper - optimise collisions - public @Nullable private VoxelShape[] faces; + // Paper start - optimise collisions + private double offsetX; + private double offsetY; + private double offsetZ; + private AABB singleAABBRepresentation; + private double[] rootCoordinatesX; + private double[] rootCoordinatesY; + private double[] rootCoordinatesZ; + private ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cachedShapeData; + private boolean isEmpty; + private ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cachedToAABBs; + private AABB cachedBounds; + private Boolean isFullBlock; + private Boolean occludesFullBlock; + + // must be power of two + private static final int MERGED_CACHE_SIZE = 16; + private ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache[] mergedORCache; + + @Override + public final double moonrise$offsetX() { + return this.offsetX; + } + + @Override + public final double moonrise$offsetY() { + return this.offsetY; + } + + @Override + public final double moonrise$offsetZ() { + return this.offsetZ; + } + + @Override + public final AABB moonrise$getSingleAABBRepresentation() { + return this.singleAABBRepresentation; + } + + @Override + public final double[] moonrise$rootCoordinatesX() { + return this.rootCoordinatesX; + } + + @Override + public final double[] moonrise$rootCoordinatesY() { + return this.rootCoordinatesY; + } + + @Override + public final double[] moonrise$rootCoordinatesZ() { + return this.rootCoordinatesZ; + } + + private static double[] extractRawArray(final DoubleList list) { + if (list instanceof it.unimi.dsi.fastutil.doubles.DoubleArrayList rawList) { + final double[] raw = rawList.elements(); + final int expected = rawList.size(); + if (raw.length == expected) { + return raw; + } else { + return java.util.Arrays.copyOf(raw, expected); + } + } else { + return list.toDoubleArray(); + } + } + + @Override + public final void moonrise$initCache() { + this.cachedShapeData = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape)this.shape).moonrise$getOrCreateCachedShapeData(); + this.isEmpty = this.cachedShapeData.isEmpty(); + + final DoubleList xList = this.getCoords(Direction.Axis.X); + final DoubleList yList = this.getCoords(Direction.Axis.Y); + final DoubleList zList = this.getCoords(Direction.Axis.Z); + + if (xList instanceof OffsetDoubleList offsetDoubleList) { + this.offsetX = offsetDoubleList.offset; + this.rootCoordinatesX = extractRawArray(offsetDoubleList.delegate); + } else { + this.rootCoordinatesX = extractRawArray(xList); + } + + if (yList instanceof OffsetDoubleList offsetDoubleList) { + this.offsetY = offsetDoubleList.offset; + this.rootCoordinatesY = extractRawArray(offsetDoubleList.delegate); + } else { + this.rootCoordinatesY = extractRawArray(yList); + } + + if (zList instanceof OffsetDoubleList offsetDoubleList) { + this.offsetZ = offsetDoubleList.offset; + this.rootCoordinatesZ = extractRawArray(offsetDoubleList.delegate); + } else { + this.rootCoordinatesZ = extractRawArray(zList); + } + + if (this.cachedShapeData.hasSingleAABB()) { + this.singleAABBRepresentation = new AABB( + this.rootCoordinatesX[0] + this.offsetX, this.rootCoordinatesY[0] + this.offsetY, this.rootCoordinatesZ[0] + this.offsetZ, + this.rootCoordinatesX[1] + this.offsetX, this.rootCoordinatesY[1] + this.offsetY, this.rootCoordinatesZ[1] + this.offsetZ + ); + this.cachedBounds = this.singleAABBRepresentation; + } + } + + @Override + public final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getCachedVoxelData() { + return this.cachedShapeData; + } + + private VoxelShape[] faceShapeClampedCache; + + @Override + public final VoxelShape moonrise$getFaceShapeClamped(final Direction direction) { + if (this.isEmpty) { + return (VoxelShape)(Object)this; + } + if ((VoxelShape)(Object)this == Shapes.block()) { + return (VoxelShape)(Object)this; + } + + VoxelShape[] cache = this.faceShapeClampedCache; + if (cache != null) { + final VoxelShape ret = cache[direction.ordinal()]; + if (ret != null) { + return ret; + } + } + + + if (cache == null) { + this.faceShapeClampedCache = cache = new VoxelShape[6]; + } + + final Direction.Axis axis = direction.getAxis(); + + final VoxelShape ret; + + if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) { + if (DoubleMath.fuzzyEquals(this.max(axis), 1.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) { + ret = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.sliceShape((VoxelShape)(Object)this, axis, this.shape.getSize(axis) - 1); + } else { + ret = Shapes.empty(); + } + } else { + if (DoubleMath.fuzzyEquals(this.min(axis), 0.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) { + ret = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.sliceShape((VoxelShape)(Object)this, axis, 0); + } else { + ret = Shapes.empty(); + } + } + + cache[direction.ordinal()] = ret; + + return ret; + } + + private boolean computeOccludesFullBlock() { + if (this.isEmpty) { + this.occludesFullBlock = Boolean.FALSE; + return false; + } + + if (this.moonrise$isFullBlock()) { + this.occludesFullBlock = Boolean.TRUE; + return true; + } + + final AABB singleAABB = this.singleAABBRepresentation; + if (singleAABB != null) { + // check if the bounding box encloses the full cube + final boolean ret = + (singleAABB.minY <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && singleAABB.maxY >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) && + (singleAABB.minX <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && singleAABB.maxX >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) && + (singleAABB.minZ <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && singleAABB.maxZ >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)); + this.occludesFullBlock = Boolean.valueOf(ret); + return ret; + } + + final boolean ret = !Shapes.joinIsNotEmpty(Shapes.block(), ((VoxelShape)(Object)this), BooleanOp.ONLY_FIRST); + this.occludesFullBlock = Boolean.valueOf(ret); + return ret; + } + + @Override + public final boolean moonrise$occludesFullBlock() { + final Boolean ret = this.occludesFullBlock; + if (ret != null) { + return ret.booleanValue(); + } + + return this.computeOccludesFullBlock(); + } + + @Override + public final boolean moonrise$occludesFullBlockIfCached() { + final Boolean ret = this.occludesFullBlock; + return ret != null ? ret.booleanValue() : false; + } + + private static int hash(final VoxelShape key) { + return it.unimi.dsi.fastutil.HashCommon.mix(System.identityHashCode(key)); + } + + @Override + public final VoxelShape moonrise$orUnoptimized(final VoxelShape other) { + // don't cache simple cases + if (((VoxelShape)(Object)this) == other) { + return other; + } + + if (this.isEmpty) { + return other; + } + + if (other.isEmpty()) { + return (VoxelShape)(Object)this; + } + + // try this cache first + final int thisCacheKey = hash(other) & (MERGED_CACHE_SIZE - 1); + final ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache cached = this.mergedORCache == null ? null : this.mergedORCache[thisCacheKey]; + if (cached != null && cached.key() == other) { + return cached.result(); + } + + // try other cache + final int otherCacheKey = hash((VoxelShape)(Object)this) & (MERGED_CACHE_SIZE - 1); + final ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache otherCache = ((VoxelShape)(Object)other).mergedORCache == null ? null : ((VoxelShape)(Object)other).mergedORCache[otherCacheKey]; + if (otherCache != null && otherCache.key() == (VoxelShape)(Object)this) { + return otherCache.result(); + } + + // note: unsure if joinUnoptimized(1, 2, OR) == joinUnoptimized(2, 1, OR) for all cases + final VoxelShape result = Shapes.joinUnoptimized((VoxelShape)(Object)this, other, BooleanOp.OR); + + if (cached != null && otherCache == null) { + // try to use second cache instead of replacing an entry in this cache + if (((VoxelShape)(Object)other).mergedORCache == null) { + ((VoxelShape)(Object)other).mergedORCache = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache[MERGED_CACHE_SIZE]; + } + ((VoxelShape)(Object)other).mergedORCache[otherCacheKey] = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache((VoxelShape)(Object)this, result); + } else { + // line is not occupied or other cache line is full + // always bias to replace this cache, as this cache is the first we check + if (this.mergedORCache == null) { + this.mergedORCache = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache[MERGED_CACHE_SIZE]; + } + this.mergedORCache[thisCacheKey] = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache(other, result); + } + + return result; + } + + private static DoubleList offsetList(final double[] src, final double by) { + final it.unimi.dsi.fastutil.doubles.DoubleArrayList wrap = it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(src); + if (by == 0.0) { + return wrap; + } + return new OffsetDoubleList(wrap, by); + } + + private List toAabbsUncached() { + final List ret; + if (this.singleAABBRepresentation != null) { + ret = new java.util.ArrayList<>(1); + ret.add(this.singleAABBRepresentation); + } else { + ret = new java.util.ArrayList<>(); + final double[] coordsX = this.rootCoordinatesX; + final double[] coordsY = this.rootCoordinatesY; + final double[] coordsZ = this.rootCoordinatesZ; + + final double offX = this.offsetX; + final double offY = this.offsetY; + final double offZ = this.offsetZ; + + this.shape.forAllBoxes((final int minX, final int minY, final int minZ, + final int maxX, final int maxY, final int maxZ) -> { + ret.add(new AABB( + coordsX[minX] + offX, + coordsY[minY] + offY, + coordsZ[minZ] + offZ, + + + coordsX[maxX] + offX, + coordsY[maxY] + offY, + coordsZ[maxZ] + offZ + )); + }, true); + } + + // cache result + this.cachedToAABBs = new ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs(ret, false, 0.0, 0.0, 0.0); + + return ret; + } + + private boolean computeFullBlock() { + Boolean ret; + if (this.isEmpty) { + ret = Boolean.FALSE; + } else if ((VoxelShape)(Object)this == Shapes.block()) { + ret = Boolean.TRUE; + } else { + final AABB singleAABB = this.singleAABBRepresentation; + if (singleAABB == null) { + final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeData = this.cachedShapeData; + final int sMinX = shapeData.minFullX(); + final int sMinY = shapeData.minFullY(); + final int sMinZ = shapeData.minFullZ(); + + final int sMaxX = shapeData.maxFullX(); + final int sMaxY = shapeData.maxFullY(); + final int sMaxZ = shapeData.maxFullZ(); + + if (Math.abs(this.rootCoordinatesX[sMinX] + this.offsetX) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && + Math.abs(this.rootCoordinatesY[sMinY] + this.offsetY) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && + Math.abs(this.rootCoordinatesZ[sMinZ] + this.offsetZ) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && + + Math.abs(1.0 - (this.rootCoordinatesX[sMaxX] + this.offsetX)) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && + Math.abs(1.0 - (this.rootCoordinatesY[sMaxY] + this.offsetY)) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && + Math.abs(1.0 - (this.rootCoordinatesZ[sMaxZ] + this.offsetZ)) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) { + + // index = z + y*sizeZ + x*(sizeZ*sizeY) + + final int sizeY = shapeData.sizeY(); + final int sizeZ = shapeData.sizeZ(); + + final long[] bitset = shapeData.voxelSet(); + + ret = Boolean.TRUE; + + check_full: + for (int x = sMinX; x < sMaxX; ++x) { + for (int y = sMinY; y < sMaxY; ++y) { + final int baseIndex = y*sizeZ + x*(sizeZ*sizeY); + if (!ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.isRangeSet(bitset, baseIndex + sMinZ, baseIndex + sMaxZ)) { + ret = Boolean.FALSE; + break check_full; + } + } + } + } else { + ret = Boolean.FALSE; + } + } else { + ret = Boolean.valueOf( + Math.abs(singleAABB.minX) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && + Math.abs(singleAABB.minY) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && + Math.abs(singleAABB.minZ) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && + + Math.abs(1.0 - singleAABB.maxX) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && + Math.abs(1.0 - singleAABB.maxY) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && + Math.abs(1.0 - singleAABB.maxZ) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON + ); + } + } + + this.isFullBlock = ret; + + return ret.booleanValue(); + } + + @Override + public final boolean moonrise$isFullBlock() { + final Boolean ret = this.isFullBlock; + + if (ret != null) { + return ret.booleanValue(); + } + + return this.computeFullBlock(); + } + + private static BlockHitResult clip(final AABB aabb, final Vec3 from, final Vec3 to, final BlockPos offset) { + final double[] minDistanceArr = new double[] { 1.0 }; + final double diffX = to.x - from.x; + final double diffY = to.y - from.y; + final double diffZ = to.z - from.z; + + final Direction direction = AABB.getDirection(aabb.move(offset), from, minDistanceArr, null, diffX, diffY, diffZ); + + if (direction == null) { + return null; + } + + final double minDistance = minDistanceArr[0]; + return new BlockHitResult(from.add(minDistance * diffX, minDistance * diffY, minDistance * diffZ), direction, offset, false); + } + + private VoxelShape calculateFaceDirect(final Direction direction, final Direction.Axis axis, final double[] coords, final double offset) { + if (coords.length == 2 && + DoubleMath.fuzzyEquals(coords[0] + offset, 0.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) && + DoubleMath.fuzzyEquals(coords[1] + offset, 1.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) { + return (VoxelShape)(Object)this; + } + + final boolean positiveDir = direction.getAxisDirection() == Direction.AxisDirection.POSITIVE; + + // see findIndex + final int index = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor( + coords, offset, (positiveDir ? (1.0 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) : (0.0 + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)), + 0, coords.length - 1 + ); + + return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.sliceShape( + (VoxelShape)(Object)this, axis, index + ); + } + // Paper end - optimise collisions + protected VoxelShape(DiscreteVoxelShape shape) { this.shape = shape; } 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 offset) { @@ -77,20 +562,96 @@ public abstract class VoxelShape { } public VoxelShape move(double xOffset, double yOffset, double zOffset) { - return (VoxelShape)(this.isEmpty() - ? Shapes.empty() - : new ArrayVoxelShape( - this.shape, - new OffsetDoubleList(this.getCoords(Direction.Axis.X), xOffset), - new OffsetDoubleList(this.getCoords(Direction.Axis.Y), yOffset), - new OffsetDoubleList(this.getCoords(Direction.Axis.Z), zOffset) - )); + // Paper start - optimise collisions + if (this.isEmpty) { + return Shapes.empty(); + } + + final ArrayVoxelShape ret = new ArrayVoxelShape( + this.shape, + offsetList(this.rootCoordinatesX, this.offsetX + xOffset), + offsetList(this.rootCoordinatesY, this.offsetY + yOffset), + offsetList(this.rootCoordinatesZ, this.offsetZ + zOffset) + ); + + 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, xOffset, yOffset, zOffset); + } + + return ret; + // Paper end - optimise collisions } public VoxelShape optimize() { - VoxelShape[] voxelShapes = new VoxelShape[]{Shapes.empty()}; - this.forAllBoxes((x1, y1, z1, x2, y2, z2) -> voxelShapes[0] = Shapes.joinUnoptimized(voxelShapes[0], Shapes.box(x1, y1, z1, x2, y2, z2), BooleanOp.OR)); - return voxelShapes[0]; + // Paper start - optimise collisions + if (this.isEmpty) { + return Shapes.empty(); + } + + if (this.singleAABBRepresentation != null) { + // note: the isFullBlock() is fuzzy, and Shapes.create() is also fuzzy which would return block() + return this.moonrise$isFullBlock() ? Shapes.block() : (VoxelShape)(Object)this; + } + + final List aabbs = this.toAabbs(); + + if (aabbs.isEmpty()) { + // We are a SliceShape, which does not properly fill isEmpty for every case + return Shapes.empty(); + } + + if (aabbs.size() == 1) { + final AABB singleAABB = aabbs.get(0); + final VoxelShape ret = Shapes.create(singleAABB); + + // forward AABB cache + if (((VoxelShape)(Object)ret).cachedToAABBs == null) { + ((VoxelShape)(Object)ret).cachedToAABBs = this.cachedToAABBs; + } + + return ret; + } else { + // reduce complexity of joins by splitting the merges (old complexity: n^2, new: nlogn) + + // set up flat array so that this merge is done in-place + final VoxelShape[] tmp = new VoxelShape[aabbs.size()]; + + // initialise as unmerged + for (int i = 0, len = aabbs.size(); i < len; ++i) { + tmp[i] = Shapes.create(aabbs.get(i)); + } + + int size = aabbs.size(); + while (size > 1) { + int newSize = 0; + for (int i = 0; i < size; i += 2) { + final int next = i + 1; + if (next >= size) { + // nothing to merge with, so leave it for next iteration + tmp[newSize++] = tmp[i]; + break; + } else { + // merge with adjacent + final VoxelShape first = tmp[i]; + final VoxelShape second = tmp[next]; + + tmp[newSize++] = Shapes.joinUnoptimized(first, second, BooleanOp.OR); + } + } + size = newSize; + } + + final VoxelShape ret = tmp[0]; + + // forward AABB cache + if (((VoxelShape)(Object)ret).cachedToAABBs == null) { + ((VoxelShape)(Object)ret).cachedToAABBs = this.cachedToAABBs; + } + + return ret; + } + // Paper end - optimise collisions } public void forAllEdges(Shapes.DoubleLineConsumer action) { @@ -122,9 +683,24 @@ public abstract class VoxelShape { } public List toAabbs() { - List list = Lists.newArrayList(); - this.forAllBoxes((x1, y1, z1, x2, y2, z2) -> list.add(new AABB(x1, y1, z1, x2, y2, z2))); - return list; + // Paper start - optimise collisions + ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cachedToAABBs = this.cachedToAABBs; + if (cachedToAABBs != null) { + if (!cachedToAABBs.isOffset()) { + return cachedToAABBs.aabbs(); + } + + // all we need to do is offset the cache + cachedToAABBs = cachedToAABBs.removeOffset(); + // update cache + this.cachedToAABBs = cachedToAABBs; + + return cachedToAABBs.aabbs(); + } + + // make new cache + return this.toAabbsUncached(); + // Paper end - optimise collisions } public double min(Direction.Axis axis, double primaryPosition, double secondaryPosition) { @@ -146,46 +722,92 @@ public abstract class VoxelShape { } protected int findIndex(Direction.Axis axis, double position) { - return Mth.binarySearch(0, this.shape.getSize(axis) + 1, value -> position < this.get(axis, value)) - 1; + // Paper start - optimise collisions + final double value = position; + 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 startVec, Vec3 endVec, BlockPos pos) { - if (this.isEmpty()) { + // Paper start - optimise collisions + if (this.isEmpty) { return null; - } else { - Vec3 vec3 = endVec.subtract(startVec); - if (vec3.lengthSqr() < 1.0E-7) { - return null; - } else { - Vec3 vec31 = startVec.add(vec3.scale(0.001)); - return this.shape - .isFullWide( - this.findIndex(Direction.Axis.X, vec31.x - pos.getX()), - this.findIndex(Direction.Axis.Y, vec31.y - pos.getY()), - this.findIndex(Direction.Axis.Z, vec31.z - pos.getZ()) - ) - ? new BlockHitResult(vec31, Direction.getApproximateNearest(vec3.x, vec3.y, vec3.z).getOpposite(), pos, true) - : AABB.clip(this.toAabbs(), startVec, endVec, pos); + } + + final Vec3 directionOpposite = endVec.subtract(startVec); + if (directionOpposite.lengthSqr() < ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) { + return null; + } + + final Vec3 fromBehind = startVec.add(directionOpposite.scale(0.001)); + final double fromBehindOffsetX = fromBehind.x - (double) pos.getX(); + final double fromBehindOffsetY = fromBehind.y - (double) pos.getY(); + final double fromBehindOffsetZ = fromBehind.z - (double) pos.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(), pos, true); } + return clip(singleAABB, startVec, endVec, pos); + } + + 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(), pos, true); } + + return AABB.clip(((VoxelShape) (Object) this).toAabbs(), startVec, endVec, pos); + // Paper end - optimise collisions } + // Paper start - optimise collisions public Optional closestPointTo(Vec3 point) { - if (this.isEmpty()) { + if (this.isEmpty) { return Optional.empty(); - } else { - Vec3[] vec3s = new Vec3[1]; - this.forAllBoxes((x1, y1, z1, x2, y2, z2) -> { - double d = Mth.clamp(point.x(), x1, x2); - double d1 = Mth.clamp(point.y(), y1, y2); - double d2 = Mth.clamp(point.z(), z1, z2); - if (vec3s[0] == null || point.distanceToSqr(d, d1, d2) < point.distanceToSqr(vec3s[0])) { - vec3s[0] = new Vec3(d, d1, d2); - } - }); - return Optional.of(vec3s[0]); } + + Vec3 ret = null; + double retDistance = Double.MAX_VALUE; + + final List aabbs = this.toAabbs(); + for (int i = 0, len = aabbs.size(); i < len; ++i) { + final AABB aabb = aabbs.get(i); + final double x = Mth.clamp(point.x, aabb.minX, aabb.maxX); + final double y = Mth.clamp(point.y, aabb.minY, aabb.maxY); + final double z = Mth.clamp(point.z, aabb.minZ, aabb.maxZ); + + double dist = point.distanceToSqr(x, y, z); + if (dist < retDistance) { + ret = new Vec3(x, y, z); + retDistance = dist; + } + } + + return Optional.ofNullable(ret); + // Paper end - optimise collisions } public VoxelShape getFaceShape(Direction side) { @@ -208,19 +830,23 @@ public abstract class VoxelShape { } private VoxelShape calculateFace(Direction side) { - Direction.Axis axis = side.getAxis(); - if (this.isCubeLikeAlong(axis)) { - return this; - } else { - Direction.AxisDirection axisDirection = side.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); + // Paper start - optimise collisions + final Direction.Axis axis = side.getAxis(); + switch (axis) { + case X: { + return this.calculateFaceDirect(side, axis, this.rootCoordinatesX, this.offsetX); + } + case Y: { + return this.calculateFaceDirect(side, axis, this.rootCoordinatesY, this.offsetY); + } + case Z: { + return this.calculateFaceDirect(side, axis, this.rootCoordinatesZ, this.offsetZ); + } + default: { + throw new IllegalStateException("Unknown axis: " + axis); } } + // Paper end - optimise collisions } protected boolean isCubeLike() { @@ -238,9 +864,30 @@ public abstract class VoxelShape { return coords.size() == 2 && DoubleMath.fuzzyEquals(coords.getDouble(0), 0.0, 1.0E-7) && DoubleMath.fuzzyEquals(coords.getDouble(1), 1.0, 1.0E-7); } - public double collide(Direction.Axis movementAxis, AABB collisionBox, double desiredOffset) { - return this.collideX(AxisCycle.between(movementAxis, Direction.Axis.X), collisionBox, desiredOffset); + // 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 movementAxis, AABB collisionBox, double desiredOffset) { if (this.isEmpty()) { diff --git a/net/minecraft/world/ticks/LevelChunkTicks.java b/net/minecraft/world/ticks/LevelChunkTicks.java index 5b6bd88a5bbbce6cce351938418eba4326e41002..faf45ac459f7c25309d6ef6dce371d484a0dae7b 100644 --- a/net/minecraft/world/ticks/LevelChunkTicks.java +++ b/net/minecraft/world/ticks/LevelChunkTicks.java @@ -17,7 +17,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.nbt.ListTag; import net.minecraft.world.level.ChunkPos; -public class LevelChunkTicks implements SerializableTickContainer, TickContainerAccess { +public class LevelChunkTicks implements SerializableTickContainer, TickContainerAccess, ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks { // Paper - rewrite chunk system private final Queue> tickQueue = new PriorityQueue<>(ScheduledTick.DRAIN_ORDER); @Nullable private List> pendingTicks; @@ -25,6 +25,30 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon @Nullable private BiConsumer, ScheduledTick> onTickAdded; + // Paper start - rewrite chunk system + /* + * Since ticks are saved using relative delays, we need to consider the entire tick list dirty when there are scheduled ticks + * and the last saved tick is not equal to the current tick + */ + /* + * In general, it would be nice to be able to "re-pack" ticks once the chunk becomes non-ticking again, but that is a + * bit out of scope for the chunk system + */ + + private boolean dirty; + private long lastSaved = Long.MIN_VALUE; + + @Override + public final boolean moonrise$isDirty(final long tick) { + return this.dirty || (!this.tickQueue.isEmpty() && tick != this.lastSaved); + } + + @Override + public final void moonrise$clearDirty() { + this.dirty = false; + } + // Paper end - rewrite chunk system + public LevelChunkTicks() { } @@ -49,7 +73,7 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon public ScheduledTick poll() { ScheduledTick scheduledTick = this.tickQueue.poll(); if (scheduledTick != null) { - this.ticksPerPosition.remove(scheduledTick); + this.ticksPerPosition.remove(scheduledTick); this.dirty = true; // Paper - rewrite chunk system } return scheduledTick; @@ -58,7 +82,7 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon @Override public void schedule(ScheduledTick tick) { if (this.ticksPerPosition.add(tick)) { - this.scheduleUnchecked(tick); + this.scheduleUnchecked(tick); this.dirty = true; // Paper - rewrite chunk system } } @@ -80,7 +104,7 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon while (iterator.hasNext()) { ScheduledTick scheduledTick = iterator.next(); if (predicate.test(scheduledTick)) { - iterator.remove(); + iterator.remove(); this.dirty = true; // Paper - rewrite chunk system this.ticksPerPosition.remove(scheduledTick); } } @@ -110,6 +134,7 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon } public ListTag save(long gametime, Function idGetter) { + this.lastSaved = gametime; // Paper - rewrite chunk system ListTag listTag = new ListTag(); for (SavedTick savedTick : this.pack(gametime)) { @@ -121,6 +146,7 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon public void unpack(long gameTime) { if (this.pendingTicks != null) { + this.lastSaved = gameTime; // Paper - rewrite chunk system int i = -this.pendingTicks.size(); for (SavedTick savedTick : this.pendingTicks) {