diff --git a/gradle.properties b/gradle.properties
index 3924a9eb8..a3bde17e1 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,7 +4,7 @@ version = 1.17.1-R0.1-SNAPSHOT
mcVersion = 1.17.1
packageVersion = 1_17_R1
-paperCommit = 091319d165bbc311443790aa03ff95a4ccc01780
+paperCommit = f8d6cbdfe50fb003389d1ffd2e910caa54e4a006
org.gradle.parallel = true
org.gradle.vfs.watch = false
diff --git a/patches/api/0001-Tuinity-API-Changes.patch b/patches/api/0001-Tuinity-API-Changes.patch
index 99624a52c..6af268157 100644
--- a/patches/api/0001-Tuinity-API-Changes.patch
+++ b/patches/api/0001-Tuinity-API-Changes.patch
@@ -35,3 +35,106 @@ index f05edac8cdd33daaf1d15a526be4d2ac2b08846d..8776b8368d2046dee02e927de8249030
/**
* Sends the component to the player
*
+diff --git a/src/main/java/org/bukkit/World.java b/src/main/java/org/bukkit/World.java
+index 8ae9198ba7fdb006dc420504a984627add20dbb5..4017cc64532a9a8e42c3a6492878cd96db13fcb3 100644
+--- a/src/main/java/org/bukkit/World.java
++++ b/src/main/java/org/bukkit/World.java
+@@ -3639,6 +3639,26 @@ public interface World extends PluginMessageRecipient, Metadatable, net.kyori.ad
+ * @param viewDistance view distance in [2, 32]
+ */
+ void setNoTickViewDistance(int viewDistance);
++
++ // Tuinity start - add view distances
++ /**
++ * Gets the sending view distance for this world.
++ *
++ * Sending view distance is the view distance where chunks will load in for players in this world.
++ *
++ * @return The sending view distance for this world.
++ */
++ public int getSendViewDistance();
++
++ /**
++ * Sets the sending view distance for this world.
++ *
++ * Sending view distance is the view distance where chunks will load in for players in this world.
++ *
++ * @param viewDistance view distance in [2, 32] or -1
++ */
++ public void setSendViewDistance(int viewDistance);
++ // Tuinity end - add view distances
+ // Paper end - view distance api
+
+ // Spigot start
+diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
+index 37ad0c478b83ecf63edfe62b5b2dcd81d6fe1e77..252f6220f7cb8dd4bf9c19ec4079819e5b052f44 100644
+--- a/src/main/java/org/bukkit/entity/Player.java
++++ b/src/main/java/org/bukkit/entity/Player.java
+@@ -1818,23 +1818,63 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+ * Gets the view distance for this player
+ *
+ * @return the player's view distance
+- * @deprecated This is unimplemented and will throw an exception at runtime. The {@link org.bukkit.World World}-based methods still work.
++ * // Tuinity - implemented
+ * @see org.bukkit.World#getViewDistance()
+ * @see org.bukkit.World#getNoTickViewDistance()
+ */
+- @Deprecated
++ //@Deprecated // Tuinity - implemented
+ public int getViewDistance();
+
+ /**
+ * Sets the view distance for this player
+ *
+ * @param viewDistance the player's view distance
+- * @deprecated This is unimplemented and will throw an exception at runtime. The {@link org.bukkit.World World}-based methods still work.
++ * // Tuinity - implemented
+ * @see org.bukkit.World#setViewDistance(int)
+ * @see org.bukkit.World#setNoTickViewDistance(int)
+ */
+- @Deprecated
++ //@Deprecated // Tuinity - implemented
+ public void setViewDistance(int viewDistance);
++
++ // Tuinity start - add view distances api
++ /**
++ * Gets the no-ticking view distance for this player.
++ *
++ * No-tick view distance is the view distance where chunks will load, however the chunks and their entities will not
++ * be set to tick.
++ *
++ * @return The no-tick view distance for this player.
++ */
++ public int getNoTickViewDistance();
++
++ /**
++ * Sets the no-ticking view distance for this player.
++ *
++ * No-tick view distance is the view distance where chunks will load, however the chunks and their entities will not
++ * be set to tick.
++ *
++ * @param viewDistance view distance in [2, 32] or -1
++ */
++ public void setNoTickViewDistance(int viewDistance);
++
++ /**
++ * Gets the sending view distance for this player.
++ *
++ * Sending view distance is the view distance where chunks will load in for players.
++ *
++ * @return The sending view distance for this player.
++ */
++ public int getSendViewDistance();
++
++ /**
++ * Sets the sending view distance for this player.
++ *
++ * Sending view distance is the view distance where chunks will load in for players.
++ *
++ * @param viewDistance view distance in [2, 32] or -1
++ */
++ public void setSendViewDistance(int viewDistance);
++ // Tuinity end - add view distances api
+ // Paper end
+
+ /**
diff --git a/patches/api/0009-AFK-API.patch b/patches/api/0009-AFK-API.patch
index bc5bf72d5..811aba506 100644
--- a/patches/api/0009-AFK-API.patch
+++ b/patches/api/0009-AFK-API.patch
@@ -81,10 +81,10 @@ index 0000000000000000000000000000000000000000..0c8b3e5e4ba412624357ea5662a78862
+ }
+}
diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
-index 37ad0c478b83ecf63edfe62b5b2dcd81d6fe1e77..40782217f40146e2509e7808d7b354269a56dc1c 100644
+index 252f6220f7cb8dd4bf9c19ec4079819e5b052f44..303dbf3696a7d56b2def1beb485344e81e781857 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
-@@ -2126,4 +2126,25 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+@@ -2166,4 +2166,25 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
@Override
Spigot spigot();
// Spigot end
diff --git a/patches/api/0018-Player-invulnerabilities.patch b/patches/api/0018-Player-invulnerabilities.patch
index ab0033bfb..a1ef18c02 100644
--- a/patches/api/0018-Player-invulnerabilities.patch
+++ b/patches/api/0018-Player-invulnerabilities.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] Player invulnerabilities
diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
-index 40782217f40146e2509e7808d7b354269a56dc1c..d212021efd579bf5a527b6ef923279b055eb7754 100644
+index 303dbf3696a7d56b2def1beb485344e81e781857..de106cc1a3352948763b2106bb0a0dd8c40ce619 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
-@@ -2146,5 +2146,26 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+@@ -2186,5 +2186,26 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
* Reset the idle timer back to 0
*/
void resetIdleTimer();
diff --git a/patches/api/0032-Fix-javadoc-warnings-missing-param-and-return.patch b/patches/api/0032-Fix-javadoc-warnings-missing-param-and-return.patch
index 67bd8d6f8..49ef7f46b 100644
--- a/patches/api/0032-Fix-javadoc-warnings-missing-param-and-return.patch
+++ b/patches/api/0032-Fix-javadoc-warnings-missing-param-and-return.patch
@@ -949,10 +949,10 @@ index a6a7429ed2e1eefb2b12b7480ed74fcc3963a864..e8027e1d505dda6effbb1698550016e8
NORMAL(false),
diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
-index d212021efd579bf5a527b6ef923279b055eb7754..bc03bbf9fa61b98bc6c208ab4a0e653f4b0ea472 100644
+index de106cc1a3352948763b2106bb0a0dd8c40ce619..340cc1a5cf22e26617bc60d39902e585494ab1d5 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
-@@ -1948,6 +1948,8 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+@@ -1988,6 +1988,8 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
void resetCooldown();
/**
@@ -961,7 +961,7 @@ index d212021efd579bf5a527b6ef923279b055eb7754..bc03bbf9fa61b98bc6c208ab4a0e653f
* @return the client option value of the player
*/
@NotNull
-@@ -1987,6 +1989,9 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+@@ -2027,6 +2029,9 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
// Paper end
// Spigot start
diff --git a/patches/api/0042-Flying-Fall-Damage-API.patch b/patches/api/0042-Flying-Fall-Damage-API.patch
index e7d50ea97..c9d381f6e 100644
--- a/patches/api/0042-Flying-Fall-Damage-API.patch
+++ b/patches/api/0042-Flying-Fall-Damage-API.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] Flying Fall Damage API
diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
-index 5bb39f78a7e87dddc38b1a641438ebcc2de945b7..87d036d914fc2ca9b3056d7c5103d2e1ceb6e51e 100644
+index f6fda238033ec63bb0727824c4d51cd49723e427..407755298456cf1c7081ec8bb2fd2936f36b9404 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
-@@ -2172,5 +2172,19 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+@@ -2212,5 +2212,19 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
* @param invulnerableTicks Invulnerable ticks remaining
*/
void setSpawnInvulnerableTicks(int invulnerableTicks);
diff --git a/patches/server/0001-Tuinity-Server-Changes.patch b/patches/server/0001-Tuinity-Server-Changes.patch
index f19470a64..06e744b95 100644
--- a/patches/server/0001-Tuinity-Server-Changes.patch
+++ b/patches/server/0001-Tuinity-Server-Changes.patch
@@ -89,6 +89,19114 @@ index 5540da58e66f83b283863d3158a9b4ab5ba636db..ab8de6c4e3c0bea2b9f498da00adf88e
standardInput = System.`in`
workingDir = rootProject.layout.projectDirectory.dir(
providers.gradleProperty("runWorkDir").forUseAtConfigurationTime().orElse("run")
+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..9efbdba758aebcad3454a9a52c8a7eae4b7fc7eb
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java
+@@ -0,0 +1,283 @@
++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..174dc7ffa66258da0b867fba5c54880e81daa6ce
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/starlight/light/SWMRNibbleArray.java
+@@ -0,0 +1,439 @@
++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..143810dc53782a9a6d870089d7bd5c3006a565b3
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/starlight/light/SkyStarLightEngine.java
+@@ -0,0 +1,715 @@
++package ca.spottedleaf.starlight.light;
++
++import com.tuinity.tuinity.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..ef930395407533a2c669499c02989bbbe0ac6101
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/starlight/light/StarLightEngine.java
+@@ -0,0 +1,1573 @@
++package ca.spottedleaf.starlight.light;
++
++import com.tuinity.tuinity.util.CoordinateUtils;
++import com.tuinity.tuinity.util.IntegerUtil;
++import com.tuinity.tuinity.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);
++ }
++ }
++
++ // old algorithm for propagating
++ // this is also the basic algorithm, the optimised algorithm is always going to be tested against this one
++ // and this one is always tested against vanilla
++ // 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 4 bits: state flags
++ // state flags:
++ // whether the propagation must set the current position's light value (0 if decrease, propagated light level if increase)
++ // 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;
++ }
++ }
++
++ 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
++ 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_HAS_SIDED_TRANSPARENT_BLOCKS : 0L);
++ }
++
++ currentNibble.set(localIndex, emittedLight);
++ 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
++ 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;
++ }
++
++ currentNibble.set(localIndex, emittedLight);
++ 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
++ 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_HAS_SIDED_TRANSPARENT_BLOCKS : 0L);
++ }
++
++ currentNibble.set(localIndex, emittedLight);
++ 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
++ 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;
++ }
++
++ currentNibble.set(localIndex, emittedLight);
++ 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..300364f693583be802a71d94cda5d96c77c7b67c
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/starlight/light/StarLightInterface.java
+@@ -0,0 +1,635 @@
++package ca.spottedleaf.starlight.light;
++
++import com.tuinity.tuinity.util.CoordinateUtils;
++import com.tuinity.tuinity.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/co/aikar/timings/MinecraftTimings.java b/src/main/java/co/aikar/timings/MinecraftTimings.java
+index b9cdbf8acccfd6b207a0116f068168f3b8c8e17d..7404989c37ee1b7aa4e6999a063180d099532f7e 100644
+--- a/src/main/java/co/aikar/timings/MinecraftTimings.java
++++ b/src/main/java/co/aikar/timings/MinecraftTimings.java
+@@ -45,6 +45,8 @@ public final class MinecraftTimings {
+
+ public static final Timing antiXrayUpdateTimer = Timings.ofSafe("anti-xray - update");
+ public static final Timing antiXrayObfuscateTimer = Timings.ofSafe("anti-xray - obfuscate");
++ public static final Timing distanceManagerTick = Timings.ofSafe("Distance Manager Tick"); // Tuinity - add timings for distance manager
++ public static final Timing scoreboardScoreSearch = Timings.ofSafe("Scoreboard score search"); // Tuinity - add timings for scoreboard search
+
+ private static final Map, String> taskNameCache = new MapMaker().weakKeys().makeMap();
+
+diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java
+index 2ff4d4921e2076abf415bd3c8f5173ecd6222168..9d920565ff65a84b1b9a2a4777fd8bc8f07e0153 100644
+--- a/src/main/java/co/aikar/timings/TimingsExport.java
++++ b/src/main/java/co/aikar/timings/TimingsExport.java
+@@ -153,7 +153,7 @@ 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()) // Tuinity - replace old player chunk management
+ ));
+ }));
+
+@@ -228,7 +228,8 @@ public class TimingsExport extends Thread {
+ parent.put("config", createObject(
+ pair("spigot", mapAsJSON(Bukkit.spigot().getSpigotConfig(), null)),
+ pair("bukkit", mapAsJSON(Bukkit.spigot().getBukkitConfig(), null)),
+- pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null))
++ pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null)), // Tuinity - add config to timings report
++ pair("tuinity", mapAsJSON(Bukkit.spigot().getTuinityConfig(), null)) // Tuinity - add config to timings report
+ ));
+
+ new TimingsExport(listeners, parent, history).start();
+diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java
+index 218f5bafeed8551b55b91c7fccaf6935c8b631ca..3918b24c98faa5232c7ffd733ba8000562132785 100644
+--- a/src/main/java/com/destroystokyo/paper/Metrics.java
++++ b/src/main/java/com/destroystokyo/paper/Metrics.java
+@@ -593,7 +593,7 @@ public class Metrics {
+ boolean logFailedRequests = config.getBoolean("logFailedRequests", false);
+ // Only start Metrics, if it's enabled in the config
+ if (config.getBoolean("enabled", true)) {
+- Metrics metrics = new Metrics("Paper", serverUUID, logFailedRequests, Bukkit.getLogger());
++ Metrics metrics = new Metrics("Tuinity", serverUUID, logFailedRequests, Bukkit.getLogger()); // Tuinity - we have our own bstats page
+
+ metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> {
+ String minecraftVersion = Bukkit.getVersion();
+@@ -603,7 +603,7 @@ public class Metrics {
+
+ metrics.addCustomChart(new Metrics.SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size()));
+ metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : "offline"));
+- metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown"));
++ metrics.addCustomChart(new Metrics.SimplePie("tuinity_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown")); // Tuinity - we have our own bstats page
+
+ metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> {
+ Map> map = new HashMap<>();
+diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java
+index ba8395435482fc4d6e02fc86794cdb0d35d4399c..af66c6d863a57b2c34006b06852e9811a74d7dfa 100644
+--- a/src/main/java/com/destroystokyo/paper/PaperCommand.java
++++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java
+@@ -272,7 +272,7 @@ public class PaperCommand extends Command {
+ int ticking = 0;
+ int entityTicking = 0;
+
+- for (ChunkHolder chunk : world.getChunkSource().chunkMap.updatingChunkMap.values()) {
++ for (ChunkHolder chunk : world.getChunkSource().chunkMap.updatingChunks.getUpdatingMap().values()) { // Tuinity - change updating chunks map
+ if (chunk.getFullChunkUnchecked() == null) {
+ continue;
+ }
+@@ -496,6 +496,46 @@ public class PaperCommand extends Command {
+ }
+ }
+
++ // Tuinity 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");
++ }
++ // Tuinity end - rewrite light engine
++
+ private void doFixLight(CommandSender sender, String[] args) {
+ if (!(sender instanceof Player)) {
+ sender.sendMessage("Only players can use this command");
+@@ -504,7 +544,7 @@ 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])); // Tuinity - MOOOOOORE
+ } catch (Exception e) {
+ sender.sendMessage("Not a number");
+ return;
+@@ -517,6 +557,13 @@ public class PaperCommand extends Command {
+ ServerLevel world = (ServerLevel) handle.level;
+ ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine();
+
++ // Tuinity start - rewrite light engine
++ if (true) {
++ this.starlightFixLight(handle, world, lightengine, radius);
++ return;
++ }
++ // Tuinity 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/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
+index 580bae0d414d371a07a6bfeefc41fdd989dc0083..d50b61876f15d95b836b3dd81d9c3492c91a8448 100644
+--- a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
++++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
+@@ -29,8 +29,8 @@ public class PaperVersionFetcher implements VersionFetcher {
+ @Nonnull
+ @Override
+ public Component getVersionMessage(@Nonnull String serverVersion) {
+- String[] parts = serverVersion.substring("git-Paper-".length()).split("[-\\s]");
+- final Component updateMessage = getUpdateStatusMessage("PaperMC/Paper", GITHUB_BRANCH_NAME, parts[0]);
++ String[] parts = serverVersion.substring("git-Tuinity-".length()).split("[-\\s]"); // Tuinity
++ final Component updateMessage = getUpdateStatusMessage("Spottedleaf/Tuinity", GITHUB_BRANCH_NAME, parts[0]); // Tuinity
+ final Component history = getHistory();
+
+ return history != null ? TextComponent.ofChildren(updateMessage, Component.newline(), history) : updateMessage;
+@@ -54,13 +54,10 @@ public class PaperVersionFetcher implements VersionFetcher {
+
+ private static Component getUpdateStatusMessage(@Nonnull String repo, @Nonnull String branch, @Nonnull String versionInfo) {
+ int distance;
+- try {
+- int jenkinsBuild = Integer.parseInt(versionInfo);
+- distance = fetchDistanceFromSiteApi(jenkinsBuild, getMinecraftVersion());
+- } catch (NumberFormatException ignored) {
++ // Tuinity - we don't have jenkins setup
+ versionInfo = versionInfo.replace("\"", "");
+ distance = fetchDistanceFromGitHub(repo, branch, versionInfo);
+- }
++ // Tuinity - we don't have jenkins setup
+
+ switch (distance) {
+ case -1:
+diff --git a/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java b/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
+index da13ff17609b7bc8076d9297edf8decf01a2ed88..b4c69d39eee19339b1de295151d7ed3bf61635c1 100644
+--- a/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
++++ b/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
+@@ -312,6 +312,7 @@ public final class PaperTickList extends ServerTickList { // extend to avo
+ toTick.tickState = STATE_SCHEDULED;
+ this.addToNotTickingReady(toTick);
+ }
++ MinecraftServer.getServer().executeMidTickTasks(); // Tuinity - exec chunk tasks during world tick
+ } catch (final Throwable thr) {
+ // start copy from TickListServer // TODO check on update
+ CrashReport crashreport = CrashReport.forThrowable(thr, "Exception while ticking");
+diff --git a/src/main/java/com/tuinity/tuinity/chunk/PlayerChunkLoader.java b/src/main/java/com/tuinity/tuinity/chunk/PlayerChunkLoader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d9bfe05d4f86229ed743113bfb0bbd983adb7e68
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/chunk/PlayerChunkLoader.java
+@@ -0,0 +1,965 @@
++package com.tuinity.tuinity.chunk;
++
++import com.destroystokyo.paper.util.misc.PlayerAreaMap;
++import com.destroystokyo.paper.util.misc.PooledLinkedHashSets;
++import com.tuinity.tuinity.config.TuinityConfig;
++import com.tuinity.tuinity.util.CoordinateUtils;
++import com.tuinity.tuinity.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() {
++ double config = TuinityConfig.playerMaxConcurrentChunkSends;
++ return Math.max(1, config <= 0 ? (int)Math.ceil(-config * this.chunkMap.level.players().size()) : (int)config);
++ }
++
++ protected int getMaxChunkLoads() {
++ double config = TuinityConfig.playerMaxConcurrentChunkLoads;
++ return Math.max(1, (config <= 0 ? (int)Math.ceil(-config * MinecraftServer.getServer().getPlayerCount()) : (int)config) * 9);
++ }
++
++ protected double getTargetSendRatePerPlayer() {
++ double config = TuinityConfig.playerTargetChunkSendRate;
++ return config <= 0 ? -config : config / MinecraftServer.getServer().getPlayerCount();
++ }
++
++ 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 long chunkKey = CoordinateUtils.getChunkKey(chunkX, 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 void trySendChunks() {
++ final long time = System.nanoTime();
++ // 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 double sendRate = this.getTargetSendRatePerPlayer();
++ final long nextDeadline = (long)((1 / sendRate) * 1.0e9) + 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 = nextDeadline;
++ 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();
++ });
++ }
++ }
++
++ protected int concurrentChunkLoads;
++ private void tryLoadChunks() {
++ if (this.chunkLoadQueue.isEmpty()) {
++ return;
++ }
++
++ final int maxLoads = this.getMaxChunkLoads();
++ for (;;) {
++ final PlayerLoaderData data = this.chunkLoadQueue.pollFirst();
++
++ final ChunkPriorityHolder queuedLoad = data.loadQueue.peekFirst();
++ if (queuedLoad == null) {
++ if (this.chunkLoadQueue.isEmpty()) {
++ break;
++ }
++ continue;
++ }
++
++ 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) {
++ // 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))) {
++ // wont reach here if unloadedTargetChunk is false
++ ++this.concurrentChunkLoads;
++ }
++ }
++ }
++
++ // 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 ? (!TuinityConfig.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 ? (!TuinityConfig.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 = TuinityConfig.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 <= TuinityConfig.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 * TuinityConfig.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/com/tuinity/tuinity/chunk/SingleThreadChunkRegionManager.java b/src/main/java/com/tuinity/tuinity/chunk/SingleThreadChunkRegionManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..49dc783a312ed62415d28cdd801dad6a96f3cc16
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/chunk/SingleThreadChunkRegionManager.java
+@@ -0,0 +1,477 @@
++package com.tuinity.tuinity.chunk;
++
++import com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet;
++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
++import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
++import net.minecraft.server.MCUtil;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.level.ChunkPos;
++import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.Iterator;
++import java.util.List;
++import java.util.function.Supplier;
++
++public final class SingleThreadChunkRegionManager {
++
++ protected final int regionSectionMergeRadius;
++ protected final int regionSectionChunkSize;
++ public final int regionChunkShift; // log2(REGION_CHUNK_SIZE)
++
++ public final ServerLevel world;
++ public final String name;
++
++ protected final Long2ObjectOpenHashMap regionsBySection = new Long2ObjectOpenHashMap<>();
++ protected final ReferenceLinkedOpenHashSet needsRecalculation = new ReferenceLinkedOpenHashSet<>();
++ protected final int minSectionRecalcCount;
++ protected final double maxDeadRegionPercent;
++ protected final Supplier regionDataSupplier;
++ protected final Supplier regionSectionDataSupplier;
++
++ public SingleThreadChunkRegionManager(final ServerLevel world, final int minSectionRecalcCount,
++ final double maxDeadRegionPercent, final int sectionMergeRadius,
++ final int regionSectionChunkShift,
++ final String name, final Supplier regionDataSupplier,
++ final Supplier regionSectionDataSupplier) {
++ this.regionSectionMergeRadius = sectionMergeRadius;
++ this.regionSectionChunkSize = 1 << regionSectionChunkShift;
++ this.regionChunkShift = regionSectionChunkShift;
++ this.world = world;
++ this.name = name;
++ this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount);
++ this.maxDeadRegionPercent = maxDeadRegionPercent;
++ this.regionDataSupplier = regionDataSupplier;
++ this.regionSectionDataSupplier = regionSectionDataSupplier;
++ }
++
++ // tested via https://gist.github.com/Spottedleaf/aa7ade3451c37b4cac061fc77074db2f
++
++ /*
++ protected void check() {
++ ReferenceOpenHashSet> checked = new ReferenceOpenHashSet<>();
++
++ for (RegionSection section : this.regionsBySection.values()) {
++ if (!checked.add(section.region)) {
++ section.region.check();
++ }
++ }
++ for (Region region : this.needsRecalculation) {
++ region.check();
++ }
++ }
++ */
++
++ protected void addToRecalcQueue(final Region region) {
++ this.needsRecalculation.add(region);
++ }
++
++ protected void removeFromRecalcQueue(final Region region) {
++ this.needsRecalculation.remove(region);
++ }
++
++ public RegionSection getRegionSection(final int chunkX, final int chunkZ) {
++ return this.regionsBySection.get(MCUtil.getCoordinateKey(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift));
++ }
++
++ public Region getRegion(final int chunkX, final int chunkZ) {
++ final RegionSection section = this.regionsBySection.get(MCUtil.getCoordinateKey(chunkX >> regionChunkShift, chunkZ >> regionChunkShift));
++ return section != null ? section.region : null;
++ }
++
++ private final List toMerge = new ArrayList<>();
++
++ protected RegionSection getOrCreateAndMergeSection(final int sectionX, final int sectionZ, final RegionSection force) {
++ final long sectionKey = MCUtil.getCoordinateKey(sectionX, sectionZ);
++
++ if (force == null) {
++ RegionSection region = this.regionsBySection.get(sectionKey);
++ if (region != null) {
++ return region;
++ }
++ }
++
++ int mergeCandidateSectionSize = -1;
++ Region mergeIntoCandidate = null;
++
++ // find optimal candidate to merge into
++
++ final int minX = sectionX - this.regionSectionMergeRadius;
++ final int maxX = sectionX + this.regionSectionMergeRadius;
++ final int minZ = sectionZ - this.regionSectionMergeRadius;
++ final int maxZ = sectionZ + this.regionSectionMergeRadius;
++ for (int currX = minX; currX <= maxX; ++currX) {
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ final RegionSection section = this.regionsBySection.get(MCUtil.getCoordinateKey(currX, currZ));
++ if (section == null) {
++ continue;
++ }
++ final Region region = section.region;
++ if (region.dead) {
++ throw new IllegalStateException("Dead region should not be in live region manager state: " + region);
++ }
++ final int sections = region.sections.size();
++
++ if (sections > mergeCandidateSectionSize) {
++ mergeCandidateSectionSize = sections;
++ mergeIntoCandidate = region;
++ }
++ this.toMerge.add(region);
++ }
++ }
++
++ // merge
++ if (mergeIntoCandidate != null) {
++ for (int i = 0; i < this.toMerge.size(); ++i) {
++ final Region region = this.toMerge.get(i);
++ if (region.dead || mergeIntoCandidate == region) {
++ continue;
++ }
++ region.mergeInto(mergeIntoCandidate);
++ }
++ this.toMerge.clear();
++ } else {
++ mergeIntoCandidate = new Region(this);
++ }
++
++ final RegionSection section;
++ if (force == null) {
++ this.regionsBySection.put(sectionKey, section = new RegionSection(sectionKey, this));
++ } else {
++ final RegionSection existing = this.regionsBySection.putIfAbsent(sectionKey, force);
++ if (existing != null) {
++ throw new IllegalStateException("Attempting to override section '" + existing.toStringWithRegion() +
++ ", with " + force.toStringWithRegion());
++ }
++
++ section = force;
++ }
++
++ mergeIntoCandidate.addRegionSection(section);
++ //mergeIntoCandidate.check();
++ //this.check();
++
++ return section;
++ }
++
++ public void addChunk(final int chunkX, final int chunkZ) {
++ this.getOrCreateAndMergeSection(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift, null).addChunk(chunkX, chunkZ);
++ }
++
++ public void removeChunk(final int chunkX, final int chunkZ) {
++ final RegionSection section = this.regionsBySection.get(
++ MCUtil.getCoordinateKey(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift)
++ );
++ if (section != null) {
++ section.removeChunk(chunkX, chunkZ);
++ } else {
++ throw new IllegalStateException("Cannot remove chunk at (" + chunkX + "," + chunkZ + ") from region state, section does not exist");
++ }
++ }
++
++ public void recalculateRegions() {
++ for (int i = 0, len = this.needsRecalculation.size(); i < len; ++i) {
++ final Region region = this.needsRecalculation.removeFirst();
++
++ this.recalculateRegion(region);
++ //this.check();
++ }
++ }
++
++ protected void recalculateRegion(final Region region) {
++ region.markedForRecalc = false;
++ //region.check();
++ // clear unused regions
++ for (final Iterator iterator = region.deadSections.iterator(); iterator.hasNext();) {
++ final RegionSection deadSection = iterator.next();
++
++ if (deadSection.hasChunks()) {
++ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!");
++ }
++ if (!region.removeRegionSection(deadSection)) {
++ throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection);
++ }
++ if (!this.regionsBySection.remove(deadSection.regionCoordinate, deadSection)) {
++ throw new IllegalStateException("Cannot remove dead section '" +
++ deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " +
++ this.regionsBySection.get(deadSection.regionCoordinate));
++ }
++ }
++ region.deadSections.clear();
++
++ // implicitly cover cases where size == 0
++ if (region.sections.size() < this.minSectionRecalcCount) {
++ //region.check();
++ return;
++ }
++
++ // run a test to see if we actually need to recalculate
++ // TODO
++
++ // destroy and rebuild the region
++ region.dead = true;
++
++ // destroy region state
++ for (final Iterator iterator = region.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
++ final RegionSection aliveSection = iterator.next();
++ if (!aliveSection.hasChunks()) {
++ throw new IllegalStateException("Alive section '" + aliveSection.toStringWithRegion() + "' has no chunks!");
++ }
++ if (!this.regionsBySection.remove(aliveSection.regionCoordinate, aliveSection)) {
++ throw new IllegalStateException("Cannot remove alive section '" +
++ aliveSection.toStringWithRegion() + "' from section state! State at section coordinate: " +
++ this.regionsBySection.get(aliveSection.regionCoordinate));
++ }
++ }
++
++ // rebuild regions
++ for (final Iterator iterator = region.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
++ final RegionSection aliveSection = iterator.next();
++ this.getOrCreateAndMergeSection(aliveSection.getSectionX(), aliveSection.getSectionZ(), aliveSection);
++ }
++ }
++
++ public static final class Region {
++ protected final IteratorSafeOrderedReferenceSet sections = new IteratorSafeOrderedReferenceSet<>();
++ protected final ReferenceOpenHashSet deadSections = new ReferenceOpenHashSet<>(16, 0.7f);
++ protected boolean dead;
++ protected boolean markedForRecalc;
++
++ public final SingleThreadChunkRegionManager regionManager;
++ public final RegionData regionData;
++
++ protected Region(final SingleThreadChunkRegionManager regionManager) {
++ this.regionManager = regionManager;
++ this.regionData = regionManager.regionDataSupplier.get();
++ }
++
++ public IteratorSafeOrderedReferenceSet.Iterator getSections() {
++ return this.sections.iterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS);
++ }
++
++ protected final double getDeadSectionPercent() {
++ return (double)this.deadSections.size() / (double)this.sections.size();
++ }
++
++ /*
++ protected void check() {
++ if (this.dead) {
++ throw new IllegalStateException("Dead region!");
++ }
++ for (final Iterator> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
++ final RegionSection section = iterator.next();
++ if (section.region != this) {
++ throw new IllegalStateException("Region section must point to us!");
++ }
++ if (this.regionManager.regionsBySection.get(section.regionCoordinate) != section) {
++ throw new IllegalStateException("Region section must match the regionmanager state!");
++ }
++ }
++ }
++ */
++
++ // note: it is not true that the region at this point is not in any region. use the region field on the section
++ // to see if it is currently in another region.
++ protected final boolean addRegionSection(final RegionSection section) {
++ if (!this.sections.add(section)) {
++ return false;
++ }
++
++ section.sectionData.addToRegion(section, section.region, this);
++
++ section.region = this;
++ return true;
++ }
++
++ protected final boolean removeRegionSection(final RegionSection section) {
++ if (!this.sections.remove(section)) {
++ return false;
++ }
++
++ section.sectionData.removeFromRegion(section, this);
++
++ return true;
++ }
++
++ protected void mergeInto(final Region mergeTarget) {
++ if (this == mergeTarget) {
++ throw new IllegalStateException("Cannot merge a region onto itself");
++ }
++ if (this.dead) {
++ throw new IllegalStateException("Source region is dead! Source " + this + ", target " + mergeTarget);
++ } else if (mergeTarget.dead) {
++ throw new IllegalStateException("Target region is dead! Source " + this + ", target " + mergeTarget);
++ }
++ this.dead = true;
++ if (this.markedForRecalc) {
++ this.regionManager.removeFromRecalcQueue(this);
++ }
++
++ for (final Iterator iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
++ final RegionSection section = iterator.next();
++
++ if (!mergeTarget.addRegionSection(section)) {
++ throw new IllegalStateException("Target cannot contain source's sections! Source " + this + ", target " + mergeTarget);
++ }
++ }
++
++ for (final RegionSection deadSection : this.deadSections) {
++ if (!this.sections.contains(deadSection)) {
++ throw new IllegalStateException("Source region does not even contain its own dead sections! Missing " + deadSection + " from region " + this);
++ }
++ mergeTarget.deadSections.add(deadSection);
++ }
++ //mergeTarget.check();
++ }
++
++ protected void markSectionAlive(final RegionSection section) {
++ this.deadSections.remove(section);
++ if (this.markedForRecalc && (this.sections.size() < this.regionManager.minSectionRecalcCount || this.getDeadSectionPercent() < this.regionManager.maxDeadRegionPercent)) {
++ this.regionManager.removeFromRecalcQueue(this);
++ this.markedForRecalc = false;
++ }
++ }
++
++ protected void markSectionDead(final RegionSection section) {
++ this.deadSections.add(section);
++ if (!this.markedForRecalc && (this.sections.size() >= this.regionManager.minSectionRecalcCount || this.sections.size() == this.deadSections.size()) && this.getDeadSectionPercent() >= this.regionManager.maxDeadRegionPercent) {
++ this.regionManager.addToRecalcQueue(this);
++ this.markedForRecalc = true;
++ }
++ }
++
++ @Override
++ public String toString() {
++ final StringBuilder ret = new StringBuilder(128);
++
++ ret.append("Region{");
++ ret.append("dead=").append(this.dead).append(',');
++ ret.append("markedForRecalc=").append(this.markedForRecalc).append(',');
++
++ ret.append("sectionCount=").append(this.sections.size()).append(',');
++ ret.append("sections=[");
++ for (final Iterator iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
++ final RegionSection section = iterator.next();
++ ret.append(section);
++ if (iterator.hasNext()) {
++ ret.append(',');
++ }
++ }
++ ret.append(']');
++
++ ret.append('}');
++ return ret.toString();
++ }
++ }
++
++ public static final class RegionSection {
++ protected final long regionCoordinate;
++ protected final long[] chunksBitset;
++ protected int chunkCount;
++ protected Region region;
++
++ public final SingleThreadChunkRegionManager regionManager;
++ public final RegionSectionData sectionData;
++
++ protected RegionSection(final long regionCoordinate, final SingleThreadChunkRegionManager regionManager) {
++ this.regionCoordinate = regionCoordinate;
++ this.regionManager = regionManager;
++ this.chunksBitset = new long[Math.max(1, regionManager.regionSectionChunkSize * regionManager.regionSectionChunkSize / Long.SIZE)];
++ this.sectionData = regionManager.regionSectionDataSupplier.get();
++ }
++
++ public int getSectionX() {
++ return MCUtil.getCoordinateX(this.regionCoordinate);
++ }
++
++ public int getSectionZ() {
++ return MCUtil.getCoordinateZ(this.regionCoordinate);
++ }
++
++ public Region getRegion() {
++ return this.region;
++ }
++
++ private int getChunkIndex(final int chunkX, final int chunkZ) {
++ return (chunkX & (this.regionManager.regionSectionChunkSize - 1)) | ((chunkZ & (this.regionManager.regionSectionChunkSize - 1)) << this.regionManager.regionChunkShift);
++ }
++
++ protected boolean hasChunks() {
++ return this.chunkCount != 0;
++ }
++
++ protected void addChunk(final int chunkX, final int chunkZ) {
++ final int index = this.getChunkIndex(chunkX, chunkZ);
++ final long bitset = this.chunksBitset[index >>> 6]; // index / Long.SIZE
++ final long after = this.chunksBitset[index >>> 6] = bitset | (1L << (index & (Long.SIZE - 1)));
++ if (after == bitset) {
++ throw new IllegalStateException("Cannot add a chunk to a section which already has the chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString());
++ }
++ if (++this.chunkCount != 1) {
++ return;
++ }
++ this.region.markSectionAlive(this);
++ }
++
++ protected void removeChunk(final int chunkX, final int chunkZ) {
++ final int index = this.getChunkIndex(chunkX, chunkZ);
++ final long before = this.chunksBitset[index >>> 6]; // index / Long.SIZE
++ final long bitset = this.chunksBitset[index >>> 6] = before & ~(1L << (index & (Long.SIZE - 1)));
++ if (before == bitset) {
++ throw new IllegalStateException("Cannot remove a chunk from a section which does not have that chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString());
++ }
++ if (--this.chunkCount != 0) {
++ return;
++ }
++ this.region.markSectionDead(this);
++ }
++
++ @Override
++ public String toString() {
++ return "RegionSection{" +
++ "regionCoordinate=" + new ChunkPos(this.regionCoordinate).toString() + "," +
++ "chunkCount=" + this.chunkCount + "," +
++ "chunksBitset=" + toString(this.chunksBitset) + "," +
++ "hash=" + this.hashCode() +
++ "}";
++ }
++
++ public String toStringWithRegion() {
++ return "RegionSection{" +
++ "regionCoordinate=" + new ChunkPos(this.regionCoordinate).toString() + "," +
++ "chunkCount=" + this.chunkCount + "," +
++ "chunksBitset=" + toString(this.chunksBitset) + "," +
++ "hash=" + this.hashCode() + "," +
++ "region=" + this.region +
++ "}";
++ }
++
++ private static String toString(final long[] array) {
++ final StringBuilder ret = new StringBuilder();
++ for (final long value : array) {
++ // zero pad the hex string
++ final char[] zeros = new char[Long.SIZE / 4];
++ Arrays.fill(zeros, '0');
++ final String string = Long.toHexString(value);
++ System.arraycopy(string.toCharArray(), 0, zeros, zeros.length - string.length(), string.length());
++
++ ret.append(zeros);
++ }
++
++ return ret.toString();
++ }
++ }
++
++ public static interface RegionData {
++
++ }
++
++ public static interface RegionSectionData {
++
++ public void removeFromRegion(final RegionSection section, final Region from);
++
++ // removal from the old region is handled via removeFromRegion
++ public void addToRegion(final RegionSection section, final Region oldRegion, final Region newRegion);
++
++ }
++}
+diff --git a/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java b/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..356a6118f1b0b091f7527aec747659025562eafc
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java
+@@ -0,0 +1,432 @@
++package com.tuinity.tuinity.config;
++
++import com.destroystokyo.paper.util.SneakyThrow;
++import net.minecraft.server.MinecraftServer;
++import org.bukkit.Bukkit;
++import org.bukkit.configuration.ConfigurationSection;
++import org.bukkit.configuration.file.YamlConfiguration;
++import java.io.File;
++import java.lang.reflect.Method;
++import java.lang.reflect.Modifier;
++import java.util.List;
++import java.util.logging.Level;
++
++public final class TuinityConfig {
++
++ public static final String CONFIG_HEADER = "Configuration file for Tuinity.";
++ public static final int CURRENT_CONFIG_VERSION = 2;
++
++ private static final Object[] EMPTY = new Object[0];
++
++ private static File configFile;
++ public static YamlConfiguration config;
++ private static int configVersion;
++ public static boolean createWorldSections = true;
++
++ public static void init(final File file) {
++ // TODO remove this in the future...
++ final File tuinityConfig = new File(file.getParent(), "tuinity.yml");
++ if (!tuinityConfig.exists()) {
++ final File oldConfig = new File(file.getParent(), "concrete.yml");
++ oldConfig.renameTo(tuinityConfig);
++ }
++ TuinityConfig.configFile = file;
++ final YamlConfiguration config = new YamlConfiguration();
++ config.options().header(CONFIG_HEADER);
++ config.options().copyDefaults(true);
++
++ if (!file.exists()) {
++ try {
++ file.createNewFile();
++ } catch (final Exception ex) {
++ Bukkit.getLogger().log(Level.SEVERE, "Failure to create tuinity config", ex);
++ }
++ } else {
++ try {
++ config.load(file);
++ } catch (final Exception ex) {
++ Bukkit.getLogger().log(Level.SEVERE, "Failure to load tuinity config", ex);
++ SneakyThrow.sneaky(ex); /* Rethrow, this is critical */
++ throw new RuntimeException(ex); // unreachable
++ }
++ }
++
++ TuinityConfig.load(config);
++ }
++
++ public static void load(final YamlConfiguration config) {
++ TuinityConfig.config = config;
++ TuinityConfig.configVersion = TuinityConfig.getInt("config-version-please-do-not-modify-me", CURRENT_CONFIG_VERSION);
++ TuinityConfig.set("config-version-please-do-not-modify-me", CURRENT_CONFIG_VERSION);
++
++ for (final Method method : TuinityConfig.class.getDeclaredMethods()) {
++ if (method.getReturnType() != void.class || method.getParameterCount() != 0 ||
++ !Modifier.isPrivate(method.getModifiers()) || !Modifier.isStatic(method.getModifiers())) {
++ continue;
++ }
++
++ try {
++ method.setAccessible(true);
++ method.invoke(null, EMPTY);
++ } catch (final Exception ex) {
++ SneakyThrow.sneaky(ex); /* Rethrow, this is critical */
++ throw new RuntimeException(ex); // unreachable
++ }
++ }
++
++ /* We re-save to add new options */
++ try {
++ config.save(TuinityConfig.configFile);
++ } catch (final Exception ex) {
++ Bukkit.getLogger().log(Level.SEVERE, "Unable to save tuinity config", ex);
++ }
++ }
++
++ static void set(final String path, final Object value) {
++ TuinityConfig.config.set(path, value);
++ }
++
++ static boolean getBoolean(final String path, final boolean dfl) {
++ TuinityConfig.config.addDefault(path, Boolean.valueOf(dfl));
++ return TuinityConfig.config.getBoolean(path, dfl);
++ }
++
++ static int getInt(final String path, final int dfl) {
++ TuinityConfig.config.addDefault(path, Integer.valueOf(dfl));
++ return TuinityConfig.config.getInt(path, dfl);
++ }
++
++ static long getLong(final String path, final long dfl) {
++ TuinityConfig.config.addDefault(path, Long.valueOf(dfl));
++ return TuinityConfig.config.getLong(path, dfl);
++ }
++
++ static double getDouble(final String path, final double dfl) {
++ TuinityConfig.config.addDefault(path, Double.valueOf(dfl));
++ return TuinityConfig.config.getDouble(path, dfl);
++ }
++
++ static String getString(final String path, final String dfl) {
++ TuinityConfig.config.addDefault(path, dfl);
++ return TuinityConfig.config.getString(path, dfl);
++ }
++
++ public static int playerMinChunkLoadRadius;
++ public static double playerMaxConcurrentChunkSends;
++ public static double playerMaxConcurrentChunkLoads;
++ public static boolean playerAutoConfigureSendViewDistance;
++ public static boolean enableMC162253Workaround;
++ public static double playerTargetChunkSendRate;
++ public static boolean playerFrustumPrioritisation;
++
++ private static void newPlayerChunkManagement() {
++ playerMinChunkLoadRadius = TuinityConfig.getInt("player-chunks.min-load-radius", 2);
++ playerMaxConcurrentChunkSends = TuinityConfig.getDouble("player-chunks.max-concurrent-sends", 5.0);
++ playerMaxConcurrentChunkLoads = TuinityConfig.getDouble("player-chunks.max-concurrent-loads", -6.0);
++ playerAutoConfigureSendViewDistance = TuinityConfig.getBoolean("player-chunks.autoconfig-send-distance", true);
++ // this costs server bandwidth. latest phosphor or starlight on the client fixes mc162253 anyways.
++ enableMC162253Workaround = TuinityConfig.getBoolean("player-chunks.enable-mc162253-workaround", true);
++ playerTargetChunkSendRate = TuinityConfig.getDouble("player-chunks.target-chunk-send-rate", -35.0);
++ playerFrustumPrioritisation = TuinityConfig.getBoolean("player-chunks.enable-frustum-priority", false);
++ }
++
++ public static final class PacketLimit {
++ public final double packetLimitInterval;
++ public final double maxPacketRate;
++ public final ViolateAction violateAction;
++
++ public PacketLimit(final double packetLimitInterval, final double maxPacketRate, final ViolateAction violateAction) {
++ this.packetLimitInterval = packetLimitInterval;
++ this.maxPacketRate = maxPacketRate;
++ this.violateAction = violateAction;
++ }
++
++ public static enum ViolateAction {
++ KICK, DROP;
++ }
++ }
++
++ public static String kickMessage;
++ public static PacketLimit allPacketsLimit;
++ public static java.util.Map>, PacketLimit> packetSpecificLimits = new java.util.HashMap<>();
++
++ private static void packetLimiter() {
++ packetSpecificLimits.clear();
++ kickMessage = org.bukkit.ChatColor.translateAlternateColorCodes('&', TuinityConfig.getString("packet-limiter.kick-message", "&cSent too many packets"));
++ allPacketsLimit = new PacketLimit(
++ TuinityConfig.getDouble("packet-limiter.limits.all.interval", 7.0),
++ TuinityConfig.getDouble("packet-limiter.limits.all.max-packet-rate", 500.0),
++ PacketLimit.ViolateAction.KICK
++ );
++ if (allPacketsLimit.maxPacketRate <= 0.0 || allPacketsLimit.packetLimitInterval <= 0.0) {
++ allPacketsLimit = null;
++ }
++ final ConfigurationSection section = TuinityConfig.config.getConfigurationSection("packet-limiter.limits");
++
++ // add default packets
++
++ // auto recipe limiting
++ TuinityConfig.getDouble("packet-limiter.limits." +
++ "PacketPlayInAutoRecipe" + ".interval", 4.0);
++ TuinityConfig.getDouble("packet-limiter.limits." +
++ "PacketPlayInAutoRecipe" + ".max-packet-rate", 5.0);
++ TuinityConfig.getString("packet-limiter.limits." +
++ "PacketPlayInAutoRecipe" + ".action", PacketLimit.ViolateAction.DROP.name());
++
++ final String canonicalName = MinecraftServer.class.getCanonicalName();
++ final String nmsPackage = canonicalName.substring(0, canonicalName.lastIndexOf("."));
++ for (final String packetClassName : section.getKeys(false)) {
++ if (packetClassName.equals("all")) {
++ continue;
++ }
++ Class> packetClazz = null;
++
++ try {
++ packetClazz = Class.forName(nmsPackage + "." + packetClassName);
++ } catch (final ClassNotFoundException ex) {
++ for (final String subpackage : java.util.Arrays.asList("game", "handshake", "login", "status")) {
++ try {
++ packetClazz = Class.forName("net.minecraft.network.protocol." + subpackage + "." + packetClassName);
++ } catch (final ClassNotFoundException ignore) {}
++ }
++ if (packetClazz == null) {
++ MinecraftServer.LOGGER.warn("Packet '" + packetClassName + "' does not exist, cannot limit it! Please update tuinity.yml");
++ continue;
++ }
++ }
++
++ if (!net.minecraft.network.protocol.Packet.class.isAssignableFrom(packetClazz)) {
++ MinecraftServer.LOGGER.warn("Packet '" + packetClassName + "' does not exist, cannot limit it! Please update tuinity.yml");
++ continue;
++ }
++
++ if (!(section.get(packetClassName.concat(".interval")) instanceof Number) || !(section.get(packetClassName.concat(".max-packet-rate")) instanceof Number)) {
++ throw new RuntimeException("Packet limit setting " + packetClassName + " is missing interval or max-packet-rate!");
++ }
++
++ final String actionString = section.getString(packetClassName.concat(".action"), "KICK");
++ PacketLimit.ViolateAction action = PacketLimit.ViolateAction.KICK;
++ for (PacketLimit.ViolateAction test : PacketLimit.ViolateAction.values()) {
++ if (actionString.equalsIgnoreCase(test.name())) {
++ action = test;
++ break;
++ }
++ }
++
++ final double interval = section.getDouble(packetClassName.concat(".interval"));
++ final double rate = section.getDouble(packetClassName.concat(".max-packet-rate"));
++
++ if (interval > 0.0 && rate > 0.0) {
++ packetSpecificLimits.put((Class)packetClazz, new PacketLimit(interval, rate, action));
++ }
++ }
++ }
++
++ public static boolean lagCompensateBlockBreaking;
++
++ private static void lagCompensateBlockBreaking() {
++ lagCompensateBlockBreaking = TuinityConfig.getBoolean("lag-compensate-block-breaking", true);
++ }
++
++ public static boolean sendFullPosForHardCollidingEntities;
++
++ private static void sendFullPosForHardCollidingEntities() {
++ sendFullPosForHardCollidingEntities = TuinityConfig.getBoolean("send-full-pos-for-hard-colliding-entities", true);
++ }
++
++ public static final class WorldConfig {
++
++ public final String worldName;
++ public String configPath;
++ ConfigurationSection worldDefaults;
++
++ public WorldConfig(final String worldName) {
++ this.worldName = worldName;
++ this.init();
++ }
++
++ public void init() {
++ this.worldDefaults = TuinityConfig.config.getConfigurationSection("world-settings.default");
++ if (this.worldDefaults == null) {
++ this.worldDefaults = TuinityConfig.config.createSection("world-settings.default");
++ }
++
++ String worldSectionPath = TuinityConfig.configVersion < 1 ? this.worldName : "world-settings.".concat(this.worldName);
++ ConfigurationSection section = TuinityConfig.config.getConfigurationSection(worldSectionPath);
++ this.configPath = worldSectionPath;
++ if (TuinityConfig.createWorldSections) {
++ if (section == null) {
++ section = TuinityConfig.config.createSection(worldSectionPath);
++ }
++ TuinityConfig.config.set(worldSectionPath, section);
++ }
++
++ this.load();
++ }
++
++ public void load() {
++ for (final Method method : TuinityConfig.WorldConfig.class.getDeclaredMethods()) {
++ if (method.getReturnType() != void.class || method.getParameterCount() != 0 ||
++ !Modifier.isPrivate(method.getModifiers()) || Modifier.isStatic(method.getModifiers())) {
++ continue;
++ }
++
++ try {
++ method.setAccessible(true);
++ method.invoke(this, EMPTY);
++ } catch (final Exception ex) {
++ SneakyThrow.sneaky(ex); /* Rethrow, this is critical */
++ throw new RuntimeException(ex); // unreachable
++ }
++ }
++
++ if (TuinityConfig.configVersion < 1) {
++ ConfigurationSection oldSection = TuinityConfig.config.getConfigurationSection(this.worldName);
++ TuinityConfig.config.set("world-settings.".concat(this.worldName), oldSection);
++ TuinityConfig.config.set(this.worldName, null);
++ }
++
++ /* We re-save to add new options */
++ try {
++ TuinityConfig.config.save(TuinityConfig.configFile);
++ } catch (final Exception ex) {
++ Bukkit.getLogger().log(Level.SEVERE, "Unable to save tuinity config", ex);
++ }
++ }
++
++ /**
++ * update world defaults for the specified path, but also sets this world's config value for the path
++ * if it exists
++ */
++ void set(final String path, final Object val) {
++ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
++ this.worldDefaults.set(path, val);
++ if (config != null && config.get(path) != null) {
++ config.set(path, val);
++ }
++ }
++
++ boolean getBoolean(final String path, final boolean dfl) {
++ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
++ this.worldDefaults.addDefault(path, Boolean.valueOf(dfl));
++ if (TuinityConfig.configVersion < 1) {
++ if (config != null && config.getBoolean(path) == dfl) {
++ config.set(path, null);
++ }
++ }
++ return config == null ? this.worldDefaults.getBoolean(path) : config.getBoolean(path, this.worldDefaults.getBoolean(path));
++ }
++
++ boolean getBooleanRaw(final String path, final boolean dfl) {
++ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
++ if (TuinityConfig.configVersion < 1) {
++ if (config != null && config.getBoolean(path) == dfl) {
++ config.set(path, null);
++ }
++ }
++ return config == null ? this.worldDefaults.getBoolean(path, dfl) : config.getBoolean(path, this.worldDefaults.getBoolean(path, dfl));
++ }
++
++ int getInt(final String path, final int dfl) {
++ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
++ this.worldDefaults.addDefault(path, Integer.valueOf(dfl));
++ if (TuinityConfig.configVersion < 1) {
++ if (config != null && config.getInt(path) == dfl) {
++ config.set(path, null);
++ }
++ }
++ return config == null ? this.worldDefaults.getInt(path) : config.getInt(path, this.worldDefaults.getInt(path));
++ }
++
++ int getIntRaw(final String path, final int dfl) {
++ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
++ if (TuinityConfig.configVersion < 1) {
++ if (config != null && config.getInt(path) == dfl) {
++ config.set(path, null);
++ }
++ }
++ return config == null ? this.worldDefaults.getInt(path, dfl) : config.getInt(path, this.worldDefaults.getInt(path, dfl));
++ }
++
++ long getLong(final String path, final long dfl) {
++ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
++ this.worldDefaults.addDefault(path, Long.valueOf(dfl));
++ if (TuinityConfig.configVersion < 1) {
++ if (config != null && config.getLong(path) == dfl) {
++ config.set(path, null);
++ }
++ }
++ return config == null ? this.worldDefaults.getLong(path) : config.getLong(path, this.worldDefaults.getLong(path));
++ }
++
++ long getLongRaw(final String path, final long dfl) {
++ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
++ if (TuinityConfig.configVersion < 1) {
++ if (config != null && config.getLong(path) == dfl) {
++ config.set(path, null);
++ }
++ }
++ return config == null ? this.worldDefaults.getLong(path, dfl) : config.getLong(path, this.worldDefaults.getLong(path, dfl));
++ }
++
++ double getDouble(final String path, final double dfl) {
++ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
++ this.worldDefaults.addDefault(path, Double.valueOf(dfl));
++ if (TuinityConfig.configVersion < 1) {
++ if (config != null && config.getDouble(path) == dfl) {
++ config.set(path, null);
++ }
++ }
++ return config == null ? this.worldDefaults.getDouble(path) : config.getDouble(path, this.worldDefaults.getDouble(path));
++ }
++
++ double getDoubleRaw(final String path, final double dfl) {
++ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
++ if (TuinityConfig.configVersion < 1) {
++ if (config != null && config.getDouble(path) == dfl) {
++ config.set(path, null);
++ }
++ }
++ return config == null ? this.worldDefaults.getDouble(path, dfl) : config.getDouble(path, this.worldDefaults.getDouble(path, dfl));
++ }
++
++ String getString(final String path, final String dfl) {
++ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
++ this.worldDefaults.addDefault(path, dfl);
++ return config == null ? this.worldDefaults.getString(path) : config.getString(path, this.worldDefaults.getString(path));
++ }
++
++ String getStringRaw(final String path, final String dfl) {
++ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
++ return config == null ? this.worldDefaults.getString(path, dfl) : config.getString(path, this.worldDefaults.getString(path, dfl));
++ }
++
++ List getList(final String path, final List dfl) {
++ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
++ this.worldDefaults.addDefault(path, dfl);
++ return config == null ? this.worldDefaults.getList(path) : config.getList(path, this.worldDefaults.getList(path));
++ }
++
++ List getListRaw(final String path, final List dfl) {
++ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
++ return config == null ? this.worldDefaults.getList(path, dfl) : config.getList(path, this.worldDefaults.getList(path, dfl));
++ }
++
++ public int spawnLimitMonsters;
++ public int spawnLimitAnimals;
++ public int spawnLimitWaterAmbient;
++ public int spawnLimitWaterAnimals;
++ public int spawnLimitAmbient;
++
++ private void perWorldSpawnLimit() {
++ final String path = "spawn-limits";
++
++ this.spawnLimitMonsters = this.getIntRaw(path + ".monsters", -1);
++ this.spawnLimitAnimals = this.getIntRaw(path + ".animals", -1);
++ this.spawnLimitWaterAmbient = this.getIntRaw(path + ".water-ambient", -1);
++ this.spawnLimitWaterAnimals = this.getIntRaw(path + ".water-animals", -1);
++ this.spawnLimitAmbient = this.getIntRaw(path + ".ambient", -1);
++ }
++ }
++
++}
+diff --git a/src/main/java/com/tuinity/tuinity/util/CachedLists.java b/src/main/java/com/tuinity/tuinity/util/CachedLists.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..01320aea07b51c97ae5f0654b81d2332f545d42e
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/util/CachedLists.java
+@@ -0,0 +1,57 @@
++package com.tuinity.tuinity.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 {
++
++ // Tuinity 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;
++ }
++ // Tuinity end - optimise collisions
++
++ public static void reset() {
++ // Tuinity start - optimise collisions
++ TEMP_COLLISION_LIST.completeReset();
++ TEMP_GET_ENTITIES_LIST.completeReset();
++ // Tuinity end - optimise collisions
++ }
++}
+diff --git a/src/main/java/com/tuinity/tuinity/util/CollisionUtil.java b/src/main/java/com/tuinity/tuinity/util/CollisionUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a2f52a68420a2d23d8f54f61f7aeede41567dc0f
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/util/CollisionUtil.java
+@@ -0,0 +1,645 @@
++package com.tuinity.tuinity.util;
++
++import com.tuinity.tuinity.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/com/tuinity/tuinity/util/CoordinateUtils.java b/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f84060952c947d79bf2dffc61c96a300e8d7fac2
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java
+@@ -0,0 +1,128 @@
++package com.tuinity.tuinity.util;
++
++import net.minecraft.core.BlockPos;
++import net.minecraft.core.SectionPos;
++import net.minecraft.util.Mth;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.ChunkPos;
++
++public final class CoordinateUtils {
++
++ // dx, dz are relative to the target chunk
++ // dx, dz in [-radius, radius]
++ public static int getNeighbourMappedIndex(final int dx, final int dz, final int radius) {
++ return (dx + radius) + (2 * radius + 1)*(dz + radius);
++ }
++
++ // the chunk keys are compatible with vanilla
++
++ public static long getChunkKey(final BlockPos pos) {
++ return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL);
++ }
++
++ public static long getChunkKey(final Entity entity) {
++ return ((Mth.lfloor(entity.getZ()) >> 4) << 32) | ((Mth.lfloor(entity.getX()) >> 4) & 0xFFFFFFFFL);
++ }
++
++ public static long getChunkKey(final ChunkPos pos) {
++ return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL);
++ }
++
++ public static long getChunkKey(final SectionPos pos) {
++ return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL);
++ }
++
++ public static long getChunkKey(final int x, final int z) {
++ return ((long)z << 32) | (x & 0xFFFFFFFFL);
++ }
++
++ public static int getChunkX(final long chunkKey) {
++ return (int)chunkKey;
++ }
++
++ public static int getChunkZ(final long chunkKey) {
++ return (int)(chunkKey >>> 32);
++ }
++
++ public static int getChunkCoordinate(final double blockCoordinate) {
++ return Mth.floor(blockCoordinate) >> 4;
++ }
++
++ // the section keys are compatible with vanilla's
++
++ static final int SECTION_X_BITS = 22;
++ static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1;
++ static final int SECTION_Y_BITS = 20;
++ static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1;
++ static final int SECTION_Z_BITS = 22;
++ static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1;
++ // format is y,z,x (in order of LSB to MSB)
++ static final int SECTION_Y_SHIFT = 0;
++ static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS;
++ static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS;
++ static final int SECTION_TO_BLOCK_SHIFT = 4;
++
++ public static long getChunkSectionKey(final int x, final int y, final int z) {
++ return ((x & SECTION_X_MASK) << SECTION_X_SHIFT)
++ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
++ | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
++ }
++
++ public static long getChunkSectionKey(final SectionPos pos) {
++ return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT)
++ | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT)
++ | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT);
++ }
++
++ public static long getChunkSectionKey(final ChunkPos pos, final int y) {
++ return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT)
++ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
++ | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
++ }
++
++ public static long getChunkSectionKey(final BlockPos pos) {
++ return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
++ ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
++ (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
++ }
++
++ public static long getChunkSectionKey(final Entity entity) {
++ return ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
++ ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
++ ((Mth.lfloor(entity.getZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
++ }
++
++ public static int getChunkSectionX(final long key) {
++ return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS));
++ }
++
++ public static int getChunkSectionY(final long key) {
++ return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS));
++ }
++
++ public static int getChunkSectionZ(final long key) {
++ return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS));
++ }
++
++ // the block coordinates are not necessarily compatible with vanilla's
++
++ public static int getBlockCoordinate(final double blockCoordinate) {
++ return Mth.floor(blockCoordinate);
++ }
++
++ public static long getBlockKey(final int x, final int y, final int z) {
++ return ((long)x & 0x7FFFFFF) | (((long)z & 0x7FFFFFF) << 27) | ((long)y << 54);
++ }
++
++ public static long getBlockKey(final BlockPos pos) {
++ return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54);
++ }
++
++ public static long getBlockKey(final Entity entity) {
++ return ((long)entity.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54);
++ }
++
++ private CoordinateUtils() {
++ throw new RuntimeException();
++ }
++}
+diff --git a/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java b/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..695444a510e616180734f5fd284f1a00a2d73ea6
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java
+@@ -0,0 +1,226 @@
++package com.tuinity.tuinity.util;
++
++public final class IntegerUtil {
++
++ public static final int HIGH_BIT_U32 = Integer.MIN_VALUE;
++ public static final long HIGH_BIT_U64 = Long.MIN_VALUE;
++
++ public static int ceilLog2(final int value) {
++ return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
++ }
++
++ public static long ceilLog2(final long value) {
++ return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
++ }
++
++ public static int floorLog2(final int value) {
++ // xor is optimized subtract for 2^n -1
++ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
++ return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
++ }
++
++ public static int floorLog2(final long value) {
++ // xor is optimized subtract for 2^n -1
++ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
++ return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
++ }
++
++ public static int roundCeilLog2(final int value) {
++ // optimized variant of 1 << (32 - leading(val - 1))
++ // given
++ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
++ // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
++ // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
++ // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1))
++ // HIGH_BIT_32 >>> (-1 + leading(val - 1))
++ return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1);
++ }
++
++ public static long roundCeilLog2(final long value) {
++ // see logic documented above
++ return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1);
++ }
++
++ public static int roundFloorLog2(final int value) {
++ // optimized variant of 1 << (31 - leading(val))
++ // given
++ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
++ // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val)))
++ // HIGH_BIT_32 >> (31 - (31 - leading(val)))
++ // HIGH_BIT_32 >> (31 - 31 + leading(val))
++ return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value);
++ }
++
++ public static long roundFloorLog2(final long value) {
++ // see logic documented above
++ return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value);
++ }
++
++ public static boolean isPowerOfTwo(final int n) {
++ // 2^n has one bit
++ // note: this rets true for 0 still
++ return IntegerUtil.getTrailingBit(n) == n;
++ }
++
++ public static boolean isPowerOfTwo(final long n) {
++ // 2^n has one bit
++ // note: this rets true for 0 still
++ return IntegerUtil.getTrailingBit(n) == n;
++ }
++
++ public static int getTrailingBit(final int n) {
++ return -n & n;
++ }
++
++ public static long getTrailingBit(final long n) {
++ return -n & n;
++ }
++
++ public static int trailingZeros(final int n) {
++ return Integer.numberOfTrailingZeros(n);
++ }
++
++ public static int trailingZeros(final long n) {
++ return Long.numberOfTrailingZeros(n);
++ }
++
++ // from hacker's delight (signed division magic value)
++ public static int getDivisorMultiple(final long numbers) {
++ return (int)(numbers >>> 32);
++ }
++
++ // from hacker's delight (signed division magic value)
++ public static int getDivisorShift(final long numbers) {
++ return (int)numbers;
++ }
++
++ // copied from hacker's delight (signed division magic value)
++ // http://www.hackersdelight.org/hdcodetxt/magic.c.txt
++ public static long getDivisorNumbers(final int d) {
++ final int ad = IntegerUtil.branchlessAbs(d);
++
++ if (ad < 2) {
++ throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d);
++ }
++
++ final int two31 = 0x80000000;
++ final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour
++
++ int p = 31;
++
++ // all these variables are UNSIGNED!
++ int t = two31 + (d >>> 31);
++ int anc = t - 1 - t%ad;
++ int q1 = (int)((two31 & mask)/(anc & mask));
++ int r1 = two31 - q1*anc;
++ int q2 = (int)((two31 & mask)/(ad & mask));
++ int r2 = two31 - q2*ad;
++ int delta;
++
++ do {
++ p = p + 1;
++ q1 = 2*q1; // Update q1 = 2**p/|nc|.
++ r1 = 2*r1; // Update r1 = rem(2**p, |nc|).
++ if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here)
++ q1 = q1 + 1;
++ r1 = r1 - anc;
++ }
++ q2 = 2*q2; // Update q2 = 2**p/|d|.
++ r2 = 2*r2; // Update r2 = rem(2**p, |d|).
++ if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here)
++ q2 = q2 + 1;
++ r2 = r2 - ad;
++ }
++ delta = ad - r2;
++ } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0));
++
++ int magicNum = q2 + 1;
++ if (d < 0) {
++ magicNum = -magicNum;
++ }
++ int shift = p - 32;
++ return ((long)magicNum << 32) | shift;
++ }
++
++ public static int branchlessAbs(final int val) {
++ // -n = -1 ^ n + 1
++ final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0
++ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
++ }
++
++ public static long branchlessAbs(final long val) {
++ // -n = -1 ^ n + 1
++ final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0
++ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
++ }
++
++ //https://github.com/skeeto/hash-prospector for hash functions
++
++ //score = ~590.47984224483832
++ public static int hash0(int x) {
++ x *= 0x36935555;
++ x ^= x >>> 16;
++ return x;
++ }
++
++ //score = ~310.01596637036749
++ public static int hash1(int x) {
++ x ^= x >>> 15;
++ x *= 0x356aaaad;
++ x ^= x >>> 17;
++ return x;
++ }
++
++ public static int hash2(int x) {
++ x ^= x >>> 16;
++ x *= 0x7feb352d;
++ x ^= x >>> 15;
++ x *= 0x846ca68b;
++ x ^= x >>> 16;
++ return x;
++ }
++
++ public static int hash3(int x) {
++ x ^= x >>> 17;
++ x *= 0xed5ad4bb;
++ x ^= x >>> 11;
++ x *= 0xac4c1b51;
++ x ^= x >>> 15;
++ x *= 0x31848bab;
++ x ^= x >>> 14;
++ return x;
++ }
++
++ //score = ~365.79959673201887
++ public static long hash1(long x) {
++ x ^= x >>> 27;
++ x *= 0xb24924b71d2d354bL;
++ x ^= x >>> 28;
++ return x;
++ }
++
++ //h2 hash
++ public static long hash2(long x) {
++ x ^= x >>> 32;
++ x *= 0xd6e8feb86659fd93L;
++ x ^= x >>> 32;
++ x *= 0xd6e8feb86659fd93L;
++ x ^= x >>> 32;
++ return x;
++ }
++
++ public static long hash3(long x) {
++ x ^= x >>> 45;
++ x *= 0xc161abe5704b6c79L;
++ x ^= x >>> 41;
++ x *= 0xe3e5389aedbc90f7L;
++ x ^= x >>> 56;
++ x *= 0x1f9aba75a52db073L;
++ x ^= x >>> 53;
++ return x;
++ }
++
++ private IntegerUtil() {
++ throw new RuntimeException();
++ }
++}
+diff --git a/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java b/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d2c7d2c7920324d7207225ed19484e804368489d
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java
+@@ -0,0 +1,100 @@
++package com.tuinity.tuinity.util;
++
++public final class IntervalledCounter {
++
++ protected long[] times;
++ protected final long interval;
++ protected long minTime;
++ protected int sum;
++ protected int head; // inclusive
++ protected int tail; // exclusive
++
++ public IntervalledCounter(final long interval) {
++ this.times = new long[8];
++ this.interval = interval;
++ }
++
++ public void updateCurrentTime() {
++ this.updateCurrentTime(System.nanoTime());
++ }
++
++ public void updateCurrentTime(final long currentTime) {
++ int sum = this.sum;
++ int head = this.head;
++ final int tail = this.tail;
++ final long minTime = currentTime - this.interval;
++
++ final int arrayLen = this.times.length;
++
++ // guard against overflow by using subtraction
++ while (head != tail && this.times[head] - minTime < 0) {
++ head = (head + 1) % arrayLen;
++ --sum;
++ }
++
++ this.sum = sum;
++ this.head = head;
++ this.minTime = minTime;
++ }
++
++ public void addTime(final long currTime) {
++ // guard against overflow by using subtraction
++ if (currTime - this.minTime < 0) {
++ return;
++ }
++ int nextTail = (this.tail + 1) % this.times.length;
++ if (nextTail == this.head) {
++ this.resize();
++ nextTail = (this.tail + 1) % this.times.length;
++ }
++
++ this.times[this.tail] = currTime;
++ this.tail = nextTail;
++ }
++
++ public void updateAndAdd(final int count) {
++ final long currTime = System.nanoTime();
++ this.updateCurrentTime(currTime);
++ for (int i = 0; i < count; ++i) {
++ this.addTime(currTime);
++ }
++ }
++
++ public void updateAndAdd(final int count, final long currTime) {
++ this.updateCurrentTime(currTime);
++ for (int i = 0; i < count; ++i) {
++ this.addTime(currTime);
++ }
++ }
++
++ private void resize() {
++ final long[] oldElements = this.times;
++ final long[] newElements = new long[this.times.length * 2];
++ this.times = newElements;
++
++ final int head = this.head;
++ final int tail = this.tail;
++ final int size = tail >= head ? (tail - head) : (tail + (oldElements.length - head));
++ this.head = 0;
++ this.tail = size;
++
++ if (tail >= head) {
++ System.arraycopy(oldElements, head, newElements, 0, size);
++ } else {
++ System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head);
++ System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail);
++ }
++ }
++
++ // returns in units per second
++ public double getRate() {
++ return this.size() / (this.interval * 1.0e-9);
++ }
++
++ public int size() {
++ final int head = this.head;
++ final int tail = this.tail;
++
++ return tail >= head ? (tail - head) : (tail + (this.times.length - head));
++ }
++}
+diff --git a/src/main/java/com/tuinity/tuinity/util/PoiAccess.java b/src/main/java/com/tuinity/tuinity/util/PoiAccess.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..e99583529a2cbdf8b764be3dff4373ec0ffaecd7
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/util/PoiAccess.java
+@@ -0,0 +1,748 @@
++package com.tuinity.tuinity.util;
++
++import it.unimi.dsi.fastutil.doubles.Double2ObjectMap;
++import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap;
++import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
++import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
++import net.minecraft.core.BlockPos;
++import net.minecraft.util.Mth;
++import net.minecraft.world.entity.ai.village.poi.PoiManager;
++import net.minecraft.world.entity.ai.village.poi.PoiRecord;
++import net.minecraft.world.entity.ai.village.poi.PoiSection;
++import net.minecraft.world.entity.ai.village.poi.PoiType;
++import java.util.ArrayList;
++import java.util.HashSet;
++import java.util.Iterator;
++import java.util.List;
++import java.util.Map;
++import java.util.Optional;
++import java.util.Set;
++import java.util.function.Predicate;
++
++/**
++ * Provides optimised access to POI data. All returned values will be identical to vanilla.
++ */
++public final class PoiAccess {
++
++ protected static double clamp(final double val, final double min, final double max) {
++ return (val < min ? min : (val > max ? max : val));
++ }
++
++ protected static double getSmallestDistanceSquared(final double boxMinX, final double boxMinY, final double boxMinZ,
++ final double boxMaxX, final double boxMaxY, final double boxMaxZ,
++
++ final double circleX, final double circleY, final double circleZ) {
++ // is the circle center inside the box?
++ if (circleX >= boxMinX && circleX <= boxMaxX && circleY >= boxMinY && circleY <= boxMaxY && circleZ >= boxMinZ && circleZ <= boxMaxZ) {
++ return 0.0;
++ }
++
++ final double boxWidthX = (boxMaxX - boxMinX) / 2.0;
++ final double boxWidthY = (boxMaxY - boxMinY) / 2.0;
++ final double boxWidthZ = (boxMaxZ - boxMinZ) / 2.0;
++
++ final double boxCenterX = (boxMinX + boxMaxX) / 2.0;
++ final double boxCenterY = (boxMinY + boxMaxY) / 2.0;
++ final double boxCenterZ = (boxMinZ + boxMaxZ) / 2.0;
++
++ double centerDiffX = circleX - boxCenterX;
++ double centerDiffY = circleY - boxCenterY;
++ double centerDiffZ = circleZ - boxCenterZ;
++
++ centerDiffX = circleX - (clamp(centerDiffX, -boxWidthX, boxWidthX) + boxCenterX);
++ centerDiffY = circleY - (clamp(centerDiffY, -boxWidthY, boxWidthY) + boxCenterY);
++ centerDiffZ = circleZ - (clamp(centerDiffZ, -boxWidthZ, boxWidthZ) + boxCenterZ);
++
++ return (centerDiffX * centerDiffX) + (centerDiffY * centerDiffY) + (centerDiffZ * centerDiffZ);
++ }
++
++
++ // key is:
++ // upper 32 bits:
++ // upper 16 bits: max y section
++ // lower 16 bits: min y section
++ // lower 32 bits:
++ // upper 16 bits: section
++ // lower 16 bits: radius
++ protected static long getKey(final int minSection, final int maxSection, final int section, final int radius) {
++ return (
++ (maxSection & 0xFFFFL) << (64 - 16)
++ | (minSection & 0xFFFFL) << (64 - 32)
++ | (section & 0xFFFFL) << (64 - 48)
++ | (radius & 0xFFFFL) << (64 - 64)
++ );
++ }
++
++ // only includes x/z axis
++ // finds the closest poi data by distance.
++ public static BlockPos findClosestPoiDataPosition(final PoiManager poiStorage,
++ final Predicate villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistance,
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final PoiRecord ret = findClosestPoiDataRecord(
++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load
++ );
++
++ return ret == null ? null : ret.getPos();
++ }
++
++ // only includes x/z axis
++ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned.
++ public static void findClosestPoiDataPositions(final PoiManager poiStorage,
++ final Predicate villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistance,
++ final PoiManager.Occupancy occupancy,
++ final boolean load,
++ final Set ret) {
++ final Set positions = new HashSet<>();
++ // pos predicate is last thing that runs before adding to ret.
++ final Predicate newPredicate = (final BlockPos pos) -> {
++ if (positionPredicate != null && !positionPredicate.test(pos)) {
++ return false;
++ }
++ return positions.add(pos.immutable());
++ };
++
++ final List toConvert = new ArrayList<>();
++ findClosestPoiDataRecords(
++ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistance, occupancy, load, toConvert
++ );
++
++ for (final PoiRecord record : toConvert) {
++ ret.add(record.getPos());
++ }
++ }
++
++ // only includes x/z axis
++ // finds the closest poi data by distance.
++ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage,
++ final Predicate villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistance,
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final List ret = new ArrayList<>();
++ findClosestPoiDataRecords(
++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load, ret
++ );
++ return ret.isEmpty() ? null : ret.get(0);
++ }
++
++ // only includes x/z axis
++ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned.
++ public static void findClosestPoiDataRecords(final PoiManager poiStorage,
++ final Predicate villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistance,
++ final PoiManager.Occupancy occupancy,
++ final boolean load,
++ final List ret) {
++ final Predicate super PoiRecord> occupancyFilter = occupancy.getTest();
++
++ final List closestRecords = new ArrayList<>();
++ double closestDistanceSquared = maxDistance * maxDistance;
++
++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4;
++ final int lowerY = WorldUtil.getMinSection(poiStorage.world);
++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4;
++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4;
++ final int upperY = WorldUtil.getMaxSection(poiStorage.world);
++ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4;
++
++ final int centerX = sourcePosition.getX() >> 4;
++ final int centerY = sourcePosition.getY() >> 4;
++ final int centerZ = sourcePosition.getZ() >> 4;
++
++ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
++ queue.enqueue(CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ));
++ final LongOpenHashSet seen = new LongOpenHashSet();
++
++ while (!queue.isEmpty()) {
++ final long key = queue.dequeueLong();
++ final int sectionX = CoordinateUtils.getChunkSectionX(key);
++ final int sectionY = CoordinateUtils.getChunkSectionY(key);
++ final int sectionZ = CoordinateUtils.getChunkSectionZ(key);
++
++ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) {
++ // out of bound chunk
++ continue;
++ }
++
++ final double sectionDistanceSquared = getSmallestDistanceSquared(
++ (sectionX << 4) + 0.5,
++ (sectionY << 4) + 0.5,
++ (sectionZ << 4) + 0.5,
++ (sectionX << 4) + 15.5,
++ (sectionY << 4) + 15.5,
++ (sectionZ << 4) + 15.5,
++ (double)sourcePosition.getX(), (double)sourcePosition.getY(), (double)sourcePosition.getZ()
++ );
++ if (sectionDistanceSquared > closestDistanceSquared) {
++ continue;
++ }
++
++ // queue all neighbours
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ for (int dy = -1; dy <= 1; ++dy) {
++ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many
++ // values are set. we only care about cardinal neighbours, so, we only care if one value is set
++ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) {
++ continue;
++ }
++
++ final int neighbourX = sectionX + dx;
++ final int neighbourY = sectionY + dy;
++ final int neighbourZ = sectionZ + dz;
++
++ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ);
++ if (seen.add(neighbourKey)) {
++ queue.enqueue(neighbourKey);
++ }
++ }
++ }
++ }
++
++ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key);
++
++ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) {
++ continue;
++ }
++
++ final PoiSection poiSection = poiSectionOptional.orElse(null);
++
++ final Map> sectionData = poiSection.getData();
++ if (sectionData.isEmpty()) {
++ continue;
++ }
++
++ // now we search the section data
++ for (final Map.Entry> entry : sectionData.entrySet()) {
++ if (!villagePlaceType.test(entry.getKey())) {
++ // filter out by poi type
++ continue;
++ }
++
++ // now we can look at the poi data
++ for (final PoiRecord poiData : entry.getValue()) {
++ if (!occupancyFilter.test(poiData)) {
++ // filter by occupancy
++ continue;
++ }
++
++ final BlockPos poiPosition = poiData.getPos();
++
++ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range
++ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) {
++ // out of range for square radius
++ continue;
++ }
++
++ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped!
++ final double dataRange = poiPosition.distSqr(sourcePosition);
++
++ if (dataRange > closestDistanceSquared) {
++ // out of range for distance check
++ continue;
++ }
++
++ if (positionPredicate != null && !positionPredicate.test(poiPosition)) {
++ // filter by position
++ continue;
++ }
++
++ if (dataRange < closestDistanceSquared) {
++ closestRecords.clear();
++ closestDistanceSquared = dataRange;
++ }
++ closestRecords.add(poiData);
++ }
++ }
++ }
++
++ // uh oh! we might have multiple records that match the distance sorting!
++ // we need to re-order our results by the way vanilla would have iterated over them.
++ closestRecords.sort((record1, record2) -> {
++ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section
++ // is fine and should be preserved (this sort is stable so we're good there)
++ // but they iterate sections by x then by z (like the following)
++ // for (int x = -dx; x <= dx; ++x)
++ // for (int z = -dz; z <= dz; ++z)
++ // ....
++ // so we need to reorder such that records with lower chunk z, then lower chunk x come first
++ final BlockPos pos1 = record1.getPos();
++ final BlockPos pos2 = record2.getPos();
++
++ final int cx1 = pos1.getX() >> 4;
++ final int cz1 = pos1.getZ() >> 4;
++
++ final int cx2 = pos2.getX() >> 4;
++ final int cz2 = pos2.getZ() >> 4;
++
++ if (cz2 != cz1) {
++ // want smaller z
++ return Integer.compare(cz1, cz2);
++ }
++
++ if (cx2 != cx1) {
++ // want smaller x
++ return Integer.compare(cx1, cx2);
++ }
++
++ // same chunk
++ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y
++ // so now we just compare section y, wanting smaller y
++
++ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4);
++ });
++
++ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully).
++ ret.addAll(closestRecords);
++ }
++
++ // finds the closest poi entry pos.
++ public static BlockPos findNearestPoiPosition(final PoiManager poiStorage,
++ final Predicate villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistance,
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final PoiRecord ret = findNearestPoiRecord(
++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load
++ );
++ return ret == null ? null : ret.getPos();
++ }
++
++ // finds the closest `max` poi entry positions.
++ public static void findNearestPoiPositions(final PoiManager poiStorage,
++ final Predicate villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistance,
++ final PoiManager.Occupancy occupancy,
++ final boolean load,
++ final int max,
++ final List ret) {
++ final Set positions = new HashSet<>();
++ // pos predicate is last thing that runs before adding to ret.
++ final Predicate newPredicate = (final BlockPos pos) -> {
++ if (positionPredicate != null && !positionPredicate.test(pos)) {
++ return false;
++ }
++ return positions.add(pos.immutable());
++ };
++
++ final List toConvert = new ArrayList<>();
++ findNearestPoiRecords(
++ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistance, occupancy, load, max, toConvert
++ );
++
++ for (final PoiRecord record : toConvert) {
++ ret.add(record.getPos());
++ }
++ }
++
++ // finds the closest poi entry.
++ public static PoiRecord findNearestPoiRecord(final PoiManager poiStorage,
++ final Predicate villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistance,
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final List ret = new ArrayList<>();
++ findNearestPoiRecords(
++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load,
++ 1, ret
++ );
++ return ret.isEmpty() ? null : ret.get(0);
++ }
++
++ // finds the closest `max` poi entries.
++ public static void findNearestPoiRecords(final PoiManager poiStorage,
++ final Predicate villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistance,
++ final PoiManager.Occupancy occupancy,
++ final boolean load,
++ final int max,
++ final List ret) {
++ final Predicate super PoiRecord> occupancyFilter = occupancy.getTest();
++
++ final double maxDistanceSquared = maxDistance * maxDistance;
++ final Double2ObjectRBTreeMap> closestRecords = new Double2ObjectRBTreeMap<>();
++ int totalRecords = 0;
++ double furthestDistanceSquared = maxDistanceSquared;
++
++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4;
++ final int lowerY = WorldUtil.getMinSection(poiStorage.world);
++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4;
++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4;
++ final int upperY = WorldUtil.getMaxSection(poiStorage.world);
++ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4;
++
++ final int centerX = sourcePosition.getX() >> 4;
++ final int centerY = sourcePosition.getY() >> 4;
++ final int centerZ = sourcePosition.getZ() >> 4;
++
++ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
++ queue.enqueue(CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ));
++ final LongOpenHashSet seen = new LongOpenHashSet();
++
++ while (!queue.isEmpty()) {
++ final long key = queue.dequeueLong();
++ final int sectionX = CoordinateUtils.getChunkSectionX(key);
++ final int sectionY = CoordinateUtils.getChunkSectionY(key);
++ final int sectionZ = CoordinateUtils.getChunkSectionZ(key);
++
++ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) {
++ // out of bound chunk
++ continue;
++ }
++
++ final double sectionDistanceSquared = getSmallestDistanceSquared(
++ (sectionX << 4) + 0.5,
++ (sectionY << 4) + 0.5,
++ (sectionZ << 4) + 0.5,
++ (sectionX << 4) + 15.5,
++ (sectionY << 4) + 15.5,
++ (sectionZ << 4) + 15.5,
++ (double) sourcePosition.getX(), (double) sourcePosition.getY(), (double) sourcePosition.getZ()
++ );
++
++ if (sectionDistanceSquared > (totalRecords >= max ? furthestDistanceSquared : maxDistanceSquared)) {
++ continue;
++ }
++
++ // queue all neighbours
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ for (int dy = -1; dy <= 1; ++dy) {
++ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many
++ // values are set. we only care about cardinal neighbours, so, we only care if one value is set
++ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) {
++ continue;
++ }
++
++ final int neighbourX = sectionX + dx;
++ final int neighbourY = sectionY + dy;
++ final int neighbourZ = sectionZ + dz;
++
++ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ);
++ if (seen.add(neighbourKey)) {
++ queue.enqueue(neighbourKey);
++ }
++ }
++ }
++ }
++
++ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key);
++
++ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) {
++ continue;
++ }
++
++ final PoiSection poiSection = poiSectionOptional.orElse(null);
++
++ final Map> sectionData = poiSection.getData();
++ if (sectionData.isEmpty()) {
++ continue;
++ }
++
++ // now we search the section data
++ for (final Map.Entry> entry : sectionData.entrySet()) {
++ if (!villagePlaceType.test(entry.getKey())) {
++ // filter out by poi type
++ continue;
++ }
++
++ // now we can look at the poi data
++ for (final PoiRecord poiData : entry.getValue()) {
++ if (!occupancyFilter.test(poiData)) {
++ // filter by occupancy
++ continue;
++ }
++
++ final BlockPos poiPosition = poiData.getPos();
++
++ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range
++ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) {
++ // out of range for square radius
++ continue;
++ }
++
++ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped!
++ final double dataRange = poiPosition.distSqr(sourcePosition);
++
++ if (dataRange > maxDistanceSquared) {
++ // out of range for distance check
++ continue;
++ }
++
++ if (dataRange > furthestDistanceSquared && totalRecords >= max) {
++ // out of range for distance check
++ continue;
++ }
++
++ if (positionPredicate != null && !positionPredicate.test(poiPosition)) {
++ // filter by position
++ continue;
++ }
++
++ if (dataRange > furthestDistanceSquared) {
++ // we know totalRecords < max, so this entry is now our furthest
++ furthestDistanceSquared = dataRange;
++ }
++
++ closestRecords.computeIfAbsent(dataRange, (final double unused) -> {
++ return new ArrayList<>();
++ }).add(poiData);
++
++ if (++totalRecords >= max) {
++ if (closestRecords.size() >= 2) {
++ int entriesInClosest = 0;
++ final Iterator>> iterator = closestRecords.double2ObjectEntrySet().iterator();
++ double nextFurthestDistanceSquared = 0.0;
++
++ for (int i = 0, len = closestRecords.size() - 1; i < len; ++i) {
++ final Double2ObjectMap.Entry> recordEntry = iterator.next();
++ entriesInClosest += recordEntry.getValue().size();
++ nextFurthestDistanceSquared = recordEntry.getDoubleKey();
++ }
++
++ if (entriesInClosest >= max) {
++ // the last set of entries at range wont even be considered for sure... nuke em
++ final Double2ObjectMap.Entry> recordEntry = iterator.next();
++ totalRecords -= recordEntry.getValue().size();
++ iterator.remove();
++
++ furthestDistanceSquared = nextFurthestDistanceSquared;
++ }
++ }
++ }
++ }
++ }
++ }
++
++ final List closestRecordsUnsorted = new ArrayList<>();
++
++ // we're done here, so now just flatten the map and sort it.
++
++ for (final List records : closestRecords.values()) {
++ closestRecordsUnsorted.addAll(records);
++ }
++
++ // uh oh! we might have multiple records that match the distance sorting!
++ // we need to re-order our results by the way vanilla would have iterated over them.
++ closestRecordsUnsorted.sort((record1, record2) -> {
++ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section
++ // is fine and should be preserved (this sort is stable so we're good there)
++ // but they iterate sections by x then by z (like the following)
++ // for (int x = -dx; x <= dx; ++x)
++ // for (int z = -dz; z <= dz; ++z)
++ // ....
++ // so we need to reorder such that records with lower chunk z, then lower chunk x come first
++ final BlockPos pos1 = record1.getPos();
++ final BlockPos pos2 = record2.getPos();
++
++ final int cx1 = pos1.getX() >> 4;
++ final int cz1 = pos1.getZ() >> 4;
++
++ final int cx2 = pos2.getX() >> 4;
++ final int cz2 = pos2.getZ() >> 4;
++
++ if (cz2 != cz1) {
++ // want smaller z
++ return Integer.compare(cz1, cz2);
++ }
++
++ if (cx2 != cx1) {
++ // want smaller x
++ return Integer.compare(cx1, cx2);
++ }
++
++ // same chunk
++ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y
++ // so now we just compare section y, wanting smaller section y
++
++ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4);
++ });
++
++ // trim out any entries exceeding our maximum
++ for (int i = closestRecordsUnsorted.size() - 1; i >= max; --i) {
++ closestRecordsUnsorted.remove(i);
++ }
++
++ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully).
++ ret.addAll(closestRecordsUnsorted);
++ }
++
++ public static BlockPos findAnyPoiPosition(final PoiManager poiStorage,
++ final Predicate villagePlaceType,
++ final Predicate positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final PoiRecord ret = findAnyPoiRecord(
++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load
++ );
++
++ return ret == null ? null : ret.getPos();
++ }
++
++ public static void findAnyPoiPositions(final PoiManager poiStorage,
++ final Predicate villagePlaceType,
++ final Predicate positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final PoiManager.Occupancy occupancy,
++ final boolean load,
++ final int max,
++ final List ret) {
++ final Set positions = new HashSet<>();
++ // pos predicate is last thing that runs before adding to ret.
++ final Predicate newPredicate = (final BlockPos pos) -> {
++ if (positionPredicate != null && !positionPredicate.test(pos)) {
++ return false;
++ }
++ return positions.add(pos.immutable());
++ };
++
++ final List toConvert = new ArrayList<>();
++ findAnyPoiRecords(
++ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, occupancy, load, max, toConvert
++ );
++
++ for (final PoiRecord record : toConvert) {
++ ret.add(record.getPos());
++ }
++ }
++
++ public static PoiRecord findAnyPoiRecord(final PoiManager poiStorage,
++ final Predicate villagePlaceType,
++ final Predicate positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final List ret = new ArrayList<>();
++ findAnyPoiRecords(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load, 1, ret);
++ return ret.isEmpty() ? null : ret.get(0);
++ }
++
++ public static void findAnyPoiRecords(final PoiManager poiStorage,
++ final Predicate villagePlaceType,
++ final Predicate positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final PoiManager.Occupancy occupancy,
++ final boolean load,
++ final int max,
++ final List ret) {
++ // the biggest issue with the original mojang implementation is that they chain so many streams together
++ // the amount of streams chained just rolls performance, even if nothing is iterated over
++ final Predicate super PoiRecord> occupancyFilter = occupancy.getTest();
++ final double rangeSquared = range * range;
++
++ int added = 0;
++
++ // First up, we need to iterate the chunks
++ // all the values here are in chunk sections
++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4;
++ final int lowerY = Math.max(WorldUtil.getMinSection(poiStorage.world), Mth.floor(sourcePosition.getY() - range) >> 4);
++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4;
++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4;
++ final int upperY = Math.min(WorldUtil.getMaxSection(poiStorage.world), Mth.floor(sourcePosition.getY() + range) >> 4);
++ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4;
++
++ // Vanilla iterates by x until max is reached then increases z
++ // vanilla also searches by increasing Y section value
++ for (int currZ = lowerZ; currZ <= upperZ; ++currZ) {
++ for (int currX = lowerX; currX <= upperX; ++currX) {
++ for (int currY = lowerY; currY <= upperY; ++currY) { // vanilla searches the entire chunk because they're actually stupid. just search the sections we need
++ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)) :
++ poiStorage.get(CoordinateUtils.getChunkSectionKey(currX, currY, currZ));
++ final PoiSection poiSection = poiSectionOptional == null ? null : poiSectionOptional.orElse(null);
++ if (poiSection == null) {
++ continue;
++ }
++
++ final Map> sectionData = poiSection.getData();
++ if (sectionData.isEmpty()) {
++ continue;
++ }
++
++ // now we search the section data
++ for (final Map.Entry> entry : sectionData.entrySet()) {
++ if (!villagePlaceType.test(entry.getKey())) {
++ // filter out by poi type
++ continue;
++ }
++
++ // now we can look at the poi data
++ for (final PoiRecord poiData : entry.getValue()) {
++ if (!occupancyFilter.test(poiData)) {
++ // filter by occupancy
++ continue;
++ }
++
++ final BlockPos poiPosition = poiData.getPos();
++
++ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range
++ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) {
++ // out of range for square radius
++ continue;
++ }
++
++ if (poiPosition.distSqr(sourcePosition) > rangeSquared) {
++ // out of range for distance check
++ continue;
++ }
++
++ if (positionPredicate != null && !positionPredicate.test(poiPosition)) {
++ // filter by position
++ continue;
++ }
++
++ // found one!
++ ret.add(poiData);
++ if (++added >= max) {
++ return;
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++
++ private PoiAccess() {
++ throw new RuntimeException();
++ }
++}
+diff --git a/src/main/java/com/tuinity/tuinity/util/TickThread.java b/src/main/java/com/tuinity/tuinity/util/TickThread.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a08377e4b0d9c2d78cf851e2c72770cf623de51a
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/util/TickThread.java
+@@ -0,0 +1,41 @@
++package com.tuinity.tuinity.util;
++
++import net.minecraft.server.MinecraftServer;
++import org.bukkit.Bukkit;
++
++public final class TickThread extends Thread {
++
++ public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("tuinity.strict-thread-checks");
++
++ static {
++ if (STRICT_THREAD_CHECKS) {
++ MinecraftServer.LOGGER.warn("Strict thread checks enabled - performance may suffer");
++ }
++ }
++
++ public static void softEnsureTickThread(final String reason) {
++ if (!STRICT_THREAD_CHECKS) {
++ return;
++ }
++ ensureTickThread(reason);
++ }
++
++
++ public static void ensureTickThread(final String reason) {
++ if (!Bukkit.isPrimaryThread()) {
++ MinecraftServer.LOGGER.fatal("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
++ }
++
++ public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */
++
++ public TickThread(final Runnable run, final String name, final int id) {
++ super(run, name);
++ this.id = id;
++ }
++
++ public static TickThread getCurrentTickThread() {
++ return (TickThread)Thread.currentThread();
++ }
++}
+diff --git a/src/main/java/com/tuinity/tuinity/util/WorldUtil.java b/src/main/java/com/tuinity/tuinity/util/WorldUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..958b3aff3dda64323456d7e0ef0346a72d43f3f1
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/util/WorldUtil.java
+@@ -0,0 +1,46 @@
++package com.tuinity.tuinity.util;
++
++import net.minecraft.world.level.LevelHeightAccessor;
++
++public final class WorldUtil {
++
++ // min, max are inclusive
++
++ public static int getMaxSection(final LevelHeightAccessor world) {
++ return world.getMaxSection() - 1; // getMaxSection() is exclusive
++ }
++
++ public static int getMinSection(final LevelHeightAccessor world) {
++ return world.getMinSection();
++ }
++
++ public static int getMaxLightSection(final LevelHeightAccessor world) {
++ return getMaxSection(world) + 1;
++ }
++
++ public static int getMinLightSection(final LevelHeightAccessor world) {
++ return getMinSection(world) - 1;
++ }
++
++
++
++ public static int getTotalSections(final LevelHeightAccessor world) {
++ return getMaxSection(world) - getMinSection(world) + 1;
++ }
++
++ public static int getTotalLightSections(final LevelHeightAccessor world) {
++ return getMaxLightSection(world) - getMinLightSection(world) + 1;
++ }
++
++ public static int getMinBlockY(final LevelHeightAccessor world) {
++ return getMinSection(world) << 4;
++ }
++
++ public static int getMaxBlockY(final LevelHeightAccessor world) {
++ return (getMaxSection(world) << 4) | 15;
++ }
++
++ private WorldUtil() {
++ throw new RuntimeException();
++ }
++}
+diff --git a/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java b/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..98a1be343d81d6431476fea6a68014def8ce923b
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java
+@@ -0,0 +1,334 @@
++package com.tuinity.tuinity.util.maplist;
++
++import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2IntMap;
++import org.bukkit.Bukkit;
++import java.util.Arrays;
++import java.util.NoSuchElementException;
++
++public final class IteratorSafeOrderedReferenceSet {
++
++ public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0;
++
++ protected final Reference2IntLinkedOpenHashMap indexMap;
++ protected int firstInvalidIndex = -1;
++
++ /* list impl */
++ protected E[] listElements;
++ protected int listSize;
++
++ protected final double maxFragFactor;
++
++ protected int iteratorCount;
++
++ private final boolean threadRestricted;
++
++ public IteratorSafeOrderedReferenceSet() {
++ this(16, 0.75f, 16, 0.2);
++ }
++
++ public IteratorSafeOrderedReferenceSet(final boolean threadRestricted) {
++ this(16, 0.75f, 16, 0.2, threadRestricted);
++ }
++
++ public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity,
++ final double maxFragFactor) {
++ this(setCapacity, setLoadFactor, arrayCapacity, maxFragFactor, false);
++ }
++ public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity,
++ final double maxFragFactor, final boolean threadRestricted) {
++ this.indexMap = new Reference2IntLinkedOpenHashMap<>(setCapacity, setLoadFactor);
++ this.indexMap.defaultReturnValue(-1);
++ this.maxFragFactor = maxFragFactor;
++ this.listElements = (E[])new Object[arrayCapacity];
++ this.threadRestricted = threadRestricted;
++ }
++
++ /*
++ public void check() {
++ int iterated = 0;
++ ReferenceOpenHashSet check = new ReferenceOpenHashSet<>();
++ if (this.listElements != null) {
++ for (int i = 0; i < this.listSize; ++i) {
++ Object obj = this.listElements[i];
++ if (obj != null) {
++ iterated++;
++ if (!check.add((E)obj)) {
++ throw new IllegalStateException("contains duplicate");
++ }
++ if (!this.contains((E)obj)) {
++ throw new IllegalStateException("desync");
++ }
++ }
++ }
++ }
++
++ if (iterated != this.size()) {
++ throw new IllegalStateException("Size is mismatched! Got " + iterated + ", expected " + this.size());
++ }
++
++ check.clear();
++ iterated = 0;
++ for (final java.util.Iterator iterator = this.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
++ final E element = iterator.next();
++ iterated++;
++ if (!check.add(element)) {
++ throw new IllegalStateException("contains duplicate (iterator is wrong)");
++ }
++ if (!this.contains(element)) {
++ throw new IllegalStateException("desync (iterator is wrong)");
++ }
++ }
++
++ if (iterated != this.size()) {
++ throw new IllegalStateException("Size is mismatched! (iterator is wrong) Got " + iterated + ", expected " + this.size());
++ }
++ }
++ */
++
++ protected final boolean allowSafeIteration() {
++ return !this.threadRestricted || Bukkit.isPrimaryThread();
++ }
++
++ protected final double getFragFactor() {
++ return 1.0 - ((double)this.indexMap.size() / (double)this.listSize);
++ }
++
++ public int createRawIterator() {
++ if (this.allowSafeIteration()) {
++ ++this.iteratorCount;
++ }
++ if (this.indexMap.isEmpty()) {
++ return -1;
++ } else {
++ return this.firstInvalidIndex == 0 ? this.indexMap.getInt(this.indexMap.firstKey()) : 0;
++ }
++ }
++
++ public int advanceRawIterator(final int index) {
++ final E[] elements = this.listElements;
++ int ret = index + 1;
++ for (int len = this.listSize; ret < len; ++ret) {
++ if (elements[ret] != null) {
++ return ret;
++ }
++ }
++
++ return -1;
++ }
++
++ public void finishRawIterator() {
++ if (this.allowSafeIteration() && --this.iteratorCount == 0) {
++ if (this.getFragFactor() >= this.maxFragFactor) {
++ this.defrag();
++ }
++ }
++ }
++
++ public boolean remove(final E element) {
++ final int index = this.indexMap.removeInt(element);
++ if (index >= 0) {
++ if (this.firstInvalidIndex < 0 || index < this.firstInvalidIndex) {
++ this.firstInvalidIndex = index;
++ }
++ if (this.listElements[index] != element) {
++ throw new IllegalStateException();
++ }
++ this.listElements[index] = null;
++ if (this.allowSafeIteration() && this.iteratorCount == 0 && this.getFragFactor() >= this.maxFragFactor) {
++ this.defrag();
++ }
++ //this.check();
++ return true;
++ }
++ return false;
++ }
++
++ public boolean contains(final E element) {
++ return this.indexMap.containsKey(element);
++ }
++
++ public boolean add(final E element) {
++ final int listSize = this.listSize;
++
++ final int previous = this.indexMap.putIfAbsent(element, listSize);
++ if (previous != -1) {
++ return false;
++ }
++
++ if (listSize >= this.listElements.length) {
++ this.listElements = Arrays.copyOf(this.listElements, listSize * 2);
++ }
++ this.listElements[listSize] = element;
++ this.listSize = listSize + 1;
++
++ //this.check();
++ return true;
++ }
++
++ protected void defrag() {
++ if (this.firstInvalidIndex < 0) {
++ return; // nothing to do
++ }
++
++ if (this.indexMap.isEmpty()) {
++ Arrays.fill(this.listElements, 0, this.listSize, null);
++ this.listSize = 0;
++ this.firstInvalidIndex = -1;
++ //this.check();
++ return;
++ }
++
++ final E[] backingArray = this.listElements;
++
++ int lastValidIndex;
++ java.util.Iterator> iterator;
++
++ if (this.firstInvalidIndex == 0) {
++ iterator = this.indexMap.reference2IntEntrySet().fastIterator();
++ lastValidIndex = 0;
++ } else {
++ lastValidIndex = this.firstInvalidIndex;
++ final E key = backingArray[lastValidIndex - 1];
++ iterator = this.indexMap.reference2IntEntrySet().fastIterator(new Reference2IntMap.Entry() {
++ @Override
++ public int getIntValue() {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public int setValue(int i) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public E getKey() {
++ return key;
++ }
++ });
++ }
++
++ while (iterator.hasNext()) {
++ final Reference2IntMap.Entry entry = iterator.next();
++
++ final int newIndex = lastValidIndex++;
++ backingArray[newIndex] = entry.getKey();
++ entry.setValue(newIndex);
++ }
++
++ // cleanup end
++ Arrays.fill(backingArray, lastValidIndex, this.listSize, null);
++ this.listSize = lastValidIndex;
++ this.firstInvalidIndex = -1;
++ //this.check();
++ }
++
++ public E rawGet(final int index) {
++ return this.listElements[index];
++ }
++
++ public int size() {
++ // always returns the correct amount - listSize can be different
++ return this.indexMap.size();
++ }
++
++ public IteratorSafeOrderedReferenceSet.Iterator iterator() {
++ return this.iterator(0);
++ }
++
++ public IteratorSafeOrderedReferenceSet.Iterator iterator(final int flags) {
++ if (this.allowSafeIteration()) {
++ ++this.iteratorCount;
++ }
++ return new BaseIterator<>(this, true, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
++ }
++
++ public java.util.Iterator unsafeIterator() {
++ return this.unsafeIterator(0);
++ }
++ public java.util.Iterator unsafeIterator(final int flags) {
++ return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
++ }
++
++ public static interface Iterator extends java.util.Iterator {
++
++ public void finishedIterating();
++
++ }
++
++ protected static final class BaseIterator implements IteratorSafeOrderedReferenceSet.Iterator {
++
++ protected final IteratorSafeOrderedReferenceSet set;
++ protected final boolean canFinish;
++ protected final int maxIndex;
++ protected int nextIndex;
++ protected E pendingValue;
++ protected boolean finished;
++ protected E lastReturned;
++
++ protected BaseIterator(final IteratorSafeOrderedReferenceSet set, final boolean canFinish, final int maxIndex) {
++ this.set = set;
++ this.canFinish = canFinish;
++ this.maxIndex = maxIndex;
++ }
++
++ @Override
++ public boolean hasNext() {
++ if (this.finished) {
++ return false;
++ }
++ if (this.pendingValue != null) {
++ return true;
++ }
++
++ final E[] elements = this.set.listElements;
++ int index, len;
++ for (index = this.nextIndex, len = Math.min(this.maxIndex, this.set.listSize); index < len; ++index) {
++ final E element = elements[index];
++ if (element != null) {
++ this.pendingValue = element;
++ this.nextIndex = index + 1;
++ return true;
++ }
++ }
++
++ this.nextIndex = index;
++ return false;
++ }
++
++ @Override
++ public E next() {
++ if (!this.hasNext()) {
++ throw new NoSuchElementException();
++ }
++ final E ret = this.pendingValue;
++
++ this.pendingValue = null;
++ this.lastReturned = ret;
++
++ return ret;
++ }
++
++ @Override
++ public void remove() {
++ final E lastReturned = this.lastReturned;
++ if (lastReturned == null) {
++ throw new IllegalStateException();
++ }
++ this.lastReturned = null;
++ this.set.remove(lastReturned);
++ }
++
++ @Override
++ public void finishedIterating() {
++ if (this.finished || !this.canFinish) {
++ throw new IllegalStateException();
++ }
++ this.lastReturned = null;
++ this.finished = true;
++ if (this.set.allowSafeIteration()) {
++ this.set.finishRawIterator();
++ }
++ }
++ }
++}
+diff --git a/src/main/java/com/tuinity/tuinity/util/math/ThreadUnsafeRandom.java b/src/main/java/com/tuinity/tuinity/util/math/ThreadUnsafeRandom.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7f2aeb5ed6775ddf38f2561aae8a82f99c8413a7
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/util/math/ThreadUnsafeRandom.java
+@@ -0,0 +1,46 @@
++package com.tuinity.tuinity.util.math;
++
++import java.util.Random;
++
++public final class ThreadUnsafeRandom extends Random {
++
++ // See javadoc and internal comments for java.util.Random where these values come from, how they are used, and the author for them.
++ private static final long multiplier = 0x5DEECE66DL;
++ private static final long addend = 0xBL;
++ private static final long mask = (1L << 48) - 1;
++
++ private static long initialScramble(long seed) {
++ return (seed ^ multiplier) & mask;
++ }
++
++ private long seed;
++
++ @Override
++ public void setSeed(long seed) {
++ // note: called by Random constructor
++ this.seed = initialScramble(seed);
++ }
++
++ @Override
++ protected int next(int bits) {
++ // avoid the expensive CAS logic used by superclass
++ return (int) (((this.seed = this.seed * multiplier + addend) & mask) >>> (48 - bits));
++ }
++
++ // Taken from
++ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/
++ // https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/blob/master/2016/06/25/fastrange.c
++ // Original license is public domain
++ public static int fastRandomBounded(final long randomInteger, final long limit) {
++ // randomInteger must be [0, pow(2, 32))
++ // limit must be [0, pow(2, 32))
++ return (int)((randomInteger * limit) >>> 32);
++ }
++
++ @Override
++ public int nextInt(int bound) {
++ // yes this breaks random's spec
++ // however there's nothing that uses this class that relies on it
++ return fastRandomBounded(this.next(32) & 0xFFFFFFFFL, bound);
++ }
++}
+diff --git a/src/main/java/com/tuinity/tuinity/util/misc/Delayed26WayDistancePropagator3D.java b/src/main/java/com/tuinity/tuinity/util/misc/Delayed26WayDistancePropagator3D.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b8fa9cd9bce312fc85b90e17094241216c620a9e
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/util/misc/Delayed26WayDistancePropagator3D.java
+@@ -0,0 +1,297 @@
++package com.tuinity.tuinity.util.misc;
++
++import com.tuinity.tuinity.util.CoordinateUtils;
++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
++import it.unimi.dsi.fastutil.longs.LongIterator;
++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
++
++public final class Delayed26WayDistancePropagator3D {
++
++ // this map is considered "stale" unless updates are propagated.
++ protected final Delayed8WayDistancePropagator2D.LevelMap levels = new Delayed8WayDistancePropagator2D.LevelMap(8192*2, 0.6f);
++
++ // this map is never stale
++ protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f);
++
++ // Generally updates to positions are made close to other updates, so we link to decrease cache misses when
++ // propagating updates
++ protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet();
++
++ @FunctionalInterface
++ public static interface LevelChangeCallback {
++
++ /**
++ * This can be called for intermediate updates. So do not rely on newLevel being close to or
++ * the exact level that is expected after a full propagation has occured.
++ */
++ public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel);
++
++ }
++
++ protected final LevelChangeCallback changeCallback;
++
++ public Delayed26WayDistancePropagator3D() {
++ this(null);
++ }
++
++ public Delayed26WayDistancePropagator3D(final LevelChangeCallback changeCallback) {
++ this.changeCallback = changeCallback;
++ }
++
++ public int getLevel(final long pos) {
++ return this.levels.get(pos);
++ }
++
++ public int getLevel(final int x, final int y, final int z) {
++ return this.levels.get(CoordinateUtils.getChunkSectionKey(x, y, z));
++ }
++
++ public void setSource(final int x, final int y, final int z, final int level) {
++ this.setSource(CoordinateUtils.getChunkSectionKey(x, y, z), level);
++ }
++
++ public void setSource(final long coordinate, final int level) {
++ if ((level & 63) != level || level == 0) {
++ throw new IllegalArgumentException("Level must be in (0, 63], not " + level);
++ }
++
++ final byte byteLevel = (byte)level;
++ final byte oldLevel = this.sources.put(coordinate, byteLevel);
++
++ if (oldLevel == byteLevel) {
++ return; // nothing to do
++ }
++
++ // queue to update later
++ this.updatedSources.add(coordinate);
++ }
++
++ public void removeSource(final int x, final int y, final int z) {
++ this.removeSource(CoordinateUtils.getChunkSectionKey(x, y, z));
++ }
++
++ public void removeSource(final long coordinate) {
++ if (this.sources.remove(coordinate) != 0) {
++ this.updatedSources.add(coordinate);
++ }
++ }
++
++ // queues used for BFS propagating levels
++ protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelIncreaseWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) {
++ this.levelIncreaseWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue();
++ }
++ }
++ protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelRemoveWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) {
++ this.levelRemoveWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue();
++ }
++ }
++ protected long levelIncreaseWorkQueueBitset;
++ protected long levelRemoveWorkQueueBitset;
++
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) {
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelIncreaseWorkQueueBitset |= (1L << level);
++ }
++
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) {
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[index];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelIncreaseWorkQueueBitset |= (1L << index);
++ }
++
++ protected final void addToRemoveWorkQueue(final long coordinate, final byte level) {
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelRemoveWorkQueueBitset |= (1L << level);
++ }
++
++ public boolean propagateUpdates() {
++ if (this.updatedSources.isEmpty()) {
++ return false;
++ }
++
++ boolean ret = false;
++
++ for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) {
++ final long coordinate = iterator.nextLong();
++
++ final byte currentLevel = this.levels.get(coordinate);
++ final byte updatedSource = this.sources.get(coordinate);
++
++ if (currentLevel == updatedSource) {
++ continue;
++ }
++ ret = true;
++
++ if (updatedSource > currentLevel) {
++ // level increase
++ this.addToIncreaseWorkQueue(coordinate, updatedSource);
++ } else {
++ // level decrease
++ this.addToRemoveWorkQueue(coordinate, currentLevel);
++ // if the current coordinate is a source, then the decrease propagation will detect that and queue
++ // the source propagation
++ }
++ }
++
++ this.updatedSources.clear();
++
++ // propagate source level increases first for performance reasons (in crowded areas hopefully the additions
++ // make the removes remove less)
++ this.propagateIncreases();
++
++ // now we propagate the decreases (which will then re-propagate clobbered sources)
++ this.propagateDecreases();
++
++ return ret;
++ }
++
++ protected void propagateIncreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset);
++ this.levelIncreaseWorkQueueBitset != 0L;
++ this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) {
++
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ byte level = queue.queuedLevels.removeFirstByte();
++
++ final boolean neighbourCheck = level < 0;
++
++ final byte currentLevel;
++ if (neighbourCheck) {
++ level = (byte)-level;
++ currentLevel = this.levels.get(coordinate);
++ } else {
++ currentLevel = this.levels.putIfGreater(coordinate, level);
++ }
++
++ if (neighbourCheck) {
++ // used when propagating from decrease to indicate that this level needs to check its neighbours
++ // this means the level at coordinate could be equal, but would still need neighbours checked
++
++ if (currentLevel != level) {
++ // something caused the level to change, which means something propagated to it (which means
++ // us propagating here is redundant), or something removed the level (which means we
++ // cannot propagate further)
++ continue;
++ }
++ } else if (currentLevel >= level) {
++ // something higher/equal propagated
++ continue;
++ }
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, level);
++ }
++
++ if (level == 1) {
++ // can't propagate 0 to neighbours
++ continue;
++ }
++
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = CoordinateUtils.getChunkSectionX(coordinate);
++ final int y = CoordinateUtils.getChunkSectionY(coordinate);
++ final int z = CoordinateUtils.getChunkSectionZ(coordinate);
++
++ for (int dy = -1; dy <= 1; ++dy) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ if ((dy | dz | dx) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
++
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z);
++ this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
++ }
++ }
++ }
++ }
++ }
++
++ protected void propagateDecreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset);
++ this.levelRemoveWorkQueueBitset != 0L;
++ this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) {
++
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ final byte level = queue.queuedLevels.removeFirstByte();
++
++ final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level);
++ if (currentLevel == 0) {
++ // something else removed
++ continue;
++ }
++
++ if (currentLevel > level) {
++ // something higher propagated here or we hit the propagation of another source
++ // in the second case we need to re-propagate because we could have just clobbered another source's
++ // propagation
++ this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking
++ continue;
++ }
++
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0);
++ }
++
++ final byte source = this.sources.get(coordinate);
++ if (source != 0) {
++ // must re-propagate source later
++ this.addToIncreaseWorkQueue(coordinate, source);
++ }
++
++ if (level == 0) {
++ // can't propagate -1 to neighbours
++ // we have to check neighbours for removing 1 just in case the neighbour is 2
++ continue;
++ }
++
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = CoordinateUtils.getChunkSectionX(coordinate);
++ final int y = CoordinateUtils.getChunkSectionY(coordinate);
++ final int z = CoordinateUtils.getChunkSectionZ(coordinate);
++
++ for (int dy = -1; dy <= 1; ++dy) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ if ((dy | dz | dx) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
++
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z);
++ this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
++ }
++ }
++ }
++ }
++
++ // propagate sources we clobbered in the process
++ this.propagateIncreases();
++ }
++}
+diff --git a/src/main/java/com/tuinity/tuinity/util/misc/Delayed8WayDistancePropagator2D.java b/src/main/java/com/tuinity/tuinity/util/misc/Delayed8WayDistancePropagator2D.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..cdd3c4032c1d6b34a10ba415bd4d0e377aa9af3c
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/util/misc/Delayed8WayDistancePropagator2D.java
+@@ -0,0 +1,718 @@
++package com.tuinity.tuinity.util.misc;
++
++import it.unimi.dsi.fastutil.HashCommon;
++import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue;
++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
++import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
++import it.unimi.dsi.fastutil.longs.LongIterator;
++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
++import net.minecraft.server.MCUtil;
++
++public final class Delayed8WayDistancePropagator2D {
++
++ // Test
++ /*
++ protected static void test(int x, int z, com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap reference, Delayed8WayDistancePropagator2D test) {
++ int got = test.getLevel(x, z);
++
++ int expect = 0;
++ Object[] nearest = reference.getObjectsInRange(x, z) == null ? null : reference.getObjectsInRange(x, z).getBackingSet();
++ if (nearest != null) {
++ for (Object _obj : nearest) {
++ if (_obj instanceof Ticket) {
++ Ticket ticket = (Ticket)_obj;
++ long ticketCoord = reference.getLastCoordinate(ticket);
++ int viewDistance = reference.getLastViewDistance(ticket);
++ int distance = Math.max(com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateX(ticketCoord) - x),
++ com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateZ(ticketCoord) - z));
++ int level = viewDistance - distance;
++ if (level > expect) {
++ expect = level;
++ }
++ }
++ }
++ }
++
++ if (expect != got) {
++ throw new IllegalStateException("Expected " + expect + " at pos (" + x + "," + z + ") but got " + got);
++ }
++ }
++
++ static class Ticket {
++
++ int x;
++ int z;
++
++ final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet empty
++ = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this);
++
++ }
++
++ public static void main(final String[] args) {
++ com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap reference = new com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap() {
++ @Override
++ protected com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getEmptySetFor(Ticket object) {
++ return object.empty;
++ }
++ };
++ Delayed8WayDistancePropagator2D test = new Delayed8WayDistancePropagator2D();
++
++ final int maxDistance = 64;
++ // test origin
++ {
++ Ticket originTicket = new Ticket();
++ int originDistance = 31;
++ // test single source
++ reference.add(originTicket, 0, 0, originDistance);
++ test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++ // test single source decrease
++ reference.update(originTicket, 0, 0, originDistance/2);
++ test.setSource(0, 0, originDistance/2); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++ // test source increase
++ originDistance = 2*originDistance;
++ reference.update(originTicket, 0, 0, originDistance);
++ test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) {
++ for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ reference.remove(originTicket);
++ test.removeSource(0, 0); test.propagateUpdates();
++ }
++
++ // test multiple sources at origin
++ {
++ int originDistance = 31;
++ java.util.List list = new java.util.ArrayList<>();
++ for (int i = 0; i < 10; ++i) {
++ Ticket a = new Ticket();
++ list.add(a);
++ a.x = (i & 1) == 1 ? -i : i;
++ a.z = (i & 1) == 1 ? -i : i;
++ }
++ for (Ticket ticket : list) {
++ reference.add(ticket, ticket.x, ticket.z, originDistance);
++ test.setSource(ticket.x, ticket.z, originDistance);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket level decrease
++
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance/2);
++ test.setSource(ticket.x, ticket.z, originDistance/2);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket level increase
++
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance*2);
++ test.setSource(ticket.x, ticket.z, originDistance*2);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket remove
++ for (int i = 0, len = list.size(); i < len; ++i) {
++ if ((i & 3) != 0) {
++ continue;
++ }
++ Ticket ticket = list.get(i);
++ reference.remove(ticket);
++ test.removeSource(ticket.x, ticket.z);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++ }
++
++ // now test at coordinate offsets
++ // test offset
++ {
++ Ticket originTicket = new Ticket();
++ int originDistance = 31;
++ int offX = 54432;
++ int offZ = -134567;
++ // test single source
++ reference.add(originTicket, offX, offZ, originDistance);
++ test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx + offX, dz + offZ, reference, test);
++ }
++ }
++ // test single source decrease
++ reference.update(originTicket, offX, offZ, originDistance/2);
++ test.setSource(offX, offZ, originDistance/2); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx + offX, dz + offZ, reference, test);
++ }
++ }
++ // test source increase
++ originDistance = 2*originDistance;
++ reference.update(originTicket, offX, offZ, originDistance);
++ test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) {
++ for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) {
++ test(dx + offX, dz + offZ, reference, test);
++ }
++ }
++
++ reference.remove(originTicket);
++ test.removeSource(offX, offZ); test.propagateUpdates();
++ }
++
++ // test multiple sources at origin
++ {
++ int originDistance = 31;
++ int offX = 54432;
++ int offZ = -134567;
++ java.util.List list = new java.util.ArrayList<>();
++ for (int i = 0; i < 10; ++i) {
++ Ticket a = new Ticket();
++ list.add(a);
++ a.x = offX + ((i & 1) == 1 ? -i : i);
++ a.z = offZ + ((i & 1) == 1 ? -i : i);
++ }
++ for (Ticket ticket : list) {
++ reference.add(ticket, ticket.x, ticket.z, originDistance);
++ test.setSource(ticket.x, ticket.z, originDistance);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket level decrease
++
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance/2);
++ test.setSource(ticket.x, ticket.z, originDistance/2);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket level increase
++
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance*2);
++ test.setSource(ticket.x, ticket.z, originDistance*2);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket remove
++ for (int i = 0, len = list.size(); i < len; ++i) {
++ if ((i & 3) != 0) {
++ continue;
++ }
++ Ticket ticket = list.get(i);
++ reference.remove(ticket);
++ test.removeSource(ticket.x, ticket.z);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++ }
++ }
++ */
++
++ // this map is considered "stale" unless updates are propagated.
++ protected final LevelMap levels = new LevelMap(8192*2, 0.6f);
++
++ // this map is never stale
++ protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f);
++
++ // Generally updates to positions are made close to other updates, so we link to decrease cache misses when
++ // propagating updates
++ protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet();
++
++ @FunctionalInterface
++ public static interface LevelChangeCallback {
++
++ /**
++ * This can be called for intermediate updates. So do not rely on newLevel being close to or
++ * the exact level that is expected after a full propagation has occured.
++ */
++ public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel);
++
++ }
++
++ protected final LevelChangeCallback changeCallback;
++
++ public Delayed8WayDistancePropagator2D() {
++ this(null);
++ }
++
++ public Delayed8WayDistancePropagator2D(final LevelChangeCallback changeCallback) {
++ this.changeCallback = changeCallback;
++ }
++
++ public int getLevel(final long pos) {
++ return this.levels.get(pos);
++ }
++
++ public int getLevel(final int x, final int z) {
++ return this.levels.get(MCUtil.getCoordinateKey(x, z));
++ }
++
++ public void setSource(final int x, final int z, final int level) {
++ this.setSource(MCUtil.getCoordinateKey(x, z), level);
++ }
++
++ public void setSource(final long coordinate, final int level) {
++ if ((level & 63) != level || level == 0) {
++ throw new IllegalArgumentException("Level must be in (0, 63], not " + level);
++ }
++
++ final byte byteLevel = (byte)level;
++ final byte oldLevel = this.sources.put(coordinate, byteLevel);
++
++ if (oldLevel == byteLevel) {
++ return; // nothing to do
++ }
++
++ // queue to update later
++ this.updatedSources.add(coordinate);
++ }
++
++ public void removeSource(final int x, final int z) {
++ this.removeSource(MCUtil.getCoordinateKey(x, z));
++ }
++
++ public void removeSource(final long coordinate) {
++ if (this.sources.remove(coordinate) != 0) {
++ this.updatedSources.add(coordinate);
++ }
++ }
++
++ // queues used for BFS propagating levels
++ protected final WorkQueue[] levelIncreaseWorkQueues = new WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) {
++ this.levelIncreaseWorkQueues[i] = new WorkQueue();
++ }
++ }
++ protected final WorkQueue[] levelRemoveWorkQueues = new WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) {
++ this.levelRemoveWorkQueues[i] = new WorkQueue();
++ }
++ }
++ protected long levelIncreaseWorkQueueBitset;
++ protected long levelRemoveWorkQueueBitset;
++
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) {
++ final WorkQueue queue = this.levelIncreaseWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelIncreaseWorkQueueBitset |= (1L << level);
++ }
++
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) {
++ final WorkQueue queue = this.levelIncreaseWorkQueues[index];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelIncreaseWorkQueueBitset |= (1L << index);
++ }
++
++ protected final void addToRemoveWorkQueue(final long coordinate, final byte level) {
++ final WorkQueue queue = this.levelRemoveWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelRemoveWorkQueueBitset |= (1L << level);
++ }
++
++ public boolean propagateUpdates() {
++ if (this.updatedSources.isEmpty()) {
++ return false;
++ }
++
++ boolean ret = false;
++
++ for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) {
++ final long coordinate = iterator.nextLong();
++
++ final byte currentLevel = this.levels.get(coordinate);
++ final byte updatedSource = this.sources.get(coordinate);
++
++ if (currentLevel == updatedSource) {
++ continue;
++ }
++ ret = true;
++
++ if (updatedSource > currentLevel) {
++ // level increase
++ this.addToIncreaseWorkQueue(coordinate, updatedSource);
++ } else {
++ // level decrease
++ this.addToRemoveWorkQueue(coordinate, currentLevel);
++ // if the current coordinate is a source, then the decrease propagation will detect that and queue
++ // the source propagation
++ }
++ }
++
++ this.updatedSources.clear();
++
++ // propagate source level increases first for performance reasons (in crowded areas hopefully the additions
++ // make the removes remove less)
++ this.propagateIncreases();
++
++ // now we propagate the decreases (which will then re-propagate clobbered sources)
++ this.propagateDecreases();
++
++ return ret;
++ }
++
++ protected void propagateIncreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset);
++ this.levelIncreaseWorkQueueBitset != 0L;
++ this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) {
++
++ final WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ byte level = queue.queuedLevels.removeFirstByte();
++
++ final boolean neighbourCheck = level < 0;
++
++ final byte currentLevel;
++ if (neighbourCheck) {
++ level = (byte)-level;
++ currentLevel = this.levels.get(coordinate);
++ } else {
++ currentLevel = this.levels.putIfGreater(coordinate, level);
++ }
++
++ if (neighbourCheck) {
++ // used when propagating from decrease to indicate that this level needs to check its neighbours
++ // this means the level at coordinate could be equal, but would still need neighbours checked
++
++ if (currentLevel != level) {
++ // something caused the level to change, which means something propagated to it (which means
++ // us propagating here is redundant), or something removed the level (which means we
++ // cannot propagate further)
++ continue;
++ }
++ } else if (currentLevel >= level) {
++ // something higher/equal propagated
++ continue;
++ }
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, level);
++ }
++
++ if (level == 1) {
++ // can't propagate 0 to neighbours
++ continue;
++ }
++
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = (int)coordinate;
++ final int z = (int)(coordinate >>> 32);
++
++ for (int dx = -1; dx <= 1; ++dx) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ if ((dx | dz) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
++
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = MCUtil.getCoordinateKey(x + dx, z + dz);
++ this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
++ }
++ }
++ }
++ }
++
++ protected void propagateDecreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset);
++ this.levelRemoveWorkQueueBitset != 0L;
++ this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) {
++
++ final WorkQueue queue = this.levelRemoveWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ final byte level = queue.queuedLevels.removeFirstByte();
++
++ final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level);
++ if (currentLevel == 0) {
++ // something else removed
++ continue;
++ }
++
++ if (currentLevel > level) {
++ // something higher propagated here or we hit the propagation of another source
++ // in the second case we need to re-propagate because we could have just clobbered another source's
++ // propagation
++ this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking
++ continue;
++ }
++
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0);
++ }
++
++ final byte source = this.sources.get(coordinate);
++ if (source != 0) {
++ // must re-propagate source later
++ this.addToIncreaseWorkQueue(coordinate, source);
++ }
++
++ if (level == 0) {
++ // can't propagate -1 to neighbours
++ // we have to check neighbours for removing 1 just in case the neighbour is 2
++ continue;
++ }
++
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = (int)coordinate;
++ final int z = (int)(coordinate >>> 32);
++
++ for (int dx = -1; dx <= 1; ++dx) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ if ((dx | dz) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
++
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = MCUtil.getCoordinateKey(x + dx, z + dz);
++ this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
++ }
++ }
++ }
++
++ // propagate sources we clobbered in the process
++ this.propagateIncreases();
++ }
++
++ protected static final class LevelMap extends Long2ByteOpenHashMap {
++ public LevelMap() {
++ super();
++ }
++
++ public LevelMap(final int expected, final float loadFactor) {
++ super(expected, loadFactor);
++ }
++
++ // copied from superclass
++ private int find(final long k) {
++ if (k == 0L) {
++ return this.containsNullKey ? this.n : -(this.n + 1);
++ } else {
++ final long[] key = this.key;
++ long curr;
++ int pos;
++ if ((curr = key[pos = (int)HashCommon.mix(k) & this.mask]) == 0L) {
++ return -(pos + 1);
++ } else if (k == curr) {
++ return pos;
++ } else {
++ while((curr = key[pos = pos + 1 & this.mask]) != 0L) {
++ if (k == curr) {
++ return pos;
++ }
++ }
++
++ return -(pos + 1);
++ }
++ }
++ }
++
++ // copied from superclass
++ private void insert(final int pos, final long k, final byte v) {
++ if (pos == this.n) {
++ this.containsNullKey = true;
++ }
++
++ this.key[pos] = k;
++ this.value[pos] = v;
++ if (this.size++ >= this.maxFill) {
++ this.rehash(HashCommon.arraySize(this.size + 1, this.f));
++ }
++ }
++
++ // copied from superclass
++ public byte putIfGreater(final long key, final byte value) {
++ final int pos = this.find(key);
++ if (pos < 0) {
++ if (this.defRetValue < value) {
++ this.insert(-pos - 1, key, value);
++ }
++ return this.defRetValue;
++ } else {
++ final byte curr = this.value[pos];
++ if (value > curr) {
++ this.value[pos] = value;
++ return curr;
++ }
++ return curr;
++ }
++ }
++
++ // copied from superclass
++ private void removeEntry(final int pos) {
++ --this.size;
++ this.shiftKeys(pos);
++ if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) {
++ this.rehash(this.n / 2);
++ }
++ }
++
++ // copied from superclass
++ private void removeNullEntry() {
++ this.containsNullKey = false;
++ --this.size;
++ if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) {
++ this.rehash(this.n / 2);
++ }
++ }
++
++ // copied from superclass
++ public byte removeIfGreaterOrEqual(final long key, final byte value) {
++ if (key == 0L) {
++ if (!this.containsNullKey) {
++ return this.defRetValue;
++ }
++ final byte current = this.value[this.n];
++ if (value >= current) {
++ this.removeNullEntry();
++ return current;
++ }
++ return current;
++ } else {
++ long[] keys = this.key;
++ byte[] values = this.value;
++ long curr;
++ int pos;
++ if ((curr = keys[pos = (int)HashCommon.mix(key) & this.mask]) == 0L) {
++ return this.defRetValue;
++ } else if (key == curr) {
++ final byte current = values[pos];
++ if (value >= current) {
++ this.removeEntry(pos);
++ return current;
++ }
++ return current;
++ } else {
++ while((curr = keys[pos = pos + 1 & this.mask]) != 0L) {
++ if (key == curr) {
++ final byte current = values[pos];
++ if (value >= current) {
++ this.removeEntry(pos);
++ return current;
++ }
++ return current;
++ }
++ }
++
++ return this.defRetValue;
++ }
++ }
++ }
++ }
++
++ protected static final class WorkQueue {
++
++ public final NoResizeLongArrayFIFODeque queuedCoordinates = new NoResizeLongArrayFIFODeque();
++ public final NoResizeByteArrayFIFODeque queuedLevels = new NoResizeByteArrayFIFODeque();
++
++ }
++
++ protected static final class NoResizeLongArrayFIFODeque extends LongArrayFIFOQueue {
++
++ /**
++ * Assumes non-empty. If empty, undefined behaviour.
++ */
++ public long removeFirstLong() {
++ // copied from superclass
++ long t = this.array[this.start];
++ if (++this.start == this.length) {
++ this.start = 0;
++ }
++
++ return t;
++ }
++ }
++
++ protected static final class NoResizeByteArrayFIFODeque extends ByteArrayFIFOQueue {
++
++ /**
++ * Assumes non-empty. If empty, undefined behaviour.
++ */
++ public byte removeFirstByte() {
++ // copied from superclass
++ byte t = this.array[this.start];
++ if (++this.start == this.length) {
++ this.start = 0;
++ }
++
++ return t;
++ }
++ }
++}
+diff --git a/src/main/java/com/tuinity/tuinity/util/table/ZeroCollidingReferenceStateTable.java b/src/main/java/com/tuinity/tuinity/util/table/ZeroCollidingReferenceStateTable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..3bd1bfab37c8a3b981c86ff09941590f028d24bc
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/util/table/ZeroCollidingReferenceStateTable.java
+@@ -0,0 +1,160 @@
++package com.tuinity.tuinity.util.table;
++
++import com.google.common.collect.Table;
++import net.minecraft.world.level.block.state.StateHolder;
++import net.minecraft.world.level.block.state.properties.Property;
++import java.util.Collection;
++import java.util.HashSet;
++import java.util.Map;
++import java.util.Set;
++
++public final class ZeroCollidingReferenceStateTable {
++
++ // upper 32 bits: starting index
++ // lower 32 bits: bitset for contained ids
++ protected final long[] this_index_table;
++ protected final Comparable>[] this_table;
++ protected final StateHolder, ?> this_state;
++
++ protected long[] index_table;
++ protected StateHolder, ?>[][] value_table;
++
++ public ZeroCollidingReferenceStateTable(final StateHolder, ?> state, final Map, Comparable>> this_map) {
++ this.this_state = state;
++ this.this_index_table = this.create_table(this_map.keySet());
++
++ int max_id = -1;
++ for (final Property> property : this_map.keySet()) {
++ final int id = lookup_vindex(property, this.this_index_table);
++ if (id > max_id) {
++ max_id = id;
++ }
++ }
++
++ this.this_table = new Comparable[max_id + 1];
++ for (final Map.Entry, Comparable>> entry : this_map.entrySet()) {
++ this.this_table[lookup_vindex(entry.getKey(), this.this_index_table)] = entry.getValue();
++ }
++ }
++
++ public void loadInTable(final Table, Comparable>, StateHolder, ?>> table,
++ final Map, Comparable>> this_map) {
++ final Set> combined = new HashSet<>(table.rowKeySet());
++ combined.addAll(this_map.keySet());
++
++ this.index_table = this.create_table(combined);
++
++ int max_id = -1;
++ for (final Property> property : combined) {
++ final int id = lookup_vindex(property, this.index_table);
++ if (id > max_id) {
++ max_id = id;
++ }
++ }
++
++ this.value_table = new StateHolder[max_id + 1][];
++
++ final Map, Map, StateHolder, ?>>> map = table.rowMap();
++ for (final Property> property : map.keySet()) {
++ final Map, StateHolder, ?>> propertyMap = map.get(property);
++
++ final int id = lookup_vindex(property, this.index_table);
++ final StateHolder, ?>[] states = this.value_table[id] = new StateHolder[property.getPossibleValues().size()];
++
++ for (final Map.Entry, StateHolder, ?>> entry : propertyMap.entrySet()) {
++ if (entry.getValue() == null) {
++ // TODO what
++ continue;
++ }
++
++ states[((Property)property).getIdFor(entry.getKey())] = entry.getValue();
++ }
++ }
++
++
++ for (final Map.Entry, Comparable>> entry : this_map.entrySet()) {
++ final Property> property = entry.getKey();
++ final int index = lookup_vindex(property, this.index_table);
++
++ if (this.value_table[index] == null) {
++ this.value_table[index] = new StateHolder[property.getPossibleValues().size()];
++ }
++
++ this.value_table[index][((Property)property).getIdFor(entry.getValue())] = this.this_state;
++ }
++ }
++
++
++ protected long[] create_table(final Collection> collection) {
++ int max_id = -1;
++ for (final Property> property : collection) {
++ final int id = property.getId();
++ if (id > max_id) {
++ max_id = id;
++ }
++ }
++
++ final long[] ret = new long[((max_id + 1) + 31) >>> 5]; // ceil((max_id + 1) / 32)
++
++ for (final Property> property : collection) {
++ final int id = property.getId();
++
++ ret[id >>> 5] |= (1L << (id & 31));
++ }
++
++ int total = 0;
++ for (int i = 1, len = ret.length; i < len; ++i) {
++ ret[i] |= (long)(total += Long.bitCount(ret[i - 1] & 0xFFFFFFFFL)) << 32;
++ }
++
++ return ret;
++ }
++
++ public Comparable> get(final Property> state) {
++ final Comparable>[] table = this.this_table;
++ final int index = lookup_vindex(state, this.this_index_table);
++
++ if (index < 0 || index >= table.length) {
++ return null;
++ }
++ return table[index];
++ }
++
++ public StateHolder, ?> get(final Property> property, final Comparable> with) {
++ final int withId = ((Property)property).getIdFor(with);
++ if (withId < 0) {
++ return null;
++ }
++
++ final int index = lookup_vindex(property, this.index_table);
++ final StateHolder, ?>[][] table = this.value_table;
++ if (index < 0 || index >= table.length) {
++ return null;
++ }
++
++ final StateHolder, ?>[] values = table[index];
++
++ if (withId >= values.length) {
++ return null;
++ }
++
++ return values[withId];
++ }
++
++ protected static int lookup_vindex(final Property> property, final long[] index_table) {
++ final int id = property.getId();
++ final long bitset_mask = (1L << (id & 31));
++ final long lower_mask = bitset_mask - 1;
++ final int index = id >>> 5;
++ if (index >= index_table.length) {
++ return -1;
++ }
++ final long index_value = index_table[index];
++ final long contains_check = ((index_value & bitset_mask) - 1) >> (Long.SIZE - 1); // -1L if doesn't contain
++
++ // index = total bits set in lower table values (upper 32 bits of index_value) plus total bits set in lower indices below id
++ // contains_check is 0 if the bitset had id set, else it's -1: so index is unaffected if contains_check == 0,
++ // otherwise it comes out as -1.
++ return (int)(((index_value >>> 32) + Long.bitCount(index_value & lower_mask)) | contains_check);
++ }
++}
+diff --git a/src/main/java/com/tuinity/tuinity/voxel/AABBVoxelShape.java b/src/main/java/com/tuinity/tuinity/voxel/AABBVoxelShape.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..370c070dedd169fe85ad2cb488dae7aa1dcf28fc
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/voxel/AABBVoxelShape.java
+@@ -0,0 +1,200 @@
++package com.tuinity.tuinity.voxel;
++
++import com.tuinity.tuinity.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/com/tuinity/tuinity/world/ChunkEntitySlices.java b/src/main/java/com/tuinity/tuinity/world/ChunkEntitySlices.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ad711b6c0628a9cd93ff0d5484769807e5e5b9c0
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/world/ChunkEntitySlices.java
+@@ -0,0 +1,500 @@
++package com.tuinity.tuinity.world;
++
++import com.destroystokyo.paper.util.maplist.EntityList;
++import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
++import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
++import net.minecraft.server.level.ChunkHolder;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.util.Mth;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.entity.EntityType;
++import net.minecraft.world.entity.boss.EnderDragonPart;
++import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
++import net.minecraft.world.phys.AABB;
++import java.util.Arrays;
++import java.util.Iterator;
++import java.util.List;
++import java.util.function.Predicate;
++
++public final class ChunkEntitySlices {
++
++ protected final int minSection;
++ protected final int maxSection;
++ protected final int chunkX;
++ protected final int chunkZ;
++ protected final ServerLevel world;
++
++ protected final EntityCollectionBySection allEntities;
++ protected final EntityCollectionBySection hardCollidingEntities;
++ protected final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass;
++ protected final EntityList entities = new EntityList();
++
++ public ChunkHolder.FullChunkStatus status;
++
++ // TODO implement container search optimisations
++
++ public ChunkEntitySlices(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkHolder.FullChunkStatus status,
++ final int minSection, final int maxSection) { // inclusive, inclusive
++ this.minSection = minSection;
++ this.maxSection = maxSection;
++ this.chunkX = chunkX;
++ this.chunkZ = chunkZ;
++ this.world = world;
++
++ this.allEntities = new EntityCollectionBySection(this);
++ this.hardCollidingEntities = new EntityCollectionBySection(this);
++ this.entitiesByClass = new Reference2ObjectOpenHashMap<>();
++
++ this.status = status;
++ }
++
++ // Tuinity start - optimise CraftChunk#getEntities
++ public org.bukkit.entity.Entity[] getChunkEntities() {
++ List ret = new java.util.ArrayList<>();
++ final Entity[] entities = this.entities.getRawData();
++ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) {
++ final Entity entity = entities[i];
++ if (entity == null) {
++ continue;
++ }
++ final org.bukkit.entity.Entity bukkit = entity.getBukkitEntity();
++ if (bukkit != null && bukkit.isValid()) {
++ ret.add(bukkit);
++ }
++ }
++
++ return ret.toArray(new org.bukkit.entity.Entity[0]);
++ }
++ // Tuinity end - optimise CraftChunk#getEntities
++
++ public boolean isEmpty() {
++ return this.entities.size() == 0;
++ }
++
++ private void updateTicketLevels() {
++ final Entity[] entities = this.entities.getRawData();
++ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) {
++ final Entity entity = entities[i];
++ entity.chunkStatus = this.status;
++ }
++ }
++
++ public synchronized void updateStatus(final ChunkHolder.FullChunkStatus status) {
++ this.status = status;
++ this.updateTicketLevels();
++ }
++
++ public synchronized void addEntity(final Entity entity, final int chunkSection) {
++ if (!this.entities.add(entity)) {
++ return;
++ }
++ entity.chunkStatus = this.status;
++ final int sectionIndex = chunkSection - this.minSection;
++
++ this.allEntities.addEntity(entity, sectionIndex);
++
++ if (entity.hardCollides()) {
++ this.hardCollidingEntities.addEntity(entity, sectionIndex);
++ }
++
++ for (final Iterator, EntityCollectionBySection>> iterator =
++ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
++ final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next();
++
++ if (entry.getKey().isInstance(entity)) {
++ entry.getValue().addEntity(entity, sectionIndex);
++ }
++ }
++ }
++
++ public synchronized void removeEntity(final Entity entity, final int chunkSection) {
++ if (!this.entities.remove(entity)) {
++ return;
++ }
++ entity.chunkStatus = ChunkHolder.FullChunkStatus.INACCESSIBLE;
++ final int sectionIndex = chunkSection - this.minSection;
++
++ this.allEntities.removeEntity(entity, sectionIndex);
++
++ if (entity.hardCollides()) {
++ this.hardCollidingEntities.removeEntity(entity, sectionIndex);
++ }
++
++ for (final Iterator, EntityCollectionBySection>> iterator =
++ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
++ final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next();
++
++ if (entry.getKey().isInstance(entity)) {
++ entry.getValue().removeEntity(entity, sectionIndex);
++ }
++ }
++ }
++
++ public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate super Entity> predicate) {
++ this.hardCollidingEntities.getEntities(except, box, into, predicate);
++ }
++
++ public void getEntities(final Entity except, final AABB box, final List into, final Predicate super Entity> predicate) {
++ this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate);
++ }
++
++ public void getEntities(final EntityType> type, final AABB box, final List super T> into,
++ final Predicate super T> predicate) {
++ this.allEntities.getEntities(type, box, (List)into, (Predicate)predicate);
++ }
++
++ protected EntityCollectionBySection initClass(final Class extends Entity> clazz) {
++ final EntityCollectionBySection ret = new EntityCollectionBySection(this);
++
++ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) {
++ final BasicEntityList sectionEntities = this.allEntities.entitiesBySection[sectionIndex];
++ if (sectionEntities == null) {
++ continue;
++ }
++
++ final Entity[] storage = sectionEntities.storage;
++
++ for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (clazz.isInstance(entity)) {
++ ret.addEntity(entity, sectionIndex);
++ }
++ }
++ }
++
++ return ret;
++ }
++
++ public void getEntities(final Class extends T> clazz, final Entity except, final AABB box, final List super T> into,
++ final Predicate super T> predicate) {
++ EntityCollectionBySection collection = this.entitiesByClass.get(clazz);
++ if (collection != null) {
++ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate);
++ } else {
++ synchronized (this) {
++ this.entitiesByClass.putIfAbsent(clazz, collection = this.initClass(clazz));
++ }
++ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate);
++ }
++ }
++
++ public synchronized void updateEntity(final Entity entity) {
++ /*// TODO
++ if (prev aabb != entity.getBoundingBox()) {
++ this.entityMap.delete(entity, prev aabb);
++ this.entityMap.insert(entity, prev aabb = entity.getBoundingBox());
++ }*/
++ }
++
++ protected static final class BasicEntityList {
++
++ protected static final Entity[] EMPTY = new Entity[0];
++ protected static final int DEFAULT_CAPACITY = 4;
++
++ protected E[] storage;
++ protected int size;
++
++ public BasicEntityList() {
++ this(0);
++ }
++
++ public BasicEntityList(final int cap) {
++ this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]);
++ }
++
++ public boolean isEmpty() {
++ return this.size == 0;
++ }
++
++ public int size() {
++ return this.size;
++ }
++
++ private void resize() {
++ if (this.storage == EMPTY) {
++ this.storage = (E[])new Entity[DEFAULT_CAPACITY];
++ } else {
++ this.storage = Arrays.copyOf(this.storage, this.storage.length * 2);
++ }
++ }
++
++ public void add(final E entity) {
++ final int idx = this.size++;
++ if (idx >= this.storage.length) {
++ this.resize();
++ this.storage[idx] = entity;
++ } else {
++ this.storage[idx] = entity;
++ }
++ }
++
++ public int indexOf(final E entity) {
++ final E[] storage = this.storage;
++
++ for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) {
++ if (storage[i] == entity) {
++ return i;
++ }
++ }
++
++ return -1;
++ }
++
++ public boolean remove(final E entity) {
++ final int idx = this.indexOf(entity);
++ if (idx == -1) {
++ return false;
++ }
++
++ final int size = --this.size;
++ final E[] storage = this.storage;
++ if (idx != size) {
++ System.arraycopy(storage, idx + 1, storage, idx, size - idx);
++ }
++
++ storage[size] = null;
++
++ return true;
++ }
++
++ public boolean has(final E entity) {
++ return this.indexOf(entity) != -1;
++ }
++ }
++
++ protected static final class EntityCollectionBySection {
++
++ protected final ChunkEntitySlices manager;
++ protected final long[] nonEmptyBitset;
++ protected final BasicEntityList[] entitiesBySection;
++ protected int count;
++
++ public EntityCollectionBySection(final ChunkEntitySlices manager) {
++ this.manager = manager;
++
++ final int sectionCount = manager.maxSection - manager.minSection + 1;
++
++ this.nonEmptyBitset = new long[(sectionCount + (Long.SIZE - 1)) >>> 6]; // (sectionCount + (Long.SIZE - 1)) / Long.SIZE
++ this.entitiesBySection = new BasicEntityList[sectionCount];
++ }
++
++ public void addEntity(final Entity entity, final int sectionIndex) {
++ BasicEntityList list = this.entitiesBySection[sectionIndex];
++
++ if (list != null && list.has(entity)) {
++ return;
++ }
++
++ if (list == null) {
++ this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>();
++ this.nonEmptyBitset[sectionIndex >>> 6] |= (1L << (sectionIndex & (Long.SIZE - 1)));
++ }
++
++ list.add(entity);
++ ++this.count;
++ }
++
++ public void removeEntity(final Entity entity, final int sectionIndex) {
++ final BasicEntityList list = this.entitiesBySection[sectionIndex];
++
++ if (list == null || !list.remove(entity)) {
++ return;
++ }
++
++ --this.count;
++
++ if (list.isEmpty()) {
++ this.entitiesBySection[sectionIndex] = null;
++ this.nonEmptyBitset[sectionIndex >>> 6] ^= (1L << (sectionIndex & (Long.SIZE - 1)));
++ }
++ }
++
++ public void getEntities(final Entity except, final AABB box, final List into, final Predicate super Entity> predicate) {
++ if (this.count == 0) {
++ return;
++ }
++
++ final int minSection = this.manager.minSection;
++ final int maxSection = this.manager.maxSection;
++
++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
++
++ // TODO use the bitset
++
++ final BasicEntityList[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate != null && !predicate.test(entity)) {
++ continue;
++ }
++
++ into.add(entity);
++ }
++ }
++ }
++
++ public void getEntitiesWithEnderDragonParts(final Entity except, final AABB box, final List into,
++ final Predicate super Entity> predicate) {
++ if (this.count == 0) {
++ return;
++ }
++
++ final int minSection = this.manager.minSection;
++ final int maxSection = this.manager.maxSection;
++
++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
++
++ // TODO use the bitset
++
++ final BasicEntityList[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate == null || predicate.test(entity)) {
++ into.add(entity);
++ } // else: continue to test the ender dragon parts
++
++ if (entity instanceof EnderDragon) {
++ for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) {
++ if (part == except || !part.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate != null && !predicate.test(part)) {
++ continue;
++ }
++
++ into.add(part);
++ }
++ }
++ }
++ }
++ }
++
++ public void getEntitiesWithEnderDragonParts(final Entity except, final Class> clazz, final AABB box, final List into,
++ final Predicate super Entity> predicate) {
++ if (this.count == 0) {
++ return;
++ }
++
++ final int minSection = this.manager.minSection;
++ final int maxSection = this.manager.maxSection;
++
++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
++
++ // TODO use the bitset
++
++ final BasicEntityList[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate == null || predicate.test(entity)) {
++ into.add(entity);
++ } // else: continue to test the ender dragon parts
++
++ if (entity instanceof EnderDragon) {
++ for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) {
++ if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) {
++ continue;
++ }
++
++ if (predicate != null && !predicate.test(part)) {
++ continue;
++ }
++
++ into.add(part);
++ }
++ }
++ }
++ }
++ }
++
++ public void getEntities(final EntityType> type, final AABB box, final List super T> into,
++ final Predicate super T> predicate) {
++ if (this.count == 0) {
++ return;
++ }
++
++ final int minSection = this.manager.minSection;
++ final int maxSection = this.manager.maxSection;
++
++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
++
++ // TODO use the bitset
++
++ final BasicEntityList[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || (type != null && entity.getType() != type) || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate != null && !predicate.test((T)entity)) {
++ continue;
++ }
++
++ into.add((T)entity);
++ }
++ }
++ }
++ }
++}
+diff --git a/src/main/java/com/tuinity/tuinity/world/EntitySliceManager.java b/src/main/java/com/tuinity/tuinity/world/EntitySliceManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f188ff6b08abddd06a3120fb15825e0f71196893
+--- /dev/null
++++ b/src/main/java/com/tuinity/tuinity/world/EntitySliceManager.java
+@@ -0,0 +1,391 @@
++package com.tuinity.tuinity.world;
++
++import com.tuinity.tuinity.util.CoordinateUtils;
++import com.tuinity.tuinity.util.WorldUtil;
++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
++import net.minecraft.core.BlockPos;
++import net.minecraft.server.level.ChunkHolder;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.util.Mth;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.entity.EntityType;
++import net.minecraft.world.phys.AABB;
++import java.util.List;
++import java.util.concurrent.locks.StampedLock;
++import java.util.function.Predicate;
++
++public final class EntitySliceManager {
++
++ protected static final int REGION_SHIFT = 5;
++ protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1;
++ protected static final int REGION_SIZE = 1 << REGION_SHIFT;
++
++ public final ServerLevel world;
++
++ private final StampedLock stateLock = new StampedLock();
++ protected final Long2ObjectOpenHashMap regions = new Long2ObjectOpenHashMap<>(64, 0.7f);
++
++ private final int minSection; // inclusive
++ private final int maxSection; // inclusive
++
++ protected final Long2ObjectOpenHashMap statusMap = new Long2ObjectOpenHashMap<>();
++ {
++ this.statusMap.defaultReturnValue(ChunkHolder.FullChunkStatus.INACCESSIBLE);
++ }
++
++ public EntitySliceManager(final ServerLevel world) {
++ this.world = world;
++ this.minSection = WorldUtil.getMinSection(world);
++ this.maxSection = WorldUtil.getMaxSection(world);
++ }
++
++ public void chunkStatusChange(final int x, final int z, final ChunkHolder.FullChunkStatus newStatus) {
++ if (newStatus == ChunkHolder.FullChunkStatus.INACCESSIBLE) {
++ this.statusMap.remove(CoordinateUtils.getChunkKey(x, z));
++ } else {
++ this.statusMap.put(CoordinateUtils.getChunkKey(x, z), newStatus);
++ final ChunkEntitySlices slices = this.getChunk(x, z);
++ if (slices != null) {
++ slices.updateStatus(newStatus);
++ }
++ }
++ }
++
++ public synchronized void addEntity(final Entity entity) {
++ final BlockPos pos = entity.blockPosition();
++ final int sectionX = pos.getX() >> 4;
++ final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection);
++ final int sectionZ = pos.getZ() >> 4;
++ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ);
++ slices.addEntity(entity, sectionY);
++
++ entity.sectionX = sectionX;
++ entity.sectionY = sectionY;
++ entity.sectionZ = sectionZ;
++ }
++
++ public synchronized void removeEntity(final Entity entity) {
++ final ChunkEntitySlices slices = this.getChunk(entity.sectionX, entity.sectionZ);
++ slices.removeEntity(entity, entity.sectionY);
++ if (slices.isEmpty()) {
++ this.removeChunk(entity.sectionX, entity.sectionZ);
++ }
++ }
++
++ public void moveEntity(final Entity entity) {
++ final BlockPos newPos = entity.blockPosition();
++ final int newSectionX = newPos.getX() >> 4;
++ final int newSectionY = Mth.clamp(newPos.getY() >> 4, this.minSection, this.maxSection);
++ final int newSectionZ = newPos.getZ() >> 4;
++
++ if (newSectionX == entity.sectionX && newSectionY == entity.sectionY && newSectionZ == entity.sectionZ) {
++ return;
++ }
++
++ synchronized (this) {
++ // are we changing chunks?
++ if (newSectionX != entity.sectionX || newSectionZ != entity.sectionZ) {
++ final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ);
++ final ChunkEntitySlices old = this.getChunk(entity.sectionX, entity.sectionZ);
++ synchronized (old) {
++ old.removeEntity(entity, entity.sectionY);
++ if (old.isEmpty()) {
++ this.removeChunk(entity.sectionX, entity.sectionZ);
++ }
++ }
++
++ synchronized (slices) {
++ slices.addEntity(entity, newSectionY);
++
++ entity.sectionX = newSectionX;
++ entity.sectionY = newSectionY;
++ entity.sectionZ = newSectionZ;
++ }
++ } else {
++ final ChunkEntitySlices slices = this.getChunk(newSectionX, newSectionZ);
++ // same chunk
++ synchronized (slices) {
++ slices.removeEntity(entity, entity.sectionY);
++ slices.addEntity(entity, newSectionY);
++ }
++ entity.sectionY = newSectionY;
++ }
++ }
++
++ }
++
++ public void getEntities(final Entity except, final AABB box, final List into, final Predicate super Entity> predicate) {
++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++
++ final int minRegionX = minChunkX >> REGION_SHIFT;
++ final int minRegionZ = minChunkZ >> REGION_SHIFT;
++ final int maxRegionX = maxChunkX >> REGION_SHIFT;
++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++
++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++
++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++
++ if (region == null) {
++ continue;
++ }
++
++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ for (int currX = minX; currX <= maxX; ++currX) {
++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
++ if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) {
++ continue;
++ }
++
++ chunk.getEntities(except, box, into, predicate);
++ }
++ }
++ }
++ }
++ }
++
++ public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate super Entity> predicate) {
++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++
++ final int minRegionX = minChunkX >> REGION_SHIFT;
++ final int minRegionZ = minChunkZ >> REGION_SHIFT;
++ final int maxRegionX = maxChunkX >> REGION_SHIFT;
++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++
++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++
++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++
++ if (region == null) {
++ continue;
++ }
++
++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ for (int currX = minX; currX <= maxX; ++currX) {
++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
++ if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) {
++ continue;
++ }
++
++ chunk.getHardCollidingEntities(except, box, into, predicate);
++ }
++ }
++ }
++ }
++ }
++
++ public void getEntities(final EntityType> type, final AABB box, final List super T> into,
++ final Predicate super T> predicate) {
++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++
++ final int minRegionX = minChunkX >> REGION_SHIFT;
++ final int minRegionZ = minChunkZ >> REGION_SHIFT;
++ final int maxRegionX = maxChunkX >> REGION_SHIFT;
++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++
++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++
++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++
++ if (region == null) {
++ continue;
++ }
++
++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ for (int currX = minX; currX <= maxX; ++currX) {
++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
++ if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) {
++ continue;
++ }
++
++ chunk.getEntities(type, box, (List)into, (Predicate)predicate);
++ }
++ }
++ }
++ }
++ }
++
++ public void getEntities(final Class extends T> clazz, final Entity except, final AABB box, final List super T> into,
++ final Predicate super T> predicate) {
++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++
++ final int minRegionX = minChunkX >> REGION_SHIFT;
++ final int minRegionZ = minChunkZ >> REGION_SHIFT;
++ final int maxRegionX = maxChunkX >> REGION_SHIFT;
++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++
++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++
++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++
++ if (region == null) {
++ continue;
++ }
++
++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ for (int currX = minX; currX <= maxX; ++currX) {
++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
++ if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) {
++ continue;
++ }
++
++ chunk.getEntities(clazz, except, box, into, predicate);
++ }
++ }
++ }
++ }
++ }
++
++ public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) {
++ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
++ if (region == null) {
++ return null;
++ }
++
++ return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT));
++ }
++
++ public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) {
++ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
++ ChunkEntitySlices ret;
++ if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) {
++ ret = new ChunkEntitySlices(this.world, chunkX, chunkZ, this.statusMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)),
++ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world));
++
++ this.addChunk(chunkX, chunkZ, ret);
++
++ return ret;
++ }
++
++ return ret;
++ }
++
++ public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) {
++ final long key = CoordinateUtils.getChunkKey(regionX, regionZ);
++ final long attempt = this.stateLock.tryOptimisticRead();
++ if (attempt != 0L) {
++ try {
++ final ChunkSlicesRegion ret = this.regions.get(key);
++
++ if (this.stateLock.validate(attempt)) {
++ return ret;
++ }
++ } catch (final Error error) {
++ throw error;
++ } catch (final Throwable thr) {
++ // ignore
++ }
++ }
++
++ this.stateLock.readLock();
++ try {
++ return this.regions.get(key);
++ } finally {
++ this.stateLock.tryUnlockRead();
++ }
++ }
++
++ public synchronized void removeChunk(final int chunkX, final int chunkZ) {
++ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
++ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT);
++
++ final ChunkSlicesRegion region = this.regions.get(key);
++ final int remaining = region.remove(relIndex);
++
++ if (remaining == 0) {
++ this.stateLock.writeLock();
++ try {
++ this.regions.remove(key);
++ } finally {
++ this.stateLock.tryUnlockWrite();
++ }
++ }
++ }
++
++ public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) {
++ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
++ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT);
++
++ ChunkSlicesRegion region = this.regions.get(key);
++ if (region != null) {
++ region.add(relIndex, slices);
++ } else {
++ region = new ChunkSlicesRegion();
++ region.add(relIndex, slices);
++ this.stateLock.writeLock();
++ try {
++ this.regions.put(key, region);
++ } finally {
++ this.stateLock.tryUnlockWrite();
++ }
++ }
++ }
++
++ public static final class ChunkSlicesRegion {
++
++ protected final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE];
++ protected int sliceCount;
++
++ public ChunkEntitySlices get(final int index) {
++ return this.slices[index];
++ }
++
++ public int remove(final int index) {
++ final ChunkEntitySlices slices = this.slices[index];
++ if (slices == null) {
++ throw new IllegalStateException();
++ }
++
++ this.slices[index] = null;
++
++ return --this.sliceCount;
++ }
++
++ public void add(final int index, final ChunkEntitySlices slices) {
++ final ChunkEntitySlices curr = this.slices[index];
++ if (curr != null) {
++ throw new IllegalStateException();
++ }
++
++ this.slices[index] = slices;
++
++ ++this.sliceCount;
++ }
++ }
++}
+diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java
+index b70aa66732fb5e957aed0901f4c76358b2c56f8e..b01d7da333bac7820e42b6f645634a15ef88ae4f 100644
+--- a/src/main/java/net/minecraft/core/BlockPos.java
++++ b/src/main/java/net/minecraft/core/BlockPos.java
+@@ -478,9 +478,9 @@ public class BlockPos extends Vec3i {
+ }
+
+ public BlockPos.MutableBlockPos set(int x, int y, int z) {
+- this.setX(x);
+- this.setY(y);
+- this.setZ(z);
++ this.x = x; // Tuinity - force inline
++ this.y = y; // Tuinity - force inline
++ this.z = z; // Tuinity - force inline
+ return this;
+ }
+
+@@ -544,19 +544,19 @@ public class BlockPos extends Vec3i {
+ // Paper start - comment out useless overrides @Override - TODO figure out why this is suddenly important to keep
+ @Override
+ public BlockPos.MutableBlockPos setX(int i) {
+- super.setX(i);
++ this.x = i; // Tuinity
+ return this;
+ }
+
+ @Override
+ public BlockPos.MutableBlockPos setY(int i) {
+- super.setY(i);
++ this.y = i; // Tuinity
+ return this;
+ }
+
+ @Override
+ public BlockPos.MutableBlockPos setZ(int i) {
+- super.setZ(i);
++ this.z = i; // Tuinity
+ return this;
+ }
+ // Paper end
+diff --git a/src/main/java/net/minecraft/core/Vec3i.java b/src/main/java/net/minecraft/core/Vec3i.java
+index 5e09890ba2fe326503a49b2dbec09845f5c8c5eb..3ad3652f8074de10222fb01c50548b4312103cc3 100644
+--- a/src/main/java/net/minecraft/core/Vec3i.java
++++ b/src/main/java/net/minecraft/core/Vec3i.java
+@@ -17,9 +17,9 @@ public class Vec3i implements Comparable {
+ return IntStream.of(vec3i.getX(), vec3i.getY(), vec3i.getZ());
+ });
+ public static final Vec3i ZERO = new Vec3i(0, 0, 0);
+- private int x;
+- private int y;
+- private int z;
++ protected int x; // Tuinity - protected
++ protected int y; // Tuinity - protected
++ protected int z; // Tuinity - protected
+
+ // Paper start
+ public boolean isValidLocation(net.minecraft.world.level.LevelHeightAccessor levelHeightAccessor) {
+@@ -84,17 +84,17 @@ public class Vec3i implements Comparable {
+ return this.z;
+ }
+
+- public Vec3i setX(int x) {
++ protected Vec3i setX(int x) { // Tuinity - not needed here - Also revert the decision to expose set on an _immutable_ type
+ this.x = x;
+ return this;
+ }
+
+- public Vec3i setY(int y) {
++ protected Vec3i setY(int y) { // Tuinity - not needed here - Also revert the decision to expose set on an _immutable_ type
+ this.y = y;
+ return this;
+ }
+
+- public Vec3i setZ(int z) {
++ protected Vec3i setZ(int z) { // Tuinity - not needed here - Also revert the decision to expose set on an _immutable_ type
+ this.z = z;
+ return this;
+ }
+diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java
+index 9d09ec3b127e3440bef6b248578dec109407f9ff..4b6bbdbdf581b8a751c08708ee24e8b2a85534a0 100644
+--- a/src/main/java/net/minecraft/network/Connection.java
++++ b/src/main/java/net/minecraft/network/Connection.java
+@@ -49,6 +49,8 @@ import org.apache.logging.log4j.Logger;
+ import org.apache.logging.log4j.Marker;
+ import org.apache.logging.log4j.MarkerManager;
+
++
++import io.netty.util.concurrent.AbstractEventExecutor; // Tuinity
+ public class Connection extends SimpleChannelInboundHandler> {
+
+ private static final float AVERAGE_PACKETS_SMOOTHING = 0.75F;
+@@ -93,6 +95,77 @@ public class Connection extends SimpleChannelInboundHandler> {
+ public boolean queueImmunity = false;
+ public ConnectionProtocol protocol;
+ // Paper end
++ // Tuinity 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);
++ }
++ }
++ }
++ }
++ // Tuinity end - add pending task queue
++
++ // Tuinity start - allow controlled flushing
++ volatile boolean canFlush = true;
++ private final java.util.concurrent.atomic.AtomicInteger packetWrites = new java.util.concurrent.atomic.AtomicInteger();
++ private int flushPacketsStart;
++ private final Object flushLock = new Object();
++
++ public void disableAutomaticFlush() {
++ synchronized (this.flushLock) {
++ this.flushPacketsStart = this.packetWrites.get(); // must be volatile and before canFlush = false
++ this.canFlush = false;
++ }
++ }
++
++ public void enableAutomaticFlush() {
++ synchronized (this.flushLock) {
++ this.canFlush = true;
++ if (this.packetWrites.get() != this.flushPacketsStart) { // must be after canFlush = true
++ this.flush(); // only make the flush call if we need to
++ }
++ }
++ }
++
++ private final void flush() {
++ if (this.channel.eventLoop().inEventLoop()) {
++ this.channel.flush();
++ } else {
++ this.channel.eventLoop().execute(() -> {
++ this.channel.flush();
++ });
++ }
++ }
++ // Tuinity end - allow controlled flushing
++ // Tuinity start - packet limiter
++ protected final Object PACKET_LIMIT_LOCK = new Object();
++ protected final com.tuinity.tuinity.util.IntervalledCounter allPacketCounts = com.tuinity.tuinity.config.TuinityConfig.allPacketsLimit != null ? new com.tuinity.tuinity.util.IntervalledCounter(
++ (long)(com.tuinity.tuinity.config.TuinityConfig.allPacketsLimit.packetLimitInterval * 1.0e9)
++ ) : null;
++ protected final java.util.Map>, com.tuinity.tuinity.util.IntervalledCounter> packetSpecificLimits = new java.util.HashMap<>();
++
++ private boolean stopReadingPackets;
++ private void killForPacketSpam() {
++ this.sendPacket(new ClientboundDisconnectPacket(org.bukkit.craftbukkit.util.CraftChatMessage.fromString(com.tuinity.tuinity.config.TuinityConfig.kickMessage, true)[0]), (future) -> {
++ this.disconnect(org.bukkit.craftbukkit.util.CraftChatMessage.fromString(com.tuinity.tuinity.config.TuinityConfig.kickMessage, true)[0]);
++ });
++ this.setReadOnly();
++ this.stopReadingPackets = true;
++ }
++ // Tuinity end - packet limiter
+
+ public Connection(PacketFlow side) {
+ this.receiving = side;
+@@ -173,6 +246,45 @@ public class Connection extends SimpleChannelInboundHandler> {
+
+ protected void channelRead0(ChannelHandlerContext channelhandlercontext, Packet> packet) {
+ if (this.channel.isOpen()) {
++ // Tuinity start - packet limiter
++ if (this.stopReadingPackets) {
++ return;
++ }
++ if (this.allPacketCounts != null ||
++ com.tuinity.tuinity.config.TuinityConfig.packetSpecificLimits.containsKey(packet.getClass())) {
++ long time = System.nanoTime();
++ synchronized (PACKET_LIMIT_LOCK) {
++ if (this.allPacketCounts != null) {
++ this.allPacketCounts.updateAndAdd(1, time);
++ if (this.allPacketCounts.getRate() >= com.tuinity.tuinity.config.TuinityConfig.allPacketsLimit.maxPacketRate) {
++ this.killForPacketSpam();
++ return;
++ }
++ }
++
++ for (Class> check = packet.getClass(); check != Object.class; check = check.getSuperclass()) {
++ com.tuinity.tuinity.config.TuinityConfig.PacketLimit packetSpecificLimit =
++ com.tuinity.tuinity.config.TuinityConfig.packetSpecificLimits.get(check);
++ if (packetSpecificLimit == null) {
++ continue;
++ }
++ com.tuinity.tuinity.util.IntervalledCounter counter = this.packetSpecificLimits.computeIfAbsent((Class)check, (clazz) -> {
++ return new com.tuinity.tuinity.util.IntervalledCounter((long)(packetSpecificLimit.packetLimitInterval * 1.0e9));
++ });
++ counter.updateAndAdd(1, time);
++ if (counter.getRate() >= packetSpecificLimit.maxPacketRate) {
++ switch (packetSpecificLimit.violateAction) {
++ case DROP:
++ return;
++ case KICK:
++ this.killForPacketSpam();
++ return;
++ }
++ }
++ }
++ }
++ }
++ // Tuinity end - packet limiter
+ try {
+ Connection.genericsFtw(packet, this.packetListener);
+ } catch (RunningOnDifferentThreadException cancelledpackethandleexception) {
+@@ -255,7 +367,7 @@ public class Connection extends SimpleChannelInboundHandler> {
+ net.minecraft.server.MCUtil.isMainThread() && packet.isReady() && this.queue.isEmpty() &&
+ (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())
+ ))) {
+- this.sendPacket(packet, callback);
++ this.writePacket(packet, callback, null); // Tuinity
+ return;
+ }
+ // write the packets to the queue, then flush - antixray hooks there already
+@@ -279,6 +391,14 @@ public class Connection extends SimpleChannelInboundHandler> {
+ }
+
+ private void sendPacket(Packet> packet, @Nullable GenericFutureListener extends Future super Void>> callback) {
++ // Tuinity start - add flush parameter
++ this.writePacket(packet, callback, Boolean.TRUE);
++ }
++ private void writePacket(Packet> packet, @Nullable GenericFutureListener extends Future super Void>> callback, Boolean flushConditional) {
++ this.packetWrites.getAndIncrement(); // must be befeore using canFlush
++ boolean effectiveFlush = flushConditional == null ? this.canFlush : flushConditional.booleanValue();
++ final boolean flush = effectiveFlush || packet instanceof net.minecraft.network.protocol.game.ClientboundKeepAlivePacket || packet instanceof ClientboundDisconnectPacket; // no delay for certain packets
++ // Tuinity end - add flush parameter
+ ConnectionProtocol enumprotocol = ConnectionProtocol.getProtocolForPacket(packet);
+ ConnectionProtocol enumprotocol1 = this.getCurrentProtocol();
+
+@@ -289,16 +409,31 @@ public class Connection extends SimpleChannelInboundHandler> {
+ }
+
+ if (this.channel.eventLoop().inEventLoop()) {
+- this.a(packet, callback, enumprotocol, enumprotocol1);
++ this.a(packet, callback, enumprotocol, enumprotocol1, flush); // Tuinity - add flush parameter
+ } else {
++ // Tuinity start - optimise packets that are not flushed
++ // note: since the type is not dynamic here, we need to actually copy the old executor code
++ // into two branches. On conflict, just re-copy - no changes were made inside the executor code.
++ if (!flush) {
++ AbstractEventExecutor.LazyRunnable run = () -> {
++ this.a(packet, callback, enumprotocol, enumprotocol1, flush); // Tuinity - add flush parameter
++ };
++ this.channel.eventLoop().execute(run);
++ } else { // Tuinity end - optimise packets that are not flushed
+ this.channel.eventLoop().execute(() -> {
+- this.a(packet, callback, enumprotocol, enumprotocol1);
++ this.a(packet, callback, enumprotocol, enumprotocol1, flush); // Tuinity - add flush parameter // Tuinity - diff on change
+ });
++ } // Tuinity
+ }
+
+ }
+
+ private void a(Packet> packet, @Nullable GenericFutureListener extends Future super Void>> genericfuturelistener, ConnectionProtocol enumprotocol, ConnectionProtocol enumprotocol1) {
++ // Tuinity start - add flush parameter
++ this.a(packet, genericfuturelistener, enumprotocol, enumprotocol1, true);
++ }
++ private void a(Packet> packet, @Nullable GenericFutureListener extends Future super Void>> genericfuturelistener, ConnectionProtocol enumprotocol, ConnectionProtocol enumprotocol1, boolean flush) {
++ // Tuinity end - add flush parameter
+ if (enumprotocol != enumprotocol1) {
+ this.setProtocol(enumprotocol);
+ }
+@@ -312,7 +447,7 @@ public class Connection extends SimpleChannelInboundHandler> {
+
+ try {
+ // Paper end
+- ChannelFuture channelfuture = this.channel.writeAndFlush(packet);
++ ChannelFuture channelfuture = flush ? this.channel.writeAndFlush(packet) : this.channel.write(packet); // Tuinity - add flush parameter
+
+ if (genericfuturelistener != null) {
+ channelfuture.addListener(genericfuturelistener);
+@@ -353,7 +488,12 @@ public class Connection extends SimpleChannelInboundHandler> {
+ return false;
+ }
+ private boolean processQueue() {
++ try { // Tuinity - add pending task queue
+ if (this.queue.isEmpty()) return true;
++ // Tuinity start - make only one flush call per sendPacketQueue() call
++ final boolean needsFlush = this.canFlush;
++ boolean hasWrotePacket = false;
++ // Tuinity end - make only one flush call per sendPacketQueue() call
+ // If we are on main, we are safe here in that nothing else should be processing queue off main anymore
+ // But if we are not on main due to login/status, the parent is synchronized on packetQueue
+ java.util.Iterator iterator = this.queue.iterator();
+@@ -361,19 +501,31 @@ public class Connection extends SimpleChannelInboundHandler> {
+ PacketHolder queued = iterator.next(); // poll -> peek
+
+ // Fix NPE (Spigot bug caused by handleDisconnection())
+- if (queued == null) {
++ if (false && queued == null) { // Tuinity - diff on change, this logic is redundant: iterator guarantees ret of an element - on change, hook the flush logic here
+ return true;
+ }
+
+ Packet> packet = queued.packet;
+ if (!packet.isReady()) {
++ // Tuinity start - make only one flush call per sendPacketQueue() call
++ if (hasWrotePacket && (needsFlush || this.canFlush)) {
++ this.flush();
++ }
++ // Tuinity end - make only one flush call per sendPacketQueue() call
+ return false;
+ } else {
+ iterator.remove();
+- this.sendPacket(packet, queued.listener);
++ this.writePacket(packet, queued.listener, (!iterator.hasNext() && (needsFlush || this.canFlush)) ? Boolean.TRUE : Boolean.FALSE); // Tuinity - make only one flush call per sendPacketQueue() call
++ hasWrotePacket = true; // Tuinity - make only one flush call per sendPacketQueue() call
+ }
+ }
+ return true;
++ } finally { // Tuinity start - add pending task queue
++ Runnable r;
++ while ((r = this.pendingTasks.poll()) != null) {
++ this.channel.eventLoop().execute(r);
++ }
++ } // Tuinity end - add pending task queue
+ }
+ // Paper end
+
+@@ -396,7 +548,14 @@ public class Connection extends SimpleChannelInboundHandler> {
+ }
+
+ if (this.packetListener instanceof ServerGamePacketListenerImpl) {
++ // Tuinity start - detailed watchdog information
++ net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener);
++ try {
++ // Tuinity end - detailed watchdog information
+ ((ServerGamePacketListenerImpl) this.packetListener).tick();
++ } finally { // Tuinity start - detailed watchdog information
++ net.minecraft.network.protocol.PacketUtils.packetProcessing.pop();
++ } // Tuinity start - detailed watchdog information
+ }
+
+ if (!this.isConnected() && !this.disconnectionHandled) {
+diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java
+index bcf53ec07b8eeec7a88fb67e6fb908362e6f51b0..7265bee436d61d33645fa2d9ed4240529834dbf5 100644
+--- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java
++++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java
+@@ -20,6 +20,24 @@ public class PacketUtils {
+
+ private static final Logger LOGGER = LogManager.getLogger();
+
++ // Tuinity start - detailed watchdog information
++ public static final java.util.concurrent.ConcurrentLinkedDeque packetProcessing = new java.util.concurrent.ConcurrentLinkedDeque<>();
++ static final java.util.concurrent.atomic.AtomicLong totalMainThreadPacketsProcessed = new java.util.concurrent.atomic.AtomicLong();
++
++ public static long getTotalProcessedPackets() {
++ return totalMainThreadPacketsProcessed.get();
++ }
++
++ public static java.util.List getCurrentPacketProcessors() {
++ java.util.List ret = new java.util.ArrayList<>(4);
++ for (PacketListener listener : packetProcessing) {
++ ret.add(listener);
++ }
++
++ return ret;
++ }
++ // Tuinity end - detailed watchdog information
++
+ public PacketUtils() {}
+
+ public static void ensureRunningOnSameThread(Packet packet, T listener, ServerLevel world) throws RunningOnDifferentThreadException {
+@@ -30,6 +48,8 @@ public class PacketUtils {
+ if (!engine.isSameThread()) {
+ Timing timing = MinecraftTimings.getPacketTiming(packet); // Paper - timings
+ engine.execute(() -> {
++ packetProcessing.push(listener); // Tuinity - detailed watchdog information
++ try { // Tuinity - detailed watchdog information
+ if (MinecraftServer.getServer().hasStopped() || (listener instanceof ServerGamePacketListenerImpl && ((ServerGamePacketListenerImpl) listener).processedDisconnect)) return; // CraftBukkit, MC-142590
+ if (listener.getConnection().isConnected()) {
+ try (Timing ignored = timing.startTiming()) { // Paper - timings
+@@ -53,6 +73,12 @@ public class PacketUtils {
+ } else {
+ PacketUtils.LOGGER.debug("Ignoring packet due to disconnection: {}", packet);
+ }
++ // Tuinity start - detailed watchdog information
++ } finally {
++ totalMainThreadPacketsProcessed.getAndIncrement();
++ packetProcessing.pop();
++ }
++ // Tuinity end - detailed watchdog information
+
+ });
+ throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD;
+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 d8be2ad889f46491e50404916fb4ae0de5f42098..5b9ea0af272c5e7a85d2a954a9214bf875bc7e9f 100644
+--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java
++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java
+@@ -32,25 +32,17 @@ public class ClientboundLightUpdatePacket implements Packet {
+- if (remainingSends.get() == 0) {
+- cleaner1.run();
+- cleaner2.run();
+- }
+- }, "Light Packet Release");
+- }
++ // Tuinity - rewrite light engine
+ }
+
+ @Override
+ public boolean hasFinishListener() {
+- return true;
++ return false; // Tuinity - rewrite light engine
+ }
+
+ // Paper end
+@@ -63,8 +55,8 @@ public class ClientboundLightUpdatePacket implements Packet com.mojang.serialization.MapCodec fieldWithFallbacks(com.mojang.serialization.Codec codec, String name, String ...fallback) {
++ return com.mojang.serialization.MapCodec.of(
++ new com.mojang.serialization.codecs.FieldEncoder<>(name, codec),
++ new FieldFallbackDecoder<>(name, java.util.Arrays.asList(fallback), codec),
++ () -> "FieldFallback[" + name + ": " + codec.toString() + "]"
++ );
++ }
++
++ // This is likely a common occurrence, sadly
++ public static final class FieldFallbackDecoder extends com.mojang.serialization.MapDecoder.Implementation {
++ protected final String name;
++ protected final List fallback;
++ private final com.mojang.serialization.Decoder elementCodec;
++
++ public FieldFallbackDecoder(final String name, final List fallback, final com.mojang.serialization.Decoder elementCodec) {
++ this.name = name;
++ this.fallback = fallback;
++ this.elementCodec = elementCodec;
++ }
++
++ @Override
++ public