diff --git a/patches/unapplied/server/Add-packet-limiter-config.patch b/patches/server/Add-packet-limiter-config.patch similarity index 97% rename from patches/unapplied/server/Add-packet-limiter-config.patch rename to patches/server/Add-packet-limiter-config.patch index c6abe20910..01b9fd79e7 100644 --- a/patches/unapplied/server/Add-packet-limiter-config.patch +++ b/patches/server/Add-packet-limiter-config.patch @@ -28,8 +28,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/com/destroystokyo/paper/PaperConfig.java +++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java @@ -0,0 +0,0 @@ public class PaperConfig { - playerMaxConcurrentChunkLoads = getDouble("settings.chunk-loading.player-max-concurrent-loads", 4.0); - globalMaxConcurrentChunkLoads = getDouble("settings.chunk-loading.global-max-concurrent-loads", 500.0); + itemValidationBookAuthorLength = getInt("settings.item-validation.book.author", itemValidationBookAuthorLength); + itemValidationBookPageLength = getInt("settings.item-validation.book.page", itemValidationBookPageLength); } + + public static final class PacketLimit { diff --git a/patches/unapplied/server/Allow-removal-addition-of-entities-to-entity-ticklis.patch b/patches/server/Allow-removal-addition-of-entities-to-entity-ticklis.patch similarity index 100% rename from patches/unapplied/server/Allow-removal-addition-of-entities-to-entity-ticklis.patch rename to patches/server/Allow-removal-addition-of-entities-to-entity-ticklis.patch diff --git a/patches/unapplied/server/Consolidate-flush-calls-for-entity-tracker-packets.patch b/patches/server/Consolidate-flush-calls-for-entity-tracker-packets.patch similarity index 55% rename from patches/unapplied/server/Consolidate-flush-calls-for-entity-tracker-packets.patch rename to patches/server/Consolidate-flush-calls-for-entity-tracker-packets.patch index 55b7554829..8c34db1f90 100644 --- a/patches/unapplied/server/Consolidate-flush-calls-for-entity-tracker-packets.patch +++ b/patches/server/Consolidate-flush-calls-for-entity-tracker-packets.patch @@ -26,27 +26,27 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - this.level.getProfiler().pop(); + }); + gameprofilerfiller.pop(); + gameprofilerfiller.pop(); ++ // Paper start - controlled flush for entity tracker packets ++ List disabledFlushes = new java.util.ArrayList<>(this.level.players.size()); ++ for (ServerPlayer player : this.level.players) { ++ net.minecraft.server.network.ServerGamePacketListenerImpl connection = player.connection; ++ if (connection != null) { ++ connection.connection.disableAutomaticFlush(); ++ disabledFlushes.add(connection.connection); ++ } ++ } ++ try { // Paper end - controlled flush for entity tracker packets + this.chunkMap.tick(); ++ // Paper start - controlled flush for entity tracker packets ++ } finally { ++ for (net.minecraft.network.Connection networkManager : disabledFlushes) { ++ networkManager.enableAutomaticFlush(); ++ } ++ } ++ // Paper end - controlled flush for entity tracker packets } - -+ // Paper start - controlled flush for entity tracker packets -+ List disabledFlushes = new java.util.ArrayList<>(this.level.players.size()); -+ for (ServerPlayer player : this.level.players) { -+ net.minecraft.server.network.ServerGamePacketListenerImpl connection = player.connection; -+ if (connection != null) { -+ connection.connection.disableAutomaticFlush(); -+ disabledFlushes.add(connection.connection); -+ } -+ } -+ try { // Paper end - controlled flush for entity tracker packets - this.chunkMap.tick(); -+ // Paper start - controlled flush for entity tracker packets -+ } finally { -+ for (net.minecraft.network.Connection networkManager : disabledFlushes) { -+ networkManager.enableAutomaticFlush(); -+ } -+ } -+ // Paper end - controlled flush for entity tracker packets } - private void getFullChunk(long pos, Consumer chunkConsumer) { diff --git a/patches/unapplied/server/Custom-table-implementation-for-blockstate-state-loo.patch b/patches/server/Custom-table-implementation-for-blockstate-state-loo.patch similarity index 100% rename from patches/unapplied/server/Custom-table-implementation-for-blockstate-state-loo.patch rename to patches/server/Custom-table-implementation-for-blockstate-state-loo.patch diff --git a/patches/unapplied/server/Detail-more-information-in-watchdog-dumps.patch b/patches/server/Detail-more-information-in-watchdog-dumps.patch similarity index 98% rename from patches/unapplied/server/Detail-more-information-in-watchdog-dumps.patch rename to patches/server/Detail-more-information-in-watchdog-dumps.patch index 061735ccdd..441e010e98 100644 --- a/patches/unapplied/server/Detail-more-information-in-watchdog-dumps.patch +++ b/patches/server/Detail-more-information-in-watchdog-dumps.patch @@ -126,7 +126,7 @@ diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/jav index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, i return this.onGround; } @@ -169,7 +169,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 if (this.noPhysics) { this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z); } else { -@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, i this.level.getProfiler().pop(); } } @@ -182,8 +182,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + // Paper end - detailed watchdog information } - protected void tryCheckInsideBlocks() { -@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + protected boolean isHorizontalCollisionMinor(Vec3 adjustedMovement) { +@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, i } public void setDeltaMovement(Vec3 velocity) { @@ -193,7 +193,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 } public void setDeltaMovement(double x, double y, double z) { -@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, i } // Paper end - fix MC-4 if (this.position.x != x || this.position.y != y || this.position.z != z) { diff --git a/patches/unapplied/server/Distance-manager-tick-timings.patch b/patches/server/Distance-manager-tick-timings.patch similarity index 96% rename from patches/unapplied/server/Distance-manager-tick-timings.patch rename to patches/server/Distance-manager-tick-timings.patch index 7af0b68df4..f7f90e4f4c 100644 --- a/patches/unapplied/server/Distance-manager-tick-timings.patch +++ b/patches/server/Distance-manager-tick-timings.patch @@ -23,8 +23,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { + public boolean runDistanceManagerUpdates() { - if (distanceManager.delayDistanceManagerTick) return false; // Paper - Chunk priority if (this.chunkMap.unloadingPlayerChunk) { net.minecraft.server.MinecraftServer.LOGGER.fatal("Cannot tick distance manager while unloading playerchunks", new Throwable()); throw new IllegalStateException("Cannot tick distance manager while unloading playerchunks"); } // Paper + co.aikar.timings.MinecraftTimings.distanceManagerTick.startTiming(); try { // Paper - add timings for distance manager boolean flag = this.distanceManager.runAllUpdates(this.chunkMap); diff --git a/patches/unapplied/server/Do-not-run-raytrace-logic-for-AIR.patch b/patches/server/Do-not-run-raytrace-logic-for-AIR.patch similarity index 100% rename from patches/unapplied/server/Do-not-run-raytrace-logic-for-AIR.patch rename to patches/server/Do-not-run-raytrace-logic-for-AIR.patch diff --git a/patches/unapplied/server/Don-t-lookup-fluid-state-when-raytracing.patch b/patches/server/Don-t-lookup-fluid-state-when-raytracing.patch similarity index 100% rename from patches/unapplied/server/Don-t-lookup-fluid-state-when-raytracing.patch rename to patches/server/Don-t-lookup-fluid-state-when-raytracing.patch diff --git a/patches/server/Don-t-read-neighbour-chunk-data-off-disk-when-conver.patch b/patches/server/Don-t-read-neighbour-chunk-data-off-disk-when-conver.patch new file mode 100644 index 0000000000..ea14812e17 --- /dev/null +++ b/patches/server/Don-t-read-neighbour-chunk-data-off-disk-when-conver.patch @@ -0,0 +1,21 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sun, 11 Apr 2021 02:58:48 -0700 +Subject: [PATCH] Don't read neighbour chunk data off disk when converting + chunks + +Lighting is purged on update anyways, so let's not add more +into the conversion process + +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java +@@ -0,0 +0,0 @@ public class ChunkStorage implements AutoCloseable { + + // CraftBukkit start + private boolean check(ServerChunkCache cps, int x, int z) throws IOException { ++ if (true) return true; // Paper - this isn't even needed anymore, light is purged updating to 1.14+, why are we holding up the conversion process reading chunk data off disk - return true, we need to set light populated to true so the converter recognizes the chunk as being "full" + ChunkPos pos = new ChunkPos(x, z); + if (cps != null) { + //com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread"); // Paper - this function is now MT-Safe diff --git a/patches/unapplied/server/Lag-compensate-block-breaking.patch b/patches/server/Lag-compensate-block-breaking.patch similarity index 100% rename from patches/unapplied/server/Lag-compensate-block-breaking.patch rename to patches/server/Lag-compensate-block-breaking.patch diff --git a/patches/unapplied/server/Make-sure-inlined-getChunkAt-has-inlined-logic-for-l.patch b/patches/server/Make-sure-inlined-getChunkAt-has-inlined-logic-for-l.patch similarity index 100% rename from patches/unapplied/server/Make-sure-inlined-getChunkAt-has-inlined-logic-for-l.patch rename to patches/server/Make-sure-inlined-getChunkAt-has-inlined-logic-for-l.patch diff --git a/patches/unapplied/server/Manually-inline-methods-in-BlockPosition.patch b/patches/server/Manually-inline-methods-in-BlockPosition.patch similarity index 94% rename from patches/unapplied/server/Manually-inline-methods-in-BlockPosition.patch rename to patches/server/Manually-inline-methods-in-BlockPosition.patch index 6b7cf66f33..7688ad56d8 100644 --- a/patches/unapplied/server/Manually-inline-methods-in-BlockPosition.patch +++ b/patches/server/Manually-inline-methods-in-BlockPosition.patch @@ -59,5 +59,5 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + protected int y; // Paper - protected + protected int z; // Paper - protected - // Paper start - public boolean isValidLocation(net.minecraft.world.level.LevelHeightAccessor levelHeightAccessor) { + private static Function> checkOffsetAxes(int maxAbsValue) { + return (vec) -> { diff --git a/patches/unapplied/server/Name-craft-scheduler-threads-according-to-the-plugin.patch b/patches/server/Name-craft-scheduler-threads-according-to-the-plugin.patch similarity index 100% rename from patches/unapplied/server/Name-craft-scheduler-threads-according-to-the-plugin.patch rename to patches/server/Name-craft-scheduler-threads-according-to-the-plugin.patch diff --git a/patches/unapplied/server/Oprimise-map-impl-for-tracked-players.patch b/patches/server/Oprimise-map-impl-for-tracked-players.patch similarity index 90% rename from patches/unapplied/server/Oprimise-map-impl-for-tracked-players.patch rename to patches/server/Oprimise-map-impl-for-tracked-players.patch index 0b9daf159a..067565502a 100644 --- a/patches/unapplied/server/Oprimise-map-impl-for-tracked-players.patch +++ b/patches/server/Oprimise-map-impl-for-tracked-players.patch @@ -12,13 +12,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -0,0 +0,0 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - - import org.bukkit.entity.Player; // CraftBukkit + import org.bukkit.entity.Player; + // CraftBukkit end +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; // Paper public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider { -@@ -0,0 +0,0 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider final Entity entity; private final int range; SectionPos lastSectionPos; diff --git a/patches/unapplied/server/Optimise-BlockSoil-nearby-water-lookup.patch b/patches/server/Optimise-BlockSoil-nearby-water-lookup.patch similarity index 100% rename from patches/unapplied/server/Optimise-BlockSoil-nearby-water-lookup.patch rename to patches/server/Optimise-BlockSoil-nearby-water-lookup.patch diff --git a/patches/unapplied/server/Optimise-random-block-ticking.patch b/patches/server/Optimise-random-block-ticking.patch similarity index 82% rename from patches/unapplied/server/Optimise-random-block-ticking.patch rename to patches/server/Optimise-random-block-ticking.patch index daaea5b65a..164c700293 100644 --- a/patches/unapplied/server/Optimise-random-block-ticking.patch +++ b/patches/server/Optimise-random-block-ticking.patch @@ -114,7 +114,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + // Paper end Biome biomebase = this.getBiome(blockposition); -- if (biomebase.shouldFreeze((LevelReader) this, blockposition1)) { +- if (biomebase.shouldFreeze(this, blockposition1)) { - org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition1, Blocks.ICE.defaultBlockState(), null); // CraftBukkit + // Paper start - optimise chunk ticking + blockposition.setY(downY); @@ -135,14 +135,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + blockposition.setY(normalY); // Paper Biome.Precipitation biomebase_precipitation = this.getBiome(blockposition).getPrecipitation(); -- if (biomebase_precipitation == Biome.Precipitation.RAIN && biomebase.isColdEnoughToSnow(blockposition1)) { +- if (biomebase_precipitation == Biome.Precipitation.RAIN && biomebase.coldEnoughToSnow(blockposition1)) { + blockposition.setY(downY); // Paper -+ if (biomebase_precipitation == Biome.Precipitation.RAIN && biomebase.isColdEnoughToSnow(blockposition)) { // Paper ++ if (biomebase_precipitation == Biome.Precipitation.RAIN && biomebase.coldEnoughToSnow(blockposition)) { // Paper biomebase_precipitation = Biome.Precipitation.SNOW; } -- iblockdata.getBlock().handlePrecipitation(iblockdata, (Level) this, blockposition1, biomebase_precipitation); -+ iblockdata.getBlock().handlePrecipitation(iblockdata, (Level) this, blockposition, biomebase_precipitation); // Paper +- iblockdata.getBlock().handlePrecipitation(iblockdata, this, blockposition1, biomebase_precipitation); ++ iblockdata.getBlock().handlePrecipitation(iblockdata, this, blockposition, biomebase_precipitation); // Paper } } @@ -157,7 +157,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 - for (int i1 = 0; i1 < l; ++i1) { - LevelChunkSection chunksection = achunksection[i1]; - -- if (chunksection != LevelChunk.EMPTY_SECTION && chunksection.isRandomlyTicking()) { +- if (chunksection.isRandomlyTicking()) { - int j1 = chunksection.bottomBlockY(); - - for (int k1 = 0; k1 < randomTickSpeed; ++k1) { @@ -214,12 +214,32 @@ diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/ne index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/net/minecraft/util/BitStorage.java +++ b/src/main/java/net/minecraft/util/BitStorage.java -@@ -0,0 +0,0 @@ public class BitStorage { - } +@@ -0,0 +0,0 @@ public interface BitStorage { + void unpack(int[] is); - } + BitStorage copy(); + + // Paper start ++ void forEach(DataBitConsumer consumer); ++ ++ @FunctionalInterface ++ interface DataBitConsumer { ++ ++ void accept(int location, int data); ++ ++ } ++ // Paper end + } +diff --git a/src/main/java/net/minecraft/util/SimpleBitStorage.java b/src/main/java/net/minecraft/util/SimpleBitStorage.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/util/SimpleBitStorage.java ++++ b/src/main/java/net/minecraft/util/SimpleBitStorage.java +@@ -0,0 +0,0 @@ public class SimpleBitStorage implements BitStorage { + return this.bits; + } + ++ // Paper start ++ @Override + public final void forEach(DataBitConsumer consumer) { + int i = 0; + long[] along = this.data; @@ -238,15 +258,31 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + } + } + } ++ // Paper end + -+ @FunctionalInterface -+ public static interface DataBitConsumer { -+ -+ void accept(int location, int data); -+ + @Override + public void getAll(IntConsumer action) { + int i = 0; +diff --git a/src/main/java/net/minecraft/util/ZeroBitStorage.java b/src/main/java/net/minecraft/util/ZeroBitStorage.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/util/ZeroBitStorage.java ++++ b/src/main/java/net/minecraft/util/ZeroBitStorage.java +@@ -0,0 +0,0 @@ public class ZeroBitStorage implements BitStorage { + return 0; + } + ++ // Paper start ++ @Override ++ public void forEach(DataBitConsumer consumer) { ++ for(int i = 0; i < this.size; ++i) { ++ consumer.accept(i, 0); ++ } + } + // Paper end - } ++ + @Override + public void getAll(IntConsumer action) { + for(int i = 0; i < this.size; ++i) { diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java @@ -289,16 +325,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java @@ -0,0 +0,0 @@ public class LevelChunkSection { - private short tickingBlockCount; private short tickingFluidCount; - public final PalettedContainer states; // Paper - package-private // Paper - public + public final PalettedContainer states; + private final PalettedContainer biomes; + public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper - // Paper start - Anti-Xray - Add parameters - @Deprecated public LevelChunkSection(int yOffset) { this(yOffset, null, null, true); } // Notice for updates: Please make sure this constructor isn't used anywhere + public LevelChunkSection(int chunkPos, PalettedContainer blockStateContainer, PalettedContainer biomeContainer) { + this.bottomBlockY = LevelChunkSection.getBottomBlockY(chunkPos); @@ -0,0 +0,0 @@ public class LevelChunkSection { --this.nonEmptyBlockCount; - if (blockState.isRandomlyTicking()) { + if (iblockdata1.isRandomlyTicking()) { --this.tickingBlockCount; + // Paper start + this.tickingList.remove(x, y, z); @@ -326,27 +362,28 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 this.nonEmptyBlockCount = 0; this.tickingBlockCount = 0; this.tickingFluidCount = 0; -- this.states.count((state, count) -> { -+ this.states.forEachLocation((state, location) -> { // Paper - FluidState fluidState = state.getFluidState(); - if (!state.isAir()) { -- this.nonEmptyBlockCount = (short)(this.nonEmptyBlockCount + count); -+ this.nonEmptyBlockCount = (short)(this.nonEmptyBlockCount + 1); // Paper - if (state.isRandomlyTicking()) { -- this.tickingBlockCount = (short)(this.tickingBlockCount + count); +- this.states.count((iblockdata, i) -> { ++ this.states.forEachLocation((iblockdata, i) -> { // Paper + FluidState fluid = iblockdata.getFluidState(); + + if (!iblockdata.isAir()) { +- this.nonEmptyBlockCount = (short) (this.nonEmptyBlockCount + i); ++ this.nonEmptyBlockCount = (short) (this.nonEmptyBlockCount + 1); // Paper + if (iblockdata.isRandomlyTicking()) { +- this.tickingBlockCount = (short) (this.tickingBlockCount + i); + // Paper start + this.tickingBlockCount = (short)(this.tickingBlockCount + 1); -+ this.tickingList.add(location, state); ++ this.tickingList.add(i, iblockdata); + // Paper end } } - if (!fluidState.isEmpty()) { -- this.nonEmptyBlockCount = (short)(this.nonEmptyBlockCount + count); -+ this.nonEmptyBlockCount = (short)(this.nonEmptyBlockCount + 1); // Paper - if (fluidState.isRandomlyTicking()) { -- this.tickingFluidCount = (short)(this.tickingFluidCount + count); -+ this.tickingFluidCount = (short)(this.tickingFluidCount + 1); // Paper + if (!fluid.isEmpty()) { +- this.nonEmptyBlockCount = (short) (this.nonEmptyBlockCount + i); ++ this.nonEmptyBlockCount = (short) (this.nonEmptyBlockCount + 1); // Paper + if (fluid.isRandomlyTicking()) { +- this.tickingFluidCount = (short) (this.tickingFluidCount + i); ++ this.tickingFluidCount = (short) (this.tickingFluidCount + 1); // Paper } } @@ -355,15 +392,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java @@ -0,0 +0,0 @@ public class PalettedContainer implements PaletteResize { + } + } + ++ // Paper start ++ public void forEachLocation(PalettedContainer.CountConsumer consumer) { ++ this.data.storage.forEach((int location, int data) -> { ++ consumer.accept(this.data.palette.valueFor(data), location); ++ }); ++ } ++ // Paper end ++ + @FunctionalInterface public interface CountConsumer { void accept(T object, int count); - } -+ -+ // Paper start -+ public void forEachLocation(PalettedContainer.CountConsumer datapaletteblock_a) { -+ this.storage.forEach((int location, int data) -> { -+ datapaletteblock_a.accept(this.palette.valueFor(data), location); -+ }); -+ } -+ // Paper end - } diff --git a/patches/unapplied/server/Send-full-pos-packets-for-hard-colliding-entities.patch b/patches/server/Send-full-pos-packets-for-hard-colliding-entities.patch similarity index 100% rename from patches/unapplied/server/Send-full-pos-packets-for-hard-colliding-entities.patch rename to patches/server/Send-full-pos-packets-for-hard-colliding-entities.patch diff --git a/patches/unapplied/server/Time-scoreboard-search.patch b/patches/server/Time-scoreboard-search.patch similarity index 100% rename from patches/unapplied/server/Time-scoreboard-search.patch rename to patches/server/Time-scoreboard-search.patch diff --git a/patches/unapplied/server/Use-correct-LevelStem-registry-when-loading-default-.patch b/patches/server/Use-correct-LevelStem-registry-when-loading-default-.patch similarity index 100% rename from patches/unapplied/server/Use-correct-LevelStem-registry-when-loading-default-.patch rename to patches/server/Use-correct-LevelStem-registry-when-loading-default-.patch diff --git a/patches/unapplied/server/Add-Raw-Byte-Entity-Serialization.patch b/patches/unapplied/server/Add-Raw-Byte-Entity-Serialization.patch deleted file mode 100644 index a8cb66c006..0000000000 --- a/patches/unapplied/server/Add-Raw-Byte-Entity-Serialization.patch +++ /dev/null @@ -1,80 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Mariell Hoversholm -Date: Sun, 24 Oct 2021 16:20:31 -0400 -Subject: [PATCH] Add Raw Byte Entity Serialization - - -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, i - } - } - -+ // Paper start - Entity serialization api -+ public boolean serializeEntity(CompoundTag compound) { -+ List pass = new java.util.ArrayList<>(this.getPassengers()); -+ this.passengers = ImmutableList.of(); -+ boolean result = save(compound); -+ this.passengers = ImmutableList.copyOf(pass); -+ return result; -+ } -+ // Paper end - public boolean save(CompoundTag nbt) { - return this.isPassenger() ? false : this.saveAsPassenger(nbt); - } -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -@@ -0,0 +0,0 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { - } - return set; - } -+ -+ @Override -+ public boolean spawnAt(Location location, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) { -+ Preconditions.checkNotNull(location, "location cannot be null"); -+ Preconditions.checkNotNull(reason, "reason cannot be null"); -+ entity.level = ((CraftWorld) location.getWorld()).getHandle(); -+ entity.setPos(location.getX(), location.getY(), location.getZ()); -+ entity.setRot(location.getYaw(), location.getPitch()); -+ return !entity.valid && entity.level.addEntity(entity, reason); -+ } - // Paper end - } -diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -@@ -0,0 +0,0 @@ public final class CraftMagicNumbers implements UnsafeValues { - return CraftItemStack.asCraftMirror(net.minecraft.world.item.ItemStack.of((CompoundTag) converted.getValue())); - } - -+ @Override -+ public byte[] serializeEntity(org.bukkit.entity.Entity entity) { -+ Preconditions.checkNotNull(entity, "null cannot be serialized"); -+ Preconditions.checkArgument(entity instanceof org.bukkit.craftbukkit.entity.CraftEntity, "only CraftEntities can be serialized"); -+ -+ CompoundTag compound = new CompoundTag(); -+ ((org.bukkit.craftbukkit.entity.CraftEntity) entity).getHandle().serializeEntity(compound); -+ return serializeNbtToBytes(compound); -+ } -+ -+ @Override -+ public org.bukkit.entity.Entity deserializeEntity(byte[] data, org.bukkit.World world, boolean preserveUUID) { -+ Preconditions.checkNotNull(data, "null cannot be deserialized"); -+ Preconditions.checkArgument(data.length > 0, "cannot deserialize nothing"); -+ -+ CompoundTag compound = deserializeNbtFromBytes(data); -+ int dataVersion = compound.getInt("DataVersion"); -+ compound = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ENTITY, compound, dataVersion, getDataVersion()); -+ if (!preserveUUID) compound.remove("UUID"); // Generate a new UUID so we don't have to worry about deserializing the same entity twice -+ return net.minecraft.world.entity.EntityType.create(compound, ((org.bukkit.craftbukkit.CraftWorld) world).getHandle()) -+ .orElseThrow(() -> new IllegalArgumentException("An ID was not found for the data. Did you downgrade?")).getBukkitEntity(); -+ } -+ - private byte[] serializeNbtToBytes(CompoundTag compound) { - compound.putInt("DataVersion", getDataVersion()); - java.io.ByteArrayOutputStream outputStream = new java.io.ByteArrayOutputStream(); diff --git a/patches/unapplied/server/Add-paper-mobcaps-and-paper-playermobcaps.patch b/patches/unapplied/server/Add-paper-mobcaps-and-paper-playermobcaps.patch deleted file mode 100644 index 6b229c7dd0..0000000000 --- a/patches/unapplied/server/Add-paper-mobcaps-and-paper-playermobcaps.patch +++ /dev/null @@ -1,373 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> -Date: Mon, 16 Aug 2021 01:31:54 -0500 -Subject: [PATCH] Add '/paper mobcaps' and '/paper playermobcaps' - -Add commands to get the mobcaps for a world, as well as the mobcaps for -each player when per-player mob spawning is enabled. - -Also has a hover text on each mob category listing what entity types are -in said category - -diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/com/destroystokyo/paper/PaperCommand.java -+++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java -@@ -0,0 +0,0 @@ package com.destroystokyo.paper; - import com.destroystokyo.paper.io.SyncLoadFinder; - import com.google.common.base.Functions; - import com.google.common.base.Joiner; -+import com.google.common.collect.ImmutableMap; - import com.google.common.collect.ImmutableSet; - import com.google.common.collect.Iterables; - import com.google.common.collect.Lists; -@@ -0,0 +0,0 @@ import com.google.common.collect.Maps; - import com.google.gson.JsonObject; - import com.google.gson.internal.Streams; - import com.google.gson.stream.JsonWriter; -+import net.kyori.adventure.text.Component; -+import net.kyori.adventure.text.ComponentLike; -+import net.kyori.adventure.text.TextComponent; -+import net.kyori.adventure.text.format.NamedTextColor; -+import net.kyori.adventure.text.format.TextColor; -+import net.minecraft.core.Registry; - import net.minecraft.resources.ResourceLocation; - import net.minecraft.server.MCUtil; - import net.minecraft.server.MinecraftServer; -@@ -0,0 +0,0 @@ import net.minecraft.server.level.ServerLevel; - import net.minecraft.server.level.ServerPlayer; - import net.minecraft.server.level.ThreadedLevelLightEngine; - import net.minecraft.world.entity.EntityType; -+import net.minecraft.world.entity.MobCategory; - import net.minecraft.world.level.ChunkPos; - import net.minecraft.network.protocol.game.ClientboundLightUpdatePacket; - import net.minecraft.resources.ResourceLocation; - import net.minecraft.server.MCUtil; -+import net.minecraft.world.level.NaturalSpawner; - import org.apache.commons.lang3.tuple.MutablePair; - import org.apache.commons.lang3.tuple.Pair; - import org.bukkit.Bukkit; -@@ -0,0 +0,0 @@ import java.util.List; - import java.util.Locale; - import java.util.Map; - import java.util.Set; -+import java.util.function.ToIntFunction; - import java.util.stream.Collectors; - - public class PaperCommand extends Command { - private static final String BASE_PERM = "bukkit.command.paper."; -- private static final ImmutableSet SUBCOMMANDS = ImmutableSet.builder().add("heap", "entity", "reload", "version", "debug", "chunkinfo", "fixlight", "syncloadinfo", "dumpitem").build(); -+ private static final ImmutableSet SUBCOMMANDS = ImmutableSet.builder().add("heap", "entity", "reload", "version", "debug", "chunkinfo", "fixlight", "syncloadinfo", "dumpitem", "mobcaps", "playermobcaps").build(); - - public PaperCommand(String name) { - super(name); -@@ -0,0 +0,0 @@ public class PaperCommand extends Command { - return getListMatchingLast(sender, args, "help", "chunks"); - } - break; -+ case "mobcaps": -+ return getListMatchingLast(sender, args, this.suggestMobcaps(sender, args)); -+ case "playermobcaps": -+ return getListMatchingLast(sender, args, this.suggestPlayerMobcaps(sender, args)); - case "chunkinfo": - List worldNames = new ArrayList<>(); - worldNames.add("*"); -@@ -0,0 +0,0 @@ public class PaperCommand extends Command { - case "syncloadinfo": - this.doSyncLoadInfo(sender, args); - break; -+ case "mobcaps": -+ this.printMobcaps(sender, args); -+ break; -+ case "playermobcaps": -+ this.printPlayerMobcaps(sender, args); -+ break; - case "ver": - if (!testPermission(sender, "version")) break; // "ver" needs a special check because it's an alias. All other commands are checked up before the switch statement (because they are present in the SUBCOMMANDS set) - case "version": -@@ -0,0 +0,0 @@ public class PaperCommand extends Command { - } - } - -+ public static final Map MOB_CATEGORY_COLORS = ImmutableMap.builder() -+ .put(MobCategory.MONSTER, NamedTextColor.RED) -+ .put(MobCategory.CREATURE, NamedTextColor.GREEN) -+ .put(MobCategory.AMBIENT, NamedTextColor.GRAY) -+ .put(MobCategory.UNDERGROUND_WATER_CREATURE, TextColor.color(0x3541E6)) -+ .put(MobCategory.WATER_CREATURE, TextColor.color(0x006EFF)) -+ .put(MobCategory.WATER_AMBIENT, TextColor.color(0x00B3FF)) -+ .put(MobCategory.MISC, TextColor.color(0x636363)) -+ .build(); -+ -+ private List suggestMobcaps(CommandSender sender, String[] args) { -+ if (args.length == 2) { -+ final List worlds = new ArrayList<>(Bukkit.getWorlds().stream().map(World::getName).toList()); -+ worlds.add("*"); -+ return worlds; -+ } -+ -+ return Collections.emptyList(); -+ } -+ -+ private List suggestPlayerMobcaps(CommandSender sender, String[] args) { -+ if (args.length == 2) { -+ final List list = new ArrayList<>(); -+ for (final Player player : Bukkit.getOnlinePlayers()) { -+ if (!(sender instanceof Player senderPlayer) || senderPlayer.canSee(player)) { -+ list.add(player.getName()); -+ } -+ } -+ return list; -+ } -+ -+ return Collections.emptyList(); -+ } -+ -+ private void printMobcaps(CommandSender sender, String[] args) { -+ final List worlds; -+ if (args.length == 1) { -+ if (sender instanceof Player player) { -+ worlds = List.of(player.getWorld()); -+ } else { -+ sender.sendMessage(Component.text("Must specify a world! ex: '/paper mobcaps world'", NamedTextColor.RED)); -+ return; -+ } -+ } else if (args.length == 2) { -+ final String input = args[1]; -+ if (input.equals("*")) { -+ worlds = Bukkit.getWorlds(); -+ } else { -+ final World world = Bukkit.getWorld(input); -+ if (world == null) { -+ sender.sendMessage(Component.text("'" + input + "' is not a valid world!", NamedTextColor.RED)); -+ return; -+ } else { -+ worlds = List.of(world); -+ } -+ } -+ } else { -+ sender.sendMessage(Component.text("Too many arguments!", NamedTextColor.RED)); -+ return; -+ } -+ -+ for (final World world : worlds) { -+ final ServerLevel level = ((CraftWorld) world).getHandle(); -+ final NaturalSpawner.SpawnState state = level.getChunkSource().getLastSpawnState(); -+ -+ final int chunks; -+ if (state == null) { -+ chunks = 0; -+ } else { -+ chunks = state.getSpawnableChunkCount(); -+ } -+ sender.sendMessage(TextComponent.ofChildren( -+ Component.text("Mobcaps for world: "), -+ Component.text(world.getName(), NamedTextColor.AQUA), -+ Component.text(" (" + chunks + " spawnable chunks)") -+ )); -+ -+ sender.sendMessage(this.buildMobcapsComponent( -+ category -> { -+ if (state == null) { -+ return 0; -+ } else { -+ return state.getMobCategoryCounts().getOrDefault(category, 0); -+ } -+ }, -+ category -> NaturalSpawner.globalLimitForCategory(level, category, chunks) -+ )); -+ } -+ } -+ -+ private void printPlayerMobcaps(CommandSender sender, String[] args) { -+ final Player player; -+ if (args.length == 1) { -+ if (sender instanceof Player pl) { -+ player = pl; -+ } else { -+ sender.sendMessage(Component.text("Must specify a player! ex: '/paper playermobcount playerName'", NamedTextColor.RED)); -+ return; -+ } -+ } else if (args.length == 2) { -+ final String input = args[1]; -+ player = Bukkit.getPlayerExact(input); -+ if (player == null) { -+ sender.sendMessage(Component.text("Could not find player named '" + input + "'", NamedTextColor.RED)); -+ return; -+ } -+ } else { -+ sender.sendMessage(Component.text("Too many arguments!", NamedTextColor.RED)); -+ return; -+ } -+ -+ final ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle(); -+ final ServerLevel level = serverPlayer.getLevel(); -+ -+ if (!level.paperConfig.perPlayerMobSpawns) { -+ sender.sendMessage(Component.text("Use '/paper mobcaps' for worlds where per-player mob spawning is disabled.", NamedTextColor.RED)); -+ return; -+ } -+ -+ sender.sendMessage(TextComponent.ofChildren(Component.text("Mobcaps for player: "), Component.text(player.getName(), NamedTextColor.GREEN))); -+ sender.sendMessage(this.buildMobcapsComponent( -+ category -> level.chunkSource.chunkMap.getMobCountNear(serverPlayer, category), -+ category -> NaturalSpawner.limitForCategory(level, category) -+ )); -+ } -+ -+ private Component buildMobcapsComponent(final ToIntFunction countGetter, final ToIntFunction limitGetter) { -+ return MOB_CATEGORY_COLORS.entrySet().stream() -+ .map(entry -> { -+ final MobCategory category = entry.getKey(); -+ final TextColor color = entry.getValue(); -+ -+ final Component categoryHover = TextComponent.ofChildren( -+ Component.text("Entity types in category ", TextColor.color(0xE0E0E0)), -+ Component.text(category.getName(), color), -+ Component.text(':', NamedTextColor.GRAY), -+ Component.newline(), -+ Component.newline(), -+ Registry.ENTITY_TYPE.entrySet().stream() -+ .filter(it -> it.getValue().getCategory() == category) -+ .map(it -> Component.translatable(it.getValue().getDescriptionId())) -+ .collect(Component.toComponent(Component.text(", ", NamedTextColor.GRAY))) -+ ); -+ -+ final Component categoryComponent = Component.text() -+ .content(" " + category.getName()) -+ .color(color) -+ .hoverEvent(categoryHover) -+ .build(); -+ -+ final TextComponent.Builder builder = Component.text() -+ .append( -+ categoryComponent, -+ Component.text(": ", NamedTextColor.GRAY) -+ ); -+ final int limit = limitGetter.applyAsInt(category); -+ if (limit != -1) { -+ builder.append( -+ Component.text(countGetter.applyAsInt(category)), -+ Component.text("/", NamedTextColor.GRAY), -+ Component.text(limit) -+ ); -+ } else { -+ builder.append(Component.text() -+ .append( -+ Component.text('n'), -+ Component.text("/", NamedTextColor.GRAY), -+ Component.text('a') -+ ) -+ .hoverEvent(Component.text("This category does not naturally spawn."))); -+ } -+ return builder; -+ }) -+ .map(ComponentLike::asComponent) -+ .collect(Component.toComponent(Component.newline())); -+ } -+ - private void doChunkInfo(CommandSender sender, String[] args) { - List worlds; - if (args.length < 2 || args[1].equals("*")) { -diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java -+++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java -@@ -0,0 +0,0 @@ public final class NaturalSpawner { - MobCategory enumcreaturetype = aenumcreaturetype[j]; - // CraftBukkit start - Use per-world spawn limits - boolean spawnThisTick = true; -- int limit = enumcreaturetype.getMaxInstancesPerChunk(); -+ final int limit = limitForCategory(world, enumcreaturetype); // Paper - switch (enumcreaturetype) { -- case MONSTER: -- spawnThisTick = spawnMonsterThisTick; -- limit = world.getWorld().getMonsterSpawnLimit(); -- break; -- case CREATURE: -- spawnThisTick = spawnAnimalThisTick; -- limit = world.getWorld().getAnimalSpawnLimit(); -- break; -- case WATER_CREATURE: -- spawnThisTick = spawnWaterThisTick; -- limit = world.getWorld().getWaterAnimalSpawnLimit(); -- break; -- case UNDERGROUND_WATER_CREATURE: -- spawnThisTick = spawnWaterUndergroundCreatureThisTick; -- limit = world.getWorld().getWaterUndergroundCreatureSpawnLimit(); -- break; -- case AMBIENT: -- spawnThisTick = spawnAmbientThisTick; -- limit = world.getWorld().getAmbientSpawnLimit(); -- break; -- case WATER_AMBIENT: -- spawnThisTick = spawnWaterAmbientThisTick; -- limit = world.getWorld().getWaterAmbientSpawnLimit(); -- break; -+ // Paper start - not mindiff so we get conflict on change -+ case MONSTER -> spawnThisTick = spawnMonsterThisTick; -+ case CREATURE -> spawnThisTick = spawnAnimalThisTick; -+ case WATER_CREATURE -> spawnThisTick = spawnWaterThisTick; -+ case UNDERGROUND_WATER_CREATURE -> spawnThisTick = spawnWaterUndergroundCreatureThisTick; -+ case AMBIENT -> spawnThisTick = spawnAmbientThisTick; -+ case WATER_AMBIENT -> spawnThisTick = spawnWaterAmbientThisTick; -+ // Paper end - } - - if (!spawnThisTick || limit == 0) { -@@ -0,0 +0,0 @@ public final class NaturalSpawner { - world.getProfiler().pop(); - } - -+ // Paper start -+ public static int limitForCategory(final ServerLevel world, final MobCategory enumcreaturetype) { -+ return switch (enumcreaturetype) { -+ case MONSTER -> world.getWorld().getMonsterSpawnLimit(); -+ case CREATURE -> world.getWorld().getAnimalSpawnLimit(); -+ case WATER_CREATURE -> world.getWorld().getWaterAnimalSpawnLimit(); -+ case UNDERGROUND_WATER_CREATURE -> world.getWorld().getWaterUndergroundCreatureSpawnLimit(); -+ case AMBIENT -> world.getWorld().getAmbientSpawnLimit(); -+ case WATER_AMBIENT -> world.getWorld().getWaterAmbientSpawnLimit(); -+ default -> enumcreaturetype.getMaxInstancesPerChunk(); -+ }; -+ } -+ -+ public static int globalLimitForCategory(final ServerLevel level, final MobCategory category, final int spawnableChunkCount) { -+ final int categoryLimit = limitForCategory(level, category); -+ if (categoryLimit < 1) { -+ return categoryLimit; -+ } -+ return categoryLimit * spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER; -+ } -+ // Paper end -+ - // Paper start - add parameters and int ret type - public static void spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) { - spawnCategoryForChunk(group, world, chunk, checker, runner); -diff --git a/src/test/java/io/papermc/paper/PaperCommandTest.java b/src/test/java/io/papermc/paper/PaperCommandTest.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/test/java/io/papermc/paper/PaperCommandTest.java -@@ -0,0 +0,0 @@ -+package io.papermc.paper; -+ -+import com.destroystokyo.paper.PaperCommand; -+import java.util.HashSet; -+import java.util.Set; -+import net.minecraft.world.entity.MobCategory; -+import org.junit.Assert; -+import org.junit.Test; -+ -+public class PaperCommandTest { -+ @Test -+ public void testMobCategoryColors() { -+ final Set missing = new HashSet<>(); -+ for (final MobCategory value : MobCategory.values()) { -+ if (!PaperCommand.MOB_CATEGORY_COLORS.containsKey(value)) { -+ missing.add(value.getName()); -+ } -+ } -+ Assert.assertTrue("PaperCommand.MOB_CATEGORY_COLORS map missing TextColors for [" + String.join(", ", missing + "]"), missing.isEmpty()); -+ } -+} diff --git a/patches/unapplied/server/Highly-optimise-single-and-multi-AABB-VoxelShapes-an.patch b/patches/unapplied/server/Highly-optimise-single-and-multi-AABB-VoxelShapes-an.patch deleted file mode 100644 index 6eb0afca78..0000000000 --- a/patches/unapplied/server/Highly-optimise-single-and-multi-AABB-VoxelShapes-an.patch +++ /dev/null @@ -1,1675 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Mon, 4 May 2020 10:06:24 -0700 -Subject: [PATCH] Highly optimise single and multi-AABB VoxelShapes and - collisions - - -diff --git a/src/main/java/io/papermc/paper/util/CachedLists.java b/src/main/java/io/papermc/paper/util/CachedLists.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/io/papermc/paper/util/CachedLists.java -+++ b/src/main/java/io/papermc/paper/util/CachedLists.java -@@ -0,0 +0,0 @@ - package io.papermc.paper.util; - -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.phys.AABB; -+import org.bukkit.Bukkit; -+import org.bukkit.craftbukkit.util.UnsafeList; -+import java.util.List; -+ - public final class CachedLists { - -- public static void reset() { -+ // Paper start - optimise collisions -+ static final UnsafeList TEMP_COLLISION_LIST = new UnsafeList<>(1024); -+ static boolean tempCollisionListInUse; -+ -+ public static UnsafeList getTempCollisionList() { -+ if (!Bukkit.isPrimaryThread() || tempCollisionListInUse) { -+ return new UnsafeList<>(16); -+ } -+ tempCollisionListInUse = true; -+ return TEMP_COLLISION_LIST; -+ } -+ -+ public static void returnTempCollisionList(List list) { -+ if (list != TEMP_COLLISION_LIST) { -+ return; -+ } -+ ((UnsafeList)list).setSize(0); -+ tempCollisionListInUse = false; -+ } - -+ static final UnsafeList TEMP_GET_ENTITIES_LIST = new UnsafeList<>(1024); -+ static boolean tempGetEntitiesListInUse; -+ -+ public static UnsafeList getTempGetEntitiesList() { -+ if (!Bukkit.isPrimaryThread() || tempGetEntitiesListInUse) { -+ return new UnsafeList<>(16); -+ } -+ tempGetEntitiesListInUse = true; -+ return TEMP_GET_ENTITIES_LIST; -+ } -+ -+ public static void returnTempGetEntitiesList(List list) { -+ if (list != TEMP_GET_ENTITIES_LIST) { -+ return; -+ } -+ ((UnsafeList)list).setSize(0); -+ tempGetEntitiesListInUse = false; -+ } -+ // Paper end - optimise collisions -+ -+ public static void reset() { -+ // Paper start - optimise collisions -+ TEMP_COLLISION_LIST.completeReset(); -+ TEMP_GET_ENTITIES_LIST.completeReset(); -+ // Paper end - optimise collisions - } - } -diff --git a/src/main/java/io/papermc/paper/util/CollisionUtil.java b/src/main/java/io/papermc/paper/util/CollisionUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/CollisionUtil.java -@@ -0,0 +0,0 @@ -+package io.papermc.paper.util; -+ -+import io.papermc.paper.voxel.AABBVoxelShape; -+import net.minecraft.core.BlockPos; -+import net.minecraft.server.level.ServerChunkCache; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.WorldGenRegion; -+import net.minecraft.util.Mth; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.item.Item; -+import net.minecraft.world.level.CollisionGetter; -+import net.minecraft.world.level.EntityGetter; -+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.LevelChunkSection; -+import net.minecraft.world.level.material.FlowingFluid; -+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.CollisionContext; -+import net.minecraft.world.phys.shapes.EntityCollisionContext; -+import net.minecraft.world.phys.shapes.Shapes; -+import net.minecraft.world.phys.shapes.VoxelShape; -+import java.util.List; -+import java.util.Optional; -+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 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), false); -+ } -+ -+ /* -+ 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; -+ } -+ -+ public static double collideX(final AABB target, final AABB source, final double source_move) { -+ if (source_move == 0.0) { -+ return 0.0; -+ } -+ -+ 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; -+ } -+ -+ public static double collideY(final AABB target, final AABB source, final double source_move) { -+ if (source_move == 0.0) { -+ return 0.0; -+ } -+ -+ 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; -+ } -+ -+ public static double collideZ(final AABB target, final AABB source, final double source_move) { -+ if (source_move == 0.0) { -+ return 0.0; -+ } -+ -+ 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; -+ } -+ -+ 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, false); -+ } -+ -+ 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, false); -+ } -+ -+ 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, false); -+ } -+ -+ 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, false); -+ } -+ -+ 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, false); -+ } -+ -+ 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, false); -+ } -+ -+ 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, false); -+ } -+ -+ 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, false); -+ } -+ -+ 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, false); -+ } -+ -+ 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, false); -+ } -+ -+ 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, false); -+ } -+ -+ 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, false); -+ } -+ -+ 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, false); -+ } -+ -+ 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, false); -+ } -+ -+ 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, false); -+ } -+ -+ public static double performCollisionsX(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 performCollisionsY(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 performCollisionsZ(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 Vec3 performCollisions(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 = performCollisionsY(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 = performCollisionsZ(axisalignedbb, z, potentialCollisions); -+ if (z != 0.0) { -+ axisalignedbb = offsetZ(axisalignedbb, z); -+ } -+ } -+ -+ if (x != 0.0) { -+ x = performCollisionsX(axisalignedbb, x, potentialCollisions); -+ if (!xSmaller && x != 0.0) { -+ axisalignedbb = offsetX(axisalignedbb, x); -+ } -+ } -+ -+ if (!xSmaller && z != 0.0) { -+ z = performCollisionsZ(axisalignedbb, z, potentialCollisions); -+ } -+ -+ return new Vec3(x, y, z); -+ } -+ -+ public static boolean addBoxesToIfIntersects(final VoxelShape shape, final AABB aabb, final List list) { -+ if (shape instanceof AABBVoxelShape) { -+ final AABBVoxelShape shapeCasted = (AABBVoxelShape)shape; -+ if (voxelShapeIntersect(shapeCasted.aabb, aabb) && !isEmpty(shapeCasted.aabb)) { -+ list.add(shapeCasted.aabb); -+ return true; -+ } -+ return false; -+ } else if (shape instanceof ArrayVoxelShape) { -+ final ArrayVoxelShape shapeCasted = (ArrayVoxelShape)shape; -+ // this can be optimised by checking an "overall shape" first, but not needed -+ -+ final double offX = shapeCasted.getOffsetX(); -+ final double offY = shapeCasted.getOffsetY(); -+ final double offZ = shapeCasted.getOffsetZ(); -+ -+ boolean ret = false; -+ -+ for (final AABB boundingBox : shapeCasted.getBoundingBoxesRepresentation()) { -+ final double minX, minY, minZ, maxX, maxY, maxZ; -+ if (voxelShapeIntersect(aabb, minX = boundingBox.minX + offX, minY = boundingBox.minY + offY, minZ = boundingBox.minZ + offZ, -+ maxX = boundingBox.maxX + offX, maxY = boundingBox.maxY + offY, maxZ = boundingBox.maxZ + offZ) -+ && !isEmpty(minX, minY, minZ, maxX, maxY, maxZ)) { -+ list.add(new AABB(minX, minY, minZ, maxX, maxY, maxZ, false)); -+ ret = true; -+ } -+ } -+ -+ return ret; -+ } else { -+ final List boxes = shape.toAabbs(); -+ -+ boolean ret = false; -+ -+ for (int i = 0, len = boxes.size(); i < len; ++i) { -+ final AABB box = boxes.get(i); -+ if (voxelShapeIntersect(box, aabb) && !isEmpty(box)) { -+ list.add(box); -+ ret = true; -+ } -+ } -+ -+ return ret; -+ } -+ } -+ -+ public static void addBoxesTo(final VoxelShape shape, final List list) { -+ if (shape instanceof AABBVoxelShape) { -+ final AABBVoxelShape shapeCasted = (AABBVoxelShape)shape; -+ if (!isEmpty(shapeCasted.aabb)) { -+ list.add(shapeCasted.aabb); -+ } -+ } else if (shape instanceof ArrayVoxelShape) { -+ final ArrayVoxelShape shapeCasted = (ArrayVoxelShape)shape; -+ -+ final double offX = shapeCasted.getOffsetX(); -+ final double offY = shapeCasted.getOffsetY(); -+ final double offZ = shapeCasted.getOffsetZ(); -+ -+ for (final AABB boundingBox : shapeCasted.getBoundingBoxesRepresentation()) { -+ final AABB box = boundingBox.move(offX, offY, offZ); -+ if (!isEmpty(box)) { -+ list.add(box); -+ } -+ } -+ } else { -+ final List boxes = shape.toAabbs(); -+ for (int i = 0, len = boxes.size(); i < len; ++i) { -+ final AABB box = boxes.get(i); -+ if (!isEmpty(box)) { -+ list.add(box); -+ } -+ } -+ } -+ } -+ -+ public static boolean isAlmostCollidingOnBorder(final WorldBorder worldborder, final AABB boundingBox) { -+ return isAlmostCollidingOnBorder(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); -+ } -+ -+ public static boolean isAlmostCollidingOnBorder(final WorldBorder worldborder, final double boxMinX, final double boxMaxX, -+ final double boxMinZ, final double boxMaxZ) { -+ final double borderMinX = worldborder.getMinX(); // -X -+ final double borderMaxX = worldborder.getMaxX(); // +X -+ -+ final double borderMinZ = worldborder.getMinZ(); // -Z -+ final double borderMaxZ = worldborder.getMaxZ(); // +Z -+ -+ return -+ // Not intersecting if we're smaller -+ !voxelShapeIntersect( -+ boxMinX + COLLISION_EPSILON, Double.NEGATIVE_INFINITY, boxMinZ + COLLISION_EPSILON, -+ boxMaxX - COLLISION_EPSILON, Double.POSITIVE_INFINITY, boxMaxZ - COLLISION_EPSILON, -+ borderMinX, Double.NEGATIVE_INFINITY, borderMinZ, borderMaxX, Double.POSITIVE_INFINITY, borderMaxZ -+ ) -+ && -+ -+ // Are intersecting if we're larger -+ voxelShapeIntersect( -+ boxMinX - COLLISION_EPSILON, Double.NEGATIVE_INFINITY, boxMinZ - COLLISION_EPSILON, -+ boxMaxX + COLLISION_EPSILON, Double.POSITIVE_INFINITY, boxMaxZ + COLLISION_EPSILON, -+ borderMinX, Double.NEGATIVE_INFINITY, borderMinZ, borderMaxX, Double.POSITIVE_INFINITY, borderMaxZ -+ ); -+ } -+ -+ public static boolean isCollidingWithBorderEdge(final WorldBorder worldborder, final AABB boundingBox) { -+ return isCollidingWithBorderEdge(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); -+ } -+ -+ public static boolean isCollidingWithBorderEdge(final WorldBorder worldborder, final double boxMinX, final double boxMaxX, -+ final double boxMinZ, final double boxMaxZ) { -+ final double borderMinX = worldborder.getMinX() + COLLISION_EPSILON; // -X -+ final double borderMaxX = worldborder.getMaxX() - COLLISION_EPSILON; // +X -+ -+ final double borderMinZ = worldborder.getMinZ() + COLLISION_EPSILON; // -Z -+ final double borderMaxZ = worldborder.getMaxZ() - COLLISION_EPSILON; // +Z -+ -+ return boxMinX < borderMinX || boxMaxX > borderMaxX || boxMinZ < borderMinZ || boxMaxZ > borderMaxZ; -+ } -+ -+ public static boolean getCollisionsForBlocksOrWorldBorder(final CollisionGetter getter, final Entity entity, final AABB aabb, -+ final List into, final boolean loadChunks, final boolean collidesWithUnloaded, -+ final boolean checkBorder, final boolean checkOnly, final BiPredicate predicate) { -+ boolean ret = false; -+ -+ if (checkBorder) { -+ if (CollisionUtil.isAlmostCollidingOnBorder(getter.getWorldBorder(), aabb)) { -+ if (checkOnly) { -+ return true; -+ } else { -+ CollisionUtil.addBoxesTo(getter.getWorldBorder().getCollisionShape(), into); -+ ret = true; -+ } -+ } -+ } -+ -+ int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1; -+ int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1; -+ -+ int minBlockY = Mth.floor(aabb.minY - COLLISION_EPSILON) - 1; -+ int maxBlockY = Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1; -+ -+ int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1; -+ int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1; -+ -+ final int minSection = WorldUtil.getMinSection(getter); -+ final int maxSection = WorldUtil.getMaxSection(getter); -+ final int minBlock = minSection << 4; -+ final int maxBlock = (maxSection << 4) | 15; -+ -+ BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); -+ CollisionContext collisionShape = null; -+ -+ // special cases: -+ if (minBlockY > maxBlock || maxBlockY < minBlock) { -+ // no point in checking -+ return ret; -+ } -+ -+ int minYIterate = Math.max(minBlock, minBlockY); -+ int maxYIterate = Math.min(maxBlock, maxBlockY); -+ -+ int minChunkX = minBlockX >> 4; -+ int maxChunkX = maxBlockX >> 4; -+ -+ int minChunkZ = minBlockZ >> 4; -+ int maxChunkZ = maxBlockZ >> 4; -+ -+ ServerChunkCache chunkProvider; -+ if (getter instanceof WorldGenRegion) { -+ chunkProvider = null; -+ } else if (getter instanceof ServerLevel) { -+ chunkProvider = ((ServerLevel)getter).getChunkSource(); -+ } else { -+ chunkProvider = null; -+ } -+ // TODO special case single chunk? -+ -+ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { -+ int minZ = currChunkZ == minChunkZ ? minBlockZ & 15 : 0; // coordinate in chunk -+ int maxZ = currChunkZ == maxChunkZ ? maxBlockZ & 15 : 15; // coordinate in chunk -+ -+ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { -+ int minX = currChunkX == minChunkX ? minBlockX & 15 : 0; // coordinate in chunk -+ int maxX = currChunkX == maxChunkX ? maxBlockX & 15 : 15; // coordinate in chunk -+ -+ int chunkXGlobalPos = currChunkX << 4; -+ int chunkZGlobalPos = currChunkZ << 4; -+ ChunkAccess chunk; -+ if (chunkProvider == null) { -+ chunk = (ChunkAccess)getter.getChunkForCollisions(currChunkX, currChunkZ); -+ } else { -+ chunk = loadChunks ? chunkProvider.getChunk(currChunkX, currChunkZ, true) : chunkProvider.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ); -+ } -+ -+ -+ if (chunk == null) { -+ if (collidesWithUnloaded) { -+ if (checkOnly) { -+ return true; -+ } else { -+ into.add(getBoxForChunk(currChunkX, currChunkZ)); -+ ret = true; -+ } -+ } -+ continue; -+ } -+ -+ LevelChunkSection[] sections = chunk.getSections(); -+ -+ // bound y -+ -+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) { -+ LevelChunkSection section = sections[(currY >> 4) - minSection]; -+ if (section == null || section.isEmpty()) { -+ // empty -+ // skip to next section -+ currY = (currY & ~(15)) + 15; // increment by 15: iterator loop increments by the extra one -+ continue; -+ } -+ -+ net.minecraft.world.level.chunk.PalettedContainer blocks = section.states; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ int localBlockIndex = (currX) | (currZ << 4) | ((currY & 15) << 8); -+ int blockX = currX | chunkXGlobalPos; -+ int blockY = currY; -+ int blockZ = currZ | chunkZGlobalPos; -+ -+ int edgeCount = ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + -+ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + -+ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0); -+ if (edgeCount == 3) { -+ continue; -+ } -+ -+ BlockState blockData = blocks.get(localBlockIndex); -+ if (blockData.isAir()) { -+ continue; -+ } -+ -+ if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) { -+ mutablePos.set(blockX, blockY, blockZ); -+ if (collisionShape == null) { -+ collisionShape = new LazyEntityCollisionContext(entity); -+ } -+ VoxelShape voxelshape2 = blockData.getCollisionShape(getter, mutablePos, collisionShape); -+ if (voxelshape2 != Shapes.empty()) { -+ VoxelShape voxelshape3 = voxelshape2.move((double)blockX, (double)blockY, (double)blockZ); -+ -+ if (predicate != null && !predicate.test(blockData, mutablePos)) { -+ continue; -+ } -+ -+ if (checkOnly) { -+ if (voxelshape3.intersects(aabb)) { -+ return true; -+ } -+ } else { -+ ret |= addBoxesToIfIntersects(voxelshape3, aabb, into); -+ } -+ } -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ return ret; -+ } -+ -+ public static boolean getEntityHardCollisions(final CollisionGetter getter, final Entity entity, AABB aabb, -+ final List into, final boolean checkOnly, final Predicate predicate) { -+ if (isEmpty(aabb) || !(getter instanceof EntityGetter entityGetter)) { -+ return false; -+ } -+ -+ boolean ret = false; -+ -+ // to comply with vanilla intersection rules, expand by -epsilon so 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 = CachedLists.getTempGetEntitiesList(); -+ try { -+ if (entity != null && entity.hardCollides()) { -+ entityGetter.getEntities(entity, aabb, predicate, entities); -+ } else { -+ entityGetter.getHardCollidingEntities(entity, aabb, predicate, entities); -+ } -+ -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ final Entity otherEntity = entities.get(i); -+ -+ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) { -+ if (checkOnly) { -+ return true; -+ } else { -+ into.add(otherEntity.getBoundingBox()); -+ ret = true; -+ } -+ } -+ } -+ } finally { -+ CachedLists.returnTempGetEntitiesList(entities); -+ } -+ -+ return ret; -+ } -+ -+ public static boolean getCollisions(final CollisionGetter view, final Entity entity, final AABB aabb, -+ final List into, final boolean loadChunks, final boolean collidesWithUnloadedChunks, -+ final boolean checkBorder, final boolean checkOnly, final BiPredicate blockPredicate, -+ final Predicate entityPredicate) { -+ if (checkOnly) { -+ return getCollisionsForBlocksOrWorldBorder(view, entity, aabb, into, loadChunks, collidesWithUnloadedChunks, checkBorder, checkOnly, blockPredicate) -+ || getEntityHardCollisions(view, entity, aabb, into, checkOnly, entityPredicate); -+ } else { -+ return getCollisionsForBlocksOrWorldBorder(view, entity, aabb, into, loadChunks, collidesWithUnloadedChunks, checkBorder, checkOnly, blockPredicate) -+ | getEntityHardCollisions(view, entity, aabb, into, checkOnly, entityPredicate); -+ } -+ } -+ -+ public static final class LazyEntityCollisionContext extends EntityCollisionContext { -+ -+ private CollisionContext delegate; -+ private final Entity entity; -+ -+ public LazyEntityCollisionContext(final Entity entity) { -+ super(false, 0.0, null, null, null, Optional.ofNullable(entity)); -+ this.entity = entity; -+ } -+ -+ public CollisionContext getDelegate() { -+ return this.delegate == null ? this.delegate = (this.entity == null ? CollisionContext.empty() : CollisionContext.of(this.entity)) : this.delegate; -+ } -+ -+ @Override -+ public boolean isDescending() { -+ return this.getDelegate().isDescending(); -+ } -+ -+ @Override -+ public boolean isAbove(final VoxelShape shape, final BlockPos pos, final boolean defaultValue) { -+ return this.getDelegate().isAbove(shape, pos, defaultValue); -+ } -+ -+ @Override -+ public boolean hasItemOnFeet(final Item item) { -+ return this.getDelegate().hasItemOnFeet(item); -+ } -+ -+ @Override -+ public boolean isHoldingItem(final Item item) { -+ return this.getDelegate().isHoldingItem(item); -+ } -+ -+ @Override -+ public boolean canStandOnFluid(final FluidState state, final FlowingFluid fluid) { -+ return this.getDelegate().canStandOnFluid(state, fluid); -+ } -+ } -+ -+ private CollisionUtil() { -+ throw new RuntimeException(); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/voxel/AABBVoxelShape.java b/src/main/java/io/papermc/paper/voxel/AABBVoxelShape.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/voxel/AABBVoxelShape.java -@@ -0,0 +0,0 @@ -+package io.papermc.paper.voxel; -+ -+import io.papermc.paper.util.CollisionUtil; -+import it.unimi.dsi.fastutil.doubles.DoubleArrayList; -+import it.unimi.dsi.fastutil.doubles.DoubleList; -+import net.minecraft.core.Direction; -+import net.minecraft.world.phys.AABB; -+import net.minecraft.world.phys.shapes.Shapes; -+import net.minecraft.world.phys.shapes.VoxelShape; -+import java.util.ArrayList; -+import java.util.List; -+ -+public final class AABBVoxelShape extends VoxelShape { -+ -+ public final AABB aabb; -+ -+ public AABBVoxelShape(AABB aabb) { -+ super(Shapes.getFullUnoptimisedCube().shape); -+ this.aabb = aabb; -+ } -+ -+ @Override -+ public boolean isEmpty() { -+ return CollisionUtil.isEmpty(this.aabb); -+ } -+ -+ @Override -+ public double min(Direction.Axis enumdirection_enumaxis) { -+ switch (enumdirection_enumaxis.ordinal()) { -+ case 0: -+ return this.aabb.minX; -+ case 1: -+ return this.aabb.minY; -+ case 2: -+ return this.aabb.minZ; -+ default: -+ throw new IllegalStateException("Unknown axis requested"); -+ } -+ } -+ -+ @Override -+ public double max(Direction.Axis enumdirection_enumaxis) { -+ switch (enumdirection_enumaxis.ordinal()) { -+ case 0: -+ return this.aabb.maxX; -+ case 1: -+ return this.aabb.maxY; -+ case 2: -+ return this.aabb.maxZ; -+ default: -+ throw new IllegalStateException("Unknown axis requested"); -+ } -+ } -+ -+ @Override -+ public AABB bounds() { -+ return this.aabb; -+ } -+ -+ // enum direction axis is from 0 -> 2, so we keep the lower bits for direction axis. -+ @Override -+ protected double get(Direction.Axis enumdirection_enumaxis, int i) { -+ switch (enumdirection_enumaxis.ordinal() | (i << 2)) { -+ case (0 | (0 << 2)): -+ return this.aabb.minX; -+ case (1 | (0 << 2)): -+ return this.aabb.minY; -+ case (2 | (0 << 2)): -+ return this.aabb.minZ; -+ case (0 | (1 << 2)): -+ return this.aabb.maxX; -+ case (1 | (1 << 2)): -+ return this.aabb.maxY; -+ case (2 | (1 << 2)): -+ return this.aabb.maxZ; -+ default: -+ throw new IllegalStateException("Unknown axis requested"); -+ } -+ } -+ -+ private DoubleList cachedListX; -+ private DoubleList cachedListY; -+ private DoubleList cachedListZ; -+ -+ @Override -+ protected DoubleList getCoords(Direction.Axis enumdirection_enumaxis) { -+ switch (enumdirection_enumaxis.ordinal()) { -+ case 0: -+ return this.cachedListX == null ? this.cachedListX = DoubleArrayList.wrap(new double[] { this.aabb.minX, this.aabb.maxX }) : this.cachedListX; -+ case 1: -+ return this.cachedListY == null ? this.cachedListY = DoubleArrayList.wrap(new double[] { this.aabb.minY, this.aabb.maxY }) : this.cachedListY; -+ case 2: -+ return this.cachedListZ == null ? this.cachedListZ = DoubleArrayList.wrap(new double[] { this.aabb.minZ, this.aabb.maxZ }) : this.cachedListZ; -+ default: -+ throw new IllegalStateException("Unknown axis requested"); -+ } -+ } -+ -+ @Override -+ public VoxelShape move(double d0, double d1, double d2) { -+ return new AABBVoxelShape(this.aabb.move(d0, d1, d2)); -+ } -+ -+ @Override -+ public VoxelShape optimize() { -+ if (this.isEmpty()) { -+ return Shapes.empty(); -+ } else if (this == Shapes.BLOCK_OPTIMISED || this.aabb.equals(Shapes.BLOCK_OPTIMISED.aabb)) { -+ return Shapes.BLOCK_OPTIMISED; -+ } -+ return this; -+ } -+ -+ @Override -+ public void forAllBoxes(Shapes.DoubleLineConsumer voxelshapes_a) { -+ voxelshapes_a.consume(this.aabb.minX, this.aabb.minY, this.aabb.minZ, this.aabb.maxX, this.aabb.maxY, this.aabb.maxZ); -+ } -+ -+ @Override -+ public List toAabbs() { // getAABBs -+ List ret = new ArrayList<>(1); -+ ret.add(this.aabb); -+ return ret; -+ } -+ -+ @Override -+ protected int findIndex(Direction.Axis enumdirection_enumaxis, double d0) { // findPointIndexAfterOffset -+ switch (enumdirection_enumaxis.ordinal()) { -+ case 0: -+ return d0 < this.aabb.maxX ? (d0 < this.aabb.minX ? -1 : 0) : 1; -+ case 1: -+ return d0 < this.aabb.maxY ? (d0 < this.aabb.minY ? -1 : 0) : 1; -+ case 2: -+ return d0 < this.aabb.maxZ ? (d0 < this.aabb.minZ ? -1 : 0) : 1; -+ default: -+ throw new IllegalStateException("Unknown axis requested"); -+ } -+ } -+ -+ @Override -+ protected VoxelShape calculateFace(Direction direction) { -+ if (this.isEmpty()) { -+ return Shapes.empty(); -+ } -+ if (this == Shapes.BLOCK_OPTIMISED) { -+ return this; -+ } -+ switch (direction) { -+ case EAST: // +X -+ case WEST: { // -X -+ final double from = direction == Direction.EAST ? 1.0 - CollisionUtil.COLLISION_EPSILON : CollisionUtil.COLLISION_EPSILON; -+ if (from > this.aabb.maxX || this.aabb.minX > from) { -+ return Shapes.empty(); -+ } -+ return new AABBVoxelShape(new AABB(0.0, this.aabb.minY, this.aabb.minZ, 1.0, this.aabb.maxY, this.aabb.maxZ)).optimize(); -+ } -+ case UP: // +Y -+ case DOWN: { // -Y -+ final double from = direction == Direction.UP ? 1.0 - CollisionUtil.COLLISION_EPSILON : CollisionUtil.COLLISION_EPSILON; -+ if (from > this.aabb.maxY || this.aabb.minY > from) { -+ return Shapes.empty(); -+ } -+ return new AABBVoxelShape(new AABB(this.aabb.minX, 0.0, this.aabb.minZ, this.aabb.maxX, 1.0, this.aabb.maxZ)).optimize(); -+ } -+ case SOUTH: // +Z -+ case NORTH: { // -Z -+ final double from = direction == Direction.SOUTH ? 1.0 - CollisionUtil.COLLISION_EPSILON : CollisionUtil.COLLISION_EPSILON; -+ if (from > this.aabb.maxZ || this.aabb.minZ > from) { -+ return Shapes.empty(); -+ } -+ return new AABBVoxelShape(new AABB(this.aabb.minX, this.aabb.minY, 0.0, this.aabb.maxX, this.aabb.maxY, 1.0)).optimize(); -+ } -+ default: { -+ throw new IllegalStateException("Unknown axis requested"); -+ } -+ } -+ } -+ -+ @Override -+ public double collide(Direction.Axis enumdirection_enumaxis, AABB axisalignedbb, double d0) { -+ if (CollisionUtil.isEmpty(this.aabb) || CollisionUtil.isEmpty(axisalignedbb)) { -+ return d0; -+ } -+ switch (enumdirection_enumaxis.ordinal()) { -+ case 0: -+ return CollisionUtil.collideX(this.aabb, axisalignedbb, d0); -+ case 1: -+ return CollisionUtil.collideY(this.aabb, axisalignedbb, d0); -+ case 2: -+ return CollisionUtil.collideZ(this.aabb, axisalignedbb, d0); -+ default: -+ throw new IllegalStateException("Unknown axis requested"); -+ } -+ } -+ -+ @Override -+ public boolean intersects(AABB axisalingedbb) { -+ return CollisionUtil.voxelShapeIntersect(this.aabb, axisalingedbb); -+ } -+} -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -0,0 +0,0 @@ public class ServerPlayer extends Player { - - if (blockposition1 != null) { - this.moveTo(blockposition1, 0.0F, 0.0F); -- if (world.noCollision(this)) { -+ if (world.noCollision(this, this.getBoundingBox(), null, true)) { // Paper - make sure this loads chunks, we default to NOT loading now - break; - } - } -@@ -0,0 +0,0 @@ public class ServerPlayer extends Player { - } else { - this.moveTo(blockposition, 0.0F, 0.0F); - -- while (!world.noCollision(this) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { -+ while (!world.noCollision(this, this.getBoundingBox(), null, true) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { // Paper - make sure this loads chunks, we default to NOT loading now - this.setPos(this.getX(), this.getY() + 1.0D, this.getZ()); - } - } -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -0,0 +0,0 @@ public abstract class PlayerList { - - worldserver1.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper - entityplayer1.forceCheckHighPriority(); // Player - Chunk priority -- while (avoidSuffocation && !worldserver1.noCollision(entityplayer1) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { -+ while (avoidSuffocation && !worldserver1.noCollision(entityplayer1, entityplayer1.getBoundingBox(), null, true) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { // Paper - make sure this loads chunks, we default to NOT loading now - entityplayer1.setPos(entityplayer1.getX(), entityplayer1.getY() + 1.0D, entityplayer1.getZ()); - } - // CraftBukkit start -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n - float f2 = this.getBlockSpeedFactor(); - - this.setDeltaMovement(this.getDeltaMovement().multiply((double) f2, 1.0D, (double) f2)); -- if (this.level.getBlockStatesIfLoaded(this.getBoundingBox().deflate(1.0E-6D)).noneMatch((iblockdata1) -> { -- return iblockdata1.is((Tag) BlockTags.FIRE) || iblockdata1.is(Blocks.LAVA); -- })) { -+ // Paper start - remove expensive streams from here -+ boolean noneMatch = true; -+ AABB fireSearchBox = this.getBoundingBox().deflate(1.0E-6D); -+ { -+ int minX = Mth.floor(fireSearchBox.minX); -+ int minY = Mth.floor(fireSearchBox.minY); -+ int minZ = Mth.floor(fireSearchBox.minZ); -+ int maxX = Mth.floor(fireSearchBox.maxX); -+ int maxY = Mth.floor(fireSearchBox.maxY); -+ int maxZ = Mth.floor(fireSearchBox.maxZ); -+ fire_search_loop: -+ for (int fz = minZ; fz <= maxZ; ++fz) { -+ for (int fx = minX; fx <= maxX; ++fx) { -+ for (int fy = minY; fy <= maxY; ++fy) { -+ net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)this.level.getChunkIfLoadedImmediately(fx >> 4, fz >> 4); -+ if (chunk == null) { -+ // Vanilla rets an empty stream if all the chunks are not loaded, so noneMatch will be true -+ // even if we're in lava/fire -+ noneMatch = true; -+ break fire_search_loop; -+ } -+ if (!noneMatch) { -+ // don't do get type, we already know we're in fire - we just need to check the chunks -+ // loaded state -+ continue; -+ } -+ -+ BlockState type = chunk.getType(fx, fy, fz); -+ if (type.is((Tag) BlockTags.FIRE) || type.is(Blocks.LAVA)) { -+ noneMatch = false; -+ // can't break, we need to retain vanilla behavior by ensuring ALL chunks are loaded -+ } -+ } -+ } -+ } -+ } -+ if (noneMatch) { -+ // Paper end - remove expensive streams from here - if (this.remainingFireTicks <= 0) { - this.setRemainingFireTicks(-this.getFireImmuneTicks()); - } -@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n - return offsetFactor; - } - -- private Vec3 collide(Vec3 movement) { -- AABB axisalignedbb = this.getBoundingBox(); -- CollisionContext voxelshapecollision = CollisionContext.of(this); -- VoxelShape voxelshape = this.level.getWorldBorder().getCollisionShape(); -- Stream stream = !this.level.getWorldBorder().isWithinBounds(axisalignedbb) ? Stream.empty() : Stream.of(voxelshape); // Paper -- Stream stream1 = this.level.getEntityCollisions(this, axisalignedbb.expandTowards(movement), (entity) -> { -- return true; -- }); -- RewindableStream streamaccumulator = new RewindableStream<>(Stream.concat(stream1, stream)); -- Vec3 vec3d1 = movement.lengthSqr() == 0.0D ? movement : Entity.collideBoundingBoxHeuristically(this, movement, axisalignedbb, this.level, voxelshapecollision, streamaccumulator); -- boolean flag = movement.x != vec3d1.x; -- boolean flag1 = movement.y != vec3d1.y; -- boolean flag2 = movement.z != vec3d1.z; -- boolean flag3 = this.onGround || flag1 && movement.y < 0.0D; -- -- if (this.maxUpStep > 0.0F && flag3 && (flag || flag2)) { -- Vec3 vec3d2 = Entity.collideBoundingBoxHeuristically(this, new Vec3(movement.x, (double) this.maxUpStep, movement.z), axisalignedbb, this.level, voxelshapecollision, streamaccumulator); -- Vec3 vec3d3 = Entity.collideBoundingBoxHeuristically(this, new Vec3(0.0D, (double) this.maxUpStep, 0.0D), axisalignedbb.expandTowards(movement.x, 0.0D, movement.z), this.level, voxelshapecollision, streamaccumulator); -- -- if (vec3d3.y < (double) this.maxUpStep) { -- Vec3 vec3d4 = Entity.collideBoundingBoxHeuristically(this, new Vec3(movement.x, 0.0D, movement.z), axisalignedbb.move(vec3d3), this.level, voxelshapecollision, streamaccumulator).add(vec3d3); -- -- if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) { -- vec3d2 = vec3d4; -+ private Vec3 collide(Vec3 moveVector) { -+ // Paper start - optimise collisions -+ // This is a copy of vanilla's except that it uses strictly AABB math -+ if (moveVector.x == 0.0 && moveVector.y == 0.0 && moveVector.z == 0.0) { -+ return moveVector; -+ } -+ -+ final Level world = this.level; -+ final AABB currBoundingBox = this.getBoundingBox(); -+ -+ if (io.papermc.paper.util.CollisionUtil.isEmpty(currBoundingBox)) { -+ return moveVector; -+ } -+ -+ final List potentialCollisions = io.papermc.paper.util.CachedLists.getTempCollisionList(); -+ try { -+ final double stepHeight = (double)this.maxUpStep; -+ final AABB collisionBox; -+ -+ if (moveVector.x == 0.0 && moveVector.z == 0.0 && moveVector.y != 0.0) { -+ if (moveVector.y > 0.0) { -+ collisionBox = io.papermc.paper.util.CollisionUtil.cutUpwards(currBoundingBox, moveVector.y); -+ } else { -+ collisionBox = io.papermc.paper.util.CollisionUtil.cutDownwards(currBoundingBox, moveVector.y); -+ } -+ } else { -+ if (stepHeight > 0.0 && (this.onGround || (moveVector.y < 0.0)) && (moveVector.x != 0.0 || moveVector.z != 0.0)) { -+ // don't bother getting the collisions if we don't need them. -+ if (moveVector.y <= 0.0) { -+ collisionBox = io.papermc.paper.util.CollisionUtil.expandUpwards(currBoundingBox.expandTowards(moveVector.x, moveVector.y, moveVector.z), stepHeight); -+ } else { -+ collisionBox = currBoundingBox.expandTowards(moveVector.x, Math.max(stepHeight, moveVector.y), moveVector.z); -+ } -+ } else { -+ collisionBox = currBoundingBox.expandTowards(moveVector.x, moveVector.y, moveVector.z); - } - } - -- if (vec3d2.horizontalDistanceSqr() > vec3d1.horizontalDistanceSqr()) { -- return vec3d2.add(Entity.collideBoundingBoxHeuristically(this, new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), axisalignedbb.move(vec3d2), this.level, voxelshapecollision, streamaccumulator)); -+ io.papermc.paper.util.CollisionUtil.getCollisions(world, this, collisionBox, potentialCollisions, false, true, -+ false, false, null, null); -+ -+ if (io.papermc.paper.util.CollisionUtil.isCollidingWithBorderEdge(world.getWorldBorder(), collisionBox)) { -+ io.papermc.paper.util.CollisionUtil.addBoxesToIfIntersects(world.getWorldBorder().getCollisionShape(), collisionBox, potentialCollisions); - } -- } - -- return vec3d1; -+ final Vec3 limitedMoveVector = io.papermc.paper.util.CollisionUtil.performCollisions(moveVector, currBoundingBox, potentialCollisions); -+ -+ if (stepHeight > 0.0 -+ && (this.onGround || (limitedMoveVector.y != moveVector.y && moveVector.y < 0.0)) -+ && (limitedMoveVector.x != moveVector.x || limitedMoveVector.z != moveVector.z)) { -+ Vec3 vec3d2 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(moveVector.x, stepHeight, moveVector.z), currBoundingBox, potentialCollisions); -+ final Vec3 vec3d3 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(0.0, stepHeight, 0.0), currBoundingBox.expandTowards(moveVector.x, 0.0, moveVector.z), potentialCollisions); -+ -+ if (vec3d3.y < stepHeight) { -+ final Vec3 vec3d4 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(moveVector.x, 0.0D, moveVector.z), currBoundingBox.move(vec3d3), potentialCollisions).add(vec3d3); -+ -+ if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) { -+ vec3d2 = vec3d4; -+ } -+ } -+ -+ if (vec3d2.horizontalDistanceSqr() > limitedMoveVector.horizontalDistanceSqr()) { -+ return vec3d2.add(io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(0.0D, -vec3d2.y + moveVector.y, 0.0D), currBoundingBox.move(vec3d2), potentialCollisions)); -+ } -+ -+ return limitedMoveVector; -+ } else { -+ return limitedMoveVector; -+ } -+ } finally { -+ io.papermc.paper.util.CachedLists.returnTempCollisionList(potentialCollisions); -+ } -+ // Paper end - optimise collisions - } - - public static Vec3 collideBoundingBoxHeuristically(@Nullable Entity entity, Vec3 movement, AABB entityBoundingBox, Level world, CollisionContext context, RewindableStream collisions) { -@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n - float f = this.dimensions.width * 0.8F; - AABB axisalignedbb = AABB.ofSize(this.getEyePosition(), (double) f, 1.0E-6D, (double) f); - -- return this.level.getBlockCollisions(this, axisalignedbb, (iblockdata, blockposition) -> { -- return iblockdata.isSuffocating(this.level, blockposition); -- }).findAny().isPresent(); -+ // Paper start -+ return io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this.level, this, axisalignedbb, null, -+ false, false, false, true, (iblockdata, blockposition) -> { -+ return iblockdata.isSuffocating(this.level, blockposition); -+ }); -+ // Paper end - } - } - -diff --git a/src/main/java/net/minecraft/world/level/CollisionGetter.java b/src/main/java/net/minecraft/world/level/CollisionGetter.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/CollisionGetter.java -+++ b/src/main/java/net/minecraft/world/level/CollisionGetter.java -@@ -0,0 +0,0 @@ public interface CollisionGetter extends BlockGetter { - return this.isUnobstructed(entity, Shapes.create(entity.getBoundingBox())); - } - -+ // Paper start - optimise collisions -+ default boolean noCollision(Entity entity, AABB box, Predicate filter, boolean loadChunks) { -+ return !io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, loadChunks, false, entity != null, true, null) -+ && !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, true, filter); -+ } -+ // Paper end - optimise collisions -+ - default boolean noCollision(AABB box) { -- return this.noCollision((Entity)null, box, (e) -> { -- return true; -- }); -+ // Paper start - optimise collisions -+ return !io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, null, box, null, false, false, false, true, null) -+ && !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, null, box, null, true, null); -+ // Paper end - optimise collisions - } - - default boolean noCollision(Entity entity) { -- return this.noCollision(entity, entity.getBoundingBox(), (e) -> { -- return true; -- }); -+ // Paper start - optimise collisions -+ AABB box = entity.getBoundingBox(); -+ return !io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, false, false, entity != null, true, null) -+ && !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, true, null); -+ // Paper end - optimise collisions - } - - default boolean noCollision(Entity entity, AABB box) { -- return this.noCollision(entity, box, (e) -> { -- return true; -- }); -+ // Paper start - optimise collisions -+ return !io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, false, false, entity != null, true, null) -+ && !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, true, null); -+ // Paper end - optimise collisions - } - - default boolean noCollision(@Nullable Entity entity, AABB box, Predicate filter) { -- try { if (entity != null) entity.collisionLoadChunks = true; // Paper -- return this.getCollisions(entity, box, filter).allMatch(VoxelShape::isEmpty); -- } finally { if (entity != null) entity.collisionLoadChunks = false; } // Paper -+ // Paper start - optimise collisions -+ return !io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, false, false, entity != null, true, null) -+ && !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, true, filter); -+ // Paper end - optimise collisions - } - - Stream getEntityCollisions(@Nullable Entity entity, AABB box, Predicate predicate); -diff --git a/src/main/java/net/minecraft/world/level/CollisionSpliterator.java b/src/main/java/net/minecraft/world/level/CollisionSpliterator.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/CollisionSpliterator.java -+++ b/src/main/java/net/minecraft/world/level/CollisionSpliterator.java -@@ -0,0 +0,0 @@ public class CollisionSpliterator extends AbstractSpliterator { - - VoxelShape voxelShape = blockState.getCollisionShape(this.collisionGetter, this.pos, this.context); - if (voxelShape == Shapes.block()) { -- if (!this.box.intersects((double)i, (double)j, (double)k, (double)i + 1.0D, (double)j + 1.0D, (double)k + 1.0D)) { -+ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(this.box, (double)i, (double)j, (double)k, (double)i + 1.0D, (double)j + 1.0D, (double)k + 1.0D)) { // Paper - keep vanilla behavior for voxelshape intersection - See comment in CollisionUtil - continue; - } - -diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/EntityGetter.java -+++ b/src/main/java/net/minecraft/world/level/EntityGetter.java -@@ -0,0 +0,0 @@ public interface EntityGetter { - return true; - } else { - for(Entity entity2 : this.getEntities(entity, shape.bounds())) { -- if (!entity2.isRemoved() && entity2.blocksBuilding && (entity == null || !entity2.isPassengerOfSameVehicle(entity)) && Shapes.joinIsNotEmpty(shape, Shapes.create(entity2.getBoundingBox()), BooleanOp.AND)) { -+ if (!entity2.isRemoved() && entity2.blocksBuilding && (entity == null || !entity2.isPassengerOfSameVehicle(entity)) && shape.intersects(entity2.getBoundingBox())) { // Paper - return false; - } - } -@@ -0,0 +0,0 @@ public interface EntityGetter { - if (box.getSize() < 1.0E-7D) { - return Stream.empty(); - } else { -- AABB aABB = box.inflate(1.0E-7D); -+ AABB aABB = box.inflate(-1.0E-7D); // Paper - needs to be negated, or else we get things we don't collide with - Predicate hardCollides = (entityx) -> { // Paper - optimise entity hard collisions - if (true || entityx.getBoundingBox().intersects(aABB)) { // Paper - always true - if (entity == null) { -diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -@@ -0,0 +0,0 @@ public abstract class BlockBehaviour { - } - this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here - this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - cache opacity for light -- -+ // TODO optimise light - } - - public Block getBlock() { -diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/phys/AABB.java -+++ b/src/main/java/net/minecraft/world/phys/AABB.java -@@ -0,0 +0,0 @@ public class AABB { - this.maxZ = Math.max(z1, z2); - } - -+ // Paper start -+ public AABB(double minX, double minY, double minZ, double maxX, double maxY, double maxZ, boolean dummy) { -+ this.minX = minX; -+ this.minY = minY; -+ this.minZ = minZ; -+ this.maxX = maxX; -+ this.maxY = maxY; -+ this.maxZ = maxZ; -+ } -+ // Paper end -+ - public AABB(BlockPos pos) { - this((double)pos.getX(), (double)pos.getY(), (double)pos.getZ(), (double)(pos.getX() + 1), (double)(pos.getY() + 1), (double)(pos.getZ() + 1)); - } -diff --git a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java -@@ -0,0 +0,0 @@ import java.util.Arrays; - import net.minecraft.Util; - import net.minecraft.core.Direction; - -+// Paper start -+import it.unimi.dsi.fastutil.doubles.AbstractDoubleList; -+// Paper end - public class ArrayVoxelShape extends VoxelShape { - private final DoubleList xs; - private final DoubleList ys; -@@ -0,0 +0,0 @@ public class ArrayVoxelShape extends VoxelShape { - } - - ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) { -+ // Paper start - optimise multi-aabb shapes -+ this(shape, xPoints, yPoints, zPoints, null, 0.0, 0.0, 0.0); -+ } -+ ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints, net.minecraft.world.phys.AABB[] boundingBoxesRepresentation, double offsetX, double offsetY, double offsetZ) { -+ // Paper end - optimise multi-aabb shapes - super(shape); - int i = shape.getXSize() + 1; - int j = shape.getYSize() + 1; -@@ -0,0 +0,0 @@ public class ArrayVoxelShape extends VoxelShape { - } else { - throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException("Lengths of point arrays must be consistent with the size of the VoxelShape.")); - } -+ // Paper start - optimise multi-aabb shapes -+ this.boundingBoxesRepresentation = boundingBoxesRepresentation == null ? this.toAabbs().toArray(EMPTY) : boundingBoxesRepresentation; -+ this.offsetX = offsetX; -+ this.offsetY = offsetY; -+ this.offsetZ = offsetZ; -+ // Paper end - optimise multi-aabb shapes - } - - @Override -@@ -0,0 +0,0 @@ public class ArrayVoxelShape extends VoxelShape { - throw new IllegalArgumentException(); - } - } -+ -+ // Paper start -+ public static final class DoubleListOffsetExposed extends AbstractDoubleList { -+ -+ public final DoubleArrayList list; -+ public final double offset; -+ -+ public DoubleListOffsetExposed(final DoubleArrayList list, final double offset) { -+ this.list = list; -+ this.offset = offset; -+ } -+ -+ @Override -+ public double getDouble(final int index) { -+ return this.list.getDouble(index) + this.offset; -+ } -+ -+ @Override -+ public int size() { -+ return this.list.size(); -+ } -+ } -+ -+ static final net.minecraft.world.phys.AABB[] EMPTY = new net.minecraft.world.phys.AABB[0]; -+ final net.minecraft.world.phys.AABB[] boundingBoxesRepresentation; -+ -+ final double offsetX; -+ final double offsetY; -+ final double offsetZ; -+ -+ public final net.minecraft.world.phys.AABB[] getBoundingBoxesRepresentation() { -+ return this.boundingBoxesRepresentation; -+ } -+ -+ public final double getOffsetX() { -+ return this.offsetX; -+ } -+ -+ public final double getOffsetY() { -+ return this.offsetY; -+ } -+ -+ public final double getOffsetZ() { -+ return this.offsetZ; -+ } -+ -+ @Override -+ public java.util.List toAabbs() { -+ if (this.boundingBoxesRepresentation == null) { -+ return super.toAabbs(); -+ } -+ java.util.List ret = new java.util.ArrayList<>(this.boundingBoxesRepresentation.length); -+ -+ double offX = this.offsetX; -+ double offY = this.offsetY; -+ double offZ = this.offsetZ; -+ -+ for (net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { -+ ret.add(boundingBox.move(offX, offY, offZ)); -+ } -+ -+ return ret; -+ } -+ -+ protected static DoubleArrayList getList(DoubleList from) { -+ if (from instanceof DoubleArrayList) { -+ return (DoubleArrayList)from; -+ } else { -+ return DoubleArrayList.wrap(from.toDoubleArray()); -+ } -+ } -+ -+ @Override -+ public VoxelShape move(double x, double y, double z) { -+ if (x == 0.0 && y == 0.0 && z == 0.0) { -+ return this; -+ } -+ DoubleListOffsetExposed xPoints, yPoints, zPoints; -+ double offsetX, offsetY, offsetZ; -+ -+ if (this.xs instanceof DoubleListOffsetExposed) { -+ xPoints = new DoubleListOffsetExposed(((DoubleListOffsetExposed)this.xs).list, offsetX = this.offsetX + x); -+ yPoints = new DoubleListOffsetExposed(((DoubleListOffsetExposed)this.ys).list, offsetY = this.offsetY + y); -+ zPoints = new DoubleListOffsetExposed(((DoubleListOffsetExposed)this.zs).list, offsetZ = this.offsetZ + z); -+ } else { -+ xPoints = new DoubleListOffsetExposed(getList(this.xs), offsetX = x); -+ yPoints = new DoubleListOffsetExposed(getList(this.ys), offsetY = y); -+ zPoints = new DoubleListOffsetExposed(getList(this.zs), offsetZ = z); -+ } -+ -+ return new ArrayVoxelShape(this.shape, xPoints, yPoints, zPoints, this.boundingBoxesRepresentation, offsetX, offsetY, offsetZ); -+ } -+ -+ @Override -+ public final boolean intersects(net.minecraft.world.phys.AABB axisalingedbb) { -+ // this can be optimised by checking an "overall shape" first, but not needed -+ double offX = this.offsetX; -+ double offY = this.offsetY; -+ double offZ = this.offsetZ; -+ -+ for (net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { -+ if (io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(axisalingedbb, boundingBox.minX + offX, boundingBox.minY + offY, boundingBox.minZ + offZ, -+ boundingBox.maxX + offX, boundingBox.maxY + offY, boundingBox.maxZ + offZ)) { -+ return true; -+ } -+ } -+ -+ return false; -+ } -+ -+ @Override -+ public void forAllBoxes(Shapes.DoubleLineConsumer doubleLineConsumer) { -+ if (this.boundingBoxesRepresentation == null) { -+ super.forAllBoxes(doubleLineConsumer); -+ return; -+ } -+ for (final net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { -+ doubleLineConsumer.consume(boundingBox.minX + this.offsetX, boundingBox.minY + this.offsetY, boundingBox.minZ + this.offsetZ, -+ boundingBox.maxX + this.offsetX, boundingBox.maxY + this.offsetY, boundingBox.maxZ + this.offsetZ); -+ } -+ } -+ -+ @Override -+ public VoxelShape optimize() { -+ if (this == Shapes.empty() || this.boundingBoxesRepresentation.length == 0) { -+ return this; -+ } -+ -+ VoxelShape simplified = Shapes.empty(); -+ for (final net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { -+ simplified = Shapes.joinUnoptimized(simplified, Shapes.box(boundingBox.minX + this.offsetX, boundingBox.minY + this.offsetY, boundingBox.minZ + this.offsetZ, -+ boundingBox.maxX + this.offsetX, boundingBox.maxY + this.offsetY, boundingBox.maxZ + this.offsetZ), BooleanOp.OR); -+ } -+ -+ if (!(simplified instanceof ArrayVoxelShape)) { -+ return simplified; -+ } -+ -+ final net.minecraft.world.phys.AABB[] boundingBoxesRepresentation = ((ArrayVoxelShape)simplified).getBoundingBoxesRepresentation(); -+ -+ if (boundingBoxesRepresentation.length == 1) { -+ return new io.papermc.paper.voxel.AABBVoxelShape(boundingBoxesRepresentation[0]).optimize(); -+ } -+ -+ return simplified; -+ } -+ // Paper end -+ - } -diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java -@@ -0,0 +0,0 @@ public final class Shapes { - DiscreteVoxelShape discreteVoxelShape = new BitSetDiscreteVoxelShape(1, 1, 1); - discreteVoxelShape.fill(0, 0, 0); - return new CubeVoxelShape(discreteVoxelShape); -- }); -+ }); public static VoxelShape getFullUnoptimisedCube() { return BLOCK; } // Paper - OBFHELPER - public static final VoxelShape INFINITY = box(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); - private static final VoxelShape EMPTY = new ArrayVoxelShape(new BitSetDiscreteVoxelShape(0, 0, 0), (DoubleList)(new DoubleArrayList(new double[]{0.0D})), (DoubleList)(new DoubleArrayList(new double[]{0.0D})), (DoubleList)(new DoubleArrayList(new double[]{0.0D}))); -+ public static final io.papermc.paper.voxel.AABBVoxelShape BLOCK_OPTIMISED = new io.papermc.paper.voxel.AABBVoxelShape(new AABB(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)); // Paper - - public static VoxelShape empty() { - return EMPTY; - } - - public static VoxelShape block() { -- return BLOCK; -+ return BLOCK_OPTIMISED; // Paper - } - - public static VoxelShape box(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { -@@ -0,0 +0,0 @@ public final class Shapes { - } - - public static VoxelShape create(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { -- if (!(maxX - minX < 1.0E-7D) && !(maxY - minY < 1.0E-7D) && !(maxZ - minZ < 1.0E-7D)) { -- int i = findBits(minX, maxX); -- int j = findBits(minY, maxY); -- int k = findBits(minZ, maxZ); -- if (i >= 0 && j >= 0 && k >= 0) { -- if (i == 0 && j == 0 && k == 0) { -- return block(); -- } else { -- int l = 1 << i; -- int m = 1 << j; -- int n = 1 << k; -- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.withFilledBounds(l, m, n, (int)Math.round(minX * (double)l), (int)Math.round(minY * (double)m), (int)Math.round(minZ * (double)n), (int)Math.round(maxX * (double)l), (int)Math.round(maxY * (double)m), (int)Math.round(maxZ * (double)n)); -- return new CubeVoxelShape(bitSetDiscreteVoxelShape); -- } -- } else { -- return new ArrayVoxelShape(BLOCK.shape, (DoubleList)DoubleArrayList.wrap(new double[]{minX, maxX}), (DoubleList)DoubleArrayList.wrap(new double[]{minY, maxY}), (DoubleList)DoubleArrayList.wrap(new double[]{minZ, maxZ})); -- } -- } else { -- return empty(); -- } -+ return new io.papermc.paper.voxel.AABBVoxelShape(new AABB(minX, minY, minZ, maxX, maxY, maxZ)); // Paper - } - - public static VoxelShape create(AABB box) { -- return create(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ); -+ return new io.papermc.paper.voxel.AABBVoxelShape(box); // Paper - } - - @VisibleForTesting -@@ -0,0 +0,0 @@ public final class Shapes { - } - - public static boolean joinIsNotEmpty(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) { -+ // Paper start - optimise voxelshape -+ if (predicate == BooleanOp.AND) { -+ if (shape1 instanceof io.papermc.paper.voxel.AABBVoxelShape && shape2 instanceof io.papermc.paper.voxel.AABBVoxelShape) { -+ return io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(((io.papermc.paper.voxel.AABBVoxelShape)shape1).aabb, ((io.papermc.paper.voxel.AABBVoxelShape)shape2).aabb); -+ } else if (shape1 instanceof io.papermc.paper.voxel.AABBVoxelShape && shape2 instanceof ArrayVoxelShape) { -+ return ((ArrayVoxelShape)shape2).intersects(((io.papermc.paper.voxel.AABBVoxelShape)shape1).aabb); -+ } else if (shape2 instanceof io.papermc.paper.voxel.AABBVoxelShape && shape1 instanceof ArrayVoxelShape) { -+ return ((ArrayVoxelShape)shape1).intersects(((io.papermc.paper.voxel.AABBVoxelShape)shape2).aabb); -+ } -+ } -+ return joinIsNotEmptyVanilla(shape1, shape2, predicate); -+ } -+ public static boolean joinIsNotEmptyVanilla(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) { -+ // Paper end - optimise voxelshape - if (predicate.apply(false, false)) { - throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException()); - } else { -@@ -0,0 +0,0 @@ public final class Shapes { - } - - public static VoxelShape getFaceShape(VoxelShape shape, Direction direction) { -+ // Paper start - optimise shape creation here for lighting, as this shape is going to be used -+ // for transparency checks -+ if (shape == BLOCK || shape == BLOCK_OPTIMISED) { -+ return BLOCK_OPTIMISED; -+ } else if (shape == empty()) { -+ return empty(); -+ } -+ -+ if (shape instanceof io.papermc.paper.voxel.AABBVoxelShape) { -+ final AABB box = ((io.papermc.paper.voxel.AABBVoxelShape)shape).aabb; -+ switch (direction) { -+ case WEST: // -X -+ case EAST: { // +X -+ final boolean useEmpty = direction == Direction.EAST ? !DoubleMath.fuzzyEquals(box.maxX, 1.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) : -+ !DoubleMath.fuzzyEquals(box.minX, 0.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON); -+ return useEmpty ? empty() : new io.papermc.paper.voxel.AABBVoxelShape(new AABB(0.0, box.minY, box.minZ, 1.0, box.maxY, box.maxZ)).optimize(); -+ } -+ case DOWN: // -Y -+ case UP: { // +Y -+ final boolean useEmpty = direction == Direction.UP ? !DoubleMath.fuzzyEquals(box.maxY, 1.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) : -+ !DoubleMath.fuzzyEquals(box.minY, 0.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON); -+ return useEmpty ? empty() : new io.papermc.paper.voxel.AABBVoxelShape(new AABB(box.minX, 0.0, box.minZ, box.maxX, 1.0, box.maxZ)).optimize(); -+ } -+ case NORTH: // -Z -+ case SOUTH: { // +Z -+ final boolean useEmpty = direction == Direction.SOUTH ? !DoubleMath.fuzzyEquals(box.maxZ, 1.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) : -+ !DoubleMath.fuzzyEquals(box.minZ,0.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON); -+ return useEmpty ? empty() : new io.papermc.paper.voxel.AABBVoxelShape(new AABB(box.minX, box.minY, 0.0, box.maxX, box.maxY, 1.0)).optimize(); -+ } -+ } -+ } -+ -+ // fall back to vanilla -+ return getFaceShapeVanilla(shape, direction); -+ } -+ public static VoxelShape getFaceShapeVanilla(VoxelShape shape, Direction direction) { -+ // Paper end - if (shape == block()) { - return block(); - } else { -@@ -0,0 +0,0 @@ public final class Shapes { - i = 0; - } - -- return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i)); -+ return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i).optimize().optimize()); // Paper - first optimize converts to ArrayVoxelShape, second optimize could convert to AABBVoxelShape - } - } - -@@ -0,0 +0,0 @@ public final class Shapes { - } - - public static boolean faceShapeOccludes(VoxelShape one, VoxelShape two) { -+ // Paper start - try to optimise for the case where the shapes do _not_ occlude -+ // which is _most_ of the time in lighting -+ if (one == getFullUnoptimisedCube() || one == BLOCK_OPTIMISED -+ || two == getFullUnoptimisedCube() || two == BLOCK_OPTIMISED) { -+ return true; -+ } -+ boolean v1Empty = one == empty(); -+ boolean v2Empty = two == empty(); -+ if (v1Empty && v2Empty) { -+ return false; -+ } -+ if ((one instanceof io.papermc.paper.voxel.AABBVoxelShape || v1Empty) -+ && (two instanceof io.papermc.paper.voxel.AABBVoxelShape || v2Empty)) { -+ if (!v1Empty && !v2Empty && (one != two)) { -+ AABB boundingBox1 = ((io.papermc.paper.voxel.AABBVoxelShape)one).aabb; -+ AABB boundingBox2 = ((io.papermc.paper.voxel.AABBVoxelShape)two).aabb; -+ // can call it here in some cases -+ -+ // check overall bounding box -+ double minY = Math.min(boundingBox1.minY, boundingBox2.minY); -+ double maxY = Math.max(boundingBox1.maxY, boundingBox2.maxY); -+ if (minY > io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON || maxY < (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) { -+ return false; -+ } -+ double minX = Math.min(boundingBox1.minX, boundingBox2.minX); -+ double maxX = Math.max(boundingBox1.maxX, boundingBox2.maxX); -+ if (minX > io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON || maxX < (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) { -+ return false; -+ } -+ double minZ = Math.min(boundingBox1.minZ, boundingBox2.minZ); -+ double maxZ = Math.max(boundingBox1.maxZ, boundingBox2.maxZ); -+ if (minZ > io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON || maxZ < (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) { -+ return false; -+ } -+ // fall through to full merge check -+ } else { -+ AABB boundingBox = v1Empty ? ((io.papermc.paper.voxel.AABBVoxelShape)two).aabb : ((io.papermc.paper.voxel.AABBVoxelShape)one).aabb; -+ // check if the bounding box encloses the full cube -+ return (boundingBox.minY <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && boundingBox.maxY >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) && -+ (boundingBox.minX <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && boundingBox.maxX >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) && -+ (boundingBox.minZ <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && boundingBox.maxZ >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)); -+ } -+ } -+ return faceShapeOccludesVanilla(one, two); -+ } -+ public static boolean faceShapeOccludesVanilla(VoxelShape one, VoxelShape two) { -+ // Paper end - if (one != block() && two != block()) { - if (one.isEmpty() && two.isEmpty()) { - return false; -diff --git a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java -@@ -0,0 +0,0 @@ import net.minecraft.world.phys.BlockHitResult; - import net.minecraft.world.phys.Vec3; - - public abstract class VoxelShape { -- protected final DiscreteVoxelShape shape; -+ public final DiscreteVoxelShape shape; // Paper - public - @Nullable - private VoxelShape[] faces; - -- VoxelShape(DiscreteVoxelShape voxels) { -+ // Paper start -+ public boolean intersects(AABB shape) { -+ return Shapes.joinIsNotEmpty(this, new io.papermc.paper.voxel.AABBVoxelShape(shape), BooleanOp.AND); -+ } -+ // Paper end -+ -+ protected VoxelShape(DiscreteVoxelShape voxels) { // Paper - protected - this.shape = voxels; - } - -@@ -0,0 +0,0 @@ public abstract class VoxelShape { - } - } - -- private VoxelShape calculateFace(Direction direction) { -+ protected VoxelShape calculateFace(Direction direction) { // Paper - Direction.Axis axis = direction.getAxis(); - DoubleList doubleList = this.getCoords(axis); - if (doubleList.size() == 2 && DoubleMath.fuzzyEquals(doubleList.getDouble(0), 0.0D, 1.0E-7D) && DoubleMath.fuzzyEquals(doubleList.getDouble(1), 1.0D, 1.0E-7D)) { diff --git a/patches/unapplied/server/Optimise-chunk-tick-iteration.patch b/patches/unapplied/server/Optimise-chunk-tick-iteration.patch deleted file mode 100644 index 7c4b0a1c77..0000000000 --- a/patches/unapplied/server/Optimise-chunk-tick-iteration.patch +++ /dev/null @@ -1,94 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 7 May 2020 05:48:54 -0700 -Subject: [PATCH] Optimise chunk tick iteration - -Use a dedicated list of entity ticking chunks to reduce the cost - -diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java -+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - - this.lastSpawnState = spawnercreature_d; - this.level.getProfiler().pop(); -- List list = Lists.newArrayList(this.chunkMap.getChunks()); -- -- Collections.shuffle(list); -+ // Paper - moved down, enabled if per-player = false - // Paper - moved natural spawn event up - this.level.timings.chunkTicks.startTiming(); // Paper -- list.forEach((playerchunk) -> { -- Optional optional = ((Either) playerchunk.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).left(); -- -- if (optional.isPresent()) { -- LevelChunk chunk = (LevelChunk) optional.get(); -+ // Paper start -+ java.util.Iterator iterator; -+ if (this.level.paperConfig.perPlayerMobSpawns) { -+ iterator = this.entityTickingChunks.iterator(); -+ } else { -+ iterator = this.entityTickingChunks.unsafeIterator(); -+ List shuffled = new java.util.ArrayList<>(this.entityTickingChunks.size()); -+ while (iterator.hasNext()) { -+ shuffled.add(iterator.next()); -+ } -+ Collections.shuffle(shuffled); -+ iterator = shuffled.iterator(); -+ } -+ try { while (iterator.hasNext()) { -+ LevelChunk chunk = iterator.next(); -+ ChunkHolder playerchunk = chunk.playerChunk; -+ if (playerchunk != null) { -+ this.level.getProfiler().push("broadcast"); -+ this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timings -+ playerchunk.broadcastChanges(chunk); -+ this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timings -+ this.level.getProfiler().pop(); -+ // Paper end - ChunkPos chunkcoordintpair = chunk.getPos(); - -- if (this.level.isPositionEntityTicking(chunkcoordintpair) && !this.chunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, false)) { // Paper - optimise isOutsideOfRange -+ if ((true || this.level.isPositionEntityTicking(chunkcoordintpair)) && !this.chunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, false)) { // Paper - optimise isOutsideOfRange // Paper - we only iterate entity ticking chunks - chunk.setInhabitedTime(chunk.getInhabitedTime() + j); - if (flag1 && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunk.getPos()) && !this.chunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, true)) { // Spigot // Paper - optimise isOutsideOfRange - NaturalSpawner.spawnForChunk(this.level, chunk, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag2); -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - // this.level.timings.doTickTiles.stopTiming(); // Spigot // Paper - } - } -- }); -+ } // Paper start - optimise chunk tick iteration -+ } finally { -+ if (iterator instanceof io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator) { -+ ((io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator)iterator).finishedIterating(); -+ } -+ } -+ // Paper end - optimise chunk tick iteration - this.level.timings.chunkTicks.stopTiming(); // Paper - this.level.getProfiler().push("customSpawners"); - if (flag1) { -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - } // Paper - timings - } - -- this.level.getProfiler().popPush("broadcast"); -- this.chunkMap.getChunks().forEach((playerchunk) -> { // Paper - no... just no... -- Optional optional = ((Either) playerchunk.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).left(); // CraftBukkit - decompile error -- -- Objects.requireNonNull(playerchunk); -- -- // Paper start - timings -- optional.ifPresent(chunk -> { -- this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timings -- playerchunk.broadcastChanges(chunk); -- this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timings -- }); -- // Paper end -- }); -- this.level.getProfiler().pop(); -+ // Paper - no, iterating just ONCE is expensive enough! Don't do it TWICE! Code moved up - this.level.getProfiler().pop(); - } - diff --git a/patches/unapplied/server/Optimise-collision-checking-in-player-move-packet-ha.patch b/patches/unapplied/server/Optimise-collision-checking-in-player-move-packet-ha.patch deleted file mode 100644 index 3e4e9a6a0d..0000000000 --- a/patches/unapplied/server/Optimise-collision-checking-in-player-move-packet-ha.patch +++ /dev/null @@ -1,168 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 2 Jul 2020 12:02:43 -0700 -Subject: [PATCH] Optimise collision checking in player move packet handling - -Move collision logic to just the hasNewCollision call instead of getCubes + hasNewCollision - -diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser - return; - } - -- boolean flag = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); -+ AABB oldBox = entity.getBoundingBox(); // Paper - copy from player movement packet - - d6 = d3 - this.vehicleLastGoodX; // Paper - diff on change, used for checking large move vectors above - d7 = d4 - this.vehicleLastGoodY - 1.0E-6D; // Paper - diff on change, used for checking large move vectors above - d8 = d5 - this.vehicleLastGoodZ; // Paper - diff on change, used for checking large move vectors above - entity.move(MoverType.PLAYER, new Vec3(d6, d7, d8)); -+ boolean didCollide = toX != entity.getX() || toY != entity.getY() || toZ != entity.getZ(); // Paper - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be... - double d11 = d7; - - d6 = d3 - entity.getX(); -@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser - boolean flag1 = false; - - if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot -- flag1 = true; -+ flag1 = true; // Paper - diff on change, this should be moved wrongly - ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", entity.getName().getString(), this.player.getName().getString(), Math.sqrt(d10)); - } - Location curPos = this.getCraftPlayer().getLocation(); // Spigot - - entity.absMoveTo(d3, d4, d5, f, f1); - this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit -- boolean flag2 = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); -- -- if (flag && (flag1 || !flag2)) { -+ // Paper start - optimise out extra getCubes -+ boolean teleportBack = flag1; // violating this is always a fail -+ if (!teleportBack) { -+ // note: only call after setLocation, or else getBoundingBox is wrong -+ AABB newBox = entity.getBoundingBox(); -+ if (didCollide || !oldBox.equals(newBox)) { -+ teleportBack = this.hasNewCollision(worldserver, entity, oldBox, newBox); -+ } // else: no collision at all detected, why do we care? -+ } -+ if (teleportBack) { // Paper end - optimise out extra getCubes - entity.absMoveTo(d0, d1, d2, f, f1); - this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit - this.connection.send(new ClientboundMoveVehiclePacket(entity)); -@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser - } - - private boolean noBlocksAround(Entity entity) { -- return entity.level.getBlockStates(entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D)).allMatch(BlockBehaviour.BlockStateBase::isAir); -+ // Paper start - stop using streams, this is already a known fixed problem in Entity#move -+ AABB box = entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D); -+ int minX = Mth.floor(box.minX); -+ int minY = Mth.floor(box.minY); -+ int minZ = Mth.floor(box.minZ); -+ int maxX = Mth.floor(box.maxX); -+ int maxY = Mth.floor(box.maxY); -+ int maxZ = Mth.floor(box.maxZ); -+ -+ Level world = entity.level; -+ BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); -+ -+ for (int y = minY; y <= maxY; ++y) { -+ for (int z = minZ; z <= maxZ; ++z) { -+ for (int x = minX; x <= maxX; ++x) { -+ pos.set(x, y, z); -+ BlockState type = world.getTypeIfLoaded(pos); -+ if (type != null && !type.isAir()) { -+ return false; -+ } -+ } -+ } -+ } -+ -+ return true; -+ // Paper end - stop using streams, this is already a known fixed problem in Entity#move - } - - @Override -@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser - } - - if (this.awaitingPositionFromClient != null) { -- if (this.tickCount - this.awaitingTeleportTime > 20) { -+ if (false && this.tickCount - this.awaitingTeleportTime > 20) { // Paper - this will greatly screw with clients with > 1000ms RTT - this.awaitingTeleportTime = this.tickCount; - this.teleport(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot()); - } -@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser - } - } - -- AABB axisalignedbb = this.player.getBoundingBox(); -+ AABB axisalignedbb = this.player.getBoundingBox(); // Paper - diff on change, should be old AABB - - d7 = d0 - this.lastGoodX; // Paper - diff on change, used for checking large move vectors above - d8 = d1 - this.lastGoodY; // Paper - diff on change, used for checking large move vectors above -@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser - } - - this.player.move(MoverType.PLAYER, new Vec3(d7, d8, d9)); -+ boolean didCollide = toX != this.player.getX() || toY != this.player.getY() || toZ != this.player.getZ(); // Paper - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be... - this.player.setOnGround(packet.isOnGround()); // CraftBukkit - SPIGOT-5810, SPIGOT-5835: reset by this.player.move - // Paper start - prevent position desync - if (this.awaitingPositionFromClient != null) { -@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser - boolean flag1 = false; - - if (!this.player.isChangingDimension() && d11 > org.spigotmc.SpigotConfig.movedWronglyThreshold && !this.player.isSleeping() && !this.player.gameMode.isCreative() && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR) { // Spigot -- flag1 = true; -+ flag1 = true; // Paper - diff on change, this should be moved wrongly - ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!", this.player.getName().getString()); - } - - this.player.absMoveTo(d0, d1, d2, f, f1); -- if (!this.player.noPhysics && !this.player.isSleeping() && (flag1 && worldserver.noCollision(this.player, axisalignedbb) || this.isPlayerCollidingWithAnythingNew((LevelReader) worldserver, axisalignedbb))) { -+ // Paper start - optimise out extra getCubes -+ // Original for reference: -+ // boolean teleportBack = flag1 && worldserver.getCubes(this.player, axisalignedbb) || (didCollide && this.a((IWorldReader) worldserver, axisalignedbb)); -+ boolean teleportBack = flag1; // violating this is always a fail -+ if (!this.player.noPhysics && !this.player.isSleeping() && !teleportBack) { -+ AABB newBox = this.player.getBoundingBox(); -+ if (didCollide || !axisalignedbb.equals(newBox)) { -+ // note: only call after setLocation, or else getBoundingBox is wrong -+ teleportBack = this.hasNewCollision(worldserver, this.player, axisalignedbb, newBox); -+ } // else: no collision at all detected, why do we care? -+ } -+ if (!this.player.noPhysics && !this.player.isSleeping() && teleportBack) { // Paper end - optimise out extra getCubes - this.teleport(d3, d4, d5, f, f1); - } else { - // CraftBukkit start - fire PlayerMoveEvent -@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser - } - } - -+ // Paper start - optimise out extra getCubes -+ private boolean hasNewCollision(final ServerLevel world, final Entity entity, final AABB oldBox, final AABB newBox) { -+ final List collisions = io.papermc.paper.util.CachedLists.getTempCollisionList(); -+ try { -+ io.papermc.paper.util.CollisionUtil.getCollisions(world, entity, newBox, collisions, false, true, -+ true, false, null, null); -+ -+ for (int i = 0, len = collisions.size(); i < len; ++i) { -+ final AABB box = collisions.get(i); -+ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(box, oldBox)) { -+ return true; -+ } -+ } -+ -+ return false; -+ } finally { -+ io.papermc.paper.util.CachedLists.returnTempCollisionList(collisions); -+ } -+ } -+ // Paper end - optimise out extra getCubes -+ - private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box) { - Stream stream = world.getCollisions(this.player, this.player.getBoundingBox().deflate(9.999999747378752E-6D), (entity) -> { - return true; diff --git a/patches/unapplied/server/Prevent-unload-calls-removing-tickets-for-sync-loads.patch b/patches/unapplied/server/Prevent-unload-calls-removing-tickets-for-sync-loads.patch deleted file mode 100644 index 3f4d8486bf..0000000000 --- a/patches/unapplied/server/Prevent-unload-calls-removing-tickets-for-sync-loads.patch +++ /dev/null @@ -1,67 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 18 Jun 2020 18:23:20 -0700 -Subject: [PATCH] Prevent unload() calls removing tickets for sync loads - - -diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java -+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - return completablefuture; - } - -+ private long syncLoadCounter; // Paper - prevent plugin unloads from removing our ticket -+ - private CompletableFuture> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { - // Paper start - add isUrgent - old sig left in place for dirty nms plugins - return getChunkFutureMainThread(chunkX, chunkZ, leastStatus, create, false); -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - ChunkHolder.FullChunkStatus currentChunkState = ChunkHolder.getFullChunkStatus(playerchunk.getTicketLevel()); - currentlyUnloading = (oldChunkState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && !currentChunkState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)); - } -+ final Long identifier; // Paper - prevent plugin unloads from removing our ticket - if (create && !currentlyUnloading) { - // CraftBukkit end - this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair); -+ identifier = Long.valueOf(this.syncLoadCounter++); // Paper - prevent plugin unloads from removing our ticket -+ this.distanceManager.addTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); // Paper - prevent plugin unloads from removing our ticket - if (isUrgent) this.distanceManager.markUrgent(chunkcoordintpair); // Paper - Chunk priority - if (this.chunkAbsent(playerchunk, l)) { - ProfilerFiller gameprofilerfiller = this.level.getProfiler(); -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - playerchunk = this.getVisibleChunkIfPresent(k); - gameprofilerfiller.pop(); - if (this.chunkAbsent(playerchunk, l)) { -+ this.distanceManager.removeTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); // Paper - throw (IllegalStateException) Util.pauseInIde((Throwable) (new IllegalStateException("No chunk holder after ticket has been added"))); - } - } -- } - -+ } else { identifier = null; } // Paper - prevent plugin unloads from removing our ticket - // Paper start - Chunk priority - CompletableFuture> future = this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(leastStatus, this.chunkMap); -+ // Paper start - prevent plugin unloads from removing our ticket -+ if (create && !currentlyUnloading) { -+ future.thenAcceptAsync((either) -> { -+ ServerChunkCache.this.distanceManager.removeTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); -+ }, ServerChunkCache.this.mainThreadProcessor); -+ } -+ // Paper end - prevent plugin unloads from removing our ticket - if (isUrgent) { - future.thenAccept(either -> this.distanceManager.clearUrgent(chunkcoordintpair)); - } -diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/TicketType.java -+++ b/src/main/java/net/minecraft/server/level/TicketType.java -@@ -0,0 +0,0 @@ public class TicketType { - public static final TicketType PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit - public static final TicketType PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit - public static final TicketType DELAY_UNLOAD = create("delay_unload", Long::compareTo, 300); // Paper -+ public static final TicketType REQUIRED_LOAD = create("required_load", Long::compareTo); // Paper - make sure getChunkAt does not fail - - public static TicketType create(String name, Comparator argumentComparator) { - return new TicketType<>(name, argumentComparator, 0L); diff --git a/patches/unapplied/server/Replace-player-chunk-loader-system.patch b/patches/unapplied/server/Replace-player-chunk-loader-system.patch deleted file mode 100644 index eff41bf1bc..0000000000 --- a/patches/unapplied/server/Replace-player-chunk-loader-system.patch +++ /dev/null @@ -1,1818 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sun, 24 Jan 2021 20:27:32 -0800 -Subject: [PATCH] Replace player chunk loader system - -The old one has undebuggable problems. Rewriting seems -the most sensible option. - -This new player chunk manager will also strictly rate limit -chunk sends so that netty threads do not get overloaded, whether -it be from the anti-xray logic or the compression itself. - -Chunk loading is also rate limited in the same manner, so this -will result in a maximum responsiveness for change. - -Config: -``` -chunk-loading: - min-load-radius: 2 - max-concurrent-sends: 2 - autoconfig-send-distance: true - target-player-chunk-send-rate: 100.0 - global-max-chunk-send-rate: -1 - enable-frustum-priority: false - global-max-chunk-load-rate: -1.0 - player-max-concurrent-loads: 4.0 - global-max-concurrent-loads: 500.0 -``` - -min-load-radius - The radius of chunks around a player that -are not throttled for loading. The number of chunks -affected is actually the configured value plus one as this -config controls the chunks the client will be able to render. - -max-concurrent-sends - The maximum number of chunks that -can be queued to send at any given time. Low values -are generally going to solve server-sided networking -bottlenecks like anti-xray and chunk compression. Client -side networking is unlikely to be helped (i.e this wont help -people running off McDonald's wifi). - -autoconfig-send-distance - Whether to try to use the client's -view distance for the send view distance in the server. In the -case that no plugin has explicitly set the send distance and -the client view distance is less-than the server's send distance, -the client's view distance will be used. This will not affect -tick view distance or no-tick view distance. - -target-player-chunk-send-rate - The maximum chunk send rate -an individual player will have. -1 means no limit - -global-max-chunk-send-rate - The maximum chunk send rate for -the whole server. -1 means no limit - -enable-frustum-priority - Whether chunks in front of a player -are prioritised to load/send first. Disabled by default -because the client can bug out due to the out of order -chunk sending. - -global-max-chunk-load-rate - The maximum chunk load rate -for the whole server. -1 means no limit - -player-max-concurrent-loads and global-max-concurrent-loads -The maximum number of concurrent loads for the server is -determined by the number of players on the server multiplied by the -`player-max-concurrent-loads`. It is then limited to -whatever `global-max-concurrent-loads` is configured to. - -diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/co/aikar/timings/TimingsExport.java -+++ b/src/main/java/co/aikar/timings/TimingsExport.java -@@ -0,0 +0,0 @@ public class TimingsExport extends Thread { - return pair(rule, world.getWorld().getGameRuleValue(rule)); - })), - pair("ticking-distance", world.getChunkSource().chunkMap.getEffectiveViewDistance()), -- pair("notick-viewdistance", world.getChunkSource().chunkMap.getEffectiveNoTickViewDistance()) -+ pair("notick-viewdistance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()) // Paper - replace old player chunk management - )); - })); - -diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/com/destroystokyo/paper/PaperConfig.java -+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java -@@ -0,0 +0,0 @@ public class PaperConfig { - itemValidationBookAuthorLength = getInt("settings.item-validation.book.author", itemValidationBookAuthorLength); - itemValidationBookPageLength = getInt("settings.item-validation.book.page", itemValidationBookPageLength); - } -+ -+ public static int playerMinChunkLoadRadius; -+ public static boolean playerAutoConfigureSendViewDistance; -+ public static int playerMaxConcurrentChunkSends; -+ public static double playerTargetChunkSendRate; -+ public static double globalMaxChunkSendRate; -+ public static boolean playerFrustumPrioritisation; -+ public static double globalMaxChunkLoadRate; -+ public static double playerMaxConcurrentChunkLoads; -+ public static double globalMaxConcurrentChunkLoads; -+ -+ private static void newPlayerChunkManagement() { -+ playerMinChunkLoadRadius = getInt("settings.chunk-loading.min-load-radius", 2); -+ playerMaxConcurrentChunkSends = getInt("settings.chunk-loading.max-concurrent-sends", 2); -+ playerAutoConfigureSendViewDistance = getBoolean("settings.chunk-loading.autoconfig-send-distance", true); -+ playerTargetChunkSendRate = getDouble("settings.chunk-loading.target-player-chunk-send-rate", 100.0); -+ globalMaxChunkSendRate = getDouble("settings.chunk-loading.global-max-chunk-send-rate", -1.0); -+ playerFrustumPrioritisation = getBoolean("settings.chunk-loading.enable-frustum-priority", false); -+ globalMaxChunkLoadRate = getDouble("settings.chunk-loading.global-max-chunk-load-rate", -1.0); -+ if (version < 23 && globalMaxChunkLoadRate == 300.0) { -+ set("settings.chunk-loading.global-max-chunk-load-rate", -1.0); -+ } -+ playerMaxConcurrentChunkLoads = getDouble("settings.chunk-loading.player-max-concurrent-loads", 4.0); -+ globalMaxConcurrentChunkLoads = getDouble("settings.chunk-loading.global-max-concurrent-loads", 500.0); -+ } - } -diff --git a/src/main/java/io/papermc/paper/chunk/PlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/PlayerChunkLoader.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/PlayerChunkLoader.java -@@ -0,0 +0,0 @@ -+package io.papermc.paper.chunk; -+ -+import com.destroystokyo.paper.PaperConfig; -+import com.destroystokyo.paper.util.misc.PlayerAreaMap; -+import com.destroystokyo.paper.util.misc.PooledLinkedHashSets; -+import io.papermc.paper.util.CoordinateUtils; -+import io.papermc.paper.util.IntervalledCounter; -+import io.papermc.paper.util.TickThread; -+import it.unimi.dsi.fastutil.longs.LongOpenHashSet; -+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; -+import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap; -+import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; -+import net.minecraft.network.protocol.Packet; -+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket; -+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket; -+import net.minecraft.server.MCUtil; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ChunkHolder; -+import net.minecraft.server.level.ChunkMap; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.server.level.TicketType; -+import net.minecraft.util.Mth; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.chunk.LevelChunk; -+import java.util.ArrayDeque; -+import java.util.ArrayList; -+import java.util.List; -+import java.util.TreeSet; -+import java.util.concurrent.atomic.AtomicInteger; -+ -+public final class PlayerChunkLoader { -+ -+ public static final int MIN_VIEW_DISTANCE = 2; -+ public static final int MAX_VIEW_DISTANCE = 32; -+ -+ public static final int TICK_TICKET_LEVEL = 31; -+ public static final int LOADED_TICKET_LEVEL = 33; -+ -+ protected final ChunkMap chunkMap; -+ protected final Reference2ObjectLinkedOpenHashMap playerMap = new Reference2ObjectLinkedOpenHashMap<>(512, 0.7f); -+ protected final ReferenceLinkedOpenHashSet chunkSendQueue = new ReferenceLinkedOpenHashSet<>(512, 0.7f); -+ -+ protected final TreeSet chunkLoadQueue = new TreeSet<>((final PlayerLoaderData p1, final PlayerLoaderData p2) -> { -+ if (p1 == p2) { -+ return 0; -+ } -+ -+ final ChunkPriorityHolder holder1 = p1.loadQueue.peekFirst(); -+ final ChunkPriorityHolder holder2 = p2.loadQueue.peekFirst(); -+ -+ final int priorityCompare = Double.compare(holder1 == null ? Double.MAX_VALUE : holder1.priority, holder2 == null ? Double.MAX_VALUE : holder2.priority); -+ -+ if (priorityCompare != 0) { -+ return priorityCompare; -+ } -+ -+ final int idCompare = Integer.compare(p1.player.getId(), p2.player.getId()); -+ -+ if (idCompare != 0) { -+ return idCompare; -+ } -+ -+ // last resort -+ return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2)); -+ }); -+ -+ protected final TreeSet chunkSendWaitQueue = new TreeSet<>((final PlayerLoaderData p1, final PlayerLoaderData p2) -> { -+ if (p1 == p2) { -+ return 0; -+ } -+ -+ final int timeCompare = Long.compare(p1.nextChunkSendTarget, p2.nextChunkSendTarget); -+ if (timeCompare != 0) { -+ return timeCompare; -+ } -+ -+ final int idCompare = Integer.compare(p1.player.getId(), p2.player.getId()); -+ -+ if (idCompare != 0) { -+ return idCompare; -+ } -+ -+ // last resort -+ return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2)); -+ }); -+ -+ -+ // no throttling is applied below this VD for loading -+ -+ /** -+ * The chunks to be sent to players, provided they're send-ready. Send-ready means the chunk and its 1 radius neighbours are loaded. -+ */ -+ public final PlayerAreaMap broadcastMap; -+ -+ /** -+ * The chunks to be brought up to send-ready status. Send-ready means the chunk and its 1 radius neighbours are loaded. -+ */ -+ public final PlayerAreaMap loadMap; -+ -+ /** -+ * Areamap used only to remove tickets for send-ready chunks. View distance is always + 1 of load view distance. Thus, -+ * this map is always representing the chunks we are actually going to load. -+ */ -+ public final PlayerAreaMap loadTicketCleanup; -+ -+ /** -+ * The chunks to brought to ticking level. Each chunk must have 2 radius neighbours loaded before this can happen. -+ */ -+ public final PlayerAreaMap tickMap; -+ -+ /** -+ * -1 if defaulting to [load distance], else always in [2, load distance] -+ */ -+ protected int rawSendDistance = -1; -+ -+ /** -+ * -1 if defaulting to [tick view distance + 1], else always in [tick view distance + 1, 32 + 1] -+ */ -+ protected int rawLoadDistance = -1; -+ -+ /** -+ * Never -1, always in [2, 32] -+ */ -+ protected int rawTickDistance = -1; -+ -+ // methods to bridge for API -+ -+ public int getTargetViewDistance() { -+ return this.getTickDistance(); -+ } -+ -+ public void setTargetViewDistance(final int distance) { -+ this.setTickDistance(distance); -+ } -+ -+ public int getTargetNoTickViewDistance() { -+ return this.getLoadDistance() - 1; -+ } -+ -+ public void setTargetNoTickViewDistance(final int distance) { -+ this.setLoadDistance(distance == -1 ? -1 : distance + 1); -+ } -+ -+ public int getTargetSendDistance() { -+ return this.rawSendDistance == -1 ? this.getLoadDistance() : this.rawSendDistance; -+ } -+ -+ public void setTargetSendDistance(final int distance) { -+ this.setSendDistance(distance); -+ } -+ -+ // internal methods -+ -+ public int getSendDistance() { -+ final int loadDistance = this.getLoadDistance(); -+ return this.rawSendDistance == -1 ? loadDistance : Math.min(this.rawSendDistance, loadDistance); -+ } -+ -+ public void setSendDistance(final int distance) { -+ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) { -+ throw new IllegalArgumentException(Integer.toString(distance)); -+ } -+ this.rawSendDistance = distance; -+ } -+ -+ public int getLoadDistance() { -+ final int tickDistance = this.getTickDistance(); -+ return this.rawLoadDistance == -1 ? tickDistance + 1 : Math.max(tickDistance + 1, this.rawLoadDistance); -+ } -+ -+ public void setLoadDistance(final int distance) { -+ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) { -+ throw new IllegalArgumentException(Integer.toString(distance)); -+ } -+ this.rawLoadDistance = distance; -+ } -+ -+ public int getTickDistance() { -+ return this.rawTickDistance; -+ } -+ -+ public void setTickDistance(final int distance) { -+ if (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE) { -+ throw new IllegalArgumentException(Integer.toString(distance)); -+ } -+ this.rawTickDistance = distance; -+ } -+ -+ /* -+ Players have 3 different types of view distance: -+ 1. Sending view distance -+ 2. Loading view distance -+ 3. Ticking view distance -+ -+ But for configuration purposes (and API) there are: -+ 1. No-tick view distance -+ 2. Tick view distance -+ 3. Broadcast view distance -+ -+ These aren't always the same as the types we represent internally. -+ -+ Loading view distance is always max(no-tick + 1, tick + 1) -+ - no-tick has 1 added because clients need an extra radius to render chunks -+ - tick has 1 added because it needs an extra radius of chunks to load before they can be marked ticking -+ -+ Loading view distance is defined as the radius of chunks that will be brought to send-ready status, which means -+ it loads chunks in radius load-view-distance + 1. -+ -+ The maximum value for send view distance is the load view distance. API can set it lower. -+ */ -+ -+ public PlayerChunkLoader(final ChunkMap chunkMap, final PooledLinkedHashSets pooledHashSets) { -+ this.chunkMap = chunkMap; -+ this.broadcastMap = new PlayerAreaMap(pooledHashSets, -+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { -+ if (player.needsChunkCenterUpdate) { -+ player.needsChunkCenterUpdate = false; -+ player.connection.send(new ClientboundSetChunkCacheCenterPacket(currPosX, currPosZ)); -+ } -+ PlayerChunkLoader.this.onChunkEnter(player, rangeX, rangeZ); -+ }, -+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { -+ PlayerChunkLoader.this.onChunkLeave(player, rangeX, rangeZ); -+ }); -+ this.loadMap = new PlayerAreaMap(pooledHashSets, -+ null, -+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { -+ if (newState != null) { -+ return; -+ } -+ PlayerChunkLoader.this.isTargetedForPlayerLoad.remove(CoordinateUtils.getChunkKey(rangeX, rangeZ)); -+ }); -+ this.loadTicketCleanup = new PlayerAreaMap(pooledHashSets, -+ null, -+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { -+ if (newState != null) { -+ return; -+ } -+ ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); -+ PlayerChunkLoader.this.chunkMap.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, LOADED_TICKET_LEVEL, chunkPos); -+ if (PlayerChunkLoader.this.chunkTicketTracker.remove(chunkPos.toLong())) { -+ --PlayerChunkLoader.this.concurrentChunkLoads; -+ } -+ }); -+ this.tickMap = new PlayerAreaMap(pooledHashSets, -+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { -+ if (newState.size() != 1) { -+ return; -+ } -+ LevelChunk chunk = PlayerChunkLoader.this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(rangeX, rangeZ); -+ if (chunk == null || !chunk.areNeighboursLoaded(2)) { -+ return; -+ } -+ -+ ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); -+ PlayerChunkLoader.this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos); -+ }, -+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { -+ if (newState != null) { -+ return; -+ } -+ ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); -+ PlayerChunkLoader.this.chunkMap.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos); -+ }); -+ } -+ -+ protected final LongOpenHashSet isTargetedForPlayerLoad = new LongOpenHashSet(); -+ protected final LongOpenHashSet chunkTicketTracker = new LongOpenHashSet(); -+ -+ // rets whether the chunk is at a loaded stage that is ready to be sent to players -+ public boolean isChunkPlayerLoaded(final int chunkX, final int chunkZ) { -+ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ final ChunkHolder chunk = this.chunkMap.getVisibleChunkIfPresent(key); -+ -+ if (chunk == null) { -+ return false; -+ } -+ -+ return chunk.getSendingChunk() != null && this.isTargetedForPlayerLoad.contains(key); -+ } -+ -+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) { -+ final PlayerLoaderData data = this.playerMap.get(player); -+ if (data == null) { -+ return false; -+ } -+ -+ return data.hasSentChunk(chunkX, chunkZ); -+ } -+ -+ protected int getMaxConcurrentChunkSends() { -+ return PaperConfig.playerMaxConcurrentChunkSends; -+ } -+ -+ protected int getMaxChunkLoads() { -+ double config = PaperConfig.playerMaxConcurrentChunkLoads; -+ double max = PaperConfig.globalMaxConcurrentChunkLoads; -+ return (int)Math.ceil(Math.min(config * MinecraftServer.getServer().getPlayerCount(), max <= 1.0 ? Double.MAX_VALUE : max)); -+ } -+ -+ protected long getTargetSendPerPlayerAddend() { -+ return PaperConfig.playerTargetChunkSendRate <= 1.0 ? 0L : (long)Math.round(1.0e9 / PaperConfig.playerTargetChunkSendRate); -+ } -+ -+ protected long getMaxSendAddend() { -+ return PaperConfig.globalMaxChunkSendRate <= 1.0 ? 0L : (long)Math.round(1.0e9 / PaperConfig.globalMaxChunkSendRate); -+ } -+ -+ public void onChunkPlayerTickReady(final int chunkX, final int chunkZ) { -+ final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); -+ this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos); -+ } -+ -+ public void onChunkSendReady(final int chunkX, final int chunkZ) { -+ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInSendRange = this.broadcastMap.getObjectsInRange(chunkX, chunkZ); -+ -+ if (playersInSendRange == null) { -+ return; -+ } -+ -+ final Object[] rawData = playersInSendRange.getBackingSet(); -+ for (int i = 0, len = rawData.length; i < len; ++i) { -+ final Object raw = rawData[i]; -+ -+ if (!(raw instanceof ServerPlayer)) { -+ continue; -+ } -+ this.onChunkEnter((ServerPlayer)raw, chunkX, chunkZ); -+ } -+ -+ // now let's try and queue mid tick logic again -+ } -+ -+ public void onChunkEnter(final ServerPlayer player, final int chunkX, final int chunkZ) { -+ final PlayerLoaderData data = this.playerMap.get(player); -+ -+ if (data == null) { -+ return; -+ } -+ -+ if (data.hasSentChunk(chunkX, chunkZ) || !this.isChunkPlayerLoaded(chunkX, chunkZ)) { -+ // if we don't have player tickets, then the load logic will pick this up and queue to send -+ return; -+ } -+ -+ final long playerPos = this.broadcastMap.getLastCoordinate(player); -+ final int playerChunkX = CoordinateUtils.getChunkX(playerPos); -+ final int playerChunkZ = CoordinateUtils.getChunkZ(playerPos); -+ final int manhattanDistance = Math.abs(playerChunkX - chunkX) + Math.abs(playerChunkZ - chunkZ); -+ -+ final ChunkPriorityHolder holder = new ChunkPriorityHolder(chunkX, chunkZ, manhattanDistance, 0.0); -+ data.sendQueue.add(holder); -+ } -+ -+ public void onChunkLoad(final int chunkX, final int chunkZ) { -+ if (this.chunkTicketTracker.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { -+ --this.concurrentChunkLoads; -+ } -+ } -+ -+ public void onChunkLeave(final ServerPlayer player, final int chunkX, final int chunkZ) { -+ final PlayerLoaderData data = this.playerMap.get(player); -+ -+ if (data == null) { -+ return; -+ } -+ -+ data.unloadChunk(chunkX, chunkZ); -+ } -+ -+ public void addPlayer(final ServerPlayer player) { -+ TickThread.ensureTickThread("Cannot add player async"); -+ if (!player.isRealPlayer) { -+ return; -+ } -+ final PlayerLoaderData data = new PlayerLoaderData(player, this); -+ if (this.playerMap.putIfAbsent(player, data) == null) { -+ data.update(); -+ } -+ } -+ -+ public void removePlayer(final ServerPlayer player) { -+ TickThread.ensureTickThread("Cannot remove player async"); -+ if (!player.isRealPlayer) { -+ return; -+ } -+ -+ final PlayerLoaderData loaderData = this.playerMap.remove(player); -+ if (loaderData == null) { -+ return; -+ } -+ loaderData.remove(); -+ this.chunkLoadQueue.remove(loaderData); -+ this.chunkSendQueue.remove(loaderData); -+ this.chunkSendWaitQueue.remove(loaderData); -+ synchronized (this.sendingChunkCounts) { -+ final int count = this.sendingChunkCounts.removeInt(loaderData); -+ if (count != 0) { -+ concurrentChunkSends.getAndAdd(-count); -+ } -+ } -+ } -+ -+ public void updatePlayer(final ServerPlayer player) { -+ TickThread.ensureTickThread("Cannot update player async"); -+ if (!player.isRealPlayer) { -+ return; -+ } -+ final PlayerLoaderData loaderData = this.playerMap.get(player); -+ if (loaderData != null) { -+ loaderData.update(); -+ } -+ } -+ -+ public PlayerLoaderData getData(final ServerPlayer player) { -+ return this.playerMap.get(player); -+ } -+ -+ public void tick() { -+ TickThread.ensureTickThread("Cannot tick async"); -+ for (final PlayerLoaderData data : this.playerMap.values()) { -+ data.update(); -+ } -+ this.tickMidTick(); -+ } -+ -+ protected static final AtomicInteger concurrentChunkSends = new AtomicInteger(); -+ protected final Reference2IntOpenHashMap sendingChunkCounts = new Reference2IntOpenHashMap<>(); -+ private static long nextChunkSend; -+ private void trySendChunks() { -+ final long time = System.nanoTime(); -+ if (time < nextChunkSend) { -+ return; -+ } -+ // drain entries from wait queue -+ while (!this.chunkSendWaitQueue.isEmpty()) { -+ final PlayerLoaderData data = this.chunkSendWaitQueue.first(); -+ -+ if (data.nextChunkSendTarget > time) { -+ break; -+ } -+ -+ this.chunkSendWaitQueue.pollFirst(); -+ -+ this.chunkSendQueue.add(data); -+ } -+ -+ if (this.chunkSendQueue.isEmpty()) { -+ return; -+ } -+ -+ final int maxSends = this.getMaxConcurrentChunkSends(); -+ final long nextPlayerDeadline = this.getTargetSendPerPlayerAddend() + time; -+ for (;;) { -+ if (this.chunkSendQueue.isEmpty()) { -+ break; -+ } -+ final int currSends = concurrentChunkSends.get(); -+ if (currSends >= maxSends) { -+ break; -+ } -+ -+ if (!concurrentChunkSends.compareAndSet(currSends, currSends + 1)) { -+ continue; -+ } -+ -+ // send chunk -+ -+ final PlayerLoaderData data = this.chunkSendQueue.removeFirst(); -+ -+ final ChunkPriorityHolder queuedSend = data.sendQueue.pollFirst(); -+ if (queuedSend == null) { -+ concurrentChunkSends.getAndDecrement(); // we never sent, so decrease -+ // stop iterating over players who have nothing to send -+ if (this.chunkSendQueue.isEmpty()) { -+ // nothing left -+ break; -+ } -+ continue; -+ } -+ -+ if (!this.isChunkPlayerLoaded(queuedSend.chunkX, queuedSend.chunkZ)) { -+ throw new IllegalStateException(); -+ } -+ -+ data.nextChunkSendTarget = nextPlayerDeadline; -+ this.chunkSendWaitQueue.add(data); -+ -+ synchronized (this.sendingChunkCounts) { -+ this.sendingChunkCounts.addTo(data, 1); -+ } -+ -+ data.sendChunk(queuedSend.chunkX, queuedSend.chunkZ, () -> { -+ synchronized (this.sendingChunkCounts) { -+ final int count = this.sendingChunkCounts.getInt(data); -+ if (count == 0) { -+ // disconnected, so we don't need to decrement: it will be decremented for us -+ return; -+ } -+ if (count == 1) { -+ this.sendingChunkCounts.removeInt(data); -+ } else { -+ this.sendingChunkCounts.put(data, count - 1); -+ } -+ } -+ -+ concurrentChunkSends.getAndDecrement(); -+ }); -+ -+ nextChunkSend = this.getMaxSendAddend() + time; -+ if (time < nextChunkSend) { -+ break; -+ } -+ } -+ } -+ -+ protected int concurrentChunkLoads; -+ // this interval prevents bursting a lot of chunk loads -+ protected static final IntervalledCounter TICKET_ADDITION_COUNTER_SHORT = new IntervalledCounter((long)(1.0e6 * 50.0)); // 50ms -+ // this interval ensures the rate is kept between ticks correctly -+ protected static final IntervalledCounter TICKET_ADDITION_COUNTER_LONG = new IntervalledCounter((long)(1.0e6 * 1000.0)); // 1000ms -+ private void tryLoadChunks() { -+ if (this.chunkLoadQueue.isEmpty()) { -+ return; -+ } -+ -+ final int maxLoads = this.getMaxChunkLoads(); -+ final long time = System.nanoTime(); -+ boolean updatedCounters = false; -+ for (;;) { -+ final PlayerLoaderData data = this.chunkLoadQueue.pollFirst(); -+ -+ final ChunkPriorityHolder queuedLoad = data.loadQueue.peekFirst(); -+ if (queuedLoad == null) { -+ if (this.chunkLoadQueue.isEmpty()) { -+ break; -+ } -+ continue; -+ } -+ -+ if (!updatedCounters) { -+ updatedCounters = true; -+ TICKET_ADDITION_COUNTER_SHORT.updateCurrentTime(time); -+ TICKET_ADDITION_COUNTER_LONG.updateCurrentTime(time); -+ } -+ -+ if (this.isChunkPlayerLoaded(queuedLoad.chunkX, queuedLoad.chunkZ)) { -+ // already loaded! -+ data.loadQueue.pollFirst(); // already loaded so we just skip -+ this.chunkLoadQueue.add(data); -+ -+ // ensure the chunk is queued to send -+ this.onChunkSendReady(queuedLoad.chunkX, queuedLoad.chunkZ); -+ continue; -+ } -+ -+ final long chunkKey = CoordinateUtils.getChunkKey(queuedLoad.chunkX, queuedLoad.chunkZ); -+ -+ final double priority = queuedLoad.priority; -+ // while we do need to rate limit chunk loads, the logic for sending chunks requires that tickets are present. -+ // when chunks are loaded (i.e spawn) but do not have this player's tickets, they have to wait behind the -+ // load queue. To avoid this problem, we check early here if tickets are required to load the chunk - if they -+ // aren't required, it bypasses the limiter system. -+ boolean unloadedTargetChunk = false; -+ unloaded_check: -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ final int offX = queuedLoad.chunkX + dx; -+ final int offZ = queuedLoad.chunkZ + dz; -+ if (this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(offX, offZ) == null) { -+ unloadedTargetChunk = true; -+ break unloaded_check; -+ } -+ } -+ } -+ if (unloadedTargetChunk && priority > 0.0) { -+ // priority > 0.0 implies rate limited chunks -+ -+ final int currentChunkLoads = this.concurrentChunkLoads; -+ if (currentChunkLoads >= maxLoads || (PaperConfig.globalMaxChunkLoadRate > 0 && (TICKET_ADDITION_COUNTER_SHORT.getRate() >= PaperConfig.globalMaxChunkLoadRate || TICKET_ADDITION_COUNTER_LONG.getRate() >= PaperConfig.globalMaxChunkLoadRate))) { -+ // don't poll, we didn't load it -+ this.chunkLoadQueue.add(data); -+ break; -+ } -+ } -+ -+ // can only poll after we decide to load -+ data.loadQueue.pollFirst(); -+ -+ // now that we've polled we can re-add to load queue -+ this.chunkLoadQueue.add(data); -+ -+ // add necessary tickets to load chunk up to send-ready -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ final int offX = queuedLoad.chunkX + dx; -+ final int offZ = queuedLoad.chunkZ + dz; -+ final ChunkPos chunkPos = new ChunkPos(offX, offZ); -+ -+ this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, LOADED_TICKET_LEVEL, chunkPos); -+ if (this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(offX, offZ) != null) { -+ continue; -+ } -+ -+ if (priority > 0.0 && this.chunkTicketTracker.add(CoordinateUtils.getChunkKey(offX, offZ))) { -+ // won't reach here if unloadedTargetChunk is false -+ ++this.concurrentChunkLoads; -+ TICKET_ADDITION_COUNTER_SHORT.addTime(time); -+ TICKET_ADDITION_COUNTER_LONG.addTime(time); -+ } -+ } -+ } -+ -+ // mark that we've added tickets here -+ this.isTargetedForPlayerLoad.add(chunkKey); -+ -+ // it's possible all we needed was the player tickets to queue up the send. -+ if (this.isChunkPlayerLoaded(queuedLoad.chunkX, queuedLoad.chunkZ)) { -+ // yup, all we needed. -+ this.onChunkSendReady(queuedLoad.chunkX, queuedLoad.chunkZ); -+ } -+ } -+ } -+ -+ public void tickMidTick() { -+ // try to send more chunks -+ this.trySendChunks(); -+ -+ // try to queue more chunks to load -+ this.tryLoadChunks(); -+ } -+ -+ static final class ChunkPriorityHolder { -+ public final int chunkX; -+ public final int chunkZ; -+ public final int manhattanDistanceToPlayer; -+ public final double priority; -+ -+ public ChunkPriorityHolder(final int chunkX, final int chunkZ, final int manhattanDistanceToPlayer, final double priority) { -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.manhattanDistanceToPlayer = manhattanDistanceToPlayer; -+ this.priority = priority; -+ } -+ } -+ -+ public static final class PlayerLoaderData { -+ -+ protected static final float FOV = 110.0f; -+ protected static final double PRIORITISED_DISTANCE = 12.0 * 16.0; -+ -+ // Player max sprint speed is approximately 8m/s -+ protected static final double LOOK_PRIORITY_SPEED_THRESHOLD = (10.0/20.0) * (10.0/20.0); -+ protected static final double LOOK_PRIORITY_YAW_DELTA_RECALC_THRESHOLD = 3.0f; -+ -+ protected double lastLocX = Double.NEGATIVE_INFINITY; -+ protected double lastLocZ = Double.NEGATIVE_INFINITY; -+ -+ protected int lastChunkX; -+ protected int lastChunkZ; -+ -+ // this is corrected so that 0 is along the positive x-axis -+ protected float lastYaw = Float.NEGATIVE_INFINITY; -+ -+ protected int lastSendDistance = Integer.MIN_VALUE; -+ protected int lastLoadDistance = Integer.MIN_VALUE; -+ protected int lastTickDistance = Integer.MIN_VALUE; -+ protected boolean usingLookingPriority; -+ -+ protected final ServerPlayer player; -+ protected final PlayerChunkLoader loader; -+ -+ // warning: modifications of this field must be aware that the loadQueue inside PlayerChunkLoader uses this field -+ // in a comparator! -+ protected final ArrayDeque loadQueue = new ArrayDeque<>(); -+ protected final LongOpenHashSet sentChunks = new LongOpenHashSet(); -+ -+ protected final TreeSet sendQueue = new TreeSet<>((final ChunkPriorityHolder p1, final ChunkPriorityHolder p2) -> { -+ final int distanceCompare = Integer.compare(p1.manhattanDistanceToPlayer, p2.manhattanDistanceToPlayer); -+ if (distanceCompare != 0) { -+ return distanceCompare; -+ } -+ -+ final int coordinateXCompare = Integer.compare(p1.chunkX, p2.chunkX); -+ if (coordinateXCompare != 0) { -+ return coordinateXCompare; -+ } -+ -+ return Integer.compare(p1.chunkZ, p2.chunkZ); -+ }); -+ -+ protected int sendViewDistance = -1; -+ protected int loadViewDistance = -1; -+ protected int tickViewDistance = -1; -+ -+ protected long nextChunkSendTarget; -+ -+ public PlayerLoaderData(final ServerPlayer player, final PlayerChunkLoader loader) { -+ this.player = player; -+ this.loader = loader; -+ } -+ -+ // these view distance methods are for api -+ public int getTargetSendViewDistance() { -+ final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; -+ final int loadViewDistance = Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance); -+ final int clientViewDistance = this.getClientViewDistance(); -+ final int sendViewDistance = Math.min(loadViewDistance, this.sendViewDistance == -1 ? (!PaperConfig.playerAutoConfigureSendViewDistance || clientViewDistance == -1 ? this.loader.getSendDistance() : clientViewDistance + 1) : this.sendViewDistance); -+ return sendViewDistance; -+ } -+ -+ public void setTargetSendViewDistance(final int distance) { -+ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) { -+ throw new IllegalArgumentException(Integer.toString(distance)); -+ } -+ this.sendViewDistance = distance; -+ } -+ -+ public int getTargetNoTickViewDistance() { -+ return (this.loadViewDistance == -1 ? this.getLoadDistance() : this.loadViewDistance) - 1; -+ } -+ -+ public void setTargetNoTickViewDistance(final int distance) { -+ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE)) { -+ throw new IllegalArgumentException(Integer.toString(distance)); -+ } -+ this.loadViewDistance = distance == -1 ? -1 : distance + 1; -+ } -+ -+ public int getTargetTickViewDistance() { -+ return this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; -+ } -+ -+ public void setTargetTickViewDistance(final int distance) { -+ if (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE) { -+ throw new IllegalArgumentException(Integer.toString(distance)); -+ } -+ this.tickViewDistance = distance; -+ } -+ -+ protected int getLoadDistance() { -+ final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; -+ -+ return Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance); -+ } -+ -+ public boolean hasSentChunk(final int chunkX, final int chunkZ) { -+ return this.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ -+ public void sendChunk(final int chunkX, final int chunkZ, final Runnable onChunkSend) { -+ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { -+ this.player.getLevel().getChunkSource().chunkMap.updateChunkTracking(this.player, -+ new ChunkPos(chunkX, chunkZ), new Packet[2], false, true); // unloaded, loaded -+ this.player.connection.connection.execute(onChunkSend); -+ } else { -+ throw new IllegalStateException(); -+ } -+ } -+ -+ public void unloadChunk(final int chunkX, final int chunkZ) { -+ if (this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { -+ this.player.getLevel().getChunkSource().chunkMap.updateChunkTracking(this.player, -+ new ChunkPos(chunkX, chunkZ), null, true, false); // unloaded, loaded -+ } -+ } -+ -+ protected static boolean triangleIntersects(final double p1x, final double p1z, // triangle point -+ final double p2x, final double p2z, // triangle point -+ final double p3x, final double p3z, // triangle point -+ -+ final double targetX, final double targetZ) { // point -+ // from barycentric coordinates: -+ // targetX = a*p1x + b*p2x + c*p3x -+ // targetZ = a*p1z + b*p2z + c*p3z -+ // 1.0 = a*1.0 + b*1.0 + c*1.0 -+ // where a, b, c >= 0.0 -+ // so, if any of a, b, c are less-than zero then there is no intersection. -+ -+ // d = ((p2z - p3z)(p1x - p3x) + (p3x - p2x)(p1z - p3z)) -+ // a = ((p2z - p3z)(targetX - p3x) + (p3x - p2x)(targetZ - p3z)) / d -+ // b = ((p3z - p1z)(targetX - p3x) + (p1x - p3x)(targetZ - p3z)) / d -+ // c = 1.0 - a - b -+ -+ final double d = (p2z - p3z)*(p1x - p3x) + (p3x - p2x)*(p1z - p3z); -+ final double a = ((p2z - p3z)*(targetX - p3x) + (p3x - p2x)*(targetZ - p3z)) / d; -+ -+ if (a < 0.0 || a > 1.0) { -+ return false; -+ } -+ -+ final double b = ((p3z - p1z)*(targetX - p3x) + (p1x - p3x)*(targetZ - p3z)) / d; -+ if (b < 0.0 || b > 1.0) { -+ return false; -+ } -+ -+ final double c = 1.0 - a - b; -+ -+ return c >= 0.0 && c <= 1.0; -+ } -+ -+ public void remove() { -+ this.loader.broadcastMap.remove(this.player); -+ this.loader.loadMap.remove(this.player); -+ this.loader.loadTicketCleanup.remove(this.player); -+ this.loader.tickMap.remove(this.player); -+ } -+ -+ protected int getClientViewDistance() { -+ return this.player.clientViewDistance == null ? -1 : this.player.clientViewDistance.intValue(); -+ } -+ -+ public void update() { -+ final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; -+ // load view cannot be less-than tick view + 1 -+ final int loadViewDistance = Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance); -+ // send view cannot be greater-than load view -+ final int clientViewDistance = this.getClientViewDistance(); -+ final int sendViewDistance = Math.min(loadViewDistance, this.sendViewDistance == -1 ? (!PaperConfig.playerAutoConfigureSendViewDistance || clientViewDistance == -1 ? this.loader.getSendDistance() : clientViewDistance + 1) : this.sendViewDistance); -+ -+ final double posX = this.player.getX(); -+ final double posZ = this.player.getZ(); -+ final float yaw = MCUtil.normalizeYaw(this.player.yRot + 90.0f); // mc yaw 0 is along the positive z axis, but obviously this is really dumb - offset so we are at positive x-axis -+ -+ // in general, we really only want to prioritise chunks in front if we know we're moving pretty fast into them. -+ final boolean useLookPriority = PaperConfig.playerFrustumPrioritisation && (this.player.getDeltaMovement().horizontalDistanceSqr() > LOOK_PRIORITY_SPEED_THRESHOLD || -+ this.player.getAbilities().flying); -+ -+ // make sure we're in the send queue -+ this.loader.chunkSendWaitQueue.add(this); -+ -+ if ( -+ // has view distance stayed the same? -+ sendViewDistance == this.lastSendDistance -+ && loadViewDistance == this.lastLoadDistance -+ && tickViewDistance == this.lastTickDistance -+ -+ && (this.usingLookingPriority ? ( -+ // has our block stayed the same (this also accounts for chunk change)? -+ Mth.floor(this.lastLocX) == Mth.floor(posX) -+ && Mth.floor(this.lastLocZ) == Mth.floor(posZ) -+ ) : ( -+ // has our chunk stayed the same -+ (Mth.floor(this.lastLocX) >> 4) == (Mth.floor(posX) >> 4) -+ && (Mth.floor(this.lastLocZ) >> 4) == (Mth.floor(posZ) >> 4) -+ )) -+ -+ // has our decision about look priority changed? -+ && this.usingLookingPriority == useLookPriority -+ -+ // if we are currently using look priority, has our yaw stayed within recalc threshold? -+ && (!this.usingLookingPriority || Math.abs(yaw - this.lastYaw) <= LOOK_PRIORITY_YAW_DELTA_RECALC_THRESHOLD) -+ ) { -+ // nothing we care about changed, so we're not re-calculating -+ return; -+ } -+ -+ final int centerChunkX = Mth.floor(posX) >> 4; -+ final int centerChunkZ = Mth.floor(posZ) >> 4; -+ -+ this.player.needsChunkCenterUpdate = true; -+ this.loader.broadcastMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, sendViewDistance); -+ this.player.needsChunkCenterUpdate = false; -+ this.loader.loadMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, loadViewDistance); -+ this.loader.loadTicketCleanup.addOrUpdate(this.player, centerChunkX, centerChunkZ, loadViewDistance + 1); -+ this.loader.tickMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, tickViewDistance); -+ -+ if (sendViewDistance != this.lastSendDistance) { -+ // update the view radius for client -+ // note that this should be after the map calls because the client wont expect unload calls not in its VD -+ // and it's possible we decreased VD here -+ this.player.connection.send(new ClientboundSetChunkCacheRadiusPacket(sendViewDistance - 1)); // client already expects the 1 radius neighbours, so subtract 1. -+ } -+ -+ this.lastLocX = posX; -+ this.lastLocZ = posZ; -+ this.lastYaw = yaw; -+ this.lastSendDistance = sendViewDistance; -+ this.lastLoadDistance = loadViewDistance; -+ this.lastTickDistance = tickViewDistance; -+ this.usingLookingPriority = useLookPriority; -+ -+ this.lastChunkX = centerChunkX; -+ this.lastChunkZ = centerChunkZ; -+ -+ // points for player "view" triangle: -+ -+ // obviously, the player pos is a vertex -+ final double p1x = posX; -+ final double p1z = posZ; -+ -+ // to the left of the looking direction -+ final double p2x = PRIORITISED_DISTANCE * Math.cos(Math.toRadians(yaw + (double)(FOV / 2.0))) // calculate rotated vector -+ + p1x; // offset vector -+ final double p2z = PRIORITISED_DISTANCE * Math.sin(Math.toRadians(yaw + (double)(FOV / 2.0))) // calculate rotated vector -+ + p1z; // offset vector -+ -+ // to the right of the looking direction -+ final double p3x = PRIORITISED_DISTANCE * Math.cos(Math.toRadians(yaw - (double)(FOV / 2.0))) // calculate rotated vector -+ + p1x; // offset vector -+ final double p3z = PRIORITISED_DISTANCE * Math.sin(Math.toRadians(yaw - (double)(FOV / 2.0))) // calculate rotated vector -+ + p1z; // offset vector -+ -+ // now that we have all of our points, we can recalculate the load queue -+ -+ final List loadQueue = new ArrayList<>(); -+ -+ // clear send queue, we are re-sorting -+ this.sendQueue.clear(); -+ -+ final int searchViewDistance = Math.max(loadViewDistance, sendViewDistance); -+ -+ for (int dx = -searchViewDistance; dx <= searchViewDistance; ++dx) { -+ for (int dz = -searchViewDistance; dz <= searchViewDistance; ++dz) { -+ final int chunkX = dx + centerChunkX; -+ final int chunkZ = dz + centerChunkZ; -+ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); -+ -+ if (this.hasSentChunk(chunkX, chunkZ)) { -+ // already sent (which means it is also loaded) -+ continue; -+ } -+ -+ final boolean loadChunk = squareDistance <= loadViewDistance; -+ final boolean sendChunk = squareDistance <= sendViewDistance; -+ -+ final boolean prioritised = useLookPriority && triangleIntersects( -+ // prioritisation triangle -+ p1x, p1z, p2x, p2z, p3x, p3z, -+ -+ // center of chunk -+ (double)((chunkX << 4) | 8), (double)((chunkZ << 4) | 8) -+ ); -+ -+ -+ final int manhattanDistance = (Math.abs(dx) + Math.abs(dz)); -+ -+ final double priority; -+ -+ if (squareDistance <= PaperConfig.playerMinChunkLoadRadius) { -+ // priority should be negative, and we also want to order it from center outwards -+ // so we want (0,0) to be the smallest, and (minLoadedRadius,minLoadedRadius) to be the greatest -+ priority = -((2 * PaperConfig.playerMinChunkLoadRadius + 1) - (dx + dz)); -+ } else { -+ if (prioritised) { -+ // we don't prioritise these chunks above others because we also want to make sure some chunks -+ // will be loaded if the player changes direction -+ priority = (double)manhattanDistance / 6.0; -+ } else { -+ priority = (double)manhattanDistance; -+ } -+ } -+ -+ final ChunkPriorityHolder holder = new ChunkPriorityHolder(chunkX, chunkZ, manhattanDistance, priority); -+ -+ if (!this.loader.isChunkPlayerLoaded(chunkX, chunkZ)) { -+ if (loadChunk) { -+ loadQueue.add(holder); -+ } -+ } else { -+ // loaded but not sent: so queue it! -+ if (sendChunk) { -+ this.sendQueue.add(holder); -+ } -+ } -+ } -+ } -+ -+ loadQueue.sort((final ChunkPriorityHolder p1, final ChunkPriorityHolder p2) -> { -+ return Double.compare(p1.priority, p2.priority); -+ }); -+ -+ // we're modifying loadQueue, must remove -+ this.loader.chunkLoadQueue.remove(this); -+ -+ this.loadQueue.clear(); -+ this.loadQueue.addAll(loadQueue); -+ -+ // must re-add -+ this.loader.chunkLoadQueue.add(this); -+ } -+ } -+} -diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/network/Connection.java -+++ b/src/main/java/net/minecraft/network/Connection.java -@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> { - public boolean queueImmunity = false; - public ConnectionProtocol protocol; - // Paper end -+ // Paper start - add pending task queue -+ private final Queue pendingTasks = new java.util.concurrent.ConcurrentLinkedQueue<>(); -+ public void execute(final Runnable run) { -+ if (this.channel == null || !this.channel.isRegistered()) { -+ run.run(); -+ return; -+ } -+ final boolean queue = !this.queue.isEmpty(); -+ if (!queue) { -+ this.channel.eventLoop().execute(run); -+ } else { -+ this.pendingTasks.add(run); -+ if (this.queue.isEmpty()) { -+ // something flushed async, dump tasks now -+ Runnable r; -+ while ((r = this.pendingTasks.poll()) != null) { -+ this.channel.eventLoop().execute(r); -+ } -+ } -+ } -+ } -+ // Paper end - add pending task queue - - // Paper start - allow controlled flushing - volatile boolean canFlush = true; -@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> { - return false; - } - private boolean processQueue() { -+ try { // Paper - add pending task queue - if (this.queue.isEmpty()) return true; - // Paper start - make only one flush call per sendPacketQueue() call - final boolean needsFlush = this.canFlush; -@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> { - } - } - return true; -+ } finally { // Paper start - add pending task queue -+ Runnable r; -+ while ((r = this.pendingTasks.poll()) != null) { -+ this.channel.eventLoop().execute(r); -+ } -+ } // Paper end - add pending task queue - } - // Paper end - -diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/MCUtil.java -+++ b/src/main/java/net/minecraft/server/MCUtil.java -@@ -0,0 +0,0 @@ public final class MCUtil { - - worldData.addProperty("name", world.getWorld().getName()); - worldData.addProperty("view-distance", world.getChunkSource().chunkMap.getEffectiveViewDistance()); -- worldData.addProperty("no-view-distance", world.getChunkSource().chunkMap.getRawNoTickViewDistance()); -+ worldData.addProperty("no-view-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()); // Paper - replace old player chunk management - worldData.addProperty("keep-spawn-loaded", world.keepSpawnInMemory); - worldData.addProperty("keep-spawn-loaded-range", world.paperConfig.keepLoadedRange); - worldData.addProperty("visible-chunk-count", visibleChunks.size()); -diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkHolder.java -+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java -@@ -0,0 +0,0 @@ public class ChunkHolder { - // Paper start - per player view distance - // there can be potential desync with player's last mapped section and the view distance map, so use the - // view distance map here. -- com.destroystokyo.paper.util.misc.PlayerAreaMap viewDistanceMap = this.chunkMap.playerViewDistanceBroadcastMap; -+ com.destroystokyo.paper.util.misc.PlayerAreaMap viewDistanceMap = this.chunkMap.playerChunkManager.broadcastMap; // Paper - replace old player chunk manager - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = viewDistanceMap.getObjectsInRange(this.pos); - if (players == null) { - return; -@@ -0,0 +0,0 @@ public class ChunkHolder { - - int viewDistance = viewDistanceMap.getLastViewDistance(player); - long lastPosition = viewDistanceMap.getLastCoordinate(player); -+ if (!this.chunkMap.playerChunkManager.isChunkSent(player, this.pos.x, this.pos.z)) continue; // Paper - replace player chunk management - - int distX = Math.abs(net.minecraft.server.MCUtil.getCoordinateX(lastPosition) - this.pos.x); - int distZ = Math.abs(net.minecraft.server.MCUtil.getCoordinateZ(lastPosition) - this.pos.z); -@@ -0,0 +0,0 @@ public class ChunkHolder { - continue; - } - ServerPlayer player = (ServerPlayer)temp; -+ if (!this.chunkMap.playerChunkManager.isChunkSent(player, this.pos.x, this.pos.z)) continue; // Paper - replace player chunk management - player.connection.send(packet); - } - } -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkMap.java -+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - final CallbackExecutor chunkLoadConversionCallbackExecutor = new CallbackExecutor(); // Paper - // Paper start - distance maps - private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>(); -- // Paper start - no-tick view distance -- int noTickViewDistance; -- public final int getRawNoTickViewDistance() { -- return this.noTickViewDistance; -- } -- public final int getEffectiveNoTickViewDistance() { -- return this.noTickViewDistance == -1 ? this.getEffectiveViewDistance() : this.noTickViewDistance; -- } -- public final int getLoadViewDistance() { -- return Math.max(this.getEffectiveViewDistance(), this.getEffectiveNoTickViewDistance()); -- } -- -- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceBroadcastMap; -- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceTickMap; -- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceNoTickMap; -- // Paper end - no-tick view distance -+ public final io.papermc.paper.chunk.PlayerChunkLoader playerChunkManager = new io.papermc.paper.chunk.PlayerChunkLoader(this, this.pooledLinkedPlayerHashSets); // Paper - replace chunk loader - // Paper start - use distance map to optimise tracker - public static boolean isLegacyTrackingEntity(Entity entity) { - return entity.isLegacyTrackingEntity; -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; - int trackRange = this.entityTrackerTrackRanges[i]; - -- trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance())); -+ trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, player.getBukkitEntity().getViewDistance())); // Paper - per player view distances - } - // Paper end - use distance map to optimise entity tracker - // Paper start - optimise PlayerChunkMap#isOutsideRange -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - // Paper start - optimise PlayerChunkMap#isOutsideRange - this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); - // Paper end - optimise PlayerChunkMap#isOutsideRange -- // Paper start - no-tick view distance -- int effectiveTickViewDistance = this.getEffectiveViewDistance(); -- int effectiveNoTickViewDistance = Math.max(this.getEffectiveNoTickViewDistance(), effectiveTickViewDistance); -- -- if (!this.skipPlayer(player)) { -- this.playerViewDistanceTickMap.add(player, chunkX, chunkZ, effectiveTickViewDistance); -- this.playerViewDistanceNoTickMap.add(player, chunkX, chunkZ, effectiveNoTickViewDistance + 2); // clients need chunk 1 neighbour, and we need another 1 for sending those extra neighbours (as we require neighbours to send) -- } -- -- player.needsChunkCenterUpdate = true; -- this.playerViewDistanceBroadcastMap.add(player, chunkX, chunkZ, effectiveNoTickViewDistance + 1); // clients need an extra neighbour to render the full view distance configured -- player.needsChunkCenterUpdate = false; -- // Paper end - no-tick view distance -+ this.playerChunkManager.addPlayer(player); // Paper - replace chunk loader - } - - void removePlayerFromDistanceMaps(ServerPlayer player) { -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - this.playerMobSpawnMap.remove(player); - this.playerChunkTickRangeMap.remove(player); - // Paper end - optimise PlayerChunkMap#isOutsideRange -- // Paper start - no-tick view distance -- this.playerViewDistanceBroadcastMap.remove(player); -- this.playerViewDistanceTickMap.remove(player); -- this.playerViewDistanceNoTickMap.remove(player); -- // Paper end - no-tick view distance -+ this.playerChunkManager.removePlayer(player); // Paper - replace chunk loader - } - - void updateMaps(ServerPlayer player) { -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; - int trackRange = this.entityTrackerTrackRanges[i]; - -- trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance())); -+ trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, player.getBukkitEntity().getViewDistance())); // Paper - per player view distances - } - // Paper end - use distance map to optimise entity tracker - // Paper start - optimise PlayerChunkMap#isOutsideRange - this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); - // Paper end - optimise PlayerChunkMap#isOutsideRange -- // Paper start - no-tick view distance -- int effectiveTickViewDistance = this.getEffectiveViewDistance(); -- int effectiveNoTickViewDistance = Math.max(this.getEffectiveNoTickViewDistance(), effectiveTickViewDistance); -- -- if (!this.skipPlayer(player)) { -- this.playerViewDistanceTickMap.update(player, chunkX, chunkZ, effectiveTickViewDistance); -- this.playerViewDistanceNoTickMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 2); // clients need chunk 1 neighbour, and we need another 1 for sending those extra neighbours (as we require neighbours to send) -- } -- -- player.needsChunkCenterUpdate = true; -- this.playerViewDistanceBroadcastMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 1); // clients need an extra neighbour to render the full view distance configured -- player.needsChunkCenterUpdate = false; -- // Paper end - no-tick view distance -+ this.playerChunkManager.updatePlayer(player); // Paper - replace chunk loader - } - // Paper end - // Paper start -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - this.regionManagers.add(this.dataRegionManager); - // Paper end - // Paper start - no-tick view distance -- this.setNoTickViewDistance(this.level.paperConfig.noTickViewDistance); -- this.playerViewDistanceTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, -- (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, -- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { -- if (newState.size() != 1) { -- return; -- } -- LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(rangeX, rangeZ); -- if (chunk == null || !chunk.areNeighboursLoaded(2)) { -- return; -- } -- -- ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); -- ChunkMap.this.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, 31, chunkPos); // entity ticking level, TODO check on update -- }, -- (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, -- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { -- if (newState != null) { -- return; -- } -- ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); -- ChunkMap.this.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, 31, chunkPos); // entity ticking level, TODO check on update -- }); -- this.playerViewDistanceNoTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets); -- this.playerViewDistanceBroadcastMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, -- (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, -- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { -- if (player.needsChunkCenterUpdate) { -- player.needsChunkCenterUpdate = false; -- player.connection.send(new ClientboundSetChunkCacheCenterPacket(currPosX, currPosZ)); -- } -- ChunkMap.this.updateChunkTracking(player, new ChunkPos(rangeX, rangeZ), new Packet[2], false, true); // unloaded, loaded -- }, -- (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, -- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { -- ChunkMap.this.updateChunkTracking(player, new ChunkPos(rangeX, rangeZ), null, true, false); // unloaded, loaded -- }); -+ this.setNoTickViewDistance(this.level.paperConfig.noTickViewDistance); // Paper - replace chunk loading system - // Paper end - no-tick view distance - this.playerMobDistanceMap = this.level.paperConfig.perPlayerMobSpawns ? new com.destroystokyo.paper.util.PlayerMobDistanceMap() : null; // Paper - // Paper start - use distance map to optimise entity tracker -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - public void checkHighPriorityChunks(ServerPlayer player) { -+ if (true) return; // Paper - replace player chunk loader - int currentTick = MinecraftServer.currentTick; - if (currentTick - player.lastHighPriorityChecked < 20 || !player.isRealPlayer) { // weed out fake players - return; -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - player.lastHighPriorityChecked = currentTick; - it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap priorities = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(); - -- int viewDistance = getEffectiveNoTickViewDistance(); -+ int viewDistance = 10;//int viewDistance = getEffectiveNoTickViewDistance(); // Paper - replace player chunk loader - net.minecraft.core.BlockPos.MutableBlockPos pos = new net.minecraft.core.BlockPos.MutableBlockPos(); - - // Prioritize circular near -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - private boolean shouldSkipPrioritization(ChunkPos coord) { -- if (playerViewDistanceNoTickMap.getObjectsInRange(coord.toLong()) == null) return true; -+ if (true) return true; // Paper - replace player chunk loader - unused outside paper player loader logic - ChunkHolder chunk = getUpdatingChunkIfPresent(coord.toLong()); - return chunk != null && (chunk.isFullChunkReady()); - } -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - int k = this.viewDistance; - - this.viewDistance = j; -- this.setNoTickViewDistance(this.getRawNoTickViewDistance()); // Paper - no-tick view distance - propagate changes to no-tick, which does the actual chunk loading/sending -+ this.playerChunkManager.setTickDistance(Mth.clamp(watchDistance, 2, 32)); // Paper - replace player loader system - } - - } -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - // Paper start - no-tick view distance - public final void setNoTickViewDistance(int viewDistance) { - viewDistance = viewDistance == -1 ? -1 : Mth.clamp(viewDistance, 2, 32); -- -- this.noTickViewDistance = viewDistance; -- int loadViewDistance = this.getLoadViewDistance(); -- this.distanceManager.setNoTickViewDistance(loadViewDistance + 2 + 2); // add 2 to account for the change to 31 -> 33 tickets // see notes in the distance map updating for the other + 2 -- -- if (this.level != null && this.level.players != null) { // this can be called from constructor, where these aren't set -- for (ServerPlayer player : this.level.players) { -- net.minecraft.server.network.ServerGamePacketListenerImpl connection = player.connection; -- if (connection != null) { -- // moved in from PlayerList -- connection.send(new net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket(loadViewDistance)); -- } -- this.updateMaps(player); -- // Paper end - no-tick view distance -- } -- } -+ this.playerChunkManager.setLoadDistance(viewDistance == -1 ? -1 : viewDistance + 1); // Paper - replace player loader system - add 1 here, we need an extra one to send to clients for chunks in this viewDistance to render - - } - -- protected void updateChunkTracking(ServerPlayer player, ChunkPos pos, Packet[] packets, boolean withinMaxWatchDistance, boolean withinViewDistance) { -+ public void updateChunkTracking(ServerPlayer player, ChunkPos pos, Packet[] packets, boolean withinMaxWatchDistance, boolean withinViewDistance) { // Paper - public - if (player.level == this.level) { - if (withinViewDistance && !withinMaxWatchDistance) { - ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong()); -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - */ // Paper end - replaced by distance map - - this.updateMaps(player); // Paper - distance maps -+ this.playerChunkManager.updatePlayer(player); // Paper - respond to movement immediately - - } - -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - // Paper start - per player view distance - // there can be potential desync with player's last mapped section and the view distance map, so use the - // view distance map here. -- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = this.playerViewDistanceBroadcastMap.getObjectsInRange(chunkPos); -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = this.playerChunkManager.broadcastMap.getObjectsInRange(chunkPos); // Paper - replace player chunk loader system - - if (inRange == null) { - return Stream.empty(); -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - continue; - } - ServerPlayer player = (ServerPlayer)temp; -- int viewDistance = this.playerViewDistanceBroadcastMap.getLastViewDistance(player); -- long lastPosition = this.playerViewDistanceBroadcastMap.getLastCoordinate(player); -+ if (!this.playerChunkManager.isChunkSent(player, chunkPos.x, chunkPos.z)) continue; // Paper - replace player chunk management -+ int viewDistance = this.playerChunkManager.broadcastMap.getLastViewDistance(player); // Paper - replace player chunk loader system -+ long lastPosition = this.playerChunkManager.broadcastMap.getLastCoordinate(player); // Paper - replace player chunk loader system - - int distX = Math.abs(MCUtil.getCoordinateX(lastPosition) - chunkPos.x); - int distZ = Math.abs(MCUtil.getCoordinateZ(lastPosition) - chunkPos.z); -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - continue; - } - ServerPlayer player = (ServerPlayer)temp; -+ if (!this.playerChunkManager.isChunkSent(player, chunkPos.x, chunkPos.z)) continue; // Paper - replace player chunk management - players.add(player); - } - } -@@ -0,0 +0,0 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially - double vec3d_dy = player.getY() - this.entity.getY(); - double vec3d_dz = player.getZ() - this.entity.getZ(); - // Paper end - remove allocation of Vec3D here -- int i = Math.min(this.getEffectiveRange(), (ChunkMap.this.viewDistance - 1) * 16); -+ int i = Math.min(this.getEffectiveRange(), player.getBukkitEntity().getViewDistance() * 16); // Paper - per player view distance - boolean flag = vec3d_dx >= (double) (-i) && vec3d_dx <= (double) i && vec3d_dz >= (double) (-i) && vec3d_dz <= (double) i && this.entity.broadcastToPlayer(player); // Paper - remove allocation of Vec3D here - - // CraftBukkit start - respect vanish API -diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/DistanceManager.java -+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - public final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap(); - private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); - public static final int MOB_SPAWN_RANGE = 8; // private final ChunkMapDistance.b f = new ChunkMapDistance.b(8); // Paper - no longer used -- private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33); -+ //private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33); // Paper - no longer used - // Paper start use a queue, but still keep unique requirement - public final java.util.Queue pendingChunkUpdates = new java.util.ArrayDeque() { - @Override -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - public boolean runAllUpdates(ChunkMap playerchunkmap) { - //this.f.a(); // Paper - no longer used - org.spigotmc.AsyncCatcher.catchOp("DistanceManagerTick"); // Paper -- this.playerTicketManager.runAllUpdates(); -+ //this.playerTicketManager.runAllUpdates(); // Paper - no longer used - int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE); - boolean flag = i != 0; - -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::addPriorityTicket"); - long pair = coords.toLong(); - ChunkHolder chunk = chunkMap.getUpdatingChunkIfPresent(pair); -- boolean needsTicket = chunkMap.playerViewDistanceNoTickMap.getObjectsInRange(pair) != null && !hasPlayerTicket(coords, 33); -+ boolean needsTicket = false; // Paper - replace old loader system - - if (needsTicket) { - Ticket ticket = new Ticket<>(TicketType.PLAYER, 33, coords); -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - return new ObjectOpenHashSet(); - })).add(player); - //this.f.update(i, 0, true); // Paper - no longer used -- this.playerTicketManager.update(i, 0, true); -+ //this.playerTicketManager.update(i, 0, true); // Paper - no longer used - } - - public void removePlayer(SectionPos pos, ServerPlayer player) { -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - if (objectset == null || objectset.isEmpty()) { // Paper - this.playersPerChunk.remove(i); - //this.f.update(i, Integer.MAX_VALUE, false); // Paper - no longer used -- this.playerTicketManager.update(i, Integer.MAX_VALUE, false); -+ //this.playerTicketManager.update(i, Integer.MAX_VALUE, false); // Paper - no longer used - } - - } -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - } - - protected void setNoTickViewDistance(int i) { // Paper - force abi breakage on usage change -- this.playerTicketManager.updateViewDistance(i); -+ throw new UnsupportedOperationException("use world api"); // Paper - no longer relevant - } - - public int getNaturalSpawnChunkCount() { -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - } - } - -+ /* Paper - replace old loader system - private class FixedPlayerDistanceChunkTracker extends ChunkTracker { - - protected final Long2ByteMap chunks = new Long2ByteOpenHashMap(); -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - } - // Paper end - } -+ */ // Paper - replace old loader system - } -diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java -+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - this.level.timings.doChunkMap.stopTiming(); // Spigot - this.level.getProfiler().popPush("chunks"); - this.level.timings.chunks.startTiming(); // Paper - timings -+ this.chunkMap.playerChunkManager.tick(); // Paper - this is mostly is to account for view distance changes - this.tickChunks(); - this.level.timings.chunks.stopTiming(); // Paper - timings - this.level.timings.doChunkUnload.startTiming(); // Spigot -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - public boolean pollTask() { - try { - boolean execChunkTask = com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue() || ServerChunkCache.this.level.asyncChunkTaskManager.pollNextChunkTask(); // Paper -+ ServerChunkCache.this.chunkMap.playerChunkManager.tickMidTick(); // Paper - if (ServerChunkCache.this.runDistanceManagerUpdates()) { - return true; - } else { -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -0,0 +0,0 @@ public class ServerPlayer extends Player { - - public double lastEntitySpawnRadiusSquared; // Paper - optimise isOutsideRange, this field is in blocks - public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet cachedSingleHashSet; // Paper -- boolean needsChunkCenterUpdate; // Paper - no-tick view distance -+ public boolean needsChunkCenterUpdate; // Paper - no-tick view distance // Paper - public - public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - there are a lot of changes to do if we change all methods leading to the event - - public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile) { -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -0,0 +0,0 @@ public abstract class PlayerList { - boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO); - - // Spigot - view distance -- playerconnection.send(new ClientboundLoginPacket(player.getId(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), worlddata.isHardcore(), this.server.levelKeys(), this.registryHolder, worldserver1.dimensionType(), worldserver1.dimension(), this.getMaxPlayers(), worldserver1.getChunkSource().chunkMap.getLoadViewDistance(), flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat())); // Paper - no-tick view distance -+ playerconnection.send(new ClientboundLoginPacket(player.getId(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), worlddata.isHardcore(), this.server.levelKeys(), this.registryHolder, worldserver1.dimensionType(), worldserver1.dimension(), this.getMaxPlayers(), worldserver1.getChunkSource().chunkMap.playerChunkManager.getLoadDistance(), flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat())); // Paper - no-tick view distance // Paper - replace old player chunk management - player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit - playerconnection.send(new ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.BRAND, (new FriendlyByteBuf(Unpooled.buffer())).writeUtf(this.getServer().getServerModName()))); - playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked())); -@@ -0,0 +0,0 @@ public abstract class PlayerList { - // CraftBukkit start - LevelData worlddata = worldserver1.getLevelData(); - entityplayer1.connection.send(new ClientboundRespawnPacket(worldserver1.dimensionType(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), entityplayer1.gameMode.getGameModeForPlayer(), entityplayer1.gameMode.getPreviousGameModeForPlayer(), worldserver1.isDebug(), worldserver1.isFlat(), flag)); -- entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getChunkSource().chunkMap.getLoadViewDistance())); // Spigot // Paper - no-tick view distance -+ entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getLoadDistance())); // Spigot // Paper - no-tick view distance// Paper - replace old player chunk management - entityplayer1.setLevel(worldserver1); - entityplayer1.unsetRemoved(); - entityplayer1.connection.teleport(new Location(worldserver1.getWorld(), entityplayer1.getX(), entityplayer1.getY(), entityplayer1.getZ(), entityplayer1.getYRot(), entityplayer1.getXRot())); -@@ -0,0 +0,0 @@ public abstract class PlayerList { - // Really shouldn't happen... - backingSet = world != null ? world.players.toArray() : players.toArray(); - } else { -- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearbyPlayers = chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(MCUtil.fastFloor(x) >> 4, MCUtil.fastFloor(z) >> 4); -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearbyPlayers = chunkMap.playerChunkManager.broadcastMap.getObjectsInRange(MCUtil.fastFloor(x) >> 4, MCUtil.fastFloor(z) >> 4); // Paper - replace old player chunk management - if (nearbyPlayers == null) { - return; - } -diff --git a/src/main/java/net/minecraft/world/item/EnderEyeItem.java b/src/main/java/net/minecraft/world/item/EnderEyeItem.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/item/EnderEyeItem.java -+++ b/src/main/java/net/minecraft/world/item/EnderEyeItem.java -@@ -0,0 +0,0 @@ public class EnderEyeItem extends Item { - - // CraftBukkit start - Use relative location for far away sounds - // world.b(1038, blockposition1.c(1, 0, 1), 0); -- int viewDistance = world.getCraftServer().getViewDistance() * 16; -+ //int viewDistance = world.getCraftServer().getViewDistance() * 16; // Paper - apply view distance patch - BlockPos soundPos = blockposition1.offset(1, 0, 1); - for (ServerPlayer player : world.getServer().getPlayerList().players) { -+ final int viewDistance = player.getBukkitEntity().getViewDistance(); // Paper - apply view distance patch - double deltaX = soundPos.getX() - player.getX(); - double deltaZ = soundPos.getZ() - player.getZ(); - double distanceSquared = deltaX * deltaX + deltaZ * deltaZ; -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i); - // Paper start - per player view distance - allow block updates for non-ticking chunks in player view distance - // if copied from above -- } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || ((ServerLevel)this).getChunkSource().chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(MCUtil.getCoordinateKey(blockposition)) != null)) { -+ } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || ((ServerLevel)this).getChunkSource().chunkMap.playerChunkManager.broadcastMap.getObjectsInRange(MCUtil.getCoordinateKey(blockposition)) != null)) { // Paper - replace old player chunk management - ((ServerLevel)this).getChunkSource().blockChanged(blockposition); - // Paper end - per player view distance - } -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -@@ -0,0 +0,0 @@ public class LevelChunk implements ChunkAccess { - ChunkMap chunkMap = chunkProviderServer.chunkMap; - // this code handles the addition of ticking tickets - the distance map handles the removal - if (!areNeighboursLoaded(bitsetBefore, 2) && areNeighboursLoaded(bitsetAfter, 2)) { -- if (chunkMap.playerViewDistanceTickMap.getObjectsInRange(this.coordinateKey) != null) { -+ if (chunkMap.playerChunkManager.tickMap.getObjectsInRange(this.coordinateKey) != null) { // Paper - replace old player chunk loading system - // now we're ready for entity ticking - chunkProviderServer.mainThreadProcessor.execute(() -> { - // double check that this condition still holds. -- if (LevelChunk.this.areNeighboursLoaded(2) && chunkMap.playerViewDistanceTickMap.getObjectsInRange(LevelChunk.this.coordinateKey) != null) { -+ if (LevelChunk.this.areNeighboursLoaded(2) && chunkMap.playerChunkManager.tickMap.getObjectsInRange(LevelChunk.this.coordinateKey) != null) { // Paper - replace old player chunk loading system -+ chunkMap.playerChunkManager.onChunkPlayerTickReady(this.chunkPos.x, this.chunkPos.z); // Paper - replace old player chunk - chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.PLAYER, LevelChunk.this.chunkPos, 31, LevelChunk.this.chunkPos); // 31 -> entity ticking, TODO check on update - } - }); -@@ -0,0 +0,0 @@ public class LevelChunk implements ChunkAccess { - - // this code handles the chunk sending - if (!areNeighboursLoaded(bitsetBefore, 1) && areNeighboursLoaded(bitsetAfter, 1)) { -- if (chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(this.coordinateKey) != null) { -- // now we're ready to send -- chunkMap.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(chunkMap.getUpdatingChunkIfPresent(this.coordinateKey), (() -> { // Copied frm PlayerChunkMap -- // double check that this condition still holds. -- if (!LevelChunk.this.areNeighboursLoaded(1)) { -- return; -- } -- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(LevelChunk.this.coordinateKey); -- if (inRange == null) { -- return; -- } -- -- // broadcast -- Object[] backingSet = inRange.getBackingSet(); -- Packet[] chunkPackets = new Packet[2]; -- for (int index = 0, len = backingSet.length; index < len; ++index) { -- Object temp = backingSet[index]; -- if (!(temp instanceof net.minecraft.server.level.ServerPlayer)) { -- continue; -- } -- net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)temp; -- chunkMap.playerLoadedChunk(player, chunkPackets, LevelChunk.this); -- } -- }))); -- } -+ // Paper start - replace old player chunk loading system -+ chunkProviderServer.mainThreadProcessor.execute(() -> { -+ if (!LevelChunk.this.areNeighboursLoaded(1)) { -+ return; -+ } -+ LevelChunk.this.postProcessGeneration(); -+ if (!LevelChunk.this.areNeighboursLoaded(1)) { -+ return; -+ } -+ chunkMap.playerChunkManager.onChunkSendReady(this.chunkPos.x, this.chunkPos.z); -+ }); -+ // Paper end - replace old player chunk loading system - } - // Paper end - no-tick view distance - } -@@ -0,0 +0,0 @@ public class LevelChunk implements ChunkAccess { - // Paper end - neighbour cache - org.bukkit.Server server = this.level.getCraftServer(); - this.level.getChunkSource().addLoadedChunk(this); // Paper -+ ((ServerLevel)this.level).getChunkSource().chunkMap.playerChunkManager.onChunkLoad(this.chunkPos.x, this.chunkPos.z); // Paper - rewrite player chunk management - if (server != null) { - /* - * If it's a new world, the first few chunks are generated inside -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World { - throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]"); - } - net.minecraft.server.level.ChunkMap chunkMap = getHandle().getChunkSource().chunkMap; -- if (viewDistance != chunkMap.getEffectiveViewDistance()) { -+ if (true) { // Paper - replace old player chunk management - chunkMap.setViewDistance(viewDistance); - } - } - - @Override - public int getNoTickViewDistance() { -- return getHandle().getChunkSource().chunkMap.getEffectiveNoTickViewDistance(); -+ return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance(); // Paper - replace old player chunk management - } - - @Override -@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World { - throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]"); - } - net.minecraft.server.level.ChunkMap chunkMap = getHandle().getChunkSource().chunkMap; -- if (viewDistance != chunkMap.getRawNoTickViewDistance()) { -+ if (true) { // Paper - replace old player chunk management - chunkMap.setNoTickViewDistance(viewDistance); - } - } - // Paper end - per player view distance -+ // Paper start - add view distances -+ @Override -+ public int getSendViewDistance() { -+ return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance(); -+ } -+ -+ @Override -+ public void setSendViewDistance(int viewDistance) { -+ getHandle().getChunkSource().chunkMap.playerChunkManager.setTargetSendDistance(viewDistance); -+ } -+ // Paper end - add view distances - - // Spigot start - private final org.bukkit.World.Spigot spigot = new org.bukkit.World.Spigot() -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - } - } - -+ // Paper start - implement view distances -+ @Override -+ public int getSendViewDistance() { -+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; -+ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); -+ if (data == null) { -+ return chunkMap.playerChunkManager.getTargetSendDistance(); -+ } -+ return data.getTargetSendViewDistance(); -+ } -+ -+ @Override -+ public void setSendViewDistance(int viewDistance) { -+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; -+ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); -+ if (data == null) { -+ throw new IllegalStateException("Player is not attached to world"); -+ } -+ -+ data.setTargetSendViewDistance(viewDistance); -+ } -+ -+ @Override -+ public int getNoTickViewDistance() { -+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; -+ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); -+ if (data == null) { -+ return chunkMap.playerChunkManager.getTargetNoTickViewDistance(); -+ } -+ return data.getTargetNoTickViewDistance(); -+ } -+ -+ @Override -+ public void setNoTickViewDistance(int viewDistance) { -+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; -+ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); -+ if (data == null) { -+ throw new IllegalStateException("Player is not attached to world"); -+ } -+ -+ data.setTargetNoTickViewDistance(viewDistance); -+ } -+ - @Override - public int getViewDistance() { -- throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; -+ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); -+ if (data == null) { -+ return chunkMap.playerChunkManager.getTargetViewDistance(); -+ } -+ return data.getTargetTickViewDistance(); - } - - @Override - public void setViewDistance(int viewDistance) { -- throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; -+ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); -+ if (data == null) { -+ throw new IllegalStateException("Player is not attached to world"); -+ } -+ -+ data.setTargetTickViewDistance(viewDistance); - } -+ // Paper end - implement view distances - - @Override - public T getClientOption(com.destroystokyo.paper.ClientOption type) { diff --git a/patches/unapplied/server/Replace-ticket-level-propagator.patch b/patches/unapplied/server/Replace-ticket-level-propagator.patch deleted file mode 100644 index 3d83702b3c..0000000000 --- a/patches/unapplied/server/Replace-ticket-level-propagator.patch +++ /dev/null @@ -1,248 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sun, 21 Mar 2021 16:25:42 -0700 -Subject: [PATCH] Replace ticket level propagator - -Mojang's propagator is slow, and this isn't surprising -given it's built on the same utilities the vanilla light engine -is built on. The simple propagator I wrote is approximately 4x -faster when simulating player movement. For a long time timing -reports have shown this function take up significant tick, ( -approx 10% or more), and async sampling data shows the level -propagation alone takes up a significant amount. So this -should help with that. A big side effect is that mid-tick -will be more effective, since more time will be allocated -to actually processing chunk tasks vs the ticket level updates. - -diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/DistanceManager.java -+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java -@@ -0,0 +0,0 @@ import net.minecraft.world.level.chunk.LevelChunk; - import org.apache.logging.log4j.LogManager; - import org.apache.logging.log4j.Logger; - -+import it.unimi.dsi.fastutil.longs.Long2IntLinkedOpenHashMap; // Paper - public abstract class DistanceManager { - - static final Logger LOGGER = LogManager.getLogger(); -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - 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.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); // Paper - replace ticket level propagator - public static final int MOB_SPAWN_RANGE = 8; // private final ChunkMapDistance.b f = new ChunkMapDistance.b(8); // Paper - no longer used - //private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33); // Paper - no longer used - // Paper start use a queue, but still keep unique requirement -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - this.mainThreadExecutor = mainThreadExecutor; - } - -+ // Paper start - replace ticket level propagator -+ protected final Long2IntLinkedOpenHashMap ticketLevelUpdates = new Long2IntLinkedOpenHashMap() { -+ @Override -+ protected void rehash(int newN) { -+ // no downsizing allowed -+ if (newN < this.n) { -+ return; -+ } -+ super.rehash(newN); -+ } -+ }; -+ protected final io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D ticketLevelPropagator = new io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D( -+ (long coordinate, byte oldLevel, byte newLevel) -> { -+ DistanceManager.this.ticketLevelUpdates.putAndMoveToLast(coordinate, convertBetweenTicketLevels(newLevel)); -+ } -+ ); -+ // 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 -+ -+ // this maps so that GOLDEN_TICKET + 1 will be 0 in the propagator, GOLDEN_TICKET will be 1, and so on -+ // we need GOLDEN_TICKET+1 as 0 because anything >= GOLDEN_TICKET+1 should be unloaded -+ public static int convertBetweenTicketLevels(final int level) { -+ return ChunkMap.MAX_CHUNK_DISTANCE - level + 1; -+ } -+ -+ protected final int getPropagatedTicketLevel(final long coordinate) { -+ return convertBetweenTicketLevels(this.ticketLevelPropagator.getLevel(coordinate)); -+ } -+ -+ protected final void updateTicketLevel(final long coordinate, final int ticketLevel) { -+ if (ticketLevel > ChunkMap.MAX_CHUNK_DISTANCE) { -+ this.ticketLevelPropagator.removeSource(coordinate); -+ } else { -+ this.ticketLevelPropagator.setSource(coordinate, convertBetweenTicketLevels(ticketLevel)); -+ } -+ } -+ // Paper end - replace ticket level propagator -+ - protected void purgeStaleTickets() { - ++this.ticketTickCounter; - ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - if ((entry.getValue()).removeIf((ticket) -> { // CraftBukkit - decompile error - return ticket.timedOut(this.ticketTickCounter); - })) { -- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false); -+ this.updateTicketLevel(entry.getLongKey(), getTicketLevelAt(entry.getValue())); // Paper - replace ticket level propagator - } - - if (((SortedArraySet) entry.getValue()).isEmpty()) { -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - @Nullable - protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k); - -+ protected long ticketLevelUpdateCount; // Paper - replace ticket level propagator - public boolean runAllUpdates(ChunkMap playerchunkmap) { - //this.f.a(); // Paper - no longer used - org.spigotmc.AsyncCatcher.catchOp("DistanceManagerTick"); // Paper - //this.playerTicketManager.runAllUpdates(); // Paper - no longer used -- int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE); -- boolean flag = i != 0; -+ boolean flag = this.ticketLevelPropagator.propagateUpdates(); // Paper - replace ticket level propagator - - if (flag) { - ; - } - -- // Paper start -- if (!this.pendingChunkUpdates.isEmpty()) { -- this.pollingPendingChunkUpdates = true; try { // Paper - Chunk priority -- while(!this.pendingChunkUpdates.isEmpty()) { -- ChunkHolder remove = this.pendingChunkUpdates.remove(); -- remove.isUpdateQueued = false; -- remove.updateFutures(playerchunkmap, this.mainThreadExecutor); -- } -- } finally { this.pollingPendingChunkUpdates = false; } // Paper - Chunk priority -- // Paper end -- return true; -- } else { -- if (!this.ticketsToRelease.isEmpty()) { -- LongIterator longiterator = this.ticketsToRelease.iterator(); -+ // Paper start - replace level propagator -+ ticket_update_loop: -+ while (!this.ticketLevelUpdates.isEmpty()) { -+ flag = true; - -- while (longiterator.hasNext()) { -- long j = longiterator.nextLong(); -+ boolean oldPolling = this.pollingPendingChunkUpdates; -+ this.pollingPendingChunkUpdates = true; -+ try { -+ for (java.util.Iterator iterator = this.ticketLevelUpdates.long2IntEntrySet().fastIterator(); iterator.hasNext();) { -+ Long2IntMap.Entry entry = iterator.next(); -+ long key = entry.getLongKey(); -+ int newLevel = entry.getIntValue(); -+ ChunkHolder chunk = this.getChunk(key); -+ -+ if (chunk == null && newLevel > ChunkMap.MAX_CHUNK_DISTANCE) { -+ // not loaded and it shouldn't be loaded! -+ continue; -+ } -+ -+ int currentLevel = chunk == null ? ChunkMap.MAX_CHUNK_DISTANCE + 1 : chunk.getTicketLevel(); -+ -+ if (currentLevel == newLevel) { -+ // nothing to do -+ continue; -+ } - -- if (this.getTickets(j).stream().anyMatch((ticket) -> { -- return ticket.getType() == TicketType.PLAYER; -- })) { -- ChunkHolder playerchunk = playerchunkmap.getUpdatingChunkIfPresent(j); -+ this.updateChunkScheduling(key, newLevel, chunk, currentLevel); -+ } - -- if (playerchunk == null) { -- throw new IllegalStateException(); -+ long recursiveCheck = ++this.ticketLevelUpdateCount; -+ while (!this.ticketLevelUpdates.isEmpty()) { -+ long key = this.ticketLevelUpdates.firstLongKey(); -+ int newLevel = this.ticketLevelUpdates.removeFirstInt(); -+ ChunkHolder chunk = this.getChunk(key); -+ -+ if (chunk == null) { -+ if (newLevel <= ChunkMap.MAX_CHUNK_DISTANCE) { -+ throw new IllegalStateException("Expected chunk holder to be created"); - } -+ // not loaded and it shouldn't be loaded! -+ continue; -+ } - -- CompletableFuture> completablefuture = playerchunk.getEntityTickingChunkFuture(); -+ int currentLevel = chunk.oldTicketLevel; - -- completablefuture.thenAccept((either) -> { -- this.mainThreadExecutor.execute(() -> { -- this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> { -- }, j, false)); -- }); -- }); -+ if (currentLevel == newLevel) { -+ // nothing to do -+ continue; -+ } -+ -+ chunk.updateFutures(playerchunkmap, this.mainThreadExecutor); -+ if (recursiveCheck != this.ticketLevelUpdateCount) { -+ // back to the start, we must create player chunks and update the ticket level fields before -+ // processing the actual level updates -+ continue ticket_update_loop; - } - } - -- this.ticketsToRelease.clear(); -- } -+ for (;;) { -+ if (recursiveCheck != this.ticketLevelUpdateCount) { -+ continue ticket_update_loop; -+ } -+ ChunkHolder pendingUpdate = this.pendingChunkUpdates.poll(); -+ if (pendingUpdate == null) { -+ break; -+ } - -- return flag; -+ pendingUpdate.updateFutures(playerchunkmap, this.mainThreadExecutor); -+ } -+ } finally { -+ this.pollingPendingChunkUpdates = oldPolling; -+ } - } -+ -+ return flag; -+ // Paper end - replace level propagator - } - boolean pollingPendingChunkUpdates = false; // Paper - Chunk priority - -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - - ticket1.setCreatedTick(this.ticketTickCounter); - if (ticket.getTicketLevel() < j) { -- this.ticketTracker.update(i, ticket.getTicketLevel(), true); -+ this.updateTicketLevel(i, ticket.getTicketLevel()); // Paper - replace ticket level propagator - } - - return ticket == ticket1; // CraftBukkit -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - // Paper start - Chunk priority - int newLevel = getTicketLevelAt(arraysetsorted); - if (newLevel > oldLevel) { -- this.ticketTracker.update(i, newLevel, false); -+ this.updateTicketLevel(i, newLevel); // Paper // Paper - replace ticket level propagator - } - // Paper end - return removed; // CraftBukkit -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - SortedArraySet> tickets = entry.getValue(); - if (tickets.remove(target)) { - // copied from removeTicket -- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false); -+ this.updateTicketLevel(entry.getLongKey(), getTicketLevelAt(tickets)); // Paper - replace ticket level propagator - - // can't use entry after it's removed - if (tickets.isEmpty()) { diff --git a/patches/unapplied/server/Rewrite-the-light-engine.patch b/patches/unapplied/server/Rewrite-the-light-engine.patch deleted file mode 100644 index f00696e0a4..0000000000 --- a/patches/unapplied/server/Rewrite-the-light-engine.patch +++ /dev/null @@ -1,4782 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Wed, 28 Oct 2020 16:51:55 -0700 -Subject: [PATCH] Rewrite the light engine - -The standard vanilla light engine is plagued by -awful performance. Paper's changes to the light engine -help a bit, however they appear to cause some lighting -errors - most easily noticed in coral generation. - -The vanilla light engine's is too abstract to be modified - -so an entirely new implementation is required to fix the -performance and lighting errors. - -The new implementation is designed primarily to optimise -light level propagations (increase and decrease). Unlike -the vanilla light engine, this implementation tracks more -information per queued value when performing a -breadth first search. Vanilla just tracks coordinate, which -means every time they handle a queued value, they must -also determine the coordinate's target light level -from its neighbours - very wasteful, especially considering -these checks read neighbour block data. -The new light engine tracks both position and target level, -as well as whether the target block needs to be read at all -(for checking sided propagation). So, the work done per coordinate -is significantly reduced because no work is done for calculating -the target level. -In my testing, the block get calls were reduced by approximately -an order of magnitude. However, the light read checks were only -reduced by approximately 2x - but this is fine, light read checks -are extremely cheap compared to block gets. - -Generation testing showed that the new light engine improved -total generation (not lighting itself, but the whole generation process) -by 2x. According to cpu time, the light engine itself spent 10x less time -lighting chunks for generation. - -diff --git a/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java -@@ -0,0 +0,0 @@ -+package ca.spottedleaf.starlight.light; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.chunk.*; -+import net.minecraft.world.phys.shapes.Shapes; -+import net.minecraft.world.phys.shapes.VoxelShape; -+ -+import java.util.ArrayList; -+import java.util.Iterator; -+import java.util.List; -+import java.util.Set; -+import java.util.stream.Collectors; -+ -+public final class BlockStarLightEngine extends StarLightEngine { -+ -+ public BlockStarLightEngine(final Level world) { -+ super(false, world); -+ } -+ -+ @Override -+ protected boolean[] getEmptinessMap(final ChunkAccess chunk) { -+ return chunk.getBlockEmptinessMap(); -+ } -+ -+ @Override -+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { -+ chunk.setBlockEmptinessMap(to); -+ } -+ -+ @Override -+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { -+ return chunk.getBlockNibbles(); -+ } -+ -+ @Override -+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { -+ chunk.setBlockNibbles(to); -+ } -+ -+ @Override -+ protected boolean canUseChunk(final ChunkAccess chunk) { -+ return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); -+ } -+ -+ @Override -+ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { -+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); -+ if (nibble != null) { -+ // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically -+ // because a block was removed - which can decrease light. with sky data, block breaking can only result -+ // in increases, and thus the existing sky block check will actually correctly propagate light through -+ // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove -+ // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running -+ // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence -+ // of vanilla data management we "hide" them. -+ nibble.setHidden(); -+ } -+ } -+ -+ @Override -+ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) { -+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) { -+ return; -+ } -+ -+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); -+ if (nibble == null) { -+ if (!initRemovedNibbles) { -+ throw new IllegalStateException(); -+ } else { -+ this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray()); -+ } -+ } else { -+ nibble.setNonNull(); -+ } -+ } -+ -+ @Override -+ protected final void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) { -+ // blocks can change opacity -+ // blocks can change emitted light -+ // blocks can change direction of propagation -+ -+ final int encodeOffset = this.coordinateOffset; -+ final int emittedMask = this.emittedLightMask; -+ -+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); -+ final BlockState blockState = this.getBlockState(worldX, worldY, worldZ); -+ final int emittedLevel = blockState.getLightEmission() & emittedMask; -+ -+ this.setLightLevel(worldX, worldY, worldZ, emittedLevel); -+ // this accounts for change in emitted light that would cause an increase -+ if (emittedLevel != 0) { -+ this.appendToIncreaseQueue( -+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | (emittedLevel & 0xFL) << (6 + 6 + 16) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) -+ ); -+ } -+ // this also accounts for a change in emitted light that would cause a decrease -+ // this also accounts for the change of direction of propagation (i.e old block was full transparent, new block is full opaque or vice versa) -+ // as it checks all neighbours (even if current level is 0) -+ this.appendToDecreaseQueue( -+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | (currentLevel & 0xFL) << (6 + 6 + 16) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ // always keep sided transparent false here, new block might be conditionally transparent which would -+ // prevent us from decreasing sources in the directions where the new block is opaque -+ // if it turns out we were wrong to de-propagate the source, the re-propagate logic WILL always -+ // catch that and fix it. -+ ); -+ // re-propagating neighbours (done by the decrease queue) will also account for opacity changes in this block -+ } -+ -+ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); -+ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); -+ -+ @Override -+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, -+ final int expect) { -+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); -+ int level = centerState.getLightEmission() & 0xF; -+ -+ if (level >= (15 - 1) || level > expect) { -+ return level; -+ } -+ -+ final int sectionOffset = this.chunkSectionIndexOffset; -+ final BlockState conditionallyOpaqueState; -+ int opacity = centerState.getOpacityIfCached(); -+ -+ if (opacity == -1) { -+ this.recalcCenterPos.set(worldX, worldY, worldZ); -+ opacity = centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos); -+ if (centerState.isConditionallyFullOpaque()) { -+ conditionallyOpaqueState = centerState; -+ } else { -+ conditionallyOpaqueState = null; -+ } -+ } else if (opacity >= 15) { -+ return level; -+ } else { -+ conditionallyOpaqueState = null; -+ } -+ opacity = Math.max(1, opacity); -+ -+ for (final AxisDirection direction : AXIS_DIRECTIONS) { -+ final int offX = worldX + direction.x; -+ final int offY = worldY + direction.y; -+ final int offZ = worldZ + direction.z; -+ -+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; -+ -+ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8)); -+ -+ if ((neighbourLevel - 1) <= level) { -+ // don't need to test transparency, we know it wont affect the result. -+ continue; -+ } -+ -+ final BlockState neighbourState = this.getBlockState(offX, offY, offZ); -+ if (neighbourState.isConditionallyFullOpaque()) { -+ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that -+ // we don't read the blockstate because most of the time this is false, so using the faster -+ // known transparency lookup results in a net win -+ this.recalcNeighbourPos.set(offX, offY, offZ); -+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); -+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); -+ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { -+ // not allowed to propagate -+ continue; -+ } -+ } -+ -+ // passed transparency, -+ -+ final int calculated = neighbourLevel - opacity; -+ level = Math.max(calculated, level); -+ if (level > expect) { -+ return level; -+ } -+ } -+ -+ return level; -+ } -+ -+ @Override -+ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) { -+ for (final BlockPos pos : positions) { -+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); -+ } -+ -+ this.performLightDecrease(lightAccess); -+ } -+ -+ protected Iterator getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) { -+ if (chunk instanceof ImposterProtoChunk || chunk instanceof LevelChunk) { -+ // implementation on Chunk is pretty awful, so write our own here. The big optimisation is -+ // skipping empty sections, and the far more optimised reading of types. -+ List sources = new ArrayList<>(); -+ -+ int offX = chunk.getPos().x << 4; -+ int offZ = chunk.getPos().z << 4; -+ -+ final LevelChunkSection[] sections = chunk.getSections(); -+ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { -+ final LevelChunkSection section = sections[sectionY - this.minSection]; -+ if (section == null || section.isEmpty()) { -+ // no sources in empty sections -+ continue; -+ } -+ final PalettedContainer states = section.states; -+ final int offY = sectionY << 4; -+ -+ for (int index = 0; index < (16 * 16 * 16); ++index) { -+ final BlockState state = states.get(index); -+ if (state.getLightEmission() <= 0) { -+ continue; -+ } -+ -+ // index = x | (z << 4) | (y << 8) -+ sources.add(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15))); -+ } -+ } -+ -+ return sources.iterator(); -+ } else { -+ // world gen and lighting run in parallel, and if lighting keeps up it can be lighting chunks that are -+ // being generated. In the nether, lava will add a lot of sources. This resulted in quite a few CME crashes. -+ // So all we do spinloop until we can collect a list of sources, and even if it is out of date we will pick up -+ // the missing sources from checkBlock. -+ for (;;) { -+ try { -+ return chunk.getLights().collect(Collectors.toList()).iterator(); -+ } catch (final Exception cme) { -+ continue; -+ } -+ } -+ } -+ } -+ -+ @Override -+ public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { -+ // setup sources -+ final int emittedMask = this.emittedLightMask; -+ for (final Iterator positions = this.getSources(lightAccess, chunk); positions.hasNext();) { -+ final BlockPos pos = positions.next(); -+ final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ()); -+ final int emittedLight = blockState.getLightEmission() & emittedMask; -+ -+ if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) { -+ // some other source is brighter -+ continue; -+ } -+ -+ this.appendToIncreaseQueue( -+ ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | (emittedLight & 0xFL) << (6 + 6 + 16) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) -+ ); -+ -+ -+ // propagation wont set this for us -+ this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight); -+ } -+ -+ if (needsEdgeChecks) { -+ // not required to propagate here, but this will reduce the hit of the edge checks -+ this.performLightIncrease(lightAccess); -+ -+ // verify neighbour edges -+ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); -+ } else { -+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection); -+ -+ this.performLightIncrease(lightAccess); -+ } -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/starlight/light/SWMRNibbleArray.java b/src/main/java/ca/spottedleaf/starlight/light/SWMRNibbleArray.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/starlight/light/SWMRNibbleArray.java -@@ -0,0 +0,0 @@ -+package ca.spottedleaf.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 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.storageUpdating != null) { -+ into = this.storageUpdating; -+ } else { -+ this.storageUpdating = into = allocateBytes(); -+ this.stateUpdating = INIT_STATE_INIT; -+ } -+ this.updatingDirty = true; -+ -+ 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) { -+ synchronized (this) { -+ // indices range from 0 -> 4096 -+ final byte[] visibleBytes = this.storageVisible; -+ if (visibleBytes == null) { -+ return 0; -+ } -+ final byte value = visibleBytes[index >>> 1]; -+ -+ // if we are an even index, we want lower 4 bits -+ // if we are an odd index, we want upper 4 bits -+ return ((value >>> ((index & 1) << 2)) & 0xF); -+ } -+ } -+ -+ // operation type: updating -+ public void set(final int x, final int y, final int z, final int value) { -+ this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value); -+ } -+ -+ // operation type: updating -+ public void set(final int index, final int value) { -+ if (!this.updatingDirty) { -+ this.swapUpdatingAndMarkDirty(); -+ } -+ final int shift = (index & 1) << 2; -+ final int i = index >>> 1; -+ -+ this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift)); -+ } -+ -+ public static final class SaveState { -+ -+ public final byte[] data; -+ public final int state; -+ -+ public SaveState(final byte[] data, final int state) { -+ this.data = data; -+ this.state = state; -+ } -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/starlight/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/light/SkyStarLightEngine.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/starlight/light/SkyStarLightEngine.java -@@ -0,0 +0,0 @@ -+package ca.spottedleaf.starlight.light; -+ -+import io.papermc.paper.util.WorldUtil; -+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.ChunkStatus; -+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.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 == EMPTY_CHUNK_SECTION) { -+ 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 chunk.getSkyEmptinessMap(); -+ } -+ -+ @Override -+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { -+ chunk.setSkyEmptinessMap(to); -+ } -+ -+ @Override -+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { -+ return chunk.getSkyNibbles(); -+ } -+ -+ @Override -+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { -+ chunk.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.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); -+ } -+ -+ @Override -+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, -+ final int toSection) { -+ Arrays.fill(this.nullPropagationCheckCache, false); -+ this.rewriteNibbleCacheForSkylight(chunk); -+ final int chunkX = chunk.getPos().x; -+ final int chunkZ = chunk.getPos().z; -+ for (int y = toSection; y >= fromSection; --y) { -+ this.checkNullSection(chunkX, y, chunkZ, true); -+ } -+ -+ super.checkChunkEdges(lightAccess, chunk, fromSection, toSection); -+ } -+ -+ @Override -+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) { -+ Arrays.fill(this.nullPropagationCheckCache, false); -+ this.rewriteNibbleCacheForSkylight(chunk); -+ final int chunkX = chunk.getPos().x; -+ final int chunkZ = chunk.getPos().z; -+ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) { -+ final int y = (int)iterator.nextShort(); -+ this.checkNullSection(chunkX, y, chunkZ, true); -+ } -+ -+ super.checkChunkEdges(lightAccess, chunk, sections); -+ } -+ -+ @Override -+ protected void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) { -+ // blocks can change opacity -+ // blocks can change direction of propagation -+ -+ // same logic applies from BlockStarLightEngine#checkBlock -+ -+ final int encodeOffset = this.coordinateOffset; -+ -+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); -+ -+ if (currentLevel == 15) { -+ // must re-propagate clobbered source -+ this.appendToIncreaseQueue( -+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | (currentLevel & 0xFL) << (6 + 6 + 16) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the block is conditionally transparent -+ ); -+ } else { -+ this.setLightLevel(worldX, worldY, worldZ, 0); -+ } -+ -+ this.appendToDecreaseQueue( -+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | (currentLevel & 0xFL) << (6 + 6 + 16) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ ); -+ } -+ -+ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); -+ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); -+ -+ @Override -+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, -+ final int expect) { -+ if (expect == 15) { -+ return expect; -+ } -+ -+ final int sectionOffset = this.chunkSectionIndexOffset; -+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); -+ int opacity = centerState.getOpacityIfCached(); -+ -+ BlockState conditionallyOpaqueState; -+ if (opacity < 0) { -+ this.recalcCenterPos.set(worldX, worldY, worldZ); -+ opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos)); -+ if (centerState.isConditionallyFullOpaque()) { -+ conditionallyOpaqueState = centerState; -+ } else { -+ conditionallyOpaqueState = null; -+ } -+ } else { -+ conditionallyOpaqueState = null; -+ opacity = Math.max(1, opacity); -+ } -+ -+ int level = 0; -+ -+ for (final AxisDirection direction : AXIS_DIRECTIONS) { -+ final int offX = worldX + direction.x; -+ final int offY = worldY + direction.y; -+ final int offZ = worldZ + direction.z; -+ -+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; -+ -+ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8)); -+ -+ if ((neighbourLevel - 1) <= level) { -+ // don't need to test transparency, we know it wont affect the result. -+ continue; -+ } -+ -+ final BlockState neighbourState = this.getBlockState(offX, offY, offZ); -+ -+ if (neighbourState.isConditionallyFullOpaque()) { -+ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that -+ // we don't read the blockstate because most of the time this is false, so using the faster -+ // known transparency lookup results in a net win -+ this.recalcNeighbourPos.set(offX, offY, offZ); -+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); -+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); -+ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { -+ // not allowed to propagate -+ continue; -+ } -+ } -+ -+ final int calculated = neighbourLevel - opacity; -+ level = Math.max(calculated, level); -+ if (level > expect) { -+ return level; -+ } -+ } -+ -+ return level; -+ } -+ -+ @Override -+ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set 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].isEmpty()) { -+ this.checkNullSection(chunkX, highestNonEmptySection, chunkZ, false); -+ // try propagate FULL to neighbours -+ -+ // check neighbours to see if we need to propagate into them -+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { -+ final int neighbourX = chunkX + direction.x; -+ final int neighbourZ = chunkZ + direction.z; -+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(neighbourX, highestNonEmptySection, neighbourZ); -+ if (neighbourNibble == null) { -+ // unloaded neighbour -+ // most of the time we fall here -+ continue; -+ } -+ -+ // it looks like we need to propagate into the neighbour -+ -+ final int incX; -+ final int incZ; -+ final int startX; -+ final int startZ; -+ -+ if (direction.x != 0) { -+ // x direction -+ incX = 0; -+ incZ = 1; -+ -+ if (direction.x < 0) { -+ // negative -+ startX = chunkX << 4; -+ } else { -+ startX = chunkX << 4 | 15; -+ } -+ startZ = chunkZ << 4; -+ } else { -+ // z direction -+ incX = 1; -+ incZ = 0; -+ -+ if (direction.z < 0) { -+ // negative -+ startZ = chunkZ << 4; -+ } else { -+ startZ = chunkZ << 4 | 15; -+ } -+ startX = chunkX << 4; -+ } -+ -+ final int encodeOffset = this.coordinateOffset; -+ final long propagateDirection = 1L << direction.ordinal(); // we only want to check in this direction -+ -+ for (int currY = highestNonEmptySection << 4, maxY = currY | 15; currY <= maxY; ++currY) { -+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { -+ this.appendToIncreaseQueue( -+ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | (15L << (6 + 6 + 16)) // we know we're at full lit here -+ | (propagateDirection << (6 + 6 + 16 + 4)) -+ // no transparent flag, we know for a fact there are no blocks here that could be directionally transparent (as the section is EMPTY) -+ ); -+ } -+ } -+ } -+ -+ if (highestNonEmptySection-- == (this.minSection - 1)) { -+ break; -+ } -+ } -+ -+ if (highestNonEmptySection >= this.minSection) { -+ // fill out our other sources -+ final int minX = chunkPos.x << 4; -+ final int maxX = chunkPos.x << 4 | 15; -+ final int minZ = chunkPos.z << 4; -+ final int maxZ = chunkPos.z << 4 | 15; -+ final int startY = highestNonEmptySection << 4 | 15; -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ this.tryPropagateSkylight(world, currX, startY + 1, currZ, false, false); -+ } -+ } -+ } // else: apparently the chunk is empty -+ -+ if (needsEdgeChecks) { -+ // not required to propagate here, but this will reduce the hit of the edge checks -+ this.performLightIncrease(lightAccess); -+ -+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { -+ this.checkNullSection(chunkX, y, chunkZ, false); -+ } -+ // no need to rewrite the nibble cache again -+ super.checkChunkEdges(lightAccess, chunk, this.minLightSection, highestNonEmptySection); -+ } else { -+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { -+ this.checkNullSection(chunkX, y, chunkZ, false); -+ } -+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, highestNonEmptySection); -+ -+ this.performLightIncrease(lightAccess); -+ } -+ } -+ -+ protected final void processDelayedIncreases() { -+ // copied from performLightIncrease -+ final long[] queue = this.increaseQueue; -+ final int decodeOffsetX = -this.encodeOffsetX; -+ final int decodeOffsetY = -this.encodeOffsetY; -+ final int decodeOffsetZ = -this.encodeOffsetZ; -+ -+ for (int i = 0, len = this.increaseQueueInitialLength; i < len; ++i) { -+ final long queueValue = queue[i]; -+ -+ final int posX = ((int)queueValue & 63) + decodeOffsetX; -+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; -+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; -+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); -+ -+ this.setLightLevel(posX, posY, posZ, propagatedLightLevel); -+ } -+ } -+ -+ protected final void processDelayedDecreases() { -+ // copied from performLightDecrease -+ final long[] queue = this.decreaseQueue; -+ final int decodeOffsetX = -this.encodeOffsetX; -+ final int decodeOffsetY = -this.encodeOffsetY; -+ final int decodeOffsetZ = -this.encodeOffsetZ; -+ -+ for (int i = 0, len = this.decreaseQueueInitialLength; i < len; ++i) { -+ final long queueValue = queue[i]; -+ -+ final int posX = ((int)queueValue & 63) + decodeOffsetX; -+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; -+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; -+ -+ this.setLightLevel(posX, posY, posZ, 0); -+ } -+ } -+ -+ // delaying the light set is useful for block changes since they need to worry about initialising nibblearrays -+ // while also queueing light at the same time (initialising nibblearrays might depend on nibbles above, so -+ // clobbering the light values will result in broken propagation) -+ protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ, -+ final boolean extrudeInitialised, final boolean delayLightSet) { -+ final BlockPos.MutableBlockPos mutablePos = this.mutablePos3; -+ final int encodeOffset = this.coordinateOffset; -+ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards. -+ -+ if (this.getLightLevelExtruded(worldX, startY + 1, worldZ) != 15) { -+ return startY; -+ } -+ -+ // ensure this section is always checked -+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised); -+ -+ BlockState above = this.getBlockState(worldX, startY + 1, worldZ); -+ if (above == null) { -+ above = AIR_BLOCK_STATE; -+ } -+ -+ 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); -+ } -+ BlockState current = this.getBlockState(worldX, startY, worldZ); -+ if (current == null) { -+ current = AIR_BLOCK_STATE; -+ } -+ -+ final VoxelShape fromShape; -+ if (above.isConditionallyFullOpaque()) { -+ this.mutablePos2.set(worldX, startY + 1, worldZ); -+ fromShape = above.getFaceOcclusionShape(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms); -+ if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { -+ // above wont let us propagate -+ break; -+ } -+ } else { -+ fromShape = Shapes.empty(); -+ } -+ -+ final int opacityIfCached = current.getOpacityIfCached(); -+ // does light propagate from the top down? -+ if (opacityIfCached != -1) { -+ if (opacityIfCached != 0) { -+ // we cannot propagate 15 through this -+ break; -+ } -+ // most of the time it falls here. -+ // add to propagate -+ // light set delayed until we determine if this nibble section is null -+ this.appendToIncreaseQueue( -+ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | (15L << (6 + 6 + 16)) // we know we're at full lit here -+ | (propagateDirection << (6 + 6 + 16 + 4)) -+ ); -+ } else { -+ mutablePos.set(worldX, startY, worldZ); -+ long flags = 0L; -+ if (current.isConditionallyFullOpaque()) { -+ final VoxelShape cullingFace = current.getFaceOcclusionShape(world, mutablePos, AxisDirection.POSITIVE_Y.nms); -+ -+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { -+ // can't propagate here, we're done on this column. -+ break; -+ } -+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; -+ } -+ -+ final int opacity = current.getLightBlock(world, mutablePos); -+ if (opacity > 0) { -+ // let the queued value (if any) handle it from here. -+ break; -+ } -+ -+ // light set delayed until we determine if this nibble section is null -+ this.appendToIncreaseQueue( -+ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | (15L << (6 + 6 + 16)) // we know we're at full lit here -+ | (propagateDirection << (6 + 6 + 16 + 4)) -+ | flags -+ ); -+ } -+ -+ above = current; -+ -+ if (this.getNibbleFromCache(worldX >> 4, startY >> 4, worldZ >> 4) == null) { -+ // we skip empty sections here, as this is just an easy way of making sure the above block -+ // can propagate through air. -+ -+ // nothing can propagate in null sections, remove the queue entry for it -+ --this.increaseQueueInitialLength; -+ -+ // advance currY to the the top of the section below -+ startY = (startY) & (~15); -+ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually -+ // end up there -+ -+ // make sure this is marked as AIR -+ above = AIR_BLOCK_STATE; -+ } else if (!delayLightSet) { -+ this.setLightLevel(worldX, startY, worldZ, 15); -+ } -+ } -+ -+ return startY; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/starlight/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/light/StarLightEngine.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/starlight/light/StarLightEngine.java -@@ -0,0 +0,0 @@ -+package ca.spottedleaf.starlight.light; -+ -+import io.papermc.paper.util.CoordinateUtils; -+import io.papermc.paper.util.IntegerUtil; -+import io.papermc.paper.util.WorldUtil; -+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.*; -+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 LevelChunkSection EMPTY_CHUNK_SECTION = new LevelChunkSection(0); -+ -+ protected static final AxisDirection[] DIRECTIONS = AxisDirection.values(); -+ protected static final AxisDirection[] AXIS_DIRECTIONS = DIRECTIONS; -+ protected static final AxisDirection[] ONLY_HORIZONTAL_DIRECTIONS = new AxisDirection[] { -+ AxisDirection.POSITIVE_X, AxisDirection.NEGATIVE_X, -+ AxisDirection.POSITIVE_Z, AxisDirection.NEGATIVE_Z -+ }; -+ -+ protected static enum AxisDirection { -+ -+ // Declaration order is important and relied upon. Do not change without modifying propagation code. -+ POSITIVE_X(1, 0, 0), NEGATIVE_X(-1, 0, 0), -+ POSITIVE_Z(0, 0, 1), NEGATIVE_Z(0, 0, -1), -+ POSITIVE_Y(0, 1, 0), NEGATIVE_Y(0, -1, 0); -+ -+ static { -+ POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X; -+ POSITIVE_Z.opposite = NEGATIVE_Z; NEGATIVE_Z.opposite = POSITIVE_Z; -+ POSITIVE_Y.opposite = NEGATIVE_Y; NEGATIVE_Y.opposite = POSITIVE_Y; -+ } -+ -+ protected AxisDirection opposite; -+ -+ public final int x; -+ public final int y; -+ public final int z; -+ public final Direction nms; -+ public final long everythingButThisDirection; -+ public final long everythingButTheOppositeDirection; -+ -+ AxisDirection(final int x, final int y, final int z) { -+ this.x = x; -+ this.y = y; -+ this.z = z; -+ this.nms = Direction.fromNormal(x, y, z); -+ this.everythingButThisDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << this.ordinal())); -+ // positive is always even, negative is always odd. Flip the 1 bit to get the negative direction. -+ this.everythingButTheOppositeDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << (this.ordinal() ^ 1))); -+ } -+ -+ public AxisDirection getOpposite() { -+ return this.opposite; -+ } -+ } -+ -+ // I'd like to thank https://www.seedofandromeda.com/blogs/29-fast-flood-fill-lighting-in-a-blocky-voxel-game-pt-1 -+ // for explaining how light propagates via breadth-first search -+ -+ // While the above is a good start to understanding the general idea of what the general principles are, it's not -+ // exactly how the vanilla light engine should behave for minecraft. -+ -+ // similar to the above, except the chunk section indices vary from [-1, 1], or [0, 2] -+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] -+ // index = x + (z * 5) + (y * 25) -+ // null index indicates the chunk section doesn't exist (empty or out of bounds) -+ protected final LevelChunkSection[] sectionCache; -+ -+ // the exact same as above, except for storing fast access to SWMRNibbleArray -+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] -+ // index = x + (z * 5) + (y * 25) -+ protected final SWMRNibbleArray[] nibbleCache; -+ -+ // the exact same as above, except for storing fast access to nibbles to call change callbacks for -+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] -+ // index = x + (z * 5) + (y * 25) -+ protected final boolean[] notifyUpdateCache; -+ -+ // always initialsed during start of lighting. -+ // index = x + (z * 5) -+ protected final ChunkAccess[] chunkCache = new ChunkAccess[5 * 5]; -+ -+ // index = x + (z * 5) -+ protected final boolean[][] emptinessMapCache = new boolean[5 * 5][]; -+ -+ protected final BlockPos.MutableBlockPos mutablePos1 = new BlockPos.MutableBlockPos(); -+ protected final BlockPos.MutableBlockPos mutablePos2 = new BlockPos.MutableBlockPos(); -+ protected final BlockPos.MutableBlockPos mutablePos3 = new BlockPos.MutableBlockPos(); -+ -+ protected int encodeOffsetX; -+ protected int encodeOffsetY; -+ protected int encodeOffsetZ; -+ -+ protected int coordinateOffset; -+ -+ protected int chunkOffsetX; -+ protected int chunkOffsetY; -+ protected int chunkOffsetZ; -+ -+ protected int chunkIndexOffset; -+ protected int chunkSectionIndexOffset; -+ -+ protected final boolean skylightPropagator; -+ protected final int emittedLightMask; -+ protected final boolean isClientSide; -+ -+ protected final Level world; -+ protected final int minLightSection; -+ protected final int maxLightSection; -+ protected final int minSection; -+ protected final int maxSection; -+ -+ protected StarLightEngine(final boolean skylightPropagator, final Level world) { -+ this.skylightPropagator = skylightPropagator; -+ this.emittedLightMask = skylightPropagator ? 0 : 0xF; -+ this.isClientSide = world.isClientSide; -+ this.world = world; -+ this.minLightSection = WorldUtil.getMinLightSection(world); -+ this.maxLightSection = WorldUtil.getMaxLightSection(world); -+ this.minSection = WorldUtil.getMinSection(world); -+ this.maxSection = WorldUtil.getMaxSection(world); -+ -+ this.sectionCache = new LevelChunkSection[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer -+ this.nibbleCache = new SWMRNibbleArray[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer -+ this.notifyUpdateCache = new boolean[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer -+ } -+ -+ protected final void setupEncodeOffset(final int centerX, final int centerY, final int centerZ) { -+ // 31 = center + encodeOffset -+ this.encodeOffsetX = 31 - centerX; -+ this.encodeOffsetY = (-(this.minLightSection - 1) << 4); // we want 0 to be the smallest encoded value -+ this.encodeOffsetZ = 31 - centerZ; -+ -+ // coordinateIndex = x | (z << 6) | (y << 12) -+ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << 6) + (this.encodeOffsetY << 12); -+ -+ // 2 = (centerX >> 4) + chunkOffset -+ this.chunkOffsetX = 2 - (centerX >> 4); -+ this.chunkOffsetY = -(this.minLightSection - 1); // lowest should be 0 -+ this.chunkOffsetZ = 2 - (centerZ >> 4); -+ -+ // chunk index = x + (5 * z) -+ this.chunkIndexOffset = this.chunkOffsetX + (5 * this.chunkOffsetZ); -+ -+ // chunk section index = x + (5 * z) + ((5*5) * y) -+ this.chunkSectionIndexOffset = this.chunkIndexOffset + ((5 * 5) * this.chunkOffsetY); -+ } -+ -+ protected final void setupCaches(final LightChunkGetter chunkProvider, final int centerX, final int centerY, final int centerZ, -+ final boolean relaxed, final boolean tryToLoadChunksFor2Radius) { -+ final int centerChunkX = centerX >> 4; -+ final int centerChunkY = centerY >> 4; -+ final int centerChunkZ = centerZ >> 4; -+ -+ this.setupEncodeOffset(centerChunkX * 16 + 7, centerChunkY * 16 + 7, centerChunkZ * 16 + 7); -+ -+ final int radius = tryToLoadChunksFor2Radius ? 2 : 1; -+ -+ for (int dz = -radius; dz <= radius; ++dz) { -+ for (int dx = -radius; dx <= radius; ++dx) { -+ final int cx = centerChunkX + dx; -+ final int cz = centerChunkZ + dz; -+ final boolean isTwoRadius = Math.max(IntegerUtil.branchlessAbs(dx), IntegerUtil.branchlessAbs(dz)) == 2; -+ final ChunkAccess chunk = (ChunkAccess)chunkProvider.getChunkForLighting(cx, cz); -+ -+ if (chunk == null) { -+ if (relaxed | isTwoRadius) { -+ continue; -+ } -+ throw new IllegalArgumentException("Trying to propagate light update before 1 radius neighbours ready"); -+ } -+ -+ if (!this.canUseChunk(chunk)) { -+ continue; -+ } -+ -+ this.setChunkInCache(cx, cz, chunk); -+ this.setEmptinessMapCache(cx, cz, this.getEmptinessMap(chunk)); -+ if (!isTwoRadius) { -+ this.setBlocksForChunkInCache(cx, cz, chunk.getSections()); -+ this.setNibblesForChunkInCache(cx, cz, this.getNibblesOnChunk(chunk)); -+ } -+ } -+ } -+ } -+ -+ protected final ChunkAccess getChunkInCache(final int chunkX, final int chunkZ) { -+ return this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset]; -+ } -+ -+ protected final void setChunkInCache(final int chunkX, final int chunkZ, final ChunkAccess chunk) { -+ this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk; -+ } -+ -+ protected final LevelChunkSection getChunkSection(final int chunkX, final int chunkY, final int chunkZ) { -+ return this.sectionCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset]; -+ } -+ -+ protected final void setChunkSectionInCache(final int chunkX, final int chunkY, final int chunkZ, final LevelChunkSection section) { -+ this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section; -+ } -+ -+ protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final LevelChunkSection[] sections) { -+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { -+ this.setChunkSectionInCache(chunkX, cy, chunkZ, -+ sections == null ? null : (cy >= this.minSection && cy <= this.maxSection ? (sections[cy - this.minSection] == null || sections[cy - this.minSection].isEmpty() ? EMPTY_CHUNK_SECTION : sections[cy - this.minSection]) : EMPTY_CHUNK_SECTION)); -+ } -+ } -+ -+ 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 chunkY = ((index / (5*5)) % (16 + 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 == EMPTY_CHUNK_SECTION ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15); -+ } -+ -+ return null; -+ } -+ -+ protected final BlockState getBlockState(final int sectionIndex, final int localIndex) { -+ final LevelChunkSection section = this.sectionCache[sectionIndex]; -+ -+ if (section != null) { -+ return section == EMPTY_CHUNK_SECTION ? AIR_BLOCK_STATE : section.states.get(localIndex); -+ } -+ -+ return null; -+ } -+ -+ 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; -+ } -+ -+ protected final long getKnownTransparency(final int worldX, final int worldY, final int worldZ) { -+ throw new UnsupportedOperationException(); // :( -+ } -+ -+ // warn: localIndex = y | (x << 4) | (z << 8) -+ protected final long getKnownTransparency(final int sectionIndex, final int localIndex) { -+ throw new UnsupportedOperationException(); // :( -+ } -+ -+ 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].isEmpty()) { -+ 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) { -+ final Boolean valueBoxed = emptinessChanges[sectionIndex]; -+ if (valueBoxed == null) { -+ if (needsInit) { -+ throw new IllegalStateException("Current chunk has not initialised emptiness map yet supplied emptiness map isn't filled?"); -+ } -+ continue; -+ } -+ 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 != EMPTY_CHUNK_SECTION) { -+ 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, chunkX)); -+ } -+ -+ // 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; -+ } -+ final int opacityCached = blockState.getOpacityIfCached(); -+ if (opacityCached != -1) { -+ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached); -+ if (targetLevel > currentLevel) { -+ currentNibble.set(localIndex, targetLevel); -+ this.postLightUpdate(offX, offY, offZ); -+ -+ if (targetLevel > 1) { -+ if (queueLength >= queue.length) { -+ queue = this.resizeIncreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((targetLevel & 0xFL) << (6 + 6 + 16)) -+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); -+ continue; -+ } -+ } -+ continue; -+ } else { -+ this.mutablePos1.set(offX, offY, offZ); -+ long flags = 0; -+ if (blockState.isConditionallyFullOpaque()) { -+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); -+ -+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { -+ continue; -+ } -+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; -+ } -+ -+ final int opacity = blockState.getLightBlock(world, this.mutablePos1); -+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity); -+ if (targetLevel <= currentLevel) { -+ continue; -+ } -+ -+ currentNibble.set(localIndex, targetLevel); -+ this.postLightUpdate(offX, offY, offZ); -+ -+ if (targetLevel > 1) { -+ if (queueLength >= queue.length) { -+ queue = this.resizeIncreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((targetLevel & 0xFL) << (6 + 6 + 16)) -+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) -+ | (flags); -+ } -+ continue; -+ } -+ } -+ } else { -+ // we actually need to worry about our state here -+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ); -+ this.mutablePos2.set(posX, posY, posZ); -+ for (final AxisDirection propagate : checkDirections) { -+ final int offX = posX + propagate.x; -+ final int offY = posY + propagate.y; -+ final int offZ = posZ + propagate.z; -+ -+ final VoxelShape fromShape = fromBlock.isConditionallyFullOpaque() ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); -+ -+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { -+ continue; -+ } -+ -+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; -+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); -+ -+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; -+ final int currentLevel; -+ -+ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) { -+ continue; // already at the level we want -+ } -+ -+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex); -+ if (blockState == null) { -+ continue; -+ } -+ final int opacityCached = blockState.getOpacityIfCached(); -+ if (opacityCached != -1) { -+ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached); -+ if (targetLevel > currentLevel) { -+ currentNibble.set(localIndex, targetLevel); -+ this.postLightUpdate(offX, offY, offZ); -+ -+ if (targetLevel > 1) { -+ if (queueLength >= queue.length) { -+ queue = this.resizeIncreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((targetLevel & 0xFL) << (6 + 6 + 16)) -+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); -+ continue; -+ } -+ } -+ continue; -+ } else { -+ this.mutablePos1.set(offX, offY, offZ); -+ long flags = 0; -+ if (blockState.isConditionallyFullOpaque()) { -+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); -+ -+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { -+ continue; -+ } -+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; -+ } -+ -+ final int opacity = blockState.getLightBlock(world, this.mutablePos1); -+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity); -+ if (targetLevel <= currentLevel) { -+ continue; -+ } -+ -+ currentNibble.set(localIndex, targetLevel); -+ this.postLightUpdate(offX, offY, offZ); -+ -+ if (targetLevel > 1) { -+ if (queueLength >= queue.length) { -+ queue = this.resizeIncreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((targetLevel & 0xFL) << (6 + 6 + 16)) -+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) -+ | (flags); -+ } -+ continue; -+ } -+ } -+ } -+ } -+ } -+ -+ protected final void performLightDecrease(final LightChunkGetter lightAccess) { -+ final BlockGetter world = lightAccess.getLevel(); -+ long[] queue = this.decreaseQueue; -+ long[] increaseQueue = this.increaseQueue; -+ int queueReadIndex = 0; -+ int queueLength = this.decreaseQueueInitialLength; -+ this.decreaseQueueInitialLength = 0; -+ int increaseQueueLength = this.increaseQueueInitialLength; -+ final int decodeOffsetX = -this.encodeOffsetX; -+ final int decodeOffsetY = -this.encodeOffsetY; -+ final int decodeOffsetZ = -this.encodeOffsetZ; -+ final int encodeOffset = this.coordinateOffset; -+ final int sectionOffset = this.chunkSectionIndexOffset; -+ final int emittedMask = this.emittedLightMask; -+ -+ while (queueReadIndex < queueLength) { -+ final long queueValue = queue[queueReadIndex++]; -+ -+ final int posX = ((int)queueValue & 63) + decodeOffsetX; -+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; -+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; -+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); -+ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63)]; -+ -+ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) { -+ // we don't need to worry about our state here. -+ for (final AxisDirection propagate : checkDirections) { -+ final int offX = posX + propagate.x; -+ final int offY = posY + propagate.y; -+ final int offZ = posZ + propagate.z; -+ -+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; -+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); -+ -+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; -+ final int lightLevel; -+ -+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { -+ // already at lowest (or unloaded), nothing we can do -+ continue; -+ } -+ -+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex); -+ if (blockState == null) { -+ continue; -+ } -+ final int opacityCached = blockState.getOpacityIfCached(); -+ if (opacityCached != -1) { -+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached)); -+ if (lightLevel > targetLevel) { -+ // it looks like another source propagated here, so re-propagate it -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((lightLevel & 0xFL) << (6 + 6 + 16)) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | FLAG_RECHECK_LEVEL; -+ continue; -+ } -+ final int emittedLight = blockState.getLightEmission() & emittedMask; -+ if (emittedLight != 0) { -+ // re-propagate source -+ // note: do not set recheck level, or else the propagation will fail -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((emittedLight & 0xFL) << (6 + 6 + 16)) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | (blockState.isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL); -+ } -+ -+ currentNibble.set(localIndex, 0); -+ this.postLightUpdate(offX, offY, offZ); -+ -+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... -+ if (queueLength >= queue.length) { -+ queue = this.resizeDecreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((targetLevel & 0xFL) << (6 + 6 + 16)) -+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); -+ continue; -+ } -+ continue; -+ } else { -+ this.mutablePos1.set(offX, offY, offZ); -+ long flags = 0; -+ if (blockState.isConditionallyFullOpaque()) { -+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); -+ -+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { -+ continue; -+ } -+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; -+ } -+ -+ final int opacity = blockState.getLightBlock(world, this.mutablePos1); -+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); -+ if (lightLevel > targetLevel) { -+ // it looks like another source propagated here, so re-propagate it -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((lightLevel & 0xFL) << (6 + 6 + 16)) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | (FLAG_RECHECK_LEVEL | flags); -+ continue; -+ } -+ final int emittedLight = blockState.getLightEmission() & emittedMask; -+ if (emittedLight != 0) { -+ // re-propagate source -+ // note: do not set recheck level, or else the propagation will fail -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((emittedLight & 0xFL) << (6 + 6 + 16)) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | (flags | FLAG_WRITE_LEVEL); -+ } -+ -+ currentNibble.set(localIndex, 0); -+ this.postLightUpdate(offX, offY, offZ); -+ -+ if (targetLevel > 0) { -+ if (queueLength >= queue.length) { -+ queue = this.resizeDecreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((targetLevel & 0xFL) << (6 + 6 + 16)) -+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) -+ | flags; -+ } -+ continue; -+ } -+ } -+ } else { -+ // we actually need to worry about our state here -+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ); -+ this.mutablePos2.set(posX, posY, posZ); -+ for (final AxisDirection propagate : checkDirections) { -+ final int offX = posX + propagate.x; -+ final int offY = posY + propagate.y; -+ final int offZ = posZ + propagate.z; -+ -+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; -+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); -+ -+ final VoxelShape fromShape = fromBlock.isConditionallyFullOpaque() ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); -+ -+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { -+ continue; -+ } -+ -+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; -+ final int lightLevel; -+ -+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { -+ // already at lowest (or unloaded), nothing we can do -+ continue; -+ } -+ -+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex); -+ if (blockState == null) { -+ continue; -+ } -+ final int opacityCached = blockState.getOpacityIfCached(); -+ if (opacityCached != -1) { -+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached)); -+ if (lightLevel > targetLevel) { -+ // it looks like another source propagated here, so re-propagate it -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((lightLevel & 0xFL) << (6 + 6 + 16)) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | FLAG_RECHECK_LEVEL; -+ continue; -+ } -+ final int emittedLight = blockState.getLightEmission() & emittedMask; -+ if (emittedLight != 0) { -+ // re-propagate source -+ // note: do not set recheck level, or else the propagation will fail -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((emittedLight & 0xFL) << (6 + 6 + 16)) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | (blockState.isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL); -+ } -+ -+ currentNibble.set(localIndex, 0); -+ this.postLightUpdate(offX, offY, offZ); -+ -+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... -+ if (queueLength >= queue.length) { -+ queue = this.resizeDecreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((targetLevel & 0xFL) << (6 + 6 + 16)) -+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); -+ continue; -+ } -+ continue; -+ } else { -+ this.mutablePos1.set(offX, offY, offZ); -+ long flags = 0; -+ if (blockState.isConditionallyFullOpaque()) { -+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); -+ -+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { -+ continue; -+ } -+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; -+ } -+ -+ final int opacity = blockState.getLightBlock(world, this.mutablePos1); -+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); -+ if (lightLevel > targetLevel) { -+ // it looks like another source propagated here, so re-propagate it -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((lightLevel & 0xFL) << (6 + 6 + 16)) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | (FLAG_RECHECK_LEVEL | flags); -+ continue; -+ } -+ final int emittedLight = blockState.getLightEmission() & emittedMask; -+ if (emittedLight != 0) { -+ // re-propagate source -+ // note: do not set recheck level, or else the propagation will fail -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((emittedLight & 0xFL) << (6 + 6 + 16)) -+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) -+ | (flags | FLAG_WRITE_LEVEL); -+ } -+ -+ currentNibble.set(localIndex, 0); -+ this.postLightUpdate(offX, offY, offZ); -+ -+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... -+ if (queueLength >= queue.length) { -+ queue = this.resizeDecreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) -+ | ((targetLevel & 0xFL) << (6 + 6 + 16)) -+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) -+ | flags; -+ } -+ continue; -+ } -+ } -+ } -+ } -+ -+ // propagate sources we clobbered -+ this.increaseQueueInitialLength = increaseQueueLength; -+ this.performLightIncrease(lightAccess); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/starlight/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/starlight/light/StarLightInterface.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/starlight/light/StarLightInterface.java -@@ -0,0 +0,0 @@ -+package ca.spottedleaf.starlight.light; -+ -+import io.papermc.paper.util.CoordinateUtils; -+import io.papermc.paper.util.WorldUtil; -+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.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.ChunkStatus; -+import net.minecraft.world.level.chunk.DataLayer; -+import net.minecraft.world.level.chunk.LightChunkGetter; -+import net.minecraft.world.level.lighting.LayerLightEventListener; -+import net.minecraft.world.level.lighting.LevelLightEngine; -+import java.util.*; -+import java.util.concurrent.CompletableFuture; -+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", (p1, p2) -> Long.compare(p1.toLong(), p2.toLong())); -+ -+ /** -+ * Can be {@code null}, indicating the light is all empty. -+ */ -+ protected final Level world; -+ protected final LightChunkGetter lightAccess; -+ -+ protected final ArrayDeque cachedSkyPropagators; -+ protected final ArrayDeque cachedBlockPropagators; -+ -+ protected final LightQueue lightQueue = new LightQueue(this); -+ -+ protected final LayerLightEventListener skyReader; -+ protected final LayerLightEventListener blockReader; -+ protected final boolean isClientSide; -+ -+ protected final int minSection; -+ protected final int maxSection; -+ protected final int minLightSection; -+ protected final int maxLightSection; -+ -+ public final LevelLightEngine lightEngine; -+ -+ 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 = 0; -+ this.maxSection = 15; -+ this.minLightSection = -1; -+ this.maxLightSection = 16; -+ } 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); -+ } -+ this.lightEngine = lightEngine; -+ this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { -+ @Override -+ public void checkBlock(final BlockPos blockPos) { -+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); -+ } -+ -+ @Override -+ public void onBlockEmissionIncrease(final BlockPos blockPos, final int i) { -+ // skylight doesn't care -+ } -+ -+ @Override -+ public boolean hasLightWork() { -+ // not really correct... -+ return StarLightInterface.this.hasUpdates(); -+ } -+ -+ @Override -+ public int runUpdates(final int i, final boolean bl, final boolean bl2) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void enableLightSources(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.getStatus().isOrAfter(ChunkStatus.LIGHT)) { -+ return null; -+ } -+ -+ final int sectionY = pos.getY(); -+ -+ if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) { -+ return null; -+ } -+ -+ if (chunk.getSkyEmptinessMap() == null) { -+ return null; -+ } -+ -+ return chunk.getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble(); -+ } -+ -+ @Override -+ public int getLightValue(final BlockPos blockPos) { -+ final int x = blockPos.getX(); -+ int y = blockPos.getY(); -+ final int z = blockPos.getZ(); -+ -+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(x >> 4, z >> 4); -+ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { -+ return 15; -+ } -+ -+ int sectionY = y >> 4; -+ -+ if (sectionY > StarLightInterface.this.maxLightSection) { -+ return 15; -+ } -+ -+ if (sectionY < StarLightInterface.this.minLightSection) { -+ sectionY = StarLightInterface.this.minLightSection; -+ y = sectionY << 4; -+ } -+ -+ final SWMRNibbleArray[] nibbles = chunk.getSkyNibbles(); -+ final SWMRNibbleArray immediate = nibbles[sectionY - StarLightInterface.this.minLightSection]; -+ -+ if (StarLightInterface.this.isClientSide) { -+ if (!immediate.isNullNibbleUpdating()) { -+ return immediate.getUpdating(x, y, z); -+ } -+ } else { -+ if (!immediate.isNullNibbleVisible()) { -+ return immediate.getVisible(x, y, z); -+ } -+ } -+ -+ final boolean[] emptinessMap = chunk.getSkyEmptinessMap(); -+ -+ if (emptinessMap == null) { -+ return 15; -+ } -+ -+ // are we above this chunk's lowest empty section? -+ int lowestY = StarLightInterface.this.minLightSection - 1; -+ for (int currY = StarLightInterface.this.maxSection; currY >= StarLightInterface.this.minSection; --currY) { -+ if (emptinessMap[currY - StarLightInterface.this.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 <= StarLightInterface.this.maxLightSection; ++currY) { -+ final SWMRNibbleArray nibble = nibbles[currY - StarLightInterface.this.minLightSection]; -+ if (StarLightInterface.this.isClientSide) { -+ if (!nibble.isNullNibbleUpdating()) { -+ return nibble.getUpdating(x, 0, z); -+ } -+ } else { -+ if (!nibble.isNullNibbleVisible()) { -+ return nibble.getVisible(x, 0, z); -+ } -+ } -+ } -+ -+ // should never reach here -+ return 15; -+ } -+ -+ @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 onBlockEmissionIncrease(final BlockPos blockPos, final int i) { -+ this.checkBlock(blockPos); -+ } -+ -+ @Override -+ public boolean hasLightWork() { -+ // not really correct... -+ return StarLightInterface.this.hasUpdates(); -+ } -+ -+ @Override -+ public int runUpdates(final int i, final boolean bl, final boolean bl2) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void enableLightSources(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 chunk.getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble(); -+ } -+ -+ @Override -+ public int getLightValue(final BlockPos blockPos) { -+ final int cx = blockPos.getX() >> 4; -+ final int cy = blockPos.getY() >> 4; -+ final int cz = blockPos.getZ() >> 4; -+ -+ if (cy < StarLightInterface.this.minLightSection || cy > StarLightInterface.this.maxLightSection) { -+ return 0; -+ } -+ -+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(cx, cz); -+ -+ if (chunk == null) { -+ return 0; -+ } -+ -+ final SWMRNibbleArray nibble = chunk.getBlockNibbles()[cy - StarLightInterface.this.minLightSection]; -+ if (StarLightInterface.this.isClientSide) { -+ return nibble.getUpdating(blockPos.getX(), blockPos.getY(), blockPos.getZ()); -+ } else { -+ return nibble.getVisible(blockPos.getX(), blockPos.getY(), blockPos.getZ()); -+ } -+ } -+ -+ @Override -+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) { -+ StarLightInterface.this.sectionChange(pos, notReady); -+ } -+ }; -+ } -+ -+ 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 ((ServerLevel)this.world).getChunkSource().getChunkAtImmediately(chunkX, chunkZ); -+ } -+ -+ public boolean hasUpdates() { -+ return !this.lightQueue.isEmpty(); -+ } -+ -+ public Level getWorld() { -+ return this.world; -+ } -+ -+ public LightChunkGetter getLightAccess() { -+ return this.lightAccess; -+ } -+ -+ protected final 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; -+ } -+ -+ protected final void releaseSkyLightEngine(final SkyStarLightEngine engine) { -+ if (this.cachedSkyPropagators == null) { -+ return; -+ } -+ synchronized (this.cachedSkyPropagators) { -+ this.cachedSkyPropagators.addFirst(engine); -+ } -+ } -+ -+ protected final 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; -+ } -+ -+ protected final void releaseBlockLightEngine(final BlockStarLightEngine engine) { -+ if (this.cachedBlockPropagators == null) { -+ return; -+ } -+ synchronized (this.cachedBlockPropagators) { -+ this.cachedBlockPropagators.addFirst(engine); -+ } -+ } -+ -+ public CompletableFuture 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 CompletableFuture 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 checkSkyEdges(final int chunkX, final int chunkZ, final ShortCollection sections) { -+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); -+ -+ try { -+ if (skyEngine != null) { -+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections); -+ } -+ } finally { -+ this.releaseSkyLightEngine(skyEngine); -+ } -+ } -+ -+ public void checkBlockEdges(final int chunkX, final int chunkZ, final ShortCollection sections) { -+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); -+ try { -+ if (blockEngine != null) { -+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections); -+ } -+ } finally { -+ this.releaseBlockLightEngine(blockEngine); -+ } -+ } -+ -+ public void scheduleChunkLight(final ChunkPos pos, final Runnable run) { -+ this.lightQueue.queueChunkLighting(pos, run); -+ } -+ -+ public void removeChunkTasks(final ChunkPos pos) { -+ this.lightQueue.removeChunk(pos); -+ } -+ -+ public void propagateChanges() { -+ if (this.lightQueue.isEmpty()) { -+ return; -+ } -+ -+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); -+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); -+ -+ try { -+ LightQueue.ChunkTasks task; -+ while ((task = this.lightQueue.removeFirstTask()) != null) { -+ if (task.lightTasks != null) { -+ for (final Runnable run : task.lightTasks) { -+ run.run(); -+ } -+ } -+ -+ final long coordinate = task.chunkCoordinate; -+ final int chunkX = CoordinateUtils.getChunkX(coordinate); -+ final int chunkZ = CoordinateUtils.getChunkZ(coordinate); -+ -+ final Set positions = task.changedPositions; -+ final Boolean[] sectionChanges = task.changedSectionSet; -+ -+ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { -+ skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); -+ } -+ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { -+ blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); -+ } -+ -+ if (skyEngine != null && task.queuedEdgeChecksSky != null) { -+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksSky); -+ } -+ if (blockEngine != null && task.queuedEdgeChecksBlock != null) { -+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksBlock); -+ } -+ -+ task.onComplete.complete(null); -+ } -+ } finally { -+ this.releaseSkyLightEngine(skyEngine); -+ this.releaseBlockLightEngine(blockEngine); -+ } -+ } -+ -+ protected static final class LightQueue { -+ -+ protected final Long2ObjectLinkedOpenHashMap chunkTasks = new Long2ObjectLinkedOpenHashMap<>(); -+ protected final StarLightInterface manager; -+ -+ public LightQueue(final StarLightInterface manager) { -+ this.manager = manager; -+ } -+ -+ public synchronized boolean isEmpty() { -+ return this.chunkTasks.isEmpty(); -+ } -+ -+ public synchronized CompletableFuture queueBlockChange(final BlockPos pos) { -+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); -+ tasks.changedPositions.add(pos.immutable()); -+ return tasks.onComplete; -+ } -+ -+ public synchronized CompletableFuture queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { -+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); -+ -+ if (tasks.changedSectionSet == null) { -+ tasks.changedSectionSet = new Boolean[this.manager.maxSection - this.manager.minSection + 1]; -+ } -+ tasks.changedSectionSet[pos.getY() - this.manager.minSection] = Boolean.valueOf(newEmptyValue); -+ -+ return tasks.onComplete; -+ } -+ -+ public synchronized CompletableFuture queueChunkLighting(final ChunkPos pos, final Runnable lightTask) { -+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); -+ if (tasks.lightTasks == null) { -+ tasks.lightTasks = new ArrayList<>(); -+ } -+ tasks.lightTasks.add(lightTask); -+ -+ return tasks.onComplete; -+ } -+ -+ public synchronized CompletableFuture queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { -+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); -+ -+ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksSky; -+ if (queuedEdges == null) { -+ queuedEdges = tasks.queuedEdgeChecksSky = new ShortOpenHashSet(); -+ } -+ queuedEdges.addAll(sections); -+ -+ return tasks.onComplete; -+ } -+ -+ public synchronized CompletableFuture queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { -+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); -+ -+ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksBlock; -+ if (queuedEdges == null) { -+ queuedEdges = tasks.queuedEdgeChecksBlock = new ShortOpenHashSet(); -+ } -+ queuedEdges.addAll(sections); -+ -+ return tasks.onComplete; -+ } -+ -+ public void removeChunk(final ChunkPos pos) { -+ final ChunkTasks tasks; -+ synchronized (this) { -+ tasks = this.chunkTasks.remove(CoordinateUtils.getChunkKey(pos)); -+ } -+ if (tasks != null) { -+ tasks.onComplete.complete(null); -+ } -+ } -+ -+ public synchronized ChunkTasks removeFirstTask() { -+ if (this.chunkTasks.isEmpty()) { -+ return null; -+ } -+ return this.chunkTasks.removeFirst(); -+ } -+ -+ protected static final class ChunkTasks { -+ -+ public final Set changedPositions = new HashSet<>(); -+ public Boolean[] changedSectionSet; -+ public ShortOpenHashSet queuedEdgeChecksSky; -+ public ShortOpenHashSet queuedEdgeChecksBlock; -+ public List lightTasks; -+ -+ public final CompletableFuture onComplete = new CompletableFuture<>(); -+ -+ public final long chunkCoordinate; -+ -+ public ChunkTasks(final long chunkCoordinate) { -+ this.chunkCoordinate = chunkCoordinate; -+ } -+ } -+ } -+} -diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/com/destroystokyo/paper/PaperCommand.java -+++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java -@@ -0,0 +0,0 @@ public class PaperCommand extends Command { - } - } - -+ // Paper start - rewrite light engine -+ private void starlightFixLight(ServerPlayer sender, ServerLevel world, ThreadedLevelLightEngine lightengine, int radius) { -+ long start = System.nanoTime(); -+ 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 -+ -+ int[] pending = new int[1]; -+ for (java.util.Iterator iterator = chunks.iterator(); iterator.hasNext();) { -+ final ChunkPos chunkPos = iterator.next(); -+ -+ final net.minecraft.world.level.chunk.ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(chunkPos.x, chunkPos.z); -+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(net.minecraft.world.level.chunk.ChunkStatus.LIGHT)) { -+ // cannot relight this chunk -+ iterator.remove(); -+ continue; -+ } -+ -+ ++pending[0]; -+ } -+ -+ int[] relitChunks = new int[1]; -+ lightengine.relight(chunks, -+ (ChunkPos chunkPos) -> { -+ ++relitChunks[0]; -+ sender.getBukkitEntity().sendMessage( -+ ChatColor.BLUE + "Relit chunk " + ChatColor.DARK_AQUA + chunkPos + ChatColor.BLUE + -+ ", progress: " + ChatColor.DARK_AQUA + (int)(Math.round(100.0 * (double)(relitChunks[0])/(double)pending[0])) + "%" -+ ); -+ }, -+ (int totalRelit) -> { -+ final long end = System.nanoTime(); -+ final long diff = Math.round(1.0e-6*(end - start)); -+ sender.getBukkitEntity().sendMessage( -+ ChatColor.BLUE + "Relit " + ChatColor.DARK_AQUA + totalRelit + ChatColor.BLUE + " chunks. Took " + -+ ChatColor.DARK_AQUA + diff + "ms" -+ ); -+ }); -+ sender.getBukkitEntity().sendMessage(ChatColor.BLUE + "Relighting " + ChatColor.DARK_AQUA + pending[0] + ChatColor.BLUE + " chunks"); -+ } -+ // Paper end - rewrite light engine -+ - private void doFixLight(CommandSender sender, String[] args) { - if (!(sender instanceof Player)) { - sender.sendMessage("Only players can use this command"); -@@ -0,0 +0,0 @@ public class PaperCommand extends Command { - int radius = 2; - if (args.length > 1) { - try { -- radius = Math.min(5, Integer.parseInt(args[1])); -+ radius = Math.min(32, Integer.parseInt(args[1])); // Paper - MOOOOOORE - } catch (Exception e) { - sender.sendMessage("Not a number"); - return; -@@ -0,0 +0,0 @@ public class PaperCommand extends Command { - ServerLevel world = (ServerLevel) handle.level; - ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine(); - -+ // Paper start - rewrite light engine -+ if (true) { -+ this.starlightFixLight(handle, world, lightengine, radius); -+ return; -+ } -+ // Paper end - rewrite light engine -+ - net.minecraft.core.BlockPos center = MCUtil.toBlockPosition(player.getLocation()); - Deque queue = new ArrayDeque<>(MCUtil.getSpiralOutChunks(center, radius)); - updateLight(sender, world, lightengine, queue); -diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java -+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java -@@ -0,0 +0,0 @@ public class ClientboundLightUpdatePacket implements Packet { -- if (remainingSends.get() == 0) { -- cleaner1.run(); -- cleaner2.run(); -- } -- }, "Light Packet Release"); -- } -+ // Paper - rewrite light engine - } - - @Override - public boolean hasFinishListener() { -- return true; -+ return false; // Paper - rewrite light engine - } - - // Paper end -@@ -0,0 +0,0 @@ public class ClientboundLightUpdatePacket implements Packet> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage - private volatile CompletableFuture> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage - private volatile CompletableFuture> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage -- private CompletableFuture chunkToSave; -+ public CompletableFuture chunkToSave; // Paper - public - @Nullable - private final DebugBuffer chunkToSaveHistory; - public int oldTicketLevel; -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkMap.java -+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - public final LongSet entitiesInLevel; - public final ServerLevel level; - private final ThreadedLevelLightEngine lightEngine; -- private final BlockableEventLoop mainThreadExecutor; -+ public final BlockableEventLoop mainThreadExecutor; // Paper - public - final java.util.concurrent.Executor mainInvokingExecutor; // Paper - public final ChunkGenerator generator; - public final Supplier overworldDataStorage; -diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java -+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java -@@ -0,0 +0,0 @@ import net.minecraft.world.level.lighting.LevelLightEngine; - import org.apache.logging.log4j.LogManager; - import org.apache.logging.log4j.Logger; - -+// Paper start -+import ca.spottedleaf.starlight.light.StarLightEngine; -+import io.papermc.paper.util.CoordinateUtils; -+import java.util.function.Supplier; -+import net.minecraft.world.level.lighting.LayerLightEventListener; -+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; -+import it.unimi.dsi.fastutil.longs.LongArrayList; -+import it.unimi.dsi.fastutil.longs.LongIterator; -+import net.minecraft.world.level.chunk.ChunkStatus; -+// Paper end -+ - public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable { - private static final Logger LOGGER = LogManager.getLogger(); - private final ProcessorMailbox taskMailbox; -@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - private volatile int taskPerBatch = 5; - private final AtomicBoolean scheduled = new AtomicBoolean(); - -+ // Paper start - replace light engine impl -+ protected final ca.spottedleaf.starlight.light.StarLightInterface theLightEngine; -+ public final boolean hasBlockLight; -+ public final boolean hasSkyLight; -+ // Paper end - replace light engine impl -+ - public ThreadedLevelLightEngine(LightChunkGetter chunkProvider, ChunkMap chunkStorage, boolean hasBlockLight, ProcessorMailbox processor, ProcessorHandle> executor) { -- super(chunkProvider, true, hasBlockLight); -+ super(chunkProvider, false, false); // Paper - destroy vanilla light engine state - this.chunkMap = chunkStorage; this.playerChunkMap = chunkMap; // Paper - this.sorterMailbox = executor; - this.taskMailbox = processor; -+ // Paper start - replace light engine impl -+ this.hasBlockLight = true; -+ this.hasSkyLight = hasBlockLight; // Nice variable name. -+ this.theLightEngine = new ca.spottedleaf.starlight.light.StarLightInterface(chunkProvider, this.hasSkyLight, this.hasBlockLight, this); -+ // Paper end - replace light engine impl -+ } -+ -+ // Paper start - replace light engine impl -+ protected final ChunkAccess getChunk(final int chunkX, final int chunkZ) { -+ return ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkAtImmediately(chunkX, chunkZ); -+ } -+ -+ protected long relightCounter; -+ -+ public int relight(java.util.Set chunks_param, -+ java.util.function.Consumer chunkLightCallback, -+ java.util.function.IntConsumer onComplete) { -+ if (!org.bukkit.Bukkit.isPrimaryThread()) { -+ throw new IllegalStateException("Must only be called on the main thread"); -+ } -+ -+ java.util.Set chunks = new java.util.LinkedHashSet<>(chunks_param); -+ // add tickets -+ java.util.Map ticketIds = new java.util.HashMap<>(); -+ int totalChunks = 0; -+ for (java.util.Iterator iterator = chunks.iterator(); iterator.hasNext();) { -+ final ChunkPos chunkPos = iterator.next(); -+ -+ final ChunkAccess chunk = ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkAtImmediately(chunkPos.x, chunkPos.z); -+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { -+ // cannot relight this chunk -+ iterator.remove(); -+ continue; -+ } -+ -+ final Long id = Long.valueOf(this.relightCounter++); -+ -+ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().addTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), id); -+ ticketIds.put(chunkPos, id); -+ -+ ++totalChunks; -+ } -+ -+ this.taskMailbox.tell(() -> { -+ this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> { -+ chunkLightCallback.accept(chunkPos); -+ ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> { -+ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong()).broadcast(new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(chunkPos, ThreadedLevelLightEngine.this, null, null, true), false); -+ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos)); -+ }); -+ }, onComplete); -+ }); -+ this.tryScheduleUpdate(); -+ -+ return totalChunks; -+ } -+ -+ private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); -+ -+ private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, final Supplier> runnable) { -+ final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld(); -+ -+ final ChunkAccess center = this.theLightEngine.getAnyChunkNow(chunkX, chunkZ); -+ if (center == null || !center.getStatus().isOrAfter(ChunkStatus.LIGHT)) { -+ // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing -+ // chunk scheduling, we could be lighting and generating a chunk at the same time -+ return; -+ } -+ -+ if (center.getStatus() != ChunkStatus.FULL) { -+ // do not keep chunk loaded, we are probably in a gen thread -+ // if we proceed to add a ticket the chunk will be loaded, which is not what we want (avoid cascading gen) -+ runnable.get(); -+ return; -+ } -+ -+ if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) { -+ // ticket logic is not safe to run off-main, re-schedule -+ world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> { -+ this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable); -+ }); -+ return; -+ } -+ -+ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ -+ final CompletableFuture updateFuture = runnable.get(); -+ -+ if (updateFuture == null) { -+ // not scheduled -+ return; -+ } -+ -+ final int references = this.chunksBeingWorkedOn.addTo(key, 1); -+ if (references == 0) { -+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ); -+ world.getChunkSource().addRegionTicket(ca.spottedleaf.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); -+ } -+ -+ // append future to this chunk and 1 radius neighbours chunk save futures -+ // this prevents us from saving the world without first waiting for the light engine -+ -+ for (int dx = -1; dx <= 1; ++dx) { -+ for (int dz = -1; dz <= 1; ++dz) { -+ ChunkHolder neighbour = world.getChunkSource().chunkMap.getUpdatingChunkIfPresent(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ)); -+ if (neighbour != null) { -+ neighbour.chunkToSave = neighbour.chunkToSave.thenCombine(updateFuture, (final ChunkAccess curr, final Void ignore) -> { -+ return curr; -+ }); -+ } -+ } -+ } -+ -+ updateFuture.thenAcceptAsync((final Void ignore) -> { -+ final int newReferences = this.chunksBeingWorkedOn.get(key); -+ if (newReferences == 1) { -+ this.chunksBeingWorkedOn.remove(key); -+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ); -+ world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); -+ } else { -+ this.chunksBeingWorkedOn.put(key, newReferences - 1); -+ } -+ }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> { -+ if (thr != null) { -+ LOGGER.fatal("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr); -+ } -+ }); -+ } -+ -+ @Override -+ public boolean hasLightWork() { -+ // route to new light engine -+ return this.theLightEngine.hasUpdates() || !this.queue.isEmpty(); - } - -+ @Override -+ public LayerLightEventListener getLayerListener(final LightLayer lightType) { -+ return lightType == LightLayer.BLOCK ? this.theLightEngine.getBlockReader() : this.theLightEngine.getSkyReader(); -+ } -+ -+ @Override -+ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) { -+ // need to use new light hooks for this -+ final int sky = this.theLightEngine.getSkyReader().getLightValue(pos) - ambientDarkness; -+ final int block = this.theLightEngine.getBlockReader().getLightValue(pos); -+ return Math.max(sky, block); -+ } -+ // Paper end - replace light engine impl -+ - @Override - public void close() { - } -@@ -0,0 +0,0 @@ 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.POST_UPDATE, Util.name(() -> { -- super.checkBlock(blockPos); -- }, () -> { -- return "checkBlock " + blockPos; -- })); -+ // Paper start - replace light engine impl -+ final BlockPos posCopy = pos.immutable(); -+ this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> { -+ return this.theLightEngine.blockChange(posCopy); -+ }); -+ // Paper end - replace light engine impl - } - - protected void updateChunkStatus(ChunkPos pos) { -+ if (true) return; // Paper - replace light engine impl - this.addTask(pos.x, pos.z, () -> { - return 0; - }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { -@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - - @Override - public void updateSectionStatus(SectionPos pos, boolean notReady) { -- this.addTask(pos.x(), pos.z(), () -> { -- return 0; -- }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { -- super.updateSectionStatus(pos, notReady); -- }, () -> { -- return "updateSectionStatus " + pos + " " + notReady; -- })); -+ // Paper start - replace light engine impl -+ this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> { -+ return this.theLightEngine.sectionChange(pos, notReady); -+ }); -+ // Paper end - replace light engine impl - } - - @Override - public void enableLightSources(ChunkPos chunkPos, boolean bl) { -+ if (true) return; // Paper - replace light engine impl - this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { - super.enableLightSources(chunkPos, bl); - }, () -> { -@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - - @Override - public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles, boolean bl) { -+ if (true) return; // Paper - replace light engine impl - this.addTask(pos.x(), pos.z(), () -> { - return 0; - }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { -@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - - @Override - public void retainData(ChunkPos pos, boolean retainData) { -+ if (true) return; // Paper - replace light engine impl - this.addTask(pos.x, pos.z, () -> { - return 0; - }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { -@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - } - - public CompletableFuture lightChunk(ChunkAccess chunk, boolean excludeBlocks) { -+ // Paper start - replace light engine impl -+ if (true) { -+ boolean lit = excludeBlocks; -+ final ChunkPos chunkPos = chunk.getPos(); -+ -+ return CompletableFuture.supplyAsync(() -> { -+ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk); -+ if (!lit) { -+ chunk.setLightCorrect(false); -+ this.theLightEngine.lightChunk(chunk, emptySections); -+ chunk.setLightCorrect(true); -+ } else { -+ this.theLightEngine.forceLoadInChunk(chunk, emptySections); -+ // can't really force the chunk to be edged checked, as we need neighbouring chunks - but we don't have -+ // them, so if it's not loaded then i guess we can't do edge checks. later loads of the chunk should -+ // catch what we miss here. -+ this.theLightEngine.checkChunkEdges(chunkPos.x, chunkPos.z); -+ } -+ -+ this.chunkMap.releaseLightTicket(chunkPos); -+ return chunk; -+ }, (runnable) -> { -+ this.theLightEngine.scheduleChunkLight(chunkPos, runnable); -+ this.tryScheduleUpdate(); -+ }).whenComplete((final ChunkAccess c, final Throwable throwable) -> { -+ if (throwable != null) { -+ LOGGER.fatal("Failed to light chunk " + chunkPos, throwable); -+ } -+ }); -+ } -+ // Paper end - replace light engine impl - ChunkPos chunkPos = chunk.getPos(); - // Paper start - //ichunkaccess.b(false); // Don't need to disable this -@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - } - - public void tryScheduleUpdate() { -- if ((!this.queue.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { // Paper -+ if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Paper // Paper - rewrite light engine - this.taskMailbox.tell(() -> { - this.runUpdate(); - this.scheduled.set(false); -@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - if (queue.poll(pre, post)) { - pre.forEach(Runnable::run); - pre.clear(); -- super.runUpdates(Integer.MAX_VALUE, true, true); -+ this.theLightEngine.propagateChanges(); // Paper - rewrite light engine - post.forEach(Runnable::run); - post.clear(); - } else { - // might have level updates to go still -- super.runUpdates(Integer.MAX_VALUE, true, true); -+ this.theLightEngine.propagateChanges(); // Paper - rewrite light engine - } - // Paper end - } -diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/TicketType.java -+++ b/src/main/java/net/minecraft/server/level/TicketType.java -@@ -0,0 +0,0 @@ public class TicketType { - public static final TicketType PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit - public static final TicketType DELAY_UNLOAD = create("delay_unload", Long::compareTo, 300); // Paper - public static final TicketType REQUIRED_LOAD = create("required_load", Long::compareTo); // Paper - make sure getChunkAt does not fail -+ public static final TicketType CHUNK_RELIGHT = create("light_update", Long::compareTo); // Paper - ensure chunks stay loaded for lighting - - public static TicketType create(String name, Comparator argumentComparator) { - return new TicketType<>(name, argumentComparator, 0L); -diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -@@ -0,0 +0,0 @@ public abstract class BlockBehaviour { - this.isViewBlocking = blockbase_info.isViewBlocking; - this.hasPostProcess = blockbase_info.hasPostProcess; - this.emissiveRendering = blockbase_info.emissiveRendering; -+ this.conditionallyFullOpaque = this.isOpaque() & this.isTransparentOnSomeFaces(); // Paper - } - // Paper start - impl cached craft block data, lazy load to fix issue with loading at the wrong time - private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData; -@@ -0,0 +0,0 @@ public abstract class BlockBehaviour { - protected boolean isTicking; - protected FluidState fluid; - // Paper end -+ // Paper start -+ protected int opacityIfCached = -1; -+ // ret -1 if opacity is dynamic, or -1 if the block is conditionally full opaque, else return opacity in [0, 15] -+ public final int getOpacityIfCached() { -+ return this.opacityIfCached; -+ } -+ -+ protected final boolean conditionallyFullOpaque; -+ public final boolean isConditionallyFullOpaque() { -+ return this.conditionallyFullOpaque; -+ } -+ // Paper end - - public void initCache() { - this.fluid = this.getBlock().getFluidState(this.asState()); // Paper - moved from getFluid() -@@ -0,0 +0,0 @@ public abstract class BlockBehaviour { - this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState()); - } - this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here -+ this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - cache opacity for light - - } - -diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java -+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java -@@ -0,0 +0,0 @@ public interface ChunkAccess extends BlockGetter, FeatureAccess { - net.minecraft.world.level.Level getLevel(); - // Paper end - -+ // Paper start -+ default ca.spottedleaf.starlight.light.SWMRNibbleArray[] getBlockNibbles() { -+ throw new UnsupportedOperationException(this.getClass().getName()); -+ } -+ default void setBlockNibbles(ca.spottedleaf.starlight.light.SWMRNibbleArray[] nibbles) { -+ throw new UnsupportedOperationException(this.getClass().getName()); -+ } -+ -+ default ca.spottedleaf.starlight.light.SWMRNibbleArray[] getSkyNibbles() { -+ throw new UnsupportedOperationException(this.getClass().getName()); -+ } -+ default void setSkyNibbles(ca.spottedleaf.starlight.light.SWMRNibbleArray[] nibbles) { -+ throw new UnsupportedOperationException(this.getClass().getName()); -+ } -+ public default boolean[] getSkyEmptinessMap() { -+ throw new UnsupportedOperationException(this.getClass().getName()); -+ } -+ public default void setSkyEmptinessMap(final boolean[] emptinessMap) { -+ throw new UnsupportedOperationException(this.getClass().getName()); -+ } -+ -+ public default boolean[] getBlockEmptinessMap() { -+ throw new UnsupportedOperationException(this.getClass().getName()); -+ } -+ -+ public default void setBlockEmptinessMap(final boolean[] emptinessMap) { -+ throw new UnsupportedOperationException(this.getClass().getName()); -+ } -+ // Paper end -+ - BlockState getType(final int x, final int y, final int z); // Paper - @Nullable - BlockState setBlockState(BlockPos pos, BlockState state, boolean moved); -diff --git a/src/main/java/net/minecraft/world/level/chunk/DataLayer.java b/src/main/java/net/minecraft/world/level/chunk/DataLayer.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/DataLayer.java -+++ b/src/main/java/net/minecraft/world/level/chunk/DataLayer.java -@@ -0,0 +0,0 @@ public final class DataLayer { - public static final int SIZE = 2048; - private static final int NIBBLE_SIZE = 4; - @Nullable -- protected byte[] data; -+ protected byte[] data; public final byte[] getDataRaw() { return this.data; } // Paper - provide accessor - // Paper start - public static byte[] EMPTY_NIBBLE = new byte[2048]; - private static final int nibbleBucketSizeMultiplier = Integer.getInteger("Paper.nibbleBucketSize", 3072); -@@ -0,0 +0,0 @@ public final class DataLayer { - boolean poolSafe = false; - public java.lang.Runnable cleaner; - private void registerCleaner() { -+ if (true) return; // Paper - purge cleaner usage - if (!poolSafe) { - cleaner = net.minecraft.server.MCUtil.registerCleaner(this, this.data, DataLayer::releaseBytes); - } else { -@@ -0,0 +0,0 @@ public final class DataLayer { - } - public DataLayer(byte[] bytes, boolean isSafe) { - this.data = bytes; -- if (!isSafe) this.data = getCloneIfSet(); // Paper - clone for safety -+ // Paper - purge cleaner usage - registerCleaner(); - // Paper end - if (bytes.length != 2048) { -@@ -0,0 +0,0 @@ public final class DataLayer { - } - // Paper end - public DataLayer copy() { -- return this.data == null ? new DataLayer() : new DataLayer(this.data); // Paper - clone in ctor -+ return this.data == null ? new DataLayer() : new DataLayer(this.data.clone()); // Paper - clone in ctor // Paper - no longer clone in constructor - } - - public String toString() { -diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java -@@ -0,0 +0,0 @@ - package net.minecraft.world.level.chunk; - -+import ca.spottedleaf.starlight.light.SWMRNibbleArray; - import it.unimi.dsi.fastutil.longs.LongSet; - import java.util.BitSet; - import java.util.Map; -@@ -0,0 +0,0 @@ public class ImposterProtoChunk extends ProtoChunk { - this.wrapped = wrapped; - } - -+ // Paper start - rewrite light engine -+ @Override -+ public SWMRNibbleArray[] getBlockNibbles() { -+ return this.getWrapped().getBlockNibbles(); -+ } -+ -+ @Override -+ public void setBlockNibbles(SWMRNibbleArray[] nibbles) { -+ this.getWrapped().setBlockNibbles(nibbles); -+ } -+ -+ @Override -+ public SWMRNibbleArray[] getSkyNibbles() { -+ return this.getWrapped().getSkyNibbles(); -+ } -+ -+ @Override -+ public void setSkyNibbles(SWMRNibbleArray[] nibbles) { -+ this.getWrapped().setSkyNibbles(nibbles); -+ } -+ -+ @Override -+ public boolean[] getSkyEmptinessMap() { -+ return this.getWrapped().getSkyEmptinessMap(); -+ } -+ -+ @Override -+ public void setSkyEmptinessMap(boolean[] emptinessMap) { -+ this.getWrapped().setSkyEmptinessMap(emptinessMap); -+ } -+ -+ @Override -+ public boolean[] getBlockEmptinessMap() { -+ return this.getWrapped().getBlockEmptinessMap(); -+ } -+ -+ @Override -+ public void setBlockEmptinessMap(boolean[] emptinessMap) { -+ this.getWrapped().setBlockEmptinessMap(emptinessMap); -+ } -+ // Paper end - rewrite light engine -+ - @Nullable - @Override - public BlockEntity getBlockEntity(BlockPos pos) { -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -@@ -0,0 +0,0 @@ - package net.minecraft.world.level.chunk; - -+import ca.spottedleaf.starlight.light.SWMRNibbleArray; -+import ca.spottedleaf.starlight.light.StarLightEngine; - import com.google.common.collect.ImmutableList; - import com.destroystokyo.paper.exception.ServerInternalException; - import com.google.common.collect.Maps; -@@ -0,0 +0,0 @@ import java.util.Collections; - import java.util.Iterator; - import java.util.Map; - import java.util.Map.Entry; --import java.util.Objects; - import java.util.Set; - import java.util.function.Consumer; - import java.util.function.Supplier; -@@ -0,0 +0,0 @@ import net.minecraft.CrashReport; - import net.minecraft.CrashReportCategory; - import net.minecraft.ReportedException; - import net.minecraft.core.BlockPos; --import net.minecraft.core.DefaultedRegistry; - import net.minecraft.core.Registry; - import net.minecraft.core.SectionPos; - import net.minecraft.nbt.CompoundTag; -@@ -0,0 +0,0 @@ public class LevelChunk implements ChunkAccess { - private volatile boolean isLightCorrect; - private final Int2ObjectMap gameEventDispatcherSections; - -+ // Paper start - rewrite light engine -+ protected volatile SWMRNibbleArray[] blockNibbles; -+ protected volatile SWMRNibbleArray[] skyNibbles; -+ protected volatile boolean[] skyEmptinessMap; -+ protected volatile boolean[] blockEmptinessMap; -+ -+ @Override -+ public SWMRNibbleArray[] getBlockNibbles() { -+ return this.blockNibbles; -+ } -+ -+ @Override -+ public void setBlockNibbles(SWMRNibbleArray[] nibbles) { -+ this.blockNibbles = nibbles; -+ } -+ -+ @Override -+ public SWMRNibbleArray[] getSkyNibbles() { -+ return this.skyNibbles; -+ } -+ -+ @Override -+ public void setSkyNibbles(SWMRNibbleArray[] nibbles) { -+ this.skyNibbles = nibbles; -+ } -+ -+ @Override -+ public boolean[] getSkyEmptinessMap() { -+ return this.skyEmptinessMap; -+ } -+ -+ @Override -+ public void setSkyEmptinessMap(boolean[] emptinessMap) { -+ this.skyEmptinessMap = emptinessMap; -+ } -+ -+ @Override -+ public boolean[] getBlockEmptinessMap() { -+ return this.blockEmptinessMap; -+ } -+ -+ @Override -+ public void setBlockEmptinessMap(boolean[] emptinessMap) { -+ this.blockEmptinessMap = emptinessMap; -+ } -+ // Paper end - rewrite light engine -+ - public LevelChunk(Level world, ChunkPos pos, ChunkBiomeContainer biomes) { - this(world, pos, biomes, UpgradeData.EMPTY, EmptyTickList.empty(), EmptyTickList.empty(), 0L, (LevelChunkSection[]) null, (Consumer) null); - } - - public LevelChunk(Level world, ChunkPos pos, ChunkBiomeContainer biomes, UpgradeData upgradeData, TickList blockTickScheduler, TickList fluidTickScheduler, long inhabitedTime, @Nullable LevelChunkSection[] sections, @Nullable Consumer loadToWorldConsumer) { -+ // Paper start -+ this.blockNibbles = StarLightEngine.getFilledEmptyLight(world); -+ this.skyNibbles = StarLightEngine.getFilledEmptyLight(world); -+ // Paper end - this.pendingBlockEntities = Maps.newHashMap(); - this.tickersInLevel = Maps.newHashMap(); - this.heightmaps = Maps.newEnumMap(Heightmap.Types.class); -@@ -0,0 +0,0 @@ public class LevelChunk implements ChunkAccess { - - public LevelChunk(ServerLevel worldserver, ProtoChunk protoChunk, @Nullable Consumer consumer) { - this(worldserver, protoChunk.getPos(), protoChunk.getBiomes(), protoChunk.getUpgradeData(), protoChunk.getBlockTicks(), protoChunk.getLiquidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), consumer); -+ // Paper start - copy over protochunk light -+ this.setBlockNibbles(protoChunk.getBlockNibbles()); -+ this.setSkyNibbles(protoChunk.getSkyNibbles()); -+ this.setSkyEmptinessMap(protoChunk.getSkyEmptinessMap()); -+ this.setBlockEmptinessMap(protoChunk.getBlockEmptinessMap()); -+ // Paper end - copy over protochunk light - Iterator iterator = protoChunk.getBlockEntities().values().iterator(); - - while (iterator.hasNext()) { -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java -@@ -0,0 +0,0 @@ public class LevelChunkSection { - short nonEmptyBlockCount; // Paper - package-private - private short tickingBlockCount; - private short tickingFluidCount; -- final PalettedContainer states; // Paper - package-private -+ public final PalettedContainer states; // Paper - package-private // Paper - public - - // Paper start - Anti-Xray - Add parameters - @Deprecated public LevelChunkSection(int yOffset) { this(yOffset, null, null, true); } // Notice for updates: Please make sure this constructor isn't used anywhere -diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -@@ -0,0 +0,0 @@ public class PalettedContainer implements PaletteResize { - return this.get(y << 8 | z << 4 | x); // Paper - inline - } - -- protected T get(int index) { -+ public T get(int index) { // Paper - public - T object = this.palette.valueFor(this.storage.get(index)); - return (T)(object == null ? this.defaultValue : object); - } -diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java -@@ -0,0 +0,0 @@ - package net.minecraft.world.level.chunk; - -+import ca.spottedleaf.starlight.light.SWMRNibbleArray; -+import ca.spottedleaf.starlight.light.StarLightEngine; - import com.google.common.collect.Lists; - import com.google.common.collect.Maps; - import com.google.common.collect.Sets; -@@ -0,0 +0,0 @@ public class ProtoChunk implements ChunkAccess { - // Paper end - private static boolean PRINTED_OUTDATED_CTOR_MSG = false; // Paper - Add level - -+ // Paper start - rewrite light engine -+ protected volatile SWMRNibbleArray[] blockNibbles; -+ protected volatile SWMRNibbleArray[] skyNibbles; -+ protected volatile boolean[] skyEmptinessMap; -+ protected volatile boolean[] blockEmptinessMap; -+ -+ @Override -+ public SWMRNibbleArray[] getBlockNibbles() { -+ return this.blockNibbles; -+ } -+ -+ @Override -+ public void setBlockNibbles(SWMRNibbleArray[] nibbles) { -+ this.blockNibbles = nibbles; -+ } -+ -+ @Override -+ public SWMRNibbleArray[] getSkyNibbles() { -+ return this.skyNibbles; -+ } -+ -+ @Override -+ public void setSkyNibbles(SWMRNibbleArray[] nibbles) { -+ this.skyNibbles = nibbles; -+ } -+ -+ @Override -+ public boolean[] getSkyEmptinessMap() { -+ return this.skyEmptinessMap; -+ } -+ -+ @Override -+ public void setSkyEmptinessMap(boolean[] emptinessMap) { -+ this.skyEmptinessMap = emptinessMap; -+ } -+ -+ @Override -+ public boolean[] getBlockEmptinessMap() { -+ return this.blockEmptinessMap; -+ } -+ -+ @Override -+ public void setBlockEmptinessMap(boolean[] emptinessMap) { -+ this.blockEmptinessMap = emptinessMap; -+ } -+ // Paper end - rewrite light engine -+ - @Deprecated // Paper start - add level - public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor world) { - // Paper start -@@ -0,0 +0,0 @@ public class ProtoChunk implements ChunkAccess { - } - } - public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, @Nullable LevelChunkSection[] levelChunkSections, ProtoTickList blockTickScheduler, ProtoTickList fluidTickScheduler, LevelHeightAccessor world, net.minecraft.server.level.ServerLevel level) { -+ // Paper start -+ this.blockNibbles = StarLightEngine.getFilledEmptyLight(world); -+ this.skyNibbles = StarLightEngine.getFilledEmptyLight(world); -+ // Paper end - this.level = level; - // Paper end - this.chunkPos = pos; -@@ -0,0 +0,0 @@ public class ProtoChunk implements ChunkAccess { - - LevelChunkSection levelChunkSection = this.getOrCreateSection(l); - BlockState blockState = levelChunkSection.setBlockState(i & 15, j & 15, k & 15, state); -- if (this.status.isOrAfter(ChunkStatus.FEATURES) && state != blockState && (state.getLightBlock(this, pos) != blockState.getLightBlock(this, pos) || state.getLightEmission() != blockState.getLightEmission() || state.useShapeForLightOcclusion() || blockState.useShapeForLightOcclusion())) { -+ if (this.status.isOrAfter(ChunkStatus.LIGHT) && state != blockState && (state.getLightBlock(this, pos) != blockState.getLightBlock(this, pos) || state.getLightEmission() != blockState.getLightEmission() || state.useShapeForLightOcclusion() || blockState.useShapeForLightOcclusion())) { // Paper - move block updates to only happen after lighting occurs (or during, thanks chunk system) - this.lightEngine.checkBlock(pos); - } - -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java -@@ -0,0 +0,0 @@ import org.apache.logging.log4j.Logger; - - public class ChunkSerializer { - -+ // Paper start - replace light engine impl -+ private static final int STARLIGHT_LIGHT_VERSION = 5; -+ -+ private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state"; -+ private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; -+ private static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; -+ // Paper end - replace light engine impl -+ - private static final Logger LOGGER = LogManager.getLogger(); - public static final String TAG_UPGRADE_DATA = "UpgradeData"; - -@@ -0,0 +0,0 @@ public class ChunkSerializer { - ProtoTickList protochunkticklist1 = new ProtoTickList<>((fluidtype) -> { - return fluidtype == null || fluidtype == Fluids.EMPTY; - }, pos, nbttagcompound1.getList("LiquidsToBeTicked", 9), world); -- boolean flag = nbttagcompound1.getBoolean("isLightOn"); -+ boolean flag = getStatus(nbt).isOrAfter(ChunkStatus.LIGHT) && nbttagcompound1.get("isLightOn") != null && nbttagcompound1.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; // Paper - ListTag nbttaglist = nbttagcompound1.getList("Sections", 10); - int i = world.getSectionsCount(); - LevelChunkSection[] achunksection = new LevelChunkSection[i]; - boolean flag1 = world.dimensionType().hasSkyLight(); - ServerChunkCache chunkproviderserver = world.getChunkSource(); - LevelLightEngine lightengine = chunkproviderserver.getLightEngine(); -+ // Paper start -+ ca.spottedleaf.starlight.light.SWMRNibbleArray[] blockNibbles = ca.spottedleaf.starlight.light.StarLightEngine.getFilledEmptyLight(world); // Paper - replace light impl -+ ca.spottedleaf.starlight.light.SWMRNibbleArray[] skyNibbles = ca.spottedleaf.starlight.light.StarLightEngine.getFilledEmptyLight(world); // Paper - replace light impl -+ final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world); -+ final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world); -+ boolean canReadSky = world.dimensionType().hasSkyLight(); -+ // Paper end - - if (flag) { - tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main -@@ -0,0 +0,0 @@ public class ChunkSerializer { - } - - for (int j = 0; j < nbttaglist.size(); ++j) { -- CompoundTag nbttagcompound2 = nbttaglist.getCompound(j); -+ CompoundTag nbttagcompound2 = nbttaglist.getCompound(j); CompoundTag sectionData = nbttagcompound2; // Paper - byte b0 = nbttagcompound2.getByte("Y"); - - if (nbttagcompound2.contains("Palette", 9) && nbttagcompound2.contains("BlockStates", 12)) { -@@ -0,0 +0,0 @@ public class ChunkSerializer { - } - - if (flag) { -- if (nbttagcompound2.contains("BlockLight", 7)) { -- // Paper start - delay this task since we're executing off-main -- DataLayer blockLight = new DataLayer(nbttagcompound2.getByteArray("BlockLight")); -- tasksToExecuteOnMain.add(() -> { -- lightengine.queueSectionData(LightLayer.BLOCK, SectionPos.of(chunkcoordintpair1, b0), blockLight, true); -- }); -- // Paper end - delay this task since we're executing off-main -+ // Paper start - rewrite light engine -+ int y = sectionData.getByte("Y"); -+ -+ if (sectionData.contains("BlockLight", 7)) { -+ // this is where our diff is -+ blockNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety -+ } else { -+ blockNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG)); - } - -- if (flag1 && nbttagcompound2.contains("SkyLight", 7)) { -- // Paper start - delay this task since we're executing off-main -- DataLayer skyLight = new DataLayer(nbttagcompound2.getByteArray("SkyLight")); -- tasksToExecuteOnMain.add(() -> { -- lightengine.queueSectionData(LightLayer.SKY, SectionPos.of(chunkcoordintpair1, b0), skyLight, true); -- }); -- // Paper end - delay this task since we're executing off-main -+ 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 ca.spottedleaf.starlight.light.SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety -+ } else { -+ skyNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG)); -+ } - } -+ // Paper end - rewrite light engine - } - } - -@@ -0,0 +0,0 @@ public class ChunkSerializer { - object = new LevelChunk(world.getLevel(), pos, biomestorage, chunkconverter, (TickList) object1, (TickList) object2, k, achunksection, // Paper start - fix massive nbt memory leak due to lambda. move lambda into a container method to not leak scope. Only clone needed NBT keys. - createLoadEntitiesConsumer(new SafeNBTCopy(nbttagcompound1, "TileEntities", "Entities", "ChunkBukkitValues")) // Paper - move CB Chunk PDC into here - );// Paper end -+ ((LevelChunk)object).setBlockNibbles(blockNibbles); // Paper - replace light impl -+ ((LevelChunk)object).setSkyNibbles(skyNibbles); // Paper - replace light impl - } else { - ProtoChunk protochunk = new ProtoChunk(pos, chunkconverter, achunksection, protochunkticklist, protochunkticklist1, world, world); // Paper - add level -+ protochunk.setBlockNibbles(blockNibbles); // Paper - replace light impl -+ protochunk.setSkyNibbles(skyNibbles); // Paper - replace light impl - - protochunk.setBiomes(biomestorage); - object = protochunk; -@@ -0,0 +0,0 @@ public class ChunkSerializer { - DataLayer[] blockLight = new DataLayer[lightenginethreaded.getMaxLightSection() - lightenginethreaded.getMinLightSection()]; - DataLayer[] skyLight = new DataLayer[lightenginethreaded.getMaxLightSection() - lightenginethreaded.getMinLightSection()]; - -- for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) { -+ for (int i = lightenginethreaded.getMinLightSection(); false && i < lightenginethreaded.getMaxLightSection(); ++i) { // Paper - don't run loop, we don't need to - light data is per chunk now - DataLayer blockArray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkPos, i)); - DataLayer skyArray = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkPos, i)); - -@@ -0,0 +0,0 @@ public class ChunkSerializer { - return saveChunk(world, chunk, null); - } - public static CompoundTag saveChunk(ServerLevel world, ChunkAccess chunk, AsyncSaveData asyncsavedata) { -+ // Paper start - rewrite light impl -+ final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world); -+ final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world); -+ ca.spottedleaf.starlight.light.SWMRNibbleArray[] blockNibbles = chunk.getBlockNibbles(); -+ ca.spottedleaf.starlight.light.SWMRNibbleArray[] skyNibbles = chunk.getSkyNibbles(); -+ // Paper end - rewrite light impl - // Paper end - ChunkPos chunkcoordintpair = chunk.getPos(); - CompoundTag nbttagcompound = new CompoundTag(); -@@ -0,0 +0,0 @@ public class ChunkSerializer { - LevelChunkSection chunksection = (LevelChunkSection) Arrays.stream(achunksection).filter((chunksection1) -> { - return chunksection1 != null && SectionPos.blockToSectionCoord(chunksection1.bottomBlockY()) == finalI; // CraftBukkit - decompile errors - }).findFirst().orElse(LevelChunk.EMPTY_SECTION); -- // Paper start - async chunk save for unload -- DataLayer nibblearray; // block light -- DataLayer nibblearray1; // sky light -- if (asyncsavedata == null) { -- nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); /// Paper - diff on method change (see getAsyncSaveData) -- nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); // Paper - diff on method change (see getAsyncSaveData) -- } else { -- nibblearray = asyncsavedata.blockLight[i - lightenginethreaded.getMinLightSection()]; -- nibblearray1 = asyncsavedata.skyLight[i - lightenginethreaded.getMinLightSection()]; -- } -- // Paper end -- if (chunksection != LevelChunk.EMPTY_SECTION || nibblearray != null || nibblearray1 != null) { -- CompoundTag nbttagcompound2 = new CompoundTag(); -+ // Paper start - replace light engine -+ ca.spottedleaf.starlight.light.SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState(); -+ ca.spottedleaf.starlight.light.SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState(); -+ if (chunksection != LevelChunk.EMPTY_SECTION || blockNibble != null || skyNibble != null) { -+ // Paper end - replace light engine -+ CompoundTag nbttagcompound2 = new CompoundTag(); CompoundTag section = nbttagcompound2; // Paper - - nbttagcompound2.putByte("Y", (byte) (i & 255)); - if (chunksection != LevelChunk.EMPTY_SECTION) { - chunksection.getStates().write(nbttagcompound2, "Palette", "BlockStates"); - } - -- if (nibblearray != null && !nibblearray.isEmpty()) { -- nbttagcompound2.putByteArray("BlockLight", nibblearray.asBytesPoolSafe().clone()); // Paper -+ // Paper start - replace light engine -+ if (blockNibble != null) { -+ if (blockNibble.data != null) { -+ section.putByteArray("BlockLight", blockNibble.data); -+ } -+ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state); - } - -- if (nibblearray1 != null && !nibblearray1.isEmpty()) { -- nbttagcompound2.putByteArray("SkyLight", nibblearray1.asBytesPoolSafe().clone()); // Paper -+ if (skyNibble != null) { -+ if (skyNibble.data != null) { -+ section.putByteArray("SkyLight", skyNibble.data); -+ } -+ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state); - } -+ // Paper end - replace light engine - - nbttaglist.add(nbttagcompound2); - } -@@ -0,0 +0,0 @@ public class ChunkSerializer { - - nbttagcompound1.put("Sections", nbttaglist); - if (flag) { -- nbttagcompound1.putBoolean("isLightOn", true); -+ nbttagcompound1.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // Paper -+ nbttagcompound1.putBoolean("isLightOn", false); // Paper - set to false but still store, this allows us to detect --eraseCache (as eraseCache _removes_) - } - - ChunkBiomeContainer biomestorage = chunk.getBiomes(); diff --git a/patches/unapplied/server/Use-hash-table-for-maintaing-changed-block-set.patch b/patches/unapplied/server/Use-hash-table-for-maintaing-changed-block-set.patch deleted file mode 100644 index 07d780b8ae..0000000000 --- a/patches/unapplied/server/Use-hash-table-for-maintaing-changed-block-set.patch +++ /dev/null @@ -1,30 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 11 Mar 2021 21:17:02 -0800 -Subject: [PATCH] Use hash table for maintaing changed block set - -When a lot of block changes occur the iteration for checking can -add up a bit and cause a small performance impact. - -diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkHolder.java -+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java -@@ -0,0 +0,0 @@ import net.minecraft.world.level.lighting.LevelLightEngine; - import net.minecraft.server.MinecraftServer; - // CraftBukkit end - -+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; // Paper -+ - public class ChunkHolder { - - public static final Either UNLOADED_CHUNK = Either.right(ChunkHolder.ChunkLoadingFailure.UNLOADED); -@@ -0,0 +0,0 @@ public class ChunkHolder { - if (i < 0 || i >= this.changedBlocksPerSection.length) return; // CraftBukkit - SPIGOT-6086, SPIGOT-6296 - if (this.changedBlocksPerSection[i] == null) { - this.hasChangedSections = true; -- this.changedBlocksPerSection[i] = new ShortArraySet(); -+ this.changedBlocksPerSection[i] = new ShortOpenHashSet(); // Paper - use a set to make setting constant-time - } - - this.changedBlocksPerSection[i].add(SectionPos.sectionRelativePos(pos));