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 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 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 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 predicate) { ++ this.hardCollidingEntities.getEntities(except, box, into, predicate); ++ } ++ ++ public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate); ++ } ++ ++ public void getEntities(final EntityType type, final AABB box, final List into, ++ final Predicate predicate) { ++ this.allEntities.getEntities(type, box, (List)into, (Predicate)predicate); ++ } ++ ++ protected EntityCollectionBySection initClass(final Class clazz) { ++ final EntityCollectionBySection ret = new EntityCollectionBySection(this); ++ ++ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) { ++ final BasicEntityList sectionEntities = this.allEntities.entitiesBySection[sectionIndex]; ++ if (sectionEntities == null) { ++ continue; ++ } ++ ++ final Entity[] storage = sectionEntities.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (clazz.isInstance(entity)) { ++ ret.addEntity(entity, sectionIndex); ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, ++ final Predicate predicate) { ++ EntityCollectionBySection collection = this.entitiesByClass.get(clazz); ++ if (collection != null) { ++ collection.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 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 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 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 into, ++ final Predicate 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 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 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 into, ++ final Predicate predicate) { ++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; ++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; ++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; ++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; ++ ++ final int minRegionX = minChunkX >> REGION_SHIFT; ++ final int minRegionZ = minChunkZ >> REGION_SHIFT; ++ final int maxRegionX = maxChunkX >> REGION_SHIFT; ++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; ++ ++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { ++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; ++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; ++ ++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { ++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); ++ ++ if (region == null) { ++ continue; ++ } ++ ++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; ++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; ++ ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); ++ if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { ++ continue; ++ } ++ ++ chunk.getEntities(type, box, (List)into, (Predicate)predicate); ++ } ++ } ++ } ++ } ++ } ++ ++ public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, ++ final Predicate predicate) { ++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; ++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; ++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; ++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; ++ ++ final int minRegionX = minChunkX >> REGION_SHIFT; ++ final int minRegionZ = minChunkZ >> REGION_SHIFT; ++ final int maxRegionX = maxChunkX >> REGION_SHIFT; ++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; ++ ++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { ++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; ++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; ++ ++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { ++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); ++ ++ if (region == null) { ++ continue; ++ } ++ ++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; ++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; ++ ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); ++ if (chunk == null || !chunk.status.isOrAfter(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> callback) { ++ // Tuinity start - add flush parameter ++ this.writePacket(packet, callback, Boolean.TRUE); ++ } ++ private void writePacket(Packet packet, @Nullable GenericFutureListener> 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> genericfuturelistener, ConnectionProtocol enumprotocol, ConnectionProtocol enumprotocol1) { ++ // Tuinity start - add flush parameter ++ this.a(packet, genericfuturelistener, enumprotocol, enumprotocol1, true); ++ } ++ private void a(Packet packet, @Nullable GenericFutureListener> 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 com.mojang.serialization.DataResult decode(final com.mojang.serialization.DynamicOps ops, final com.mojang.serialization.MapLike input) { ++ T value = input.get(name); ++ if (value == null) { ++ for (String fall : fallback) { ++ value = input.get(fall); ++ if (value != null) { ++ break; ++ } ++ } ++ if (value == null) { ++ return com.mojang.serialization.DataResult.error("No key " + name + " in " + input); ++ } ++ } ++ return elementCodec.parse(ops, value); ++ } ++ ++ @Override ++ public java.util.stream.Stream keys(final com.mojang.serialization.DynamicOps ops) { ++ return java.util.stream.Stream.of(ops.createString(name)); ++ } ++ ++ @Override ++ public boolean equals(final Object o) { ++ if (this == o) { ++ return true; ++ } ++ if (o == null || getClass() != o.getClass()) { ++ return false; ++ } ++ final FieldFallbackDecoder that = (FieldFallbackDecoder)o; ++ return java.util.Objects.equals(name, that.name) && java.util.Objects.equals(elementCodec, that.elementCodec) ++ && java.util.Objects.equals(fallback, that.fallback); ++ } ++ ++ @Override ++ public int hashCode() { ++ return java.util.Objects.hash(name, fallback, elementCodec); ++ } ++ ++ @Override ++ public String toString() { ++ return "FieldDecoder[" + name + ": " + elementCodec + ']'; ++ } ++ } + } +diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java +index cfd43069ee2b6f79afb12e10d223f6bf75100034..75c31ec2553c0959f1ac34b554a39a73144da8bd 100644 +--- a/src/main/java/net/minecraft/server/Main.java ++++ b/src/main/java/net/minecraft/server/Main.java +@@ -57,7 +57,7 @@ import org.apache.logging.log4j.Logger; + // CraftBukkit start + import net.minecraft.SharedConstants; + +-public class Main { ++public class Main { // + + private static final Logger LOGGER = LogManager.getLogger(); + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 3dded5c491ace6b073a7bc3178976bd70f0b9393..f25bb4214cffd0050241ea229b6acb0c16b2b0a5 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -293,6 +293,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); + public int autosavePeriod; + public boolean serverAutoSave = false; // Paper +@@ -327,6 +328,76 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= MAX_CHUNK_EXEC_TIME) { ++ if (!moreTasks) { ++ lastMidTickExecuteFailure = currTime; ++ } ++ ++ // note: negative values reduce the time ++ long overuse = diff - MAX_CHUNK_EXEC_TIME; ++ if (overuse >= (10L * 1000L * 1000L)) { // 10ms ++ // make sure something like a GC or dumb plugin doesn't screw us over... ++ overuse = 10L * 1000L * 1000L; // 10ms ++ } ++ ++ double overuseCount = (double)overuse/(double)MAX_CHUNK_EXEC_TIME; ++ long extraSleep = (long)Math.round(overuseCount*CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME); ++ ++ lastMidTickExecute = currTime + extraSleep; ++ return; ++ } ++ } ++ } finally { ++ co.aikar.timings.MinecraftTimings.midTickChunkTasks.stopTiming(); ++ } ++ } ++ // Tuinity end - execute chunk tasks mid tick ++ + public MinecraftServer(OptionSet options, DataPackConfig datapackconfiguration, Thread thread, RegistryAccess.RegistryHolder iregistrycustom_dimension, LevelStorageSource.LevelStorageAccess convertable_conversionsession, WorldData savedata, PackRepository resourcepackrepository, Proxy proxy, DataFixer datafixer, ServerResources datapackresources, @Nullable MinecraftSessionService minecraftsessionservice, @Nullable GameProfileRepository gameprofilerepository, @Nullable GameProfileCache usercache, ChunkProgressListenerFactory worldloadlistenerfactory) { + super("Server"); + SERVER = this; // Paper - better singleton +@@ -1141,6 +1212,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { +- midTickLoadChunks(); // will only do loads since we are still considered !canSleepForTick ++ // Tuinity - replace logic + return !this.canOversleep(); + }); + isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); +@@ -1459,6 +1518,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop // Spigot - Spigot > // CraftBukkit - cb > vanilla! ++ return "Tuinity"; // Tuinity - Tuinity > //Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! + } + + public SystemReport fillSystemReport(SystemReport details) { +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index 6d7eef79de7a899ccdbc3194d925bb4caa0a4b03..9667a74c9b77ea6acd9d2ebce30c685ed4b53e59 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -223,6 +223,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + io.papermc.paper.brigadier.PaperBrigadierProviderImpl.INSTANCE.getClass(); // init PaperBrigadierProvider + io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.getClass(); // load mappings for stacktrace deobf + // Paper end ++ com.tuinity.tuinity.config.TuinityConfig.init((java.io.File) options.valueOf("tuinity-settings")); // Tuinity - Server Config + + this.setPvpAllowed(dedicatedserverproperties.pvp); + this.setFlightAllowed(dedicatedserverproperties.allowFlight); +diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java +index f542998d3aac3b5f3039b906b8dadd636c1fb164..6c32e2922b0d0b4df3fe79b5558c134647c34a6c 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java ++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java +@@ -41,6 +41,8 @@ import net.minecraft.world.level.lighting.LevelLightEngine; + import net.minecraft.server.MinecraftServer; + // CraftBukkit end + ++import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; // Tuinity ++ + public class ChunkHolder { + + public static final Either UNLOADED_CHUNK = Either.right(ChunkHolder.ChunkLoadingFailure.UNLOADED); +@@ -55,7 +57,7 @@ public class ChunkHolder { + private volatile CompletableFuture> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage + private volatile CompletableFuture> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage + private volatile CompletableFuture> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage +- private CompletableFuture chunkToSave; ++ public CompletableFuture chunkToSave; // Tuinity - public + @Nullable + private final DebugBuffer chunkToSaveHistory; + public int oldTicketLevel; +@@ -238,6 +240,12 @@ public class ChunkHolder { + long key = net.minecraft.server.MCUtil.getCoordinateKey(this.pos); + this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key); + this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key); ++ // Tuinity start - optimise checkDespawn ++ LevelChunk chunk = this.getFullChunkUnchecked(); ++ if (chunk != null) { ++ chunk.updateGeneralAreaCache(); ++ } ++ // Tuinity end - optimise checkDespawn + } + // Paper end - optimise isOutsideOfRange + long lastAutoSaveTime; // Paper - incremental autosave +@@ -388,7 +396,7 @@ public class ChunkHolder { + if (i < 0 || i >= this.changedBlocksPerSection.length) return; // CraftBukkit - SPIGOT-6086, SPIGOT-6296 + if (this.changedBlocksPerSection[i] == null) { + this.hasChangedSections = true; +- this.changedBlocksPerSection[i] = new ShortArraySet(); ++ this.changedBlocksPerSection[i] = new ShortOpenHashSet(); // Tuinity - use a set to make setting constant-time + } + + this.changedBlocksPerSection[i].add(SectionPos.sectionRelativePos(pos)); +@@ -489,7 +497,7 @@ public class ChunkHolder { + // Paper start - per player view distance + // there can be potential desync with player's last mapped section and the view distance map, so use the + // view distance map here. +- com.destroystokyo.paper.util.misc.PlayerAreaMap viewDistanceMap = this.chunkMap.playerViewDistanceBroadcastMap; ++ com.destroystokyo.paper.util.misc.PlayerAreaMap viewDistanceMap = this.chunkMap.playerChunkManager.broadcastMap; // Tuinity - replace old player chunk manager + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = viewDistanceMap.getObjectsInRange(this.pos); + if (players == null) { + return; +@@ -506,6 +514,7 @@ public class ChunkHolder { + + int viewDistance = viewDistanceMap.getLastViewDistance(player); + long lastPosition = viewDistanceMap.getLastCoordinate(player); ++ if (!this.chunkMap.playerChunkManager.isChunkSent(player, this.pos.x, this.pos.z)) continue; // Tuinity - replace player chunk management + + int distX = Math.abs(net.minecraft.server.MCUtil.getCoordinateX(lastPosition) - this.pos.x); + int distZ = Math.abs(net.minecraft.server.MCUtil.getCoordinateZ(lastPosition) - this.pos.z); +@@ -522,6 +531,7 @@ public class ChunkHolder { + continue; + } + ServerPlayer player = (ServerPlayer)temp; ++ if (!this.chunkMap.playerChunkManager.isChunkSent(player, this.pos.x, this.pos.z)) continue; // Tuinity - replace player chunk management + player.connection.send(packet); + } + } +@@ -597,7 +607,13 @@ public class ChunkHolder { + CompletableFuture completablefuture1 = new CompletableFuture(); + + completablefuture1.thenRunAsync(() -> { ++ // Tuinity start - do not allow ticket level changes ++ boolean unloadingBefore = this.chunkMap.unloadingPlayerChunk; ++ this.chunkMap.unloadingPlayerChunk = true; ++ try { ++ // Tuinity end - do not allow ticket level changes + playerchunkmap.onFullChunkStatusChange(this.pos, playerchunk_state); ++ } finally { this.chunkMap.unloadingPlayerChunk = unloadingBefore; } // Tuinity - do not allow ticket level changes + }, executor); + this.pendingFullStateConfirmation = completablefuture1; + completablefuture.thenAccept((either) -> { +@@ -607,12 +623,23 @@ public class ChunkHolder { + }); + } + ++ private boolean loadCallbackScheduled = false; ++ private boolean unloadCallbackScheduled = false; ++ + private void demoteFullChunk(ChunkMap playerchunkmap, ChunkHolder.FullChunkStatus playerchunk_state) { + this.pendingFullStateConfirmation.cancel(false); ++ // Tuinity start - do not allow ticket level changes ++ boolean unloadingBefore = this.chunkMap.unloadingPlayerChunk; ++ this.chunkMap.unloadingPlayerChunk = true; ++ try { // Tuinity end - do not allow ticket level changes + playerchunkmap.onFullChunkStatusChange(this.pos, playerchunk_state); ++ } finally { this.chunkMap.unloadingPlayerChunk = unloadingBefore; } // Tuinity - do not allow ticket level changes + } + +- protected void updateFutures(ChunkMap chunkStorage, Executor executor) { ++ protected long updateCount; // Tuinity - correctly handle recursion ++ public void updateFutures(ChunkMap chunkStorage, Executor executor) { // Tuinity ++ com.tuinity.tuinity.util.TickThread.ensureTickThread("Async ticket level update"); // Tuinity ++ long updateCount = ++this.updateCount; // Tuinity - correctly handle recursion + ChunkStatus chunkstatus = ChunkHolder.getStatus(this.oldTicketLevel); + ChunkStatus chunkstatus1 = ChunkHolder.getStatus(this.ticketLevel); + boolean flag = this.oldTicketLevel <= ChunkMap.MAX_CHUNK_DISTANCE; +@@ -622,10 +649,23 @@ public class ChunkHolder { + // CraftBukkit start + // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins. + if (playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && !playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { +- this.getStatusFutureUncheckedMain(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main ++ this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main // Tuinity - is always on main ++ com.tuinity.tuinity.util.TickThread.ensureTickThread("Async full status chunk future completion"); // Tuinity + LevelChunk chunk = (LevelChunk)either.left().orElse(null); +- if (chunk != null) { ++ if (chunk != null && chunk.wasLoadCallbackInvoked() && ChunkHolder.this.ticketLevel > 33) { // Tuinity - only invoke unload if load was called ++ // Tuinity start - only schedule once, now the future is no longer completed as RIGHT if unloaded... ++ if (ChunkHolder.this.unloadCallbackScheduled) { ++ return; ++ } ++ ChunkHolder.this.unloadCallbackScheduled = true; ++ // Tuinity end - only schedule once, now the future is no longer completed as RIGHT if unloaded... + chunkStorage.callbackExecutor.execute(() -> { ++ // Tuinity start - only schedule once, now the future is no longer completed as RIGHT if unloaded... ++ ChunkHolder.this.unloadCallbackScheduled = false; ++ if (ChunkHolder.this.ticketLevel <= 33) { ++ return; ++ } ++ // Tuinity end - only schedule once, now the future is no longer completed as RIGHT if unloaded... + // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick + // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag. + // These actions may however happen deferred, so we manually set the needsSaving flag already here. +@@ -641,6 +681,12 @@ public class ChunkHolder { + + // Run callback right away if the future was already done + chunkStorage.callbackExecutor.run(); ++ // Tuinity start - correctly handle recursion ++ if (this.updateCount != updateCount) { ++ // something else updated ticket level for us. ++ return; ++ } ++ // Tuinity end - correctly handle recursion + } + // CraftBukkit end + CompletableFuture completablefuture; +@@ -681,7 +727,8 @@ public class ChunkHolder { + this.fullChunkFuture = chunkStorage.prepareAccessibleChunk(this); + this.scheduleFullChunkPromotion(chunkStorage, this.fullChunkFuture, executor, ChunkHolder.FullChunkStatus.BORDER); + // Paper start - cache ticking ready status +- ensureMain(this.fullChunkFuture).thenAccept(either -> { // Paper - ensure main ++ this.fullChunkFuture.thenAccept(either -> { // Paper - ensure main // Tuinity - always fired on main ++ com.tuinity.tuinity.util.TickThread.ensureTickThread("Async full chunk future completion"); // Tuinity + final Optional left = either.left(); + if (left.isPresent() && ChunkHolder.this.fullChunkCreateCount == expectCreateCount) { + // note: Here is a very good place to add callbacks to logic waiting on this. +@@ -712,7 +759,8 @@ public class ChunkHolder { + this.tickingChunkFuture = chunkStorage.prepareTickingChunk(this); + this.scheduleFullChunkPromotion(chunkStorage, this.tickingChunkFuture, executor, ChunkHolder.FullChunkStatus.TICKING); + // Paper start - cache ticking ready status +- ensureMain(this.tickingChunkFuture).thenAccept(either -> { // Paper - ensure main ++ this.tickingChunkFuture.thenAccept(either -> { // Paper - ensure main // Tuinity - always completed on main ++ com.tuinity.tuinity.util.TickThread.ensureTickThread("Async ticking chunk future completion"); // Tuinity + either.ifLeft(chunk -> { + // note: Here is a very good place to add callbacks to logic waiting on this. + ChunkHolder.this.isTickingReady = true; +@@ -720,6 +768,9 @@ public class ChunkHolder { + // Paper start - rewrite ticklistserver + ChunkHolder.this.chunkMap.level.onChunkSetTicking(ChunkHolder.this.pos.x, ChunkHolder.this.pos.z); + // Paper end - rewrite ticklistserver ++ // Tuinity start - ticking chunk set ++ ChunkHolder.this.chunkMap.level.getChunkSource().tickingChunks.add(chunk); ++ // Tuinity end - ticking chunk set + }); + }); + // Paper end +@@ -729,6 +780,12 @@ public class ChunkHolder { + if (flag4 && !flag5) { + this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage + this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; ++ // Tuinity start - ticking chunk set ++ LevelChunk chunkIfCached = this.getFullChunkUnchecked(); ++ if (chunkIfCached != null) { ++ this.chunkMap.level.getChunkSource().tickingChunks.remove(chunkIfCached); ++ } ++ // Tuinity end - ticking chunk set + } + + boolean flag6 = playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.ENTITY_TICKING); +@@ -742,9 +799,13 @@ public class ChunkHolder { + this.entityTickingChunkFuture = chunkStorage.prepareEntityTickingChunk(this.pos); + this.scheduleFullChunkPromotion(chunkStorage, this.entityTickingChunkFuture, executor, ChunkHolder.FullChunkStatus.ENTITY_TICKING); + // Paper start - cache ticking ready status +- ensureMain(this.entityTickingChunkFuture).thenAccept(either -> { // Paper ensureMain ++ this.entityTickingChunkFuture.thenAccept(either -> { // Paper ensureMain // Tuinity - always completed on main ++ com.tuinity.tuinity.util.TickThread.ensureTickThread("Async entity ticking chunk future completion"); // Tuinity + either.ifLeft(chunk -> { + ChunkHolder.this.isEntityTickingReady = true; ++ // Tuinity start - entity ticking chunk set ++ ChunkHolder.this.chunkMap.level.getChunkSource().entityTickingChunks.add(chunk); ++ // Tuinity end - entity ticking chunk set + }); + }); + // Paper end +@@ -754,6 +815,12 @@ public class ChunkHolder { + if (flag6 && !flag7) { + this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage + this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; ++ // Tuinity start - entity ticking chunk set ++ LevelChunk chunkIfCached = this.getFullChunkUnchecked(); ++ if (chunkIfCached != null) { ++ this.chunkMap.level.getChunkSource().entityTickingChunks.remove(chunkIfCached); ++ } ++ // Tuinity end - entity ticking chunk set + } + + if (!playerchunk_state1.isOrAfter(playerchunk_state)) { +@@ -784,11 +851,19 @@ public class ChunkHolder { + // CraftBukkit start + // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins. + if (!playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { +- this.getStatusFutureUncheckedMain(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main ++ this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main // Tuinity - is always on main ++ com.tuinity.tuinity.util.TickThread.ensureTickThread("Async full status chunk future completion"); // Tuinity + LevelChunk chunk = (LevelChunk)either.left().orElse(null); +- if (chunk != null) { ++ if (chunk != null && ChunkHolder.this.oldTicketLevel <= 33 && !chunk.wasLoadCallbackInvoked()) { // Tuinity - ensure ticket level is set to loaded before calling, as now this can complete with ticket level > 33 ++ // Tuinity start - only schedule once, now the future is no longer completed as RIGHT if unloaded... ++ if (ChunkHolder.this.loadCallbackScheduled) { ++ return; ++ } ++ ChunkHolder.this.loadCallbackScheduled = true; ++ // Tuinity end - only schedule once, now the future is no longer completed as RIGHT if unloaded... + chunkStorage.callbackExecutor.execute(() -> { +- chunk.loadCallback(); ++ ChunkHolder.this.loadCallbackScheduled = false; // Tuinity - only schedule once, now the future is no longer completed as RIGHT if unloaded... ++ if (ChunkHolder.this.oldTicketLevel <= 33) chunk.loadCallback(); // Tuinity " + }); + } + }).exceptionally((throwable) -> { +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 42fd259f4492e539112b5bcb310aaaadab58a443..b9b985268f5627a238c302f81400a05bfd7c592d 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -104,6 +104,7 @@ import org.apache.logging.log4j.LogManager; + import org.apache.logging.log4j.Logger; + + import org.bukkit.entity.Player; // CraftBukkit ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; // Tuinity + + public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider { + +@@ -116,8 +117,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + public static final int MAX_VIEW_DISTANCE = 33; + public static final int MAX_CHUNK_DISTANCE = 33 + ChunkStatus.maxDistance(); + // Paper start - faster copying +- public final Long2ObjectLinkedOpenHashMap updatingChunkMap = new com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy<>(); // Paper - faster copying +- public final Long2ObjectLinkedOpenHashMap visibleChunkMap = new ProtectedVisibleChunksMap(); // Paper - faster copying ++ // Tuinity start - Don't copy ++ public final com.destroystokyo.paper.util.map.QueuedChangesMapLong2Object updatingChunks = new com.destroystokyo.paper.util.map.QueuedChangesMapLong2Object<>(); ++ // Tuinity end - Don't copy + + private class ProtectedVisibleChunksMap extends com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy { + @Override +@@ -140,8 +142,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + } + // Paper end +- public final com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy pendingVisibleChunks = new com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy(); // Paper - this is used if the visible chunks is updated while iterating only +- public transient com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy visibleChunksClone; // Paper - used for async access of visible chunks, clone and cache only when needed ++ // Tuinity - Don't copy + public static final int FORCED_TICKET_LEVEL = 31; + // public final Long2ObjectLinkedOpenHashMap updatingChunkMap = new Long2ObjectLinkedOpenHashMap(); // Paper - moved up + // public volatile Long2ObjectLinkedOpenHashMap visibleChunkMap; // Paper - moved up +@@ -149,7 +150,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + public final LongSet entitiesInLevel; + public final ServerLevel level; + private final ThreadedLevelLightEngine lightEngine; +- private final BlockableEventLoop mainThreadExecutor; ++ public final BlockableEventLoop mainThreadExecutor; // Tuinity - public + final java.util.concurrent.Executor mainInvokingExecutor; // Paper + public final ChunkGenerator generator; + public final Supplier overworldDataStorage; +@@ -182,32 +183,29 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + public final CallbackExecutor callbackExecutor = new CallbackExecutor(); + public static final class CallbackExecutor implements java.util.concurrent.Executor, Runnable { + +- // Paper start - replace impl with recursive safe multi entry queue +- // it's possible to schedule multiple tasks currently, so it's vital we change this impl +- // If we recurse into the executor again, we will append to another queue, ensuring task order consistency +- private java.util.Queue queue = new java.util.ArrayDeque<>(); // Paper - remove final ++ // Tuinity start - revert paper's change ++ private Runnable queued; + + @Override + public void execute(Runnable runnable) { + org.spigotmc.AsyncCatcher.catchOp("Callback Executor execute"); +- if (this.queue == null) { +- this.queue = new java.util.ArrayDeque<>(); ++ if (queued != null) { ++ MinecraftServer.LOGGER.fatal("Failed to schedule runnable", new IllegalStateException("Already queued")); // Paper - make sure this is printed ++ throw new IllegalStateException("Already queued"); + } +- this.queue.add(runnable); ++ queued = runnable; + } ++ // Tuinity end - revert paper's change + + @Override + public void run() { + org.spigotmc.AsyncCatcher.catchOp("Callback Executor run"); +- if (this.queue == null) { +- return; +- } +- java.util.Queue queue = this.queue; +- this.queue = null; +- // Paper end +- Runnable task; +- while ((task = queue.poll()) != null) { // Paper ++ // Tuinity start - revert paper's change ++ Runnable task = queued; ++ queued = null; ++ if (task != null) { + task.run(); ++ // Tuinity end - revert paper's change + } + } + }; +@@ -216,22 +214,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + final CallbackExecutor chunkLoadConversionCallbackExecutor = new CallbackExecutor(); // Paper + // Paper start - distance maps + private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>(); +- // Paper start - no-tick view distance +- int noTickViewDistance; +- public final int getRawNoTickViewDistance() { +- return this.noTickViewDistance; +- } +- public final int getEffectiveNoTickViewDistance() { +- return this.noTickViewDistance == -1 ? this.getEffectiveViewDistance() : this.noTickViewDistance; +- } +- public final int getLoadViewDistance() { +- return Math.max(this.getEffectiveViewDistance(), this.getEffectiveNoTickViewDistance()); +- } +- +- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceBroadcastMap; +- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceTickMap; +- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceNoTickMap; +- // Paper end - no-tick view distance ++ public final com.tuinity.tuinity.chunk.PlayerChunkLoader playerChunkManager = new com.tuinity.tuinity.chunk.PlayerChunkLoader(this, this.pooledLinkedPlayerHashSets); // Tuinity - replace chunk loader + // Paper start - use distance map to optimise tracker + public static boolean isLegacyTrackingEntity(Entity entity) { + return entity.isLegacyTrackingEntity; +@@ -240,7 +223,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // inlined EnumMap, TrackingRange.TrackingRangeType + static final org.spigotmc.TrackingRange.TrackingRangeType[] TRACKING_RANGE_TYPES = org.spigotmc.TrackingRange.TrackingRangeType.values(); + public final com.destroystokyo.paper.util.misc.PlayerAreaMap[] playerEntityTrackerTrackMaps; +- final int[] entityTrackerTrackRanges; ++ final int[] entityTrackerTrackRanges; public int getEntityTrackerRange(final int ordinal) { return this.entityTrackerTrackRanges[ordinal]; } // Tuinity - public read + + private int convertSpigotRangeToVanilla(final int vanilla) { + return MinecraftServer.getServer().getScaledTrackingDistance(vanilla); +@@ -257,6 +240,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap; // this map is absent from updateMaps since it's controlled at the start of the chunkproviderserver tick + public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerChunkTickRangeMap; + // Paper end - optimise PlayerChunkMap#isOutsideRange ++ // Tuinity start - optimise checkDespawn ++ public static final int GENERAL_AREA_MAP_SQUARE_RADIUS = 40; ++ public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE = 16.0 * (GENERAL_AREA_MAP_SQUARE_RADIUS - 1); ++ public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE_SQUARED = GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE * GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE; ++ public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerGeneralAreaMap; ++ // Tuinity end - optimise checkDespawn + + void addPlayerToDistanceMaps(ServerPlayer player) { + int chunkX = MCUtil.getChunkCoordinate(player.getX()); +@@ -267,7 +256,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; + int trackRange = this.entityTrackerTrackRanges[i]; + +- trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance())); ++ trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, player.getBukkitEntity().getViewDistance())); // Tuinity - per player view distances + } + // Paper end - use distance map to optimise entity tracker + // Paper start - optimise PlayerChunkMap#isOutsideRange +@@ -276,19 +265,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper start - optimise PlayerChunkMap#isOutsideRange + this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); + // Paper end - optimise PlayerChunkMap#isOutsideRange +- // Paper start - no-tick view distance +- int effectiveTickViewDistance = this.getEffectiveViewDistance(); +- int effectiveNoTickViewDistance = Math.max(this.getEffectiveNoTickViewDistance(), effectiveTickViewDistance); +- +- if (!this.skipPlayer(player)) { +- this.playerViewDistanceTickMap.add(player, chunkX, chunkZ, effectiveTickViewDistance); +- this.playerViewDistanceNoTickMap.add(player, chunkX, chunkZ, effectiveNoTickViewDistance + 2); // clients need chunk 1 neighbour, and we need another 1 for sending those extra neighbours (as we require neighbours to send) +- } +- +- player.needsChunkCenterUpdate = true; +- this.playerViewDistanceBroadcastMap.add(player, chunkX, chunkZ, effectiveNoTickViewDistance + 1); // clients need an extra neighbour to render the full view distance configured +- player.needsChunkCenterUpdate = false; +- // Paper end - no-tick view distance ++ this.playerChunkManager.addPlayer(player); // Tuinity - replace chunk loader ++ // Tuinity start - optimise checkDespawn ++ this.playerGeneralAreaMap.add(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); ++ // Tuinity end - optimise checkDespawn + } + + void removePlayerFromDistanceMaps(ServerPlayer player) { +@@ -301,11 +281,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.playerMobSpawnMap.remove(player); + this.playerChunkTickRangeMap.remove(player); + // Paper end - optimise PlayerChunkMap#isOutsideRange +- // Paper start - no-tick view distance +- this.playerViewDistanceBroadcastMap.remove(player); +- this.playerViewDistanceTickMap.remove(player); +- this.playerViewDistanceNoTickMap.remove(player); +- // Paper end - no-tick view distance ++ this.playerChunkManager.removePlayer(player); // Tuinity - replace chunk loader ++ // Tuinity start - optimise checkDespawn ++ this.playerGeneralAreaMap.remove(player); ++ // Tuinity end - optimise checkDespawn + } + + void updateMaps(ServerPlayer player) { +@@ -317,28 +296,125 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; + int trackRange = this.entityTrackerTrackRanges[i]; + +- trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance())); ++ trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, player.getBukkitEntity().getViewDistance())); // Tuinity - per player view distances + } + // Paper end - use distance map to optimise entity tracker + // Paper start - optimise PlayerChunkMap#isOutsideRange + this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); + // Paper end - optimise PlayerChunkMap#isOutsideRange +- // Paper start - no-tick view distance +- int effectiveTickViewDistance = this.getEffectiveViewDistance(); +- int effectiveNoTickViewDistance = Math.max(this.getEffectiveNoTickViewDistance(), effectiveTickViewDistance); ++ this.playerChunkManager.updatePlayer(player); // Tuinity - replace chunk loader ++ // Tuinity start - optimise checkDespawn ++ this.playerGeneralAreaMap.update(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); ++ // Tuinity end - optimise checkDespawn ++ } ++ // Paper end ++ // Tuinity start ++ public final List regionManagers = new java.util.ArrayList<>(); ++ public final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager dataRegionManager; + +- if (!this.skipPlayer(player)) { +- this.playerViewDistanceTickMap.update(player, chunkX, chunkZ, effectiveTickViewDistance); +- this.playerViewDistanceNoTickMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 2); // clients need chunk 1 neighbour, and we need another 1 for sending those extra neighbours (as we require neighbours to send) ++ public static final class DataRegionData implements com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionData { ++ // Tuinity start - optimise notify() ++ private com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet navigators; ++ ++ public com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet getNavigators() { ++ return this.navigators; ++ } ++ ++ public boolean addToNavigators(final Mob navigator) { ++ if (this.navigators == null) { ++ this.navigators = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(); ++ } ++ return this.navigators.add(navigator); + } + +- player.needsChunkCenterUpdate = true; +- this.playerViewDistanceBroadcastMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 1); // clients need an extra neighbour to render the full view distance configured +- player.needsChunkCenterUpdate = false; +- // Paper end - no-tick view distance ++ public boolean removeFromNavigators(final Mob navigator) { ++ if (this.navigators == null) { ++ return false; ++ } ++ return this.navigators.remove(navigator); ++ } ++ // Tuinity end - optimise notify() + } +- // Paper end + ++ public static final class DataRegionSectionData implements com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSectionData { ++ ++ // Tuinity start - optimise notify() ++ private com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet navigators; ++ ++ public com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet getNavigators() { ++ return this.navigators; ++ } ++ ++ public boolean addToNavigators(final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section, final Mob navigator) { ++ if (this.navigators == null) { ++ this.navigators = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(); ++ } ++ final boolean ret = this.navigators.add(navigator); ++ if (ret) { ++ final DataRegionData data = (DataRegionData)section.getRegion().regionData; ++ if (!data.addToNavigators(navigator)) { ++ throw new IllegalStateException(); ++ } ++ } ++ return ret; ++ } ++ ++ public boolean removeFromNavigators(final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section, final Mob navigator) { ++ if (this.navigators == null) { ++ return false; ++ } ++ final boolean ret = this.navigators.remove(navigator); ++ if (ret) { ++ final DataRegionData data = (DataRegionData)section.getRegion().regionData; ++ if (!data.removeFromNavigators(navigator)) { ++ throw new IllegalStateException(); ++ } ++ } ++ return ret; ++ } ++ // Tuinity end - optimise notify() ++ ++ @Override ++ public void removeFromRegion(final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section, ++ final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region from) { ++ final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData; ++ final DataRegionData fromData = (DataRegionData)from.regionData; ++ // Tuinity start - optimise notify() ++ if (sectionData.navigators != null) { ++ for (final Iterator iterator = sectionData.navigators.unsafeIterator(com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { ++ if (!fromData.removeFromNavigators(iterator.next())) { ++ throw new IllegalStateException(); ++ } ++ } ++ } ++ // Tuinity end - optimise notify() ++ } ++ ++ @Override ++ public void addToRegion(final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section, ++ final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region oldRegion, ++ final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region newRegion) { ++ final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData; ++ final DataRegionData oldRegionData = oldRegion == null ? null : (DataRegionData)oldRegion.regionData; ++ final DataRegionData newRegionData = (DataRegionData)newRegion.regionData; ++ // Tuinity start - optimise notify() ++ if (sectionData.navigators != null) { ++ for (final Iterator iterator = sectionData.navigators.unsafeIterator(com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { ++ if (!newRegionData.addToNavigators(iterator.next())) { ++ throw new IllegalStateException(); ++ } ++ } ++ } ++ // Tuinity end - optimise notify() ++ } ++ } ++ ++ public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) { ++ return this.pendingUnloads.get(com.tuinity.tuinity.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ // Tuiniy end ++ ++ boolean unloadingPlayerChunk = false; // Tuinity - do not allow ticket level changes while unloading chunks + private final java.util.concurrent.ExecutorService lightThread; // Paper + public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureManager structureManager, Executor executor, BlockableEventLoop mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory, int viewDistance, boolean dsync) { + super(new File(session.getDimensionPath(world.dimension()), "region"), dataFixer, dsync); +@@ -465,53 +541,28 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + }); + // Paper end - optimise PlayerChunkMap#isOutsideRange +- // Paper start - no-tick view distance +- this.setNoTickViewDistance(this.level.paperConfig.noTickViewDistance); +- this.playerViewDistanceTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, +- (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { +- checkHighPriorityChunks(player); +- if (newState.size() != 1) { +- return; +- } +- LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(rangeX, rangeZ); +- if (chunk == null || !chunk.areNeighboursLoaded(2)) { +- return; +- } +- +- ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); +- ChunkMap.this.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, 31, chunkPos); // entity ticking level, TODO check on update +- }, ++ this.setNoTickViewDistance(this.level.paperConfig.noTickViewDistance); // Tuinity - replace chunk loading system ++ // Tuinity start ++ this.dataRegionManager = new com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager(this.level, 2, (1.0 / 3.0), 1, 6, "Data", DataRegionData::new, DataRegionSectionData::new); ++ this.regionManagers.add(this.dataRegionManager); ++ // Tuinity end ++ // Tuinity start - optimise checkDespawn ++ this.playerGeneralAreaMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, + (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { +- if (newState != null) { +- return; ++ LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ); ++ if (chunk != null) { ++ chunk.updateGeneralAreaCache(newState); + } +- ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); +- ChunkMap.this.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, 31, chunkPos); // entity ticking level, TODO check on update +- // Paper start +- ChunkMap.this.level.getChunkSource().clearPriorityTickets(chunkPos); + }, +- (player, prevPos, newPos) -> { +- player.lastHighPriorityChecked = -1; // reset and recheck +- checkHighPriorityChunks(player); +- }); +- // Paper end +- this.playerViewDistanceNoTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets); +- this.playerViewDistanceBroadcastMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, + (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { +- if (player.needsChunkCenterUpdate) { +- player.needsChunkCenterUpdate = false; +- player.connection.send(new ClientboundSetChunkCacheCenterPacket(currPosX, currPosZ)); ++ LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ); ++ if (chunk != null) { ++ chunk.updateGeneralAreaCache(newState); + } +- ChunkMap.this.updateChunkTracking(player, new ChunkPos(rangeX, rangeZ), new Packet[2], false, true); // unloaded, loaded +- }, +- (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { +- ChunkMap.this.updateChunkTracking(player, new ChunkPos(rangeX, rangeZ), null, true, false); // unloaded, loaded + }); +- // Paper end - no-tick view distance ++ // Tuinity end - optimise checkDespawn + } + + // Paper start - Chunk Prioritization +@@ -545,6 +596,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public void checkHighPriorityChunks(ServerPlayer player) { ++ if (true) return; // Tuinity - replace player chunk loader + int currentTick = MinecraftServer.currentTick; + if (currentTick - player.lastHighPriorityChecked < 20 || !player.isRealPlayer) { // weed out fake players + return; +@@ -552,7 +604,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + player.lastHighPriorityChecked = currentTick; + it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap priorities = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(); + +- int viewDistance = getEffectiveNoTickViewDistance(); ++ int viewDistance = 10;//int viewDistance = getEffectiveNoTickViewDistance(); // Tuinity - replace player chunk loader + net.minecraft.core.BlockPos.MutableBlockPos pos = new net.minecraft.core.BlockPos.MutableBlockPos(); + + // Prioritize circular near +@@ -618,7 +670,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + private boolean shouldSkipPrioritization(ChunkPos coord) { +- if (playerViewDistanceNoTickMap.getObjectsInRange(coord.toLong()) == null) return true; ++ if (true) return true; // Tuinity - replace player chunk loader - unused outside paper player loader logic + ChunkHolder chunk = getUpdatingChunkIfPresent(coord.toLong()); + return chunk != null && (chunk.isFullChunkReady()); + } +@@ -686,7 +738,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + @Nullable + public ChunkHolder getUpdatingChunkIfPresent(long pos) { +- return (ChunkHolder) this.updatingChunkMap.get(pos); ++ return this.updatingChunks.getUpdating(pos); // Tuinity - Don't copy + } + + // Paper start - remove cloning of visible chunks unless accessed as a collection async +@@ -694,47 +746,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + private boolean isIterating = false; + private boolean hasPendingVisibleUpdate = false; + public void forEachVisibleChunk(java.util.function.Consumer consumer) { +- org.spigotmc.AsyncCatcher.catchOp("forEachVisibleChunk"); +- boolean prev = isIterating; +- isIterating = true; +- try { +- for (ChunkHolder value : this.visibleChunkMap.values()) { +- consumer.accept(value); +- } +- } finally { +- this.isIterating = prev; +- if (!this.isIterating && this.hasPendingVisibleUpdate) { +- ((ProtectedVisibleChunksMap)this.visibleChunkMap).copyFrom(this.pendingVisibleChunks); +- this.pendingVisibleChunks.clear(); +- this.hasPendingVisibleUpdate = false; +- } +- } ++ throw new UnsupportedOperationException(); // Tuinity - Don't copy + } + public Long2ObjectLinkedOpenHashMap getVisibleChunks() { +- if (Thread.currentThread() == this.level.thread) { +- return this.visibleChunkMap; +- } else { +- synchronized (this.visibleChunkMap) { +- if (DEBUG_ASYNC_VISIBLE_CHUNKS) new Throwable("Async getVisibleChunks").printStackTrace(); +- if (this.visibleChunksClone == null) { +- this.visibleChunksClone = this.hasPendingVisibleUpdate ? this.pendingVisibleChunks.clone() : ((ProtectedVisibleChunksMap)this.visibleChunkMap).clone(); +- } +- return this.visibleChunksClone; +- } ++ // Tuinity start - Don't copy (except in rare cases) ++ synchronized (this.updatingChunks) { ++ return this.updatingChunks.getVisibleMap().clone(); + } ++ // Tuinity end - Don't copy (except in rare cases) + } + // Paper end + + @Nullable + public ChunkHolder getVisibleChunkIfPresent(long pos) { +- // Paper start - mt safe get +- if (Thread.currentThread() != this.level.thread) { +- synchronized (this.visibleChunkMap) { +- return (ChunkHolder) (this.hasPendingVisibleUpdate ? this.pendingVisibleChunks.get(pos) : ((ProtectedVisibleChunksMap)this.visibleChunkMap).safeGet(pos)); +- } ++ // Tuinity start - Don't copy ++ if (Thread.currentThread() == this.level.thread) { ++ return this.updatingChunks.getVisible(pos); + } +- return (ChunkHolder) (this.hasPendingVisibleUpdate ? this.pendingVisibleChunks.get(pos) : ((ProtectedVisibleChunksMap)this.visibleChunkMap).safeGet(pos)); +- // Paper end ++ return this.updatingChunks.getVisibleAsync(pos); ++ // Tuinity end - Don't copy + } + + protected IntSupplier getChunkQueueLevel(long pos) { +@@ -856,12 +886,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + @Nullable + ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k) { ++ if (this.unloadingPlayerChunk) { LOGGER.fatal("Cannot tick distance manager while unloading playerchunks", new Throwable()); throw new IllegalStateException("Cannot tick distance manager while unloading playerchunks"); } // Tuinity + if (k > ChunkMap.MAX_CHUNK_DISTANCE && level > ChunkMap.MAX_CHUNK_DISTANCE) { + return holder; + } else { + if (holder != null) { + holder.setTicketLevel(level); +- holder.updateRanges(); // Paper - optimise isOutsideOfRange ++ // Tuinity - move to correct place + } + + if (holder != null) { +@@ -876,11 +907,17 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + holder = (ChunkHolder) this.pendingUnloads.remove(pos); + if (holder != null) { + holder.setTicketLevel(level); ++ holder.updateRanges(); // Paper - optimise isOutsideOfRange // Tuinity - move to correct place + } else { + holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this); ++ // Tuinity start ++ for (int index = 0, len = this.regionManagers.size(); index < len; ++index) { ++ this.regionManagers.get(index).addChunk(holder.pos.x, holder.pos.z); ++ } ++ // Tuinity end + } + +- this.updatingChunkMap.put(pos, holder); ++ this.updatingChunks.queueUpdate(pos, holder); // Tuinity - Don't copy + this.modified = true; + } + +@@ -1035,7 +1072,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + while (longiterator.hasNext()) { // Spigot + long j = longiterator.nextLong(); + longiterator.remove(); // Spigot +- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j); ++ ChunkHolder playerchunk = this.updatingChunks.queueRemove(j); // Tuinity - Don't copy + + if (playerchunk != null) { + this.pendingUnloads.put(j, playerchunk); +@@ -1072,7 +1109,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.level, chunkPos.x, chunkPos.z, +- poiData, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY); ++ poiData, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY); // Tuinity - use normal priority + + if (!chunk.isUnsaved()) { + return; +@@ -1093,7 +1130,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + asyncSaveData = ChunkSerializer.getAsyncSaveData(this.level, chunk); + } + +- this.level.asyncChunkTaskManager.scheduleChunkSave(chunkPos.x, chunkPos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY, ++ this.level.asyncChunkTaskManager.scheduleChunkSave(chunkPos.x, chunkPos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, // Tuinity - use normal priority + asyncSaveData, chunk); + + chunk.setUnsaved(false); +@@ -1109,7 +1146,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + if (completablefuture1 != completablefuture) { + this.scheduleUnload(pos, holder); + } else { +- if (this.pendingUnloads.remove(pos, holder) && ichunkaccess != null) { ++ // Tuinity start - do not allow ticket level changes while unloading chunks ++ org.spigotmc.AsyncCatcher.catchOp("playerchunk unload"); ++ boolean unloadingBefore = this.unloadingPlayerChunk; ++ this.unloadingPlayerChunk = true; ++ try { ++ // Tuinity end - do not allow ticket level changes while unloading chunks ++ // Tuinity start ++ boolean removed; ++ if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) { ++ for (int index = 0, len = this.regionManagers.size(); index < len; ++index) { ++ this.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z); ++ } ++ // Tuinity end + if (ichunkaccess instanceof LevelChunk) { + ((LevelChunk) ichunkaccess).setLoaded(false); + } +@@ -1134,7 +1183,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.lightEngine.updateChunkStatus(ichunkaccess.getPos()); + this.lightEngine.tryScheduleUpdate(); + this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null); ++ } else if (removed) { // Tuinity start ++ for (int index = 0, len = this.regionManagers.size(); index < len; ++index) { ++ this.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z); ++ } + } ++ // Tuinity end ++ } finally { this.unloadingPlayerChunk = unloadingBefore; } // Tuinity - do not allow ticket level changes while unloading chunks + + } + }; +@@ -1153,19 +1208,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + if (!this.modified) { + return false; + } else { +- // Paper start - stop cloning visibleChunks +- synchronized (this.visibleChunkMap) { +- if (isIterating) { +- hasPendingVisibleUpdate = true; +- this.pendingVisibleChunks.copyFrom((com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy)this.updatingChunkMap); +- } else { +- hasPendingVisibleUpdate = false; +- this.pendingVisibleChunks.clear(); +- ((ProtectedVisibleChunksMap)this.visibleChunkMap).copyFrom((com.destroystokyo.paper.util.map.Long2ObjectLinkedOpenHashMapFastCopy)this.updatingChunkMap); +- this.visibleChunksClone = null; +- } ++ // Tuinity start - Don't copy ++ synchronized (this.updatingChunks) { ++ this.updatingChunks.performUpdates(); + } +- // Paper end ++ // Tuinity end - Don't copy + + this.modified = false; + return true; +@@ -1178,11 +1225,20 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + if (requiredStatus == ChunkStatus.EMPTY) { + return this.scheduleChunkLoad(chunkcoordintpair); + } else { ++ // Tuinity start - revert 1.17 chunk system changes ++ CompletableFuture> future = holder.getOrScheduleFuture(requiredStatus.getParent(), this); ++ return future.thenComposeAsync((either) -> { ++ Optional optional = either.left(); ++ if (!optional.isPresent()) { ++ return CompletableFuture.completedFuture(either); ++ } ++ // Tuinity end - revert 1.17 chunk system changes ++ ++ // Tuinity start - revert 1.17 chunk system changes + if (requiredStatus == ChunkStatus.LIGHT) { + this.distanceManager.addTicket(TicketType.LIGHT, chunkcoordintpair, 33 + ChunkStatus.getDistance(ChunkStatus.LIGHT), chunkcoordintpair); + } +- +- Optional optional = ((Either) holder.getOrScheduleFuture(requiredStatus.getParent(), this).getNow(ChunkHolder.UNLOADED_CHUNK)).left(); ++ // Tuinity end - revert 1.17 chunk system changes + + if (optional.isPresent() && ((ChunkAccess) optional.get()).getStatus().isOrAfter(requiredStatus)) { + CompletableFuture> completablefuture = requiredStatus.load(this.level, this.structureManager, this.lightEngine, (ichunkaccess) -> { +@@ -1194,6 +1250,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } else { + return this.scheduleChunkGeneration(holder, requiredStatus); + } ++ }, this.mainThreadExecutor).thenComposeAsync(CompletableFuture::completedFuture, this.mainThreadExecutor); // Tuinity - revert 1.17 chunk system changes + } + } + +@@ -1294,7 +1351,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + }); + Executor executor = (runnable) -> { + // Paper start - optimize chunk status progression without jumping through thread pool +- if (holder.canAdvanceStatus()) { ++ if (false && holder.canAdvanceStatus()) { // Tuinity - these optimisations will be revisited later + this.mainInvokingExecutor.execute(runnable); + return; + } +@@ -1325,7 +1382,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.releaseLightTicket(chunkcoordintpair); + return CompletableFuture.completedFuture(Either.right(playerchunk_failure)); + }); +- }, executor); ++ }, executor).thenComposeAsync((either) -> { // Tuinity start - force competion on the main thread ++ return CompletableFuture.completedFuture(either); ++ }, this.mainThreadExecutor); // use the main executor, we want to ensure only one chunk callback can be completed per runnable execute ++ // Tuinity end - force competion on the main thread + } + + protected void releaseLightTicket(ChunkPos pos) { +@@ -1484,9 +1544,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + chunk.unpackTicks(); + return chunk; + }); +- }, (runnable) -> { +- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(playerchunk, runnable)); +- }); ++ }, this.mainThreadExecutor); // Tuinity - queue to execute immediately so this doesn't delay chunk unloading + } + + public int getTickingGenerated() { +@@ -1571,7 +1629,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + int k = this.viewDistance; + + this.viewDistance = j; +- this.setNoTickViewDistance(this.getRawNoTickViewDistance()); //Paper - no-tick view distance - propagate changes to no-tick, which does the actual chunk loading/sending ++ this.playerChunkManager.setTickDistance(Mth.clamp(viewDistance, 2, 32)); // Tuinity - replace player loader system + } + + } +@@ -1579,26 +1637,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper start - no-tick view distance + public final void setNoTickViewDistance(int viewDistance) { + viewDistance = viewDistance == -1 ? -1 : Mth.clamp(viewDistance, 2, 32); +- +- this.noTickViewDistance = viewDistance; +- int loadViewDistance = this.getLoadViewDistance(); +- this.distanceManager.setNoTickViewDistance(loadViewDistance + 2 + 2); // add 2 to account for the change to 31 -> 33 tickets // see notes in the distance map updating for the other + 2 +- +- if (this.level != null && this.level.players != null) { // this can be called from constructor, where these aren't set +- for (ServerPlayer player : this.level.players) { +- net.minecraft.server.network.ServerGamePacketListenerImpl connection = player.connection; +- if (connection != null) { +- // moved in from PlayerList +- connection.send(new net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket(loadViewDistance)); +- } +- this.updateMaps(player); +- // Paper end - no-tick view distance +- } +- } ++ this.playerChunkManager.setLoadDistance(viewDistance == -1 ? -1 : viewDistance + 1); // Tuinity - replace player loader system - add 1 here, we need an extra one to send to clients for chunks in this viewDistance to render + + } + +- protected void updateChunkTracking(ServerPlayer player, ChunkPos pos, Packet[] packets, boolean withinMaxWatchDistance, boolean withinViewDistance) { ++ public void updateChunkTracking(ServerPlayer player, ChunkPos pos, Packet[] packets, boolean withinMaxWatchDistance, boolean withinViewDistance) { // Tuinity - public + if (player.level == this.level) { + if (withinViewDistance && !withinMaxWatchDistance) { + ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong()); +@@ -1622,7 +1665,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public int size() { +- return this.visibleChunkMap.size(); ++ return this.updatingChunks.getVisibleMap().size(); // Tuinity - Don't copy + } + + protected DistanceManager getDistanceManager() { +@@ -1927,6 +1970,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + */ // Paper end - replaced by distance map + + this.updateMaps(player); // Paper - distance maps ++ this.playerChunkManager.updatePlayer(player); // Tuinity - respond to movement immediately + + } + +@@ -1935,7 +1979,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper start - per player view distance + // there can be potential desync with player's last mapped section and the view distance map, so use the + // view distance map here. +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = this.playerViewDistanceBroadcastMap.getObjectsInRange(chunkPos); ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = this.playerChunkManager.broadcastMap.getObjectsInRange(chunkPos); // Tuinity - replace player chunk loader system + + if (inRange == null) { + return Stream.empty(); +@@ -1951,8 +1995,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + continue; + } + ServerPlayer player = (ServerPlayer)temp; +- int viewDistance = this.playerViewDistanceBroadcastMap.getLastViewDistance(player); +- long lastPosition = this.playerViewDistanceBroadcastMap.getLastCoordinate(player); ++ if (!this.playerChunkManager.isChunkSent(player, chunkPos.x, chunkPos.z)) continue; // Tuinity - replace player chunk management ++ int viewDistance = this.playerChunkManager.broadcastMap.getLastViewDistance(player); // Tuinity - replace player chunk loader system ++ long lastPosition = this.playerChunkManager.broadcastMap.getLastCoordinate(player); // Tuinity - replace player chunk loader system + + int distX = Math.abs(MCUtil.getCoordinateX(lastPosition) - chunkPos.x); + int distZ = Math.abs(MCUtil.getCoordinateZ(lastPosition) - chunkPos.z); +@@ -1967,6 +2012,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + continue; + } + ServerPlayer player = (ServerPlayer)temp; ++ if (!this.playerChunkManager.isChunkSent(player, chunkPos.x, chunkPos.z)) continue; // Tuinity - replace player chunk management + players.add(player); + } + } +@@ -2281,7 +2327,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially + final Entity entity; + private final int range; + SectionPos lastSectionPos; +- public final Set seenBy = Sets.newIdentityHashSet(); ++ public final Set seenBy = new ReferenceOpenHashSet<>(); // Tuinity - optimise map impl + + public TrackedEntity(Entity entity, int i, int j, boolean flag) { + this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast, this.seenBy); // CraftBukkit +@@ -2381,7 +2427,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially + double vec3d_dy = player.getY() - this.entity.getY(); + double vec3d_dz = player.getZ() - this.entity.getZ(); + // Paper end - remove allocation of Vec3D here +- int i = Math.min(this.getEffectiveRange(), (ChunkMap.this.viewDistance - 1) * 16); ++ int i = Math.min(this.getEffectiveRange(), player.getBukkitEntity().getViewDistance() * 16); // Tuinity - per player view distance + boolean flag = vec3d_dx >= (double) (-i) && vec3d_dx <= (double) i && vec3d_dz >= (double) (-i) && vec3d_dz <= (double) i && this.entity.broadcastToPlayer(player); // Paper - remove allocation of Vec3D here + + // CraftBukkit start - respect vanish API +@@ -2416,7 +2462,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially + int j = entity.getType().clientTrackingRange() * 16; + j = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, j); // Paper + +- if (j < i) { // Paper - we need the lowest range thanks to the fact that our tracker doesn't account for passenger logic ++ if (j > i) { // Paper - we need the lowest range thanks to the fact that our tracker doesn't account for passenger logic // Tuinity - not anymore! + i = j; + } + } +diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java +index 1cc4e0a1f3d8235ef88b48e01ca8b78a263d2676..428d94c60b826ddf3797d6713661dff1ca835ac2 100644 +--- a/src/main/java/net/minecraft/server/level/DistanceManager.java ++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java +@@ -36,6 +36,7 @@ import net.minecraft.world.level.chunk.LevelChunk; + import org.apache.logging.log4j.LogManager; + import org.apache.logging.log4j.Logger; + ++import it.unimi.dsi.fastutil.longs.Long2IntLinkedOpenHashMap; // Tuinity + public abstract class DistanceManager { + + static final Logger LOGGER = LogManager.getLogger(); +@@ -44,9 +45,9 @@ public abstract class DistanceManager { + private static final int INITIAL_TICKET_LIST_CAPACITY = 4; + final Long2ObjectMap> playersPerChunk = new Long2ObjectOpenHashMap(); + public final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap(); +- private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); ++ //private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); // Tuinity - replace ticket level propagator + public static final int MOB_SPAWN_RANGE = 8; // private final ChunkMapDistance.b f = new ChunkMapDistance.b(8); // Paper - no longer used +- private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33); ++ //private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33); // Tuinity - no longer used + // Paper start use a queue, but still keep unique requirement + public final java.util.Queue pendingChunkUpdates = new java.util.ArrayDeque() { + @Override +@@ -77,6 +78,46 @@ public abstract class DistanceManager { + this.mainThreadExecutor = mainThreadExecutor; + } + ++ // Tuinity start - replace ticket level propagator ++ protected final Long2IntLinkedOpenHashMap ticketLevelUpdates = new Long2IntLinkedOpenHashMap() { ++ @Override ++ protected void rehash(int newN) { ++ // no downsizing allowed ++ if (newN < this.n) { ++ return; ++ } ++ super.rehash(newN); ++ } ++ }; ++ protected final com.tuinity.tuinity.util.misc.Delayed8WayDistancePropagator2D ticketLevelPropagator = new com.tuinity.tuinity.util.misc.Delayed8WayDistancePropagator2D( ++ (long coordinate, byte oldLevel, byte newLevel) -> { ++ DistanceManager.this.ticketLevelUpdates.putAndMoveToLast(coordinate, convertBetweenTicketLevels(newLevel)); ++ } ++ ); ++ // function for converting between ticket levels and propagator levels and vice versa ++ // the problem is the ticket level propagator will propagate from a set source down to zero, whereas mojang expects ++ // levels to propagate from a set value up to a maximum value. so we need to convert the levels we put into the propagator ++ // and the levels we get out of the propagator ++ ++ // this maps so that GOLDEN_TICKET + 1 will be 0 in the propagator, GOLDEN_TICKET will be 1, and so on ++ // we need GOLDEN_TICKET+1 as 0 because anything >= GOLDEN_TICKET+1 should be unloaded ++ public static int convertBetweenTicketLevels(final int level) { ++ return ChunkMap.MAX_CHUNK_DISTANCE - level + 1; ++ } ++ ++ protected final int getPropagatedTicketLevel(final long coordinate) { ++ return convertBetweenTicketLevels(this.ticketLevelPropagator.getLevel(coordinate)); ++ } ++ ++ protected final void updateTicketLevel(final long coordinate, final int ticketLevel) { ++ if (ticketLevel > ChunkMap.MAX_CHUNK_DISTANCE) { ++ this.ticketLevelPropagator.removeSource(coordinate); ++ } else { ++ this.ticketLevelPropagator.setSource(coordinate, convertBetweenTicketLevels(ticketLevel)); ++ } ++ } ++ // Tuinity end - replace ticket level propagator ++ + protected void purgeStaleTickets() { + ++this.ticketTickCounter; + ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); +@@ -87,7 +128,7 @@ public abstract class DistanceManager { + if ((entry.getValue()).removeIf((ticket) -> { // CraftBukkit - decompile error + return ticket.timedOut(this.ticketTickCounter); + })) { +- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false); ++ this.updateTicketLevel(entry.getLongKey(), getTicketLevelAt(entry.getValue())); // Tuinity - replace ticket level propagator + } + + if (((SortedArraySet) entry.getValue()).isEmpty()) { +@@ -110,60 +151,93 @@ public abstract class DistanceManager { + @Nullable + protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k); + ++ protected long ticketLevelUpdateCount; // Tuinity - replace ticket level propagator + public boolean runAllUpdates(ChunkMap playerchunkmap) { + //this.f.a(); // Paper - no longer used + org.spigotmc.AsyncCatcher.catchOp("DistanceManagerTick"); // Paper +- this.playerTicketManager.runAllUpdates(); +- int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE); +- boolean flag = i != 0; ++ //this.playerTicketManager.runAllUpdates(); // Tuinity - no longer used ++ boolean flag = this.ticketLevelPropagator.propagateUpdates(); // Tuinity - replace ticket level propagator + + if (flag) { + ; + } + +- // Paper start +- if (!this.pendingChunkUpdates.isEmpty()) { +- this.pollingPendingChunkUpdates = true; try { // Paper - Chunk priority +- while(!this.pendingChunkUpdates.isEmpty()) { +- ChunkHolder remove = this.pendingChunkUpdates.remove(); +- remove.isUpdateQueued = false; +- remove.updateFutures(playerchunkmap, this.mainThreadExecutor); +- } +- } finally { this.pollingPendingChunkUpdates = false; } // Paper - Chunk priority +- // Paper end +- return true; +- } else { +- if (!this.ticketsToRelease.isEmpty()) { +- LongIterator longiterator = this.ticketsToRelease.iterator(); ++ // Tuinity start - replace level propagator ++ ticket_update_loop: ++ while (!this.ticketLevelUpdates.isEmpty()) { ++ flag = true; + +- while (longiterator.hasNext()) { +- long j = longiterator.nextLong(); ++ boolean oldPolling = this.pollingPendingChunkUpdates; ++ this.pollingPendingChunkUpdates = true; ++ try { ++ for (java.util.Iterator iterator = this.ticketLevelUpdates.long2IntEntrySet().fastIterator(); iterator.hasNext();) { ++ Long2IntMap.Entry entry = iterator.next(); ++ long key = entry.getLongKey(); ++ int newLevel = entry.getIntValue(); ++ ChunkHolder chunk = this.getChunk(key); ++ ++ if (chunk == null && newLevel > ChunkMap.MAX_CHUNK_DISTANCE) { ++ // not loaded and it shouldn't be loaded! ++ continue; ++ } ++ ++ int currentLevel = chunk == null ? ChunkMap.MAX_CHUNK_DISTANCE + 1 : chunk.getTicketLevel(); ++ ++ if (currentLevel == newLevel) { ++ // nothing to do ++ continue; ++ } + +- if (this.getTickets(j).stream().anyMatch((ticket) -> { +- return ticket.getType() == TicketType.PLAYER; +- })) { +- ChunkHolder playerchunk = playerchunkmap.getUpdatingChunkIfPresent(j); ++ this.updateChunkScheduling(key, newLevel, chunk, currentLevel); ++ } + +- if (playerchunk == null) { +- throw new IllegalStateException(); ++ long recursiveCheck = ++this.ticketLevelUpdateCount; ++ while (!this.ticketLevelUpdates.isEmpty()) { ++ long key = this.ticketLevelUpdates.firstLongKey(); ++ int newLevel = this.ticketLevelUpdates.removeFirstInt(); ++ ChunkHolder chunk = this.getChunk(key); ++ ++ if (chunk == null) { ++ if (newLevel <= ChunkMap.MAX_CHUNK_DISTANCE) { ++ throw new IllegalStateException("Expected chunk holder to be created"); + } ++ // not loaded and it shouldn't be loaded! ++ continue; ++ } + +- CompletableFuture> completablefuture = playerchunk.getEntityTickingChunkFuture(); ++ int currentLevel = chunk.oldTicketLevel; + +- completablefuture.thenAccept((either) -> { +- this.mainThreadExecutor.execute(() -> { +- this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> { +- }, j, false)); +- }); +- }); ++ if (currentLevel == newLevel) { ++ // nothing to do ++ continue; ++ } ++ ++ chunk.updateFutures(playerchunkmap, this.mainThreadExecutor); ++ if (recursiveCheck != this.ticketLevelUpdateCount) { ++ // back to the start, we must create player chunks and update the ticket level fields before ++ // processing the actual level updates ++ continue ticket_update_loop; + } + } + +- this.ticketsToRelease.clear(); +- } ++ for (;;) { ++ if (recursiveCheck != this.ticketLevelUpdateCount) { ++ continue ticket_update_loop; ++ } ++ ChunkHolder pendingUpdate = this.pendingChunkUpdates.poll(); ++ if (pendingUpdate == null) { ++ break; ++ } + +- return flag; ++ pendingUpdate.updateFutures(playerchunkmap, this.mainThreadExecutor); ++ } ++ } finally { ++ this.pollingPendingChunkUpdates = oldPolling; ++ } + } ++ ++ return flag; ++ // Tuinity end - replace level propagator + } + boolean pollingPendingChunkUpdates = false; // Paper - Chunk priority + +@@ -175,7 +249,7 @@ public abstract class DistanceManager { + + ticket1.setCreatedTick(this.ticketTickCounter); + if (ticket.getTicketLevel() < j) { +- this.ticketTracker.update(i, ticket.getTicketLevel(), true); ++ this.updateTicketLevel(i, ticket.getTicketLevel()); // Tuinity - replace ticket level propagator + } + + return ticket == ticket1; // CraftBukkit +@@ -219,7 +293,7 @@ public abstract class DistanceManager { + // Paper start - Chunk priority + int newLevel = getTicketLevelAt(arraysetsorted); + if (newLevel > oldLevel) { +- this.ticketTracker.update(i, newLevel, false); ++ this.updateTicketLevel(i, newLevel); // Paper // Tuinity - replace ticket level propagator + } + // Paper end + return removed; // CraftBukkit +@@ -313,7 +387,7 @@ public abstract class DistanceManager { + org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::addPriorityTicket"); + long pair = coords.toLong(); + ChunkHolder chunk = chunkMap.getUpdatingChunkIfPresent(pair); +- boolean needsTicket = chunkMap.playerViewDistanceNoTickMap.getObjectsInRange(pair) != null && !hasPlayerTicket(coords, 33); ++ boolean needsTicket = false; // Tuinity - replace old loader system + + if (needsTicket) { + Ticket ticket = new Ticket<>(TicketType.PLAYER, 33, coords); +@@ -411,7 +485,7 @@ public abstract class DistanceManager { + return new ObjectOpenHashSet(); + })).add(player); + //this.f.update(i, 0, true); // Paper - no longer used +- this.playerTicketManager.update(i, 0, true); ++ //this.playerTicketManager.update(i, 0, true); // Tuinity - no longer used + } + + public void removePlayer(SectionPos pos, ServerPlayer player) { +@@ -423,7 +497,7 @@ public abstract class DistanceManager { + if (objectset == null || objectset.isEmpty()) { // Paper + this.playersPerChunk.remove(i); + //this.f.update(i, Integer.MAX_VALUE, false); // Paper - no longer used +- this.playerTicketManager.update(i, Integer.MAX_VALUE, false); ++ //this.playerTicketManager.update(i, Integer.MAX_VALUE, false); // Tuinity - no longer used + } + + } +@@ -442,7 +516,7 @@ public abstract class DistanceManager { + } + + protected void setNoTickViewDistance(int i) { // Paper - force abi breakage on usage change +- this.playerTicketManager.updateViewDistance(i); ++ throw new UnsupportedOperationException("use world api"); // Tuinity - no longer relevant + } + + public int getNaturalSpawnChunkCount() { +@@ -507,7 +581,7 @@ public abstract class DistanceManager { + SortedArraySet> tickets = entry.getValue(); + if (tickets.remove(target)) { + // copied from removeTicket +- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false); ++ this.updateTicketLevel(entry.getLongKey(), getTicketLevelAt(tickets)); // Tuinity - replace ticket level propagator + + // can't use entry after it's removed + if (tickets.isEmpty()) { +@@ -563,6 +637,7 @@ public abstract class DistanceManager { + } + } + ++ /* Tuinity - replace old loader system + private class FixedPlayerDistanceChunkTracker extends ChunkTracker { + + protected final Long2ByteMap chunks = new Long2ByteOpenHashMap(); +@@ -858,4 +933,5 @@ public abstract class DistanceManager { + } + // Paper end + } ++ */ // Tuinity - replace old loader system + } +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index c5d77b446afc162adb4b64d23f34596363b990b6..db39671881b622189961b39309a323a1b35d680e 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -47,6 +47,7 @@ import net.minecraft.world.level.storage.LevelStorageSource; + import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; // Paper + + public class ServerChunkCache extends ChunkSource { ++ public static final org.apache.logging.log4j.Logger LOGGER = org.apache.logging.log4j.LogManager.getLogger(); // Tuinity + + public static final List CHUNK_STATUSES = ChunkStatus.getStatusList(); + private final DistanceManager distanceManager; +@@ -138,7 +139,7 @@ public class ServerChunkCache extends ChunkSource { + return (LevelChunk)this.getChunk(x, z, ChunkStatus.FULL, true); + } + +- private long chunkFutureAwaitCounter; ++ long chunkFutureAwaitCounter; // Tuinity - private -> package private + + public void getEntityTickingChunkAsync(int x, int z, java.util.function.Consumer onLoad) { + if (Thread.currentThread() != this.mainThread) { +@@ -200,9 +201,9 @@ public class ServerChunkCache extends ChunkSource { + + try { + if (onLoad != null) { +- chunkMap.callbackExecutor.execute(() -> { ++ // Tuinity - revert incorrect use of callback executor + onLoad.accept(either == null ? null : either.left().orElse(null)); // indicate failure to the callback. +- }); ++ // Tuinity - revert incorrect use of callback executor + } + } catch (Throwable thr) { + if (thr instanceof ThreadDeath) { +@@ -226,6 +227,166 @@ public class ServerChunkCache extends ChunkSource { + } + // Paper end - rewrite ticklistserver + ++ // Tuinity start ++ // this will try to avoid chunk neighbours for lighting ++ public final ChunkAccess getFullStatusChunkAt(int chunkX, int chunkZ) { ++ LevelChunk ifLoaded = this.getChunkAtIfLoadedImmediately(chunkX, chunkZ); ++ if (ifLoaded != null) { ++ return ifLoaded; ++ } ++ ++ ChunkAccess empty = this.getChunk(chunkX, chunkZ, ChunkStatus.EMPTY, true); ++ if (empty != null && empty.getStatus().isOrAfter(ChunkStatus.FULL)) { ++ return empty; ++ } ++ return this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true); ++ } ++ ++ public final ChunkAccess getFullStatusChunkAtIfLoaded(int chunkX, int chunkZ) { ++ LevelChunk ifLoaded = this.getChunkAtIfLoadedImmediately(chunkX, chunkZ); ++ if (ifLoaded != null) { ++ return ifLoaded; ++ } ++ ++ ChunkAccess ret = this.getChunkAtImmediately(chunkX, chunkZ); ++ if (ret != null && ret.getStatus().isOrAfter(ChunkStatus.FULL)) { ++ return ret; ++ } else { ++ return null; ++ } ++ } ++ ++ void getChunkAtAsynchronously(int chunkX, int chunkZ, int ticketLevel, ++ java.util.function.Consumer consumer) { ++ this.getChunkAtAsynchronously(chunkX, chunkZ, ticketLevel, (ChunkHolder chunkHolder) -> { ++ if (ticketLevel <= 33) { ++ return (CompletableFuture)chunkHolder.getFullChunkFuture(); ++ } else { ++ return chunkHolder.getOrScheduleFuture(ChunkHolder.getStatus(ticketLevel), ServerChunkCache.this.chunkMap); ++ } ++ }, consumer); ++ } ++ ++ void getChunkAtAsynchronously(int chunkX, int chunkZ, int ticketLevel, ++ java.util.function.Function>> function, ++ java.util.function.Consumer consumer) { ++ if (Thread.currentThread() != this.mainThread) { ++ throw new IllegalStateException(); ++ } ++ ++ ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); ++ Long identifier = Long.valueOf(this.chunkFutureAwaitCounter++); ++ this.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, identifier); ++ this.runDistanceManagerUpdates(); ++ ++ ChunkHolder chunk = this.chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong()); ++ ++ if (chunk == null) { ++ throw new IllegalStateException("Expected playerchunk " + chunkPos + " in world '" + this.level.getWorld().getName() + "'"); ++ } ++ ++ CompletableFuture> future = function.apply(chunk); ++ ++ future.whenCompleteAsync((either, throwable) -> { ++ try { ++ if (throwable != null) { ++ if (throwable instanceof ThreadDeath) { ++ throw (ThreadDeath)throwable; ++ } ++ LOGGER.fatal("Failed to complete future await for chunk " + chunkPos.toString() + " in world '" + ServerChunkCache.this.level.getWorld().getName() + "'", throwable); ++ } else if (either.right().isPresent()) { ++ LOGGER.fatal("Failed to complete future await for chunk " + chunkPos.toString() + " in world '" + ServerChunkCache.this.level.getWorld().getName() + "': " + either.right().get().toString()); ++ } ++ ++ try { ++ if (consumer != null) { ++ consumer.accept(either == null ? null : either.left().orElse(null)); // indicate failure to the callback. ++ } ++ } catch (Throwable thr) { ++ if (thr instanceof ThreadDeath) { ++ throw (ThreadDeath)thr; ++ } ++ LOGGER.fatal("Load callback for future await failed " + chunkPos.toString() + " in world '" + ServerChunkCache.this.level.getWorld().getName() + "'", thr); ++ return; ++ } ++ } finally { ++ // due to odd behaviour with CB unload implementation we need to have these AFTER the load callback. ++ ServerChunkCache.this.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); ++ ServerChunkCache.this.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, identifier); ++ } ++ }, this.mainThreadProcessor); ++ } ++ ++ void chunkLoadAccept(int chunkX, int chunkZ, ChunkAccess chunk, java.util.function.Consumer consumer) { ++ try { ++ consumer.accept(chunk); ++ } catch (Throwable throwable) { ++ if (throwable instanceof ThreadDeath) { ++ throw (ThreadDeath)throwable; ++ } ++ LOGGER.error("Load callback for chunk " + chunkX + "," + chunkZ + " in world '" + this.level.getWorld().getName() + "' threw an exception", throwable); ++ } ++ } ++ ++ public final void getChunkAtAsynchronously(int chunkX, int chunkZ, ChunkStatus status, boolean gen, boolean allowSubTicketLevel, java.util.function.Consumer onLoad) { ++ // try to fire sync ++ int chunkStatusTicketLevel = 33 + ChunkStatus.getDistance(status); ++ ChunkHolder playerChunk = this.chunkMap.getUpdatingChunkIfPresent(com.tuinity.tuinity.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ if (playerChunk != null) { ++ ChunkStatus holderStatus = playerChunk.getChunkHolderStatus(); ++ ChunkAccess immediate = playerChunk.getAvailableChunkNow(); ++ if (immediate != null) { ++ if (allowSubTicketLevel ? immediate.getStatus().isOrAfter(status) : (playerChunk.getTicketLevel() <= chunkStatusTicketLevel && holderStatus != null && holderStatus.isOrAfter(status))) { ++ this.chunkLoadAccept(chunkX, chunkZ, immediate, onLoad); ++ return; ++ } else { ++ if (gen || (!allowSubTicketLevel && immediate.getStatus().isOrAfter(status))) { ++ this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); ++ return; ++ } else { ++ this.chunkLoadAccept(chunkX, chunkZ, null, onLoad); ++ return; ++ } ++ } ++ } ++ } ++ ++ // need to fire async ++ ++ if (gen && !allowSubTicketLevel) { ++ this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); ++ return; ++ } ++ ++ this.getChunkAtAsynchronously(chunkX, chunkZ, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.EMPTY), (ChunkAccess chunk) -> { ++ if (chunk == null) { ++ throw new IllegalStateException("Chunk cannot be null"); ++ } ++ ++ if (!chunk.getStatus().isOrAfter(status)) { ++ if (gen) { ++ this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); ++ return; ++ } else { ++ ServerChunkCache.this.chunkLoadAccept(chunkX, chunkZ, null, onLoad); ++ return; ++ } ++ } else { ++ if (allowSubTicketLevel) { ++ ServerChunkCache.this.chunkLoadAccept(chunkX, chunkZ, chunk, onLoad); ++ return; ++ } else { ++ this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); ++ return; ++ } ++ } ++ }); ++ } ++ ++ final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet tickingChunks = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); ++ final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet entityTickingChunks = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); ++ // Tuinity end ++ + public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureManager structureManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, boolean flag, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkstatusupdatelistener, Supplier supplier) { + this.level = world; + this.mainThreadProcessor = new ServerChunkCache.MainThreadExecutor(world); +@@ -569,6 +730,8 @@ public class ServerChunkCache extends ChunkSource { + return completablefuture; + } + ++ private long syncLoadCounter; // Tuinity - prevent plugin unloads from removing our ticket ++ + private CompletableFuture> getChunkFutureMainThread(int i, int j, ChunkStatus chunkstatus, boolean flag) { + // Paper start - add isUrgent - old sig left in place for dirty nms plugins + return getChunkFutureMainThread(i, j, chunkstatus, flag, false); +@@ -587,9 +750,12 @@ public class ServerChunkCache extends ChunkSource { + ChunkHolder.FullChunkStatus currentChunkState = ChunkHolder.getFullChunkStatus(playerchunk.getTicketLevel()); + currentlyUnloading = (oldChunkState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && !currentChunkState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)); + } ++ final Long identifier; // Tuinity - prevent plugin unloads from removing our ticket + if (flag && !currentlyUnloading) { + // CraftBukkit end + this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair); ++ identifier = Long.valueOf(this.syncLoadCounter++); // Tuinity - prevent plugin unloads from removing our ticket ++ this.distanceManager.addTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); // Tuinity - prevent plugin unloads from removing our ticket + if (isUrgent) this.distanceManager.markUrgent(chunkcoordintpair); // Paper - Chunk priority + if (this.chunkAbsent(playerchunk, l)) { + ProfilerFiller gameprofilerfiller = this.level.getProfiler(); +@@ -600,12 +766,20 @@ public class ServerChunkCache extends ChunkSource { + playerchunk = this.getVisibleChunkIfPresent(k); + gameprofilerfiller.pop(); + if (this.chunkAbsent(playerchunk, l)) { ++ this.distanceManager.removeTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); // Tuinity + throw (IllegalStateException) Util.pauseInIde((Throwable) (new IllegalStateException("No chunk holder after ticket has been added"))); + } + } +- } ++ } else { identifier = null; } // Tuinity - prevent plugin unloads from removing our ticket + // Paper start - Chunk priority + CompletableFuture> future = this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(chunkstatus, this.chunkMap); ++ // Tuinity start - prevent plugin unloads from removing our ticket ++ if (flag && !currentlyUnloading) { ++ future.thenAcceptAsync((either) -> { ++ ServerChunkCache.this.distanceManager.removeTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); ++ }, ServerChunkCache.this.mainThreadProcessor); ++ } ++ // Tuinity end - prevent plugin unloads from removing our ticket + if (isUrgent) { + future.thenAccept(either -> this.distanceManager.clearUrgent(chunkcoordintpair)); + } +@@ -663,6 +837,8 @@ public class ServerChunkCache extends ChunkSource { + + public boolean runDistanceManagerUpdates() { + if (distanceManager.delayDistanceManagerTick) return false; // Paper - Chunk priority ++ if (this.chunkMap.unloadingPlayerChunk) { LOGGER.fatal("Cannot tick distance manager while unloading playerchunks", new Throwable()); throw new IllegalStateException("Cannot tick distance manager while unloading playerchunks"); } // Tuinity ++ co.aikar.timings.MinecraftTimings.distanceManagerTick.startTiming(); try { // Tuinity - add timings for distance manager + boolean flag = this.distanceManager.runAllUpdates(this.chunkMap); + boolean flag1 = this.chunkMap.promoteChunkMap(); + +@@ -672,6 +848,7 @@ public class ServerChunkCache extends ChunkSource { + this.clearCache(); + return true; + } ++ } finally { co.aikar.timings.MinecraftTimings.distanceManagerTick.stopTiming(); } // Tuinity - add timings for distance manager + } + + // Paper start - helper +@@ -729,6 +906,7 @@ public class ServerChunkCache extends ChunkSource { + + // CraftBukkit start - modelled on below + public void purgeUnload() { ++ if (true) return; // Tuinity - tickets will be removed later, this behavior isn't really well accounted for by the chunk system + this.level.getProfiler().push("purge"); + this.distanceManager.purgeStaleTickets(); + this.runDistanceManagerUpdates(); +@@ -744,17 +922,18 @@ public class ServerChunkCache extends ChunkSource { + this.level.getProfiler().push("purge"); + this.level.timings.doChunkMap.startTiming(); // Spigot + this.distanceManager.purgeStaleTickets(); +- this.level.getServer().midTickLoadChunks(); // Paper ++ // Tuinity - replace logic + this.runDistanceManagerUpdates(); + this.level.timings.doChunkMap.stopTiming(); // Spigot + this.level.getProfiler().popPush("chunks"); + this.level.timings.chunks.startTiming(); // Paper - timings ++ this.chunkMap.playerChunkManager.tick(); // Tuinity - this is mostly is to account for view distance changes + this.tickChunks(); + this.level.timings.chunks.stopTiming(); // Paper - timings + this.level.timings.doChunkUnload.startTiming(); // Spigot + this.level.getProfiler().popPush("unload"); + this.chunkMap.tick(booleansupplier); +- this.level.getServer().midTickLoadChunks(); // Paper ++ // Tuinity - replace logic + this.level.timings.doChunkUnload.stopTiming(); // Spigot + this.level.getProfiler().pop(); + this.clearCache(); +@@ -832,18 +1011,26 @@ public class ServerChunkCache extends ChunkSource { + //Collections.shuffle(list); // Paper + // Paper - moved up + this.level.timings.chunkTicks.startTiming(); // Paper +- final int[] chunksTicked = {0}; this.chunkMap.forEachVisibleChunk((playerchunk) -> { // Paper - safe iterator incase chunk loads, also no wrapping +- Optional optional = ((Either) playerchunk.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).left(); +- +- if (optional.isPresent()) { +- LevelChunk chunk = (LevelChunk) optional.get(); ++ // Tuinity start ++ int chunksTicked = 0; ++ com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator iterator = this.entityTickingChunks.iterator(); ++ try { while (iterator.hasNext()) { ++ LevelChunk chunk = iterator.next(); ++ ChunkHolder playerchunk = chunk.playerChunk; ++ if (playerchunk != null) { ++ this.level.getProfiler().push("broadcast"); ++ this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timings ++ playerchunk.broadcastChanges(chunk); ++ this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timings ++ this.level.getProfiler().pop(); ++ // Tuinity end + ChunkPos chunkcoordintpair = chunk.getPos(); + +- if (this.level.isPositionEntityTicking(chunkcoordintpair) && !this.chunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, false)) { // Paper - optimise isOutsideOfRange ++ if ((true || this.level.isPositionEntityTicking(chunkcoordintpair)) && !this.chunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, false)) { // Paper - optimise isOutsideOfRange // Tuinity - we only iterate entity ticking chunks + chunk.setInhabitedTime(chunk.getInhabitedTime() + j); + if (flag1 && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunk.getPos()) && !this.chunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, true)) { // Spigot // Paper - optimise isOutsideOfRange + NaturalSpawner.spawnForChunk(this.level, chunk, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag2); +- if (chunksTicked[0]++ % 10 == 0) this.level.getServer().midTickLoadChunks(); // Paper ++ if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper // Tuinity + } + + // this.level.timings.doTickTiles.startTiming(); // Spigot // Paper +@@ -851,7 +1038,11 @@ public class ServerChunkCache extends ChunkSource { + // this.level.timings.doTickTiles.stopTiming(); // Spigot // Paper + } + } +- }); ++ } // Tuinity start - optimise chunk tick iteration ++ } finally { ++ iterator.finishedIterating(); ++ } ++ // Tuinity end - optimise chunk tick iteration + this.level.timings.chunkTicks.stopTiming(); // Paper + this.level.getProfiler().push("customSpawners"); + if (flag1) { +@@ -860,25 +1051,28 @@ public class ServerChunkCache extends ChunkSource { + } // Paper - timings + } + +- this.level.getProfiler().popPush("broadcast"); +- this.chunkMap.forEachVisibleChunk((playerchunk) -> { // Paper - safe iterator incase chunk loads, also no wrapping +- Optional optional = ((Either) playerchunk.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).left(); // CraftBukkit - decompile error +- +- Objects.requireNonNull(playerchunk); +- +- // Paper start - timings +- optional.ifPresent(chunk -> { +- this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timings +- playerchunk.broadcastChanges(chunk); +- this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timings +- }); +- // Paper end +- }); +- this.level.getProfiler().pop(); ++ // Tuinity - no, iterating just ONCE is expensive enough! Don't do it TWICE! Code moved up + this.level.getProfiler().pop(); + } + ++ // Tuinity start - controlled flush for entity tracker packets ++ List disabledFlushes = new java.util.ArrayList<>(this.level.players.size()); ++ for (ServerPlayer player : this.level.players) { ++ net.minecraft.server.network.ServerGamePacketListenerImpl connection = player.connection; ++ if (connection != null) { ++ connection.connection.disableAutomaticFlush(); ++ disabledFlushes.add(connection.connection); ++ } ++ } ++ try { // Tuinity end - controlled flush for entity tracker packets + this.chunkMap.tick(); ++ // Tuinity start - controlled flush for entity tracker packets ++ } finally { ++ for (net.minecraft.network.Connection networkManager : disabledFlushes) { ++ networkManager.enableAutomaticFlush(); ++ } ++ } ++ // Tuinity end - controlled flush for entity tracker packets + } + + private void getFullChunk(long pos, Consumer chunkConsumer) { +@@ -1025,46 +1219,14 @@ public class ServerChunkCache extends ChunkSource { + super.doRunTask(task); + } + +- // Paper start +- private long lastMidTickChunkTask = 0; +- public boolean pollChunkLoadTasks() { +- if (com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue() || ServerChunkCache.this.level.asyncChunkTaskManager.pollNextChunkTask()) { +- try { +- ServerChunkCache.this.runDistanceManagerUpdates(); +- } finally { +- // from below: process pending Chunk loadCallback() and unloadCallback() after each run task +- chunkMap.callbackExecutor.run(); +- } +- return true; +- } +- return false; +- } +- public void midTickLoadChunks() { +- net.minecraft.server.MinecraftServer server = ServerChunkCache.this.level.getServer(); +- // always try to load chunks, restrain generation/other updates only. don't count these towards tick count +- //noinspection StatementWithEmptyBody +- while (pollChunkLoadTasks()) {} +- +- if (System.nanoTime() - lastMidTickChunkTask < 200000) { +- return; +- } +- +- for (;server.midTickChunksTasksRan < com.destroystokyo.paper.PaperConfig.midTickChunkTasks && server.haveTime();) { +- if (this.pollTask()) { +- server.midTickChunksTasksRan++; +- lastMidTickChunkTask = System.nanoTime(); +- } else { +- break; +- } +- } +- } +- // Paper end ++ // Tuinity - replace logic + + @Override + // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task + public boolean pollTask() { + try { + boolean execChunkTask = com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue() || ServerChunkCache.this.level.asyncChunkTaskManager.pollNextChunkTask(); // Paper ++ ServerChunkCache.this.chunkMap.playerChunkManager.tickMidTick(); // Tuinity + if (ServerChunkCache.this.runDistanceManagerUpdates()) { + return true; + } else { +diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java +index 2f3e69ad809199ffc2661d524bb627ec8dbc2e80..0fcd6a9162f5bddb3c4fc42b3a64efde7c7d9a9b 100644 +--- a/src/main/java/net/minecraft/server/level/ServerEntity.java ++++ b/src/main/java/net/minecraft/server/level/ServerEntity.java +@@ -173,7 +173,7 @@ public class ServerEntity { + // Paper end - remove allocation of Vec3D here + boolean flag4 = k < -32768L || k > 32767L || l < -32768L || l > 32767L || i1 < -32768L || i1 > 32767L; + +- if (!flag4 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.isOnGround()) { ++ if (!flag4 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.isOnGround() && !(com.tuinity.tuinity.config.TuinityConfig.sendFullPosForHardCollidingEntities && this.entity.hardCollides())) { // Tuinity - send full pos for hard colliding entities to prevent collision problems due to desync + if ((!flag2 || !flag3) && !(this.entity instanceof AbstractArrow)) { + if (flag2) { + packet1 = new ClientboundMoveEntityPacket.Pos(this.entity.getId(), (short) ((int) k), (short) ((int) l), (short) ((int) i1), this.entity.isOnGround()); +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 6ecf60c69a27f8db1c245db15449bba581c3dbf5..7642170bf5a0eaa11110238fa5cf1a7e1ff20a20 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -115,6 +115,7 @@ import net.minecraft.world.level.block.EntityBlock; + import net.minecraft.world.level.block.entity.BlockEntity; + import net.minecraft.world.level.block.entity.TickingBlockEntity; + import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.ChunkAccess; + import net.minecraft.world.level.chunk.ChunkGenerator; + import net.minecraft.world.level.chunk.LevelChunk; + import net.minecraft.world.level.chunk.LevelChunkSection; +@@ -165,6 +166,7 @@ import org.bukkit.event.server.MapInitializeEvent; + import org.bukkit.event.weather.LightningStrikeEvent; + import org.bukkit.event.world.TimeSkipEvent; + // CraftBukkit end ++import it.unimi.dsi.fastutil.ints.IntArrayList; // Tuinity + + public class ServerLevel extends net.minecraft.world.level.Level implements WorldGenLevel { + +@@ -193,7 +195,9 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + final Int2ObjectMap dragonParts; + private final StructureFeatureManager structureFeatureManager; + private final boolean tickTime; +- ++ // Tuinity start - execute chunk tasks mid tick ++ public long lastMidTickExecuteFailure; ++ // Tuinity end - execute chunk tasks mid tick + + // CraftBukkit start + private int tickPosition; +@@ -304,6 +308,172 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + } + } + // Paper end - rewrite ticklistserver ++ // Tuinity start ++ public final boolean areChunksLoadedForMove(AABB axisalignedbb) { ++ // copied code from collision methods, so that we can guarantee that they wont load chunks (we don't override ++ // ICollisionAccess methods for VoxelShapes) ++ // be more strict too, add a block (dumb plugins in move events?) ++ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; ++ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3; ++ ++ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; ++ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; ++ ++ int minChunkX = minBlockX >> 4; ++ int maxChunkX = maxBlockX >> 4; ++ ++ int minChunkZ = minBlockZ >> 4; ++ int maxChunkZ = maxBlockZ >> 4; ++ ++ ServerChunkCache chunkProvider = this.getChunkSource(); ++ ++ for (int cx = minChunkX; cx <= maxChunkX; ++cx) { ++ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { ++ if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ public final void loadChunksForMoveAsync(AABB axisalignedbb, double toX, double toZ, ++ java.util.function.Consumer> onLoad) { ++ if (Thread.currentThread() != this.thread) { ++ this.getChunkSource().mainThreadProcessor.execute(() -> { ++ this.loadChunksForMoveAsync(axisalignedbb, toX, toZ, onLoad); ++ }); ++ return; ++ } ++ List ret = new java.util.ArrayList<>(); ++ IntArrayList ticketLevels = new IntArrayList(); ++ ++ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; ++ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3; ++ ++ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; ++ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; ++ ++ int minChunkX = minBlockX >> 4; ++ int maxChunkX = maxBlockX >> 4; ++ ++ int minChunkZ = minBlockZ >> 4; ++ int maxChunkZ = maxBlockZ >> 4; ++ ++ ServerChunkCache chunkProvider = this.getChunkSource(); ++ ++ int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1); ++ int[] loadedChunks = new int[1]; ++ ++ Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++); ++ ++ java.util.function.Consumer consumer = (ChunkAccess chunk) -> { ++ if (chunk != null) { ++ int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel()); ++ ret.add(chunk); ++ ticketLevels.add(ticketLevel); ++ chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier); ++ } ++ if (++loadedChunks[0] == requiredChunks) { ++ try { ++ onLoad.accept(java.util.Collections.unmodifiableList(ret)); ++ } finally { ++ for (int i = 0, len = ret.size(); i < len; ++i) { ++ ChunkPos chunkPos = ret.get(i).getPos(); ++ int ticketLevel = ticketLevels.getInt(i); ++ ++ chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); ++ chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier); ++ } ++ } ++ } ++ }; ++ ++ for (int cx = minChunkX; cx <= maxChunkX; ++cx) { ++ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { ++ chunkProvider.getChunkAtAsynchronously(cx, cz, net.minecraft.world.level.chunk.ChunkStatus.FULL, true, false, consumer); ++ } ++ } ++ } ++ // Tuinity end ++ // Tuinity start - optimise checkDespawn ++ public final List playersAffectingSpawning = new java.util.ArrayList<>(); ++ // Tuinity end - optimise checkDespawn ++ // Tuinity start - optimise get nearest players for entity AI ++ @Override ++ public final ServerPlayer getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions condition, @Nullable LivingEntity source, ++ double centerX, double centerY, double centerZ) { ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby; ++ nearby = this.getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(Mth.floor(centerX) >> 4, Mth.floor(centerZ) >> 4); ++ ++ if (nearby == null) { ++ return null; ++ } ++ ++ Object[] backingSet = nearby.getBackingSet(); ++ ++ double closestDistanceSquared = Double.MAX_VALUE; ++ ServerPlayer closest = null; ++ ++ for (int i = 0, len = backingSet.length; i < len; ++i) { ++ Object _player = backingSet[i]; ++ if (!(_player instanceof ServerPlayer)) { ++ continue; ++ } ++ ServerPlayer player = (ServerPlayer)_player; ++ ++ double distanceSquared = player.distanceToSqr(centerX, centerY, centerZ); ++ if (distanceSquared < closestDistanceSquared && condition.test(source, player)) { ++ closest = player; ++ closestDistanceSquared = distanceSquared; ++ } ++ } ++ ++ return closest; ++ } ++ ++ @Override ++ public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions pathfindertargetcondition, LivingEntity entityliving) { ++ return this.getNearestPlayer(pathfindertargetcondition, entityliving, entityliving.getX(), entityliving.getY(), entityliving.getZ()); ++ } ++ ++ @Override ++ public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions pathfindertargetcondition, ++ double d0, double d1, double d2) { ++ return this.getNearestPlayer(pathfindertargetcondition, null, d0, d1, d2); ++ } ++ ++ @Override ++ public List getNearbyPlayers(net.minecraft.world.entity.ai.targeting.TargetingConditions condition, LivingEntity source, AABB axisalignedbb) { ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby; ++ double centerX = (axisalignedbb.maxX + axisalignedbb.minX) * 0.5; ++ double centerZ = (axisalignedbb.maxZ + axisalignedbb.minZ) * 0.5; ++ nearby = this.getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(Mth.floor(centerX) >> 4, Mth.floor(centerZ) >> 4); ++ ++ List ret = new java.util.ArrayList<>(); ++ ++ if (nearby == null) { ++ return ret; ++ } ++ ++ Object[] backingSet = nearby.getBackingSet(); ++ ++ for (int i = 0, len = backingSet.length; i < len; ++i) { ++ Object _player = backingSet[i]; ++ if (!(_player instanceof ServerPlayer)) { ++ continue; ++ } ++ ServerPlayer player = (ServerPlayer)_player; ++ ++ if (axisalignedbb.contains(player.getX(), player.getY(), player.getZ()) && condition.test(source, player)) { ++ ret.add(player); ++ } ++ } ++ ++ return ret; ++ } ++ // Tuinity end - optimise get nearest players for entity AI + + // Add env and gen to constructor, WorldData -> WorldDataServer + public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, ServerLevelData iworlddataserver, ResourceKey resourcekey, DimensionType dimensionmanager, ChunkProgressListener worldloadlistener, ChunkGenerator chunkgenerator, boolean flag, long i, List list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen) { +@@ -351,7 +521,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + DataFixer datafixer = minecraftserver.getFixerUpper(); + EntityPersistentStorage entitypersistentstorage = new EntityStorage(this, new File(convertable_conversionsession.getDimensionPath(resourcekey), "entities"), datafixer, flag2, minecraftserver); + +- this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage); ++ this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage, this.entitySliceManager); // Tuinity + StructureManager definedstructuremanager = minecraftserver.getStructureManager(); + int j = this.spigotConfig.viewDistance; // Spigot + PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; +@@ -386,6 +556,10 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + this.asyncChunkTaskManager = new com.destroystokyo.paper.io.chunk.ChunkTaskManager(this); // Paper + } + ++ // Tuinity start - optimise collision ++ ++ // Tuinity end - optimise collision ++ + // CraftBukkit start + @Override + public BlockEntity getTileEntity(BlockPos pos, boolean validate) { +@@ -439,6 +613,14 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + } + + public void tick(BooleanSupplier shouldKeepTicking) { ++ // Tuinity start - optimise checkDespawn ++ this.playersAffectingSpawning.clear(); ++ for (ServerPlayer player : this.players) { ++ if (net.minecraft.world.entity.EntitySelector.affectsSpawning.test(player)) { ++ this.playersAffectingSpawning.add(player); ++ } ++ } ++ // Tuinity end - optimise checkDespawn + ProfilerFiller gameprofilerfiller = this.getProfiler(); + + this.handlingTick = true; +@@ -584,7 +766,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + } + timings.scheduledBlocks.stopTiming(); // Paper + +- this.getServer().midTickLoadChunks(); // Paper ++ // Tuinity - replace logic + gameprofilerfiller.popPush("raid"); + this.timings.raids.startTiming(); // Paper - timings + this.raids.tick(); +@@ -597,7 +779,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + timings.doSounds.startTiming(); // Spigot + this.runBlockEvents(); + timings.doSounds.stopTiming(); // Spigot +- this.getServer().midTickLoadChunks(); // Paper ++ // Tuinity - replace logic + this.handlingTick = false; + gameprofilerfiller.pop(); + boolean flag3 = true || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players +@@ -644,12 +826,12 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + timings.entityTick.stopTiming(); // Spigot + timings.tickEntities.stopTiming(); // Spigot + gameprofilerfiller.pop(); +- this.getServer().midTickLoadChunks(); // Paper ++ // Tuinity - replace logic + this.tickBlockEntities(); + } + + gameprofilerfiller.push("entityManagement"); +- this.getServer().midTickLoadChunks(); // Paper ++ // Tuinity - replace logic + this.entityManager.tick(); + gameprofilerfiller.pop(); + } +@@ -694,6 +876,10 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + entityplayer.stopSleepInBed(false, false); + }); + } ++ // Paper start - optimise random block ticking ++ private final BlockPos.MutableBlockPos chunkTickMutablePosition = new BlockPos.MutableBlockPos(); ++ private final com.tuinity.tuinity.util.math.ThreadUnsafeRandom randomTickRandom = new com.tuinity.tuinity.util.math.ThreadUnsafeRandom(); ++ // Paper end + + public void tickChunk(LevelChunk chunk, int randomTickSpeed) { + ChunkPos chunkcoordintpair = chunk.getPos(); +@@ -703,10 +889,10 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + ProfilerFiller gameprofilerfiller = this.getProfiler(); + + gameprofilerfiller.push("thunder"); +- BlockPos blockposition; ++ final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition; // Paper - use mutable to reduce allocation rate, final to force compile fail on change + + if (!this.paperConfig.disableThunder && flag && this.isThundering() && this.random.nextInt(100000) == 0) { // Paper - Disable thunder +- blockposition = this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15)); ++ blockposition.set(this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15))); // Paper + if (this.isRainingAt(blockposition)) { + DifficultyInstance difficultydamagescaler = this.getCurrentDifficultyAt(blockposition); + boolean flag1 = this.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && this.random.nextDouble() < (double) difficultydamagescaler.getEffectiveDifficulty() * paperConfig.skeleHorseSpawnChance && !this.getBlockState(blockposition.below()).is(Blocks.LIGHTNING_ROD); // Paper +@@ -729,64 +915,78 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + } + + gameprofilerfiller.popPush("iceandsnow"); +- if (!this.paperConfig.disableIceAndSnow && this.random.nextInt(16) == 0) { // Paper - Disable ice and snow +- blockposition = this.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, this.getBlockRandomPos(j, 0, k, 15)); +- BlockPos blockposition1 = blockposition.below(); ++ if (!this.paperConfig.disableIceAndSnow && this.randomTickRandom.nextInt(16) == 0) { // Paper - Disable ice and snow // Paper - optimise random ticking ++ // Paper start - optimise chunk ticking ++ this.getRandomBlockPosition(j, 0, k, 15, blockposition); ++ int normalY = chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, blockposition.getX() & 15, blockposition.getZ() & 15) + 1; ++ int downY = normalY - 1; ++ blockposition.setY(normalY); ++ // Paper end + Biome biomebase = this.getBiome(blockposition); + +- if (biomebase.shouldFreeze((LevelReader) this, blockposition1)) { +- org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition1, Blocks.ICE.defaultBlockState(), null); // CraftBukkit ++ // Paper start - optimise chunk ticking ++ blockposition.setY(downY); ++ if (biomebase.shouldFreeze(this, blockposition)) { ++ org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition, Blocks.ICE.defaultBlockState(), null); // CraftBukkit ++ // Paper end + } + + if (flag) { ++ blockposition.setY(normalY); // Paper + if (biomebase.shouldSnow(this, blockposition)) { + org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition, Blocks.SNOW.defaultBlockState(), null); // CraftBukkit + } + +- BlockState iblockdata = this.getBlockState(blockposition1); ++ blockposition.setY(downY); // Paper ++ BlockState iblockdata = this.getBlockState(blockposition); // Paper ++ blockposition.setY(normalY); // Paper + Biome.Precipitation biomebase_precipitation = this.getBiome(blockposition).getPrecipitation(); + +- if (biomebase_precipitation == Biome.Precipitation.RAIN && biomebase.isColdEnoughToSnow(blockposition1)) { ++ blockposition.setY(downY); // Paper ++ if (biomebase_precipitation == Biome.Precipitation.RAIN && biomebase.isColdEnoughToSnow(blockposition)) { // Paper + biomebase_precipitation = Biome.Precipitation.SNOW; + } + +- iblockdata.getBlock().handlePrecipitation(iblockdata, (net.minecraft.world.level.Level) this, blockposition1, biomebase_precipitation); ++ iblockdata.getBlock().handlePrecipitation(iblockdata, (net.minecraft.world.level.Level) this, blockposition, biomebase_precipitation); // Paper + } + } + +- gameprofilerfiller.popPush("tickBlocks"); ++ // Paper start - optimise random block ticking ++ gameprofilerfiller.popPush("randomTick"); + timings.chunkTicksBlocks.startTiming(); // Paper + if (randomTickSpeed > 0) { +- LevelChunkSection[] achunksection = chunk.getSections(); +- int l = achunksection.length; +- +- for (int i1 = 0; i1 < l; ++i1) { +- LevelChunkSection chunksection = achunksection[i1]; +- +- if (chunksection != LevelChunk.EMPTY_SECTION && chunksection.isRandomlyTicking()) { +- int j1 = chunksection.bottomBlockY(); +- +- for (int k1 = 0; k1 < randomTickSpeed; ++k1) { +- BlockPos blockposition2 = this.getBlockRandomPos(j, j1, k, 15); +- +- gameprofilerfiller.push("randomTick"); +- BlockState iblockdata1 = chunksection.getBlockState(blockposition2.getX() - j, blockposition2.getY() - j1, blockposition2.getZ() - k); ++ LevelChunkSection[] sections = chunk.getSections(); ++ int minSection = com.tuinity.tuinity.util.WorldUtil.getMinSection(this); ++ for (int sectionIndex = 0; sectionIndex < sections.length; ++sectionIndex) { ++ LevelChunkSection section = sections[sectionIndex]; ++ if (section == null || section.tickingList.size() == 0) { ++ continue; ++ } + +- if (iblockdata1.isRandomlyTicking()) { +- iblockdata1.randomTick(this, blockposition2, this.random); +- } ++ int yPos = (sectionIndex + minSection) << 4; ++ for (int a = 0; a < randomTickSpeed; ++a) { ++ int tickingBlocks = section.tickingList.size(); ++ int index = this.randomTickRandom.nextInt(16 * 16 * 16); ++ if (index >= tickingBlocks) { ++ continue; ++ } + +- FluidState fluid = iblockdata1.getFluidState(); ++ long raw = section.tickingList.getRaw(index); ++ int location = com.destroystokyo.paper.util.maplist.IBlockDataList.getLocationFromRaw(raw); ++ int randomX = location & 15; ++ int randomY = ((location >>> (4 + 4)) & 255) | yPos; ++ int randomZ = (location >>> 4) & 15; + +- if (fluid.isRandomlyTicking()) { +- fluid.randomTick(this, blockposition2, this.random); +- } ++ BlockPos blockposition2 = blockposition.set(j + randomX, randomY, k + randomZ); ++ BlockState iblockdata = com.destroystokyo.paper.util.maplist.IBlockDataList.getBlockDataFromRaw(raw); + +- gameprofilerfiller.pop(); +- } ++ iblockdata.randomTick(this, blockposition2, this.randomTickRandom); ++ // We drop the fluid tick since LAVA is ALREADY TICKED by the above method (See LiquidBlock). ++ // TODO CHECK ON UPDATE + } + } + } ++ // Paper end - optimise random block ticking + timings.chunkTicksBlocks.stopTiming(); // Paper + gameprofilerfiller.pop(); + } +@@ -912,7 +1112,27 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + + } + ++ // Tuinity start - log detailed entity tick information ++ // TODO replace with varhandle ++ static final java.util.concurrent.atomic.AtomicReference currentlyTickingEntity = new java.util.concurrent.atomic.AtomicReference<>(); ++ ++ public static List getCurrentlyTickingEntities() { ++ Entity ticking = currentlyTickingEntity.get(); ++ List ret = java.util.Arrays.asList(ticking == null ? new Entity[0] : new Entity[] { ticking }); ++ ++ return ret; ++ } ++ // Tuinity end - log detailed entity tick information ++ + public void tickNonPassenger(Entity entity) { ++ // Tuinity start - log detailed entity tick information ++ com.tuinity.tuinity.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); ++ this.entityManager.updateNavigatorsInRegion(entity); // Tuinity - optimise notify ++ try { ++ if (currentlyTickingEntity.get() == null) { ++ currentlyTickingEntity.lazySet(entity); ++ } ++ // Tuinity end - log detailed entity tick information + ++TimingHistory.entityTicks; // Paper - timings + // Spigot start + co.aikar.timings.Timing timer; // Paper +@@ -953,7 +1173,13 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + } + + // } finally { timer.stopTiming(); } // Paper - timings - move up +- ++ // Tuinity start - log detailed entity tick information ++ } finally { ++ if (currentlyTickingEntity.get() == entity) { ++ currentlyTickingEntity.lazySet(null); ++ } ++ } ++ // Tuinity end - log detailed entity tick information + } + + private void tickPassenger(Entity vehicle, Entity passenger) { +@@ -1245,9 +1471,13 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + // Spigot Start + for (BlockEntity tileentity : chunk.getBlockEntities().values()) { + if (tileentity instanceof net.minecraft.world.Container) { ++ // Tuinity start - this area looks like it can load chunks, change the behavior ++ // chests for example can apply physics to the world ++ // so instead we just change the active container and call the event + for (org.bukkit.entity.HumanEntity h : Lists.newArrayList(((net.minecraft.world.Container) tileentity).getViewers())) { +- h.closeInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED); // Paper ++ ((org.bukkit.craftbukkit.entity.CraftHumanEntity)h).getHandle().closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED); // Paper + } ++ // Tuiniy end + } + } + // Spigot End +@@ -1344,9 +1574,19 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + VoxelShape voxelshape1 = newState.getCollisionShape(this, pos); + + if (Shapes.joinIsNotEmpty(voxelshape, voxelshape1, BooleanOp.NOT_SAME)) { +- Iterator iterator = this.navigatingMobs.iterator(); ++ // Tuinity start - optimise notify() ++ com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region region = this.getChunkSource().chunkMap.dataRegionManager.getRegion(pos.getX() >> 4, pos.getZ() >> 4); ++ if (region == null) { ++ return; ++ } ++ com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet navigatorsFromRegion = ((ChunkMap.DataRegionData)region.regionData).getNavigators(); ++ if (navigatorsFromRegion == null) { ++ return; ++ } ++ com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator iterator = navigatorsFromRegion.iterator(); + +- while (iterator.hasNext()) { ++ ++ try { while (iterator.hasNext()) { // Tuinity end - optimise notify() + // CraftBukkit start - fix SPIGOT-6362 + Mob entityinsentient; + try { +@@ -1365,6 +1605,11 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + navigationabstract.recomputePath(pos); + } + } ++ // Tuinity start - optimise notify() ++ } finally { ++ iterator.finishedIterating(); ++ } ++ // Tuinity end - optimise notify() + + } + } // Paper +@@ -2146,10 +2391,12 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + + public void onTickingStart(Entity entity) { + ServerLevel.this.entityTickList.add(entity); ++ ServerLevel.this.entityManager.addNavigatorsIfPathingToRegion(entity); // Tuinity - optimise notify + } + + public void onTickingEnd(Entity entity) { + ServerLevel.this.entityTickList.remove(entity); ++ ServerLevel.this.entityManager.removeNavigatorsFromData(entity); // Tuinity - optimise notify + } + + public void onTrackingStart(Entity entity) { +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 8e2bccc3a9ddb17a4978596056189eb776976338..dcba69c0ad3288ddc64dacc58b6fb857eed3109c 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -261,7 +261,7 @@ public class ServerPlayer extends Player { + + public double lastEntitySpawnRadiusSquared; // Paper - optimise isOutsideRange, this field is in blocks + public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet cachedSingleHashSet; // Paper +- boolean needsChunkCenterUpdate; // Paper - no-tick view distance ++ public boolean needsChunkCenterUpdate; // Paper - no-tick view distance // Tuinity - public + public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - there are a lot of changes to do if we change all methods leading to the event + + public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile) { +@@ -419,7 +419,7 @@ public class ServerPlayer extends Player { + + if (blockposition1 != null) { + this.moveTo(blockposition1, 0.0F, 0.0F); +- if (world.noCollision(this)) { ++ if (world.noCollision(this, this.getBoundingBox(), null, true)) { // Tuinity - make sure this loads chunks, we default to NOT loading now + break; + } + } +@@ -427,7 +427,7 @@ public class ServerPlayer extends Player { + } else { + this.moveTo(blockposition, 0.0F, 0.0F); + +- while (!world.noCollision(this) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { ++ while (!world.noCollision(this, this.getBoundingBox(), null, true) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { // Tuinity - make sure this loads chunks, we default to NOT loading now + this.setPos(this.getX(), this.getY() + 1.0D, this.getZ()); + } + } +@@ -1558,6 +1558,18 @@ public class ServerPlayer extends Player { + this.connection.send(new ClientboundContainerClosePacket(this.containerMenu.containerId)); + this.doCloseContainer(); + } ++ // Tuinity start - special close for unloaded inventory ++ @Override ++ public void closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) { ++ // copied from above ++ CraftEventFactory.handleInventoryCloseEvent(this, reason); // CraftBukkit ++ // Paper end ++ // copied from below ++ this.connection.send(new ClientboundContainerClosePacket(this.containerMenu.containerId)); ++ this.containerMenu = this.inventoryMenu; ++ // do not run close logic ++ } ++ // Tuinity end - special close for unloaded inventory + + public void doCloseContainer() { + this.containerMenu.removed((Player) this); +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +index e572088cad8b9e09b1d64f7971bacac2f10c5b17..b2c8cae1a777cd63a35ed1340caf205b1b3bb0ad 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +@@ -56,14 +56,28 @@ public class ServerPlayerGameMode { + @Nullable + private GameType previousGameModeForPlayer; + private boolean isDestroyingBlock; +- private int destroyProgressStart; ++ private int destroyProgressStart; private long lastDigTime; // Tuinity - lag compensate block breaking + private BlockPos destroyPos; + private int gameTicks; + private boolean hasDelayedDestroy; + private BlockPos delayedDestroyPos; +- private int delayedTickStart; ++ private int delayedTickStart; private long hasDestroyedTooFastStartTime; // Tuinity - lag compensate block breaking + private int lastSentState; + ++ // Tuinity start - lag compensate block breaking ++ private int getTimeDiggingLagCompensate() { ++ int lagCompensated = (int)((System.nanoTime() - this.lastDigTime) / (50L * 1000L * 1000L)); ++ int tickDiff = this.gameTicks - this.destroyProgressStart; ++ return (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking && lagCompensated > (tickDiff + 1)) ? lagCompensated : tickDiff; // add one to ensure we don't lag compensate unless we need to ++ } ++ ++ private int getTimeDiggingTooFastLagCompensate() { ++ int lagCompensated = (int)((System.nanoTime() - this.hasDestroyedTooFastStartTime) / (50L * 1000L * 1000L)); ++ int tickDiff = this.gameTicks - this.delayedTickStart; ++ return (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking && lagCompensated > (tickDiff + 1)) ? lagCompensated : tickDiff; // add one to ensure we don't lag compensate unless we need to ++ } ++ // Tuinity end ++ + public ServerPlayerGameMode(ServerPlayer player) { + this.gameModeForPlayer = GameType.DEFAULT_MODE; + this.destroyPos = BlockPos.ZERO; +@@ -130,7 +144,7 @@ public class ServerPlayerGameMode { + if (iblockdata == null || iblockdata.isAir()) { // Paper + this.hasDelayedDestroy = false; + } else { +- float f = this.incrementDestroyProgress(iblockdata, this.delayedDestroyPos, this.delayedTickStart); ++ float f = this.updateBlockBreakAnimation(iblockdata, this.delayedDestroyPos, this.getTimeDiggingTooFastLagCompensate()); // Tuinity - lag compensate destroying blocks + + if (f >= 1.0F) { + this.hasDelayedDestroy = false; +@@ -150,7 +164,7 @@ public class ServerPlayerGameMode { + this.lastSentState = -1; + this.isDestroyingBlock = false; + } else { +- this.incrementDestroyProgress(iblockdata, this.destroyPos, this.destroyProgressStart); ++ this.updateBlockBreakAnimation(iblockdata, this.destroyPos, this.getTimeDiggingLagCompensate()); // Tuinity - lag compensate destroying + } + } + +@@ -158,6 +172,12 @@ public class ServerPlayerGameMode { + + private float incrementDestroyProgress(BlockState state, BlockPos pos, int i) { + int j = this.gameTicks - i; ++ // Tuinity start - change i (startTime) to totalTime ++ return this.updateBlockBreakAnimation(state, pos, j); ++ } ++ private float updateBlockBreakAnimation(BlockState state, BlockPos pos, int totalTime) { ++ int j = totalTime; ++ // Tuinity end + float f = state.getDestroyProgress(this.player, this.player.level, pos) * (float) (j + 1); + int k = (int) (f * 10.0F); + +@@ -225,7 +245,7 @@ public class ServerPlayerGameMode { + return; + } + +- this.destroyProgressStart = this.gameTicks; ++ this.destroyProgressStart = this.gameTicks; this.lastDigTime = System.nanoTime(); // Tuinity - lag compensate block breaking + float f = 1.0F; + + iblockdata = this.level.getBlockState(pos); +@@ -278,12 +298,12 @@ public class ServerPlayerGameMode { + int j = (int) (f * 10.0F); + + this.level.destroyBlockProgress(this.player.getId(), pos, j); +- this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, "actual start of destroying")); ++ if (!com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, "actual start of destroying")); + this.lastSentState = j; + } + } else if (action == ServerboundPlayerActionPacket.Action.STOP_DESTROY_BLOCK) { + if (pos.equals(this.destroyPos)) { +- int k = this.gameTicks - this.destroyProgressStart; ++ int k = this.getTimeDiggingLagCompensate(); // Tuinity - lag compensate block breaking + + iblockdata = this.level.getBlockState(pos); + if (!iblockdata.isAir()) { +@@ -300,12 +320,18 @@ public class ServerPlayerGameMode { + this.isDestroyingBlock = false; + this.hasDelayedDestroy = true; + this.delayedDestroyPos = pos; +- this.delayedTickStart = this.destroyProgressStart; ++ this.delayedTickStart = this.destroyProgressStart; this.hasDestroyedTooFastStartTime = this.lastDigTime; // Tuinity - lag compensate block breaking + } + } + } + ++ // Tuinity start - this can cause clients on a lagging server to think they're not currently destroying a block ++ if (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) { ++ this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); ++ } else { + this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, "stopped destroying")); ++ } ++ // Tuinity end - this can cause clients on a lagging server to think they're not currently destroying a block + } else if (action == ServerboundPlayerActionPacket.Action.ABORT_DESTROY_BLOCK) { + this.isDestroyingBlock = false; + if (!Objects.equals(this.destroyPos, pos) && !BlockPos.ZERO.equals(this.destroyPos)) { +@@ -317,7 +343,7 @@ public class ServerPlayerGameMode { + } + + this.level.destroyBlockProgress(this.player.getId(), pos, -1); +- this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, "aborted destroying")); ++ if (!com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, "aborted destroying")); // Tuinity - this can cause clients on a lagging server to think they stopped destroying a block they're currently destroying + } + + } +@@ -327,7 +353,13 @@ public class ServerPlayerGameMode { + + public void destroyAndAck(BlockPos pos, ServerboundPlayerActionPacket.Action action, String reason) { + if (this.destroyBlock(pos)) { ++ // Tuinity start - this can cause clients on a lagging server to think they're not currently destroying a block ++ if (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) { ++ this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); ++ } else { + this.player.connection.send(new ClientboundBlockBreakAckPacket(pos, this.level.getBlockState(pos), action, true, reason)); ++ } ++ // Tuinity end - this can cause clients on a lagging server to think they're not currently destroying a block + } else { + this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // CraftBukkit - SPIGOT-5196 + } +diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +index f0df7b2bd618c7be18c4c86f735b303dc73d98ad..73e1efe5bcec0522384118f0ca5e4d4f3e8bce82 100644 +--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java ++++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +@@ -25,6 +25,17 @@ import net.minecraft.world.level.lighting.LevelLightEngine; + import org.apache.logging.log4j.LogManager; + import org.apache.logging.log4j.Logger; + ++// Tuinity start ++import ca.spottedleaf.starlight.light.StarLightEngine; ++import com.tuinity.tuinity.util.CoordinateUtils; ++import java.util.function.Supplier; ++import net.minecraft.world.level.lighting.LayerLightEventListener; ++import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import it.unimi.dsi.fastutil.longs.LongIterator; ++import net.minecraft.world.level.chunk.ChunkStatus; ++// Tuinity end ++ + public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable { + private static final Logger LOGGER = LogManager.getLogger(); + private final ProcessorMailbox taskMailbox; +@@ -168,13 +179,166 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + private volatile int taskPerBatch = 5; + private final AtomicBoolean scheduled = new AtomicBoolean(); + ++ // Tuinity start - replace light engine impl ++ protected final ca.spottedleaf.starlight.light.StarLightInterface theLightEngine; ++ public final boolean hasBlockLight; ++ public final boolean hasSkyLight; ++ // Tuinity end - replace light engine impl ++ + public ThreadedLevelLightEngine(LightChunkGetter chunkProvider, ChunkMap chunkStorage, boolean hasBlockLight, ProcessorMailbox processor, ProcessorHandle> executor) { +- super(chunkProvider, true, hasBlockLight); ++ super(chunkProvider, false, false); // Tuinity - destroy vanilla light engine state + this.chunkMap = chunkStorage; this.playerChunkMap = chunkMap; // Paper + this.sorterMailbox = executor; + this.taskMailbox = processor; ++ // Tuinity start - replace light engine impl ++ this.hasBlockLight = true; ++ this.hasSkyLight = hasBlockLight; // Nice variable name. ++ this.theLightEngine = new ca.spottedleaf.starlight.light.StarLightInterface(chunkProvider, this.hasSkyLight, this.hasBlockLight, this); ++ // Tuinity end - replace light engine impl ++ } ++ ++ // Tuinity start - replace light engine impl ++ protected final ChunkAccess getChunk(final int chunkX, final int chunkZ) { ++ return ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkAtImmediately(chunkX, chunkZ); ++ } ++ ++ protected long relightCounter; ++ ++ public int relight(java.util.Set chunks_param, ++ java.util.function.Consumer chunkLightCallback, ++ java.util.function.IntConsumer onComplete) { ++ if (!org.bukkit.Bukkit.isPrimaryThread()) { ++ throw new IllegalStateException("Must only be called on the main thread"); ++ } ++ ++ java.util.Set chunks = new java.util.LinkedHashSet<>(chunks_param); ++ // add tickets ++ java.util.Map ticketIds = new java.util.HashMap<>(); ++ int totalChunks = 0; ++ for (java.util.Iterator iterator = chunks.iterator(); iterator.hasNext();) { ++ final ChunkPos chunkPos = iterator.next(); ++ ++ final ChunkAccess chunk = ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkAtImmediately(chunkPos.x, chunkPos.z); ++ if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { ++ // cannot relight this chunk ++ iterator.remove(); ++ continue; ++ } ++ ++ final Long id = Long.valueOf(this.relightCounter++); ++ ++ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().addTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), id); ++ ticketIds.put(chunkPos, id); ++ ++ ++totalChunks; ++ } ++ ++ this.taskMailbox.tell(() -> { ++ this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> { ++ chunkLightCallback.accept(chunkPos); ++ ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> { ++ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong()).broadcast(new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(chunkPos, ThreadedLevelLightEngine.this, null, null, true), false); ++ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos)); ++ }); ++ }, onComplete); ++ }); ++ this.tryScheduleUpdate(); ++ ++ return totalChunks; ++ } ++ ++ private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); ++ ++ private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, final Supplier> runnable) { ++ final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld(); ++ ++ final ChunkAccess center = this.theLightEngine.getAnyChunkNow(chunkX, chunkZ); ++ if (center == null || !center.getStatus().isOrAfter(ChunkStatus.LIGHT)) { ++ // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing ++ // chunk scheduling, we could be lighting and generating a chunk at the same time ++ return; ++ } ++ ++ if (center.getStatus() != ChunkStatus.FULL) { ++ // do not keep chunk loaded, we are probably in a gen thread ++ // if we proceed to add a ticket the chunk will be loaded, which is not what we want (avoid cascading gen) ++ runnable.get(); ++ return; ++ } ++ ++ if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) { ++ // ticket logic is not safe to run off-main, re-schedule ++ world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> { ++ this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable); ++ }); ++ return; ++ } ++ ++ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ final CompletableFuture updateFuture = runnable.get(); ++ ++ if (updateFuture == null) { ++ // not scheduled ++ return; ++ } ++ ++ final int references = this.chunksBeingWorkedOn.addTo(key, 1); ++ if (references == 0) { ++ final ChunkPos pos = new ChunkPos(chunkX, chunkZ); ++ world.getChunkSource().addRegionTicket(ca.spottedleaf.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); ++ } ++ ++ // append future to this chunk and 1 radius neighbours chunk save futures ++ // this prevents us from saving the world without first waiting for the light engine ++ ++ for (int dx = -1; dx <= 1; ++dx) { ++ for (int dz = -1; dz <= 1; ++dz) { ++ ChunkHolder neighbour = world.getChunkSource().chunkMap.getUpdatingChunkIfPresent(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ)); ++ if (neighbour != null) { ++ neighbour.chunkToSave = neighbour.chunkToSave.thenCombine(updateFuture, (final ChunkAccess curr, final Void ignore) -> { ++ return curr; ++ }); ++ } ++ } ++ } ++ ++ updateFuture.thenAcceptAsync((final Void ignore) -> { ++ final int newReferences = this.chunksBeingWorkedOn.get(key); ++ if (newReferences == 1) { ++ this.chunksBeingWorkedOn.remove(key); ++ final ChunkPos pos = new ChunkPos(chunkX, chunkZ); ++ world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); ++ } else { ++ this.chunksBeingWorkedOn.put(key, newReferences - 1); ++ } ++ }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> { ++ if (thr != null) { ++ LOGGER.fatal("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr); ++ } ++ }); ++ } ++ ++ @Override ++ public boolean hasLightWork() { ++ // route to new light engine ++ return this.theLightEngine.hasUpdates() || !this.queue.isEmpty(); + } + ++ @Override ++ public LayerLightEventListener getLayerListener(final LightLayer lightType) { ++ return lightType == LightLayer.BLOCK ? this.theLightEngine.getBlockReader() : this.theLightEngine.getSkyReader(); ++ } ++ ++ @Override ++ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) { ++ // need to use new light hooks for this ++ final int sky = this.theLightEngine.getSkyReader().getLightValue(pos) - ambientDarkness; ++ final int block = this.theLightEngine.getBlockReader().getLightValue(pos); ++ return Math.max(sky, block); ++ } ++ // Tuinity end - replace light engine impl ++ + @Override + public void close() { + } +@@ -191,15 +355,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + + @Override + public void checkBlock(BlockPos pos) { +- BlockPos blockPos = pos.immutable(); +- this.addTask(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()), ThreadedLevelLightEngine.TaskType.POST_UPDATE, Util.name(() -> { +- super.checkBlock(blockPos); +- }, () -> { +- return "checkBlock " + blockPos; +- })); ++ // Tuinity start - replace light engine impl ++ final BlockPos posCopy = pos.immutable(); ++ this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> { ++ return this.theLightEngine.blockChange(posCopy); ++ }); ++ // Tuinity end - replace light engine impl + } + + protected void updateChunkStatus(ChunkPos pos) { ++ if (true) return; // Tuinity - replace light engine impl + this.addTask(pos.x, pos.z, () -> { + return 0; + }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { +@@ -222,17 +387,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + + @Override + public void updateSectionStatus(SectionPos pos, boolean notReady) { +- this.addTask(pos.x(), pos.z(), () -> { +- return 0; +- }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { +- super.updateSectionStatus(pos, notReady); +- }, () -> { +- return "updateSectionStatus " + pos + " " + notReady; +- })); ++ // Tuinity start - replace light engine impl ++ this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> { ++ return this.theLightEngine.sectionChange(pos, notReady); ++ }); ++ // Tuinity end - replace light engine impl + } + + @Override + public void enableLightSources(ChunkPos chunkPos, boolean bl) { ++ if (true) return; // Tuinity - replace light engine impl + this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { + super.enableLightSources(chunkPos, bl); + }, () -> { +@@ -242,6 +406,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + + @Override + public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles, boolean bl) { ++ if (true) return; // Tuinity - replace light engine impl + this.addTask(pos.x(), pos.z(), () -> { + return 0; + }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { +@@ -263,6 +428,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + + @Override + public void retainData(ChunkPos pos, boolean retainData) { ++ if (true) return; // Tuinity - replace light engine impl + this.addTask(pos.x, pos.z, () -> { + return 0; + }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { +@@ -273,6 +439,37 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + } + + public CompletableFuture lightChunk(ChunkAccess chunk, boolean excludeBlocks) { ++ // Tuinity start - replace light engine impl ++ if (true) { ++ boolean lit = excludeBlocks; ++ final ChunkPos chunkPos = chunk.getPos(); ++ ++ return CompletableFuture.supplyAsync(() -> { ++ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk); ++ if (!lit) { ++ chunk.setLightCorrect(false); ++ this.theLightEngine.lightChunk(chunk, emptySections); ++ chunk.setLightCorrect(true); ++ } else { ++ this.theLightEngine.forceLoadInChunk(chunk, emptySections); ++ // can't really force the chunk to be edged checked, as we need neighbouring chunks - but we don't have ++ // them, so if it's not loaded then i guess we can't do edge checks. later loads of the chunk should ++ // catch what we miss here. ++ this.theLightEngine.checkChunkEdges(chunkPos.x, chunkPos.z); ++ } ++ ++ this.chunkMap.releaseLightTicket(chunkPos); ++ return chunk; ++ }, (runnable) -> { ++ this.theLightEngine.scheduleChunkLight(chunkPos, runnable); ++ this.tryScheduleUpdate(); ++ }).whenComplete((final ChunkAccess c, final Throwable throwable) -> { ++ if (throwable != null) { ++ LOGGER.fatal("Failed to light chunk " + chunkPos, throwable); ++ } ++ }); ++ } ++ // Tuinity end - replace light engine impl + ChunkPos chunkPos = chunk.getPos(); + // Paper start + //ichunkaccess.b(false); // Don't need to disable this +@@ -320,7 +517,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + } + + public void tryScheduleUpdate() { +- if ((!this.queue.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { // Paper ++ if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Paper // Tuinity - rewrite light engine + this.taskMailbox.tell(() -> { + this.runUpdate(); + this.scheduled.set(false); +@@ -337,12 +534,12 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + if (queue.poll(pre, post)) { + pre.forEach(Runnable::run); + pre.clear(); +- super.runUpdates(Integer.MAX_VALUE, true, true); ++ this.theLightEngine.propagateChanges(); // Tuinity - rewrite light engine + post.forEach(Runnable::run); + post.clear(); + } else { + // might have level updates to go still +- super.runUpdates(Integer.MAX_VALUE, true, true); ++ this.theLightEngine.propagateChanges(); // Tuinity - rewrite light engine + } + // Paper end + } +diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java +index 3c1698ba0d3bc412ab957777d9b5211dbc555208..c438cbfa1f964a5bea98bca85e688d8019e1c4fb 100644 +--- a/src/main/java/net/minecraft/server/level/TicketType.java ++++ b/src/main/java/net/minecraft/server/level/TicketType.java +@@ -31,6 +31,8 @@ public class TicketType { + public static final TicketType PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit + public static final TicketType PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit + public static final TicketType DELAY_UNLOAD = create("delay_unload", Long::compareTo, 300); // Paper ++ public static final TicketType CHUNK_RELIGHT = create("light_update", Long::compareTo); // Tuinity - ensure chunks stay loaded for lighting ++ public static final TicketType REQUIRED_LOAD = create("required_load", Long::compareTo); // Tuinity - make sure getChunkAt does not fail + + public static TicketType create(String name, Comparator argumentComparator) { + return new TicketType<>(name, argumentComparator, 0L); +diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +index 0f6b534a4c789a2f09f6c4624e5d58b99c7ed0e6..fea852674098fe411841d8e5ebeace7d11d94e4f 100644 +--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java ++++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +@@ -77,6 +77,23 @@ public class WorldGenRegion implements WorldGenLevel { + @Nullable + private Supplier currentlyGenerating; + ++ // Tuinity start ++ // No-op, this class doesn't provide entity access ++ @Override ++ public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { ++ return Collections.emptyList(); ++ } ++ ++ @Override ++ public void getEntities(Entity except, AABB box, Predicate predicate, List into) {} ++ ++ @Override ++ public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) {} ++ ++ @Override ++ public void getEntitiesByClass(Class clazz, Entity except, AABB box, List into, Predicate predicate) {} ++ // Tuinity end ++ + public WorldGenRegion(ServerLevel world, List list, ChunkStatus chunkstatus, int i) { + this.generatingStatus = chunkstatus; + this.writeRadiusCutoff = i; +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 6ff04fc7202a7eb1f2b5978a2e2a573945d9dde1..53e9ba24338690b1c5f12250748273758c68f4d1 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -536,6 +536,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser + double d10 = Math.max(d6 * d6 + d7 * d7 + d8 * d8, (currDeltaX * currDeltaX + currDeltaY * currDeltaY + currDeltaZ * currDeltaZ) - 1); + // Paper end - fix large move vectors killing the server + ++ // Tuinity start - fix large move vectors killing the server ++ double otherFieldX = d3 - this.vehicleLastGoodX; ++ double otherFieldY = d4 - this.vehicleLastGoodY - 1.0E-6D; ++ double otherFieldZ = d5 - this.vehicleLastGoodZ; ++ d10 = Math.max(d10, (otherFieldX * otherFieldX + otherFieldY * otherFieldY + otherFieldZ * otherFieldZ) - 1); ++ // Tuinity end - fix large move vectors killing the server + + // CraftBukkit start - handle custom speeds and skipped ticks + this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick; +@@ -576,12 +582,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser + return; + } + +- boolean flag = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); ++ AABB oldBox = entity.getBoundingBox(); // Tuinity - copy from player movement packet + +- d6 = d3 - this.vehicleLastGoodX; +- d7 = d4 - this.vehicleLastGoodY - 1.0E-6D; +- d8 = d5 - this.vehicleLastGoodZ; ++ d6 = d3 - this.vehicleLastGoodX; // Tuinity - diff on change, used for checking large move vectors above ++ d7 = d4 - this.vehicleLastGoodY - 1.0E-6D; // Tuinity - diff on change, used for checking large move vectors above ++ d8 = d5 - this.vehicleLastGoodZ; // Tuinity - diff on change, used for checking large move vectors above + entity.move(MoverType.PLAYER, new Vec3(d6, d7, d8)); ++ boolean didCollide = toX != entity.getX() || toY != entity.getY() || toZ != entity.getZ(); // Tuinity - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be... + double d11 = d7; + + d6 = d3 - entity.getX(); +@@ -595,16 +602,23 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser + boolean flag1 = false; + + if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot +- flag1 = true; ++ flag1 = true; // Tuinity - diff on change, this should be moved wrongly + ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", entity.getName().getString(), this.player.getName().getString(), Math.sqrt(d10)); + } + Location curPos = this.getCraftPlayer().getLocation(); // Spigot + + entity.absMoveTo(d3, d4, d5, f, f1); + this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit +- boolean flag2 = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); +- +- if (flag && (flag1 || !flag2)) { ++ // Tuinity start - optimise out extra getCubes ++ boolean teleportBack = flag1; // violating this is always a fail ++ if (!teleportBack) { ++ // note: only call after setLocation, or else getBoundingBox is wrong ++ AABB newBox = entity.getBoundingBox(); ++ if (didCollide || !oldBox.equals(newBox)) { ++ teleportBack = this.hasNewCollision(worldserver, entity, oldBox, newBox); ++ } // else: no collision at all detected, why do we care? ++ } ++ if (teleportBack) { // Tuinity end - optimise out extra getCubes + entity.absMoveTo(d0, d1, d2, f, f1); + this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit + this.connection.send(new ClientboundMoveVehiclePacket(entity)); +@@ -690,7 +704,32 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser + } + + private boolean noBlocksAround(Entity entity) { +- return entity.level.getBlockStates(entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D)).allMatch(BlockBehaviour.BlockStateBase::isAir); ++ // Tuinity start - stop using streams, this is already a known fixed problem in Entity#move ++ AABB box = entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D); ++ int minX = Mth.floor(box.minX); ++ int minY = Mth.floor(box.minY); ++ int minZ = Mth.floor(box.minZ); ++ int maxX = Mth.floor(box.maxX); ++ int maxY = Mth.floor(box.maxY); ++ int maxZ = Mth.floor(box.maxZ); ++ ++ Level world = entity.level; ++ BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); ++ ++ for (int y = minY; y <= maxY; ++y) { ++ for (int z = minZ; z <= maxZ; ++z) { ++ for (int x = minX; x <= maxX; ++x) { ++ pos.set(x, y, z); ++ BlockState type = world.getTypeIfLoaded(pos); ++ if (type != null && !type.isAir()) { ++ return false; ++ } ++ } ++ } ++ } ++ ++ return true; ++ // Tuinity end - stop using streams, this is already a known fixed problem in Entity#move + } + + @Override +@@ -1227,7 +1266,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser + } + + if (this.awaitingPositionFromClient != null) { +- if (this.tickCount - this.awaitingTeleportTime > 20) { ++ if (false && this.tickCount - this.awaitingTeleportTime > 20) { // Tuinity - this will greatly screw with clients with > 1000ms RTT + this.awaitingTeleportTime = this.tickCount; + this.teleport(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot()); + } +@@ -1266,6 +1305,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser + double currDeltaZ = toZ - prevZ; + double d11 = Math.max(d7 * d7 + d8 * d8 + d9 * d9, (currDeltaX * currDeltaX + currDeltaY * currDeltaY + currDeltaZ * currDeltaZ) - 1); + // Paper end - fix large move vectors killing the server ++ // Tuinity start - fix large move vectors killing the server ++ double otherFieldX = d0 - this.lastGoodX; ++ double otherFieldY = d1 - this.lastGoodY; ++ double otherFieldZ = d2 - this.lastGoodZ; ++ d11 = Math.max(d11, (otherFieldX * otherFieldX + otherFieldY * otherFieldY + otherFieldZ * otherFieldZ) - 1); ++ // Tuinity end - fix large move vectors killing the server + + if (this.player.isSleeping()) { + if (d11 > 1.0D) { +@@ -1315,11 +1360,11 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser + } + } + +- AABB axisalignedbb = this.player.getBoundingBox(); ++ AABB axisalignedbb = this.player.getBoundingBox(); // Tuinity - diff on change, should be old AABB + +- d7 = d0 - this.lastGoodX; +- d8 = d1 - this.lastGoodY; +- d9 = d2 - this.lastGoodZ; ++ d7 = d0 - this.lastGoodX; // Tuinity - diff on change, used for checking large move vectors above ++ d8 = d1 - this.lastGoodY; // Tuinity - diff on change, used for checking large move vectors above ++ d9 = d2 - this.lastGoodZ; // Tuinity - diff on change, used for checking large move vectors above + boolean flag = d8 > 0.0D; + + if (this.player.isOnGround() && !packet.isOnGround() && flag) { +@@ -1354,6 +1399,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser + } + + this.player.move(MoverType.PLAYER, new Vec3(d7, d8, d9)); ++ boolean didCollide = toX != this.player.getX() || toY != this.player.getY() || toZ != this.player.getZ(); // Tuinity - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be... + this.player.setOnGround(packet.isOnGround()); // CraftBukkit - SPIGOT-5810, SPIGOT-5835: reset by this.player.move + // Paper start - prevent position desync + if (this.awaitingPositionFromClient != null) { +@@ -1373,12 +1419,23 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser + boolean flag1 = false; + + if (!this.player.isChangingDimension() && d11 > org.spigotmc.SpigotConfig.movedWronglyThreshold && !this.player.isSleeping() && !this.player.gameMode.isCreative() && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR) { // Spigot +- flag1 = true; ++ flag1 = true; // Tuinity - diff on change, this should be moved wrongly + ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!", this.player.getName().getString()); + } + + this.player.absMoveTo(d0, d1, d2, f, f1); +- if (!this.player.noPhysics && !this.player.isSleeping() && (flag1 && worldserver.noCollision(this.player, axisalignedbb) || this.isPlayerCollidingWithAnythingNew((LevelReader) worldserver, axisalignedbb))) { ++ // Tuinity start - optimise out extra getCubes ++ // Original for reference: ++ // boolean teleportBack = flag1 && worldserver.getCubes(this.player, axisalignedbb) || (didCollide && this.a((IWorldReader) worldserver, axisalignedbb)); ++ boolean teleportBack = flag1; // violating this is always a fail ++ if (!this.player.noPhysics && !this.player.isSleeping() && !teleportBack) { ++ AABB newBox = this.player.getBoundingBox(); ++ if (didCollide || !axisalignedbb.equals(newBox)) { ++ // note: only call after setLocation, or else getBoundingBox is wrong ++ teleportBack = this.hasNewCollision(worldserver, this.player, axisalignedbb, newBox); ++ } // else: no collision at all detected, why do we care? ++ } ++ if (!this.player.noPhysics && !this.player.isSleeping() && teleportBack) { // Tuinity end - optimise out extra getCubes + this.teleport(d3, d4, d5, f, f1); + } else { + // CraftBukkit start - fire PlayerMoveEvent +@@ -1465,6 +1522,27 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser + } + } + ++ // Tuinity start - optimise out extra getCubes ++ private boolean hasNewCollision(final ServerLevel world, final Entity entity, final AABB oldBox, final AABB newBox) { ++ final List collisions = com.tuinity.tuinity.util.CachedLists.getTempCollisionList(); ++ try { ++ com.tuinity.tuinity.util.CollisionUtil.getCollisions(world, entity, newBox, collisions, true, false, ++ true, false, null, null); ++ ++ for (int i = 0, len = collisions.size(); i < len; ++i) { ++ final AABB box = collisions.get(i); ++ if (!com.tuinity.tuinity.util.CollisionUtil.voxelShapeIntersect(box, oldBox)) { ++ return true; ++ } ++ } ++ ++ return false; ++ } finally { ++ com.tuinity.tuinity.util.CachedLists.returnTempCollisionList(collisions); ++ } ++ } ++ // Tuinity end - optimise out extra getCubes ++ + private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box) { + Stream stream = world.getCollisions(this.player, this.player.getBoundingBox().deflate(9.999999747378752E-6D), (entity) -> { + return true; +diff --git a/src/main/java/net/minecraft/server/players/GameProfileCache.java b/src/main/java/net/minecraft/server/players/GameProfileCache.java +index 61405c2b53e03a4b83e2c70c6e4d3739ca9676cb..1f307f8e3f0f484dad33e9af085dabd93a3509fd 100644 +--- a/src/main/java/net/minecraft/server/players/GameProfileCache.java ++++ b/src/main/java/net/minecraft/server/players/GameProfileCache.java +@@ -62,6 +62,11 @@ public class GameProfileCache { + @Nullable + private Executor executor; + ++ // Tuinity start ++ protected final java.util.concurrent.locks.ReentrantLock stateLock = new java.util.concurrent.locks.ReentrantLock(); ++ protected final java.util.concurrent.locks.ReentrantLock lookupLock = new java.util.concurrent.locks.ReentrantLock(); ++ // Tuinity end ++ + public GameProfileCache(GameProfileRepository profileRepository, File cacheFile) { + this.profileRepository = profileRepository; + this.file = cacheFile; +@@ -69,6 +74,7 @@ public class GameProfileCache { + } + + private void safeAdd(GameProfileCache.GameProfileInfo entry) { ++ try { this.stateLock.lock(); // Tuinity - allow better concurrency + GameProfile gameprofile = entry.getProfile(); + + entry.setLastAccess(this.getNextOperation()); +@@ -83,6 +89,7 @@ public class GameProfileCache { + if (uuid != null) { + this.profilesByUUID.put(uuid, entry); + } ++ } finally { this.stateLock.unlock(); } // Tuinity - allow better concurrency + + } + +@@ -119,7 +126,7 @@ public class GameProfileCache { + return com.destroystokyo.paper.PaperConfig.isProxyOnlineMode(); // Paper + } + +- public synchronized void add(GameProfile profile) { // Paper - synchronize ++ public void add(GameProfile profile) { // Paper - synchronize // Tuinity - allow better concurrency + Calendar calendar = Calendar.getInstance(); + + calendar.setTime(new Date()); +@@ -142,8 +149,9 @@ public class GameProfileCache { + } + // Paper end + +- public synchronized Optional get(String name) { // Paper - synchronize ++ public Optional get(String name) { // Paper - synchronize // Tuinity start - allow better concurrency + String s1 = name.toLowerCase(Locale.ROOT); ++ boolean stateLocked = true; try { this.stateLock.lock(); // Tuinity - allow better concurrency + GameProfileCache.GameProfileInfo usercache_usercacheentry = (GameProfileCache.GameProfileInfo) this.profilesByName.get(s1); + boolean flag = false; + +@@ -157,10 +165,14 @@ public class GameProfileCache { + Optional optional; + + if (usercache_usercacheentry != null) { ++ stateLocked = false; this.stateLock.unlock(); // Tuinity - allow better concurrency + usercache_usercacheentry.setLastAccess(this.getNextOperation()); + optional = Optional.of(usercache_usercacheentry.getProfile()); + } else { ++ stateLocked = false; this.stateLock.unlock(); // Tuinity - allow better concurrency ++ try { this.lookupLock.lock(); // Tuinity - allow better concurrency + optional = GameProfileCache.lookupGameProfile(this.profileRepository, name); // Spigot - use correct case for offline players ++ } finally { this.lookupLock.unlock(); } // Tuinity - allow better concurrency + if (optional.isPresent()) { + this.add((GameProfile) optional.get()); + flag = false; +@@ -172,6 +184,7 @@ public class GameProfileCache { + } + + return optional; ++ } finally { if (stateLocked) { this.stateLock.unlock(); } } // Tuinity - allow better concurrency + } + + public void getAsync(String username, Consumer> consumer) { +@@ -322,7 +335,9 @@ public class GameProfileCache { + } + + private Stream getTopMRUProfiles(int limit) { ++ try { this.stateLock.lock(); // Tuinity - allow better concurrency + return ImmutableList.copyOf(this.profilesByUUID.values()).stream().sorted(Comparator.comparing(GameProfileCache.GameProfileInfo::getLastAccess).reversed()).limit((long) limit); ++ } finally { this.stateLock.unlock(); } // Tuinity - allow better concurrency + } + + private static JsonElement writeGameProfile(GameProfileCache.GameProfileInfo entry, DateFormat dateFormat) { +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 65657c009f6d5a5d5740e80f912a5893333c7085..2fd0a432ed52ca137622a1f631e886aaf77f33d3 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -177,6 +177,7 @@ public abstract class PlayerList { + abstract public void loadAndSaveFiles(); // Paper - moved from DedicatedPlayerList constructor + + public void placeNewPlayer(Connection connection, ServerPlayer player) { ++ player.isRealPlayer = true; // Paper // Tuinity - this is a better place to write this that works and isn't overriden by plugins + ServerPlayer prev = pendingPlayers.put(player.getUUID(), player);// Paper + if (prev != null) { + disconnectPendingPlayer(prev); +@@ -266,7 +267,7 @@ public abstract class PlayerList { + boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO); + + // Spigot - view distance +- playerconnection.send(new ClientboundLoginPacket(player.getId(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), worlddata.isHardcore(), this.server.levelKeys(), this.registryHolder, worldserver1.dimensionType(), worldserver1.dimension(), this.getMaxPlayers(), worldserver1.getChunkSource().chunkMap.getLoadViewDistance(), flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat())); // Paper - no-tick view distance ++ playerconnection.send(new ClientboundLoginPacket(player.getId(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), worlddata.isHardcore(), this.server.levelKeys(), this.registryHolder, worldserver1.dimensionType(), worldserver1.dimension(), this.getMaxPlayers(), worldserver1.getChunkSource().chunkMap.playerChunkManager.getLoadDistance(), flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat())); // Paper - no-tick view distance // Tuinity - replace old player chunk management + player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit + playerconnection.send(new ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.BRAND, (new FriendlyByteBuf(Unpooled.buffer())).writeUtf(this.getServer().getServerModName()))); + playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked())); +@@ -725,7 +726,7 @@ public abstract class PlayerList { + SocketAddress socketaddress = loginlistener.connection.getRemoteAddress(); + + ServerPlayer entity = new ServerPlayer(this.server, this.server.getLevel(Level.OVERWORLD), gameprofile); +- entity.isRealPlayer = true; // Paper - Chunk priority ++ // Tuinity - some plugins (namely protocolsupport) bypass this logic completely! So this needs to be moved. + Player player = entity.getBukkitEntity(); + PlayerLoginEvent event = new PlayerLoginEvent(player, hostname, ((java.net.InetSocketAddress) socketaddress).getAddress(), ((java.net.InetSocketAddress) loginlistener.connection.getRawAddress()).getAddress()); + +@@ -929,13 +930,13 @@ public abstract class PlayerList { + + worldserver1.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper + entityplayer1.forceCheckHighPriority(); // Player - Chunk priority +- while (avoidSuffocation && !worldserver1.noCollision(entityplayer1) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { ++ while (avoidSuffocation && !worldserver1.noCollision(entityplayer1, entityplayer1.getBoundingBox(), null, true) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { // Tuinity - make sure this loads chunks, we default to NOT loading now + entityplayer1.setPos(entityplayer1.getX(), entityplayer1.getY() + 1.0D, entityplayer1.getZ()); + } + // CraftBukkit start + LevelData worlddata = worldserver1.getLevelData(); + entityplayer1.connection.send(new ClientboundRespawnPacket(worldserver1.dimensionType(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), entityplayer1.gameMode.getGameModeForPlayer(), entityplayer1.gameMode.getPreviousGameModeForPlayer(), worldserver1.isDebug(), worldserver1.isFlat(), flag)); +- entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getChunkSource().chunkMap.getLoadViewDistance())); // Spigot // Paper - no-tick view distance ++ entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getLoadDistance())); // Spigot // Paper - no-tick view distance// Tuinity - replace old player chunk management + entityplayer1.setLevel(worldserver1); + entityplayer1.unsetRemoved(); + entityplayer1.connection.teleport(new Location(worldserver1.getWorld(), entityplayer1.getX(), entityplayer1.getY(), entityplayer1.getZ(), entityplayer1.getYRot(), entityplayer1.getXRot())); +@@ -1210,7 +1211,7 @@ public abstract class PlayerList { + // Really shouldn't happen... + backingSet = world != null ? world.players.toArray() : players.toArray(); + } else { +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearbyPlayers = chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(MCUtil.fastFloor(x) >> 4, MCUtil.fastFloor(z) >> 4); ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearbyPlayers = chunkMap.playerChunkManager.broadcastMap.getObjectsInRange(MCUtil.fastFloor(x) >> 4, MCUtil.fastFloor(z) >> 4); // Tuinity - replace old player chunk management + if (nearbyPlayers == null) { + return; + } +diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/net/minecraft/util/BitStorage.java +index 07e1374ac3430662edd9f585e59b785e329f0820..9f9c0b56f0891e9c423d79f8ae4c3643a2b91048 100644 +--- a/src/main/java/net/minecraft/util/BitStorage.java ++++ b/src/main/java/net/minecraft/util/BitStorage.java +@@ -104,4 +104,32 @@ public class BitStorage { + } + + } ++ ++ // Paper start ++ public final void forEach(DataBitConsumer consumer) { ++ int i = 0; ++ long[] along = this.data; ++ int j = along.length; ++ ++ for (int k = 0; k < j; ++k) { ++ long l = along[k]; ++ ++ for (int i1 = 0; i1 < this.valuesPerLong; ++i1) { ++ consumer.accept(i, (int) (l & this.mask)); ++ l >>= this.bits; ++ ++i; ++ if (i >= this.size) { ++ return; ++ } ++ } ++ } ++ } ++ ++ @FunctionalInterface ++ public static interface DataBitConsumer { ++ ++ void accept(int location, int data); ++ ++ } ++ // Paper end + } +diff --git a/src/main/java/net/minecraft/util/valueproviders/IntProvider.java b/src/main/java/net/minecraft/util/valueproviders/IntProvider.java +index 020a19cd683dd3779c5116d12b3cdcd3b3ca69b4..17d209c347b07acef451180c97835f41b8bf8433 100644 +--- a/src/main/java/net/minecraft/util/valueproviders/IntProvider.java ++++ b/src/main/java/net/minecraft/util/valueproviders/IntProvider.java +@@ -9,13 +9,44 @@ import net.minecraft.core.Registry; + + public abstract class IntProvider { + private static final Codec> CONSTANT_OR_DISPATCH_CODEC = Codec.either(Codec.INT, Registry.INT_PROVIDER_TYPES.dispatch(IntProvider::getType, IntProviderType::codec)); +- public static final Codec CODEC = CONSTANT_OR_DISPATCH_CODEC.xmap((either) -> { ++ public static final Codec CODEC_REAL = CONSTANT_OR_DISPATCH_CODEC.xmap((either) -> { // Paper - used by CODEC below + return either.map(ConstantInt::of, (intProvider) -> { + return intProvider; + }); + }, (intProvider) -> { + return intProvider.getType() == IntProviderType.CONSTANT ? Either.left(((ConstantInt)intProvider).getValue()) : Either.right(intProvider); + }); ++ // Tuinity start ++ public static final Codec CODEC = new Codec<>() { ++ @Override ++ public DataResult> decode(com.mojang.serialization.DynamicOps ops, T input) { ++ /* ++ UniformInt: ++ count -> { (old format) ++ base, spread ++ } -> {UniformInt} { (new format & type) ++ base, base + spread ++ } */ ++ ++ ++ if (ops.get(input, "base").result().isPresent() && ops.get(input, "spread").result().isPresent()) { ++ // detected old format ++ int base = ops.getNumberValue(ops.get(input, "base").result().get()).result().get().intValue(); ++ int spread = ops.getNumberValue(ops.get(input, "spread").result().get()).result().get().intValue(); ++ return DataResult.success(new com.mojang.datafixers.util.Pair<>(UniformInt.of(base, base + spread), input)); ++ } ++ ++ // not old format, forward to real codec ++ return CODEC_REAL.decode(ops, input); ++ } ++ ++ @Override ++ public DataResult encode(IntProvider input, com.mojang.serialization.DynamicOps ops, T prefix) { ++ // forward to real codec ++ return CODEC_REAL.encode(input, ops, prefix); ++ } ++ }; ++ // Tuinity end + public static final Codec NON_NEGATIVE_CODEC = codec(0, Integer.MAX_VALUE); + public static final Codec POSITIVE_CODEC = codec(1, Integer.MAX_VALUE); + +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 5ffc9d02ee10e9efe653e124b1eb9cbd0adc3df9..e75efd67acb063e3ce7506839e4a888241bda703 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -356,8 +356,27 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + } + + public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getPlayersInTrackRange() { +- return ((ServerLevel)this.level).getChunkSource().chunkMap.playerEntityTrackerTrackMaps[this.trackingRangeType.ordinal()] +- .getObjectsInRange(MCUtil.getCoordinateKey(this)); ++ // Tuinity start - determine highest range of passengers ++ if (this.passengers.isEmpty()) { ++ return ((ServerLevel)this.level).getChunkSource().chunkMap.playerEntityTrackerTrackMaps[this.trackingRangeType.ordinal()] ++ .getObjectsInRange(MCUtil.getCoordinateKey(this)); ++ } ++ Iterable passengers = this.getIndirectPassengers(); ++ net.minecraft.server.level.ChunkMap chunkMap = ((ServerLevel)this.level).getChunkSource().chunkMap; ++ org.spigotmc.TrackingRange.TrackingRangeType type = this.trackingRangeType; ++ int range = chunkMap.getEntityTrackerRange(type.ordinal()); ++ ++ for (Entity passenger : passengers) { ++ org.spigotmc.TrackingRange.TrackingRangeType passengerType = passenger.trackingRangeType; ++ int passengerRange = chunkMap.getEntityTrackerRange(passengerType.ordinal()); ++ if (passengerRange > range) { ++ type = passengerType; ++ range = passengerRange; ++ } ++ } ++ ++ return chunkMap.playerEntityTrackerTrackMaps[type.ordinal()].getObjectsInRange(MCUtil.getCoordinateKey(this)); ++ // Tuinity end - determine highest range of passengers + } + // Paper end - optimise entity tracking + +@@ -392,6 +411,56 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + } + // Paper end - make end portalling safe + ++ // Tuinity start ++ public final AABB getBoundingBoxAt(double x, double y, double z) { ++ return this.dimensions.makeBoundingBox(x, y, z); ++ } ++ // Tuinity end ++ ++ // Tuinity start ++ /** ++ * Overriding this field will cause memory leaks. ++ */ ++ private final boolean hardCollides; ++ ++ private static final java.util.Map, Boolean> cachedOverrides = java.util.Collections.synchronizedMap(new java.util.WeakHashMap<>()); ++ { ++ /* // Goodbye, broken on reobf... ++ Boolean hardCollides = cachedOverrides.get(this.getClass()); ++ if (hardCollides == null) { ++ try { ++ java.lang.reflect.Method getHardCollisionBoxEntityMethod = Entity.class.getMethod("canCollideWith", Entity.class); ++ java.lang.reflect.Method hasHardCollisionBoxMethod = Entity.class.getMethod("canBeCollidedWith"); ++ if (!this.getClass().getMethod(hasHardCollisionBoxMethod.getName(), hasHardCollisionBoxMethod.getParameterTypes()).equals(hasHardCollisionBoxMethod) ++ || !this.getClass().getMethod(getHardCollisionBoxEntityMethod.getName(), getHardCollisionBoxEntityMethod.getParameterTypes()).equals(getHardCollisionBoxEntityMethod)) { ++ hardCollides = Boolean.TRUE; ++ } else { ++ hardCollides = Boolean.FALSE; ++ } ++ cachedOverrides.put(this.getClass(), hardCollides); ++ } ++ catch (ThreadDeath thr) { throw thr; } ++ catch (Throwable thr) { ++ // shouldn't happen, just explode ++ throw new RuntimeException(thr); ++ } ++ } */ ++ this.hardCollides = this instanceof Boat ++ || this instanceof net.minecraft.world.entity.monster.Shulker ++ || this instanceof net.minecraft.world.entity.vehicle.AbstractMinecart; ++ } ++ ++ public final boolean hardCollides() { ++ return this.hardCollides; ++ } ++ ++ public net.minecraft.server.level.ChunkHolder.FullChunkStatus chunkStatus; ++ ++ public int sectionX = Integer.MIN_VALUE; ++ public int sectionY = Integer.MIN_VALUE; ++ public int sectionZ = Integer.MIN_VALUE; ++ // Tuinity end ++ + public Entity(EntityType type, Level world) { + this.id = Entity.ENTITY_COUNTER.incrementAndGet(); + this.passengers = ImmutableList.of(); +@@ -813,7 +882,42 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + return this.onGround; + } + ++ // Tuinity start - detailed watchdog information ++ public final Object posLock = new Object(); // Tuinity - log detailed entity tick information ++ ++ private Vec3 moveVector; ++ private double moveStartX; ++ private double moveStartY; ++ private double moveStartZ; ++ ++ public final Vec3 getMoveVector() { ++ return this.moveVector; ++ } ++ ++ public final double getMoveStartX() { ++ return this.moveStartX; ++ } ++ ++ public final double getMoveStartY() { ++ return this.moveStartY; ++ } ++ ++ public final double getMoveStartZ() { ++ return this.moveStartZ; ++ } ++ // Tuinity end - detailed watchdog information ++ + public void move(MoverType movementType, Vec3 movement) { ++ // Tuinity start - detailed watchdog information ++ com.tuinity.tuinity.util.TickThread.ensureTickThread("Cannot move an entity off-main"); ++ synchronized (this.posLock) { ++ this.moveStartX = this.getX(); ++ this.moveStartY = this.getY(); ++ this.moveStartZ = this.getZ(); ++ this.moveVector = movement; ++ } ++ try { ++ // Tuinity end - detailed watchdog information + if (this.noPhysics) { + this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z); + } else { +@@ -949,9 +1053,44 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + float f2 = this.getBlockSpeedFactor(); + + this.setDeltaMovement(this.getDeltaMovement().multiply((double) f2, 1.0D, (double) f2)); +- if (this.level.getBlockStatesIfLoaded(this.getBoundingBox().deflate(1.0E-6D)).noneMatch((iblockdata1) -> { +- return iblockdata1.is((Tag) BlockTags.FIRE) || iblockdata1.is(Blocks.LAVA); +- })) { ++ // Tuinity start - remove expensive streams from here ++ boolean noneMatch = true; ++ AABB fireSearchBox = this.getBoundingBox().deflate(1.0E-6D); ++ { ++ int minX = Mth.floor(fireSearchBox.minX); ++ int minY = Mth.floor(fireSearchBox.minY); ++ int minZ = Mth.floor(fireSearchBox.minZ); ++ int maxX = Mth.floor(fireSearchBox.maxX); ++ int maxY = Mth.floor(fireSearchBox.maxY); ++ int maxZ = Mth.floor(fireSearchBox.maxZ); ++ fire_search_loop: ++ for (int fz = minZ; fz <= maxZ; ++fz) { ++ for (int fx = minX; fx <= maxX; ++fx) { ++ for (int fy = minY; fy <= maxY; ++fy) { ++ net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)this.level.getChunkIfLoadedImmediately(fx >> 4, fz >> 4); ++ if (chunk == null) { ++ // Vanilla rets an empty stream if all the chunks are not loaded, so noneMatch will be true ++ // even if we're in lava/fire ++ noneMatch = true; ++ break fire_search_loop; ++ } ++ if (!noneMatch) { ++ // don't do get type, we already know we're in fire - we just need to check the chunks ++ // loaded state ++ continue; ++ } ++ ++ BlockState type = chunk.getType(fx, fy, fz); ++ if (type.is((Tag) BlockTags.FIRE) || type.is(Blocks.LAVA)) { ++ noneMatch = false; ++ // can't break, we need to retain vanilla behavior by ensuring ALL chunks are loaded ++ } ++ } ++ } ++ } ++ } ++ if (noneMatch) { ++ // Tuinity end - remove expensive streams from here + if (this.remainingFireTicks <= 0) { + this.setRemainingFireTicks(-this.getFireImmuneTicks()); + } +@@ -968,6 +1107,13 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + this.level.getProfiler().pop(); + } + } ++ // Tuinity start - detailed watchdog information ++ } finally { ++ synchronized (this.posLock) { // Tuinity ++ this.moveVector = null; ++ } // Tuinity ++ } ++ // Tuinity end - detailed watchdog information + } + + protected void tryCheckInsideBlocks() { +@@ -1073,39 +1219,79 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + return offsetFactor; + } + +- private Vec3 collide(Vec3 movement) { +- AABB axisalignedbb = this.getBoundingBox(); +- CollisionContext voxelshapecollision = CollisionContext.of(this); +- VoxelShape voxelshape = this.level.getWorldBorder().getCollisionShape(); +- Stream stream = !this.level.getWorldBorder().isWithinBounds(axisalignedbb) ? Stream.empty() : Stream.of(voxelshape); // Paper +- Stream stream1 = this.level.getEntityCollisions(this, axisalignedbb.expandTowards(movement), (entity) -> { +- return true; +- }); +- RewindableStream streamaccumulator = new RewindableStream<>(Stream.concat(stream1, stream)); +- Vec3 vec3d1 = movement.lengthSqr() == 0.0D ? movement : Entity.collideBoundingBoxHeuristically(this, movement, axisalignedbb, this.level, voxelshapecollision, streamaccumulator); +- boolean flag = movement.x != vec3d1.x; +- boolean flag1 = movement.y != vec3d1.y; +- boolean flag2 = movement.z != vec3d1.z; +- boolean flag3 = this.onGround || flag1 && movement.y < 0.0D; +- +- if (this.maxUpStep > 0.0F && flag3 && (flag || flag2)) { +- Vec3 vec3d2 = Entity.collideBoundingBoxHeuristically(this, new Vec3(movement.x, (double) this.maxUpStep, movement.z), axisalignedbb, this.level, voxelshapecollision, streamaccumulator); +- Vec3 vec3d3 = Entity.collideBoundingBoxHeuristically(this, new Vec3(0.0D, (double) this.maxUpStep, 0.0D), axisalignedbb.expandTowards(movement.x, 0.0D, movement.z), this.level, voxelshapecollision, streamaccumulator); +- +- if (vec3d3.y < (double) this.maxUpStep) { +- Vec3 vec3d4 = Entity.collideBoundingBoxHeuristically(this, new Vec3(movement.x, 0.0D, movement.z), axisalignedbb.move(vec3d3), this.level, voxelshapecollision, streamaccumulator).add(vec3d3); +- +- if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) { +- vec3d2 = vec3d4; ++ private Vec3 collide(Vec3 moveVector) { ++ // Tuinity start - optimise collisions ++ // This is a copy of vanilla's except that it uses strictly AABB math ++ if (moveVector.x == 0.0 && moveVector.y == 0.0 && moveVector.z == 0.0) { ++ return moveVector; ++ } ++ ++ final Level world = this.level; ++ final AABB currBoundingBox = this.getBoundingBox(); ++ ++ if (com.tuinity.tuinity.util.CollisionUtil.isEmpty(currBoundingBox)) { ++ return moveVector; ++ } ++ ++ final List potentialCollisions = com.tuinity.tuinity.util.CachedLists.getTempCollisionList(); ++ try { ++ final double stepHeight = (double)this.maxUpStep; ++ final AABB collisionBox; ++ ++ if (moveVector.x == 0.0 && moveVector.z == 0.0 && moveVector.y != 0.0) { ++ if (moveVector.y > 0.0) { ++ collisionBox = com.tuinity.tuinity.util.CollisionUtil.cutUpwards(currBoundingBox, moveVector.y); ++ } else { ++ collisionBox = com.tuinity.tuinity.util.CollisionUtil.cutDownwards(currBoundingBox, moveVector.y); ++ } ++ } else { ++ if (stepHeight > 0.0 && (this.onGround || (moveVector.y < 0.0)) && (moveVector.x != 0.0 || moveVector.z != 0.0)) { ++ // don't bother getting the collisions if we don't need them. ++ if (moveVector.y <= 0.0) { ++ collisionBox = com.tuinity.tuinity.util.CollisionUtil.expandUpwards(currBoundingBox.expandTowards(moveVector.x, moveVector.y, moveVector.z), stepHeight); ++ } else { ++ collisionBox = currBoundingBox.expandTowards(moveVector.x, Math.max(stepHeight, moveVector.y), moveVector.z); ++ } ++ } else { ++ collisionBox = currBoundingBox.expandTowards(moveVector.x, moveVector.y, moveVector.z); + } + } + +- if (vec3d2.horizontalDistanceSqr() > vec3d1.horizontalDistanceSqr()) { +- return vec3d2.add(Entity.collideBoundingBoxHeuristically(this, new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), axisalignedbb.move(vec3d2), this.level, voxelshapecollision, streamaccumulator)); ++ com.tuinity.tuinity.util.CollisionUtil.getCollisions(world, this, collisionBox, potentialCollisions, false, true, ++ false, false, null, null); ++ ++ if (com.tuinity.tuinity.util.CollisionUtil.isCollidingWithBorderEdge(world.getWorldBorder(), collisionBox)) { ++ com.tuinity.tuinity.util.CollisionUtil.addBoxesToIfIntersects(world.getWorldBorder().getCollisionShape(), collisionBox, potentialCollisions); + } +- } + +- return vec3d1; ++ final Vec3 limitedMoveVector = com.tuinity.tuinity.util.CollisionUtil.performCollisions(moveVector, currBoundingBox, potentialCollisions); ++ ++ if (stepHeight > 0.0 ++ && (this.onGround || (limitedMoveVector.y != moveVector.y && moveVector.y < 0.0)) ++ && (limitedMoveVector.x != moveVector.x || limitedMoveVector.z != moveVector.z)) { ++ Vec3 vec3d2 = com.tuinity.tuinity.util.CollisionUtil.performCollisions(new Vec3(moveVector.x, stepHeight, moveVector.z), currBoundingBox, potentialCollisions); ++ final Vec3 vec3d3 = com.tuinity.tuinity.util.CollisionUtil.performCollisions(new Vec3(0.0, stepHeight, 0.0), currBoundingBox.expandTowards(moveVector.x, 0.0, moveVector.z), potentialCollisions); ++ ++ if (vec3d3.y < stepHeight) { ++ final Vec3 vec3d4 = com.tuinity.tuinity.util.CollisionUtil.performCollisions(new Vec3(moveVector.x, 0.0D, moveVector.z), currBoundingBox.move(vec3d3), potentialCollisions).add(vec3d3); ++ ++ if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) { ++ vec3d2 = vec3d4; ++ } ++ } ++ ++ if (vec3d2.horizontalDistanceSqr() > limitedMoveVector.horizontalDistanceSqr()) { ++ return vec3d2.add(com.tuinity.tuinity.util.CollisionUtil.performCollisions(new Vec3(0.0D, -vec3d2.y + moveVector.y, 0.0D), currBoundingBox.move(vec3d2), potentialCollisions)); ++ } ++ ++ return limitedMoveVector; ++ } else { ++ return limitedMoveVector; ++ } ++ } finally { ++ com.tuinity.tuinity.util.CachedLists.returnTempCollisionList(potentialCollisions); ++ } ++ // Tuinity end - optimise collisions + } + + public static Vec3 collideBoundingBoxHeuristically(@Nullable Entity entity, Vec3 movement, AABB entityBoundingBox, Level world, CollisionContext context, RewindableStream collisions) { +@@ -2244,9 +2430,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + float f = this.dimensions.width * 0.8F; + AABB axisalignedbb = AABB.ofSize(this.getEyePosition(), (double) f, 1.0E-6D, (double) f); + +- return this.level.getBlockCollisions(this, axisalignedbb, (iblockdata, blockposition) -> { +- return iblockdata.isSuffocating(this.level, blockposition); +- }).findAny().isPresent(); ++ // Tuinity start ++ return com.tuinity.tuinity.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this.level, this, axisalignedbb, null, ++ false, false, false, true, (iblockdata, blockposition) -> { ++ return iblockdata.isSuffocating(this.level, blockposition); ++ }); ++ // Tuinity end + } + } + +@@ -2254,11 +2443,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + return InteractionResult.PASS; + } + +- public boolean canCollideWith(Entity other) { ++ public boolean canCollideWith(Entity other) { // Tuinity - diff on change, hard colliding entities override this - TODO CHECK ON UPDATE - AbstractMinecart/Boat override + return other.canBeCollidedWith() && !this.isPassengerOfSameVehicle(other); + } + +- public boolean canBeCollidedWith() { ++ public boolean canBeCollidedWith() { // Tuinity - diff on change, hard colliding entities override this TODO CHECK ON UPDATE - Boat/Shulker override + return false; + } + +@@ -3727,7 +3916,9 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + } + + public void setDeltaMovement(Vec3 velocity) { ++ synchronized (this.posLock) { // Tuinity + this.deltaMovement = velocity; ++ } // Tuinity + } + + public void setDeltaMovement(double x, double y, double z) { +@@ -3789,7 +3980,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + public final void setPosRaw(double x, double y, double z) { + // Paper start - fix MC-4 + if (this instanceof ItemEntity) { +- if (com.destroystokyo.paper.PaperConfig.fixEntityPositionDesync) { ++ if (false && com.destroystokyo.paper.PaperConfig.fixEntityPositionDesync) { // Tuinity - revert + // encode/decode from PacketPlayOutEntity + x = Mth.lfloor(x * 4096.0D) * (1 / 4096.0D); + y = Mth.lfloor(y * 4096.0D) * (1 / 4096.0D); +@@ -3804,7 +3995,9 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + } + // Paper end + if (this.position.x != x || this.position.y != y || this.position.z != z) { ++ synchronized (this.posLock) { // Tuinity + this.position = new Vec3(x, y, z); ++ } // Tuinity + int i = Mth.floor(x); + int j = Mth.floor(y); + int k = Mth.floor(z); +diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java +index 0ce0e7a923da812a02d9ab83607d3cc9c87047df..f8c6d88d6bf71e7bc46b5f44e688229da5fd3da2 100644 +--- a/src/main/java/net/minecraft/world/entity/Mob.java ++++ b/src/main/java/net/minecraft/world/entity/Mob.java +@@ -785,7 +785,12 @@ public abstract class Mob extends LivingEntity { + if (this.level.getDifficulty() == Difficulty.PEACEFUL && this.shouldDespawnInPeaceful()) { + this.discard(); + } else if (!this.isPersistenceRequired() && !this.requiresCustomPersistence()) { +- Player entityhuman = this.level.findNearbyPlayer(this, -1.0D, EntitySelector.affectsSpawning); // Paper ++ // Tuinity start - optimise checkDespawn ++ Player entityhuman = this.level.findNearbyPlayer(this, level.paperConfig.hardDespawnDistance + 1, EntitySelector.affectsSpawning); // Paper ++ if (entityhuman == null) { ++ entityhuman = ((ServerLevel)this.level).playersAffectingSpawning.isEmpty() ? null : ((ServerLevel)this.level).playersAffectingSpawning.get(0); ++ } ++ // Tuinity end - optimise checkDespawn + + if (entityhuman != null) { + double d0 = entityhuman.distanceToSqr((Entity) this); // CraftBukkit - decompile error +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java +index 84a0ee595bebcc1947c602c4c06e7437706ce37c..efe66264ad5717bf3aac0fbda07275fb5571acc1 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java +@@ -83,7 +83,11 @@ public class AcquirePoi extends Behavior { + return true; + } + }; +- Set set = poiManager.findAllClosestFirst(this.poiType.getPredicate(), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE).limit(5L).collect(Collectors.toSet()); ++ // Tuinity start - optimise POI access ++ java.util.List poiposes = new java.util.ArrayList<>(); ++ com.tuinity.tuinity.util.PoiAccess.findNearestPoiPositions(poiManager, this.poiType.getPredicate(), predicate, entity.blockPosition(), 48, 48*48, PoiManager.Occupancy.HAS_SPACE, false, 5, poiposes); ++ Set set = new java.util.HashSet<>(poiposes); ++ // Tuinity end - optimise POI access + Path path = entity.getNavigation().createPath(set, this.poiType.getValidRange()); + if (path != null && path.canReach()) { + BlockPos blockPos = path.getTarget(); +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java b/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java +index 09998d160a6d79fdb5a5041a5d572649a1532e6a..3fe1f9bd4bb670d9a1ddabf2475f4d8f44d7e6fe 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java +@@ -30,11 +30,19 @@ public class GateBehavior extends Behavior { + + @Override + protected boolean canStillUse(ServerLevel world, E entity, long time) { +- return this.behaviors.stream().filter((behavior) -> { +- return behavior.getStatus() == Behavior.Status.RUNNING; +- }).anyMatch((behavior) -> { +- return behavior.canStillUse(world, entity, time); +- }); ++ // Tuinity start - remove streams ++ List>> entries = this.behaviors.entries; ++ for (int i = 0; i < entries.size(); i++) { ++ ShufflingList.WeightedEntry> entry = entries.get(i); ++ Behavior behavior = entry.getData(); ++ if (behavior.getStatus() == Status.RUNNING) { ++ if (behavior.canStillUse(world, entity, time)) { ++ return true; ++ } ++ } ++ } ++ return false; ++ // Tuinity end - remove streams + } + + @Override +@@ -45,25 +53,35 @@ public class GateBehavior extends Behavior { + @Override + protected void start(ServerLevel world, E entity, long time) { + this.orderPolicy.apply(this.behaviors); +- this.runningPolicy.apply(this.behaviors.stream(), world, entity, time); ++ this.runningPolicy.apply(this.behaviors.entries, world, entity, time); // Tuinity - remove streams + } + + @Override + protected void tick(ServerLevel world, E entity, long time) { +- this.behaviors.stream().filter((behavior) -> { +- return behavior.getStatus() == Behavior.Status.RUNNING; +- }).forEach((behavior) -> { +- behavior.tickOrStop(world, entity, time); +- }); ++ // Tuinity start - remove streams ++ List>> entries = this.behaviors.entries; ++ for (int i = 0; i < entries.size(); i++) { ++ ShufflingList.WeightedEntry> entry = entries.get(i); ++ Behavior behavior = entry.getData(); ++ if (behavior.getStatus() == Status.RUNNING) { ++ behavior.tickOrStop(world, entity, time); ++ } ++ } ++ // Tuinity end - remove streams + } + + @Override + protected void stop(ServerLevel world, E entity, long time) { +- this.behaviors.stream().filter((behavior) -> { +- return behavior.getStatus() == Behavior.Status.RUNNING; +- }).forEach((behavior) -> { +- behavior.doStop(world, entity, time); +- }); ++ // Tuinity start - remove streams ++ List>> entries = this.behaviors.entries; ++ for (int i = 0; i < entries.size(); i++) { ++ ShufflingList.WeightedEntry> entry = entries.get(i); ++ Behavior behavior = entry.getData(); ++ if (behavior.getStatus() == Status.RUNNING) { ++ behavior.doStop(world, entity, time); ++ } ++ } ++ // Tuinity end - remove streams + this.exitErasedMemories.forEach(entity.getBrain()::eraseMemory); + } + +@@ -94,25 +112,33 @@ public class GateBehavior extends Behavior { + public static enum RunningPolicy { + RUN_ONE { + @Override +- public void apply(Stream> tasks, ServerLevel world, E entity, long time) { +- tasks.filter((behavior) -> { +- return behavior.getStatus() == Behavior.Status.STOPPED; +- }).filter((behavior) -> { +- return behavior.tryStart(world, entity, time); +- }).findFirst(); ++ // Tuinity start - remove streams ++ public void apply(List>> tasks, ServerLevel world, E entity, long time) { ++ for (int i = 0; i < tasks.size(); i++) { ++ ShufflingList.WeightedEntry> task = tasks.get(i); ++ Behavior behavior = task.getData(); ++ if (behavior.getStatus() == Status.STOPPED && behavior.tryStart(world, entity, time)) { ++ break; ++ } ++ } ++ // Tuinity end - remove streams + } + }, + TRY_ALL { + @Override +- public void apply(Stream> tasks, ServerLevel world, E entity, long time) { +- tasks.filter((behavior) -> { +- return behavior.getStatus() == Behavior.Status.STOPPED; +- }).forEach((behavior) -> { +- behavior.tryStart(world, entity, time); +- }); ++ // Tuinity start - remove streams ++ public void apply(List>> tasks, ServerLevel world, E entity, long time) { ++ for (int i = 0; i < tasks.size(); i++) { ++ ShufflingList.WeightedEntry> task = tasks.get(i); ++ Behavior behavior = task.getData(); ++ if (behavior.getStatus() == Status.STOPPED) { ++ behavior.tryStart(world, entity, time); ++ } ++ } ++ // Tuinity end - remove streams + } + }; + +- public abstract void apply(Stream> tasks, ServerLevel world, E entity, long time); ++ public abstract void apply(List>> tasks, ServerLevel world, E entity, long time); // Tuinity - remove streams + } + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/SetLookAndInteract.java b/src/main/java/net/minecraft/world/entity/ai/behavior/SetLookAndInteract.java +index 1f59e790d62f0be8e505e339a6699ca3964aea0d..bb43e47d4b3989610a52c1941598865aee93ac04 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/SetLookAndInteract.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/SetLookAndInteract.java +@@ -34,21 +34,42 @@ public class SetLookAndInteract extends Behavior { + + @Override + public boolean checkExtraStartConditions(ServerLevel world, LivingEntity entity) { +- return this.selfFilter.test(entity) && this.getVisibleEntities(entity).stream().anyMatch(this::isMatchingTarget); ++ // Tuinity start - remove streams ++ if (!this.selfFilter.test(entity)) { ++ return false; ++ } ++ ++ List visibleEntities = this.getVisibleEntities(entity); ++ for (int i = 0; i < visibleEntities.size(); i++) { ++ LivingEntity livingEntity = visibleEntities.get(i); ++ if (this.isMatchingTarget(livingEntity)) { ++ return true; ++ } ++ } ++ return false; ++ // Tuinity end - remove streams + } + + @Override + public void start(ServerLevel world, LivingEntity entity, long time) { + super.start(world, entity, time); + Brain brain = entity.getBrain(); +- brain.getMemory(MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES).ifPresent((list) -> { +- list.stream().filter((livingEntity2) -> { +- return livingEntity2.distanceToSqr(entity) <= (double)this.interactionRangeSqr; +- }).filter(this::isMatchingTarget).findFirst().ifPresent((livingEntity) -> { +- brain.setMemory(MemoryModuleType.INTERACTION_TARGET, livingEntity); +- brain.setMemory(MemoryModuleType.LOOK_TARGET, new EntityTracker(livingEntity, true)); +- }); +- }); ++ // Tuinity start - remove streams ++ List list = brain.getMemory(MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES).orElse(null); ++ if (list != null) { ++ double maxRangeSquared = (double)this.interactionRangeSqr; ++ for (int i = 0; i < list.size(); i++) { ++ LivingEntity livingEntity2 = list.get(i); ++ if (livingEntity2.distanceToSqr(entity) <= maxRangeSquared) { ++ if (this.isMatchingTarget(livingEntity2)) { ++ brain.setMemory(MemoryModuleType.INTERACTION_TARGET, livingEntity2); ++ brain.setMemory(MemoryModuleType.LOOK_TARGET, new EntityTracker(livingEntity2, true)); ++ break; ++ } ++ } ++ } ++ } ++ // Tuinity end - remove streams + } + + private boolean isMatchingTarget(LivingEntity entity) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/ShufflingList.java b/src/main/java/net/minecraft/world/entity/ai/behavior/ShufflingList.java +index 4fa64b1e2004810906bb0b174436c8e687a75ada..d5a3c6d239abbb31c52ec2dfb9b18b1b705cbc88 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/ShufflingList.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/ShufflingList.java +@@ -12,7 +12,7 @@ import java.util.Random; + import java.util.stream.Stream; + + public class ShufflingList { +- protected final List> entries; ++ public final List> entries; // Tuinity - public + private final Random random = new Random(); + private final boolean isUnsafe; // Paper + +diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java +index e605daac0c90f5d0b9315d1499938feb0e478d0e..570316cf7831de70086fae35676006ee052851e0 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java ++++ b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java +@@ -27,7 +27,7 @@ import net.minecraft.world.phys.Vec3; + + public abstract class PathNavigation { + private static final int MAX_TIME_RECOMPUTE = 20; +- protected final Mob mob; ++ protected final Mob mob; public final Mob getEntity() { return this.mob; } // Tuinity - public accessor + protected final Level level; + @Nullable + protected Path path; +@@ -40,7 +40,7 @@ public abstract class PathNavigation { + protected long lastTimeoutCheck; + protected double timeoutLimit; + protected float maxDistanceToWaypoint = 0.5F; +- protected boolean hasDelayedRecomputation; ++ protected boolean hasDelayedRecomputation; protected final boolean needsPathRecalculation() { return this.hasDelayedRecomputation; } // Tuinity - public accessor + protected long timeLastRecompute; + protected NodeEvaluator nodeEvaluator; + private BlockPos targetPos; +@@ -49,6 +49,13 @@ public abstract class PathNavigation { + public final PathFinder pathFinder; + private boolean isStuck; + ++ // Tuinity start ++ public boolean isViableForPathRecalculationChecking() { ++ return !this.needsPathRecalculation() && ++ (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0); ++ } ++ // Tuinity end ++ + public PathNavigation(Mob mob, Level world) { + this.mob = mob; + this.level = world; +@@ -404,7 +411,7 @@ public abstract class PathNavigation { + } + + public void recomputePath(BlockPos pos) { +- if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) { ++ if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) { // Tuinity - diff on change - needed for isViableForPathRecalculationChecking() + Node node = this.path.getEndNode(); + Vec3 vec3 = new Vec3(((double)node.x + this.mob.getX()) / 2.0D, ((double)node.y + this.mob.getY()) / 2.0D, ((double)node.z + this.mob.getZ()) / 2.0D); + if (pos.closerThan(vec3, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()))) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java +index e41b2fa1db6fb77a26cdb498904021b430e35be0..f0ba454eea673bf02d1f6d7fe30c4f672643fe0c 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java +@@ -49,8 +49,12 @@ public class NearestBedSensor extends Sensor { + return true; + } + }; +- Stream stream = poiManager.findAll(PoiType.HOME.getPredicate(), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY); +- Path path = entity.getNavigation().createPath(stream, PoiType.HOME.getValidRange()); ++ // Tuinity start - optimise POI access ++ java.util.List poiposes = new java.util.ArrayList<>(); ++ // don't ask me why it's unbounded. ask mojang. ++ com.tuinity.tuinity.util.PoiAccess.findAnyPoiPositions(poiManager, PoiType.HOME.getPredicate(), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes); ++ Path path = entity.getNavigation().createPath(new java.util.HashSet<>(poiposes), PoiType.HOME.getValidRange()); ++ // Tuinity end - optimise POI access + if (path != null && path.canReach()) { + BlockPos blockPos = path.getTarget(); + Optional optional = poiManager.getType(blockPos); +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java +index 49f3b25d28072b61f5cc97260df61df892a58714..de6b591eb865c6f5c23aaa4b9374bb9bbaaa85f6 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java +@@ -25,17 +25,20 @@ public class NearestItemSensor extends Sensor { + protected void doTick(ServerLevel world, Mob entity) { + Brain brain = entity.getBrain(); + List list = world.getEntitiesOfClass(ItemEntity.class, entity.getBoundingBox().inflate(8.0D, 4.0D, 8.0D), (itemEntity) -> { +- return true; ++ return itemEntity.closerThan(entity, 9.0D) && entity.wantsToPickUp(itemEntity.getItem()); // Tuinity - move predicate into getEntities + }); +- list.sort(Comparator.comparingDouble(entity::distanceToSqr)); ++ list.sort((e1, e2) -> Double.compare(entity.distanceToSqr(e1), entity.distanceToSqr(e2))); // better to take the sort perf hit than using line of sight more than we need to. ++ // Tuinity start - remove streams + // Paper start - remove streams in favour of lists + ItemEntity nearest = null; +- for (ItemEntity entityItem : list) { +- if (entity.wantsToPickUp(entityItem.getItem()) && entityItem.closerThan(entity, 9.0D) && entity.hasLineOfSight(entityItem)) { ++ for (int i = 0; i < list.size(); i++) { ++ ItemEntity entityItem = list.get(i); ++ if (entity.hasLineOfSight(entityItem)) { + nearest = entityItem; + break; + } + } ++ // Tuinity end - remove streams + brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, Optional.ofNullable(nearest)); + // Paper end + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java +index ffd83db0a419ab589e89feeddd3fb038d6ed5839..31ef567cf4f331d3329dd176392686db56aead66 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java +@@ -18,12 +18,19 @@ public class NearestLivingEntitySensor extends Sensor { + List list = world.getEntitiesOfClass(LivingEntity.class, aABB, (livingEntity2) -> { + return livingEntity2 != entity && livingEntity2.isAlive(); + }); +- list.sort(Comparator.comparingDouble(entity::distanceToSqr)); ++ // Tuinity start - remove streams ++ list.sort((e1, e2) -> Double.compare(entity.distanceToSqr(e1), entity.distanceToSqr(e2))); + Brain brain = entity.getBrain(); + brain.setMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES, list); + // Paper start - remove streams in favour of lists +- List visibleMobs = new java.util.ArrayList<>(list); +- visibleMobs.removeIf(otherEntityLiving -> !Sensor.isEntityTargetable(entity, otherEntityLiving)); ++ List visibleMobs = new java.util.ArrayList<>(); ++ for (int i = 0, len = list.size(); i < len; i++) { ++ LivingEntity nearby = list.get(i); ++ if (Sensor.isEntityTargetable(entity, nearby)) { ++ visibleMobs.add(nearby); ++ } ++ } ++ // Tuinity end - remove streams + brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, visibleMobs); + // Paper end + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java +index 457ea75137b8b02dc32bf1769ae8d57c470da470..3392a8d425d9f5e1417a665fb1514d013bf89337 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java +@@ -21,25 +21,31 @@ public class PlayerSensor extends Sensor { + + @Override + protected void doTick(ServerLevel world, LivingEntity entity) { +- // Paper start - remove streams in favour of lists +- List players = new java.util.ArrayList<>(world.players()); +- players.removeIf(player -> !EntitySelector.NO_SPECTATORS.test(player) || !entity.closerThan(player, 16.0D)); // Paper - removeIf only re-allocates once compared to iterator ++ // Tuinity start - remove streams ++ List players = (List)world.getNearbyPlayers(entity, entity.getX(), entity.getY(), entity.getZ(), 16.0D, EntitySelector.NO_SPECTATORS); ++ players.sort((e1, e2) -> Double.compare(entity.distanceToSqr(e1), entity.distanceToSqr(e2))); + Brain brain = entity.getBrain(); + + brain.setMemory(MemoryModuleType.NEAREST_PLAYERS, players); + +- Player nearest = null, nearestTargetable = null; +- for (Player player : players) { +- if (Sensor.isEntityTargetable(entity, player)) { +- if (nearest == null) nearest = player; +- if (Sensor.isEntityAttackable(entity, player)) { +- nearestTargetable = player; +- break; // Both variables are assigned, no reason to loop further +- } ++ Player firstTargetable = null; ++ Player firstAttackable = null; ++ for (int index = 0, len = players.size(); index < len; ++index) { ++ Player player = players.get(index); ++ if (firstTargetable == null && isEntityTargetable(entity, player)) { ++ firstTargetable = player; ++ } ++ if (firstAttackable == null && isEntityAttackable(entity, player)) { ++ firstAttackable = player; ++ } ++ ++ if (firstAttackable != null && firstTargetable != null) { ++ break; + } + } +- brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_PLAYER, nearest); +- brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER, nearestTargetable); +- // Paper end ++ ++ brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_PLAYER, firstTargetable); ++ brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER, Optional.ofNullable(firstAttackable)); ++ // Tuinity end - remove streams + } + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/VillagerBabiesSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/VillagerBabiesSensor.java +index 478010bc291fa3276aab0f66ce6283403af710ec..a198538fe4ae560adc66fad5a2f6b80bbd894e4b 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/VillagerBabiesSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/VillagerBabiesSensor.java +@@ -22,7 +22,17 @@ public class VillagerBabiesSensor extends Sensor { + } + + private List getNearestVillagerBabies(LivingEntity entities) { +- return this.getVisibleEntities(entities).stream().filter(this::isVillagerBaby).collect(Collectors.toList()); ++ // Tuinity start - remove streams ++ List list = new java.util.ArrayList<>(); ++ List visibleEntities = this.getVisibleEntities(entities); ++ for (int i = 0; i < visibleEntities.size(); i++) { ++ LivingEntity livingEntity = visibleEntities.get(i); ++ if (this.isVillagerBaby(livingEntity)) { ++ list.add(livingEntity); ++ } ++ } ++ return list; ++ // Tuinity end - remove streams + } + + private boolean isVillagerBaby(LivingEntity entity) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +index 6c3455823f996e0421975b7f4a00f4e333e9f514..3ba30a2e6f1e3eb82b2b6e8968fd2babbf220ded 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +@@ -37,7 +37,7 @@ public class PoiManager extends SectionStorage { + public static final int VILLAGE_SECTION_SIZE = 1; + private final PoiManager.DistanceTracker distanceTracker; + private final LongSet loadedChunks = new LongOpenHashSet(); +- private final net.minecraft.server.level.ServerLevel world; // Paper ++ public final net.minecraft.server.level.ServerLevel world; // Paper // Tuinity public + + public PoiManager(File directory, DataFixer dataFixer, boolean dsync, LevelHeightAccessor world) { + super(directory, PoiSection::codec, PoiSection::new, dataFixer, DataFixTypes.POI_CHUNK, dsync, world); +@@ -100,36 +100,55 @@ public class PoiManager extends SectionStorage { + } + + public Optional find(Predicate typePredicate, Predicate posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) { +- return this.findAll(typePredicate, posPredicate, pos, radius, occupationStatus).findFirst(); ++ // Tuinity start - re-route to faster logic ++ BlockPos ret = com.tuinity.tuinity.util.PoiAccess.findAnyPoiPosition(this, typePredicate, posPredicate, pos, radius, occupationStatus, false); ++ return Optional.ofNullable(ret); ++ // Tuinity end - re-route to faster logic + } + + public Optional findClosest(Predicate typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) { +- return this.getInRange(typePredicate, pos, radius, occupationStatus).map(PoiRecord::getPos).min(Comparator.comparingDouble((blockPos2) -> { +- return blockPos2.distSqr(pos); +- })); ++ // Tuinity start - re-route to faster logic ++ BlockPos ret = com.tuinity.tuinity.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, null, pos, radius, radius*radius, occupationStatus, false); ++ return Optional.ofNullable(ret); ++ // Tuinity end - re-route to faster logic + } + + public Optional findClosest(Predicate predicate, Predicate predicate2, BlockPos blockPos, int i, PoiManager.Occupancy occupancy) { +- return this.getInRange(predicate, blockPos, i, occupancy).map(PoiRecord::getPos).filter(predicate2).min(Comparator.comparingDouble((blockPos2) -> { +- return blockPos2.distSqr(blockPos); +- })); ++ // Tuinity start - re-route to faster logic ++ BlockPos ret = com.tuinity.tuinity.util.PoiAccess.findClosestPoiDataPosition(this, predicate, predicate2, blockPos, i, i*i, occupancy, false); ++ return Optional.ofNullable(ret); ++ // Tuinity end - re-route to faster logic + } + + public Optional take(Predicate typePredicate, Predicate positionPredicate, BlockPos pos, int radius) { +- return this.getInRange(typePredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE).filter((poi) -> { +- return positionPredicate.test(poi.getPos()); +- }).findFirst().map((poi) -> { +- poi.acquireTicket(); +- return poi.getPos(); +- }); ++ // Tuinity start - re-route to faster logic ++ PoiRecord ret = com.tuinity.tuinity.util.PoiAccess.findAnyPoiRecord( ++ this, typePredicate, positionPredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE, false ++ ); ++ if (ret == null) { ++ return Optional.empty(); ++ } ++ ret.acquireTicket(); ++ return Optional.of(ret.getPos()); ++ // Tuinity end - re-route to faster logic + } + + public Optional getRandom(Predicate typePredicate, Predicate positionPredicate, PoiManager.Occupancy occupationStatus, BlockPos pos, int radius, Random random) { +- List list = this.getInRange(typePredicate, pos, radius, occupationStatus).collect(Collectors.toList()); +- Collections.shuffle(list, random); +- return list.stream().filter((poiRecord) -> { +- return positionPredicate.test(poiRecord.getPos()); +- }).findFirst().map(PoiRecord::getPos); ++ // Tuinity start - re-route to faster logic ++ List list = new java.util.ArrayList<>(); ++ com.tuinity.tuinity.util.PoiAccess.findAnyPoiRecords( ++ this, typePredicate, positionPredicate, pos, radius, occupationStatus, false, Integer.MAX_VALUE, list ++ ); ++ ++ // the old method shuffled the list and then tried to find the first element in it that ++ // matched positionPredicate, however we moved positionPredicate into the poi search. This means we can avoid a ++ // shuffle entirely, and just pick a random element from list ++ if (list.isEmpty()) { ++ return Optional.empty(); ++ } ++ ++ return Optional.of(list.get(random.nextInt(list.size())).getPos()); ++ // Tuinity end - re-route to faster logic + } + + public boolean release(BlockPos pos) { +@@ -183,7 +202,7 @@ public class PoiManager extends SectionStorage { + data = this.getData(chunkcoordintpair); + } + com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world, +- chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY); ++ chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY); // Tuinity - use normal priority + } + // Paper end + this.distanceTracker.runAllUpdates(); +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java +index 75c1c4671fedb425dea20dc4fb0c6cb2304dee83..fc7b364adc2d0e5db22aa25e029c2e13c84d6096 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java +@@ -25,12 +25,12 @@ import org.apache.logging.log4j.Logger; + public class PoiSection { + private static final Logger LOGGER = LogManager.getLogger(); + private final Short2ObjectMap records = new Short2ObjectOpenHashMap<>(); +- private final Map> byType = Maps.newHashMap(); ++ private final Map> byType = Maps.newHashMap(); public final Map> getData() { return this.byType; } // Tuinity - public accessor + private final Runnable setDirty; + private boolean isValid; + + public static Codec codec(Runnable updateListener) { +- return RecordCodecBuilder.create((instance) -> { ++ return RecordCodecBuilder.create((instance) -> { // Tuinity - decompile fix + return instance.group(RecordCodecBuilder.point(updateListener), Codec.BOOL.optionalFieldOf("Valid", Boolean.valueOf(false)).forGetter((poiSet) -> { + return poiSet.isValid; + }), PoiRecord.codec(updateListener).listOf().fieldOf("Records").forGetter((poiSet) -> { +diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +index 925f16d5eb092518ef774f69a8d99689feb0f5d7..01d8af06f19427354cac95d691e65d31253fef94 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +@@ -91,7 +91,7 @@ public class Turtle extends Animal { + } + + public void setHomePos(BlockPos pos) { +- this.entityData.set(Turtle.HOME_POS, pos); ++ this.entityData.set(Turtle.HOME_POS, pos.immutable()); // Paper - called with mutablepos... + } + + public BlockPos getHomePos() { // Paper - public +diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java +index 19980b2d627eb3cacf8d0c3e6785ad2206910fbc..e7a7de5ad9b64876df77e20465631ca8e5b19a4a 100644 +--- a/src/main/java/net/minecraft/world/entity/player/Player.java ++++ b/src/main/java/net/minecraft/world/entity/player/Player.java +@@ -498,6 +498,11 @@ public abstract class Player extends LivingEntity { + this.containerMenu = this.inventoryMenu; + } + // Paper end ++ // Tuinity start - special close for unloaded inventory ++ public void closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) { ++ this.containerMenu = this.inventoryMenu; ++ } ++ // Tuinity end - special close for unloaded inventory + + public void closeContainer() { + this.containerMenu = this.inventoryMenu; +diff --git a/src/main/java/net/minecraft/world/item/EnderEyeItem.java b/src/main/java/net/minecraft/world/item/EnderEyeItem.java +index 7ccfe737fdf7f07b731ea0ff82e897564350705c..abcc3dac7c7369a3f37e85ddeecbe272833298c9 100644 +--- a/src/main/java/net/minecraft/world/item/EnderEyeItem.java ++++ b/src/main/java/net/minecraft/world/item/EnderEyeItem.java +@@ -60,9 +60,10 @@ public class EnderEyeItem extends Item { + + // CraftBukkit start - Use relative location for far away sounds + // world.b(1038, blockposition1.c(1, 0, 1), 0); +- int viewDistance = world.getCraftServer().getViewDistance() * 16; ++ //int viewDistance = world.getCraftServer().getViewDistance() * 16; // Tuinity - apply view distance patch + BlockPos soundPos = blockposition1.offset(1, 0, 1); + for (ServerPlayer player : world.getServer().getPlayerList().players) { ++ final int viewDistance = player.getBukkitEntity().getViewDistance(); // Tuinity - apply view distance patch + double deltaX = soundPos.getX() - player.getX(); + double deltaZ = soundPos.getZ() - player.getZ(); + double distanceSquared = deltaX * deltaX + deltaZ * deltaZ; +diff --git a/src/main/java/net/minecraft/world/level/BlockGetter.java b/src/main/java/net/minecraft/world/level/BlockGetter.java +index fe4dba491b586757a16aa36e62682f364daa2602..ec781ab232d12cedb5f0236860377c4917c576d7 100644 +--- a/src/main/java/net/minecraft/world/level/BlockGetter.java ++++ b/src/main/java/net/minecraft/world/level/BlockGetter.java +@@ -84,7 +84,8 @@ public interface BlockGetter extends LevelHeightAccessor { + return BlockHitResult.miss(raytrace1.getTo(), Direction.getNearest(vec3d.x, vec3d.y, vec3d.z), new BlockPos(raytrace1.getTo())); + } + // Paper end +- FluidState fluid = this.getFluidState(blockposition); ++ if (iblockdata.isAir()) return null; // Tuinity - optimise air cases ++ FluidState fluid = iblockdata.getFluidState(); // Tuinity - don't need to go to world state again + Vec3 vec3d = raytrace1.getFrom(); + Vec3 vec3d1 = raytrace1.getTo(); + VoxelShape voxelshape = raytrace1.getBlockShape(iblockdata, this, blockposition); +diff --git a/src/main/java/net/minecraft/world/level/CollisionGetter.java b/src/main/java/net/minecraft/world/level/CollisionGetter.java +index 2a784a8342e708e0813c7076a2ca8e429446ffd3..b909bd7bf10adc9165df49a210df0d73912cd626 100644 +--- a/src/main/java/net/minecraft/world/level/CollisionGetter.java ++++ b/src/main/java/net/minecraft/world/level/CollisionGetter.java +@@ -36,28 +36,40 @@ public interface CollisionGetter extends BlockGetter { + return this.isUnobstructed(entity, Shapes.create(entity.getBoundingBox())); + } + ++ // Tuinity start - optimise collisions ++ default boolean noCollision(Entity entity, AABB box, Predicate filter, boolean loadChunks) { ++ return !com.tuinity.tuinity.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, loadChunks, false, entity != null, true, null) ++ && !com.tuinity.tuinity.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, true, filter); ++ } ++ // Tuinity end - optimise collisions ++ + default boolean noCollision(AABB box) { +- return this.noCollision((Entity)null, box, (e) -> { +- return true; +- }); ++ // Tuinity start - optimise collisions ++ return !com.tuinity.tuinity.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, null, box, null, false, false, false, true, null) ++ && !com.tuinity.tuinity.util.CollisionUtil.getEntityHardCollisions(this, null, box, null, true, null); ++ // Tuinity end - optimise collisions + } + + default boolean noCollision(Entity entity) { +- return this.noCollision(entity, entity.getBoundingBox(), (e) -> { +- return true; +- }); ++ // Tuinity start - optimise collisions ++ AABB box = entity.getBoundingBox(); ++ return !com.tuinity.tuinity.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, false, false, entity != null, true, null) ++ && !com.tuinity.tuinity.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, true, null); ++ // Tuinity end - optimise collisions + } + + default boolean noCollision(Entity entity, AABB box) { +- return this.noCollision(entity, box, (e) -> { +- return true; +- }); ++ // Tuinity start - optimise collisions ++ return !com.tuinity.tuinity.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, false, false, entity != null, true, null) ++ && !com.tuinity.tuinity.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, true, null); ++ // Tuinity end - optimise collisions + } + + default boolean noCollision(@Nullable Entity entity, AABB box, Predicate filter) { +- try { if (entity != null) entity.collisionLoadChunks = true; // Paper +- return this.getCollisions(entity, box, filter).allMatch(VoxelShape::isEmpty); +- } finally { if (entity != null) entity.collisionLoadChunks = false; } // Paper ++ // Tuinity start - optimise collisions ++ return !com.tuinity.tuinity.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, false, false, entity != null, true, null) ++ && !com.tuinity.tuinity.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, true, filter); ++ // Tuinity end - optimise collisions + } + + Stream getEntityCollisions(@Nullable Entity entity, AABB box, Predicate predicate); +diff --git a/src/main/java/net/minecraft/world/level/CollisionSpliterator.java b/src/main/java/net/minecraft/world/level/CollisionSpliterator.java +index e420c98d9ccc45d570984dc30fdb928883edec9f..ac83704692cf60c34b579ed11689863ef191cad3 100644 +--- a/src/main/java/net/minecraft/world/level/CollisionSpliterator.java ++++ b/src/main/java/net/minecraft/world/level/CollisionSpliterator.java +@@ -99,7 +99,7 @@ public class CollisionSpliterator extends AbstractSpliterator { + + VoxelShape voxelShape = blockState.getCollisionShape(this.collisionGetter, this.pos, this.context); + if (voxelShape == Shapes.block()) { +- if (!this.box.intersects((double)i, (double)j, (double)k, (double)i + 1.0D, (double)j + 1.0D, (double)k + 1.0D)) { ++ if (!com.tuinity.tuinity.util.CollisionUtil.voxelShapeIntersect(this.box, (double)i, (double)j, (double)k, (double)i + 1.0D, (double)j + 1.0D, (double)k + 1.0D)) { // Tuinity - keep vanilla behavior for voxelshape intersection - See comment in CollisionUtil + continue; + } + +diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java +index 325e244c46ec208a2e7e18d71ccbbfcc25fc1bce..6a4e44dd8935018d1b5283761dfb8e855be62987 100644 +--- a/src/main/java/net/minecraft/world/level/EntityGetter.java ++++ b/src/main/java/net/minecraft/world/level/EntityGetter.java +@@ -18,6 +18,18 @@ import net.minecraft.world.phys.shapes.Shapes; + import net.minecraft.world.phys.shapes.VoxelShape; + + public interface EntityGetter { ++ ++ // Tuinity start ++ List getHardCollidingEntities(Entity except, AABB box, Predicate predicate); ++ ++ void getEntities(Entity except, AABB box, Predicate predicate, List into); ++ ++ void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into); ++ ++ void getEntitiesByClass(Class clazz, Entity except, final AABB box, List into, ++ Predicate predicate); ++ // Tuinity end ++ + List getEntities(@Nullable Entity except, AABB box, Predicate predicate); + + List getEntities(EntityTypeTest filter, AABB box, Predicate predicate); +@@ -37,7 +49,7 @@ public interface EntityGetter { + return true; + } else { + for(Entity entity2 : this.getEntities(entity, shape.bounds())) { +- if (!entity2.isRemoved() && entity2.blocksBuilding && (entity == null || !entity2.isPassengerOfSameVehicle(entity)) && Shapes.joinIsNotEmpty(shape, Shapes.create(entity2.getBoundingBox()), BooleanOp.AND)) { ++ if (!entity2.isRemoved() && entity2.blocksBuilding && (entity == null || !entity2.isPassengerOfSameVehicle(entity)) && shape.intersects(entity2.getBoundingBox())) { // Tuinity + return false; + } + } +@@ -54,9 +66,9 @@ public interface EntityGetter { + if (box.getSize() < 1.0E-7D) { + return Stream.empty(); + } else { +- AABB aABB = box.inflate(1.0E-7D); +- return this.getEntities(entity, aABB, predicate.and((entityx) -> { +- if (entityx.getBoundingBox().intersects(aABB)) { ++ AABB aABB = box.inflate(-1.0E-7D); // Tuinity - needs to be negated, or else we get things we don't collide with ++ Predicate hardCollides = (entityx) -> { // Tuinity - optimise entity hard collisions ++ if (true || entityx.getBoundingBox().intersects(aABB)) { // Tuinity - always true + if (entity == null) { + if (entityx.canBeCollidedWith()) { + return true; +@@ -67,7 +79,11 @@ public interface EntityGetter { + } + + return false; +- })).stream().map(Entity::getBoundingBox).map(Shapes::create); ++ }; // Tuinity start - optimise entity hard collisions ++ predicate = predicate == null ? hardCollides : hardCollides.and(predicate); ++ return (entity != null && entity.hardCollides() ? this.getEntities(entity, aABB, predicate) : this.getHardCollidingEntities(entity, aABB, predicate)) ++ .stream().map(Entity::getBoundingBox).map(Shapes::create); ++ // Tuinity end - optimise entity hard collisions + } + } + +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 474078b68f1bf22037495f42bae59b790fd8cb46..61a4dea715689b0ce9247040db5dd2080ee2e167 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -167,6 +167,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public final com.destroystokyo.paper.PaperWorldConfig paperConfig; // Paper + public final com.destroystokyo.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray + ++ public final com.tuinity.tuinity.config.TuinityConfig.WorldConfig tuinityConfig; // Tuinity - Server Config ++ + public final co.aikar.timings.WorldTimingsHandler timings; // Paper + public static BlockPos lastPhysicsProblem; // Spigot + private org.spigotmc.TickLimiter entityLimiter; +@@ -203,9 +205,117 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + return this.typeKey; + } + ++ // Tuinity start ++ protected final com.tuinity.tuinity.world.EntitySliceManager entitySliceManager; ++ ++ // Tuinity start - optimise CraftChunk#getEntities ++ public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) { ++ com.tuinity.tuinity.world.ChunkEntitySlices slices = this.entitySliceManager.getChunk(chunkX, chunkZ); ++ if (slices == null) { ++ return new org.bukkit.entity.Entity[0]; ++ } ++ return slices.getChunkEntities(); ++ } ++ // Tuinity end - optimise CraftChunk#getEntities ++ ++ @Override ++ public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { ++ List ret = new java.util.ArrayList<>(); ++ this.entitySliceManager.getEntities(except, box, ret, predicate); ++ return ret; ++ } ++ ++ @Override ++ public void getEntities(Entity except, AABB box, Predicate predicate, List into) { ++ this.entitySliceManager.getEntities(except, box, into, predicate); ++ } ++ ++ @Override ++ public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) { ++ this.entitySliceManager.getHardCollidingEntities(except, box, into, predicate); ++ } ++ ++ @Override ++ public void getEntitiesByClass(Class clazz, Entity except, final AABB box, List into, ++ Predicate predicate) { ++ this.entitySliceManager.getEntities((Class)clazz, except, box, (List)into, (Predicate)predicate); ++ } ++ ++ @Override ++ public List getEntitiesOfClass(Class entityClass, AABB box, Predicate predicate) { ++ List ret = new java.util.ArrayList<>(); ++ this.entitySliceManager.getEntities(entityClass, null, box, ret, predicate); ++ return ret; ++ } ++ // Tuinity end ++ // Tuinity start - optimise checkDespawn ++ public final List getNearbyPlayers(@Nullable Entity source, double sourceX, double sourceY, ++ double sourceZ, double maxRange, @Nullable Predicate predicate) { ++ LevelChunk chunk; ++ if (maxRange < 0.0 || maxRange >= net.minecraft.server.level.ChunkMap.GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE || ++ (chunk = (LevelChunk)this.getChunkIfLoadedImmediately(Mth.floor(sourceX) >> 4, Mth.floor(sourceZ) >> 4)) == null) { ++ return this.getNearbyPlayersSlow(source, sourceX, sourceY, sourceZ, maxRange, predicate); ++ } ++ ++ List ret = new java.util.ArrayList<>(); ++ chunk.getNearestPlayers(sourceX, sourceY, sourceZ, predicate, maxRange, ret); ++ return ret; ++ } ++ ++ private List getNearbyPlayersSlow(@Nullable Entity source, double sourceX, double sourceY, ++ double sourceZ, double maxRange, @Nullable Predicate predicate) { ++ List ret = new java.util.ArrayList<>(); ++ double maxRangeSquared = maxRange * maxRange; ++ ++ for (net.minecraft.server.level.ServerPlayer player : (List)this.players()) { ++ if ((maxRange < 0.0 || player.distanceToSqr(sourceX, sourceY, sourceZ) < maxRangeSquared)) { ++ if (predicate == null || predicate.test(player)) { ++ ret.add(player); ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ private net.minecraft.server.level.ServerPlayer getNearestPlayerSlow(@Nullable Entity source, double sourceX, double sourceY, ++ double sourceZ, double maxRange, @Nullable Predicate predicate) { ++ net.minecraft.server.level.ServerPlayer closest = null; ++ double closestRangeSquared = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange; ++ ++ for (net.minecraft.server.level.ServerPlayer player : (List)this.players()) { ++ double distanceSquared = player.distanceToSqr(sourceX, sourceY, sourceZ); ++ if (distanceSquared < closestRangeSquared && (predicate == null || predicate.test(player))) { ++ closest = player; ++ closestRangeSquared = distanceSquared; ++ } ++ } ++ ++ return closest; ++ } ++ ++ ++ public final net.minecraft.server.level.ServerPlayer getNearestPlayer(@Nullable Entity source, double sourceX, double sourceY, ++ double sourceZ, double maxRange, @Nullable Predicate predicate) { ++ LevelChunk chunk; ++ if (maxRange < 0.0 || maxRange >= net.minecraft.server.level.ChunkMap.GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE || ++ (chunk = (LevelChunk)this.getChunkIfLoadedImmediately(Mth.floor(sourceX) >> 4, Mth.floor(sourceZ) >> 4)) == null) { ++ return this.getNearestPlayerSlow(source, sourceX, sourceY, sourceZ, maxRange, predicate); ++ } ++ ++ return chunk.findNearestPlayer(sourceX, sourceY, sourceZ, maxRange, predicate); ++ } ++ ++ @Override ++ public @Nullable Player getNearestPlayer(double d0, double d1, double d2, double d3, @Nullable Predicate predicate) { ++ return this.getNearestPlayer(null, d0, d1, d2, d3, predicate); ++ } ++ // Tuinity end - optimise checkDespawn ++ + protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, final DimensionType dimensionmanager, Supplier supplier, boolean flag, boolean flag1, long i, org.bukkit.generator.ChunkGenerator gen, org.bukkit.World.Environment env, java.util.concurrent.Executor executor) { // Paper - Anti-Xray - Pass executor + this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot + this.paperConfig = new com.destroystokyo.paper.PaperWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName(), this.spigotConfig); // Paper ++ this.tuinityConfig = new com.tuinity.tuinity.config.TuinityConfig.WorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData)worlddatamutable).getLevelName()); // Tuinity - Server Config + this.generator = gen; + this.world = new CraftWorld((ServerLevel) this, gen, env); + this.ticksPerAnimalSpawns = this.getCraftServer().getTicksPerAnimalSpawns(); // CraftBukkit +@@ -279,6 +389,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + this.chunkPacketBlockController = this.paperConfig.antiXray ? + new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) + : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray ++ this.entitySliceManager = new com.tuinity.tuinity.world.EntitySliceManager((ServerLevel)this); // Tuinity + } + + // Paper start +@@ -364,6 +475,15 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + @Override + public final LevelChunk getChunk(int chunkX, int chunkZ) { // Paper - final to help inline ++ // Tuinity start - make sure loaded chunks get the inlined variant of this function ++ net.minecraft.server.level.ServerChunkCache cps = ((ServerLevel)this).getChunkSource(); ++ if (cps.mainThread == Thread.currentThread()) { ++ LevelChunk ifLoaded = cps.getChunkAtIfLoadedMainThread(chunkX, chunkZ); ++ if (ifLoaded != null) { ++ return ifLoaded; ++ } ++ } ++ // Tuinity end - make sure loaded chunks get the inlined variant of this function + return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true); // Paper - avoid a method jump + } + +@@ -552,7 +672,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i); + // Paper start - per player view distance - allow block updates for non-ticking chunks in player view distance + // if copied from above +- } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || ((ServerLevel)this).getChunkSource().chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(MCUtil.getCoordinateKey(blockposition)) != null)) { ++ } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || ((ServerLevel)this).getChunkSource().chunkMap.playerChunkManager.broadcastMap.getObjectsInRange(MCUtil.getCoordinateKey(blockposition)) != null)) { // Tuinity - replace old player chunk management + ((ServerLevel)this).getChunkSource().blockChanged(blockposition); + // Paper end - per player view distance + } +@@ -867,6 +987,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public void guardEntityTick(Consumer tickConsumer, T entity) { + try { + tickConsumer.accept(entity); ++ MinecraftServer.getServer().executeMidTickTasks(); // Tuinity - execute chunk tasks mid tick + } catch (Throwable throwable) { + if (throwable instanceof ThreadDeath) throw throwable; // Paper + // Paper start - Prevent tile entity and entity crashes +@@ -996,26 +1117,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public List getEntities(@Nullable Entity except, AABB box, Predicate predicate) { + this.getProfiler().incrementCounter("getEntities"); + List list = Lists.newArrayList(); +- +- this.getEntities().get(box, (entity1) -> { +- if (entity1 != except && predicate.test(entity1)) { +- list.add(entity1); +- } +- +- if (entity1 instanceof EnderDragon) { +- EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity1).getSubEntities(); +- int i = aentitycomplexpart.length; +- +- for (int j = 0; j < i; ++j) { +- EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; +- +- if (entity1 != except && predicate.test(entitycomplexpart)) { +- list.add(entitycomplexpart); +- } +- } +- } +- +- }, predicate == net.minecraft.world.entity.EntitySelector.CONTAINER_ENTITY_SELECTOR); // Paper ++ this.entitySliceManager.getEntities(except, box, list, predicate); // Tuinity - optimise this call + return list; + } + +@@ -1024,26 +1126,22 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + this.getProfiler().incrementCounter("getEntities"); + List list = Lists.newArrayList(); + +- this.getEntities().get(filter, box, (entity) -> { +- if (predicate.test(entity)) { +- list.add(entity); +- } +- +- if (entity instanceof EnderDragon) { +- EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity).getSubEntities(); +- int i = aentitycomplexpart.length; +- +- for (int j = 0; j < i; ++j) { +- EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; +- T t0 = filter.tryCast(entitycomplexpart); +- +- if (t0 != null && predicate.test(t0)) { +- list.add(t0); +- } +- } ++ // Tuinity start - optimise this call ++ if (filter instanceof net.minecraft.world.entity.EntityType) { ++ this.entitySliceManager.getEntities((net.minecraft.world.entity.EntityType)filter, box, list, predicate); ++ } else { ++ Predicate test = (obj) -> { ++ return filter.tryCast(obj) != null; ++ }; ++ predicate = predicate == null ? test : test.and((Predicate)predicate); ++ Class base; ++ if (filter == null || (base = filter.getBaseClass()) == null || base == Entity.class) { ++ this.entitySliceManager.getEntities((Entity) null, box, (List)list, (Predicate)predicate); ++ } else { ++ this.entitySliceManager.getEntities(base, null, box, (List)list, (Predicate)predicate); // Tuinity - optimise this call + } +- +- }); ++ } ++ // Tuinity end - optimise this call + return list; + } + +@@ -1331,10 +1429,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public abstract TagContainer getTagManager(); + + public BlockPos getBlockRandomPos(int x, int y, int z, int l) { ++ // Paper start - allow use of mutable pos ++ BlockPos.MutableBlockPos ret = new BlockPos.MutableBlockPos(); ++ this.getRandomBlockPosition(x, y, z, l, ret); ++ return ret.immutable(); ++ } ++ public final BlockPos.MutableBlockPos getRandomBlockPosition(int x, int y, int z, int l, BlockPos.MutableBlockPos out) { ++ // Paper end + this.randValue = this.randValue * 3 + 1013904223; + int i1 = this.randValue >> 2; + +- return new BlockPos(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); ++ out.set(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); // Paper - change to setValues call ++ return out; // Paper + } + + public boolean noSave() { +diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +index 31fbcf6a35b902ce80c0a5a23dabb8ec3d8cbdfc..0059f0488acc22ebddc2faf4c5879f9f0c24fd14 100644 +--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java ++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +@@ -262,7 +262,7 @@ public final class NaturalSpawner { + blockposition_mutableblockposition.set(l, i, i1); + double d0 = (double) l + 0.5D; + double d1 = (double) i1 + 0.5D; +- Player entityhuman = world.getNearestPlayer(d0, (double) i, d1, -1.0D, false); ++ Player entityhuman = (chunk instanceof LevelChunk) ? ((LevelChunk)chunk).findNearestPlayer(d0, i, d1, 576.0D, net.minecraft.world.entity.EntitySelector.NO_SPECTATORS) : world.getNearestPlayer(d0, (double) i, d1, -1.0D, false); // Tuinity - use chunk's player cache to optimize search in range + + if (entityhuman != null) { + double d2 = entityhuman.distanceToSqr(d0, (double) i, d1); +@@ -335,7 +335,7 @@ public final class NaturalSpawner { + } + + private static boolean isRightDistanceToPlayerAndSpawnPoint(ServerLevel world, ChunkAccess chunk, BlockPos.MutableBlockPos pos, double squaredDistance) { +- return squaredDistance <= 576.0D ? false : (world.getSharedSpawnPos().closerThan((Position) (new Vec3((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D)), 24.0D) ? false : Objects.equals(new ChunkPos(pos), chunk.getPos()) || world.isPositionEntityTicking((BlockPos) pos)); ++ return squaredDistance <= 576.0D ? false : (world.getSharedSpawnPos().closerThan((Position) (new Vec3((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D)), 24.0D) ? false : Objects.equals(new ChunkPos(pos), chunk.getPos()) || world.isPositionEntityTicking((BlockPos) pos)); // Tuinity - diff on change, copy into caller + } + + private static Boolean isValidSpawnPostitionForType(ServerLevel world, MobCategory group, StructureFeatureManager structureAccessor, ChunkGenerator chunkGenerator, MobSpawnSettings.SpawnerData spawnEntry, BlockPos.MutableBlockPos pos, double squaredDistance) { // Paper +diff --git a/src/main/java/net/minecraft/world/level/block/FarmBlock.java b/src/main/java/net/minecraft/world/level/block/FarmBlock.java +index aa1ba8b74ab70b6cede99e4853ac0203f388ab06..a242a80b16c7d074d52a52728646224b1a0091d4 100644 +--- a/src/main/java/net/minecraft/world/level/block/FarmBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/FarmBlock.java +@@ -139,19 +139,28 @@ public class FarmBlock extends Block { + } + + private static boolean isNearWater(LevelReader world, BlockPos pos) { +- Iterator iterator = BlockPos.betweenClosed(pos.offset(-4, 0, -4), pos.offset(4, 1, 4)).iterator(); +- +- BlockPos blockposition1; +- +- do { +- if (!iterator.hasNext()) { +- return false; ++ // Tuinity start - remove abstract block iteration ++ int xOff = pos.getX(); ++ int yOff = pos.getY(); ++ int zOff = pos.getZ(); ++ ++ for (int dz = -4; dz <= 4; ++dz) { ++ int z = dz + zOff; ++ for (int dx = -4; dx <= 4; ++dx) { ++ int x = xOff + dx; ++ for (int dy = 0; dy <= 1; ++dy) { ++ int y = dy + yOff; ++ net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)world.getChunk(x >> 4, z >> 4); ++ net.minecraft.world.level.material.FluidState fluid = chunk.getBlockData(x, y, z).getFluidState(); ++ if (fluid.is(FluidTags.WATER)) { ++ return true; ++ } ++ } + } ++ } + +- blockposition1 = (BlockPos) iterator.next(); +- } while (!world.getFluidState(blockposition1).is((Tag) FluidTags.WATER)); +- +- return true; ++ return false; ++ // Tuinity end - remove abstract block iteration + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +index 1179c62695da4dcf02590c97d8da3c6fcdbee9ef..04d5ef90cd4171f9360017ac0c01ce48ae6ec983 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java ++++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +@@ -610,14 +610,14 @@ public abstract class BlockBehaviour { + + public abstract static class BlockStateBase extends StateHolder { + +- private final int lightEmission; +- private final boolean useShapeForLightOcclusion; ++ private final int lightEmission; public final int getEmittedLight() { return this.lightEmission; } // Tuinity - OBFHELPER ++ private final boolean useShapeForLightOcclusion; public final boolean isTransparentOnSomeFaces() { return this.useShapeForLightOcclusion; } // Tuinity - OBFHELPER + private final boolean isAir; + private final Material material; + private final MaterialColor materialColor; + public final float destroySpeed; + private final boolean requiresCorrectToolForDrops; +- private final boolean canOcclude; ++ private final boolean canOcclude; public final boolean isOpaque() { return this.canOcclude; } // Tuinity - OBFHELPER + private final BlockBehaviour.StatePredicate isRedstoneConductor; + private final BlockBehaviour.StatePredicate isSuffocating; + private final BlockBehaviour.StatePredicate isViewBlocking; +@@ -643,6 +643,7 @@ public abstract class BlockBehaviour { + this.isViewBlocking = blockbase_info.isViewBlocking; + this.hasPostProcess = blockbase_info.hasPostProcess; + this.emissiveRendering = blockbase_info.emissiveRendering; ++ this.conditionallyFullOpaque = this.isOpaque() & this.isTransparentOnSomeFaces(); // Tuinity + } + // Paper start - impl cached craft block data, lazy load to fix issue with loading at the wrong time + private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData; +@@ -658,13 +659,34 @@ public abstract class BlockBehaviour { + protected FluidState fluid; + // Paper end + ++ // Tuinity start ++ protected boolean shapeExceedsCube = true; ++ public final boolean shapeExceedsCube() { ++ return this.shapeExceedsCube; ++ } ++ // Tuinity end ++ // Tuinity start ++ protected int opacityIfCached = -1; ++ // ret -1 if opacity is dynamic, or -1 if the block is conditionally full opaque, else return opacity in [0, 15] ++ public final int getOpacityIfCached() { ++ return this.opacityIfCached; ++ } ++ ++ protected final boolean conditionallyFullOpaque; ++ public final boolean isConditionallyFullOpaque() { ++ return this.conditionallyFullOpaque; ++ } ++ // Tuinity end ++ + public void initCache() { + this.fluid = this.getBlock().getFluidState(this.asState()); // Paper - moved from getFluid() + this.isTicking = this.getBlock().isRandomlyTicking(this.asState()); // Paper - moved from isTicking() + if (!this.getBlock().hasDynamicShape()) { + this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState()); + } +- ++ this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Tuinity - moved from actual method to here ++ this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Tuinity - cache opacity for light ++ // TODO optimise light + } + + public Block getBlock() { +@@ -700,7 +722,7 @@ public abstract class BlockBehaviour { + } + + public final boolean hasLargeCollisionShape() { // Paper +- return this.cache == null || this.cache.largeCollisionShape; ++ return this.shapeExceedsCube; // Tuinity - moved into shape cache init + } + + public final boolean useShapeForLightOcclusion() { // Paper +diff --git a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java +index baf1cb77eb170a44d821eae572d059f18ea46d7e..5d25223cb2f31e78b1608bd2846effba5b4301a4 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java ++++ b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java +@@ -40,11 +40,13 @@ public abstract class StateHolder { + private final ImmutableMap, Comparable> values; + private Table, Comparable, S> neighbours; + protected final MapCodec propertiesCodec; ++ protected final com.tuinity.tuinity.util.table.ZeroCollidingReferenceStateTable optimisedTable; // Tuinity - optimise state lookup + + protected StateHolder(O owner, ImmutableMap, Comparable> entries, MapCodec codec) { + this.owner = owner; + this.values = entries; + this.propertiesCodec = codec; ++ this.optimisedTable = new com.tuinity.tuinity.util.table.ZeroCollidingReferenceStateTable(this, entries); // Tuinity - optimise state lookup + } + + public > S cycle(Property property) { +@@ -85,11 +87,11 @@ public abstract class StateHolder { + } + + public > boolean hasProperty(Property property) { +- return this.values.containsKey(property); ++ return this.optimisedTable.get(property) != null; // Tuinity - optimise state lookup + } + + public > T getValue(Property property) { +- Comparable comparable = this.values.get(property); ++ Comparable comparable = this.optimisedTable.get(property); // Tuinity - optimise state lookup + if (comparable == null) { + throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner); + } else { +@@ -98,24 +100,18 @@ public abstract class StateHolder { + } + + public > Optional getOptionalValue(Property property) { +- Comparable comparable = this.values.get(property); ++ Comparable comparable = this.optimisedTable.get(property); // Tuinity - optimise state lookup + return comparable == null ? Optional.empty() : Optional.of(property.getValueClass().cast(comparable)); + } + + public , V extends T> S setValue(Property property, V value) { +- Comparable comparable = this.values.get(property); +- if (comparable == null) { +- throw new IllegalArgumentException("Cannot set property " + property + " as it does not exist in " + this.owner); +- } else if (comparable == value) { +- return (S)this; +- } else { +- S object = this.neighbours.get(property, value); +- if (object == null) { +- throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); +- } else { +- return object; +- } ++ // Tuinity start - optimise state lookup ++ final S ret = (S)this.optimisedTable.get(property, value); ++ if (ret == null) { ++ throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); + } ++ return ret; ++ // Tuinity end - optimise state lookup + } + + public void populateNeighbours(Map, Comparable>, S> states) { +@@ -134,7 +130,7 @@ public abstract class StateHolder { + } + } + +- this.neighbours = (Table, Comparable, S>)(table.isEmpty() ? table : ArrayTable.create(table)); ++ this.neighbours = (Table, Comparable, S>)(table.isEmpty() ? table : ArrayTable.create(table)); this.optimisedTable.loadInTable((Table)this.neighbours, this.values); // Tuinity - optimise state lookup + } + } + +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java +index ff1a0d125edd2ea10c870cbb62ae9aa23644b6dc..90c5d20d92dd0dba3503c0f8bc16ed533ca59869 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java +@@ -7,6 +7,13 @@ import java.util.Optional; + public class BooleanProperty extends Property { + private final ImmutableSet values = ImmutableSet.of(true, false); + ++ // Tuinity start - optimise iblockdata state lookup ++ @Override ++ public final int getIdFor(final Boolean value) { ++ return value.booleanValue() ? 1 : 0; ++ } ++ // Tuinity end - optimise iblockdata state lookup ++ + protected BooleanProperty(String name) { + super(name, Boolean.class); + } +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java +index bcf8b24e9f9e9870c1a1d27c721a6a433305d55a..32aa07141682ebdd99c2fce9b64c9f283a5d5707 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java +@@ -17,6 +17,15 @@ public class EnumProperty & StringRepresentable> extends Prope + private final ImmutableSet values; + private final Map names = Maps.newHashMap(); + ++ // Tuinity start - optimise iblockdata state lookup ++ private int[] idLookupTable; ++ ++ @Override ++ public final int getIdFor(final T value) { ++ return this.idLookupTable[value.ordinal()]; ++ } ++ // Tuinity end - optimise iblockdata state lookup ++ + protected EnumProperty(String name, Class type, Collection values) { + super(name, type); + this.values = ImmutableSet.copyOf(values); +@@ -31,6 +40,14 @@ public class EnumProperty & StringRepresentable> extends Prope + this.names.put(string, enum_); + } + ++ // Tuinity start - optimise iblockdata state lookup ++ int id = 0; ++ this.idLookupTable = new int[type.getEnumConstants().length]; ++ java.util.Arrays.fill(this.idLookupTable, -1); ++ for (final T value : this.getPossibleValues()) { ++ this.idLookupTable[value.ordinal()] = id++; ++ } ++ // Tuinity end - optimise iblockdata state lookup + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java +index 72f508321ebffcca31240fbdd068b4d185454cbc..346ae8ff58afd1c1f439c150c3d21143b41c3295 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java +@@ -13,6 +13,16 @@ public class IntegerProperty extends Property { + public final int min; + public final int max; + ++ // Tuinity start - optimise iblockdata state lookup ++ @Override ++ public final int getIdFor(final Integer value) { ++ final int val = value.intValue(); ++ final int ret = val - this.min; ++ ++ return ret | ((this.max - ret) >> 31); ++ } ++ // Tuinity end - optimise iblockdata state lookup ++ + protected IntegerProperty(String name, int min, int max) { + super(name, Integer.class); + this.min = min; +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java +index 81b43e0b0146729a8a1c6ade82634c86cde67857..9d5e76877bc06b3318c817c40821a453ac4c4a97 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java +@@ -20,6 +20,17 @@ public abstract class Property> { + }, this::getName); + private final Codec> valueCodec = this.codec.xmap(this::value, Property.Value::value); + ++ // Tuinity start - optimise iblockdata state lookup ++ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger(); ++ private final int id = ID_GENERATOR.getAndIncrement(); ++ ++ public final int getId() { ++ return this.id; ++ } ++ ++ public abstract int getIdFor(final T value); ++ // Tuinity end - optimise state lookup ++ + protected Property(String name, Class type) { + this.clazz = type; + this.name = name; +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +index 63203172a127d812fd59cea0546b67e855ce3ad5..498988b70617f086f047d8d293e525377971e66e 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +@@ -1,5 +1,6 @@ + package net.minecraft.world.level.chunk; + ++import ca.spottedleaf.starlight.light.SWMRNibbleArray; + import it.unimi.dsi.fastutil.shorts.ShortArrayList; + import it.unimi.dsi.fastutil.shorts.ShortList; + import java.util.Collection; +@@ -42,6 +43,36 @@ public interface ChunkAccess extends BlockGetter, FeatureAccess { + } + // Paper end + ++ // Tuinity start ++ default SWMRNibbleArray[] getBlockNibbles() { ++ throw new UnsupportedOperationException(this.getClass().getName()); ++ } ++ default void setBlockNibbles(SWMRNibbleArray[] nibbles) { ++ throw new UnsupportedOperationException(this.getClass().getName()); ++ } ++ ++ default SWMRNibbleArray[] getSkyNibbles() { ++ throw new UnsupportedOperationException(this.getClass().getName()); ++ } ++ default void setSkyNibbles(SWMRNibbleArray[] nibbles) { ++ throw new UnsupportedOperationException(this.getClass().getName()); ++ } ++ public default boolean[] getSkyEmptinessMap() { ++ throw new UnsupportedOperationException(this.getClass().getName()); ++ } ++ public default void setSkyEmptinessMap(final boolean[] emptinessMap) { ++ throw new UnsupportedOperationException(this.getClass().getName()); ++ } ++ ++ public default boolean[] getBlockEmptinessMap() { ++ throw new UnsupportedOperationException(this.getClass().getName()); ++ } ++ ++ public default void setBlockEmptinessMap(final boolean[] emptinessMap) { ++ throw new UnsupportedOperationException(this.getClass().getName()); ++ } ++ // Tuinity end ++ + BlockState getType(final int x, final int y, final int z); // Paper + @Nullable + BlockState setBlockState(BlockPos pos, BlockState state, boolean moved); +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +index 4a40fa5f7c71481ef0b275955bbd81a737889781..a5d7fdd9ec0342e000e467a002846873a10d75fc 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +@@ -196,7 +196,7 @@ public abstract class ChunkGenerator { + // Get origin location (re)defined by event call. + center = new BlockPos(event.getOrigin().getBlockX(), event.getOrigin().getBlockY(), event.getOrigin().getBlockZ()); + // Get world (re)defined by event call. +- world = ((org.bukkit.craftbukkit.CraftWorld) event.getOrigin().getWorld()).getHandle(); ++ //world = ((org.bukkit.craftbukkit.CraftWorld) event.getOrigin().getWorld()).getHandle(); // Tuinity - callers and this function don't expect this to change + // Get radius and whether to find unexplored structures (re)defined by event call. + radius = event.getRadius(); + skipExistingChunks = event.shouldFindUnexplored(); +diff --git a/src/main/java/net/minecraft/world/level/chunk/DataLayer.java b/src/main/java/net/minecraft/world/level/chunk/DataLayer.java +index c4acc7e0035d37167cf5a56f8d90ba17f85cbbe3..2beae8a39987dd9c514e9d369056e65943d7b130 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/DataLayer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/DataLayer.java +@@ -12,7 +12,7 @@ public class DataLayer { + public static final int SIZE = 2048; + private static final int NIBBLE_SIZE = 4; + @Nullable +- protected byte[] data; ++ protected byte[] data; public final byte[] getDataRaw() { return this.data; } // Tuinity - provide accessor + // Paper start + public static final DataLayer EMPTY_NIBBLE_ARRAY = new DataLayer() { + @Override +@@ -62,6 +62,7 @@ public class DataLayer { + boolean poolSafe = false; + public java.lang.Runnable cleaner; + private void registerCleaner() { ++ if (true) return; // Tuinity - purge cleaner usage + if (!poolSafe) { + cleaner = net.minecraft.server.MCUtil.registerCleaner(this, this.data, DataLayer::releaseBytes); + } else { +@@ -76,7 +77,7 @@ public class DataLayer { + } + public DataLayer(byte[] bytes, boolean isSafe) { + this.data = bytes; +- if (!isSafe) this.data = getCloneIfSet(); // Paper - clone for safety ++ // Tuinity - purge cleaner usage + registerCleaner(); + // Paper end + if (bytes.length != 2048) { +@@ -162,7 +163,7 @@ public class DataLayer { + } + // Paper end + public DataLayer copy() { +- return this.data == null ? new DataLayer() : new DataLayer(this.data); // Paper - clone in ctor ++ return this.data == null ? new DataLayer() : new DataLayer(this.data.clone()); // Paper - clone in ctor // Tuinity - no longer clone in constructor + } + + public String toString() { +diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java +index 8245c5834ec69beb8e3b95fb3900601009a9273f..88f30cd8e57ccb69da633daac49f8bc9e44111da 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java +@@ -1,5 +1,6 @@ + package net.minecraft.world.level.chunk; + ++import ca.spottedleaf.starlight.light.SWMRNibbleArray; + import it.unimi.dsi.fastutil.longs.LongSet; + import java.util.BitSet; + import java.util.Map; +@@ -29,6 +30,48 @@ public class ImposterProtoChunk extends ProtoChunk { + this.wrapped = wrapped; + } + ++ // Tuinity start - rewrite light engine ++ @Override ++ public SWMRNibbleArray[] getBlockNibbles() { ++ return this.getWrapped().getBlockNibbles(); ++ } ++ ++ @Override ++ public void setBlockNibbles(SWMRNibbleArray[] nibbles) { ++ this.getWrapped().setBlockNibbles(nibbles); ++ } ++ ++ @Override ++ public SWMRNibbleArray[] getSkyNibbles() { ++ return this.getWrapped().getSkyNibbles(); ++ } ++ ++ @Override ++ public void setSkyNibbles(SWMRNibbleArray[] nibbles) { ++ this.getWrapped().setSkyNibbles(nibbles); ++ } ++ ++ @Override ++ public boolean[] getSkyEmptinessMap() { ++ return this.getWrapped().getSkyEmptinessMap(); ++ } ++ ++ @Override ++ public void setSkyEmptinessMap(boolean[] emptinessMap) { ++ this.getWrapped().setSkyEmptinessMap(emptinessMap); ++ } ++ ++ @Override ++ public boolean[] getBlockEmptinessMap() { ++ return this.getWrapped().getBlockEmptinessMap(); ++ } ++ ++ @Override ++ public void setBlockEmptinessMap(boolean[] emptinessMap) { ++ this.getWrapped().setBlockEmptinessMap(emptinessMap); ++ } ++ // Tuinity end - rewrite light engine ++ + @Nullable + @Override + public BlockEntity getBlockEntity(BlockPos pos) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index cc02b577453fa251f0f1b508281ddea2513138a1..54e23d303aad286ab46c3e5f9b17a5f9922e2942 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -1,5 +1,7 @@ + package net.minecraft.world.level.chunk; + ++import ca.spottedleaf.starlight.light.SWMRNibbleArray; ++import ca.spottedleaf.starlight.light.StarLightEngine; + import com.google.common.collect.ImmutableList; + import com.destroystokyo.paper.exception.ServerInternalException; + import com.google.common.collect.Maps; +@@ -17,7 +19,6 @@ import java.util.Collections; + import java.util.Iterator; + import java.util.Map; + import java.util.Map.Entry; +-import java.util.Objects; + import java.util.Set; + import java.util.function.Consumer; + import java.util.function.Supplier; +@@ -28,7 +29,6 @@ import net.minecraft.CrashReport; + import net.minecraft.CrashReportCategory; + import net.minecraft.ReportedException; + import net.minecraft.core.BlockPos; +-import net.minecraft.core.DefaultedRegistry; + import net.minecraft.core.Registry; + import net.minecraft.core.SectionPos; + import net.minecraft.nbt.CompoundTag; +@@ -125,11 +125,62 @@ public class LevelChunk implements ChunkAccess { + private volatile boolean isLightCorrect; + private final Int2ObjectMap gameEventDispatcherSections; + ++ // Tuinity start - rewrite light engine ++ protected volatile SWMRNibbleArray[] blockNibbles; ++ protected volatile SWMRNibbleArray[] skyNibbles; ++ protected volatile boolean[] skyEmptinessMap; ++ protected volatile boolean[] blockEmptinessMap; ++ ++ @Override ++ public SWMRNibbleArray[] getBlockNibbles() { ++ return this.blockNibbles; ++ } ++ ++ @Override ++ public void setBlockNibbles(SWMRNibbleArray[] nibbles) { ++ this.blockNibbles = nibbles; ++ } ++ ++ @Override ++ public SWMRNibbleArray[] getSkyNibbles() { ++ return this.skyNibbles; ++ } ++ ++ @Override ++ public void setSkyNibbles(SWMRNibbleArray[] nibbles) { ++ this.skyNibbles = nibbles; ++ } ++ ++ @Override ++ public boolean[] getSkyEmptinessMap() { ++ return this.skyEmptinessMap; ++ } ++ ++ @Override ++ public void setSkyEmptinessMap(boolean[] emptinessMap) { ++ this.skyEmptinessMap = emptinessMap; ++ } ++ ++ @Override ++ public boolean[] getBlockEmptinessMap() { ++ return this.blockEmptinessMap; ++ } ++ ++ @Override ++ public void setBlockEmptinessMap(boolean[] emptinessMap) { ++ this.blockEmptinessMap = emptinessMap; ++ } ++ // Tuinity end - rewrite light engine ++ + public LevelChunk(Level world, ChunkPos pos, ChunkBiomeContainer biomes) { + this(world, pos, biomes, UpgradeData.EMPTY, EmptyTickList.empty(), EmptyTickList.empty(), 0L, (LevelChunkSection[]) null, (Consumer) null); + } + + public LevelChunk(Level world, ChunkPos pos, ChunkBiomeContainer biomes, UpgradeData upgradeData, TickList blockTickScheduler, TickList fluidTickScheduler, long inhabitedTime, @Nullable LevelChunkSection[] sections, @Nullable Consumer loadToWorldConsumer) { ++ // Tuinity start ++ this.blockNibbles = StarLightEngine.getFilledEmptyLight(world); ++ this.skyNibbles = StarLightEngine.getFilledEmptyLight(world); ++ // Tuinity end + this.pendingBlockEntities = Maps.newHashMap(); + this.tickersInLevel = Maps.newHashMap(); + this.heightmaps = Maps.newEnumMap(Heightmap.Types.class); +@@ -192,7 +243,7 @@ public class LevelChunk implements ChunkAccess { + return NEIGHBOUR_CACHE_RADIUS; + } + +- boolean loadedTicketLevel; ++ boolean loadedTicketLevel; public final boolean wasLoadCallbackInvoked() { return this.loadedTicketLevel; } // Tuinity - public accessor + private long neighbourChunksLoadedBitset; + private final LevelChunk[] loadedNeighbourChunks = new LevelChunk[(NEIGHBOUR_CACHE_RADIUS * 2 + 1) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)]; + +@@ -242,11 +293,12 @@ public class LevelChunk implements ChunkAccess { + ChunkMap chunkMap = chunkProviderServer.chunkMap; + // this code handles the addition of ticking tickets - the distance map handles the removal + if (!areNeighboursLoaded(bitsetBefore, 2) && areNeighboursLoaded(bitsetAfter, 2)) { +- if (chunkMap.playerViewDistanceTickMap.getObjectsInRange(this.coordinateKey) != null) { ++ if (chunkMap.playerChunkManager.tickMap.getObjectsInRange(this.coordinateKey) != null) { // Tuinity - replace old player chunk loading system + // now we're ready for entity ticking + chunkProviderServer.mainThreadProcessor.execute(() -> { + // double check that this condition still holds. +- if (LevelChunk.this.areNeighboursLoaded(2) && chunkMap.playerViewDistanceTickMap.getObjectsInRange(LevelChunk.this.coordinateKey) != null) { ++ if (LevelChunk.this.areNeighboursLoaded(2) && chunkMap.playerChunkManager.tickMap.getObjectsInRange(LevelChunk.this.coordinateKey) != null) { // Tuinity - replace old player chunk loading system ++ chunkMap.playerChunkManager.onChunkPlayerTickReady(this.chunkPos.x, this.chunkPos.z); // Tuinity - replace old player chunk + chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.PLAYER, LevelChunk.this.chunkPos, 31, LevelChunk.this.chunkPos); // 31 -> entity ticking, TODO check on update + } + }); +@@ -255,31 +307,18 @@ public class LevelChunk implements ChunkAccess { + + // this code handles the chunk sending + if (!areNeighboursLoaded(bitsetBefore, 1) && areNeighboursLoaded(bitsetAfter, 1)) { +- if (chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(this.coordinateKey) != null) { +- // now we're ready to send +- chunkMap.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(chunkMap.getUpdatingChunkIfPresent(this.coordinateKey), (() -> { // Copied frm PlayerChunkMap +- // double check that this condition still holds. +- if (!LevelChunk.this.areNeighboursLoaded(1)) { +- return; +- } +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(LevelChunk.this.coordinateKey); +- if (inRange == null) { +- return; +- } +- +- // broadcast +- Object[] backingSet = inRange.getBackingSet(); +- Packet[] chunkPackets = new Packet[2]; +- for (int index = 0, len = backingSet.length; index < len; ++index) { +- Object temp = backingSet[index]; +- if (!(temp instanceof net.minecraft.server.level.ServerPlayer)) { +- continue; +- } +- net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)temp; +- chunkMap.playerLoadedChunk(player, chunkPackets, LevelChunk.this); +- } +- }))); +- } ++ // Tuinity start - replace old player chunk loading system ++ chunkProviderServer.mainThreadProcessor.execute(() -> { ++ if (!LevelChunk.this.areNeighboursLoaded(1)) { ++ return; ++ } ++ LevelChunk.this.postProcessGeneration(); ++ if (!LevelChunk.this.areNeighboursLoaded(1)) { ++ return; ++ } ++ chunkMap.playerChunkManager.onChunkSendReady(this.chunkPos.x, this.chunkPos.z); ++ }); ++ // Tuinity end - replace old player chunk loading system + } + // Paper end - no-tick view distance + } +@@ -330,9 +369,102 @@ public class LevelChunk implements ChunkAccess { + } + } + // Paper end ++ // Tuinity start - optimise checkDespawn ++ private boolean playerGeneralAreaCacheSet; ++ private com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playerGeneralAreaCache; ++ ++ public com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getPlayerGeneralAreaCache() { ++ if (!this.playerGeneralAreaCacheSet) { ++ this.updateGeneralAreaCache(); ++ } ++ return this.playerGeneralAreaCache; ++ } ++ ++ public void updateGeneralAreaCache() { ++ this.updateGeneralAreaCache(((ServerLevel)this.level).getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(this.coordinateKey)); ++ } ++ ++ public void updateGeneralAreaCache(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet value) { ++ this.playerGeneralAreaCacheSet = true; ++ this.playerGeneralAreaCache = value; ++ } ++ ++ public net.minecraft.server.level.ServerPlayer findNearestPlayer(double sourceX, double sourceY, double sourceZ, ++ double maxRange, java.util.function.Predicate predicate) { ++ if (!this.playerGeneralAreaCacheSet) { ++ this.updateGeneralAreaCache(); ++ } ++ ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby = this.playerGeneralAreaCache; ++ ++ if (nearby == null) { ++ return null; ++ } ++ ++ Object[] backingSet = nearby.getBackingSet(); ++ double closestDistance = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange; ++ net.minecraft.server.level.ServerPlayer closest = null; ++ for (int i = 0, len = backingSet.length; i < len; ++i) { ++ Object _player = backingSet[i]; ++ if (!(_player instanceof net.minecraft.server.level.ServerPlayer)) { ++ continue; ++ } ++ net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)_player; ++ ++ double distance = player.distanceToSqr(sourceX, sourceY, sourceZ); ++ if (distance < closestDistance && predicate.test(player)) { ++ closest = player; ++ closestDistance = distance; ++ } ++ } ++ ++ return closest; ++ } ++ ++ public void getNearestPlayers(double sourceX, double sourceY, double sourceZ, java.util.function.Predicate predicate, ++ double range, java.util.List ret) { ++ if (!this.playerGeneralAreaCacheSet) { ++ this.updateGeneralAreaCache(); ++ } ++ ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby = this.playerGeneralAreaCache; ++ ++ if (nearby == null) { ++ return; ++ } ++ ++ double rangeSquared = range * range; ++ ++ Object[] backingSet = nearby.getBackingSet(); ++ for (int i = 0, len = backingSet.length; i < len; ++i) { ++ Object _player = backingSet[i]; ++ if (!(_player instanceof net.minecraft.server.level.ServerPlayer)) { ++ continue; ++ } ++ net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)_player; ++ ++ if (range >= 0.0) { ++ double distanceSquared = player.distanceToSqr(sourceX, sourceY, sourceZ); ++ if (distanceSquared > rangeSquared) { ++ continue; ++ } ++ } ++ ++ if (predicate == null || predicate.test(player)) { ++ ret.add(player); ++ } ++ } ++ } ++ // Tuinity end - optimise checkDespawn + + public LevelChunk(ServerLevel worldserver, ProtoChunk protoChunk, @Nullable Consumer consumer) { + this(worldserver, protoChunk.getPos(), protoChunk.getBiomes(), protoChunk.getUpgradeData(), protoChunk.getBlockTicks(), protoChunk.getLiquidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), consumer); ++ // Tuinity start - copy over protochunk light ++ this.setBlockNibbles(protoChunk.getBlockNibbles()); ++ this.setSkyNibbles(protoChunk.getSkyNibbles()); ++ this.setSkyEmptinessMap(protoChunk.getSkyEmptinessMap()); ++ this.setBlockEmptinessMap(protoChunk.getBlockEmptinessMap()); ++ // Tuinity end - copy over protochunk light + Iterator iterator = protoChunk.getBlockEntities().values().iterator(); + + while (iterator.hasNext()) { +@@ -788,6 +920,7 @@ public class LevelChunk implements ChunkAccess { + + // CraftBukkit start + public void loadCallback() { ++ if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Tuinity + // Paper start - neighbour cache + int chunkX = this.chunkPos.x; + int chunkZ = this.chunkPos.z; +@@ -807,6 +940,7 @@ public class LevelChunk implements ChunkAccess { + // Paper end - neighbour cache + org.bukkit.Server server = this.level.getCraftServer(); + this.level.getChunkSource().addLoadedChunk(this); // Paper ++ ((ServerLevel)this.level).getChunkSource().chunkMap.playerChunkManager.onChunkLoad(this.chunkPos.x, this.chunkPos.z); // Tuinity - rewrite player chunk management + if (server != null) { + /* + * If it's a new world, the first few chunks are generated inside +@@ -842,6 +976,7 @@ public class LevelChunk implements ChunkAccess { + } + + public void unloadCallback() { ++ if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Tuinity + org.bukkit.Server server = this.level.getCraftServer(); + org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(this.bukkitChunk, this.isUnsaved()); + server.getPluginManager().callEvent(unloadEvent); +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +index cdac1f7b30e4c043dcb12ac9e29af926df8170bd..5d2f76eeb4aef0a5ee8c202c1c682171d4d5b2ea 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +@@ -18,7 +18,8 @@ public class LevelChunkSection { + short nonEmptyBlockCount; // Paper - package-private + private short tickingBlockCount; + private short tickingFluidCount; +- final PalettedContainer states; // Paper - package-private ++ public final PalettedContainer states; // Paper - package-private // Tuinity - public ++ public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper + + // Paper start - Anti-Xray - Add parameters + @Deprecated public LevelChunkSection(int yOffset) { this(yOffset, null, null, true); } // Notice for updates: Please make sure this constructor isn't used anywhere +@@ -79,6 +80,9 @@ public class LevelChunkSection { + --this.nonEmptyBlockCount; + if (blockState.isRandomlyTicking()) { + --this.tickingBlockCount; ++ // Paper start ++ this.tickingList.remove(x, y, z); ++ // Paper end + } + } + +@@ -90,6 +94,9 @@ public class LevelChunkSection { + ++this.nonEmptyBlockCount; + if (state.isRandomlyTicking()) { + ++this.tickingBlockCount; ++ // Paper start ++ this.tickingList.add(x, y, z, state); ++ // Paper end + } + } + +@@ -125,22 +132,28 @@ public class LevelChunkSection { + } + + public void recalcBlockCounts() { ++ // Paper start ++ this.tickingList.clear(); ++ // Paper end + this.nonEmptyBlockCount = 0; + this.tickingBlockCount = 0; + this.tickingFluidCount = 0; +- this.states.count((state, count) -> { ++ this.states.forEachLocation((state, location) -> { // Paper + FluidState fluidState = state.getFluidState(); + if (!state.isAir()) { +- this.nonEmptyBlockCount = (short)(this.nonEmptyBlockCount + count); ++ this.nonEmptyBlockCount = (short)(this.nonEmptyBlockCount + 1); // Paper + if (state.isRandomlyTicking()) { +- this.tickingBlockCount = (short)(this.tickingBlockCount + count); ++ // Paper start ++ this.tickingBlockCount = (short)(this.tickingBlockCount + 1); ++ this.tickingList.add(location, state); ++ // Paper end + } + } + + if (!fluidState.isEmpty()) { +- this.nonEmptyBlockCount = (short)(this.nonEmptyBlockCount + count); ++ this.nonEmptyBlockCount = (short)(this.nonEmptyBlockCount + 1); // Paper + if (fluidState.isRandomlyTicking()) { +- this.tickingFluidCount = (short)(this.tickingFluidCount + count); ++ this.tickingFluidCount = (short)(this.tickingFluidCount + 1); // Paper + } + } + +diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +index 554474d4b2e57d8a005b3c3b9b23f32a62243058..ebeb3e3b0619b034a9681da999e9ac33cc241718 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +@@ -174,7 +174,7 @@ public class PalettedContainer implements PaletteResize { + return this.get(y << 8 | z << 4 | x); // Paper - inline + } + +- protected T get(int index) { ++ public T get(int index) { // Tuinity - public + T object = this.palette.valueFor(this.storage.get(index)); + return (T)(object == null ? this.defaultValue : object); + } +@@ -320,4 +320,12 @@ public class PalettedContainer implements PaletteResize { + public interface CountConsumer { + void accept(T object, int count); + } ++ ++ // Paper start ++ public void forEachLocation(PalettedContainer.CountConsumer datapaletteblock_a) { ++ this.storage.forEach((int location, int data) -> { ++ datapaletteblock_a.accept(this.palette.valueFor(data), location); ++ }); ++ } ++ // Paper end + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java +index 78bd3274866fed3d627a3eda7b96b92716507d38..ccdadf5d7c07d74f5bea94fc21784114b6d520da 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java +@@ -1,5 +1,7 @@ + package net.minecraft.world.level.chunk; + ++import ca.spottedleaf.starlight.light.SWMRNibbleArray; ++import ca.spottedleaf.starlight.light.StarLightEngine; + import com.google.common.collect.Lists; + import com.google.common.collect.Maps; + import com.google.common.collect.Sets; +@@ -65,6 +67,53 @@ public class ProtoChunk implements ChunkAccess { + private volatile boolean isLightCorrect; + final net.minecraft.world.level.Level level; // Paper - Add level + ++ // Tuinity start - rewrite light engine ++ protected volatile SWMRNibbleArray[] blockNibbles; ++ protected volatile SWMRNibbleArray[] skyNibbles; ++ protected volatile boolean[] skyEmptinessMap; ++ protected volatile boolean[] blockEmptinessMap; ++ ++ @Override ++ public SWMRNibbleArray[] getBlockNibbles() { ++ return this.blockNibbles; ++ } ++ ++ @Override ++ public void setBlockNibbles(SWMRNibbleArray[] nibbles) { ++ this.blockNibbles = nibbles; ++ } ++ ++ @Override ++ public SWMRNibbleArray[] getSkyNibbles() { ++ return this.skyNibbles; ++ } ++ ++ @Override ++ public void setSkyNibbles(SWMRNibbleArray[] nibbles) { ++ this.skyNibbles = nibbles; ++ } ++ ++ @Override ++ public boolean[] getSkyEmptinessMap() { ++ return this.skyEmptinessMap; ++ } ++ ++ @Override ++ public void setSkyEmptinessMap(boolean[] emptinessMap) { ++ this.skyEmptinessMap = emptinessMap; ++ } ++ ++ @Override ++ public boolean[] getBlockEmptinessMap() { ++ return this.blockEmptinessMap; ++ } ++ ++ @Override ++ public void setBlockEmptinessMap(boolean[] emptinessMap) { ++ this.blockEmptinessMap = emptinessMap; ++ } ++ // Tuinity end - rewrite light engine ++ + // Paper start - add level + @Deprecated public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor world) { this(pos, upgradeData, world, null); } + public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor world, net.minecraft.server.level.ServerLevel level) { +@@ -79,6 +128,10 @@ public class ProtoChunk implements ChunkAccess { + // Paper start - add level + @Deprecated public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, @Nullable LevelChunkSection[] levelChunkSections, ProtoTickList blockTickScheduler, ProtoTickList fluidTickScheduler, LevelHeightAccessor world) { this(pos, upgradeData, levelChunkSections, blockTickScheduler, fluidTickScheduler, world, null); } + public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, @Nullable LevelChunkSection[] levelChunkSections, ProtoTickList blockTickScheduler, ProtoTickList fluidTickScheduler, LevelHeightAccessor world, net.minecraft.server.level.ServerLevel level) { ++ // Tuinity start ++ this.blockNibbles = StarLightEngine.getFilledEmptyLight(world); ++ this.skyNibbles = StarLightEngine.getFilledEmptyLight(world); ++ // Tuinity end + this.level = level; + // Paper end + this.chunkPos = pos; +@@ -176,7 +229,7 @@ public class ProtoChunk implements ChunkAccess { + + LevelChunkSection levelChunkSection = this.getOrCreateSection(l); + BlockState blockState = levelChunkSection.setBlockState(i & 15, j & 15, k & 15, state); +- if (this.status.isOrAfter(ChunkStatus.FEATURES) && state != blockState && (state.getLightBlock(this, pos) != blockState.getLightBlock(this, pos) || state.getLightEmission() != blockState.getLightEmission() || state.useShapeForLightOcclusion() || blockState.useShapeForLightOcclusion())) { ++ if (this.status.isOrAfter(ChunkStatus.LIGHT) && state != blockState && (state.getLightBlock(this, pos) != blockState.getLightBlock(this, pos) || state.getLightEmission() != blockState.getLightEmission() || state.useShapeForLightOcclusion() || blockState.useShapeForLightOcclusion())) { // Tuinity - move block updates to only happen after lighting occurs (or during, thanks chunk system) + this.lightEngine.checkBlock(pos); + } + +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +index 9ee2321150d3de7bf1a11c212951bc69872f4dd8..9815a91040e7cbcbcd171e68b0a935ea26ff8a2a 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +@@ -65,6 +65,21 @@ import org.apache.logging.log4j.Logger; + + public class ChunkSerializer { + ++ // Tuinity start - replace light engine impl ++ private static final int STARLIGHT_LIGHT_VERSION = 5; ++ ++ private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state"; ++ private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; ++ private static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; ++ // Tuinity end - replace light engine impl ++ // Tuinity start ++ // TODO: Check on update ++ public static long getLastWorldSaveTime(CompoundTag chunkData) { ++ CompoundTag levelData = chunkData.getCompound("Level"); ++ return levelData.getLong("LastUpdate"); ++ } ++ // Tuinity end ++ + private static final Logger LOGGER = LogManager.getLogger(); + public static final String TAG_UPGRADE_DATA = "UpgradeData"; + +@@ -118,7 +133,7 @@ public class ChunkSerializer { + } + // Paper end + BiomeSource worldchunkmanager = chunkgenerator.getBiomeSource(); +- CompoundTag nbttagcompound1 = nbt.getCompound("Level"); // Paper - diff on change, see ChunkSerializer#getChunkCoordinate ++ CompoundTag nbttagcompound1 = nbt.getCompound("Level"); // Paper - diff on change, see ChunkSerializer#getChunkCoordinate // Tuinity - diff on change + ChunkPos chunkcoordintpair1 = new ChunkPos(nbttagcompound1.getInt("xPos"), nbttagcompound1.getInt("zPos")); // Paper - diff on change, see ChunkSerializer#getChunkCoordinate + + if (!Objects.equals(pos, chunkcoordintpair1)) { +@@ -133,13 +148,20 @@ public class ChunkSerializer { + ProtoTickList protochunkticklist1 = new ProtoTickList<>((fluidtype) -> { + return fluidtype == null || fluidtype == Fluids.EMPTY; + }, pos, nbttagcompound1.getList("LiquidsToBeTicked", 9), world); +- boolean flag = nbttagcompound1.getBoolean("isLightOn"); ++ boolean flag = getStatus(nbt).isOrAfter(ChunkStatus.LIGHT) && nbttagcompound1.get("isLightOn") != null && nbttagcompound1.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; // Tuinity + ListTag nbttaglist = nbttagcompound1.getList("Sections", 10); + int i = world.getSectionsCount(); + LevelChunkSection[] achunksection = new LevelChunkSection[i]; + boolean flag1 = world.dimensionType().hasSkyLight(); + ServerChunkCache chunkproviderserver = world.getChunkSource(); + LevelLightEngine lightengine = chunkproviderserver.getLightEngine(); ++ // Tuinity start ++ ca.spottedleaf.starlight.light.SWMRNibbleArray[] blockNibbles = ca.spottedleaf.starlight.light.StarLightEngine.getFilledEmptyLight(world); // Tuinity - replace light impl ++ ca.spottedleaf.starlight.light.SWMRNibbleArray[] skyNibbles = ca.spottedleaf.starlight.light.StarLightEngine.getFilledEmptyLight(world); // Tuinity - replace light impl ++ final int minSection = com.tuinity.tuinity.util.WorldUtil.getMinLightSection(world); ++ final int maxSection = com.tuinity.tuinity.util.WorldUtil.getMaxLightSection(world); ++ boolean canReadSky = world.dimensionType().hasSkyLight(); ++ // Tuinity end + + if (flag) { + tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main +@@ -148,7 +170,7 @@ public class ChunkSerializer { + } + + for (int j = 0; j < nbttaglist.size(); ++j) { +- CompoundTag nbttagcompound2 = nbttaglist.getCompound(j); ++ CompoundTag nbttagcompound2 = nbttaglist.getCompound(j); CompoundTag sectionData = nbttagcompound2; // Tuinity + byte b0 = nbttagcompound2.getByte("Y"); + + if (nbttagcompound2.contains("Palette", 9) && nbttagcompound2.contains("BlockStates", 12)) { +@@ -166,23 +188,29 @@ public class ChunkSerializer { + } + + if (flag) { +- if (nbttagcompound2.contains("BlockLight", 7)) { +- // Paper start - delay this task since we're executing off-main +- DataLayer blockLight = new DataLayer(nbttagcompound2.getByteArray("BlockLight")); +- tasksToExecuteOnMain.add(() -> { +- lightengine.queueSectionData(LightLayer.BLOCK, SectionPos.of(chunkcoordintpair1, b0), blockLight, true); +- }); +- // Paper end - delay this task since we're executing off-main ++ // Tuinity start - rewrite light engine ++ int y = sectionData.getByte("Y"); ++ ++ if (sectionData.contains("BlockLight", 7)) { ++ // this is where our diff is ++ blockNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety ++ } else { ++ blockNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG)); + } + +- if (flag1 && nbttagcompound2.contains("SkyLight", 7)) { +- // Paper start - delay this task since we're executing off-main +- DataLayer skyLight = new DataLayer(nbttagcompound2.getByteArray("SkyLight")); +- tasksToExecuteOnMain.add(() -> { +- lightengine.queueSectionData(LightLayer.SKY, SectionPos.of(chunkcoordintpair1, b0), skyLight, true); +- }); +- // Paper end - delay this task since we're executing off-main ++ if (canReadSky) { ++ if (sectionData.contains("SkyLight", 7)) { ++ // we store under the same key so mod programs editing nbt ++ // can still read the data, hopefully. ++ // however, for compatibility we store chunks as unlit so vanilla ++ // is forced to re-light them if it encounters our data. It's too much of a burden ++ // to try and maintain compatibility with a broken and inferior skylight management system. ++ skyNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety ++ } else { ++ skyNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG)); ++ } + } ++ // Tuinity end - rewrite light engine + } + } + +@@ -226,8 +254,12 @@ public class ChunkSerializer { + object = new LevelChunk(world.getLevel(), pos, biomestorage, chunkconverter, (TickList) object1, (TickList) object2, k, achunksection, // Paper start - fix massive nbt memory leak due to lambda. move lambda into a container method to not leak scope. Only clone needed NBT keys. + createLoadEntitiesConsumer(new SafeNBTCopy(nbttagcompound1, "TileEntities", "Entities", "ChunkBukkitValues")) // Paper - move CB Chunk PDC into here + );// Paper end ++ ((LevelChunk)object).setBlockNibbles(blockNibbles); // Tuinity - replace light impl ++ ((LevelChunk)object).setSkyNibbles(skyNibbles); // Tuinity - replace light impl + } else { + ProtoChunk protochunk = new ProtoChunk(pos, chunkconverter, achunksection, protochunkticklist, protochunkticklist1, world, world); // Paper - add level ++ protochunk.setBlockNibbles(blockNibbles); // Tuinity - replace light impl ++ protochunk.setSkyNibbles(skyNibbles); // Tuinity - replace light impl + + protochunk.setBiomes(biomestorage); + object = protochunk; +@@ -408,7 +440,7 @@ public class ChunkSerializer { + DataLayer[] blockLight = new DataLayer[lightenginethreaded.getMaxLightSection() - lightenginethreaded.getMinLightSection()]; + DataLayer[] skyLight = new DataLayer[lightenginethreaded.getMaxLightSection() - lightenginethreaded.getMinLightSection()]; + +- for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) { ++ for (int i = lightenginethreaded.getMinLightSection(); false && i < lightenginethreaded.getMaxLightSection(); ++i) { // Tuinity - don't run loop, we don't need to - light data is per chunk now + DataLayer blockArray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkPos, i)); + DataLayer skyArray = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkPos, i)); + +@@ -457,6 +489,12 @@ public class ChunkSerializer { + return saveChunk(world, chunk, null); + } + public static CompoundTag saveChunk(ServerLevel world, ChunkAccess chunk, AsyncSaveData asyncsavedata) { ++ // Tuinity start - rewrite light impl ++ final int minSection = com.tuinity.tuinity.util.WorldUtil.getMinLightSection(world); ++ final int maxSection = com.tuinity.tuinity.util.WorldUtil.getMaxLightSection(world); ++ ca.spottedleaf.starlight.light.SWMRNibbleArray[] blockNibbles = chunk.getBlockNibbles(); ++ ca.spottedleaf.starlight.light.SWMRNibbleArray[] skyNibbles = chunk.getSkyNibbles(); ++ // Tuinity end - rewrite light impl + // Paper end + ChunkPos chunkcoordintpair = chunk.getPos(); + CompoundTag nbttagcompound = new CompoundTag(); +@@ -466,7 +504,7 @@ public class ChunkSerializer { + nbttagcompound.put("Level", nbttagcompound1); + nbttagcompound1.putInt("xPos", chunkcoordintpair.x); + nbttagcompound1.putInt("zPos", chunkcoordintpair.z); +- nbttagcompound1.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading ++ nbttagcompound1.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading // Tuinity - diff on change + nbttagcompound1.putLong("InhabitedTime", chunk.getInhabitedTime()); + nbttagcompound1.putString("Status", chunk.getStatus().getName()); + UpgradeData chunkconverter = chunk.getUpgradeData(); +@@ -485,32 +523,33 @@ public class ChunkSerializer { + LevelChunkSection chunksection = (LevelChunkSection) Arrays.stream(achunksection).filter((chunksection1) -> { + return chunksection1 != null && SectionPos.blockToSectionCoord(chunksection1.bottomBlockY()) == finalI; // CraftBukkit - decompile errors + }).findFirst().orElse(LevelChunk.EMPTY_SECTION); +- // Paper start - async chunk save for unload +- DataLayer nibblearray; // block light +- DataLayer nibblearray1; // sky light +- if (asyncsavedata == null) { +- nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); /// Paper - diff on method change (see getAsyncSaveData) +- nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); // Paper - diff on method change (see getAsyncSaveData) +- } else { +- nibblearray = asyncsavedata.blockLight[i - lightenginethreaded.getMinLightSection()]; +- nibblearray1 = asyncsavedata.skyLight[i - lightenginethreaded.getMinLightSection()]; +- } +- // Paper end +- if (chunksection != LevelChunk.EMPTY_SECTION || nibblearray != null || nibblearray1 != null) { +- CompoundTag nbttagcompound2 = new CompoundTag(); ++ // Tuinity start - replace light engine ++ ca.spottedleaf.starlight.light.SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState(); ++ ca.spottedleaf.starlight.light.SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState(); ++ if (chunksection != LevelChunk.EMPTY_SECTION || blockNibble != null || skyNibble != null) { ++ // Tuinity end - replace light engine ++ CompoundTag nbttagcompound2 = new CompoundTag(); CompoundTag section = nbttagcompound2; // Tuinity + + nbttagcompound2.putByte("Y", (byte) (i & 255)); + if (chunksection != LevelChunk.EMPTY_SECTION) { + chunksection.getStates().write(nbttagcompound2, "Palette", "BlockStates"); + } + +- if (nibblearray != null && !nibblearray.isEmpty()) { +- nbttagcompound2.putByteArray("BlockLight", nibblearray.asBytesPoolSafe().clone()); // Paper ++ // Tuinity start - replace light engine ++ if (blockNibble != null) { ++ if (blockNibble.data != null) { ++ section.putByteArray("BlockLight", blockNibble.data); ++ } ++ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state); + } + +- if (nibblearray1 != null && !nibblearray1.isEmpty()) { +- nbttagcompound2.putByteArray("SkyLight", nibblearray1.asBytesPoolSafe().clone()); // Paper ++ if (skyNibble != null) { ++ if (skyNibble.data != null) { ++ section.putByteArray("SkyLight", skyNibble.data); ++ } ++ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state); + } ++ // Tuinity end - replace light engine + + nbttaglist.add(nbttagcompound2); + } +@@ -518,7 +557,8 @@ public class ChunkSerializer { + + nbttagcompound1.put("Sections", nbttaglist); + if (flag) { +- nbttagcompound1.putBoolean("isLightOn", true); ++ nbttagcompound1.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // Tuinity ++ nbttagcompound1.putBoolean("isLightOn", false); // Tuinity - set to false but still store, this allows us to detect --eraseCache (as eraseCache _removes_) + } + + ChunkBiomeContainer biomestorage = chunk.getBiomes(); +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java +index 176610b31f66b890afe61f4de46c412382bb8d22..70ec2feef1553afca2c8cca3a7f19498637b41d5 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java +@@ -34,12 +34,13 @@ public class ChunkStorage implements AutoCloseable { + this.fixerUpper = dataFixer; + // Paper start - async chunk io + // remove IO worker +- this.regionFileCache = new RegionFileStorage(directory, dsync); // Paper - nuke IOWorker ++ this.regionFileCache = new RegionFileStorage(directory, dsync, true); // Paper - nuke IOWorker // Tuinity + // Paper end - async chunk io + } + + // CraftBukkit start + private boolean check(ServerChunkCache cps, int x, int z) throws IOException { ++ if (true) return true; // Tuinity - this isn't even needed anymore, light is purged updating to 1.14+, why are we holding up the conversion process reading chunk data off disk - return true, we need to set light populated to true so the converter recognizes the chunk as being "full" + ChunkPos pos = new ChunkPos(x, z); + if (cps != null) { + //com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread"); // Paper - this function is now MT-Safe +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java +index c8298a597818227de33a4afce4698ec0666cf758..b49b0c4cac8aec09ffe970c92e5a75047c0e1f1d 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java +@@ -9,6 +9,27 @@ import java.util.BitSet; + public class RegionBitmap { + private final BitSet used = new BitSet(); + ++ // Tuinity start ++ public final void copyFrom(RegionBitmap other) { ++ BitSet thisBitset = this.used; ++ BitSet otherBitset = other.used; ++ ++ for (int i = 0; i < Math.max(thisBitset.size(), otherBitset.size()); ++i) { ++ thisBitset.set(i, otherBitset.get(i)); ++ } ++ } ++ ++ public final boolean tryAllocate(int from, int length) { ++ BitSet bitset = this.used; ++ int firstSet = bitset.nextSetBit(from); ++ if (firstSet > 0 && firstSet < (from + length)) { ++ return false; ++ } ++ bitset.set(from, from + length); ++ return true; ++ } ++ // Tuinity end ++ + public void force(int start, int size) { + this.used.set(start, start + size); + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +index c22391a0d4b7db49bd3994b0887939a7d8019391..118adb6fbdc56ca03652f114c1b7ced0ef26a628 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +@@ -55,6 +55,341 @@ public class RegionFile implements AutoCloseable { + public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper + public final File regionFile; // Paper + ++ // Tuinity start - try to recover from RegionFile header corruption ++ private static long roundToSectors(long bytes) { ++ long sectors = bytes >>> 12; // 4096 = 2^12 ++ long remainingBytes = bytes & 4095; ++ long sign = -remainingBytes; // sign is 1 if nonzero ++ return sectors + (sign >>> 63); ++ } ++ ++ private static final CompoundTag OVERSIZED_COMPOUND = new CompoundTag(); ++ ++ private CompoundTag attemptRead(long sector, int chunkDataLength, long fileLength) throws IOException { ++ try { ++ if (chunkDataLength < 0) { ++ return null; ++ } ++ ++ long offset = sector * 4096L + 4L; // offset for chunk data ++ ++ if ((offset + chunkDataLength) > fileLength) { ++ return null; ++ } ++ ++ ByteBuffer chunkData = ByteBuffer.allocate(chunkDataLength); ++ if (chunkDataLength != this.file.read(chunkData, offset)) { ++ return null; ++ } ++ ++ ((java.nio.Buffer)chunkData).flip(); ++ ++ byte compressionType = chunkData.get(); ++ if (compressionType < 0) { // compressionType & 128 != 0 ++ // oversized chunk ++ return OVERSIZED_COMPOUND; ++ } ++ ++ RegionFileVersion compression = RegionFileVersion.fromId(compressionType); ++ if (compression == null) { ++ return null; ++ } ++ ++ InputStream input = compression.wrap(new ByteArrayInputStream(chunkData.array(), chunkData.position(), chunkDataLength - chunkData.position())); ++ ++ return NbtIo.read((java.io.DataInput)new DataInputStream(new BufferedInputStream(input))); ++ } catch (Exception ex) { ++ return null; ++ } ++ } ++ ++ private int getLength(long sector) throws IOException { ++ ByteBuffer length = ByteBuffer.allocate(4); ++ if (4 != this.file.read(length, sector * 4096L)) { ++ return -1; ++ } ++ ++ return length.getInt(0); ++ } ++ ++ private void backupRegionFile() { ++ File backup = new File(this.regionFile.getParent(), this.regionFile.getName() + "." + new java.util.Random().nextLong() + ".backup"); ++ this.backupRegionFile(backup); ++ } ++ ++ private void backupRegionFile(File to) { ++ try { ++ this.file.force(true); ++ LOGGER.warn("Backing up regionfile \"" + this.regionFile.getAbsolutePath() + "\" to " + to.getAbsolutePath()); ++ java.nio.file.Files.copy(this.regionFile.toPath(), to.toPath()); ++ LOGGER.warn("Backed up the regionfile to " + to.getAbsolutePath()); ++ } catch (IOException ex) { ++ LOGGER.error("Failed to backup to " + to.getAbsolutePath(), ex); ++ } ++ } ++ ++ // note: only call for CHUNK regionfiles ++ void recalculateHeader() throws IOException { ++ if (!this.canRecalcHeader) { ++ return; ++ } ++ synchronized (this) { ++ LOGGER.warn("Corrupt regionfile header detected! Attempting to re-calculate header offsets for regionfile " + this.regionFile.getAbsolutePath(), new Throwable()); ++ ++ // try to backup file so maybe it could be sent to us for further investigation ++ ++ this.backupRegionFile(); ++ CompoundTag[] compounds = new CompoundTag[32 * 32]; // only in the regionfile (i.e exclude mojang/aikar oversized data) ++ int[] rawLengths = new int[32 * 32]; // length of chunk data including 4 byte length field, bytes ++ int[] sectorOffsets = new int[32 * 32]; // in sectors ++ boolean[] hasAikarOversized = new boolean[32 * 32]; ++ ++ long fileLength = this.file.size(); ++ long totalSectors = roundToSectors(fileLength); ++ ++ // search the regionfile from start to finish for the most up-to-date chunk data ++ ++ for (long i = 2, maxSector = Math.min((long)(Integer.MAX_VALUE >>> 8), totalSectors); i < maxSector; ++i) { // first two sectors are header, skip ++ int chunkDataLength = this.getLength(i); ++ CompoundTag compound = this.attemptRead(i, chunkDataLength, fileLength); ++ if (compound == null || compound == OVERSIZED_COMPOUND) { ++ continue; ++ } ++ ++ ChunkPos chunkPos = ChunkSerializer.getChunkCoordinate(compound); ++ int location = (chunkPos.x & 31) | ((chunkPos.z & 31) << 5); ++ ++ CompoundTag otherCompound = compounds[location]; ++ ++ if (otherCompound != null && ChunkSerializer.getLastWorldSaveTime(otherCompound) > ChunkSerializer.getLastWorldSaveTime(compound)) { ++ continue; // don't overwrite newer data. ++ } ++ ++ // aikar oversized? ++ File aikarOversizedFile = this.getOversizedFile(chunkPos.x, chunkPos.z); ++ boolean isAikarOversized = false; ++ if (aikarOversizedFile.exists()) { ++ try { ++ CompoundTag aikarOversizedCompound = this.getOversizedData(chunkPos.x, chunkPos.z); ++ if (ChunkSerializer.getLastWorldSaveTime(compound) == ChunkSerializer.getLastWorldSaveTime(aikarOversizedCompound)) { ++ // best we got for an id. hope it's good enough ++ isAikarOversized = true; ++ } ++ } catch (Exception ex) { ++ LOGGER.error("Failed to read aikar oversized data for absolute chunk (" + chunkPos.x + "," + chunkPos.z + ") in regionfile " + this.regionFile.getAbsolutePath() + ", oversized data for this chunk will be lost", ex); ++ // fall through, if we can't read aikar oversized we can't risk corrupting chunk data ++ } ++ } ++ ++ hasAikarOversized[location] = isAikarOversized; ++ compounds[location] = compound; ++ rawLengths[location] = chunkDataLength + 4; ++ sectorOffsets[location] = (int)i; ++ ++ int chunkSectorLength = (int)roundToSectors(rawLengths[location]); ++ i += chunkSectorLength; ++ --i; // gets incremented next iteration ++ } ++ ++ // forge style oversized data is already handled by the local search, and aikar data we just hope ++ // we get it right as aikar data has no identifiers we could use to try and find its corresponding ++ // local data compound ++ ++ java.nio.file.Path containingFolder = this.externalFileDir; ++ File[] regionFiles = containingFolder.toFile().listFiles(); ++ boolean[] oversized = new boolean[32 * 32]; ++ RegionFileVersion[] oversizedCompressionTypes = new RegionFileVersion[32 * 32]; ++ ++ if (regionFiles != null) { ++ ChunkPos ourLowerLeftPosition = RegionFileStorage.getRegionFileCoordinates(this.regionFile); ++ ++ if (ourLowerLeftPosition == null) { ++ LOGGER.fatal("Unable to get chunk location of regionfile " + this.regionFile.getAbsolutePath() + ", cannot recover oversized chunks"); ++ } else { ++ int lowerXBound = ourLowerLeftPosition.x; // inclusive ++ int lowerZBound = ourLowerLeftPosition.z; // inclusive ++ int upperXBound = lowerXBound + 32 - 1; // inclusive ++ int upperZBound = lowerZBound + 32 - 1; // inclusive ++ ++ // read mojang oversized data ++ for (File regionFile : regionFiles) { ++ ChunkPos oversizedCoords = getOversizedChunkPair(regionFile); ++ if (oversizedCoords == null) { ++ continue; ++ } ++ ++ if ((oversizedCoords.x < lowerXBound || oversizedCoords.x > upperXBound) || (oversizedCoords.z < lowerZBound || oversizedCoords.z > upperZBound)) { ++ continue; // not in our regionfile ++ } ++ ++ // ensure oversized data is valid & is newer than data in the regionfile ++ ++ int location = (oversizedCoords.x & 31) | ((oversizedCoords.z & 31) << 5); ++ ++ byte[] chunkData; ++ try { ++ chunkData = Files.readAllBytes(regionFile.toPath()); ++ } catch (Exception ex) { ++ LOGGER.error("Failed to read oversized chunk data in file " + regionFile.getAbsolutePath() + ", data will be lost", ex); ++ continue; ++ } ++ ++ CompoundTag compound = null; ++ ++ // We do not know the compression type, as it's stored in the regionfile. So we need to try all of them ++ RegionFileVersion compression = null; ++ for (RegionFileVersion compressionType : RegionFileVersion.VERSIONS.values()) { ++ try { ++ DataInputStream in = new DataInputStream(new BufferedInputStream(compressionType.wrap(new ByteArrayInputStream(chunkData)))); // typical java ++ compound = NbtIo.read((java.io.DataInput)in); ++ compression = compressionType; ++ break; // reaches here iff readNBT does not throw ++ } catch (Exception ex) { ++ continue; ++ } ++ } ++ ++ if (compound == null) { ++ LOGGER.error("Failed to read oversized chunk data in file " + regionFile.getAbsolutePath() + ", it's corrupt. Its data will be lost"); ++ continue; ++ } ++ ++ if (compounds[location] == null || ChunkSerializer.getLastWorldSaveTime(compound) > ChunkSerializer.getLastWorldSaveTime(compounds[location])) { ++ oversized[location] = true; ++ oversizedCompressionTypes[location] = compression; ++ } ++ } ++ } ++ } ++ ++ // now we need to calculate a new offset header ++ ++ int[] calculatedOffsets = new int[32 * 32]; ++ RegionBitmap newSectorAllocations = new RegionBitmap(); ++ newSectorAllocations.force(0, 2); // make space for header ++ ++ // allocate sectors for normal chunks ++ ++ for (int chunkX = 0; chunkX < 32; ++chunkX) { ++ for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { ++ int location = chunkX | (chunkZ << 5); ++ ++ if (oversized[location]) { ++ continue; ++ } ++ ++ int rawLength = rawLengths[location]; // bytes ++ int sectorOffset = sectorOffsets[location]; // sectors ++ int sectorLength = (int)roundToSectors(rawLength); ++ ++ if (newSectorAllocations.tryAllocate(sectorOffset, sectorLength)) { ++ calculatedOffsets[location] = sectorOffset << 8 | (sectorLength > 255 ? 255 : sectorLength); // support forge style oversized ++ } else { ++ LOGGER.error("Failed to allocate space for local chunk (overlapping data??) at (" + chunkX + "," + chunkZ + ") in regionfile " + this.regionFile.getAbsolutePath() + ", chunk will be regenerated"); ++ } ++ } ++ } ++ ++ // allocate sectors for oversized chunks ++ ++ for (int chunkX = 0; chunkX < 32; ++chunkX) { ++ for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { ++ int location = chunkX | (chunkZ << 5); ++ ++ if (!oversized[location]) { ++ continue; ++ } ++ ++ int sectorOffset = newSectorAllocations.allocate(1); ++ int sectorLength = 1; ++ ++ try { ++ this.file.write(this.createExternalStub(oversizedCompressionTypes[location]), sectorOffset * 4096); ++ // only allocate in the new offsets if the write succeeds ++ calculatedOffsets[location] = sectorOffset << 8 | (sectorLength > 255 ? 255 : sectorLength); // support forge style oversized ++ } catch (IOException ex) { ++ newSectorAllocations.free(sectorOffset, sectorLength); ++ LOGGER.error("Failed to write new oversized chunk data holder, local chunk at (" + chunkX + "," + chunkZ + ") in regionfile " + this.regionFile.getAbsolutePath() + " will be regenerated"); ++ } ++ } ++ } ++ ++ // rewrite aikar oversized data ++ ++ this.oversizedCount = 0; ++ for (int chunkX = 0; chunkX < 32; ++chunkX) { ++ for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { ++ int location = chunkX | (chunkZ << 5); ++ int isAikarOversized = hasAikarOversized[location] ? 1 : 0; ++ ++ this.oversizedCount += isAikarOversized; ++ this.oversized[location] = (byte)isAikarOversized; ++ } ++ } ++ ++ if (this.oversizedCount > 0) { ++ try { ++ this.writeOversizedMeta(); ++ } catch (Exception ex) { ++ LOGGER.error("Failed to write aikar oversized chunk meta, all aikar style oversized chunk data will be lost for regionfile " + this.regionFile.getAbsolutePath(), ex); ++ this.getOversizedMetaFile().delete(); ++ } ++ } else { ++ this.getOversizedMetaFile().delete(); ++ } ++ ++ this.usedSectors.copyFrom(newSectorAllocations); ++ ++ // before we overwrite the old sectors, print a summary of the chunks that got changed. ++ ++ LOGGER.info("Starting summary of changes for regionfile " + this.regionFile.getAbsolutePath()); ++ ++ for (int chunkX = 0; chunkX < 32; ++chunkX) { ++ for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { ++ int location = chunkX | (chunkZ << 5); ++ ++ int oldOffset = this.offsets.get(location); ++ int newOffset = calculatedOffsets[location]; ++ ++ if (oldOffset == newOffset) { ++ continue; ++ } ++ ++ this.offsets.put(location, newOffset); // overwrite incorrect offset ++ ++ if (oldOffset == 0) { ++ // found lost data ++ LOGGER.info("Found missing data for local chunk (" + chunkX + "," + chunkZ + ") in regionfile " + this.regionFile.getAbsolutePath()); ++ } else if (newOffset == 0) { ++ LOGGER.warn("Data for local chunk (" + chunkX + "," + chunkZ + ") could not be recovered in regionfile " + this.regionFile.getAbsolutePath() + ", it will be regenerated"); ++ } else { ++ LOGGER.info("Local chunk (" + chunkX + "," + chunkZ + ") changed to point to newer data or correct chunk in regionfile " + this.regionFile.getAbsolutePath()); ++ } ++ } ++ } ++ ++ LOGGER.info("End of change summary for regionfile " + this.regionFile.getAbsolutePath()); ++ ++ // simply destroy the timestamp header, it's not used ++ ++ for (int i = 0; i < 32 * 32; ++i) { ++ this.timestamps.put(i, calculatedOffsets[i] != 0 ? (int)System.currentTimeMillis() : 0); // write a valid timestamp for valid chunks, I do not want to find out whatever dumb program actually checks this ++ } ++ ++ // write new header ++ try { ++ this.flush(); ++ this.file.force(true); // try to ensure it goes through... ++ LOGGER.info("Successfully wrote new header to disk for regionfile " + this.regionFile.getAbsolutePath()); ++ } catch (IOException ex) { ++ LOGGER.fatal("Failed to write new header to disk for regionfile " + this.regionFile.getAbsolutePath(), ex); ++ } ++ } ++ } ++ ++ final boolean canRecalcHeader; // final forces compile fail on new constructor ++ // Tuinity end ++ + // Paper start - Cache chunk status + private final ChunkStatus[] statuses = new ChunkStatus[32 * 32]; + +@@ -82,8 +417,19 @@ public class RegionFile implements AutoCloseable { + public RegionFile(File file, File directory, boolean dsync) throws IOException { + this(file.toPath(), directory.toPath(), RegionFileVersion.VERSION_DEFLATE, dsync); + } ++ // Tuinity start - add can recalc flag ++ public RegionFile(File file, File directory, boolean dsync, boolean canRecalcHeader) throws IOException { ++ this(file.toPath(), directory.toPath(), RegionFileVersion.VERSION_DEFLATE, dsync, canRecalcHeader); ++ } ++ // Tuinity end - add can recalc flag + + public RegionFile(Path file, Path directory, RegionFileVersion outputChunkStreamVersion, boolean dsync) throws IOException { ++ // Tuinity start - add can recalc flag ++ this(file, directory, outputChunkStreamVersion, dsync, false); ++ } ++ public RegionFile(Path file, Path directory, RegionFileVersion outputChunkStreamVersion, boolean dsync, boolean canRecalcHeader) throws IOException { ++ this.canRecalcHeader = canRecalcHeader; ++ // Tuinity end - add can recalc flag + this.header = ByteBuffer.allocateDirect(8192); + this.regionFile = file.toFile(); // Paper + initOversizedState(); // Paper +@@ -112,14 +458,16 @@ public class RegionFile implements AutoCloseable { + RegionFile.LOGGER.warn("Region file {} has truncated header: {}", file, i); + } + +- long j = Files.size(file); ++ final long j = Files.size(file); final long regionFileSize = j; // Tuinity - recalculate header on header corruption + ++ boolean needsHeaderRecalc = false; // Tuinity - recalculate header on header corruption ++ boolean hasBackedUp = false; // Tuinity - recalculate header on header corruption + for (int k = 0; k < 1024; ++k) { +- int l = this.offsets.get(k); ++ final int l = this.offsets.get(k); final int headerLocation = l; // Tuinity - we expect this to be the header location + + if (l != 0) { +- int i1 = RegionFile.getSectorNumber(l); +- int j1 = RegionFile.getNumSectors(l); ++ final int i1 = RegionFile.getSectorNumber(l); final int offset = i1; // Tuinity - we expect this to be offset in file in sectors ++ int j1 = RegionFile.getNumSectors(l); final int sectorLength; // Tuinity - diff on change, we expect this to be sector length of region - watch out for reassignments + // Spigot start + if (j1 == 255) { + // We're maxed out, so we need to read the proper length from the section +@@ -128,32 +476,102 @@ public class RegionFile implements AutoCloseable { + j1 = (realLen.getInt(0) + 4) / 4096 + 1; + } + // Spigot end ++ sectorLength = j1; // Tuinity - diff on change, we expect this to be sector length of region + + if (i1 < 2) { + RegionFile.LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", file, k, i1); +- this.offsets.put(k, 0); ++ //this.offsets.put(k, 0); // Tuinity - we catch this, but need it in the header for the summary change + } else if (j1 == 0) { + RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; size has to be > 0", file, k); +- this.offsets.put(k, 0); ++ //this.offsets.put(k, 0); // Tuinity - we catch this, but need it in the header for the summary change + } else if ((long) i1 * 4096L > j) { + RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; sector {} is out of bounds", file, k, i1); +- this.offsets.put(k, 0); ++ //this.offsets.put(k, 0); // Tuinity - we catch this, but need it in the header for the summary change + } else { +- this.usedSectors.force(i1, j1); ++ //this.usedSectors.force(i1, j1); // Tuinity - move this down so we can check if it fails to allocate ++ } ++ // Tuinity start - recalculate header on header corruption ++ if (offset < 2 || sectorLength <= 0 || ((long)offset * 4096L) > regionFileSize) { ++ if (canRecalcHeader) { ++ LOGGER.error("Detected invalid header for regionfile " + this.regionFile.getAbsolutePath() + "! Recalculating header..."); ++ needsHeaderRecalc = true; ++ break; ++ } else { ++ // location = chunkX | (chunkZ << 5); ++ LOGGER.fatal("Detected invalid header for regionfile " + this.regionFile.getAbsolutePath() + ++ "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header"); ++ if (!hasBackedUp) { ++ hasBackedUp = true; ++ this.backupRegionFile(); ++ } ++ this.timestamps.put(headerLocation, 0); // be consistent, delete the timestamp too ++ this.offsets.put(headerLocation, 0); // delete the entry from header ++ continue; ++ } + } ++ boolean failedToAllocate = !this.usedSectors.tryAllocate(offset, sectorLength); ++ if (failedToAllocate) { ++ LOGGER.error("Overlapping allocation by local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") in regionfile " + this.regionFile.getAbsolutePath()); ++ } ++ if (failedToAllocate & !canRecalcHeader) { ++ // location = chunkX | (chunkZ << 5); ++ LOGGER.fatal("Detected invalid header for regionfile " + this.regionFile.getAbsolutePath() + ++ "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header"); ++ if (!hasBackedUp) { ++ hasBackedUp = true; ++ this.backupRegionFile(); ++ } ++ this.timestamps.put(headerLocation, 0); // be consistent, delete the timestamp too ++ this.offsets.put(headerLocation, 0); // delete the entry from header ++ continue; ++ } ++ needsHeaderRecalc |= failedToAllocate; ++ // Tuinity end - recalculate header on header corruption + } + } ++ // Tuinity start - recalculate header on header corruption ++ // we move the recalc here so comparison to old header is correct when logging to console ++ if (needsHeaderRecalc) { // true if header gave us overlapping allocations or had other issues ++ LOGGER.error("Recalculating regionfile " + this.regionFile.getAbsolutePath() + ", header gave erroneous offsets & locations"); ++ this.recalculateHeader(); ++ } ++ // Tuinity end + } + + } + } + + private Path getExternalChunkPath(ChunkPos chunkPos) { +- String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; ++ String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; // Tuinity - diff on change + + return this.externalFileDir.resolve(s); + } + ++ // Tuinity start ++ private static ChunkPos getOversizedChunkPair(File file) { ++ String fileName = file.getName(); ++ ++ if (!fileName.startsWith("c.") || !fileName.endsWith(".mcc")) { ++ return null; ++ } ++ ++ String[] split = fileName.split("\\."); ++ ++ if (split.length != 4) { ++ return null; ++ } ++ ++ try { ++ int x = Integer.parseInt(split[1]); ++ int z = Integer.parseInt(split[2]); ++ ++ return new ChunkPos(x, z); ++ } catch (NumberFormatException ex) { ++ return null; ++ } ++ } ++ // Tuinity end ++ + @Nullable + public synchronized DataInputStream getChunkDataInputStream(ChunkPos pos) throws IOException { + int i = this.getOffset(pos); +@@ -177,6 +595,12 @@ public class RegionFile implements AutoCloseable { + ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error + if (bytebuffer.remaining() < 5) { + RegionFile.LOGGER.error("Chunk {} header is truncated: expected {} but read {}", pos, l, bytebuffer.remaining()); ++ // Tuinity start - recalculate header on regionfile corruption ++ if (this.canRecalcHeader) { ++ this.recalculateHeader(); ++ return this.getChunkDataInputStream(pos); ++ } ++ // Tuinity end - recalculate header on regionfile corruption + return null; + } else { + int i1 = bytebuffer.getInt(); +@@ -184,6 +608,12 @@ public class RegionFile implements AutoCloseable { + + if (i1 == 0) { + RegionFile.LOGGER.warn("Chunk {} is allocated, but stream is missing", pos); ++ // Tuinity start - recalculate header on regionfile corruption ++ if (this.canRecalcHeader) { ++ this.recalculateHeader(); ++ return this.getChunkDataInputStream(pos); ++ } ++ // Tuinity end - recalculate header on regionfile corruption + return null; + } else { + int j1 = i1 - 1; +@@ -191,17 +621,49 @@ public class RegionFile implements AutoCloseable { + if (RegionFile.isExternalStreamChunk(b0)) { + if (j1 != 0) { + RegionFile.LOGGER.warn("Chunk has both internal and external streams"); ++ // Tuinity start - recalculate header on regionfile corruption ++ if (this.canRecalcHeader) { ++ this.recalculateHeader(); ++ return this.getChunkDataInputStream(pos); ++ } ++ // Tuinity end - recalculate header on regionfile corruption + } + +- return this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0)); ++ // Tuinity start - recalculate header on regionfile corruption ++ final DataInputStream ret = this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0)); ++ if (ret == null && this.canRecalcHeader) { ++ this.recalculateHeader(); ++ return this.getChunkDataInputStream(pos); ++ } ++ return ret; ++ // Tuinity end - recalculate header on regionfile corruption + } else if (j1 > bytebuffer.remaining()) { + RegionFile.LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", pos, j1, bytebuffer.remaining()); ++ // Tuinity start - recalculate header on regionfile corruption ++ if (this.canRecalcHeader) { ++ this.recalculateHeader(); ++ return this.getChunkDataInputStream(pos); ++ } ++ // Tuinity end - recalculate header on regionfile corruption + return null; + } else if (j1 < 0) { + RegionFile.LOGGER.error("Declared size {} of chunk {} is negative", i1, pos); ++ // Tuinity start - recalculate header on regionfile corruption ++ if (this.canRecalcHeader) { ++ this.recalculateHeader(); ++ return this.getChunkDataInputStream(pos); ++ } ++ // Tuinity end - recalculate header on regionfile corruption + return null; + } else { +- return this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1)); ++ // Tuinity start - recalculate header on regionfile corruption ++ final DataInputStream ret = this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1)); ++ if (ret == null && this.canRecalcHeader) { ++ this.recalculateHeader(); ++ return this.getChunkDataInputStream(pos); ++ } ++ return ret; ++ // Tuinity end - recalculate header on regionfile corruption + } + } + } +@@ -376,10 +838,15 @@ public class RegionFile implements AutoCloseable { + } + + private ByteBuffer createExternalStub() { ++ // Tuinity start - add version param ++ return this.createExternalStub(this.version); ++ } ++ private ByteBuffer createExternalStub(RegionFileVersion version) { ++ // Tuinity end - add version param + ByteBuffer bytebuffer = ByteBuffer.allocate(5); + + bytebuffer.putInt(1); +- bytebuffer.put((byte) (this.version.getId() | 128)); ++ bytebuffer.put((byte) (version.getId() | 128)); // Tuinity - replace with version param + ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error + return bytebuffer; + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +index 6496108953effae82391b5c1ea6fdec8482731cd..a6f831fea2245e2d1f44ffa60f96b6f1243b888b 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +@@ -25,7 +25,15 @@ public class RegionFileStorage implements AutoCloseable { + private final File folder; + private final boolean sync; + ++ private final boolean isChunkData; // Tuinity ++ + RegionFileStorage(File directory, boolean dsync) { ++ // Tuinity start - add isChunkData param ++ this(directory, dsync, false); ++ } ++ RegionFileStorage(File directory, boolean dsync, boolean isChunkData) { ++ this.isChunkData = isChunkData; ++ // Tuinity end - add isChunkData param + this.folder = directory; + this.sync = dsync; + } +@@ -90,9 +98,9 @@ public class RegionFileStorage implements AutoCloseable { + + File file = this.folder; + int j = chunkcoordintpair.getRegionX(); +- File file1 = new File(file, "r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); ++ File file1 = new File(file, "r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); // Tuinity - diff on change + if (existingOnly && !file1.exists()) return null; // CraftBukkit +- RegionFile regionfile1 = new RegionFile(file1, this.folder, this.sync); ++ RegionFile regionfile1 = new RegionFile(file1, this.folder, this.sync, this.isChunkData); // Tuinity - allow for chunk regionfiles to regen header + + this.regionCache.putAndMoveToFirst(i, regionfile1); + // Paper start +@@ -180,6 +188,13 @@ public class RegionFileStorage implements AutoCloseable { + if (regionfile == null) { + return null; + } ++ // Tuinity start - Add regionfile parameter ++ return this.read(pos, regionfile); ++ } ++ public CompoundTag read(ChunkPos pos, RegionFile regionfile) throws IOException { ++ // We add the regionfile parameter to avoid the potential deadlock (on fileLock) if we went back to obtain a regionfile ++ // if we decide to re-read ++ // Tuinity end + // CraftBukkit end + try { // Paper + DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos); +@@ -196,6 +211,17 @@ public class RegionFileStorage implements AutoCloseable { + try { + if (datainputstream != null) { + nbttagcompound = NbtIo.read((DataInput) datainputstream); ++ // Tuinity start - recover from corrupt regionfile header ++ if (this.isChunkData) { ++ ChunkPos chunkPos = ChunkSerializer.getChunkCoordinate(nbttagcompound); ++ if (!chunkPos.equals(pos)) { ++ MinecraftServer.LOGGER.error("Attempting to read chunk data at " + pos.toString() + " but got chunk data for " + chunkPos.toString() + " instead! Attempting regionfile recalculation for regionfile " + regionfile.regionFile.getAbsolutePath()); ++ regionfile.recalculateHeader(); ++ regionfile.fileLock.lock(); // otherwise we will unlock twice and only lock once. ++ return this.read(pos, regionfile); ++ } ++ } ++ // Tuinity end - recover from corrupt regionfile header + break label43; + } + +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java +index b7835b9b904e7d4bff64f7189049e334f5ab4d6f..ae638ac0a0557de204471fef4b03bdb0ad310b2b 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java +@@ -12,7 +12,7 @@ import java.util.zip.InflaterInputStream; + import javax.annotation.Nullable; + + public class RegionFileVersion { +- private static final Int2ObjectMap VERSIONS = new Int2ObjectOpenHashMap<>(); ++ public static final Int2ObjectMap VERSIONS = new Int2ObjectOpenHashMap<>(); // Tuinity - public + public static final RegionFileVersion VERSION_GZIP = register(new RegionFileVersion(1, GZIPInputStream::new, GZIPOutputStream::new)); + public static final RegionFileVersion VERSION_DEFLATE = register(new RegionFileVersion(2, InflaterInputStream::new, DeflaterOutputStream::new)); + public static final RegionFileVersion VERSION_NONE = register(new RegionFileVersion(3, (inputStream) -> { +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java +index 90f7b06bd2c558be35c4577044fa033e1fb5cc22..8f244db7e46ac1a3d2c8358f001d488900a76926 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java +@@ -61,11 +61,11 @@ public class SectionStorage extends RegionFileStorage implements AutoCloseabl + } + + @Nullable +- protected Optional get(long pos) { ++ public Optional get(long pos) { // Tuinity - public + return this.storage.get(pos); + } + +- protected Optional getOrLoad(long pos) { ++ public Optional getOrLoad(long pos) { // Tuinity - public + if (this.outsideStoredRange(pos)) { + return Optional.empty(); + } else { +diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java +index f01182a0ac8a14bcd5b1deb778306e7bf1bf70ed..2cfc54a577d0a63a504e24bc54fd763fe51083e5 100644 +--- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java ++++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java +@@ -9,54 +9,40 @@ import javax.annotation.Nullable; + import net.minecraft.world.entity.Entity; + + public class EntityTickList { +- private Int2ObjectMap active = new Int2ObjectLinkedOpenHashMap<>(); +- private Int2ObjectMap passive = new Int2ObjectLinkedOpenHashMap<>(); +- @Nullable +- private Int2ObjectMap iterated; ++ private final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet entities = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(true); // Tuinity - rewrite this, always keep this updated - why would we EVER tick an entity that's not ticking? + + private void ensureActiveIsNotIterated() { +- if (this.iterated == this.active) { +- this.passive.clear(); +- +- for(Entry entry : Int2ObjectMaps.fastIterable(this.active)) { +- this.passive.put(entry.getIntKey(), entry.getValue()); +- } +- +- Int2ObjectMap int2ObjectMap = this.active; +- this.active = this.passive; +- this.passive = int2ObjectMap; +- } ++ // Tuinity - replace with better logic, do not delay removals + + } + + public void add(Entity entity) { + this.ensureActiveIsNotIterated(); +- this.active.put(entity.getId(), entity); ++ this.entities.add(entity); // Tuinity - replace with better logic, do not delay removals/additions + } + + public void remove(Entity entity) { + this.ensureActiveIsNotIterated(); +- this.active.remove(entity.getId()); ++ this.entities.remove(entity); // Tuinity - replace with better logic, do not delay removals/additions + } + + public boolean contains(Entity entity) { +- return this.active.containsKey(entity.getId()); ++ return this.entities.contains(entity); // Tuinity - replace with better logic, do not delay removals/additions + } + + public void forEach(Consumer action) { +- if (this.iterated != null) { +- throw new UnsupportedOperationException("Only one concurrent iteration supported"); +- } else { +- this.iterated = this.active; +- +- try { +- for(Entity entity : this.active.values()) { +- action.accept(entity); +- } +- } finally { +- this.iterated = null; ++ // Tuinity start - replace with better logic, do not delay removals/additions ++ // To ensure nothing weird happens with dimension travelling, do not iterate over new entries... ++ // (by dfl iterator() is configured to not iterate over new entries) ++ com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator iterator = this.entities.iterator(); ++ try { ++ while (iterator.hasNext()) { ++ action.accept(iterator.next()); + } +- ++ } finally { ++ iterator.finishedIterating(); + } ++ ++ // Tuinity end - replace with better logic, do not delay removals/additions + } + } +diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java +index 5a0a1b01e89b122811b0b567e1ee27081953e638..a40af675d594c0c3a24f61948c28bd682115263e 100644 +--- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java ++++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java +@@ -41,8 +41,10 @@ public class PersistentEntitySectionManager implements A + private final Long2ObjectMap chunkLoadStatuses = new Long2ObjectOpenHashMap<>(); + private final LongSet chunksToUnload = new LongOpenHashSet(); + private final Queue> loadingInbox = Queues.newConcurrentLinkedQueue(); ++ public final com.tuinity.tuinity.world.EntitySliceManager entitySliceManager; // Tuinity + +- public PersistentEntitySectionManager(Class entityClass, LevelCallback handler, EntityPersistentStorage dataAccess) { ++ public PersistentEntitySectionManager(Class entityClass, LevelCallback handler, EntityPersistentStorage dataAccess, com.tuinity.tuinity.world.EntitySliceManager entitySliceManager) { // Tuinity ++ this.entitySliceManager = entitySliceManager; // Tuinity + this.visibleEntityStorage = new EntityLookup<>(); + this.sectionStorage = new EntitySectionStorage<>(entityClass, this.chunkVisibility); + this.chunkVisibility.defaultReturnValue(Visibility.HIDDEN); +@@ -52,6 +54,65 @@ public class PersistentEntitySectionManager implements A + this.entityGetter = new LevelEntityGetterAdapter<>(this.visibleEntityStorage, this.sectionStorage); + } + ++ // Tuinity start - optimise notify() ++ public final void removeNavigatorsFromData(Entity entity, final int chunkX, final int chunkZ) { ++ if (!(entity instanceof net.minecraft.world.entity.Mob)) { ++ return; ++ } ++ com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section = ++ this.entitySliceManager.world.getChunkSource().chunkMap.dataRegionManager.getRegionSection(chunkX, chunkZ); ++ if (section != null) { ++ net.minecraft.server.level.ChunkMap.DataRegionSectionData sectionData = (net.minecraft.server.level.ChunkMap.DataRegionSectionData)section.sectionData; ++ sectionData.removeFromNavigators(section, ((net.minecraft.world.entity.Mob)entity)); ++ } ++ } ++ ++ public final void removeNavigatorsFromData(Entity entity) { ++ if (!(entity instanceof net.minecraft.world.entity.Mob)) { ++ return; ++ } ++ BlockPos entityPos = entity.blockPosition(); ++ com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section = ++ this.entitySliceManager.world.getChunkSource().chunkMap.dataRegionManager.getRegionSection(entityPos.getX() >> 4, entityPos.getZ() >> 4); ++ if (section != null) { ++ net.minecraft.server.level.ChunkMap.DataRegionSectionData sectionData = (net.minecraft.server.level.ChunkMap.DataRegionSectionData)section.sectionData; ++ sectionData.removeFromNavigators(section, ((net.minecraft.world.entity.Mob)entity)); ++ } ++ } ++ ++ public final void addNavigatorsIfPathingToRegion(Entity entity) { ++ if (!(entity instanceof net.minecraft.world.entity.Mob)) { ++ return; ++ } ++ BlockPos entityPos = entity.blockPosition(); ++ com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section = ++ this.entitySliceManager.world.getChunkSource().chunkMap.dataRegionManager.getRegionSection(entityPos.getX() >> 4, entityPos.getZ() >> 4); ++ if (section != null) { ++ net.minecraft.server.level.ChunkMap.DataRegionSectionData sectionData = (net.minecraft.server.level.ChunkMap.DataRegionSectionData)section.sectionData; ++ if (((net.minecraft.world.entity.Mob)entity).getNavigation().isViableForPathRecalculationChecking()) { ++ sectionData.addToNavigators(section, ((net.minecraft.world.entity.Mob)entity)); ++ } ++ } ++ } ++ ++ public final void updateNavigatorsInRegion(Entity entity) { ++ if (!(entity instanceof net.minecraft.world.entity.Mob)) { ++ return; ++ } ++ BlockPos entityPos = entity.blockPosition(); ++ com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section = ++ this.entitySliceManager.world.getChunkSource().chunkMap.dataRegionManager.getRegionSection(entityPos.getX() >> 4, entityPos.getZ() >> 4); ++ if (section != null) { ++ net.minecraft.server.level.ChunkMap.DataRegionSectionData sectionData = (net.minecraft.server.level.ChunkMap.DataRegionSectionData)section.sectionData; ++ if (((net.minecraft.world.entity.Mob)entity).getNavigation().isViableForPathRecalculationChecking()) { ++ sectionData.addToNavigators(section, ((net.minecraft.world.entity.Mob)entity)); ++ } else { ++ sectionData.removeFromNavigators(section, ((net.minecraft.world.entity.Mob)entity)); ++ } ++ } ++ } ++ // Tuinity end - optimise notify() ++ + void removeSectionIfEmpty(long sectionPos, EntitySection section) { + if (section.isEmpty()) { + this.sectionStorage.remove(sectionPos); +@@ -93,6 +154,7 @@ public class PersistentEntitySectionManager implements A + long l = SectionPos.asLong(entity.blockPosition()); + EntitySection entitySection = this.sectionStorage.getOrCreateSection(l); + entitySection.add(entity); ++ this.entitySliceManager.addEntity((Entity)entity); // Tuinity + entity.setLevelCallback(new PersistentEntitySectionManager.Callback(entity, l, entitySection)); + if (!existing) { + this.callbacks.onCreated(entity); +@@ -147,6 +209,7 @@ public class PersistentEntitySectionManager implements A + + public void updateChunkStatus(ChunkPos chunkPos, ChunkHolder.FullChunkStatus levelType) { + Visibility visibility = Visibility.fromFullChunkStatus(levelType); ++ this.entitySliceManager.chunkStatusChange(chunkPos.x, chunkPos.z, levelType); // Tuinity + this.updateChunkStatus(chunkPos, visibility); + } + +@@ -382,18 +445,38 @@ public class PersistentEntitySectionManager implements A + @Override + public void onMove() { + BlockPos blockPos = this.entity.blockPosition(); +- long l = SectionPos.asLong(blockPos); ++ long l = SectionPos.asLong(blockPos); // Tuinity - diff on change, new position section + if (l != this.currentSectionKey) { +- Visibility visibility = this.currentSection.getStatus(); ++ PersistentEntitySectionManager.this.entitySliceManager.moveEntity((Entity)this.entity); // Tuinity ++ // Tuinity start ++ int shift = PersistentEntitySectionManager.this.entitySliceManager.world.getChunkSource().chunkMap.dataRegionManager.regionChunkShift; ++ int oldChunkX = com.tuinity.tuinity.util.CoordinateUtils.getChunkSectionX(this.currentSectionKey); ++ int oldChunkZ = com.tuinity.tuinity.util.CoordinateUtils.getChunkSectionZ(this.currentSectionKey); ++ int oldRegionX = oldChunkX >> shift; ++ int oldRegionZ = oldChunkZ >> shift; ++ ++ int newRegionX = com.tuinity.tuinity.util.CoordinateUtils.getChunkSectionX(l) >> shift; ++ int newRegionZ = com.tuinity.tuinity.util.CoordinateUtils.getChunkSectionZ(l) >> shift; ++ ++ if (oldRegionX != newRegionX || oldRegionZ != newRegionZ) { ++ PersistentEntitySectionManager.this.removeNavigatorsFromData((Entity)this.entity, oldChunkX, oldChunkZ); ++ } ++ // Tuinity end ++ Visibility visibility = this.currentSection.getStatus(); // Tuinity - diff on change - this should be OLD section visibility + if (!this.currentSection.remove(this.entity)) { + PersistentEntitySectionManager.LOGGER.warn("Entity {} wasn't found in section {} (moving to {})", this.entity, SectionPos.of(this.currentSectionKey), l); + } + + PersistentEntitySectionManager.this.removeSectionIfEmpty(this.currentSectionKey, this.currentSection); +- EntitySection entitySection = PersistentEntitySectionManager.this.sectionStorage.getOrCreateSection(l); ++ EntitySection entitySection = PersistentEntitySectionManager.this.sectionStorage.getOrCreateSection(l); // Tuinity - diff on change, this should be NEW section + entitySection.add(this.entity); + this.currentSection = entitySection; + this.currentSectionKey = l; ++ // Tuinity start ++ if ((oldRegionX != newRegionX || oldRegionZ != newRegionZ) && visibility.isTicking() && entitySection.getStatus().isTicking()) { ++ PersistentEntitySectionManager.this.addNavigatorsIfPathingToRegion((Entity)this.entity); ++ } ++ // Tuinity end + this.updateStatus(visibility, entitySection.getStatus()); + } + +@@ -427,6 +510,7 @@ public class PersistentEntitySectionManager implements A + if (!this.currentSection.remove(this.entity)) { + PersistentEntitySectionManager.LOGGER.warn("Entity {} wasn't found in section {} (destroying due to {})", this.entity, SectionPos.of(this.currentSectionKey), reason); + } ++ PersistentEntitySectionManager.this.entitySliceManager.removeEntity((Entity)this.entity); // Tuinity + + Visibility visibility = PersistentEntitySectionManager.getEffectiveStatus(this.entity, this.currentSection.getStatus()); + if (visibility.isTicking()) { +diff --git a/src/main/java/net/minecraft/world/level/levelgen/feature/blockplacers/ColumnPlacer.java b/src/main/java/net/minecraft/world/level/levelgen/feature/blockplacers/ColumnPlacer.java +index 05bba5410fbd9f8e333584ccbd65a909f3040322..de3122f450edacaf2eed6f60b0680ebe64f7d214 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/feature/blockplacers/ColumnPlacer.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/feature/blockplacers/ColumnPlacer.java +@@ -10,11 +10,28 @@ import net.minecraft.world.level.LevelAccessor; + import net.minecraft.world.level.block.state.BlockState; + + public class ColumnPlacer extends BlockPlacer { ++ // Tuinity start + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> { +- return instance.group(IntProvider.NON_NEGATIVE_CODEC.fieldOf("size").forGetter((columnPlacer) -> { +- return columnPlacer.size; +- })).apply(instance, ColumnPlacer::new); ++ return instance.group( ++ IntProvider.NON_NEGATIVE_CODEC.optionalFieldOf("size").forGetter((columnPlacer) -> { ++ return java.util.Optional.of(columnPlacer.size); ++ }), ++ Codec.INT.optionalFieldOf("min_size").forGetter((columnPlacer) -> { ++ return java.util.Optional.empty(); ++ }), ++ Codec.INT.optionalFieldOf("extra_size").forGetter((columnPlacer) -> { ++ return java.util.Optional.empty(); ++ }) ++ ).apply(instance, ColumnPlacer::new); + }); ++ public ColumnPlacer(java.util.Optional size, java.util.Optional minSize, java.util.Optional extraSize) { ++ if (size.isPresent()) { ++ this.size = size.get(); ++ } else { ++ this.size = net.minecraft.util.valueproviders.BiasedToBottomInt.of(minSize.get().intValue(), minSize.get().intValue() + extraSize.get().intValue()); ++ } ++ } ++ // Tuinity end + private final IntProvider size; + + public ColumnPlacer(IntProvider size) { +diff --git a/src/main/java/net/minecraft/world/level/levelgen/feature/configurations/TreeConfiguration.java b/src/main/java/net/minecraft/world/level/levelgen/feature/configurations/TreeConfiguration.java +index 5da68897148192905c2747676c1ee2ee649f923f..b990099cf274f8cb0d96c139345cf0bf328affd6 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/feature/configurations/TreeConfiguration.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/feature/configurations/TreeConfiguration.java +@@ -18,13 +18,13 @@ public class TreeConfiguration implements FeatureConfiguration { + return treeConfiguration.trunkProvider; + }), TrunkPlacer.CODEC.fieldOf("trunk_placer").forGetter((treeConfiguration) -> { + return treeConfiguration.trunkPlacer; +- }), BlockStateProvider.CODEC.fieldOf("foliage_provider").forGetter((treeConfiguration) -> { ++ }), net.minecraft.server.MCUtil.fieldWithFallbacks(BlockStateProvider.CODEC, "foliage_provider", "leaves_provider").forGetter((treeConfiguration) -> { // Paper - provide fallback for rename + return treeConfiguration.foliageProvider; +- }), BlockStateProvider.CODEC.fieldOf("sapling_provider").forGetter((treeConfiguration) -> { ++ }), BlockStateProvider.CODEC.optionalFieldOf("sapling_provider", new SimpleStateProvider(Blocks.OAK_SAPLING.defaultBlockState())).forGetter((treeConfiguration) -> { // Paper - provide default - it looks like for now this is OK because it's just used to check canSurvive. Same check happens in 1.16.5 for the default we provide - so it should retain behavior... + return treeConfiguration.saplingProvider; + }), FoliagePlacer.CODEC.fieldOf("foliage_placer").forGetter((treeConfiguration) -> { + return treeConfiguration.foliagePlacer; +- }), BlockStateProvider.CODEC.fieldOf("dirt_provider").forGetter((treeConfiguration) -> { ++ }), BlockStateProvider.CODEC.optionalFieldOf("dirt_provider", new SimpleStateProvider(Blocks.DIRT.defaultBlockState())).forGetter((treeConfiguration) -> { // Paper - provide defaults, old data DOES NOT have this key (thankfully ALL OLD DATA used DIRT) + return treeConfiguration.dirtProvider; + }), FeatureSize.CODEC.fieldOf("minimum_size").forGetter((treeConfiguration) -> { + return treeConfiguration.minimumSize; +diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +index d5ba2e679ed1858ea18e18feffce50544ae036c2..78da12a3feb05a5504daf8379be3d568c389a458 100644 +--- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java ++++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +@@ -52,16 +52,37 @@ public class PortalForcer { + // int i = flag ? 16 : 128; + // CraftBukkit end + +- villageplace.ensureLoadedAndValid(this.level, blockposition, i); +- Optional optional = villageplace.getInSquare((villageplacetype) -> { +- return villageplacetype == PoiType.NETHER_PORTAL; +- }, blockposition, i, PoiManager.Occupancy.ANY).sorted(Comparator.comparingDouble((PoiRecord villageplacerecord) -> { // CraftBukkit - decompile error +- return villageplacerecord.getPos().distSqr(blockposition); +- }).thenComparingInt((villageplacerecord) -> { +- return villageplacerecord.getPos().getY(); +- })).filter((villageplacerecord) -> { +- return this.level.getBlockState(villageplacerecord.getPos()).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); +- }).findFirst(); ++ // Tuinity start - optimise portals ++ Optional optional; ++ java.util.List records = new java.util.ArrayList<>(); ++ com.tuinity.tuinity.util.PoiAccess.findClosestPoiDataRecords( ++ villageplace, ++ (PoiType type) -> { ++ return type == PoiType.NETHER_PORTAL; ++ }, ++ (BlockPos pos) -> { ++ net.minecraft.world.level.chunk.ChunkAccess lowest = this.level.getChunk(pos.getX() >> 4, pos.getZ() >> 4, net.minecraft.world.level.chunk.ChunkStatus.EMPTY); ++ if (!lowest.getStatus().isOrAfter(net.minecraft.world.level.chunk.ChunkStatus.FULL)) { ++ // why would we generate the chunk? ++ return false; ++ } ++ return lowest.getBlockState(pos).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); ++ }, ++ blockposition, i, Double.MAX_VALUE, PoiManager.Occupancy.ANY, true, records ++ ); ++ ++ // this gets us most of the way there, but we bias towards lower y values. ++ PoiRecord lowestYRecord = null; ++ for (PoiRecord record : records) { ++ if (lowestYRecord == null) { ++ lowestYRecord = record; ++ } else if (lowestYRecord.getPos().getY() > record.getPos().getY()) { ++ lowestYRecord = record; ++ } ++ } ++ // now we're done ++ optional = Optional.ofNullable(lowestYRecord); ++ // Tuinity end - optimise portals + + return optional.map((villageplacerecord) -> { + BlockPos blockposition1 = villageplacerecord.getPos(); +diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java +index 120498a39b7ca7aee9763084507508d4a1c425aa..6f7e6429c35eea346517cbf08cf223fc6d838a8c 100644 +--- a/src/main/java/net/minecraft/world/phys/AABB.java ++++ b/src/main/java/net/minecraft/world/phys/AABB.java +@@ -25,6 +25,17 @@ public class AABB { + this.maxZ = Math.max(z1, z2); + } + ++ // Tuinity start ++ public AABB(double minX, double minY, double minZ, double maxX, double maxY, double maxZ, boolean dummy) { ++ this.minX = minX; ++ this.minY = minY; ++ this.minZ = minZ; ++ this.maxX = maxX; ++ this.maxY = maxY; ++ this.maxZ = maxZ; ++ } ++ // Tuinity end ++ + public AABB(BlockPos pos) { + this((double)pos.getX(), (double)pos.getY(), (double)pos.getZ(), (double)(pos.getX() + 1), (double)(pos.getY() + 1), (double)(pos.getZ() + 1)); + } +diff --git a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java +index 99427b6130895ddecee8bcf77db72d809c24c375..af1ef430e81cb9bdd749aa235577c63fa381f4c5 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java +@@ -6,6 +6,9 @@ import java.util.Arrays; + import net.minecraft.Util; + import net.minecraft.core.Direction; + ++// Tuinity start ++import it.unimi.dsi.fastutil.doubles.AbstractDoubleList; ++// Tuinity end + public class ArrayVoxelShape extends VoxelShape { + private final DoubleList xs; + private final DoubleList ys; +@@ -16,6 +19,11 @@ public class ArrayVoxelShape extends VoxelShape { + } + + ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) { ++ // Tuinity start - optimise multi-aabb shapes ++ this(shape, xPoints, yPoints, zPoints, null, 0.0, 0.0, 0.0); ++ } ++ ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints, net.minecraft.world.phys.AABB[] boundingBoxesRepresentation, double offsetX, double offsetY, double offsetZ) { ++ // Tuinity end - optimise multi-aabb shapes + super(shape); + int i = shape.getXSize() + 1; + int j = shape.getYSize() + 1; +@@ -27,6 +35,12 @@ public class ArrayVoxelShape extends VoxelShape { + } else { + throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException("Lengths of point arrays must be consistent with the size of the VoxelShape.")); + } ++ // Tuinity start - optimise multi-aabb shapes ++ this.boundingBoxesRepresentation = boundingBoxesRepresentation == null ? this.toAabbs().toArray(EMPTY) : boundingBoxesRepresentation; ++ this.offsetX = offsetX; ++ this.offsetY = offsetY; ++ this.offsetZ = offsetZ; ++ // Tuinity end - optimise multi-aabb shapes + } + + @Override +@@ -42,4 +56,152 @@ public class ArrayVoxelShape extends VoxelShape { + throw new IllegalArgumentException(); + } + } ++ ++ // Tuinity start ++ public static final class DoubleListOffsetExposed extends AbstractDoubleList { ++ ++ public final DoubleArrayList list; ++ public final double offset; ++ ++ public DoubleListOffsetExposed(final DoubleArrayList list, final double offset) { ++ this.list = list; ++ this.offset = offset; ++ } ++ ++ @Override ++ public double getDouble(final int index) { ++ return this.list.getDouble(index) + this.offset; ++ } ++ ++ @Override ++ public int size() { ++ return this.list.size(); ++ } ++ } ++ ++ static final net.minecraft.world.phys.AABB[] EMPTY = new net.minecraft.world.phys.AABB[0]; ++ final net.minecraft.world.phys.AABB[] boundingBoxesRepresentation; ++ ++ final double offsetX; ++ final double offsetY; ++ final double offsetZ; ++ ++ public final net.minecraft.world.phys.AABB[] getBoundingBoxesRepresentation() { ++ return this.boundingBoxesRepresentation; ++ } ++ ++ public final double getOffsetX() { ++ return this.offsetX; ++ } ++ ++ public final double getOffsetY() { ++ return this.offsetY; ++ } ++ ++ public final double getOffsetZ() { ++ return this.offsetZ; ++ } ++ ++ @Override ++ public java.util.List toAabbs() { ++ if (this.boundingBoxesRepresentation == null) { ++ return super.toAabbs(); ++ } ++ java.util.List ret = new java.util.ArrayList<>(this.boundingBoxesRepresentation.length); ++ ++ double offX = this.offsetX; ++ double offY = this.offsetY; ++ double offZ = this.offsetZ; ++ ++ for (net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { ++ ret.add(boundingBox.move(offX, offY, offZ)); ++ } ++ ++ return ret; ++ } ++ ++ protected static DoubleArrayList getList(DoubleList from) { ++ if (from instanceof DoubleArrayList) { ++ return (DoubleArrayList)from; ++ } else { ++ return DoubleArrayList.wrap(from.toDoubleArray()); ++ } ++ } ++ ++ @Override ++ public VoxelShape move(double x, double y, double z) { ++ if (x == 0.0 && y == 0.0 && z == 0.0) { ++ return this; ++ } ++ DoubleListOffsetExposed xPoints, yPoints, zPoints; ++ double offsetX, offsetY, offsetZ; ++ ++ if (this.xs instanceof DoubleListOffsetExposed) { ++ xPoints = new DoubleListOffsetExposed(((DoubleListOffsetExposed)this.xs).list, offsetX = this.offsetX + x); ++ yPoints = new DoubleListOffsetExposed(((DoubleListOffsetExposed)this.ys).list, offsetY = this.offsetY + y); ++ zPoints = new DoubleListOffsetExposed(((DoubleListOffsetExposed)this.zs).list, offsetZ = this.offsetZ + z); ++ } else { ++ xPoints = new DoubleListOffsetExposed(getList(this.xs), offsetX = x); ++ yPoints = new DoubleListOffsetExposed(getList(this.ys), offsetY = y); ++ zPoints = new DoubleListOffsetExposed(getList(this.zs), offsetZ = z); ++ } ++ ++ return new ArrayVoxelShape(this.shape, xPoints, yPoints, zPoints, this.boundingBoxesRepresentation, offsetX, offsetY, offsetZ); ++ } ++ ++ @Override ++ public final boolean intersects(net.minecraft.world.phys.AABB axisalingedbb) { ++ // this can be optimised by checking an "overall shape" first, but not needed ++ double offX = this.offsetX; ++ double offY = this.offsetY; ++ double offZ = this.offsetZ; ++ ++ for (net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { ++ if (com.tuinity.tuinity.util.CollisionUtil.voxelShapeIntersect(axisalingedbb, boundingBox.minX + offX, boundingBox.minY + offY, boundingBox.minZ + offZ, ++ boundingBox.maxX + offX, boundingBox.maxY + offY, boundingBox.maxZ + offZ)) { ++ return true; ++ } ++ } ++ ++ return false; ++ } ++ ++ @Override ++ public void forAllBoxes(Shapes.DoubleLineConsumer doubleLineConsumer) { ++ if (this.boundingBoxesRepresentation == null) { ++ super.forAllBoxes(doubleLineConsumer); ++ return; ++ } ++ for (final net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { ++ doubleLineConsumer.consume(boundingBox.minX + this.offsetX, boundingBox.minY + this.offsetY, boundingBox.minZ + this.offsetZ, ++ boundingBox.maxX + this.offsetX, boundingBox.maxY + this.offsetY, boundingBox.maxZ + this.offsetZ); ++ } ++ } ++ ++ @Override ++ public VoxelShape optimize() { ++ if (this == Shapes.empty() || this.boundingBoxesRepresentation.length == 0) { ++ return this; ++ } ++ ++ VoxelShape simplified = Shapes.empty(); ++ for (final net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { ++ simplified = Shapes.joinUnoptimized(simplified, Shapes.box(boundingBox.minX + this.offsetX, boundingBox.minY + this.offsetY, boundingBox.minZ + this.offsetZ, ++ boundingBox.maxX + this.offsetX, boundingBox.maxY + this.offsetY, boundingBox.maxZ + this.offsetZ), BooleanOp.OR); ++ } ++ ++ if (!(simplified instanceof ArrayVoxelShape)) { ++ return simplified; ++ } ++ ++ final net.minecraft.world.phys.AABB[] boundingBoxesRepresentation = ((ArrayVoxelShape)simplified).getBoundingBoxesRepresentation(); ++ ++ if (boundingBoxesRepresentation.length == 1) { ++ return new com.tuinity.tuinity.voxel.AABBVoxelShape(boundingBoxesRepresentation[0]).optimize(); ++ } ++ ++ return simplified; ++ } ++ // Tuinity end ++ + } +diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java +index 16bc18cacbf7a23fb744c8a12e7fd8da699b2fea..472c47a585da7d95b3f4774d3caef1d864b6337a 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java +@@ -26,16 +26,17 @@ public final class Shapes { + DiscreteVoxelShape discreteVoxelShape = new BitSetDiscreteVoxelShape(1, 1, 1); + discreteVoxelShape.fill(0, 0, 0); + return new CubeVoxelShape(discreteVoxelShape); +- }); ++ }); public static VoxelShape getFullUnoptimisedCube() { return BLOCK; } // Tuinity - OBFHELPER + public static final VoxelShape INFINITY = box(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + private static final VoxelShape EMPTY = new ArrayVoxelShape(new BitSetDiscreteVoxelShape(0, 0, 0), (DoubleList)(new DoubleArrayList(new double[]{0.0D})), (DoubleList)(new DoubleArrayList(new double[]{0.0D})), (DoubleList)(new DoubleArrayList(new double[]{0.0D}))); ++ public static final com.tuinity.tuinity.voxel.AABBVoxelShape BLOCK_OPTIMISED = new com.tuinity.tuinity.voxel.AABBVoxelShape(new AABB(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)); // Tuinity + + public static VoxelShape empty() { + return EMPTY; + } + + public static VoxelShape block() { +- return BLOCK; ++ return BLOCK_OPTIMISED; // Tuinity + } + + public static VoxelShape box(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { +@@ -47,30 +48,11 @@ public final class Shapes { + } + + public static VoxelShape create(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { +- if (!(maxX - minX < 1.0E-7D) && !(maxY - minY < 1.0E-7D) && !(maxZ - minZ < 1.0E-7D)) { +- int i = findBits(minX, maxX); +- int j = findBits(minY, maxY); +- int k = findBits(minZ, maxZ); +- if (i >= 0 && j >= 0 && k >= 0) { +- if (i == 0 && j == 0 && k == 0) { +- return block(); +- } else { +- int l = 1 << i; +- int m = 1 << j; +- int n = 1 << k; +- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.withFilledBounds(l, m, n, (int)Math.round(minX * (double)l), (int)Math.round(minY * (double)m), (int)Math.round(minZ * (double)n), (int)Math.round(maxX * (double)l), (int)Math.round(maxY * (double)m), (int)Math.round(maxZ * (double)n)); +- return new CubeVoxelShape(bitSetDiscreteVoxelShape); +- } +- } else { +- return new ArrayVoxelShape(BLOCK.shape, (DoubleList)DoubleArrayList.wrap(new double[]{minX, maxX}), (DoubleList)DoubleArrayList.wrap(new double[]{minY, maxY}), (DoubleList)DoubleArrayList.wrap(new double[]{minZ, maxZ})); +- } +- } else { +- return empty(); +- } ++ return new com.tuinity.tuinity.voxel.AABBVoxelShape(new AABB(minX, minY, minZ, maxX, maxY, maxZ)); // Tuinity + } + + public static VoxelShape create(AABB box) { +- return create(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ); ++ return new com.tuinity.tuinity.voxel.AABBVoxelShape(box); // Tuinity + } + + @VisibleForTesting +@@ -132,6 +114,20 @@ public final class Shapes { + } + + public static boolean joinIsNotEmpty(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) { ++ // Tuinity start - optimise voxelshape ++ if (predicate == BooleanOp.AND) { ++ if (shape1 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape && shape2 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) { ++ return com.tuinity.tuinity.util.CollisionUtil.voxelShapeIntersect(((com.tuinity.tuinity.voxel.AABBVoxelShape)shape1).aabb, ((com.tuinity.tuinity.voxel.AABBVoxelShape)shape2).aabb); ++ } else if (shape1 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape && shape2 instanceof ArrayVoxelShape) { ++ return ((ArrayVoxelShape)shape2).intersects(((com.tuinity.tuinity.voxel.AABBVoxelShape)shape1).aabb); ++ } else if (shape2 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape && shape1 instanceof ArrayVoxelShape) { ++ return ((ArrayVoxelShape)shape1).intersects(((com.tuinity.tuinity.voxel.AABBVoxelShape)shape2).aabb); ++ } ++ } ++ return joinIsNotEmptyVanilla(shape1, shape2, predicate); ++ } ++ public static boolean joinIsNotEmptyVanilla(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) { ++ // Tuinity end - optimise voxelshape + if (predicate.apply(false, false)) { + throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException()); + } else { +@@ -285,6 +281,43 @@ public final class Shapes { + } + + public static VoxelShape getFaceShape(VoxelShape shape, Direction direction) { ++ // Tuinity start - optimise shape creation here for lighting, as this shape is going to be used ++ // for transparency checks ++ if (shape == BLOCK || shape == BLOCK_OPTIMISED) { ++ return BLOCK_OPTIMISED; ++ } else if (shape == empty()) { ++ return empty(); ++ } ++ ++ if (shape instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) { ++ final AABB box = ((com.tuinity.tuinity.voxel.AABBVoxelShape)shape).aabb; ++ switch (direction) { ++ case WEST: // -X ++ case EAST: { // +X ++ final boolean useEmpty = direction == Direction.EAST ? !DoubleMath.fuzzyEquals(box.maxX, 1.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON) : ++ !DoubleMath.fuzzyEquals(box.minX, 0.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON); ++ return useEmpty ? empty() : new com.tuinity.tuinity.voxel.AABBVoxelShape(new AABB(0.0, box.minY, box.minZ, 1.0, box.maxY, box.maxZ)).optimize(); ++ } ++ case DOWN: // -Y ++ case UP: { // +Y ++ final boolean useEmpty = direction == Direction.UP ? !DoubleMath.fuzzyEquals(box.maxY, 1.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON) : ++ !DoubleMath.fuzzyEquals(box.minY, 0.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON); ++ return useEmpty ? empty() : new com.tuinity.tuinity.voxel.AABBVoxelShape(new AABB(box.minX, 0.0, box.minZ, box.maxX, 1.0, box.maxZ)).optimize(); ++ } ++ case NORTH: // -Z ++ case SOUTH: { // +Z ++ final boolean useEmpty = direction == Direction.SOUTH ? !DoubleMath.fuzzyEquals(box.maxZ, 1.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON) : ++ !DoubleMath.fuzzyEquals(box.minZ,0.0, com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON); ++ return useEmpty ? empty() : new com.tuinity.tuinity.voxel.AABBVoxelShape(new AABB(box.minX, box.minY, 0.0, box.maxX, box.maxY, 1.0)).optimize(); ++ } ++ } ++ } ++ ++ // fall back to vanilla ++ return getFaceShapeVanilla(shape, direction); ++ } ++ public static VoxelShape getFaceShapeVanilla(VoxelShape shape, Direction direction) { ++ // Tuinity end + if (shape == block()) { + return block(); + } else { +@@ -299,7 +332,7 @@ public final class Shapes { + i = 0; + } + +- return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i)); ++ return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i).optimize().optimize()); // Tuinity - first optimize converts to ArrayVoxelShape, second optimize could convert to AABBVoxelShape + } + } + +@@ -324,6 +357,53 @@ public final class Shapes { + } + + public static boolean faceShapeOccludes(VoxelShape one, VoxelShape two) { ++ // Tuinity start - try to optimise for the case where the shapes do _not_ occlude ++ // which is _most_ of the time in lighting ++ if (one == getFullUnoptimisedCube() || one == BLOCK_OPTIMISED ++ || two == getFullUnoptimisedCube() || two == BLOCK_OPTIMISED) { ++ return true; ++ } ++ boolean v1Empty = one == empty(); ++ boolean v2Empty = two == empty(); ++ if (v1Empty && v2Empty) { ++ return false; ++ } ++ if ((one instanceof com.tuinity.tuinity.voxel.AABBVoxelShape || v1Empty) ++ && (two instanceof com.tuinity.tuinity.voxel.AABBVoxelShape || v2Empty)) { ++ if (!v1Empty && !v2Empty && (one != two)) { ++ AABB boundingBox1 = ((com.tuinity.tuinity.voxel.AABBVoxelShape)one).aabb; ++ AABB boundingBox2 = ((com.tuinity.tuinity.voxel.AABBVoxelShape)two).aabb; ++ // can call it here in some cases ++ ++ // check overall bounding box ++ double minY = Math.min(boundingBox1.minY, boundingBox2.minY); ++ double maxY = Math.max(boundingBox1.maxY, boundingBox2.maxY); ++ if (minY > com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON || maxY < (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)) { ++ return false; ++ } ++ double minX = Math.min(boundingBox1.minX, boundingBox2.minX); ++ double maxX = Math.max(boundingBox1.maxX, boundingBox2.maxX); ++ if (minX > com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON || maxX < (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)) { ++ return false; ++ } ++ double minZ = Math.min(boundingBox1.minZ, boundingBox2.minZ); ++ double maxZ = Math.max(boundingBox1.maxZ, boundingBox2.maxZ); ++ if (minZ > com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON || maxZ < (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)) { ++ return false; ++ } ++ // fall through to full merge check ++ } else { ++ AABB boundingBox = v1Empty ? ((com.tuinity.tuinity.voxel.AABBVoxelShape)two).aabb : ((com.tuinity.tuinity.voxel.AABBVoxelShape)one).aabb; ++ // check if the bounding box encloses the full cube ++ return (boundingBox.minY <= com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON && boundingBox.maxY >= (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)) && ++ (boundingBox.minX <= com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON && boundingBox.maxX >= (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)) && ++ (boundingBox.minZ <= com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON && boundingBox.maxZ >= (1 - com.tuinity.tuinity.util.CollisionUtil.COLLISION_EPSILON)); ++ } ++ } ++ return faceShapeOccludesVanilla(one, two); ++ } ++ public static boolean faceShapeOccludesVanilla(VoxelShape one, VoxelShape two) { ++ // Tuinity end + if (one != block() && two != block()) { + if (one.isEmpty() && two.isEmpty()) { + return false; +diff --git a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java +index f325d76c79d63629200262a77eab7cdcc9beedfa..ad23eafd6d9e7901f726977ad8404fa34dc0874e 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java +@@ -16,11 +16,17 @@ import net.minecraft.world.phys.BlockHitResult; + import net.minecraft.world.phys.Vec3; + + public abstract class VoxelShape { +- protected final DiscreteVoxelShape shape; ++ public final DiscreteVoxelShape shape; // Tuinity - public + @Nullable + private VoxelShape[] faces; + +- VoxelShape(DiscreteVoxelShape voxels) { ++ // Tuinity start ++ public boolean intersects(AABB shape) { ++ return Shapes.joinIsNotEmpty(this, new com.tuinity.tuinity.voxel.AABBVoxelShape(shape), BooleanOp.AND); ++ } ++ // Tuinity end ++ ++ protected VoxelShape(DiscreteVoxelShape voxels) { // Tuinity - protected + this.shape = voxels; + } + +@@ -163,7 +169,7 @@ public abstract class VoxelShape { + } + } + +- private VoxelShape calculateFace(Direction direction) { ++ protected VoxelShape calculateFace(Direction direction) { // Tuinity + Direction.Axis axis = direction.getAxis(); + DoubleList doubleList = this.getCoords(axis); + if (doubleList.size() == 2 && DoubleMath.fuzzyEquals(doubleList.getDouble(0), 0.0D, 1.0E-7D) && DoubleMath.fuzzyEquals(doubleList.getDouble(1), 1.0D, 1.0E-7D)) { +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +index 40d6dfe30e8f388fb2014ba81f9ea4a986354b88..9de4b1c9402e78c661b4d2dc7d70439e75768bc8 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +@@ -110,13 +110,7 @@ public class CraftChunk implements Chunk { + this.getWorld().getChunkAt(x, z); // Transient load for this tick + } + +- // Paper start - improve CraftChunk#getEntities +- return this.worldServer.entityManager.sectionStorage.getExistingSectionsInChunk(ChunkPos.asLong(this.x, this.z)) +- .flatMap(net.minecraft.world.level.entity.EntitySection::getEntities) +- .map(net.minecraft.world.entity.Entity::getBukkitEntity) +- .filter(entity -> entity != null && entity.isValid()) +- .toArray(Entity[]::new); +- // Paper end ++ return ((CraftWorld)this.getWorld()).getHandle().getChunkEntities(this.x, this.z); // Tuinity - optimise this better than paper :) + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index c79b193ad822b8c246f24a87cd418892bc18ff5a..4342bc5aad49fe372d561296a6b63818a443d089 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -230,7 +230,7 @@ import javax.annotation.Nullable; // Paper + import javax.annotation.Nonnull; // Paper + + public final class CraftServer implements Server { +- private final String serverName = "Paper"; // Paper ++ private final String serverName = "Tuinity"; // Tuinity // Paper + private final String serverVersion; + private final String bukkitVersion = Versioning.getBukkitVersion(); + private final Logger logger = Logger.getLogger("Minecraft"); +@@ -875,6 +875,7 @@ public final class CraftServer implements Server { + + org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot + com.destroystokyo.paper.PaperConfig.init((File) console.options.valueOf("paper-settings")); // Paper ++ com.tuinity.tuinity.config.TuinityConfig.init((File) console.options.valueOf("tuinity-settings")); // Tuinity - Server Config + for (ServerLevel world : this.console.getAllLevels()) { + world.serverLevelData.setDifficulty(config.difficulty); + world.setSpawnSettings(config.spawnMonsters, config.spawnAnimals); +@@ -909,6 +910,7 @@ public final class CraftServer implements Server { + } + world.spigotConfig.init(); // Spigot + world.paperConfig.init(); // Paper ++ world.tuinityConfig.init(); // Tuinity - Server Config + } + + Plugin[] pluginClone = pluginManager.getPlugins().clone(); // Paper +@@ -2374,6 +2376,14 @@ public final class CraftServer implements Server { + return com.destroystokyo.paper.PaperConfig.config; + } + ++ // Tuinity start - add config to timings report ++ @Override ++ public YamlConfiguration getTuinityConfig() ++ { ++ return com.tuinity.tuinity.config.TuinityConfig.config; ++ } ++ // Tuinity end - add config to timings report ++ + @Override + public void restart() { + org.spigotmc.RestartCommand.restart(); +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 3403b75c8311f1e52a0533363c5f0307442f8a15..92cb1fd2419eb3a3e64ebc0c5e699a79483f8c44 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -289,7 +289,7 @@ public class CraftWorld implements World { + public int getTileEntityCount() { + return net.minecraft.server.MCUtil.ensureMain(() -> { + // We don't use the full world tile entity list, so we must iterate chunks +- Long2ObjectLinkedOpenHashMap chunks = world.getChunkSource().chunkMap.visibleChunkMap; ++ Long2ObjectLinkedOpenHashMap chunks = world.getChunkSource().chunkMap.updatingChunks.getVisibleMap(); // Tuinity - change updating chunks map + int size = 0; + for (ChunkHolder playerchunk : chunks.values()) { + net.minecraft.world.level.chunk.LevelChunk chunk = playerchunk.getTickingChunk(); +@@ -312,7 +312,7 @@ public class CraftWorld implements World { + return net.minecraft.server.MCUtil.ensureMain(() -> { + int ret = 0; + +- for (ChunkHolder chunkHolder : world.getChunkSource().chunkMap.visibleChunkMap.values()) { ++ for (ChunkHolder chunkHolder : world.getChunkSource().chunkMap.updatingChunks.getVisibleMap().values()) { // Tuinity - change updating chunks map + if (chunkHolder.getTickingChunk() != null) { + ++ret; + } +@@ -351,13 +351,20 @@ public class CraftWorld implements World { + this.generator = gen; + + this.environment = env; ++ // Tuinity start - per world spawn limits ++ this.monsterSpawn = world.tuinityConfig.spawnLimitMonsters; ++ this.animalSpawn = world.tuinityConfig.spawnLimitAnimals; ++ this.waterAmbientSpawn = world.tuinityConfig.spawnLimitWaterAmbient; ++ this.waterAnimalSpawn = world.tuinityConfig.spawnLimitWaterAnimals; ++ this.ambientSpawn = world.tuinityConfig.spawnLimitAmbient; + // Paper start - per world spawn limits +- this.monsterSpawn = this.world.paperConfig.spawnLimitMonsters; +- this.animalSpawn = this.world.paperConfig.spawnLimitAnimals; +- this.waterAnimalSpawn = this.world.paperConfig.spawnLimitWaterAnimals; +- this.waterAmbientSpawn = this.world.paperConfig.spawnLimitWaterAmbient; +- this.ambientSpawn = this.world.paperConfig.spawnLimitAmbient; ++ if (this.monsterSpawn == -1) this.monsterSpawn = this.world.paperConfig.spawnLimitMonsters; ++ if (this.animalSpawn == -1) this.animalSpawn = this.world.paperConfig.spawnLimitAnimals; ++ if (this.waterAnimalSpawn == -1) this.waterAnimalSpawn = this.world.paperConfig.spawnLimitWaterAnimals; ++ if (this.waterAmbientSpawn == -1) this.waterAmbientSpawn = this.world.paperConfig.spawnLimitWaterAmbient; ++ if (this.ambientSpawn == -1) this.ambientSpawn = this.world.paperConfig.spawnLimitAmbient; + // Paper end ++ // Tuinity end - per world spawn limits + } + + @Override +@@ -431,14 +438,7 @@ public class CraftWorld implements World { + + @Override + public Chunk getChunkAt(int x, int z) { +- // Paper start - add ticket to hold chunk for a little while longer if plugin accesses it +- net.minecraft.world.level.chunk.LevelChunk chunk = world.getChunkSource().getChunkAtIfLoadedImmediately(x, z); +- if (chunk == null) { +- addTicket(x, z); +- chunk = this.world.getChunkSource().getChunk(x, z, true); +- } +- return chunk.bukkitChunk; +- // Paper end ++ return this.world.getChunkSource().getChunk(x, z, true).bukkitChunk; // Tuinity - revert paper diff + } + + // Paper start +@@ -486,13 +486,16 @@ public class CraftWorld implements World { + public Chunk[] getLoadedChunks() { + // Paper start + if (Thread.currentThread() != world.getLevel().thread) { +- synchronized (world.getChunkSource().chunkMap.visibleChunkMap) { +- Long2ObjectLinkedOpenHashMap chunks = world.getChunkSource().chunkMap.visibleChunkMap; +- return chunks.values().stream().map(ChunkHolder::getFullChunk).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.LevelChunk::getBukkitChunk).toArray(Chunk[]::new); ++ // Tuinity start - change updating chunks map ++ Long2ObjectLinkedOpenHashMap chunks; ++ synchronized (world.getChunkSource().chunkMap.updatingChunks) { ++ chunks = world.getChunkSource().chunkMap.updatingChunks.getVisibleMap().clone(); + } ++ return chunks.values().stream().map(ChunkHolder::getFullChunk).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.LevelChunk::getBukkitChunk).toArray(Chunk[]::new); ++ // Tuinity end - change updating chunks map + } + // Paper end +- Long2ObjectLinkedOpenHashMap chunks = this.world.getChunkSource().chunkMap.visibleChunkMap; ++ Long2ObjectLinkedOpenHashMap chunks = world.getChunkSource().chunkMap.updatingChunks.getVisibleMap(); // Tuinity - change updating chunks map + return chunks.values().stream().map(ChunkHolder::getFullChunk).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.LevelChunk::getBukkitChunk).toArray(Chunk[]::new); + } + +@@ -2671,7 +2674,7 @@ public class CraftWorld implements World { + // Paper end + return this.world.getChunkSource().getChunkAtAsynchronously(x, z, gen, urgent).thenComposeAsync((either) -> { + net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk) either.left().orElse(null); +- if (chunk != null) addTicket(x, z); // Paper ++ if (false && chunk != null) addTicket(x, z); // Paper // Tuinity - revert + return java.util.concurrent.CompletableFuture.completedFuture(chunk == null ? null : chunk.getBukkitChunk()); + }, net.minecraft.server.MinecraftServer.getServer()); + } +@@ -2696,14 +2699,14 @@ public class CraftWorld implements World { + throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]"); + } + net.minecraft.server.level.ChunkMap chunkMap = getHandle().getChunkSource().chunkMap; +- if (viewDistance != chunkMap.getEffectiveViewDistance()) { ++ if (true) { // Tuinity - replace old player chunk management + chunkMap.setViewDistance(viewDistance); + } + } + + @Override + public int getNoTickViewDistance() { +- return getHandle().getChunkSource().chunkMap.getEffectiveNoTickViewDistance(); ++ return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance(); // Tuinity - replace old player chunk management + } + + @Override +@@ -2712,11 +2715,22 @@ public class CraftWorld implements World { + throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]"); + } + net.minecraft.server.level.ChunkMap chunkMap = getHandle().getChunkSource().chunkMap; +- if (viewDistance != chunkMap.getRawNoTickViewDistance()) { ++ if (true) { // Tuinity - replace old player chunk management + chunkMap.setNoTickViewDistance(viewDistance); + } + } + // Paper end - per player view distance ++ // Tuinity start - add view distances ++ @Override ++ public int getSendViewDistance() { ++ return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance(); ++ } ++ ++ @Override ++ public void setSendViewDistance(int viewDistance) { ++ getHandle().getChunkSource().chunkMap.playerChunkManager.setTargetSendDistance(viewDistance); ++ } ++ // Tuinity end - add view distances + + // Spigot start + private final org.bukkit.World.Spigot spigot = new org.bukkit.World.Spigot() +diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java +index c3c7b34ceb1b8f0ed042b29924c633fa7519dc30..c59deadcfbfd5afbf951a167979a4eceb0c63579 100644 +--- a/src/main/java/org/bukkit/craftbukkit/Main.java ++++ b/src/main/java/org/bukkit/craftbukkit/Main.java +@@ -146,6 +146,13 @@ public class Main { + .defaultsTo(new File("paper.yml")) + .describedAs("Yml file"); + // Paper end ++ // Tuinity start - Server Config ++ acceptsAll(asList("tuinity", "tuinity-settings"), "File for tuinity settings") ++ .withRequiredArg() ++ .ofType(File.class) ++ .defaultsTo(new File("tuinity.yml")) ++ .describedAs("Yml file"); ++ // Tuinity end - Server Config + + // Paper start + acceptsAll(asList("server-name"), "Name of the server") +@@ -269,7 +276,7 @@ public class Main { + if (buildDate.before(deadline.getTime())) { + // Paper start - This is some stupid bullshit + System.err.println("*** Warning, you've not updated in a while! ***"); +- System.err.println("*** Please download a new build as per instructions from https://papermc.io/downloads ***"); // Paper ++ System.err.println("*** Please download a new build ***"); // Paper // Tuinity + //System.err.println("*** Server will start in 20 seconds ***"); + //Thread.sleep(TimeUnit.SECONDS.toMillis(20)); + // Paper End +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index 8246ad7ebecdfc0b7519fe4412fef7b07407e850..c0a508295d2e68d92ec8d24e14f9b7626911f548 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -517,27 +517,36 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + this.entity.setYHeadRot(yaw); + } + +- @Override// Paper start +- public java.util.concurrent.CompletableFuture teleportAsync(Location loc, @javax.annotation.Nonnull org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause) { +- net.minecraft.server.level.ChunkMap playerChunkMap = ((CraftWorld) loc.getWorld()).getHandle().getChunkSource().chunkMap; +- java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); +- +- loc.getWorld().getChunkAtAsyncUrgently(loc).thenCompose(chunk -> { +- net.minecraft.world.level.ChunkPos pair = new net.minecraft.world.level.ChunkPos(chunk.getX(), chunk.getZ()); +- ((CraftWorld) loc.getWorld()).getHandle().getChunkSource().addTicketAtLevel(TicketType.POST_TELEPORT, pair, 31, 0); +- net.minecraft.server.level.ChunkHolder updatingChunk = playerChunkMap.getUpdatingChunkIfPresent(pair.toLong()); +- if (updatingChunk != null) { +- return updatingChunk.getEntityTickingChunkFuture(); +- } else { +- return java.util.concurrent.CompletableFuture.completedFuture(com.mojang.datafixers.util.Either.left(((org.bukkit.craftbukkit.CraftChunk)chunk).getHandle())); ++ // Tuinity start - implement teleportAsync better ++ @Override ++ public java.util.concurrent.CompletableFuture teleportAsync(Location location, TeleportCause cause) { ++ Preconditions.checkArgument(location != null, "location"); ++ location.checkFinite(); ++ Location locationClone = location.clone(); // clone so we don't need to worry about mutations after this call. ++ ++ net.minecraft.server.level.ServerLevel world = ((CraftWorld)locationClone.getWorld()).getHandle(); ++ java.util.concurrent.CompletableFuture ret = new java.util.concurrent.CompletableFuture<>(); ++ ++ world.loadChunksForMoveAsync(getHandle().getBoundingBoxAt(locationClone.getX(), locationClone.getY(), locationClone.getZ()), location.getX(), location.getZ(), (list) -> { ++ net.minecraft.server.level.ServerChunkCache chunkProviderServer = world.getChunkSource(); ++ for (net.minecraft.world.level.chunk.ChunkAccess chunk : list) { ++ chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.POST_TELEPORT, chunk.getPos(), 33, CraftEntity.this.getEntityId()); + } +- }).thenAccept((chunk) -> future.complete(teleport(loc, cause))).exceptionally(ex -> { +- future.completeExceptionally(ex); +- return null; ++ net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { ++ try { ++ ret.complete(CraftEntity.this.teleport(locationClone, cause) ? Boolean.TRUE : Boolean.FALSE); ++ } catch (Throwable throwable) { ++ if (throwable instanceof ThreadDeath) { ++ throw (ThreadDeath)throwable; ++ } ++ ret.completeExceptionally(throwable); ++ } ++ }); + }); +- return future; ++ ++ return ret; + } +- // Paper end ++ // Tuinity end - implement teleportAsync better + + @Override + public boolean teleport(Location location) { +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 2e41a1bc20f257f4f461a74623a3ffe2d3112fdd..eb578c1deb8e48c4dc0bd334118136dbc7060334 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -516,15 +516,70 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + } + } + ++ // Tuinity start - implement view distances ++ @Override ++ public int getSendViewDistance() { ++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; ++ com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); ++ if (data == null) { ++ return chunkMap.playerChunkManager.getTargetSendDistance(); ++ } ++ return data.getTargetSendViewDistance(); ++ } ++ ++ @Override ++ public void setSendViewDistance(int viewDistance) { ++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; ++ com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); ++ if (data == null) { ++ throw new IllegalStateException("Player is not attached to world"); ++ } ++ ++ data.setTargetSendViewDistance(viewDistance); ++ } ++ ++ @Override ++ public int getNoTickViewDistance() { ++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; ++ com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); ++ if (data == null) { ++ return chunkMap.playerChunkManager.getTargetNoTickViewDistance(); ++ } ++ return data.getTargetNoTickViewDistance(); ++ } ++ ++ @Override ++ public void setNoTickViewDistance(int viewDistance) { ++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; ++ com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); ++ if (data == null) { ++ throw new IllegalStateException("Player is not attached to world"); ++ } ++ ++ data.setTargetNoTickViewDistance(viewDistance); ++ } ++ + @Override + public int getViewDistance() { +- throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO ++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; ++ com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); ++ if (data == null) { ++ return chunkMap.playerChunkManager.getTargetViewDistance(); ++ } ++ return data.getTargetTickViewDistance(); + } + + @Override + public void setViewDistance(int viewDistance) { +- throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO ++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; ++ com.tuinity.tuinity.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); ++ if (data == null) { ++ throw new IllegalStateException("Player is not attached to world"); ++ } ++ ++ data.setTargetTickViewDistance(viewDistance); + } ++ // Tuinity end - implement view distances + + @Override + public T getClientOption(com.destroystokyo.paper.ClientOption type) { +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java +index 2f3e2a404f55f09ae4db8261e495275e31228034..eb6cde923012d34a53a31f72b86870837e5f0824 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java +@@ -25,7 +25,10 @@ class CraftAsyncTask extends CraftTask { + @Override + public void run() { + final Thread thread = Thread.currentThread(); +- synchronized (this.workers) { ++ // Tuinity start - name threads according to running plugin ++ final String nameBefore = thread.getName(); ++ thread.setName(nameBefore + " - " + this.getOwner().getName()); ++ try { synchronized (this.workers) { // Tuinity end - name threads according to running plugin + if (getPeriod() == CraftTask.CANCEL) { + // Never continue running after cancelled. + // Checking this with the lock is important! +@@ -92,6 +95,7 @@ class CraftAsyncTask extends CraftTask { + } + } + } ++ } finally { thread.setName(nameBefore); } // Tuinity - name worker thread according + } + + LinkedList getWorkers() { +diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java +index 8ccfe9488db44d7d2cf4040a5b4cead33da1d5f4..d8c572b686c332eca722922c8a96d4629232856a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java ++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java +@@ -113,9 +113,18 @@ public final class CraftScoreboardManager implements ScoreboardManager { + + // CraftBukkit method + public void getScoreboardScores(ObjectiveCriteria criteria, String name, Consumer consumer) { ++ // Tuinity start - add timings for scoreboard search ++ // plugins leaking scoreboards will make this very expensive, let server owners debug it easily ++ co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.startTimingIfSync(); ++ try { ++ // Tuinity end - add timings for scoreboard search + for (CraftScoreboard scoreboard : this.scoreboards) { + Scoreboard board = scoreboard.board; + board.forAllObjectives(criteria, name, (score) -> consumer.accept(score)); + } ++ } finally { // Tuinity start - add timings for scoreboard search ++ co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.stopTimingIfSync(); ++ } ++ // Tuinity end - add timings for scoreboard search + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java +index a430506c31d9ce7a5c90d726a68f097498629545..e8c5109c36d437287e3eec23a5d1031f197a6162 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java +@@ -1,5 +1,6 @@ + package org.bukkit.craftbukkit.util; + ++import java.util.Collections; + import java.util.List; + import java.util.Random; + import java.util.function.Predicate; +@@ -234,4 +235,20 @@ public class DummyGeneratorAccess implements LevelAccessor { + public boolean destroyBlock(BlockPos pos, boolean drop, Entity breakingEntity, int maxUpdateDepth) { + return false; // SPIGOT-6515 + } ++ ++ // Tuinity start ++ @Override ++ public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { ++ return Collections.emptyList(); ++ } ++ ++ @Override ++ public void getEntities(Entity except, AABB box, Predicate predicate, List into) {} ++ ++ @Override ++ public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) {} ++ ++ @Override ++ public void getEntitiesByClass(Class clazz, Entity except, AABB box, List into, Predicate predicate) {} ++ // Tuinity end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java +index d40c0d8be1b0153d62021b8bcb6e8b37fd0acb4e..025540a62e805816cb93307c472bf0de64e2b01f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java +@@ -119,6 +119,32 @@ public class UnsafeList extends AbstractList implements List, RandomAcc + return this.indexOf(o) >= 0; + } + ++ // Tuinity start ++ protected transient int maxSize; ++ public void setSize(int size) { ++ if (this.maxSize < this.size) { ++ this.maxSize = this.size; ++ } ++ this.size = size; ++ } ++ ++ public void completeReset() { ++ if (this.data != null) { ++ Arrays.fill(this.data, 0, Math.max(this.size, this.maxSize), null); ++ } ++ this.size = 0; ++ this.maxSize = 0; ++ if (this.iterPool != null) { ++ for (Iterator temp : this.iterPool) { ++ if (temp == null) { ++ continue; ++ } ++ ((Itr)temp).valid = false; ++ } ++ } ++ } ++ // Tuinity end ++ + @Override + public void clear() { + // Create new array to reset memory usage to initial capacity +diff --git a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +index 774556a62eb240da42e84db4502e2ed43495be17..001b1e5197eaa51bfff9031aa6c69876c9a47960 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +@@ -11,7 +11,7 @@ public final class Versioning { + public static String getBukkitVersion() { + String result = "Unknown-Version"; + +- InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/io.papermc.paper/paper-api/pom.properties"); ++ InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/com.tuinity/tuinity-api/pom.properties"); // Tuinity + Properties properties = new Properties(); + + if (stream != null) { +diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java +index a08583863f9fa08016bdfc7949a273eaa4429927..966639cc6ba6684bfb52e91ac047808cf4d003e4 100644 +--- a/src/main/java/org/spigotmc/ActivationRange.java ++++ b/src/main/java/org/spigotmc/ActivationRange.java +@@ -205,7 +205,13 @@ public class ActivationRange + ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate( villagerActivationRange, 256, waterActivationRange ); + // Paper end + +- world.getEntities().get(maxBB, ActivationRange::activateEntity); ++ // Tuinity start ++ java.util.List entities = world.getEntities((Entity)null, maxBB, null); ++ for (int i = 0; i < entities.size(); i++) { ++ Entity entity = entities.get(i); ++ ActivationRange.activateEntity(entity); ++ } ++ // Tuinity end + } + MinecraftTimings.entityActivationCheckTimer.stopTiming(); + } +diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java +index 7a23b56752f6733ee626a8b1e4c3b78591855c4e..17151d9dc8c7030b54f1ba0956424d9d704c81ab 100644 +--- a/src/main/java/org/spigotmc/AsyncCatcher.java ++++ b/src/main/java/org/spigotmc/AsyncCatcher.java +@@ -10,8 +10,9 @@ public class AsyncCatcher + + public static void catchOp(String reason) + { +- if ( AsyncCatcher.enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread ) ++ if ( (AsyncCatcher.enabled || com.tuinity.tuinity.util.TickThread.STRICT_THREAD_CHECKS) && Thread.currentThread() != MinecraftServer.getServer().serverThread ) // Tuinity + { ++ MinecraftServer.LOGGER.fatal("Thread " + Thread.currentThread().getName() + " failed thread check for reason: Asynchronous " + reason, new Throwable()); // Tuinity - not all exceptions are printed + throw new IllegalStateException( "Asynchronous " + reason + "!" ); + } + } +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index dcfbe77bdb25d9c58ffb7b75c48bdb580bc0de47..fcd3917e0af080ae4726e7f511abf586df4dc7de 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -23,6 +23,78 @@ public class WatchdogThread extends Thread + private volatile long lastTick; + private volatile boolean stopping; + ++ // Tuinity start - log detailed tick information ++ private void dumpEntity(net.minecraft.world.entity.Entity entity) { ++ Logger log = Bukkit.getServer().getLogger(); ++ double posX, posY, posZ; ++ net.minecraft.world.phys.Vec3 mot; ++ double moveStartX, moveStartY, moveStartZ; ++ net.minecraft.world.phys.Vec3 moveVec; ++ synchronized (entity.posLock) { ++ posX = entity.getX(); ++ posY = entity.getY(); ++ posZ = entity.getZ(); ++ mot = entity.getDeltaMovement(); ++ moveStartX = entity.getMoveStartX(); ++ moveStartY = entity.getMoveStartY(); ++ moveStartZ = entity.getMoveStartZ(); ++ moveVec = entity.getMoveVector(); ++ } ++ ++ String entityType = entity.getMinecraftKey().toString(); ++ java.util.UUID entityUUID = entity.getUUID(); ++ net.minecraft.world.level.Level world = entity.level; ++ ++ log.log(Level.SEVERE, "Ticking entity: " + entityType + ", entity class: " + entity.getClass().getName()); ++ log.log(Level.SEVERE, "Entity status: removed: " + entity.isRemoved() + ", valid: " + entity.valid + ", alive: " + entity.isAlive() + ", is passenger: " + entity.isPassenger()); ++ log.log(Level.SEVERE, "Entity UUID: " + entityUUID); ++ log.log(Level.SEVERE, "Position: world: '" + (world == null ? "unknown world?" : world.getWorld().getName()) + "' at location (" + posX + ", " + posY + ", " + posZ + ")"); ++ log.log(Level.SEVERE, "Velocity: " + (mot == null ? "unknown velocity" : mot.toString()) + " (in blocks per tick)"); ++ log.log(Level.SEVERE, "Entity AABB: " + entity.getBoundingBox()); ++ if (moveVec != null) { ++ log.log(Level.SEVERE, "Move call information: "); ++ log.log(Level.SEVERE, "Start position: (" + moveStartX + ", " + moveStartY + ", " + moveStartZ + ")"); ++ log.log(Level.SEVERE, "Move vector: " + moveVec.toString()); ++ } ++ } ++ ++ private void dumpTickingInfo() { ++ Logger log = Bukkit.getServer().getLogger(); ++ ++ // ticking entities ++ for (net.minecraft.world.entity.Entity entity : net.minecraft.server.level.ServerLevel.getCurrentlyTickingEntities()) { ++ this.dumpEntity(entity); ++ net.minecraft.world.entity.Entity vehicle = entity.getVehicle(); ++ if (vehicle != null) { ++ log.log(Level.SEVERE, "Detailing vehicle for above entity:"); ++ this.dumpEntity(vehicle); ++ } ++ } ++ ++ // packet processors ++ for (net.minecraft.network.PacketListener packetListener : net.minecraft.network.protocol.PacketUtils.getCurrentPacketProcessors()) { ++ if (packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl) { ++ net.minecraft.server.level.ServerPlayer player = ((net.minecraft.server.network.ServerGamePacketListenerImpl)packetListener).player; ++ long totalPackets = net.minecraft.network.protocol.PacketUtils.getTotalProcessedPackets(); ++ if (player == null) { ++ log.log(Level.SEVERE, "Handling packet for player connection or ticking player connection (null player): " + packetListener); ++ log.log(Level.SEVERE, "Total packets processed on the main thread for all players: " + totalPackets); ++ } else { ++ this.dumpEntity(player); ++ net.minecraft.world.entity.Entity vehicle = player.getVehicle(); ++ if (vehicle != null) { ++ log.log(Level.SEVERE, "Detailing vehicle for above entity:"); ++ this.dumpEntity(vehicle); ++ } ++ log.log(Level.SEVERE, "Total packets processed on the main thread for all players: " + totalPackets); ++ } ++ } else { ++ log.log(Level.SEVERE, "Handling packet for connection: " + packetListener); ++ } ++ } ++ } ++ // Tuinity end - log detailed tick information ++ + private WatchdogThread(long timeoutTime, boolean restart) + { + super( "Paper Watchdog Thread" ); +@@ -121,6 +193,7 @@ public class WatchdogThread extends Thread + log.log( Level.SEVERE, "------------------------------" ); + log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper + com.destroystokyo.paper.io.chunk.ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper ++ this.dumpTickingInfo(); // Tuinity - log detailed tick information + WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( server.serverThread.getId(), Integer.MAX_VALUE ), log ); + log.log( Level.SEVERE, "------------------------------" ); + // diff --git a/src/main/resources/logo.png b/src/main/resources/logo.png index a7d785f60c884ee4ee487cc364402d66c3dc2ecc..1e6ee83b1a207eca59d82b25c06895ce894e8173 100644 GIT binary patch diff --git a/patches/server/0002-Rebrand.patch b/patches/server/0002-Rebrand.patch index cabb882a1..9af1812b8 100644 --- a/patches/server/0002-Rebrand.patch +++ b/patches/server/0002-Rebrand.patch @@ -50,36 +50,36 @@ index e0b1f0671d16ddddcb6725acd25a1d1d69e42701..8c3c68465197fafc14849dc38a572e30 .completer(new ConsoleCommandCompleter(this.server)) .option(LineReader.Option.COMPLETE_IN_WORD, true); diff --git a/src/main/java/net/minecraft/server/Eula.java b/src/main/java/net/minecraft/server/Eula.java -index a1d5c0f8fe2adb2ee56f3217e089211ec7c61eb0..43a88bc58716eef4040584944f52893c5a47699f 100644 +index 3789df8ef9c0b4150c3baccf84dbaff57c0fb65a..43a88bc58716eef4040584944f52893c5a47699f 100644 --- a/src/main/java/net/minecraft/server/Eula.java +++ b/src/main/java/net/minecraft/server/Eula.java @@ -64,7 +64,7 @@ public class Eula { try { Properties properties = new Properties(); properties.setProperty("eula", "false"); -- properties.store(outputStream, "By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula).\nYou also agree that tacos are tasty, and the best food in the world."); // Paper - fix lag; +- properties.store(outputStream, "By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula)."); // Paper - fix lag; // Tuinity - Tacos are disgusting + properties.store(outputStream, "By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula).\nYou also agree that tacos are tasty, and the best food in the world."); // Paper - fix lag; // Tuinity - Tacos are disgusting // Purpur - no they're not } catch (Throwable var5) { if (outputStream != null) { try { diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 3dded5c491ace6b073a7bc3178976bd70f0b9393..1164e6a63e895818b3645b31588e27ae221e4859 100644 +index f25bb4214cffd0050241ea229b6acb0c16b2b0a5..be1bc7fda4104d61f91c2815c6ba3c612a019bed 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1651,7 +1651,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop // Spigot - Spigot > // CraftBukkit - cb > vanilla! +- return "Tuinity"; // Tuinity - Tuinity > //Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! + return "Purpur"; // Purpur // Tuinity // Paper // Spigot // CraftBukkit } public SystemReport fillSystemReport(SystemReport details) { diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java -index 6d7eef79de7a899ccdbc3194d925bb4caa0a4b03..2c6b0018727409f7d56081cb207174f5dc37a6a7 100644 +index 9667a74c9b77ea6acd9d2ebce30c685ed4b53e59..1749d134260adcb33d1757630c6ba2fdc43d2e6d 100644 --- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java -@@ -281,11 +281,12 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface +@@ -282,11 +282,12 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface DedicatedServer.LOGGER.warn("**** SERVER IS RUNNING IN OFFLINE/INSECURE MODE!"); DedicatedServer.LOGGER.warn("The server will make no attempt to authenticate usernames. Beware."); // Spigot start @@ -220,14 +220,14 @@ index 0000000000000000000000000000000000000000..cabfcebf9f944f7a2a2a1cffc7401435 + } +} diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index c79b193ad822b8c246f24a87cd418892bc18ff5a..977d4c6d3b770caef9a619978063b2e7a8ecac82 100644 +index 4342bc5aad49fe372d561296a6b63818a443d089..ce46da5d75049ad2ec2db677569e35f13960dafa 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -230,7 +230,7 @@ import javax.annotation.Nullable; // Paper import javax.annotation.Nonnull; // Paper public final class CraftServer implements Server { -- private final String serverName = "Paper"; // Paper +- private final String serverName = "Tuinity"; // Tuinity // Paper + private final String serverName = "Purpur"; // Paper // Tuinity // Purpur private final String serverVersion; private final String bukkitVersion = Versioning.getBukkitVersion(); @@ -246,14 +246,14 @@ index 7a8db8d481e9487ea83a640af208242f4987ad28..82d56d8c4616f1012e70697dae8d1d31 @Override diff --git a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java -index 774556a62eb240da42e84db4502e2ed43495be17..13b98439320ac1401a920c01d7cf5a4b3a23deff 100644 +index 001b1e5197eaa51bfff9031aa6c69876c9a47960..13b98439320ac1401a920c01d7cf5a4b3a23deff 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +++ b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java @@ -11,7 +11,7 @@ public final class Versioning { public static String getBukkitVersion() { String result = "Unknown-Version"; -- InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/io.papermc.paper/paper-api/pom.properties"); +- InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/com.tuinity/tuinity-api/pom.properties"); // Tuinity + InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/net.pl3x.purpur/purpur-api/pom.properties"); // Tuinity // Purpur Properties properties = new Properties(); diff --git a/patches/server/0003-Purpur-config-files.patch b/patches/server/0003-Purpur-config-files.patch index c629397b5..b7e48e55c 100644 --- a/patches/server/0003-Purpur-config-files.patch +++ b/patches/server/0003-Purpur-config-files.patch @@ -5,14 +5,14 @@ Subject: [PATCH] Purpur config files diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java -index 218f5bafeed8551b55b91c7fccaf6935c8b631ca..4d8740678049aa749b42618470e9cc838555528d 100644 +index 3918b24c98faa5232c7ffd733ba8000562132785..4d8740678049aa749b42618470e9cc838555528d 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 metrics = new Metrics("Purpur", serverUUID, logFailedRequests, Bukkit.getLogger()); // Purpur metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { @@ -22,7 +22,7 @@ index 218f5bafeed8551b55b91c7fccaf6935c8b631ca..4d8740678049aa749b42618470e9cc83 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.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : (PaperConfig.isProxyOnlineMode() ? "bungee" : "offline"))); // Purpur + metrics.addCustomChart(new Metrics.SimplePie("purpur_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown")); // Purpur @@ -80,7 +80,7 @@ index 134bb2a4826419110c10a483834747b942576e58..d9e868b6c70da18b4ce23c80e2aaf347 if (this.source.acceptsSuccess() && !this.silent) { this.source.sendMessage(message, Util.NIL_UUID); diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java -index 2c6b0018727409f7d56081cb207174f5dc37a6a7..1f9cc47bac2cd8344ecc355c22101fc48b13fb62 100644 +index 1749d134260adcb33d1757630c6ba2fdc43d2e6d..1b8d836607d52c3bc67ad5f2accbc94663637d49 100644 --- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java @@ -219,6 +219,15 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface @@ -100,22 +100,23 @@ index 2c6b0018727409f7d56081cb207174f5dc37a6a7..1f9cc47bac2cd8344ecc355c22101fc4 io.papermc.paper.brigadier.PaperBrigadierProviderImpl.INSTANCE.getClass(); // init PaperBrigadierProvider io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.getClass(); // load mappings for stacktrace deobf diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 474078b68f1bf22037495f42bae59b790fd8cb46..bda7cf2b8431f8541cba98e2a641dad03e736fa8 100644 +index 61a4dea715689b0ce9247040db5dd2080ee2e167..ffe76b8afd2a2c3153751c73ee7bbf4c9351e12c 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -166,7 +166,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { +@@ -166,8 +166,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public final com.destroystokyo.paper.PaperWorldConfig paperConfig; // Paper public final com.destroystokyo.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray - + public final com.tuinity.tuinity.config.TuinityConfig.WorldConfig tuinityConfig; // Tuinity - Server Config + public final net.pl3x.purpur.PurpurWorldConfig purpurConfig; // Purpur + public final co.aikar.timings.WorldTimingsHandler timings; // Paper public static BlockPos lastPhysicsProblem; // Spigot - private org.spigotmc.TickLimiter entityLimiter; -@@ -206,6 +206,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, final DimensionType dimensionmanager, Supplier supplier, boolean flag, boolean flag1, long i, org.bukkit.generator.ChunkGenerator gen, org.bukkit.World.Environment env, java.util.concurrent.Executor executor) { // Paper - Anti-Xray - Pass executor +@@ -316,6 +316,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot this.paperConfig = new com.destroystokyo.paper.PaperWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName(), this.spigotConfig); // Paper + this.tuinityConfig = new com.tuinity.tuinity.config.TuinityConfig.WorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData)worlddatamutable).getLevelName()); // Tuinity - Server Config + this.purpurConfig = new net.pl3x.purpur.PurpurWorldConfig((ServerLevel) this, ((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName(), env); // Purpur this.generator = gen; this.world = new CraftWorld((ServerLevel) this, gen, env); @@ -455,26 +456,26 @@ index 0000000000000000000000000000000000000000..6e7f56fe2b78d7a09d5d130f2c88338f + } +} diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index 977d4c6d3b770caef9a619978063b2e7a8ecac82..9123ae3a99efb80bc938e013a4eff4259cec1084 100644 +index ce46da5d75049ad2ec2db677569e35f13960dafa..5a9e6b904893be6782a50b89582e6a8e7e3ef7e7 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -875,6 +875,7 @@ public final class CraftServer implements Server { - +@@ -876,6 +876,7 @@ public final class CraftServer implements Server { org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot com.destroystokyo.paper.PaperConfig.init((File) console.options.valueOf("paper-settings")); // Paper + com.tuinity.tuinity.config.TuinityConfig.init((File) console.options.valueOf("tuinity-settings")); // Tuinity - Server Config + net.pl3x.purpur.PurpurConfig.init((File) console.options.valueOf("purpur-settings")); // Purpur for (ServerLevel world : this.console.getAllLevels()) { world.serverLevelData.setDifficulty(config.difficulty); world.setSpawnSettings(config.spawnMonsters, config.spawnAnimals); -@@ -909,6 +910,7 @@ public final class CraftServer implements Server { - } +@@ -911,6 +912,7 @@ public final class CraftServer implements Server { world.spigotConfig.init(); // Spigot world.paperConfig.init(); // Paper + world.tuinityConfig.init(); // Tuinity - Server Config + world.purpurConfig.init(); // Purpur } Plugin[] pluginClone = pluginManager.getPlugins().clone(); // Paper -@@ -924,6 +926,7 @@ public final class CraftServer implements Server { +@@ -926,6 +928,7 @@ public final class CraftServer implements Server { this.reloadData(); org.spigotmc.SpigotConfig.registerCommands(); // Spigot com.destroystokyo.paper.PaperConfig.registerCommands(); // Paper @@ -482,9 +483,9 @@ index 977d4c6d3b770caef9a619978063b2e7a8ecac82..9123ae3a99efb80bc938e013a4eff425 this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*"); this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions"); -@@ -2374,6 +2377,18 @@ public final class CraftServer implements Server { - return com.destroystokyo.paper.PaperConfig.config; +@@ -2384,6 +2387,18 @@ public final class CraftServer implements Server { } + // Tuinity end - add config to timings report + // Purpur start + @Override @@ -502,12 +503,12 @@ index 977d4c6d3b770caef9a619978063b2e7a8ecac82..9123ae3a99efb80bc938e013a4eff425 public void restart() { org.spigotmc.RestartCommand.restart(); diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java -index c3c7b34ceb1b8f0ed042b29924c633fa7519dc30..910ffb563d66ce3d5a3ea562fd1b9d6d2030cccb 100644 +index c59deadcfbfd5afbf951a167979a4eceb0c63579..a335d1689ebf01e0e96a45c640188dc024610e2c 100644 --- a/src/main/java/org/bukkit/craftbukkit/Main.java +++ b/src/main/java/org/bukkit/craftbukkit/Main.java -@@ -147,6 +147,14 @@ public class Main { +@@ -154,6 +154,14 @@ public class Main { .describedAs("Yml file"); - // Paper end + // Tuinity end - Server Config + // Purpur Start + acceptsAll(asList("purpur", "purpur-settings"), "File for purpur settings") diff --git a/patches/server/0004-Component-related-conveniences.patch b/patches/server/0004-Component-related-conveniences.patch index 7aead1d89..2e36b6d1c 100644 --- a/patches/server/0004-Component-related-conveniences.patch +++ b/patches/server/0004-Component-related-conveniences.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Component related conveniences diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 8e2bccc3a9ddb17a4978596056189eb776976338..cf06e4f51984a64c294f402c62a03214eea0e5c3 100644 +index dcba69c0ad3288ddc64dacc58b6fb857eed3109c..57200e6b419ab0793df6498467325b6d5690c17f 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -1661,6 +1661,26 @@ public class ServerPlayer extends Player { +@@ -1673,6 +1673,26 @@ public class ServerPlayer extends Player { } // CraftBukkit end @@ -36,10 +36,10 @@ index 8e2bccc3a9ddb17a4978596056189eb776976338..cf06e4f51984a64c294f402c62a03214 public void displayClientMessage(Component message, boolean actionBar) { this.sendMessage(message, actionBar ? ChatType.GAME_INFO : ChatType.CHAT, Util.NIL_UUID); diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 65657c009f6d5a5d5740e80f912a5893333c7085..c647a768a10076969f81fed33cefb7a4aa3b8597 100644 +index 2fd0a432ed52ca137622a1f631e886aaf77f33d3..a8a1affc747aee30c227d79f563facb8503d9c5e 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -1390,6 +1390,62 @@ public abstract class PlayerList { +@@ -1391,6 +1391,62 @@ public abstract class PlayerList { } // CraftBukkit end @@ -103,10 +103,10 @@ index 65657c009f6d5a5d5740e80f912a5893333c7085..c647a768a10076969f81fed33cefb7a4 this.server.sendMessage(message, sender); Iterator iterator = this.players.iterator(); diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 5ffc9d02ee10e9efe653e124b1eb9cbd0adc3df9..042da7707f7fd01a6195be15f97557d049ded22a 100644 +index e75efd67acb063e3ce7506839e4a888241bda703..6800ba596ce3decfe268ccf897d7951a233e730d 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -3376,6 +3376,34 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -3565,6 +3565,34 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n return SlotAccess.NULL; } diff --git a/patches/server/0005-Ridables.patch b/patches/server/0005-Ridables.patch index 03a2bb9bc..c9c49cf75 100644 --- a/patches/server/0005-Ridables.patch +++ b/patches/server/0005-Ridables.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Ridables diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java -index b70aa66732fb5e957aed0901f4c76358b2c56f8e..c281018818e5f27e23a155565eb2130fcd16a295 100644 +index b01d7da333bac7820e42b6f645634a15ef88ae4f..30a4b80eacf76ad7f0a48b79bcf01b752c0af48a 100644 --- a/src/main/java/net/minecraft/core/BlockPos.java +++ b/src/main/java/net/minecraft/core/BlockPos.java @@ -41,6 +41,12 @@ public class BlockPos extends Vec3i { @@ -22,10 +22,10 @@ index b70aa66732fb5e957aed0901f4c76358b2c56f8e..c281018818e5f27e23a155565eb2130f super(x, y, z); } diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 1164e6a63e895818b3645b31588e27ae221e4859..745df03f3b425a247d63d7d896c3dcf2c8baa0e0 100644 +index be1bc7fda4104d61f91c2815c6ba3c612a019bed..2ede76a55c72840d915ed282609b1ca14f549929 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1532,6 +1532,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper @@ -34,10 +34,10 @@ index 1164e6a63e895818b3645b31588e27ae221e4859..745df03f3b425a247d63d7d896c3dcf2 this.profiler.push(() -> { diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 6ecf60c69a27f8db1c245db15449bba581c3dbf5..a02b0d819392db96a370c57818a378dd5fa27ef4 100644 +index 7642170bf5a0eaa11110238fa5cf1a7e1ff20a20..d575fcc1aae45c810c21fd8c112e3e63cdaf7d71 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -201,6 +201,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl +@@ -205,6 +205,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl public final UUID uuid; public boolean hasPhysicsEvent = true; // Paper public boolean hasEntityMoveEvent = false; // Paper @@ -46,7 +46,7 @@ index 6ecf60c69a27f8db1c245db15449bba581c3dbf5..a02b0d819392db96a370c57818a378dd return new Throwable(entity + " Added to world at " + new java.util.Date()); } diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index cf06e4f51984a64c294f402c62a03214eea0e5c3..d9df08cdf065b0316f43ca53988ccbe119e5106f 100644 +index 57200e6b419ab0793df6498467325b6d5690c17f..bd787b4122502eacacadd8d47ea5aa5dc5e023e5 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -664,6 +664,15 @@ public class ServerPlayer extends Player { @@ -65,7 +65,7 @@ index cf06e4f51984a64c294f402c62a03214eea0e5c3..d9df08cdf065b0316f43ca53988ccbe1 } public void doTick() { -@@ -2398,4 +2407,6 @@ public class ServerPlayer extends Player { +@@ -2410,4 +2419,6 @@ public class ServerPlayer extends Player { return (CraftPlayer) super.getBukkitEntity(); } // CraftBukkit end @@ -73,10 +73,10 @@ index cf06e4f51984a64c294f402c62a03214eea0e5c3..d9df08cdf065b0316f43ca53988ccbe1 + } diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index 6ff04fc7202a7eb1f2b5978a2e2a573945d9dde1..dbb5f0bd721b2490f727a0b2462cf215e729947d 100644 +index 53e9ba24338690b1c5f12250748273758c68f4d1..26a391223dc1595892cb8bee4815525adb593d32 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -2329,6 +2329,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser +@@ -2407,6 +2407,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser ServerGamePacketListenerImpl.this.cserver.getPluginManager().callEvent(event); @@ -86,7 +86,7 @@ index 6ff04fc7202a7eb1f2b5978a2e2a573945d9dde1..dbb5f0bd721b2490f727a0b2462cf215 if ((entity instanceof AbstractFish && origItem != null && origItem.asItem() == Items.WATER_BUCKET) && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem)) { ServerGamePacketListenerImpl.this.send(new ClientboundAddMobPacket((AbstractFish) entity)); diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 042da7707f7fd01a6195be15f97557d049ded22a..081506649de3929becd448664e2c5b0d7b4859fc 100644 +index 6800ba596ce3decfe268ccf897d7951a233e730d..d40b224c0d8397d3876c605e27275756d927f843 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -230,7 +230,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n @@ -107,7 +107,7 @@ index 042da7707f7fd01a6195be15f97557d049ded22a..081506649de3929becd448664e2c5b0d private float eyeHeight; public boolean isInPowderSnow; public boolean wasInPowderSnow; -@@ -2412,6 +2412,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -2601,6 +2601,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n this.passengers = ImmutableList.copyOf(list); } @@ -120,7 +120,7 @@ index 042da7707f7fd01a6195be15f97557d049ded22a..081506649de3929becd448664e2c5b0d } return true; // CraftBukkit } -@@ -2452,6 +2458,14 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -2641,6 +2647,14 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n return false; } // Spigot end @@ -135,7 +135,7 @@ index 042da7707f7fd01a6195be15f97557d049ded22a..081506649de3929becd448664e2c5b0d if (this.passengers.size() == 1 && this.passengers.get(0) == entity) { this.passengers = ImmutableList.of(); } else { -@@ -4015,4 +4029,41 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -4208,4 +4222,41 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n return ((ServerChunkCache) level.getChunkSource()).isPositionTicking(this); } // Paper end @@ -262,7 +262,7 @@ index ed745776316c5346ee1b44c8f022c40359b7e642..911a6f63e8ffeacc95fa49cdf99140c1 // Paper end if (!this.level.isClientSide && this.isSensitiveToWater() && this.isInWaterRainOrBubble()) { diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java -index 0ce0e7a923da812a02d9ab83607d3cc9c87047df..a597a09b428cb2b3dc7fef5503ef705cf7b2a1fb 100644 +index f8c6d88d6bf71e7bc46b5f44e688229da5fd3da2..2a310cb6cc1bfbf0c8d65c96b6bdbe8450d7c3ce 100644 --- a/src/main/java/net/minecraft/world/entity/Mob.java +++ b/src/main/java/net/minecraft/world/entity/Mob.java @@ -140,6 +140,8 @@ public abstract class Mob extends LivingEntity { @@ -274,7 +274,7 @@ index 0ce0e7a923da812a02d9ab83607d3cc9c87047df..a597a09b428cb2b3dc7fef5503ef705c this.jumpControl = new JumpControl(this); this.bodyRotationControl = this.createBodyControl(); this.navigation = this.createNavigation(world); -@@ -1274,7 +1276,7 @@ public abstract class Mob extends LivingEntity { +@@ -1279,7 +1281,7 @@ public abstract class Mob extends LivingEntity { protected void onOffspringSpawnedFromEgg(Player player, Mob child) {} protected InteractionResult mobInteract(Player player, InteractionHand hand) { @@ -283,7 +283,7 @@ index 0ce0e7a923da812a02d9ab83607d3cc9c87047df..a597a09b428cb2b3dc7fef5503ef705c } public boolean isWithinRestriction() { -@@ -1633,4 +1635,52 @@ public abstract class Mob extends LivingEntity { +@@ -1638,4 +1640,52 @@ public abstract class Mob extends LivingEntity { return itemmonsteregg == null ? null : new ItemStack(itemmonsteregg); } @@ -1895,7 +1895,7 @@ index 84015b5bf4b9a11670ad4d984844a431876efb63..5f61fcffebf4d853711a38d1f315f3de return "entity.minecraft.tropical_fish.predefined." + variant; } diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java -index 925f16d5eb092518ef774f69a8d99689feb0f5d7..c86f13d190b41cb18dd833af39c7b4916068fd69 100644 +index 01d8af06f19427354cac95d691e65d31253fef94..631539a752a038926355c23aeb160af64f363a61 100644 --- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java +++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java @@ -90,6 +90,18 @@ public class Turtle extends Animal { @@ -1915,7 +1915,7 @@ index 925f16d5eb092518ef774f69a8d99689feb0f5d7..c86f13d190b41cb18dd833af39c7b491 + // Purpur end + public void setHomePos(BlockPos pos) { - this.entityData.set(Turtle.HOME_POS, pos); + this.entityData.set(Turtle.HOME_POS, pos.immutable()); // Paper - called with mutablepos... } @@ -192,6 +204,7 @@ public class Turtle extends Animal { @@ -4472,7 +4472,7 @@ index c4f7c94255e4631a3c0355f9260132ba28296f50..d6c31596e21041a124a263054ccb6447 this.setTradingPlayer(player); this.openTradingScreen(player, this.getDisplayName(), 1); diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java -index 19980b2d627eb3cacf8d0c3e6785ad2206910fbc..dd7e76f9e5fd05a38507f32b3e021efe43315049 100644 +index e7a7de5ad9b64876df77e20465631ca8e5b19a4a..7b4f41d53373a56ad50cf4a9a761d87612600da7 100644 --- a/src/main/java/net/minecraft/world/entity/player/Player.java +++ b/src/main/java/net/minecraft/world/entity/player/Player.java @@ -193,6 +193,19 @@ public abstract class Player extends LivingEntity { @@ -5718,10 +5718,10 @@ index 0000000000000000000000000000000000000000..8eefb7b7eb33aecf48ac206d3f0139e0 + } +} diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -index 8246ad7ebecdfc0b7519fe4412fef7b07407e850..6b85ba7d9bad9f648b4a6cb5a3938509b3e73cca 100644 +index c0a508295d2e68d92ec8d24e14f9b7626911f548..edc08af4ec2ce6e90c30da286c0ba5ac16efd3fc 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -@@ -1207,4 +1207,27 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { +@@ -1216,4 +1216,27 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { return getHandle().isTicking(); } // Paper end diff --git a/patches/server/0007-Timings-stuff.patch b/patches/server/0007-Timings-stuff.patch index f540946f0..26a568013 100644 --- a/patches/server/0007-Timings-stuff.patch +++ b/patches/server/0007-Timings-stuff.patch @@ -5,26 +5,27 @@ Subject: [PATCH] Timings stuff diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java -index 2ff4d4921e2076abf415bd3c8f5173ecd6222168..dd9efe48006d060397ef3932afa3665f89ea99b3 100644 +index 9d920565ff65a84b1b9a2a4777fd8bc8f07e0153..69d85e5a0a5ab9ffb682d8565aa08a4a567b68da 100644 --- a/src/main/java/co/aikar/timings/TimingsExport.java +++ b/src/main/java/co/aikar/timings/TimingsExport.java -@@ -226,9 +226,14 @@ public class TimingsExport extends Thread { +@@ -226,10 +226,14 @@ public class TimingsExport extends Thread { // Information on the users Config parent.put("config", createObject( +- pair("spigot", mapAsJSON(Bukkit.spigot().getSpigotConfig(), null)), + // Purpur start + pair("server.properties", mapAsJSON(Bukkit.spigot().getServerProperties())), -+ pair("bukkit", mapAsJSON(Bukkit.spigot().getBukkitConfig(), null)), - 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("spigot", mapAsJSON(Bukkit.spigot().getSpigotConfig(), 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 ++ pair("tuinity", mapAsJSON(Bukkit.spigot().getTuinityConfig(), null)), // Tuinity - add config to timings report + pair("purpur", mapAsJSON(Bukkit.spigot().getPurpurConfig(), null)) + // Purpur end )); new TimingsExport(listeners, parent, history).start(); -@@ -269,6 +274,19 @@ public class TimingsExport extends Thread { +@@ -270,6 +274,19 @@ public class TimingsExport extends Thread { return timingsCost; } @@ -44,7 +45,7 @@ index 2ff4d4921e2076abf415bd3c8f5173ecd6222168..dd9efe48006d060397ef3932afa3665f private static JSONObject mapAsJSON(ConfigurationSection config, String parentKey) { JSONObject object = new JSONObject(); -@@ -305,7 +323,7 @@ public class TimingsExport extends Thread { +@@ -306,7 +323,7 @@ public class TimingsExport extends Thread { String response = null; String timingsURL = null; try { diff --git a/patches/server/0011-AFK-API.patch b/patches/server/0011-AFK-API.patch index baf55d17b..3513e7fb5 100644 --- a/patches/server/0011-AFK-API.patch +++ b/patches/server/0011-AFK-API.patch @@ -5,10 +5,10 @@ Subject: [PATCH] AFK API diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index d9df08cdf065b0316f43ca53988ccbe119e5106f..687f1bfa07246e2297be7124edf5bf7568a7ce09 100644 +index bd787b4122502eacacadd8d47ea5aa5dc5e023e5..22366098d0a3f6df2ba650ef01ed4be77bee0496 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -1936,8 +1936,58 @@ public class ServerPlayer extends Player { +@@ -1948,8 +1948,58 @@ public class ServerPlayer extends Player { public void resetLastActionTime() { this.lastActionTime = Util.getMillis(); @@ -68,7 +68,7 @@ index d9df08cdf065b0316f43ca53988ccbe119e5106f..687f1bfa07246e2297be7124edf5bf75 return this.stats; } diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index dbb5f0bd721b2490f727a0b2462cf215e729947d..c5863036c0fdeee755494dfc55c527f4696c92c1 100644 +index 26a391223dc1595892cb8bee4815525adb593d32..ff5e61ba0cb9602a332dfc59a40b2a9776acaef4 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -390,6 +390,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser @@ -84,7 +84,7 @@ index dbb5f0bd721b2490f727a0b2462cf215e729947d..c5863036c0fdeee755494dfc55c527f4 this.player.resetLastActionTime(); // CraftBukkit - SPIGOT-854 this.disconnect(new TranslatableComponent("multiplayer.disconnect.idling"), org.bukkit.event.player.PlayerKickEvent.Cause.IDLING); // Paper - kick event cause } -@@ -648,6 +654,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser +@@ -662,6 +668,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser this.lastYaw = to.getYaw(); this.lastPitch = to.getPitch(); @@ -93,18 +93,16 @@ index dbb5f0bd721b2490f727a0b2462cf215e729947d..c5863036c0fdeee755494dfc55c527f4 // Skip the first time we do this if (true) { // Spigot - don't skip any move events Location oldTo = to.clone(); -@@ -1373,8 +1381,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser - boolean flag1 = false; +@@ -1420,7 +1428,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser if (!this.player.isChangingDimension() && d11 > org.spigotmc.SpigotConfig.movedWronglyThreshold && !this.player.isSleeping() && !this.player.gameMode.isCreative() && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR) { // Spigot -- flag1 = true; + flag1 = true; // Tuinity - diff on change, this should be moved wrongly - ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!", this.player.getName().getString()); -+ flag1 = true; // Tuinity - diff on change, this should be moved wrongly + ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!, ({})", this.player.getName().getString(), d11); // Purpur } this.player.absMoveTo(d0, d1, d2, f, f1); -@@ -1413,6 +1421,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser +@@ -1470,6 +1478,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser this.lastYaw = to.getYaw(); this.lastPitch = to.getPitch(); @@ -148,7 +146,7 @@ index a060cca08631fb42041e3a79a9abc422fe7757af..e7b11d1ba984ea14f0cdf8e84f9eaab4 private EntitySelector() {} // Paper start diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java -index dd7e76f9e5fd05a38507f32b3e021efe43315049..995098c7e7eaa15fa1cda40fdb763cf4607f3381 100644 +index 7b4f41d53373a56ad50cf4a9a761d87612600da7..d6b610a848f37db24af9b219be2f22aeaf892388 100644 --- a/src/main/java/net/minecraft/world/entity/player/Player.java +++ b/src/main/java/net/minecraft/world/entity/player/Player.java @@ -196,6 +196,13 @@ public abstract class Player extends LivingEntity { @@ -166,10 +164,10 @@ index dd7e76f9e5fd05a38507f32b3e021efe43315049..995098c7e7eaa15fa1cda40fdb763cf4 public boolean processClick(InteractionHand hand) { Entity vehicle = getRootVehicle(); diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java -index 325e244c46ec208a2e7e18d71ccbbfcc25fc1bce..3645ebf52ad1461937ce6cc0cf38a92176627227 100644 +index 6a4e44dd8935018d1b5283761dfb8e855be62987..afe70b0d5bd98d05bbb7afc756108f09d35a5848 100644 --- a/src/main/java/net/minecraft/world/level/EntityGetter.java +++ b/src/main/java/net/minecraft/world/level/EntityGetter.java -@@ -145,7 +145,7 @@ public interface EntityGetter { +@@ -161,7 +161,7 @@ public interface EntityGetter { default boolean hasNearbyAlivePlayer(double x, double y, double z, double range) { for(Player player : this.players()) { @@ -222,7 +220,7 @@ index 332754f541165590ecd96d650f16f40924b4263a..bfc1077dc7d5cfb48f5cc98b289b98d4 public boolean untamedTamablesAreRidable = true; public boolean useNightVisionWhenRiding = false; diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index 2e41a1bc20f257f4f461a74623a3ffe2d3112fdd..d858590b7ffea28dcfbc48973df6e3c916886b65 100644 +index eb578c1deb8e48c4dc0bd334118136dbc7060334..03381a67c15153162f70dc7681e06b4a7378ae9d 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -423,10 +423,15 @@ public class CraftPlayer extends CraftHumanEntity implements Player { @@ -242,7 +240,7 @@ index 2e41a1bc20f257f4f461a74623a3ffe2d3112fdd..d858590b7ffea28dcfbc48973df6e3c9 for (ServerPlayer player : (List) server.getHandle().players) { if (player.getBukkitEntity().canSee(this)) { player.connection.send(new ClientboundPlayerInfoPacket(ClientboundPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME, this.getHandle())); -@@ -2475,4 +2480,21 @@ public class CraftPlayer extends CraftHumanEntity implements Player { +@@ -2530,4 +2535,21 @@ public class CraftPlayer extends CraftHumanEntity implements Player { return this.spigot; } // Spigot end @@ -265,7 +263,7 @@ index 2e41a1bc20f257f4f461a74623a3ffe2d3112fdd..d858590b7ffea28dcfbc48973df6e3c9 + // Purpur end } diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java -index a08583863f9fa08016bdfc7949a273eaa4429927..f36c97529edbd3642d0ba37887a232226f766a35 100644 +index 966639cc6ba6684bfb52e91ac047808cf4d003e4..2941a30967bae072f6726587714e1cba694b9efa 100644 --- a/src/main/java/org/spigotmc/ActivationRange.java +++ b/src/main/java/org/spigotmc/ActivationRange.java @@ -194,6 +194,7 @@ public class ActivationRange diff --git a/patches/server/0012-Bring-back-server-name.patch b/patches/server/0012-Bring-back-server-name.patch index 03892b169..3d291697b 100644 --- a/patches/server/0012-Bring-back-server-name.patch +++ b/patches/server/0012-Bring-back-server-name.patch @@ -17,10 +17,10 @@ index 0544ac93513d3a274bfb53bb6120bd598f4d603b..9ce5984fbeba4839290c9d213d441957 public final boolean spawnNpcs = this.get("spawn-npcs", true); public final boolean pvp = this.get("pvp", true); diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index 9123ae3a99efb80bc938e013a4eff4259cec1084..2749eecddd51e8e4553fe7aaa4d01a9f6a6bf73b 100644 +index 5a9e6b904893be6782a50b89582e6a8e7e3ef7e7..641269f5e2a9e4a254449818e99eae9cf6ae866d 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -2543,4 +2543,11 @@ public final class CraftServer implements Server { +@@ -2553,4 +2553,11 @@ public final class CraftServer implements Server { } // Paper end diff --git a/patches/server/0013-Configurable-server-mod-name.patch b/patches/server/0013-Configurable-server-mod-name.patch index 0690e532a..90a1925af 100644 --- a/patches/server/0013-Configurable-server-mod-name.patch +++ b/patches/server/0013-Configurable-server-mod-name.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Configurable server mod name diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 745df03f3b425a247d63d7d896c3dcf2c8baa0e0..53c4db48c184da5229c1de7ab5df447a248954da 100644 +index 2ede76a55c72840d915ed282609b1ca14f549929..7d1c39fec406aca69705db055b6d8f859142c986 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1652,7 +1652,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= KEEPALIVE_LIMIT) { // check keepalive limit, don't fire if already disconnected ServerGamePacketListenerImpl.LOGGER.warn("{} was kicked due to keepalive timeout!", this.player.getScoreboardName()); // more info -@@ -2991,6 +3007,16 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser +@@ -3069,6 +3085,16 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser @Override public void handleKeepAlive(ServerboundKeepAlivePacket packet) { diff --git a/patches/server/0027-Disable-outdated-build-check.patch b/patches/server/0027-Disable-outdated-build-check.patch index e7395e604..d5a4a49c8 100644 --- a/patches/server/0027-Disable-outdated-build-check.patch +++ b/patches/server/0027-Disable-outdated-build-check.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Disable outdated build check diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java -index 910ffb563d66ce3d5a3ea562fd1b9d6d2030cccb..0d201ea0f65d2e9972d39091bc0960ae0221b174 100644 +index a335d1689ebf01e0e96a45c640188dc024610e2c..01ca38ee35f2c1e5031ea4b8aca09a2a59c4a475 100644 --- a/src/main/java/org/bukkit/craftbukkit/Main.java +++ b/src/main/java/org/bukkit/craftbukkit/Main.java -@@ -269,7 +269,7 @@ public class Main { +@@ -276,7 +276,7 @@ public class Main { System.setProperty(TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY, "false"); // Paper } diff --git a/patches/server/0029-Zombie-horse-naturally-spawn.patch b/patches/server/0029-Zombie-horse-naturally-spawn.patch index a19389d55..92617510f 100644 --- a/patches/server/0029-Zombie-horse-naturally-spawn.patch +++ b/patches/server/0029-Zombie-horse-naturally-spawn.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Zombie horse naturally spawn diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index a02b0d819392db96a370c57818a378dd5fa27ef4..e4ac7b3a8a10056e3e24eef5263cafa65f29145f 100644 +index d575fcc1aae45c810c21fd8c112e3e63cdaf7d71..50639af3813578669ce039dc44d827df1196d723 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -87,6 +87,7 @@ import net.minecraft.world.entity.ai.village.poi.PoiManager; @@ -16,7 +16,7 @@ index a02b0d819392db96a370c57818a378dd5fa27ef4..e4ac7b3a8a10056e3e24eef5263cafa6 import net.minecraft.world.entity.animal.horse.SkeletonHorse; import net.minecraft.world.entity.boss.EnderDragonPart; import net.minecraft.world.entity.boss.enderdragon.EnderDragon; -@@ -713,12 +714,18 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl +@@ -899,12 +900,18 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl boolean flag1 = this.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && this.random.nextDouble() < (double) difficultydamagescaler.getEffectiveDifficulty() * paperConfig.skeleHorseSpawnChance && !this.getBlockState(blockposition.below()).is(Blocks.LIGHTNING_ROD); // Paper if (flag1) { diff --git a/patches/server/0046-Signs-allow-color-codes.patch b/patches/server/0046-Signs-allow-color-codes.patch index c79625ef6..13f14ecf7 100644 --- a/patches/server/0046-Signs-allow-color-codes.patch +++ b/patches/server/0046-Signs-allow-color-codes.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Signs allow color codes diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 335893049b3625a80cef78526eff3761cd8d5fbf..f641c27b9ab9e2ed5cd4f8122998df58577225ea 100644 +index 156f04fab90d44775ec8036da1b9a763544c8ccb..18b21258e39cbc54eaeaa44a4398a3b14847d6ff 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -1460,6 +1460,7 @@ public class ServerPlayer extends Player { @@ -17,10 +17,10 @@ index 335893049b3625a80cef78526eff3761cd8d5fbf..f641c27b9ab9e2ed5cd4f8122998df58 this.connection.send(new ClientboundBlockUpdatePacket(this.level, sign.getBlockPos())); this.connection.send(new ClientboundOpenSignEditorPacket(sign.getBlockPos())); diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index fa38157d4c422557b8ee05f47c833f840556b93d..0252c916dd1ac44265401696e34b1c8e71b2f3c1 100644 +index 58cd2fdf616f450e7906e90dc9725f1067c13052..c19b708cd7c6ec18ea44a5afa3b7e1c4ac4ddcb6 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -2981,11 +2981,16 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser +@@ -3059,11 +3059,16 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser } } // Paper end diff --git a/patches/server/0047-Allow-soil-to-moisten-from-water-directly-under-it.patch b/patches/server/0047-Allow-soil-to-moisten-from-water-directly-under-it.patch index 3d5966caf..ec16a6e6d 100644 --- a/patches/server/0047-Allow-soil-to-moisten-from-water-directly-under-it.patch +++ b/patches/server/0047-Allow-soil-to-moisten-from-water-directly-under-it.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Allow soil to moisten from water directly under it diff --git a/src/main/java/net/minecraft/world/level/block/FarmBlock.java b/src/main/java/net/minecraft/world/level/block/FarmBlock.java -index aa1ba8b74ab70b6cede99e4853ac0203f388ab06..7c636d601fde4aeae29cfcca4bf49ea05a8b4f4c 100644 +index a242a80b16c7d074d52a52728646224b1a0091d4..5d9d77cb382c8075af2713a0ce26c28a35a0aaa8 100644 --- a/src/main/java/net/minecraft/world/level/block/FarmBlock.java +++ b/src/main/java/net/minecraft/world/level/block/FarmBlock.java @@ -50,23 +50,6 @@ public class FarmBlock extends Block { @@ -32,15 +32,15 @@ index aa1ba8b74ab70b6cede99e4853ac0203f388ab06..7c636d601fde4aeae29cfcca4bf49ea0 @Override public VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext context) { return FarmBlock.SHAPE; -@@ -145,7 +128,7 @@ public class FarmBlock extends Block { - - do { - if (!iterator.hasNext()) { -- return false; -+ return ((ServerLevel) world).purpurConfig.farmlandGetsMoistFromBelow && world.getFluidState(pos.relative(Direction.DOWN)).is(FluidTags.WATER); // Purpur +@@ -159,7 +142,7 @@ public class FarmBlock extends Block { } + } + +- return false; ++ return ((ServerLevel) world).purpurConfig.farmlandGetsMoistFromBelow && world.getFluidState(pos.relative(Direction.DOWN)).is(FluidTags.WATER); // Purpur + // Tuinity end - remove abstract block iteration + } - blockposition1 = (BlockPos) iterator.next(); diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java index 1849f7970c5356f84a2b50a2a1f1b9742d822c67..8164b35f2c2d80a6b75ac10bd4fe293e47da3eba 100644 --- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java diff --git a/patches/server/0053-Fix-the-dead-lagging-the-server.patch b/patches/server/0053-Fix-the-dead-lagging-the-server.patch index fe5578131..b957c36b5 100644 --- a/patches/server/0053-Fix-the-dead-lagging-the-server.patch +++ b/patches/server/0053-Fix-the-dead-lagging-the-server.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Fix the dead lagging the server diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index a43c95abc8f6d0226ca097495ed9aeab0649d02b..f3e842a7ca7c8acba63796a5e74bb89545c829fb 100644 +index a12fcb4be21f32933cee916c863d6637ad87ef15..8197ad7db6ec1a750b732e597af75f41ab056778 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -1549,6 +1549,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -1735,6 +1735,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n this.setXRot(Mth.clamp(pitch, -90.0F, 90.0F) % 360.0F); this.yRotO = this.getYRot(); this.xRotO = this.getXRot(); diff --git a/patches/server/0055-Add-permission-for-F3-N-debug.patch b/patches/server/0055-Add-permission-for-F3-N-debug.patch index 37f655b2b..78182cb9a 100644 --- a/patches/server/0055-Add-permission-for-F3-N-debug.patch +++ b/patches/server/0055-Add-permission-for-F3-N-debug.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Add permission for F3+N debug diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index f2c328c52c127eb48b6c4ab4e1348a1bfd75c299..826fe9c183521e57466ba73d3e819c5368ff46c6 100644 +index 242157b074794d1c69eec7af53d27efa7ef7f56e..e3d557e570a56b9b0016206775fcd5313060f573 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -1157,6 +1157,7 @@ public abstract class PlayerList { +@@ -1158,6 +1158,7 @@ public abstract class PlayerList { } else { b0 = (byte) (24 + i); } diff --git a/patches/server/0057-Configurable-TPS-Catchup.patch b/patches/server/0057-Configurable-TPS-Catchup.patch index d26e136ff..9e50a95ae 100644 --- a/patches/server/0057-Configurable-TPS-Catchup.patch +++ b/patches/server/0057-Configurable-TPS-Catchup.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Configurable TPS Catchup diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index ac78bea2e411324c10a0183b1c73ce542bdbd13d..9fe12f0be064940e195b33559752349c6ca8680e 100644 +index 53d429eec63448f5e30ffb7026d49579264bd705..c96feac8afa0e0ea05513692d1e937ae7cbecd66 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1190,7 +1190,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { return Component.Serializer.toJson((Component) (new TextComponent(s))); -@@ -1179,10 +1182,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser +@@ -1218,10 +1221,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser private void a(List list, UnaryOperator unaryoperator, ItemStack itemstack, int slot, ItemStack handItem) { // CraftBukkit ListTag nbttaglist = new ListTag(); @@ -44,7 +44,7 @@ index 0252c916dd1ac44265401696e34b1c8e71b2f3c1..684cbd2cdce9472f4b263a44ec6f29db Objects.requireNonNull(nbttaglist); stream.forEach(nbttaglist::add); -@@ -1192,10 +1198,10 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser +@@ -1231,10 +1237,10 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser for (int j = list.size(); i < j; ++i) { TextFilter.FilteredText itextfilter_a = (TextFilter.FilteredText) list.get(i); @@ -57,7 +57,7 @@ index 0252c916dd1ac44265401696e34b1c8e71b2f3c1..684cbd2cdce9472f4b263a44ec6f29db if (!s.equals(s1)) { nbttagcompound.putString(String.valueOf(i), (String) unaryoperator.apply(s1)); -@@ -1211,6 +1217,16 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser +@@ -1250,6 +1256,16 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser this.player.getInventory().setItem(slot, CraftEventFactory.handleEditBookEvent(player, slot, handItem, itemstack)); // CraftBukkit // Paper - Don't ignore result (see other callsite for handleEditBookEvent) } diff --git a/patches/server/0084-Entity-lifespan.patch b/patches/server/0084-Entity-lifespan.patch index 81174e1d4..91d2556fe 100644 --- a/patches/server/0084-Entity-lifespan.patch +++ b/patches/server/0084-Entity-lifespan.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Entity lifespan diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java -index f7c71db9e33b7127f13f8d9b802322deda375c49..ff40d97479cd3aa8c7a164cdb1dbef92d4ab77ee 100644 +index f7c915ababdacd0901787d6dd1c08accacadabe5..df79f95ab2c8b9106cac889787b840b166ff0a9c 100644 --- a/src/main/java/net/minecraft/world/entity/Mob.java +++ b/src/main/java/net/minecraft/world/entity/Mob.java @@ -126,6 +126,7 @@ public abstract class Mob extends LivingEntity { @@ -80,7 +80,7 @@ index f7c71db9e33b7127f13f8d9b802322deda375c49..ff40d97479cd3aa8c7a164cdb1dbef92 } @Override -@@ -1579,6 +1613,7 @@ public abstract class Mob extends LivingEntity { +@@ -1584,6 +1618,7 @@ public abstract class Mob extends LivingEntity { this.setLastHurtMob(target); } diff --git a/patches/server/0085-Add-option-to-teleport-to-spawn-if-outside-world-bor.patch b/patches/server/0085-Add-option-to-teleport-to-spawn-if-outside-world-bor.patch index 8b4ef57a2..7d9d32a0e 100644 --- a/patches/server/0085-Add-option-to-teleport-to-spawn-if-outside-world-bor.patch +++ b/patches/server/0085-Add-option-to-teleport-to-spawn-if-outside-world-bor.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Add option to teleport to spawn if outside world border diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index d4e12961d00c44b3d62bcc5ad710e1379c5b297a..4b74790581b0f629507e5d00c0882bad0f0e168e 100644 +index 116fdc97069071c84912f2b9b045d0e22c1b6ba3..39493db2f716fe165431d9f8b3566c07e3657c8d 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -2476,5 +2476,25 @@ public class ServerPlayer extends Player { +@@ -2488,5 +2488,25 @@ public class ServerPlayer extends Player { } // CraftBukkit end diff --git a/patches/server/0086-Squid-EAR-immunity.patch b/patches/server/0086-Squid-EAR-immunity.patch index eef94584e..1571c5071 100644 --- a/patches/server/0086-Squid-EAR-immunity.patch +++ b/patches/server/0086-Squid-EAR-immunity.patch @@ -25,7 +25,7 @@ index b9e6082a7f15b83a6121684177516a4f0478d5a6..50de6f8b6a18ddd330ae1bb6ba8f58b7 public boolean spiderRidable = false; diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java -index f36c97529edbd3642d0ba37887a232226f766a35..6b6750f20a70bd7dd74db431321d57e306b1e2cd 100644 +index 2941a30967bae072f6726587714e1cba694b9efa..3cfca02792f48e917dc445e2d32e7c18556c151b 100644 --- a/src/main/java/org/spigotmc/ActivationRange.java +++ b/src/main/java/org/spigotmc/ActivationRange.java @@ -14,6 +14,7 @@ import net.minecraft.world.entity.ambient.AmbientCreature; @@ -36,7 +36,7 @@ index f36c97529edbd3642d0ba37887a232226f766a35..6b6750f20a70bd7dd74db431321d57e3 import net.minecraft.world.entity.animal.WaterAnimal; import net.minecraft.world.entity.animal.horse.Llama; import net.minecraft.world.entity.boss.EnderDragonPart; -@@ -348,6 +349,7 @@ public class ActivationRange +@@ -354,6 +355,7 @@ public class ActivationRange */ public static boolean checkIfActive(Entity entity) { diff --git a/patches/server/0096-Add-no-random-tick-block-list.patch b/patches/server/0096-Add-no-random-tick-block-list.patch index b4b951e9a..f5cee97f7 100644 --- a/patches/server/0096-Add-no-random-tick-block-list.patch +++ b/patches/server/0096-Add-no-random-tick-block-list.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Add no-random-tick block list diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index e4ac7b3a8a10056e3e24eef5263cafa65f29145f..f3aa58b31c4e3384c3315e53de4e9e4cc6181adc 100644 +index 50639af3813578669ce039dc44d827df1196d723..db54cb97296d6c2b2d893dca87cea5bd35eeb0db 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -318,7 +318,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl +@@ -488,7 +488,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl this.players = Lists.newArrayList(); this.entityTickList = new EntityTickList(); Predicate predicate = (block) -> { // CraftBukkit - decompile eror @@ -18,10 +18,10 @@ index e4ac7b3a8a10056e3e24eef5263cafa65f29145f..f3aa58b31c4e3384c3315e53de4e9e4c DefaultedRegistry registryblocks = Registry.BLOCK; diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -index a107304351381d68fdaa35a4d7ff214e6c1546a6..6e5dcf3c8a537729a18c085ebb5ab46b59c1d24c 100644 +index 7538262e14c86e4da9cd4cb887b76f649bfef2e6..f34973be478de4f088a0593b45bd89e558a13609 100644 --- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -@@ -893,10 +893,12 @@ public abstract class BlockBehaviour { +@@ -915,10 +915,12 @@ public abstract class BlockBehaviour { } public void tick(ServerLevel world, BlockPos pos, Random random) { diff --git a/patches/server/0099-Stop-squids-floating-on-top-of-water.patch b/patches/server/0099-Stop-squids-floating-on-top-of-water.patch index b12f8d1ff..a60125b51 100644 --- a/patches/server/0099-Stop-squids-floating-on-top-of-water.patch +++ b/patches/server/0099-Stop-squids-floating-on-top-of-water.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Stop squids floating on top of water diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index e64d4a3dd193bf7481884616034c5916114c649c..e5e93f6e2f1b17b165029bc1163c465920cc3fb9 100644 +index d4f1984fcf965cb5b9bb9f2e10d790286d25c613..3d947ba3719d494f202f28c5d93d28944a6860c9 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -3640,11 +3640,17 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -3829,11 +3829,17 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n this.yRotO = this.getYRot(); } @@ -45,10 +45,10 @@ index f96def2ebdf114823c322c2d4318d039e20eab97..2affff346a7fe81480e86cb61996039d @Override diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java -index 120498a39b7ca7aee9763084507508d4a1c425aa..0dfc639043998cd3bd32afaaf8153459172cc9f9 100644 +index 6f7e6429c35eea346517cbf08cf223fc6d838a8c..6a77112180556675af38cb1b3ce0b38a42ce9525 100644 --- a/src/main/java/net/minecraft/world/phys/AABB.java +++ b/src/main/java/net/minecraft/world/phys/AABB.java -@@ -356,4 +356,10 @@ public class AABB { +@@ -367,4 +367,10 @@ public class AABB { public static AABB ofSize(Vec3 center, double dx, double dy, double dz) { return new AABB(center.x - dx / 2.0D, center.y - dy / 2.0D, center.z - dz / 2.0D, center.x + dx / 2.0D, center.y + dy / 2.0D, center.z + dz / 2.0D); } diff --git a/patches/server/0102-Entities-can-use-portals-configuration.patch b/patches/server/0102-Entities-can-use-portals-configuration.patch index 67ba13920..eeef0a963 100644 --- a/patches/server/0102-Entities-can-use-portals-configuration.patch +++ b/patches/server/0102-Entities-can-use-portals-configuration.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Entities can use portals configuration diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index e5e93f6e2f1b17b165029bc1163c465920cc3fb9..de75f6317e661c79d3957180f6d5b20d23af42e4 100644 +index 3d947ba3719d494f202f28c5d93d28944a6860c9..721fd382ac1a024fea3360c100f5f7ad665e73bb 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -2512,7 +2512,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -2701,7 +2701,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n public void handleInsidePortal(BlockPos pos) { if (this.isOnPortalCooldown()) { this.setPortalCooldown(); @@ -17,7 +17,7 @@ index e5e93f6e2f1b17b165029bc1163c465920cc3fb9..de75f6317e661c79d3957180f6d5b20d if (!this.level.isClientSide && !pos.equals(this.portalEntrancePos)) { this.portalEntrancePos = pos.immutable(); } -@@ -3144,7 +3144,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -3333,7 +3333,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n } public boolean canChangeDimensions() { diff --git a/patches/server/0105-Allow-toggling-special-MobSpawners-per-world.patch b/patches/server/0105-Allow-toggling-special-MobSpawners-per-world.patch index 1570e60c7..e9baa5f9d 100644 --- a/patches/server/0105-Allow-toggling-special-MobSpawners-per-world.patch +++ b/patches/server/0105-Allow-toggling-special-MobSpawners-per-world.patch @@ -6,7 +6,7 @@ Subject: [PATCH] Allow toggling special MobSpawners per world In vanilla, these are all hardcoded on for world type 0 (overworld) and hardcoded off for every other world type. Default config behaviour matches this. diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index f3aa58b31c4e3384c3315e53de4e9e4cc6181adc..dd8248b89ee8694a18fffc4d880c92038d725cf7 100644 +index db54cb97296d6c2b2d893dca87cea5bd35eeb0db..98af132fd4a03afb016a05fd004ffe3e1d5cd957 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -83,6 +83,7 @@ import net.minecraft.world.entity.MobCategory; @@ -27,7 +27,7 @@ index f3aa58b31c4e3384c3315e53de4e9e4cc6181adc..dd8248b89ee8694a18fffc4d880c9203 import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.raid.Raid; import net.minecraft.world.entity.raid.Raids; -@@ -132,6 +135,8 @@ import net.minecraft.world.level.gameevent.GameEvent; +@@ -133,6 +136,8 @@ import net.minecraft.world.level.gameevent.GameEvent; import net.minecraft.world.level.gameevent.GameEventListenerRegistrar; import net.minecraft.world.level.gameevent.vibrations.VibrationPath; import net.minecraft.world.level.levelgen.Heightmap; @@ -36,7 +36,7 @@ index f3aa58b31c4e3384c3315e53de4e9e4cc6181adc..dd8248b89ee8694a18fffc4d880c9203 import net.minecraft.world.level.levelgen.feature.StructureFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.StructureStart; -@@ -341,7 +346,24 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl +@@ -511,7 +516,24 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl this.dragonParts = new Int2ObjectOpenHashMap(); this.tickTime = flag1; this.server = minecraftserver; diff --git a/patches/server/0113-Stonecutter-damage.patch b/patches/server/0113-Stonecutter-damage.patch index f3727d314..64706cb18 100644 --- a/patches/server/0113-Stonecutter-damage.patch +++ b/patches/server/0113-Stonecutter-damage.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Stonecutter damage diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index de75f6317e661c79d3957180f6d5b20d23af42e4..1e21e75558dc69cb9c32dce99644ddd022165b58 100644 +index 721fd382ac1a024fea3360c100f5f7ad665e73bb..260a2d8616030c0f5619eb357719b980f93570c8 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -897,7 +897,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -1001,7 +1001,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n } // CraftBukkit end diff --git a/patches/server/0114-Configurable-daylight-cycle.patch b/patches/server/0114-Configurable-daylight-cycle.patch index 100dd79b2..9892e112c 100644 --- a/patches/server/0114-Configurable-daylight-cycle.patch +++ b/patches/server/0114-Configurable-daylight-cycle.patch @@ -18,26 +18,26 @@ index 689ad22925b2561f7c8db961743eb1f821dbb25f..fa3c960992cc240161817e54659d83fe public ClientboundSetTimePacket(long time, long timeOfDay, boolean doDaylightCycle) { this.gameTime = time % 192000; // Paper - fix guardian beam diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index dd8248b89ee8694a18fffc4d880c92038d725cf7..9abc5063efe95a48a70045d8cedc15ab245ee65c 100644 +index 98af132fd4a03afb016a05fd004ffe3e1d5cd957..f15da84c0e5f9ab67c4d9ec59ee37de35df76f5c 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -200,6 +200,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl +@@ -201,6 +201,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + final Int2ObjectMap dragonParts; private final StructureFeatureManager structureFeatureManager; private final boolean tickTime; - + private double fakeTime; // Purpur - - // CraftBukkit start - private int tickPosition; -@@ -408,6 +409,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + // Tuinity start - execute chunk tasks mid tick + public long lastMidTickExecuteFailure; + // Tuinity end - execute chunk tasks mid tick +@@ -578,6 +579,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit this.asyncChunkTaskManager = new com.destroystokyo.paper.io.chunk.ChunkTaskManager(this); // Paper + this.fakeTime = this.serverLevelData.getDayTime(); // Purpur } - // CraftBukkit start -@@ -687,6 +689,18 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl + // Tuinity start - optimise collision +@@ -869,6 +871,18 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl this.liquidTicks.nextTick(); // Paper this.serverLevelData.getScheduledEvents().tick(this.server, i); if (this.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { @@ -56,7 +56,7 @@ index dd8248b89ee8694a18fffc4d880c92038d725cf7..9abc5063efe95a48a70045d8cedc15ab this.setDayTime(this.levelData.getDayTime() + 1L); } -@@ -695,6 +709,12 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl +@@ -877,6 +891,12 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl public void setDayTime(long timeOfDay) { this.serverLevelData.setDayTime(timeOfDay); diff --git a/patches/server/0119-Add-adjustable-breeding-cooldown-to-config.patch b/patches/server/0119-Add-adjustable-breeding-cooldown-to-config.patch index a6ea216fd..aeb33dab5 100644 --- a/patches/server/0119-Add-adjustable-breeding-cooldown-to-config.patch +++ b/patches/server/0119-Add-adjustable-breeding-cooldown-to-config.patch @@ -33,10 +33,10 @@ index 5a503a255b4e7e684a8f42d8190430397ca81683..7a90c6a628571730eee382e1efcfe1b9 entityageable.setBaby(true); entityageable.moveTo(this.getX(), this.getY(), this.getZ(), 0.0F, 0.0F); diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 2571738bd83e21207e5463be5e2ba7d7b7f94a87..9427f43db158d2e813e5b8584079df027df91635 100644 +index e52a708904d7c7d8d19e9def3bc915a9141c2bed..42c9952ee868fa54d9b6f2db8e08e3bc2a9b18de 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -184,6 +184,49 @@ public abstract class Level implements LevelAccessor, AutoCloseable { +@@ -186,6 +186,49 @@ public abstract class Level implements LevelAccessor, AutoCloseable { } // Paper end - fix and optimise world upgrading @@ -86,9 +86,9 @@ index 2571738bd83e21207e5463be5e2ba7d7b7f94a87..9427f43db158d2e813e5b8584079df02 public CraftWorld getWorld() { return this.world; } -@@ -207,6 +250,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot +@@ -317,6 +360,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { this.paperConfig = new com.destroystokyo.paper.PaperWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName(), this.spigotConfig); // Paper + this.tuinityConfig = new com.tuinity.tuinity.config.TuinityConfig.WorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData)worlddatamutable).getLevelName()); // Tuinity - Server Config this.purpurConfig = new net.pl3x.purpur.PurpurWorldConfig((ServerLevel) this, ((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName(), env); // Purpur + this.playerBreedingCooldowns = this.getNewBreedingCooldownCache(); // Purpur this.generator = gen; diff --git a/patches/server/0126-Add-critical-hit-check-to-EntityDamagedByEntityEvent.patch b/patches/server/0126-Add-critical-hit-check-to-EntityDamagedByEntityEvent.patch index 0eb8dfb5f..f71efd547 100644 --- a/patches/server/0126-Add-critical-hit-check-to-EntityDamagedByEntityEvent.patch +++ b/patches/server/0126-Add-critical-hit-check-to-EntityDamagedByEntityEvent.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Add critical hit check to EntityDamagedByEntityEvent diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java -index fbdf7d211fa7b3acaddc035ec6e04ab599c32c78..fab14cd3148cb81a343b7b1fb4f94da977dba763 100644 +index 52d43c2bf1fa1bfa97aab02a9837c6b11ecafc9d..d57a1984328750f5a6db748ef2db21fb3e2dff00 100644 --- a/src/main/java/net/minecraft/world/entity/player/Player.java +++ b/src/main/java/net/minecraft/world/entity/player/Player.java @@ -182,6 +182,7 @@ public abstract class Player extends LivingEntity { @@ -16,7 +16,7 @@ index fbdf7d211fa7b3acaddc035ec6e04ab599c32c78..fab14cd3148cb81a343b7b1fb4f94da9 // CraftBukkit start public boolean fauxSleeping; -@@ -1236,6 +1237,7 @@ public abstract class Player extends LivingEntity { +@@ -1241,6 +1242,7 @@ public abstract class Player extends LivingEntity { flag2 = flag2 && !level.paperConfig.disablePlayerCrits; // Paper flag2 = flag2 && !this.isSprinting(); if (flag2) { @@ -24,7 +24,7 @@ index fbdf7d211fa7b3acaddc035ec6e04ab599c32c78..fab14cd3148cb81a343b7b1fb4f94da9 f *= 1.5F; } -@@ -1272,6 +1274,7 @@ public abstract class Player extends LivingEntity { +@@ -1277,6 +1279,7 @@ public abstract class Player extends LivingEntity { Vec3 vec3d = target.getDeltaMovement(); boolean flag5 = target.hurt(DamageSource.playerAttack(this), f); diff --git a/patches/server/0130-Changeable-Mob-Left-Handed-Chance.patch b/patches/server/0130-Changeable-Mob-Left-Handed-Chance.patch index 014c8bf2f..35353cd91 100644 --- a/patches/server/0130-Changeable-Mob-Left-Handed-Chance.patch +++ b/patches/server/0130-Changeable-Mob-Left-Handed-Chance.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Changeable Mob Left Handed Chance diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java -index ff40d97479cd3aa8c7a164cdb1dbef92d4ab77ee..20af97629b614da792bdee2f8e450433570102f2 100644 +index df79f95ab2c8b9106cac889787b840b166ff0a9c..2f061e2825127e4a38e261ccacf56926a2421bb0 100644 --- a/src/main/java/net/minecraft/world/entity/Mob.java +++ b/src/main/java/net/minecraft/world/entity/Mob.java -@@ -1199,7 +1199,7 @@ public abstract class Mob extends LivingEntity { +@@ -1204,7 +1204,7 @@ public abstract class Mob extends LivingEntity { @Nullable public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType spawnReason, @Nullable SpawnGroupData entityData, @Nullable CompoundTag entityNbt) { this.getAttribute(Attributes.FOLLOW_RANGE).addPermanentModifier(new AttributeModifier("Random spawn bonus", this.random.nextGaussian() * 0.05D, AttributeModifier.Operation.MULTIPLY_BASE)); diff --git a/patches/server/0138-Spread-out-and-optimise-player-list-ticksSpread-out-.patch b/patches/server/0138-Spread-out-and-optimise-player-list-ticksSpread-out-.patch index 93dd38e21..fef68fffe 100644 --- a/patches/server/0138-Spread-out-and-optimise-player-list-ticksSpread-out-.patch +++ b/patches/server/0138-Spread-out-and-optimise-player-list-ticksSpread-out-.patch @@ -6,10 +6,10 @@ Subject: [PATCH] Spread out and optimise player list ticksSpread out and diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 826fe9c183521e57466ba73d3e819c5368ff46c6..1dae795321bc35ceaedd6f9218b61e6231066e7b 100644 +index e3d557e570a56b9b0016206775fcd5313060f573..ecb2eafd9eb851ed1a39866c840dc0a5381277b8 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -1006,22 +1006,22 @@ public abstract class PlayerList { +@@ -1007,22 +1007,22 @@ public abstract class PlayerList { } public void tick() { @@ -46,10 +46,10 @@ index 826fe9c183521e57466ba73d3e819c5368ff46c6..1dae795321bc35ceaedd6f9218b61e62 public void broadcastAll(Packet packet) { diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index 3306decd607ae190d6eaf674b3a59393c93dae34..98a3b692d17a7aa8166bf53a1d707c43590ed7f7 100644 +index bb9362a958b60f320d3f544e24206e1f8925e846..d74bb5ae378d3b7a2edcffeb4fc242e855949078 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -1508,7 +1508,13 @@ public class CraftPlayer extends CraftHumanEntity implements Player { +@@ -1563,7 +1563,13 @@ public class CraftPlayer extends CraftHumanEntity implements Player { @Override public boolean canSee(Player player) { diff --git a/patches/server/0143-Implement-TPSBar.patch b/patches/server/0143-Implement-TPSBar.patch index fa84b2a0d..98b376b6d 100644 --- a/patches/server/0143-Implement-TPSBar.patch +++ b/patches/server/0143-Implement-TPSBar.patch @@ -17,10 +17,10 @@ index f164106a957c24bdb71bde85d82948a64613fd91..bef1f6ca54fc6c1741b54b8e071a90eb if (environment.includeIntegrated) { diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index afe22502613d131bbca9af28b1716768e9a46f9c..4410ead33e18790f8674765affeba736a89c8ddd 100644 +index 214b25f57f15e2127b92ec88117c36d4b2096477..652e596c37bf8d865c954b31ad7d2562b9e95c46 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1048,6 +1048,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause return; } -@@ -1110,6 +1112,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser +@@ -1149,6 +1151,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser if (byteTotal > byteAllowed) { ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send too large of a book. Book Size: " + byteTotal + " - Allowed: "+ byteAllowed + " - Pages: " + pageList.size()); diff --git a/patches/server/0148-Add-MC-4-fix-back.patch b/patches/server/0148-Add-MC-4-fix-back.patch index 5d1a3ffb5..58dd756ec 100644 --- a/patches/server/0148-Add-MC-4-fix-back.patch +++ b/patches/server/0148-Add-MC-4-fix-back.patch @@ -5,14 +5,14 @@ Subject: [PATCH] Add MC-4 fix back diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 1e21e75558dc69cb9c32dce99644ddd022165b58..9c0bf033f15d344944fefcfb0696f83197a5704d 100644 +index 260a2d8616030c0f5619eb357719b980f93570c8..b7b00d28f60350958dd0e92b755237c1f15040ec 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -3838,7 +3838,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -4029,7 +4029,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n public final void setPosRaw(double x, double y, double z) { // Paper start - fix MC-4 if (this instanceof ItemEntity) { -- if (com.destroystokyo.paper.PaperConfig.fixEntityPositionDesync) { +- if (false && com.destroystokyo.paper.PaperConfig.fixEntityPositionDesync) { // Tuinity - revert + if (com.destroystokyo.paper.PaperConfig.fixEntityPositionDesync) { // Purpur // encode/decode from PacketPlayOutEntity x = Mth.lfloor(x * 4096.0D) * (1 / 4096.0D); diff --git a/patches/server/0151-Add-EntityTeleportHinderedEvent.patch b/patches/server/0151-Add-EntityTeleportHinderedEvent.patch index 7744a3420..b0a3759b2 100644 --- a/patches/server/0151-Add-EntityTeleportHinderedEvent.patch +++ b/patches/server/0151-Add-EntityTeleportHinderedEvent.patch @@ -98,10 +98,10 @@ index 393d3ce76b833044daa3f18670780b848b95e5c9..b967dfd7d675ed53a76fa743df0d3fba noteBlockIgnoreAbove = getBoolean("gameplay-mechanics.note-block-ignore-above", noteBlockIgnoreAbove); persistentTileEntityDisplayNames = getBoolean("gameplay-mechanics.persistent-tileentity-display-names-and-lore", persistentTileEntityDisplayNames); diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -index 6b85ba7d9bad9f648b4a6cb5a3938509b3e73cca..d2c776b8c189dc01dadb7399d3892399ebcb6276 100644 +index edc08af4ec2ce6e90c30da286c0ba5ac16efd3fc..3a92c0112befe51e795f81b1fce52e1f083f6373 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -@@ -550,6 +550,10 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { +@@ -559,6 +559,10 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { location.checkFinite(); if (this.entity.isVehicle() || this.entity.isRemoved()) { @@ -113,10 +113,10 @@ index 6b85ba7d9bad9f648b4a6cb5a3938509b3e73cca..d2c776b8c189dc01dadb7399d3892399 } diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index 98a3b692d17a7aa8166bf53a1d707c43590ed7f7..a3b72845e6a7323f6b7e73d0494bf2c58de62c7b 100644 +index d74bb5ae378d3b7a2edcffeb4fc242e855949078..cd8668f9f035b1a0c2c67b18dad4411ce20bf7d7 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -947,6 +947,10 @@ public class CraftPlayer extends CraftHumanEntity implements Player { +@@ -1002,6 +1002,10 @@ public class CraftPlayer extends CraftHumanEntity implements Player { } if (entity.isVehicle()) { diff --git a/patches/server/0154-Movement-options-for-armor-stands.patch b/patches/server/0154-Movement-options-for-armor-stands.patch index fa3bcc4c6..02cf934db 100644 --- a/patches/server/0154-Movement-options-for-armor-stands.patch +++ b/patches/server/0154-Movement-options-for-armor-stands.patch @@ -17,10 +17,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 9c0bf033f15d344944fefcfb0696f83197a5704d..c220dbe126f754b3acb35559855df5f577ca4801 100644 +index b7b00d28f60350958dd0e92b755237c1f15040ec..60986fbef5d31ae59187d51b5686126f797d3970 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -1402,7 +1402,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -1588,7 +1588,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n return this.isInWater() || flag; } diff --git a/patches/server/0155-Fix-stuck-in-portals.patch b/patches/server/0155-Fix-stuck-in-portals.patch index ace6f2de6..5e4be70e8 100644 --- a/patches/server/0155-Fix-stuck-in-portals.patch +++ b/patches/server/0155-Fix-stuck-in-portals.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Fix stuck in portals diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 85d2fda86638075130def6a47912682637186d84..6e9d011dd855a5fcb9fb603751a1dff434db7fe0 100644 +index b52cea07a77bd5124881e144483e148cbf5ad54d..4e8333d134b05bdf45356d14b974b8de013cb6d2 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -1137,6 +1137,7 @@ public class ServerPlayer extends Player { @@ -17,10 +17,10 @@ index 85d2fda86638075130def6a47912682637186d84..6e9d011dd855a5fcb9fb603751a1dff4 // CraftBukkit end this.setLevel(worldserver); diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index c220dbe126f754b3acb35559855df5f577ca4801..e87e30d6ff41ca1097b61729a8f3f678119d6007 100644 +index 60986fbef5d31ae59187d51b5686126f797d3970..3c0979e7031fb33257b5ec1efcb68882ab96efcd 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -2509,12 +2509,15 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -2698,12 +2698,15 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n return Vec3.directionFromRotation(this.getRotationVector()); } diff --git a/patches/server/0156-Toggle-for-water-sensitive-mob-damage.patch b/patches/server/0156-Toggle-for-water-sensitive-mob-damage.patch index e9f9c1165..9c97b8819 100644 --- a/patches/server/0156-Toggle-for-water-sensitive-mob-damage.patch +++ b/patches/server/0156-Toggle-for-water-sensitive-mob-damage.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Toggle for water sensitive mob damage diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java -index 713b80dde1f6cf3ba516513873c949a57047a566..9e144b0e3838b371d9fe6d146bc171f6e70e25b2 100644 +index ec916af4cace6bdbe5412cbabcbfaed5b1198b39..2b8f2b72b3f1f3766436c41487ec8ecab350349a 100644 --- a/src/main/java/net/minecraft/world/entity/Mob.java +++ b/src/main/java/net/minecraft/world/entity/Mob.java -@@ -856,7 +856,7 @@ public abstract class Mob extends LivingEntity { +@@ -861,7 +861,7 @@ public abstract class Mob extends LivingEntity { if (goalFloat.canUse()) goalFloat.tick(); this.getJumpControl().tick(); } diff --git a/patches/server/0159-Add-unsafe-Entity-serialization-API.patch b/patches/server/0159-Add-unsafe-Entity-serialization-API.patch index 001d7ecf8..7a9f57462 100644 --- a/patches/server/0159-Add-unsafe-Entity-serialization-API.patch +++ b/patches/server/0159-Add-unsafe-Entity-serialization-API.patch @@ -17,10 +17,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -index d2c776b8c189dc01dadb7399d3892399ebcb6276..e2b1574af471699f93956130b50268647f24e0b9 100644 +index 3a92c0112befe51e795f81b1fce52e1f083f6373..1035e023003521574a09fdea3fd08e5fca66d8fc 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -@@ -1233,5 +1233,12 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { +@@ -1242,5 +1242,12 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { public boolean isRidableInWater() { return getHandle().rideableUnderWater(); } diff --git a/patches/server/0161-Dont-run-with-scissors.patch b/patches/server/0161-Dont-run-with-scissors.patch index 0a1d4ae5a..9779e17ca 100644 --- a/patches/server/0161-Dont-run-with-scissors.patch +++ b/patches/server/0161-Dont-run-with-scissors.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Dont run with scissors! diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index 4279bb45a75a9ae0510fa96bdee4efd331433e3e..0d3e73240e5d363427379f50ecb163482c6f1bfc 100644 +index 844f76a712bec16fb7dea19fa27f45970bc61773..8581edaa5512c6b85229308761d5930aeafc3386 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -1498,6 +1498,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser +@@ -1555,6 +1555,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser this.player.fallDistance = 0.0F; } diff --git a/patches/server/0174-Config-for-skipping-night.patch b/patches/server/0174-Config-for-skipping-night.patch index d0517fee9..f0ef5e970 100644 --- a/patches/server/0174-Config-for-skipping-night.patch +++ b/patches/server/0174-Config-for-skipping-night.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Config for skipping night diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 9abc5063efe95a48a70045d8cedc15ab245ee65c..57a29f286815ee20abc87c0dffa0c6d682adaf35 100644 +index f15da84c0e5f9ab67c4d9ec59ee37de35df76f5c..bd32872ef404f90a078e02ec434cac547c5eef75 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -579,7 +579,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl +@@ -761,7 +761,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl // CraftBukkit end i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); diff --git a/patches/server/0177-Drowning-Settings.patch b/patches/server/0177-Drowning-Settings.patch index 82d8ad8c5..d9bf74bdf 100644 --- a/patches/server/0177-Drowning-Settings.patch +++ b/patches/server/0177-Drowning-Settings.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Drowning Settings diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index e87e30d6ff41ca1097b61729a8f3f678119d6007..fe8fbb95489b2f79dff73c5fce064a90708e208b 100644 +index 3c0979e7031fb33257b5ec1efcb68882ab96efcd..cc00c159e672f9a1bfb72c3e6b6b94fa6e53045f 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -2743,7 +2743,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -2932,7 +2932,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n } public int getMaxAirSupply() { diff --git a/patches/server/0178-Break-individual-slabs-when-sneaking.patch b/patches/server/0178-Break-individual-slabs-when-sneaking.patch index 56a570c8f..9d4e66c80 100644 --- a/patches/server/0178-Break-individual-slabs-when-sneaking.patch +++ b/patches/server/0178-Break-individual-slabs-when-sneaking.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Break individual slabs when sneaking diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -index e572088cad8b9e09b1d64f7971bacac2f10c5b17..50c0b4cfe9dab9bd1c1e1f3f9a2b0aa970931cc1 100644 +index b2c8cae1a777cd63a35ed1340caf205b1b3bb0ad..11270c763dc5e260074260e10a6dd9a9b7a09c8f 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -@@ -387,6 +387,7 @@ public class ServerPlayerGameMode { +@@ -419,6 +419,7 @@ public class ServerPlayerGameMode { } return false; } diff --git a/patches/server/0188-Configurable-damage-settings-for-magma-blocks.patch b/patches/server/0188-Configurable-damage-settings-for-magma-blocks.patch index e8a63ebfa..0ed97491d 100644 --- a/patches/server/0188-Configurable-damage-settings-for-magma-blocks.patch +++ b/patches/server/0188-Configurable-damage-settings-for-magma-blocks.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Configurable damage settings for magma blocks diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index fe8fbb95489b2f79dff73c5fce064a90708e208b..7fae7966027d19dd9757b4761401c21d81f051ac 100644 +index cc00c159e672f9a1bfb72c3e6b6b94fa6e53045f..eea15d795475b79994c1b6ba55e862399831f960 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -897,7 +897,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -1001,7 +1001,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n } // CraftBukkit end diff --git a/patches/server/0201-Add-config-change-multiplier-critical-damage-value.patch b/patches/server/0201-Add-config-change-multiplier-critical-damage-value.patch index fd6b69e72..803c27daa 100644 --- a/patches/server/0201-Add-config-change-multiplier-critical-damage-value.patch +++ b/patches/server/0201-Add-config-change-multiplier-critical-damage-value.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Add config change multiplier critical damage value diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java -index 6301f71df9573f91040934c85a8530f2cf2bfdad..6fa69683727f2111571468261ef16f7c0ff5c238 100644 +index 656b62c93dcbe15a79ebe684c18f4dc31ddc0dbe..5963b407d936e930a370677113ba947d2c2a198d 100644 --- a/src/main/java/net/minecraft/world/entity/player/Player.java +++ b/src/main/java/net/minecraft/world/entity/player/Player.java -@@ -1248,7 +1248,7 @@ public abstract class Player extends LivingEntity { +@@ -1253,7 +1253,7 @@ public abstract class Player extends LivingEntity { flag2 = flag2 && !this.isSprinting(); if (flag2) { this.isCritical = true; // Purpur diff --git a/patches/server/0205-ShulkerBox-allow-oversized-stacks.patch b/patches/server/0205-ShulkerBox-allow-oversized-stacks.patch index fbc1c5542..db5ff00ed 100644 --- a/patches/server/0205-ShulkerBox-allow-oversized-stacks.patch +++ b/patches/server/0205-ShulkerBox-allow-oversized-stacks.patch @@ -9,10 +9,10 @@ creating an itemstack using the TileEntity's NBT data (how it handles it for creative players) instead of routing it through the LootableBuilder. diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -index 50c0b4cfe9dab9bd1c1e1f3f9a2b0aa970931cc1..5048a7f109d704ccc5bffdf220f97154ec0dd288 100644 +index 11270c763dc5e260074260e10a6dd9a9b7a09c8f..a25e4eeb1ce4e18c7cb14e269ad3852715209cb9 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -@@ -416,7 +416,7 @@ public class ServerPlayerGameMode { +@@ -448,7 +448,7 @@ public class ServerPlayerGameMode { block.destroy(this.level, pos, iblockdata); } diff --git a/patches/server/0207-API-for-any-mob-to-burn-daylight.patch b/patches/server/0207-API-for-any-mob-to-burn-daylight.patch index 87eb56165..0db05b3ff 100644 --- a/patches/server/0207-API-for-any-mob-to-burn-daylight.patch +++ b/patches/server/0207-API-for-any-mob-to-burn-daylight.patch @@ -6,10 +6,10 @@ Subject: [PATCH] API for any mob to burn daylight Co-authored by: Encode42 diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 7fae7966027d19dd9757b4761401c21d81f051ac..936213c774400c5fc6f7e28cbaafb79c67ddf3e7 100644 +index eea15d795475b79994c1b6ba55e862399831f960..f3304cefa0a8862ef27dcb00519a8a3b9b94da26 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -4079,5 +4079,18 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -4272,5 +4272,18 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n public boolean canSaveToDisk() { return true; } @@ -89,10 +89,10 @@ index 2ece26f762f9764db27bea60b63f2121d1fd4211..85d9e1d485699c375e888503b71910b3 public boolean isSensitiveToWater() { diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java -index 9e144b0e3838b371d9fe6d146bc171f6e70e25b2..9fdc2fb0144db68868e09cf0b1dd19305fc91a50 100644 +index 2b8f2b72b3f1f3766436c41487ec8ecab350349a..ab55aa6f5717d191d0afb2da357178b294bad40e 100644 --- a/src/main/java/net/minecraft/world/entity/Mob.java +++ b/src/main/java/net/minecraft/world/entity/Mob.java -@@ -1630,17 +1630,7 @@ public abstract class Mob extends LivingEntity { +@@ -1635,17 +1635,7 @@ public abstract class Mob extends LivingEntity { } public boolean isSunBurnTick() { @@ -333,10 +333,10 @@ index 7eed6c176345c766a99d4304d61d28354291960f..40f2fd62b1d36843c5539932d2fb2496 // Paper end diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -index e2b1574af471699f93956130b50268647f24e0b9..c5733b0d9d4de696421a30b5d7266334af004c3b 100644 +index 1035e023003521574a09fdea3fd08e5fca66d8fc..7484f9c13e41f9be305134595b7052dfff4d72c3 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -@@ -1240,5 +1240,10 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { +@@ -1249,5 +1249,10 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { entity.absMoveTo(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); return !entity.valid && entity.level.addEntity(entity, spawnReason); } diff --git a/patches/server/0208-Fix-advancement-triggers-on-entity-death.patch b/patches/server/0208-Fix-advancement-triggers-on-entity-death.patch index 2d399b31c..31a1aeb91 100644 --- a/patches/server/0208-Fix-advancement-triggers-on-entity-death.patch +++ b/patches/server/0208-Fix-advancement-triggers-on-entity-death.patch @@ -47,10 +47,10 @@ index 85d9e1d485699c375e888503b71910b39afc6988..b7ed8f7fec01003a5a006b8c46400e79 CompoundTag nbttagcompound = stack.getTag(); diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java -index 9fdc2fb0144db68868e09cf0b1dd19305fc91a50..79956cae6a6fc2a30f52ebadf5f86ee1f13c1e91 100644 +index ab55aa6f5717d191d0afb2da357178b294bad40e..ad5197e5f814c02e81c52b2acb2e8554f746a28c 100644 --- a/src/main/java/net/minecraft/world/entity/Mob.java +++ b/src/main/java/net/minecraft/world/entity/Mob.java -@@ -1015,6 +1015,41 @@ public abstract class Mob extends LivingEntity { +@@ -1020,6 +1020,41 @@ public abstract class Mob extends LivingEntity { } @@ -139,10 +139,10 @@ index c9a44a4765f43b9c0247ed1005f4c13469bdee95..6d08c8c31a32ea38f06410fbaddf19b9 public boolean canTakeItem(ItemStack stack) { net.minecraft.world.entity.EquipmentSlot enumitemslot = Mob.getEquipmentSlotForItem(stack); diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java -index 6fa69683727f2111571468261ef16f7c0ff5c238..747f493d6206635787271e8329e56caa89252f35 100644 +index 5963b407d936e930a370677113ba947d2c2a198d..b60e38add38ac58d01f419dcb1b07a9b79bf1cb8 100644 --- a/src/main/java/net/minecraft/world/entity/player/Player.java +++ b/src/main/java/net/minecraft/world/entity/player/Player.java -@@ -1984,6 +1984,52 @@ public abstract class Player extends LivingEntity { +@@ -1989,6 +1989,52 @@ public abstract class Player extends LivingEntity { } diff --git a/patches/server/0212-Add-toggle-for-end-portal-safe-teleporting.patch b/patches/server/0212-Add-toggle-for-end-portal-safe-teleporting.patch index 693788b9e..4f5350da9 100644 --- a/patches/server/0212-Add-toggle-for-end-portal-safe-teleporting.patch +++ b/patches/server/0212-Add-toggle-for-end-portal-safe-teleporting.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Add toggle for end portal safe teleporting diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 936213c774400c5fc6f7e28cbaafb79c67ddf3e7..a75b0a7d8bbb6ed0871865c70b5a5256a4c61cbe 100644 +index f3304cefa0a8862ef27dcb00519a8a3b9b94da26..0615c4cc3ada4673cb44eaafc4c7fbe189fb1093 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -2560,7 +2560,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n +@@ -2749,7 +2749,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n } this.processPortalCooldown(); diff --git a/patches/server/0213-Flying-Fall-Damage-API.patch b/patches/server/0213-Flying-Fall-Damage-API.patch index 3aae8a79d..a3c1e35ee 100644 --- a/patches/server/0213-Flying-Fall-Damage-API.patch +++ b/patches/server/0213-Flying-Fall-Damage-API.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Flying Fall Damage API diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java -index 747f493d6206635787271e8329e56caa89252f35..cfb12af54b9593e9b642c8d87ab349f93853f4a6 100644 +index b60e38add38ac58d01f419dcb1b07a9b79bf1cb8..e2b2ee49f9b8220c2de3f26389e78b6321c60d33 100644 --- a/src/main/java/net/minecraft/world/entity/player/Player.java +++ b/src/main/java/net/minecraft/world/entity/player/Player.java @@ -183,6 +183,7 @@ public abstract class Player extends LivingEntity { @@ -16,7 +16,7 @@ index 747f493d6206635787271e8329e56caa89252f35..cfb12af54b9593e9b642c8d87ab349f9 // CraftBukkit start public boolean fauxSleeping; -@@ -1723,7 +1724,7 @@ public abstract class Player extends LivingEntity { +@@ -1728,7 +1729,7 @@ public abstract class Player extends LivingEntity { @Override public boolean causeFallDamage(float fallDistance, float damageMultiplier, DamageSource damageSource) { @@ -26,10 +26,10 @@ index 747f493d6206635787271e8329e56caa89252f35..cfb12af54b9593e9b642c8d87ab349f9 } else { if (fallDistance >= 2.0F) { diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index a3b72845e6a7323f6b7e73d0494bf2c58de62c7b..26f0b436e2a9c8cf239f38e91f98fb580262b272 100644 +index cd8668f9f035b1a0c2c67b18dad4411ce20bf7d7..60e882e0fad1d9153f10cdc42e3c1d43a7642bc1 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -2521,5 +2521,15 @@ public class CraftPlayer extends CraftHumanEntity implements Player { +@@ -2576,5 +2576,15 @@ public class CraftPlayer extends CraftHumanEntity implements Player { public void setSpawnInvulnerableTicks(int spawnInvulnerableTime) { getHandle().spawnInvulnerableTime = spawnInvulnerableTime; } diff --git a/patches/server/0214-Make-lightning-rod-range-configurable.patch b/patches/server/0214-Make-lightning-rod-range-configurable.patch index ec781230c..e19a893f3 100644 --- a/patches/server/0214-Make-lightning-rod-range-configurable.patch +++ b/patches/server/0214-Make-lightning-rod-range-configurable.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Make lightning rod range configurable diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 57a29f286815ee20abc87c0dffa0c6d682adaf35..8d56327ee9ff88baba34af2931c2f074cdb8f078 100644 +index bd32872ef404f90a078e02ec434cac547c5eef75..bfcd0def5ed81c8579f7fbbbb580797439c4cfc5 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -846,7 +846,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl +@@ -1046,7 +1046,7 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl return villageplacetype == PoiType.LIGHTNING_ROD; }, (blockposition1) -> { return blockposition1.getY() == this.getLevel().getHeight(Heightmap.Types.WORLD_SURFACE, blockposition1.getX(), blockposition1.getZ()) - 1; diff --git a/patches/server/0215-Burp-after-eating-food-fills-hunger-bar-completely.patch b/patches/server/0215-Burp-after-eating-food-fills-hunger-bar-completely.patch index e220a6b09..103815f51 100644 --- a/patches/server/0215-Burp-after-eating-food-fills-hunger-bar-completely.patch +++ b/patches/server/0215-Burp-after-eating-food-fills-hunger-bar-completely.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Burp after eating food fills hunger bar completely diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java -index cfb12af54b9593e9b642c8d87ab349f93853f4a6..894e5adaed05e6e83d53f7c04a9a059f6c6abb29 100644 +index e2b2ee49f9b8220c2de3f26389e78b6321c60d33..79782538277b60a1c1c16c1c75b53b7d8a2c00ee 100644 --- a/src/main/java/net/minecraft/world/entity/player/Player.java +++ b/src/main/java/net/minecraft/world/entity/player/Player.java @@ -196,6 +196,8 @@ public abstract class Player extends LivingEntity { @@ -30,7 +30,7 @@ index cfb12af54b9593e9b642c8d87ab349f93853f4a6..894e5adaed05e6e83d53f7c04a9a059f this.noPhysics = this.isSpectator(); if (this.isSpectator()) { this.onGround = false; -@@ -2338,7 +2346,7 @@ public abstract class Player extends LivingEntity { +@@ -2343,7 +2351,7 @@ public abstract class Player extends LivingEntity { public ItemStack eat(Level world, ItemStack stack) { this.getFoodData().eat(stack.getItem(), stack); this.awardStat(Stats.ITEM_USED.get(stack.getItem())); diff --git a/patches/server/0216-Allow-player-join-full-server-by-permission.patch b/patches/server/0216-Allow-player-join-full-server-by-permission.patch index 519fa5fee..43c73401c 100644 --- a/patches/server/0216-Allow-player-join-full-server-by-permission.patch +++ b/patches/server/0216-Allow-player-join-full-server-by-permission.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Allow player join full server by permission diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index c93a5bbf546b8b501306a98e84f995ec1a8c8c9e..f4b33bf57851ee179b64c20de83ca26d01b2f453 100644 +index 5043ddc861c9eab1e47e2cac68f4b73da317d27f..4b93a16db40889a0de99bdac038b0cc8ac37039f 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -758,7 +758,7 @@ public abstract class PlayerList { +@@ -759,7 +759,7 @@ public abstract class PlayerList { event.disallow(PlayerLoginEvent.Result.KICK_BANNED, PaperAdventure.asAdventure(chatmessage)); // Paper - Adventure } else { // return this.players.size() >= this.maxPlayers && !this.d(gameprofile) ? new ChatMessage("multiplayer.disconnect.server_full") : null; diff --git a/patches/server/0224-Armor-click-equip-options.patch b/patches/server/0224-Armor-click-equip-options.patch index 7434b0044..25f90b5a5 100644 --- a/patches/server/0224-Armor-click-equip-options.patch +++ b/patches/server/0224-Armor-click-equip-options.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Armor click equip options diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -index 5048a7f109d704ccc5bffdf220f97154ec0dd288..e8c3b14601ce1d352cfd4c031c029f5541d6cfe0 100644 +index a25e4eeb1ce4e18c7cb14e269ad3852715209cb9..111779c0a614b29bea5ad7a8301d704efaf62bf1 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -@@ -466,7 +466,7 @@ public class ServerPlayerGameMode { +@@ -498,7 +498,7 @@ public class ServerPlayerGameMode { return interactionresultwrapper.getResult(); } else { player.setItemInHand(hand, itemstack1); diff --git a/patches/server/0225-Add-uptime-command.patch b/patches/server/0225-Add-uptime-command.patch index 160ef919c..9f2ea1535 100644 --- a/patches/server/0225-Add-uptime-command.patch +++ b/patches/server/0225-Add-uptime-command.patch @@ -17,13 +17,13 @@ index bef1f6ca54fc6c1741b54b8e071a90eb5e3a36f3..84c970ed188509127cd5b4d268a5fa1f } diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 4410ead33e18790f8674765affeba736a89c8ddd..2e8ea3883cbf0dd5ce06af7595e0f3b070ec729a 100644 +index 652e596c37bf8d865c954b31ad7d2562b9e95c46..e74ddf6b72dbd335f8a06f05341d5a4d99afa428 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -293,6 +293,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); public int autosavePeriod; diff --git a/patches/server/0228-SPIGOT-5988-Fix-bed-respawn-location-not-resetting.patch b/patches/server/0228-SPIGOT-5988-Fix-bed-respawn-location-not-resetting.patch index 1416a7b13..1aa32c513 100644 --- a/patches/server/0228-SPIGOT-5988-Fix-bed-respawn-location-not-resetting.patch +++ b/patches/server/0228-SPIGOT-5988-Fix-bed-respawn-location-not-resetting.patch @@ -5,10 +5,10 @@ Subject: [PATCH] SPIGOT-5988 Fix bed respawn location not resetting diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index f4b33bf57851ee179b64c20de83ca26d01b2f453..c36040edb47f40cfb5740639c8c761e311a6fae4 100644 +index 4b93a16db40889a0de99bdac038b0cc8ac37039f..602b96d5dfbeb9a24072a8c80db3547a2af2ae6f 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -896,6 +896,7 @@ public abstract class PlayerList { +@@ -897,6 +897,7 @@ public abstract class PlayerList { location = new Location(worldserver1.getWorld(), vec3d.x, vec3d.y, vec3d.z, f1, 0.0F); } else if (blockposition != null) { entityplayer1.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, 0.0F)); diff --git a/patches/server/0231-Customizable-sleeping-actionbar-messages.patch b/patches/server/0231-Customizable-sleeping-actionbar-messages.patch index 2072dc446..0a722f822 100644 --- a/patches/server/0231-Customizable-sleeping-actionbar-messages.patch +++ b/patches/server/0231-Customizable-sleeping-actionbar-messages.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Customizable sleeping actionbar messages diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 8d56327ee9ff88baba34af2931c2f074cdb8f078..fd68db19b041df90b3c4ff15dc0e5b2f4f9bb860 100644 +index bfcd0def5ed81c8579f7fbbbb580797439c4cfc5..4d921ac4d064b02416e43bba93b70406ff0417eb 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -4,6 +4,7 @@ import com.google.common.annotations.VisibleForTesting; @@ -26,7 +26,7 @@ index 8d56327ee9ff88baba34af2931c2f074cdb8f078..fd68db19b041df90b3c4ff15dc0e5b2f import net.minecraft.CrashReport; import net.minecraft.core.BlockPos; import net.minecraft.core.DefaultedRegistry; -@@ -157,6 +161,7 @@ import net.minecraft.world.phys.Vec3; +@@ -158,6 +162,7 @@ import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.shapes.BooleanOp; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; @@ -34,7 +34,7 @@ index 8d56327ee9ff88baba34af2931c2f074cdb8f078..fd68db19b041df90b3c4ff15dc0e5b2f import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -@@ -889,11 +894,29 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl +@@ -1089,11 +1094,29 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl if (this.canSleepThroughNights()) { if (!this.getServer().isSingleplayer() || this.getServer().isPublished()) { int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE);