diff --git a/build.gradle.kts b/build.gradle.kts index 8ec7e7496..9ed2bfba9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ plugins { java id("com.github.johnrengelman.shadow") version "7.0.0" apply false - id("io.papermc.paperweight.patcher") version "1.1.6" + id("io.papermc.paperweight.patcher") version "1.1.7" } repositories { @@ -71,19 +71,4 @@ paperweight { serverOutputDir.set(layout.projectDirectory.dir("Purpur-Server")) } } - - useStandardUpstream("tuinity") { - url.set(github("pl3xgaming", "Tuinity")) - ref.set(providers.gradleProperty("tuinityCommit")) - - withStandardPatcher { - baseName("Tuinity") - - apiPatchDir.set(layout.projectDirectory.dir("patches/api")) - apiOutputDir.set(layout.projectDirectory.dir("Purpur-API")) - - serverPatchDir.set(layout.projectDirectory.dir("patches/server")) - serverOutputDir.set(layout.projectDirectory.dir("Purpur-Server")) - } - } } diff --git a/gradle.properties b/gradle.properties index 06fc4e325..5a18b7700 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,8 +4,7 @@ version = 1.17-R0.1-SNAPSHOT mcVersion = 1.17 packageVersion = 1_17_R1 -paperCommit = 1c77d647136053dd15098ee6d971b4c428f42bfa -tuinityCommit = 30b16d03f643d809ea96faf4085a954dd38d0013 +paperCommit = 0ad55267a387bf9cea3b6d0caeb843b3253cca65 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 new file mode 100644 index 000000000..2ef8b843e --- /dev/null +++ b/patches/api/0001-Tuinity-API-Changes.patch @@ -0,0 +1,128 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Initial Source +Date: Sat, 26 Jun 2021 23:30:37 -0500 +Subject: [PATCH] Tuinity API Changes + + +diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java +index e88b47a838dc472ad64271a518ee1789f7be19fa..f55ae8275c297c4c86215fba8d7197ffe9715879 100644 +--- a/src/main/java/org/bukkit/Server.java ++++ b/src/main/java/org/bukkit/Server.java +@@ -1606,6 +1606,14 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi + } + // Paper end + ++ // Tuinity start - add config to timings report ++ @NotNull ++ public org.bukkit.configuration.file.YamlConfiguration getTuinityConfig() ++ { ++ throw new UnsupportedOperationException("Not supported yet."); ++ } ++ // Tuinity end - add config to timings report ++ + /** + * 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 da83b4cbed0be6f693c7cbb1cc032356f12d7883..51c334f68052f58fbb9c10fb9ed31ab42780ceac 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/0001-Build-System-Changes.patch b/patches/api/0002-Build-System-Changes.patch similarity index 88% rename from patches/api/0001-Build-System-Changes.patch rename to patches/api/0002-Build-System-Changes.patch index db168b559..9b46f9429 100644 --- a/patches/api/0001-Build-System-Changes.patch +++ b/patches/api/0002-Build-System-Changes.patch @@ -6,7 +6,7 @@ Subject: [PATCH] Build System Changes todo: merge with rebrand patch diff --git a/build.gradle.kts b/build.gradle.kts -index 6d04816e22f44a33c001d2b7e080402fba6af86c..d63d357d1357bfb8ffae09cb7ba0cd361f09f4e5 100644 +index e142072f31a41b25ac637970f79e71ab70c2f28c..407bf7c09304c76ef1dbec43bb5d665f02ce4840 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { diff --git a/patches/api/0002-Purpur-config-files.patch b/patches/api/0003-Purpur-config-files.patch similarity index 100% rename from patches/api/0002-Purpur-config-files.patch rename to patches/api/0003-Purpur-config-files.patch diff --git a/patches/api/0003-Default-permissions.patch b/patches/api/0004-Default-permissions.patch similarity index 100% rename from patches/api/0003-Default-permissions.patch rename to patches/api/0004-Default-permissions.patch diff --git a/patches/api/0004-Ridables.patch b/patches/api/0005-Ridables.patch similarity index 100% rename from patches/api/0004-Ridables.patch rename to patches/api/0005-Ridables.patch diff --git a/patches/api/0005-Allow-inventory-resizing.patch b/patches/api/0006-Allow-inventory-resizing.patch similarity index 100% rename from patches/api/0005-Allow-inventory-resizing.patch rename to patches/api/0006-Allow-inventory-resizing.patch diff --git a/patches/api/0006-Advancement-API.patch b/patches/api/0007-Advancement-API.patch similarity index 100% rename from patches/api/0006-Advancement-API.patch rename to patches/api/0007-Advancement-API.patch diff --git a/patches/api/0007-Llama-API.patch b/patches/api/0008-Llama-API.patch similarity index 100% rename from patches/api/0007-Llama-API.patch rename to patches/api/0008-Llama-API.patch diff --git a/patches/api/0008-AFK-API.patch b/patches/api/0009-AFK-API.patch similarity index 100% rename from patches/api/0008-AFK-API.patch rename to patches/api/0009-AFK-API.patch diff --git a/patches/api/0009-Bring-back-server-name.patch b/patches/api/0010-Bring-back-server-name.patch similarity index 100% rename from patches/api/0009-Bring-back-server-name.patch rename to patches/api/0010-Bring-back-server-name.patch diff --git a/patches/api/0010-ExecuteCommandEvent.patch b/patches/api/0011-ExecuteCommandEvent.patch similarity index 100% rename from patches/api/0010-ExecuteCommandEvent.patch rename to patches/api/0011-ExecuteCommandEvent.patch diff --git a/patches/api/0011-LivingEntity-safeFallDistance.patch b/patches/api/0012-LivingEntity-safeFallDistance.patch similarity index 100% rename from patches/api/0011-LivingEntity-safeFallDistance.patch rename to patches/api/0012-LivingEntity-safeFallDistance.patch diff --git a/patches/api/0012-Lagging-threshold.patch b/patches/api/0013-Lagging-threshold.patch similarity index 100% rename from patches/api/0012-Lagging-threshold.patch rename to patches/api/0013-Lagging-threshold.patch diff --git a/patches/api/0013-ItemFactory-getMonsterEgg.patch b/patches/api/0014-ItemFactory-getMonsterEgg.patch similarity index 100% rename from patches/api/0013-ItemFactory-getMonsterEgg.patch rename to patches/api/0014-ItemFactory-getMonsterEgg.patch diff --git a/patches/api/0014-PlayerSetSpawnerTypeWithEggEvent.patch b/patches/api/0015-PlayerSetSpawnerTypeWithEggEvent.patch similarity index 100% rename from patches/api/0014-PlayerSetSpawnerTypeWithEggEvent.patch rename to patches/api/0015-PlayerSetSpawnerTypeWithEggEvent.patch diff --git a/patches/api/0015-EMC-MonsterEggSpawnEvent.patch b/patches/api/0016-EMC-MonsterEggSpawnEvent.patch similarity index 100% rename from patches/api/0015-EMC-MonsterEggSpawnEvent.patch rename to patches/api/0016-EMC-MonsterEggSpawnEvent.patch diff --git a/patches/api/0016-Villager-resetOffers.patch b/patches/api/0017-Villager-resetOffers.patch similarity index 100% rename from patches/api/0016-Villager-resetOffers.patch rename to patches/api/0017-Villager-resetOffers.patch diff --git a/patches/api/0017-Player-invulnerabilities.patch b/patches/api/0018-Player-invulnerabilities.patch similarity index 100% rename from patches/api/0017-Player-invulnerabilities.patch rename to patches/api/0018-Player-invulnerabilities.patch diff --git a/patches/api/0018-Anvil-API.patch b/patches/api/0019-Anvil-API.patch similarity index 100% rename from patches/api/0018-Anvil-API.patch rename to patches/api/0019-Anvil-API.patch diff --git a/patches/api/0019-ItemStack-convenience-methods.patch b/patches/api/0020-ItemStack-convenience-methods.patch similarity index 100% rename from patches/api/0019-ItemStack-convenience-methods.patch rename to patches/api/0020-ItemStack-convenience-methods.patch diff --git a/patches/api/0020-Phantoms-attracted-to-crystals-and-crystals-shoot-ph.patch b/patches/api/0021-Phantoms-attracted-to-crystals-and-crystals-shoot-ph.patch similarity index 100% rename from patches/api/0020-Phantoms-attracted-to-crystals-and-crystals-shoot-ph.patch rename to patches/api/0021-Phantoms-attracted-to-crystals-and-crystals-shoot-ph.patch diff --git a/patches/api/0021-ChatColor-conveniences.patch b/patches/api/0022-ChatColor-conveniences.patch similarity index 100% rename from patches/api/0021-ChatColor-conveniences.patch rename to patches/api/0022-ChatColor-conveniences.patch diff --git a/patches/api/0022-LivingEntity-broadcastItemBreak.patch b/patches/api/0023-LivingEntity-broadcastItemBreak.patch similarity index 100% rename from patches/api/0022-LivingEntity-broadcastItemBreak.patch rename to patches/api/0023-LivingEntity-broadcastItemBreak.patch diff --git a/patches/api/0023-Item-entity-immunities.patch b/patches/api/0024-Item-entity-immunities.patch similarity index 100% rename from patches/api/0023-Item-entity-immunities.patch rename to patches/api/0024-Item-entity-immunities.patch diff --git a/patches/api/0024-Spigot-Improve-output-of-plugins-command.patch b/patches/api/0025-Spigot-Improve-output-of-plugins-command.patch similarity index 100% rename from patches/api/0024-Spigot-Improve-output-of-plugins-command.patch rename to patches/api/0025-Spigot-Improve-output-of-plugins-command.patch diff --git a/patches/api/0025-Add-option-to-disable-zombie-aggressiveness-towards-.patch b/patches/api/0026-Add-option-to-disable-zombie-aggressiveness-towards-.patch similarity index 100% rename from patches/api/0025-Add-option-to-disable-zombie-aggressiveness-towards-.patch rename to patches/api/0026-Add-option-to-disable-zombie-aggressiveness-towards-.patch diff --git a/patches/api/0026-Add-predicate-to-recipe-s-ExactChoice-ingredient.patch b/patches/api/0027-Add-predicate-to-recipe-s-ExactChoice-ingredient.patch similarity index 100% rename from patches/api/0026-Add-predicate-to-recipe-s-ExactChoice-ingredient.patch rename to patches/api/0027-Add-predicate-to-recipe-s-ExactChoice-ingredient.patch diff --git a/patches/api/0027-Add-critical-hit-check-to-EntityDamagedByEntityEvent.patch b/patches/api/0028-Add-critical-hit-check-to-EntityDamagedByEntityEvent.patch similarity index 100% rename from patches/api/0027-Add-critical-hit-check-to-EntityDamagedByEntityEvent.patch rename to patches/api/0028-Add-critical-hit-check-to-EntityDamagedByEntityEvent.patch diff --git a/patches/api/0028-Left-handed-API.patch b/patches/api/0029-Left-handed-API.patch similarity index 100% rename from patches/api/0028-Left-handed-API.patch rename to patches/api/0029-Left-handed-API.patch diff --git a/patches/api/0029-Alphabetize-in-game-plugins-list.patch b/patches/api/0030-Alphabetize-in-game-plugins-list.patch similarity index 100% rename from patches/api/0029-Alphabetize-in-game-plugins-list.patch rename to patches/api/0030-Alphabetize-in-game-plugins-list.patch diff --git a/patches/api/0030-Rabid-Wolf-API.patch b/patches/api/0031-Rabid-Wolf-API.patch similarity index 100% rename from patches/api/0030-Rabid-Wolf-API.patch rename to patches/api/0031-Rabid-Wolf-API.patch diff --git a/patches/api/0031-Fix-javadoc-warnings-missing-param-and-return.patch b/patches/api/0032-Fix-javadoc-warnings-missing-param-and-return.patch similarity index 100% rename from patches/api/0031-Fix-javadoc-warnings-missing-param-and-return.patch rename to patches/api/0032-Fix-javadoc-warnings-missing-param-and-return.patch diff --git a/patches/api/0032-PlayerBookTooLargeEvent.patch b/patches/api/0033-PlayerBookTooLargeEvent.patch similarity index 100% rename from patches/api/0032-PlayerBookTooLargeEvent.patch rename to patches/api/0033-PlayerBookTooLargeEvent.patch diff --git a/patches/api/0033-Full-netherite-armor-grants-fire-resistance.patch b/patches/api/0034-Full-netherite-armor-grants-fire-resistance.patch similarity index 100% rename from patches/api/0033-Full-netherite-armor-grants-fire-resistance.patch rename to patches/api/0034-Full-netherite-armor-grants-fire-resistance.patch diff --git a/patches/api/0034-Add-EntityTeleportHinderedEvent.patch b/patches/api/0035-Add-EntityTeleportHinderedEvent.patch similarity index 100% rename from patches/api/0034-Add-EntityTeleportHinderedEvent.patch rename to patches/api/0035-Add-EntityTeleportHinderedEvent.patch diff --git a/patches/api/0035-Add-StructureGenerateEvent.patch b/patches/api/0036-Add-StructureGenerateEvent.patch similarity index 100% rename from patches/api/0035-Add-StructureGenerateEvent.patch rename to patches/api/0036-Add-StructureGenerateEvent.patch diff --git a/patches/api/0036-Add-unsafe-Entity-serialization-API.patch b/patches/api/0037-Add-unsafe-Entity-serialization-API.patch similarity index 100% rename from patches/api/0036-Add-unsafe-Entity-serialization-API.patch rename to patches/api/0037-Add-unsafe-Entity-serialization-API.patch diff --git a/patches/api/0037-Conflict-on-change-for-adventure-deprecations.patch b/patches/api/0038-Conflict-on-change-for-adventure-deprecations.patch similarity index 99% rename from patches/api/0037-Conflict-on-change-for-adventure-deprecations.patch rename to patches/api/0038-Conflict-on-change-for-adventure-deprecations.patch index ce9070162..b79d4997d 100644 --- a/patches/api/0037-Conflict-on-change-for-adventure-deprecations.patch +++ b/patches/api/0038-Conflict-on-change-for-adventure-deprecations.patch @@ -72,7 +72,7 @@ index 047b6b3d037ccdc9c83d8f46d5469884f4811717..7354aed0a328d1eab59f29d12d401ff1 return server.getShutdownMessage(); } diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java -index 31b776d35786a932eb912010778ff83b35244f6d..8d3c9a355bea2b217f69f6eb70e0962a370e09b8 100644 +index 3ee71225eb67ec728b414a8cdc0570d48d5603e3..a7fcd00ae37f9a2026759642562b1059c9dd9526 100644 --- a/src/main/java/org/bukkit/Server.java +++ b/src/main/java/org/bukkit/Server.java @@ -250,7 +250,7 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi @@ -170,7 +170,7 @@ index cdcf02ff9e80f5908a8fa22e82701445d5e2d298..83eba2421cdfa56c2f5b9ebaac18d563 /** diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java -index f1e5727e5ff8db2195d1f80a4938c1d9b05718cc..9f2bab23c0e11532816381e54591aa10b2b30d9b 100644 +index 93d9e1b7b4ac9b9ccee7006375d5a96eaf9508ca..2c9860d7e55d254e3b201f4580ac400090ffe6e1 100644 --- a/src/main/java/org/bukkit/entity/Player.java +++ b/src/main/java/org/bukkit/entity/Player.java @@ -75,7 +75,7 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM diff --git a/patches/api/0038-Add-enchantment-target-for-bows-and-crossbows.patch b/patches/api/0039-Add-enchantment-target-for-bows-and-crossbows.patch similarity index 100% rename from patches/api/0038-Add-enchantment-target-for-bows-and-crossbows.patch rename to patches/api/0039-Add-enchantment-target-for-bows-and-crossbows.patch diff --git a/patches/api/0039-Iron-golem-poppy-calms-anger.patch b/patches/api/0040-Iron-golem-poppy-calms-anger.patch similarity index 100% rename from patches/api/0039-Iron-golem-poppy-calms-anger.patch rename to patches/api/0040-Iron-golem-poppy-calms-anger.patch diff --git a/patches/api/0040-API-for-any-mob-to-burn-daylight.patch b/patches/api/0041-API-for-any-mob-to-burn-daylight.patch similarity index 100% rename from patches/api/0040-API-for-any-mob-to-burn-daylight.patch rename to patches/api/0041-API-for-any-mob-to-burn-daylight.patch diff --git a/patches/api/0041-Flying-Fall-Damage-API.patch b/patches/api/0042-Flying-Fall-Damage-API.patch similarity index 100% rename from patches/api/0041-Flying-Fall-Damage-API.patch rename to patches/api/0042-Flying-Fall-Damage-API.patch diff --git a/patches/api/0042-Add-back-player-spawned-endermite-API.patch b/patches/api/0043-Add-back-player-spawned-endermite-API.patch similarity index 100% rename from patches/api/0042-Add-back-player-spawned-endermite-API.patch rename to patches/api/0043-Add-back-player-spawned-endermite-API.patch diff --git a/patches/server/0001-Tuinity-Server-Changes.patch b/patches/server/0001-Tuinity-Server-Changes.patch new file mode 100644 index 000000000..d022b1114 --- /dev/null +++ b/patches/server/0001-Tuinity-Server-Changes.patch @@ -0,0 +1,17806 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Initial Source +Date: Sat, 26 Jun 2021 23:30:38 -0500 +Subject: [PATCH] Tuinity Server Changes + +Build changes + +Update version fetcher repo + +Sets the target github repo to Tuinity in the version checker. Also disables the jenkins build lookups. + +This patch is licensed under the MIT license. See /licenses/MIT.md. + +MC-Dev fixes + +Util patch + +Tuinity Server Config + +Fix incorrect isRealPlayer init + +Some plugins, namely ProtocolSupport, don't route to where +paper placed their logic. So it wont correctly set in this case. + +Fix by moving it to a different place. + +Allow controlled flushing for network manager + +Only make one flush call when emptying the packet queue too + +This patch will be used to optimise out flush calls in later +patches. + +Not implemeneted + +Currently a placeholder patch. + +Rewrite the light engine + +The standard vanilla light engine is plagued by +awful performance. Paper's changes to the light engine +help a bit, however they appear to cause some lighting +errors - most easily noticed in coral generation. + +The vanilla light engine's is too abstract to be modified - +so an entirely new implementation is required to fix the +performance and lighting errors. + +The new implementation is designed primarily to optimise +light level propagations (increase and decrease). Unlike +the vanilla light engine, this implementation tracks more +information per queued value when performing a +breadth first search. Vanilla just tracks coordinate, which +means every time they handle a queued value, they must +also determine the coordinate's target light level +from its neighbours - very wasteful, especially considering +these checks read neighbour block data. +The new light engine tracks both position and target level, +as well as whether the target block needs to be read at all +(for checking sided propagation). So, the work done per coordinate +is significantly reduced because no work is done for calculating +the target level. +In my testing, the block get calls were reduced by approximately +an order of magnitude. However, the light read checks were only +reduced by approximately 2x - but this is fine, light read checks +are extremely cheap compared to block gets. + +Generation testing showed that the new light engine improved +total generation (not lighting itself, but the whole generation process) +by 2x. According to cpu time, the light engine itself spent 10x less time +lighting chunks for generation. + +Rewrite entity bounding box lookup calls + +For whatever reason, Mojang thought it was OK to make this system +scale logn relative to the number of entity sections loaded. +On top of that, they do a hashtable lookup per section - before +this was just a basic array access. + +This patch brings back entity slices for lookup only. + +Highly optimise single and multi-AABB VoxelShapes and collisions + +Optimise chunk tick iteration + +Use a dedicated list of entity ticking chunks to reduce the cost + +Execute chunk tasks mid-tick + +This will help the server load chunks if tick times are high. + +Change writes to use NORMAL priority rather than LOW + +Should limit build up of I/O tasks, or at least properly +indicate to server owners that I/O is falling behind + +Per World Spawn Limits + +This patch is licensed under the MIT license. See /licenses/MIT.md. + +Make CallbackExecutor strict again + +The correct fix for double scheduling is to avoid it. The reason +this class is used is because double scheduling causes issues +elsewhere, and it acts as an explicit detector of what double +schedules. Effectively, use the callback executor as a tool of +finding issues rather than hiding these issues. + +This patch also reverts incorrect use(s) of the class by paper. + +- getChunkFutureAsynchronously + There is no risk at all of recursion. The future is executed on + the chunk provider's thread queue, the same place general plugin + load callbacks are executed on. Forcing the task execution into + the callback executor also prevents the future from catching + any exception thrown from it. + +Improve async tp to not load chunks when crossing worlds + +Fixes an issue where a getCubes call would load neighbouring chunks. +Loads less chunks than paper's implementation + +Revert getChunkAt(Async) retaining chunks for long periods of time + +Rework PlayerChunk main thread checks + +These need to fail instead of continuing, as hiding these errors +the way paper has is just going to allow unexpected reordering +of callbacks. + +For example, thanks to this patch incorrect future +completion (completion of the world gen future, +PlayerChunkMap#b(PlayerChunk, ChunkStatus)) was detected and fixed. + +Prevent unload() calls removing tickets for sync loads + +Do not allow ticket level changes while unloading playerchunks + +Sync loading the chunk at this stage would cause it to load +older data, as well as screwing our region state. + +Revert MC-4 fix + +When messing around with collisions, I ran into problems where +entity position was off by ULP and that caused clipping problems. +Now, the collision epsilon is 1.0e-7 to account for those errors. + +But this patch is going to cause problems on the order of 1.0e-4. + +I do not want to deal with clipping problems. The very fact it works +shows it's causing the clipping to occur serverside. + +Don't allow StructureLocateEvent to change worlds + +Callers and even the function itself aren't expecting +this to happen + +Do not allow the server to unload chunks at request of plugins + +In general the chunk system is not well suited for this behavior, +especially if it is called during a chunk load. The chunks pushed +to be unloaded will simply be unloaded next tick, rather than +immediately. + +Do not run close logic for inventories on chunk unload + +Still call the event and change the active container though. We +want to avoid close logic because it's possible to load the +chunk through it. This should also be OK from a leak prevention/ +state desync POV because the TE is getting unloaded anyways. + +Correctly handle recursion for chunkholder updates + +If a chunk ticket level is brought up while unloading it would +cause a recursive call which would handle the increase but then +the caller would think the chunk would be unloaded. + +Don't read neighbour chunk data off disk when converting chunks + +Lighting is purged on update anyways, so let's not add more +into the conversion process + +Do not copy visible chunks + +For servers with a lot of chunk holders, copying for each +tickDistanceManager call can take up quite a bit in +the function. I saw approximately 1/3rd of the function +on the copy. + +Replace player chunk loader system + +The old one has undebuggable problems. Rewriting seems +the most sensible option. + +This new player chunk manager will also strictly rate limit +chunk sends so that netty threads do not get overloaded, whether +it be from the anti-xray logic or the compression itself. + +Chunk loading is also rate limited in the same manner, so this +will result in a maximum responsiveness for change. + +Config: +``` +player-chunks: + autoconfig-send-distance: true + min-load-radius: 3 + max-concurrent-sends: 12.0 + max-concurrent-loads: 5.0 +``` + +autoconfig-send-distance - Whether to try to use the client's +view distance for the send view distance in the server. In the +case that no plugin has explicitly set the send distance and +the client view distance is less-than the server's send distance, +the client's view distance will be used. This will not affect +tick view distance or no-tick view distance. + +min-load-radius - The radius of chunks around a player that +are not throttled for loading. The number of chunks +affected is actually the configured value plus one as this +config controls the chunks the client will be able to render. + +max-concurrent-sends - The maximum number of chunks that +can be queued to send at any given time. Low values +are generally going to solve server-sided networking +bottlenecks like anti-xray and chunk compression. Client +side networking is unlikely to be helped (i.e this wont help +people running off McDonald's wifi). Setting this +value to negative will make the server dynamically scale it +with players. i.e -5 will use 5 * online players for the max sends. + +max-concurrent-loads - The maxmium number of chunks +that can be queued to be loaded at any given time. Lower +values help the responsitivity to player movement and +higher values help loading when the server is at a low TPS. + +Replace ticket level propagator + +Mojang's propagator is slow, and this isn't surprising +given it's built on the same utilities the vanilla light engine +is built on. The simple propagator I wrote is approximately 4x +faster when simulating player movement. For a long time timing +reports have shown this function take up significant tick, ( +approx 10% or more), and async sampling data shows the level +propagation alone takes up a significant amount. So this +should help with that. A big side effect is that mid-tick +will be more effective, since more time will be allocated +to actually processing chunk tasks vs the ticket level updates. + +Attempt to recalculate regionfile header if it is corrupt + +Instead of trying to relocate the chunk, which is seems to never +be the correct choice, so we end up duplicating or swapping chunks, +we instead drop the current regionfile header and recalculate - +hoping that at least then we don't swap chunks, and maybe recover +them all. + +Custom table implementation for blockstate state lookups + +Testing some redstone intensive machines showed to bring about a 10% +improvement. + +Detail more information in watchdog dumps + +- Dump position, world, velocity, and uuid for currently ticking entities +- Dump player name, player uuid, position, and world for packet handling + +Optimise collision checking in player move packet handling + +Move collision logic to just the hasNewCollision call instead of getCubes + hasNewCollision + +Manually inline methods in BlockPosition + +Separate lookup locking from state access in UserCache + +Prevent lookups from stalling simple state access/write calls + +Distance manager tick timings + +Recently this has been taking up more time, so add a timings to +really figure out how much. + +Name craft scheduler threads according to the plugin using them + +Provides quick access to culprits running far more threads than +they should be + +Make sure inlined getChunkAt has inlined logic for loaded chunks + +Tux did some profiling some time ago and showed that the +previous getChunkAt method which had inlined logic for loaded +chunks did get inlined, but the standard CPS.getChunkAt +method was not inlined. + +Paper recently reverted this optimisation, so it's been reintroduced +here. + +Add packet limiter config + +Example config: +packet-limiter: + kick-message: '&cSent too many packets' + limits: + all: + interval: 7.0 + max-packet-rate: 500.0 + PacketPlayInAutoRecipe: + interval: 4.0 + max-packet-rate: 5.0 + action: DROP + +all section refers to all incoming packets, the action for all is +hard coded to KICK. + +For specific limits, the section name is the class's name, +and an action can be defined: DROP or KICK + +If interval or rate are less-than 0, the limit is ignored + +Lag compensate block breaking + +Use time instead of ticks if ticks fall behind + +Fix chunks refusing to unload at low TPS + +The full chunk future is appended to the chunk save future, but +when moving to unloaded ticket level it is not being completed with +the empty chunk access, so the chunk save must wait for the full +chunk future to complete. We can simply schedule to the immediate +executor to get this effect, rather than the main mailbox. + +Use hash table for maintaing changed block set + +When a lot of block changes occur the iteration for checking can +add up a bit and cause a small performance impact. + +Consolidate flush calls for entity tracker packets + +Most server packets seem to be sent from here, so try to avoid +expensive flush calls from them. + +This change was motivated due to local testing: + +- My server spawn has 130 cows in it (for testing a prev. patch) +- Try to let 200 players join spawn + +Without this change, I could only get 20 players on before they +all started timing out due to the load put on the Netty I/O threads. + +With this change I could get all 200 on at 0ms ping. + +(one of the primary issues is that my CPU is kinda trash, and having +4 extra threads at 100% is just too much for it). + +So in general this patch should reduce Netty I/O thread load. + +Don't lookup fluid state when raytracing + +Just use the iblockdata already retrieved, removes a getType call. + +Time scoreboard search + +Plugins leaking scoreboards will make this very expensive, +let server owners debug it easily + +Send full pos packets for hard colliding entities + +Prevent collision problems due to desync (i.e boats) + +Configurable under +`send-full-pos-for-hard-colliding-entities` + +Do not run raytrace logic for AIR + +Saves approx. 5% for the raytrace call, as most (expensive) +raytracing tends to go through air and returning early is an +easy win. The remaining problems with this function +are mostly with the block getting itself. + +Make entity tracker use highest range of passengers + +This should prevent people from having to up their animal range +just so players riding horses or whatever can be seen at the +configured player range. + +Oprimise map impl for tracked players + +Reference2BooleanOpenHashMap is going to have +better lookups than HashMap. + +Stop large move vectors in player packet handling from killing the server + +Looks like we need to check three vectors, not two. fun. + +Optimise BlockSoil nearby water lookup + +Apparently the abstract block iteration was taking about +75% of the method call. + +Fix BlockPos reobf problem + +Non-reobf'd fields don't conflict, but reobf'd do! So +explicitly cast on field access to avoid. + +Allow removal/addition of entities to entity ticklist during tick + +It really doesn't make any sense that we would iterate over removed +entities during tick. Sure - tick entity checks removed, but +does it check if the entity is in an entity ticking chunk? +No it doesn't. So, allowing removal while iteration +ENSURES only entities MARKED TO TICK are ticked. + +Do not allow ticket level changes when updating chunk ticking state + +This WILL cause state corruption if it happens. So, don't +allow it. + +Optimise CraftChunk#getEntities + +Why the fuck was it iterating over every single entity +in the world + +Optimise random block ticking + +Massive performance improvement for random block ticking. +The performance increase comes from the fact that the vast +majority of attempted block ticks (~95% in my testing) fail +because the randomly selected block is not tickable. + +Now only tickable blocks are targeted, however this means that +the maximum number of block ticks occurs per chunk. However, +not all chunks are going to be targeted. The percent chance +of a chunk being targeted is based on how many tickable blocks +are in the chunk. +This means that while block ticks are spread out less, the +total number of blocks ticked per world tick remains the same. +Therefore, the chance of a random tickable block being ticked +remains the same. + +Optimise non-flush packet sending + +Places like entity tracking make heavy use of packet sending, +and internally netty will use some very expensive thread wakeup +calls when scheduling. + +Thanks to various hacks in ProtocolLib as well as other +plugins, we cannot simply use a queue of packets to group +send on execute. We have to call execute for each packet. + +Tux's suggestion here is exactly what was needed - tag +the Runnable indicating it should not make a wakeup call. + +Big thanks to Tux for making this possible as I had given +up on this optimisation before he came along. + +Locally this patch drops the entity tracker tick by a full 1.5x. + +Optimise nearby player lookups + +Use a distance map to map out close players. +Note that it's important that we cache the distance map value per chunk +since the penalty of a map lookup could outweigh the benefits of +searching less players (as it basically did in the outside range patch). + +Fix Codec log spam + +Mojang did NOT add dataconverters for world gen configurations +that they CHANGED. So, the codec fails to parse old data. + +This fixes two instances: +- IntProvider is new and Mojang did not account for old data. + Thankfully, only ColumnPlace needed to be special cased. +- TreeConfiguration had changes. Thankfully, they were + only renames for one value and thankfully defaults could + be provided for two new values (WITHOUT changing behavior). + +diff --git a/build.gradle.kts b/build.gradle.kts +index 5540da58e66f83b283863d3158a9b4ab5ba636db..f766183883f009368b79e1201553c184a373e2cf 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -30,8 +30,8 @@ repositories { + } + + dependencies { +- implementation(project(":Paper-API")) +- implementation(project(":Paper-MojangAPI")) ++ implementation(project(":Tuinity-API")) // Tuinity ++ implementation("com.destroystokyo.paper:paper-mojangapi:1.16.5-R0.1-SNAPSHOT") // Tuinity + // Paper start + implementation("org.jline:jline-terminal-jansi:3.12.1") + implementation("net.minecrell:terminalconsoleappender:1.2.0") +@@ -82,7 +82,7 @@ tasks.jar { + attributes( + "Main-Class" to "org.bukkit.craftbukkit.Main", + "Implementation-Title" to "CraftBukkit", +- "Implementation-Version" to "git-Paper-$implementationVersion", ++ "Implementation-Version" to "git-Tuinity-$implementationVersion", // Tuinity + "Implementation-Vendor" to SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(Date()), // Paper + "Specification-Title" to "Bukkit", + "Specification-Version" to project.version, +@@ -178,7 +178,7 @@ tasks.test { + fun TaskContainer.registerRunTask( + name: String, block: JavaExec.() -> Unit + ): TaskProvider = register(name) { +- group = "paper" ++ group = "paperweight" + 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..e883b2af853666c11a14a541aac331d97c6ad057 +--- /dev/null ++++ b/src/main/java/com/tuinity/tuinity/util/CollisionUtil.java +@@ -0,0 +1,600 @@ ++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.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.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.Shapes; ++import net.minecraft.world.phys.shapes.VoxelShape; ++import java.util.List; ++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 (entity != null) { ++ 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 = (ServerChunkCache)((WorldGenRegion)getter).getChunkSource(); ++ } 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 = entity == null ? CollisionContext.empty() : CollisionContext.of(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.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); ++ } ++ } ++ ++ 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/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..009febf8fccbfc9a638f018f921ebd9ec9068196 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); ++ ((Vec3i)this).x = x; // Tuinity - force inline ++ ((Vec3i)this).y = y; // Tuinity - force inline ++ ((Vec3i)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); ++ ((Vec3i)this).x = i; // Tuinity + return this; + } + + @Override + public BlockPos.MutableBlockPos setY(int i) { +- super.setY(i); ++ ((Vec3i)this).y = i; // Tuinity + return this; + } + + @Override + public BlockPos.MutableBlockPos setZ(int i) { +- super.setZ(i); ++ ((Vec3i)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 c203a78a28e6457bd25b34b5c5ecaa35e3f9211e..0232fb8123c7dfa735802442f8575c6ce1566847 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 7682bd72c3932a9b20f14e552711d74f70b969b1..f3bf6270c7735869083559f907c65d95144dbe6f 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -297,6 +297,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); + public int autosavePeriod; + public boolean serverAutoSave = false; // Paper +@@ -331,6 +332,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 +@@ -1133,6 +1204,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(); +@@ -1457,6 +1516,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 systemreport) { +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index 6a1fd7a983724d9e16e8aef06052108ba7ed46f1..9bb0ff063da162f2b5c91d367d9555c1cf1a3ab1 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -222,6 +222,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 9fe60d058ea1702930981dbd06093dc594e6bf8e..2dd5909a08a8d4bc250b36d297ef9f3f04aed8bf 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)) { +@@ -783,11 +850,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 7f7bc04a30a0422b2d589adb488082c0aa5326dc..1430411a6f7ef3730d87c022774d7d623f2f415f 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -103,6 +103,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 { + +@@ -115,8 +116,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 +@@ -139,8 +141,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 +@@ -148,7 +149,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; +@@ -181,32 +182,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 + } + } + }; +@@ -215,22 +213,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; +@@ -239,7 +222,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); +@@ -256,6 +239,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()); +@@ -266,7 +255,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 +@@ -275,19 +264,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) { +@@ -300,11 +280,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) { +@@ -316,27 +295,50 @@ 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; ++ ++ public static final class DataRegionData implements com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionData { ++ } ++ ++ public static final class DataRegionSectionData implements com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSectionData { ++ ++ @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; ++ } + +- 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) ++ @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; + } ++ } + +- 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 final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) { ++ return this.pendingUnloads.get(com.tuinity.tuinity.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } +- // Paper end ++ // Tuiniy end ++ ++ boolean unloadingPlayerChunk = false; // Tuinity - do not allow ticket level changes while unloading chunks + + 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); +@@ -453,53 +455,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, ++ 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) -> { +- checkHighPriorityChunks(player); +- if (newState.size() != 1) { +- return; +- } +- LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(rangeX, rangeZ); +- if (chunk == null || !chunk.areNeighboursLoaded(2)) { +- 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().addTicketAtLevel(TicketType.PLAYER, chunkPos, 31, chunkPos); // entity ticking level, TODO check on update + }, + (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { +- if (newState != null) { +- return; ++ 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)); +- } +- 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 +@@ -533,6 +510,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; +@@ -540,7 +518,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 +@@ -606,7 +584,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()); + } +@@ -674,7 +652,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 +@@ -682,47 +660,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) { +@@ -844,12 +800,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) { +@@ -864,11 +821,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; + } + +@@ -1023,7 +986,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); +@@ -1060,7 +1023,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; +@@ -1081,7 +1044,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); +@@ -1097,7 +1060,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); + } +@@ -1122,7 +1097,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 + + } + }; +@@ -1141,19 +1122,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; +@@ -1166,11 +1139,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.FEATURES), 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) -> { +@@ -1182,6 +1164,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 + } + } + +@@ -1282,7 +1265,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; + } +@@ -1313,7 +1296,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) { +@@ -1472,9 +1458,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() { +@@ -1559,7 +1543,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 + } + + } +@@ -1567,26 +1551,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()); +@@ -1610,7 +1579,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() { +@@ -1915,6 +1884,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 + + } + +@@ -1923,7 +1893,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(); +@@ -1939,8 +1909,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); +@@ -1955,6 +1926,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); + } + } +@@ -2265,7 +2237,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 +@@ -2365,7 +2337,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 +@@ -2400,7 +2372,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 82d3cfb2d346a8b929e9469ae09369f6a639f81d..958b7044c196ebd66f60391c33c64ad2ff82d4e8 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -48,6 +48,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; +@@ -139,7 +140,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) { +@@ -201,9 +202,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) { +@@ -227,6 +228,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); +@@ -570,6 +731,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); +@@ -588,9 +751,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(); +@@ -601,12 +767,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)); + } +@@ -664,6 +838,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(); + +@@ -673,6 +849,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 +@@ -730,6 +907,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(); +@@ -745,17 +923,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(); +@@ -833,12 +1012,15 @@ 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()) { ++ // 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"); +- LevelChunk chunk = (LevelChunk) optional.get(); ++ // Tuinity end + + this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timings + playerchunk.broadcastChanges(chunk); +@@ -846,11 +1028,11 @@ public class ServerChunkCache extends ChunkSource { + this.level.getProfiler().pop(); + 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 +@@ -858,7 +1040,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) { +@@ -871,7 +1057,24 @@ public class ServerChunkCache extends ChunkSource { + 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) { +@@ -1018,46 +1221,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 44aa0c4ec6f0e4df2541c74fa7de852dae59bda5..a00627e0fa38632449042f59c053b4dac13e58bf 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 87b3eee23441717660aa6bb81248986cb7ef2fb4..a4546a80b0d1fde8c4f92fea575745b1352d95b0 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; +@@ -588,7 +770,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,76 @@ 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); ++ 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 + Biome.Precipitation biomebase_precipitation = this.getBiome(blockposition).getPrecipitation(); + +- if (biomebase_precipitation == Biome.Precipitation.RAIN && biomebase.isColdEnoughToSnow(blockposition1)) { ++ 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 +1110,26 @@ 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"); ++ 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 +1170,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 +1468,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 +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 935b22199ebdf84db591f8442e0506d8fcc92e02..217af30c81438836864312e125cf512b491a3ab4 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -255,7 +255,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) { +@@ -418,7 +418,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; + } + } +@@ -426,7 +426,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()); + } + } +@@ -1557,6 +1557,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/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 8708eb173fbb17cf84a0c6d9a42623750472f650..9dc5c82ea1d497174d288000f9f1ccaabbcb4a18 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -537,6 +537,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; +@@ -577,12 +583,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(); +@@ -596,16 +603,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)); +@@ -691,7 +705,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 +@@ -1247,7 +1286,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()); + } +@@ -1286,6 +1325,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) { +@@ -1335,11 +1380,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) { +@@ -1374,6 +1419,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) { +@@ -1393,12 +1439,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 +@@ -1485,6 +1542,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 9a428e166561b4bc028732ec563d3b2e99f81a8e..771c575ffe46db94d9c91f3fd0440d4deb460de7 100644 +--- a/src/main/java/net/minecraft/server/players/GameProfileCache.java ++++ b/src/main/java/net/minecraft/server/players/GameProfileCache.java +@@ -60,6 +60,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; +@@ -67,6 +72,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()); +@@ -81,6 +87,7 @@ public class GameProfileCache { + if (uuid != null) { + this.profilesByUUID.put(uuid, entry); + } ++ } finally { this.stateLock.unlock(); } // Tuinity - allow better concurrency + + } + +@@ -118,7 +125,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()); +@@ -135,8 +142,9 @@ public class GameProfileCache { + } + + @Nullable +- public synchronized GameProfile get(String name) { // Paper - synchronize ++ public GameProfile 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; + +@@ -150,10 +158,14 @@ public class GameProfileCache { + GameProfile gameprofile; + + if (usercache_usercacheentry != null) { ++ stateLocked = false; this.stateLock.unlock(); // Tuinity - allow better concurrency + usercache_usercacheentry.setLastAccess(this.getNextOperation()); + gameprofile = usercache_usercacheentry.getProfile(); + } else { ++ stateLocked = false; this.stateLock.unlock(); // Tuinity - allow better concurrency ++ try { this.lookupLock.lock(); // Tuinity - allow better concurrency + gameprofile = GameProfileCache.lookupGameProfile(this.profileRepository, name); // Spigot - use correct case for offline players ++ } finally { this.lookupLock.unlock(); } // Tuinity - allow better concurrency + if (gameprofile != null) { + this.add(gameprofile); + flag = false; +@@ -165,6 +177,7 @@ public class GameProfileCache { + } + + return gameprofile; ++ } finally { if (stateLocked) { this.stateLock.unlock(); } } // Tuinity - allow better concurrency + } + + public void getAsync(String s, Consumer consumer) { +@@ -326,7 +339,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 a6ecbb7eb1df7923d58366327e0f00512a3a1677..d65ca67bb0d46ecd8a4003e44cfec71661679480 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -175,6 +175,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); +@@ -264,7 +265,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())); +@@ -723,7 +724,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()); + +@@ -927,13 +928,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())); +@@ -1208,7 +1209,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/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 4fd030ef9537d9b31c6167d73349f4c4a6b33a15..ca7718053a6a2eb715ea3671bd4bc15304ede420 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 c4c5c35e37b793f3b74349ff03c0829f4913b91c..154b3c767d079f72643c826b962892c1029b0a1b 100644 +--- a/src/main/java/net/minecraft/world/entity/Mob.java ++++ b/src/main/java/net/minecraft/world/entity/Mob.java +@@ -784,7 +784,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/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +index 6c3455823f996e0421975b7f4a00f4e333e9f514..7db31cc24e68aab55a9ba735165da23059bdc626 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 +@@ -183,7 +183,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/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 1bccd932851045c374e3092d33dc77fab680d0db..069f658003d96a05aac0b30af1d89f15ea554475 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 1c171599dbde44389013dc08ca8263742d58d721..862553e98a7237d39a26036c615b9cbd001bd3bf 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -166,6 +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 co.aikar.timings.WorldTimingsHandler timings; // Paper + public static BlockPos lastPhysicsProblem; // Spigot + private org.spigotmc.TickLimiter entityLimiter; +@@ -195,9 +197,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.getEntities(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 +@@ -271,6 +381,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 +@@ -356,6 +467,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 + } + +@@ -544,7 +664,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 + } +@@ -855,6 +975,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 +@@ -984,26 +1105,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; + } + +@@ -1012,26 +1114,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; + } + +@@ -1319,10 +1417,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 55b937f802ee7066cb13b9a497932038b2905ff0..b2329de658df71ce03a6fe0c41c02997afdf868f 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/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 c2b0b1adcff5baf169901710d492317d44b93846..c7636191fa2ba92db95a7f779d0e5a1bd45198aa 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 25570730f376665ca6477263d3b3f94d725ecd21..21d85b2f70e5ffe46220905b27715579d7fcdc59 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/DataLayer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/DataLayer.java +@@ -11,7 +11,7 @@ public class DataLayer { + public static final int LAYER_SIZE = 128; + 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 byte[] EMPTY_NIBBLE = new byte[2048]; + private static final int nibbleBucketSizeMultiplier = Integer.getInteger("Paper.nibbleBucketSize", 3072); +@@ -54,6 +54,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 { +@@ -68,7 +69,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) { +@@ -153,7 +154,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 18432dc1e5067d65cda709b7b3bcc2dd37b77d02..917fa5a3106259c01d6a01acf770890dbdf50f1a 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/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/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java +index e94b5a7fe47831e2c3e0935e316737a2422e4250..265343f8663e0c9551ed286d954889ad08043b16 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); +@@ -93,6 +95,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 +150,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); + } + +@@ -383,6 +387,7 @@ public class PersistentEntitySectionManager implements A + BlockPos blockPos = this.entity.blockPosition(); + long l = SectionPos.asLong(blockPos); + if (l != this.currentSectionKey) { ++ PersistentEntitySectionManager.this.entitySliceManager.moveEntity((Entity)this.entity); // Tuinity + Visibility visibility = this.currentSection.getStatus(); + if (!this.currentSection.remove(this.entity)) { + PersistentEntitySectionManager.LOGGER.warn("Entity {} wasn't found in section {} (moving to {})", this.entity, SectionPos.of(this.currentSectionKey), l); +@@ -426,6 +431,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/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/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +index 08a5fabb1d13db26014bb5751aa271c0a0bdcb7a..05dae6fae8482dba551974f3a348d86b30c47c96 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +@@ -111,10 +111,7 @@ public class CraftChunk implements Chunk { + } + + Location location = new Location(null, 0, 0, 0); +- return this.getWorld().getEntities().stream().filter((entity) -> { +- entity.getLocation(location); +- return location.getBlockX() >> 4 == this.x && location.getBlockZ() >> 4 == this.z; +- }).toArray(Entity[]::new); ++ return ((CraftWorld)this.getWorld()).getHandle().getChunkEntities(this.x, this.z); // Tuinity - optimise this... jesus christ, iterating over all entities? + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index b020d44f95ab8aafb270ca6e36abe86d6589fc9b..2aa45667054a721465f353f0500e30d90f7d16a4 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 +@@ -2364,6 +2366,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 32f82c60b680180b256edff127e5a6ded42fccf4..332c33c95cc450d76f512e58dd6d4b61ea2de431 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 76e2ae09855e0efaaa0856d2f49e4968adbccbdc..fc8a8ccd4273ec5d44b216d96034ac048a2ac38e 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..7361bf04de16d0526dc4cdbd0f564713df041d90 100644 +--- a/src/main/java/org/spigotmc/ActivationRange.java ++++ b/src/main/java/org/spigotmc/ActivationRange.java +@@ -205,7 +205,7 @@ public class ActivationRange + ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate( villagerActivationRange, 256, waterActivationRange ); + // Paper end + +- world.getEntities().get(maxBB, ActivationRange::activateEntity); ++ world.getEntities(null, maxBB).forEach(ActivationRange::activateEntity); // Tuinity + } + 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 +literal 23418 +zcmeFYWmH_-wk=#}0m0oV+}+*X-CYZUyF+mI1a}V}+)04oE&+lQG(i$vzslbGp0n?J +z=ic{jYv22KpcSyj9HY-U`xv9I#adM{YAUiQNQ6iL002c^PD%svd*t^E0Uq+RRpJCnpDr?!(Jf-{(g()1mU$77W~Nxy+`GL;Hd+mDj%< +z`hrC2`H9Xp$xKhL+Bo|HKmFo7=M-Xnd+W#BZSvAC(iiMjve~^o$4Z89_W8CRjpga8 +zv+emII+5u5MEB{@eX9E4yRDb7&Bx>ddEq2BvbJprC(KVzdi!1Hv=?V?eMFm!UV_HW +z<0^u;`E~qf&`C*71k3BLYs^6^yabk)Yj{C^qJi!8i)sPCIKOO3EVK#shU4l~4n!S! +zBamlrZCbkCbo>x;?i@>dx2tj1p0!K)a=rGr2p4?tkPD7Uai7@eu^4La)+NpTh=0lZ +zE(P78GpKSAt^%r$xC~`T`{upc*$uzT!TfrA3JV59PrYKoJM^id?usWu%mjxS3 +zGIWly9E+e~H|x~U79-+u{MV>MAb|`0-4G}cnLI5ADba_gfd16vBrVvaLTf}7i67f- +zUV>Jio#tHA0!~sm0$WpoKSN7XaeXh@QicB@RolHM98-~@BV9w0u_uzyp}OO(fy4Wr +zJ6yWa1vkdp)+N7#EaB{Yfr3+`*@tt=c)q!jl6ZmDL#*2gzWIp{69Q}B$Lkw*6d3E9 +zj_uz&w}qyzxK`wOuXwhtqv;8552mSzd|Q}T3AX)_u}HT0BEIyM(c#%fVL8Kl$)^Q- +z{cz)BjBTo$%y58*RW}_BV3pCfwG+RGqat@@zbbZ*z)HjE?T7VtC8S+15t5s!b=&=C +zuMOnJ=l9)reQEq93r~0)2a~9Mdp~&f4J|(Ua$ai{S=hrYe&$}+ef~hR0L`9fAlnp& +zU~_CYxN1ByF`|jyx`3l081=asXX^0V#MfW0Y0VrDu1g;K6Cwtlws(!g$mm8Fc@kmk +zHk0P``>>$trX +zOKO@8`AeIqg~@X}4s0Zdc8nGX+gS2a=5c65je_?zXn&ci*C*B5)?qG%26@N56+L?} +z_?X#4UxDE@4hnDzJay4|Cf4;AR@J+8U27CLTh9I}A5l!L!2P|^Ua+g%qMzCQE@)hL +zA~-&6za6OQ9%Z{iDi=UpL2anxJKdKrIBFb}_xMXd-+yzVJCa`;xB$c`45(a{uL_?H +z^UnJDof4?Z78^HM^tq81kY(+E0IU@LG@-q&gT3lP`C0BLFBi5s1Hd|u9H{b+){b^? +zVfdKsCmro&{*7zt>f#S%YdjlEHqY`MS=yCESuK;xK&3U=$+|Z6v2q#ROz^|kPh-CI +z>sUvd=>Cp2gT~p^Qg3DBWn_`9>(sprp=nf0XDapeDmb1HW5tc+++|m7V{%gSN~|TR +z2bCXzg_I)5(8c;v>pwESE4h025uEHWm_tcvSv0-E6WR7g3T6}C=sFV%aA~|;mXOgp +zbsW9;LREE09ya@GgH3vFkvo)fU+79v^vy#Rp>OwSYk=3Vr;1)qSCZpdl#Z+8kNuNv +zr^K;X5$X^A^f#^C!Z%c=re|~`tieWGWEBCuDKtv+o*#NWII)B$(msOTpG_kyd|Tr} +zl)isjXV6?~MrnDMVo}|bMAWHywlydwi}d}AA=Ng?^q9luER>~(DYgLsJPsoth{ +z^L0l>OMO@3z^=mT0BbDwT+^};TfH95K77HfImHl;O0l6Aa0i~unu9BP)>#ALajXQY +z(IAA-I$io%FzIkRDIA|X@Q&P^4W(}lk35PWxacGM{hEaQV`_!->QPK!o8{@lbl2vF +zAcCregL@&8&6g|$_T4n2`{CTidQJRh@uSlrAblg^dVxZ_UF{=zb0Tl@30I07F>s); +zB&Kq)g3KwCC(YS#qB6%v(@u$+^UMy-IEbUBQRDrBd@SheXBkgd<+C)wHq7gZ(RrO} +zX!YuqAdL^xp06x-d@$GZi>I*hU^L$u?W*m7&ycfFm$3!GKI38q6HzRhlyDjtX3lk +zl_?UG_9#t6uvIWv*&fx{vD>Y@YE%u*tmbl(Bt~m|8qUIR0E|ALe6o5B=)H;yd_lB9t3rQ!Y&l&## +zzEU*<$W>9t0Q=QZHACiN3EfqjA0BYIZ~f*20=YI6 +z{Em>2YX%d;v8O`|!SJKCxu8K^-rF~)StR^C_LzNYw?u|A-=xtK-E-CjvDW|v%}GaV +zP>LRLIvV@eU*8rL+c>f^D~|Rho639&VFj+l>yKJ4eIbWwD1l)MhOwTp9h+ +zK!7t0Oq{PSv;PgWzj^ +zi2;$Q6@7i22*WE_EZhAtRz=}@`u0)mfGB7G72hJ-GiF`^`eUMM(=a7(D(M}eL3wZG +zFgJ!gwvo5oi;MJpB%3^>s1ziW!x29I>H*20qpye1QzPY>*ui(rBWy_*%W{S`Y{OcR +z;zbDin`pyGAs3FMCRuEgvf!aoa@6cdNO8fd3@T}3!-A}!yy%eqNH6iIQ=srUm7A-3 +z!8WfxcESolm%~pvfNKS|ZTrS^QSb(nHCJKWNSZP1GS$UHdHlbS(qQ*g+7V7wT2`i! +zCxckawhv=4=em7PU-BOj;j-nuX7I#<1U2USu!s`sw|Qj0q(K7!e^j9MdH2b +zX}Up?>zr*gO%5&!lvoTUo{7b6mpGhRS1CzUtDJF4sWML|SaLy?OkcEgdVghZenRPl +zT*Mv9rd-4+A|eydl#C&mDzu6tWq+O)Y&AkmU>Bp27=kM5vFtu`ZzpZj@vGCo%vv{! +zzD)2e@LRT0LJOtL8t(XrO%vUysrYBGv+PiA9+s_+d00Q+Os}~m2GF=$VK(3)kO;U) +zjZ(o|6V%dWCy8&B&RJwqs2^}HSFy}|r+o@ApT97fjefwV(2k_5KvPxZ-@*}agR2!XW+R!#l>B$r!C#W9v@`sABQLji9L~ +z)E1^q%&2Ce@bIuybSCHF!L{|^5@~kU7GR9=h@2P6w~h<7IaY-IJ<_^ooUV+zZ666S +zPKvBHrs$lAcyJSiemBCBcrQMAiG$%VZ0yLA$2k;--khf~luvd6wPK;ZDyK5m5)H1l +z<8p|zfHgHurFh+Dl3F&5`tfWjh%Edv0XKZ&3cWqpoOL`l`+Y7Se&h#|a#-sSP{z-G +ziX!wTBm`(FcNsQuP1zVT*(2S!mL~Je1pw;qTk?&33{E4vlsj$W +zD3L=e#5ax!b$WJjG1qUw*eHu{+CGOFQ05#}RKFBY}O*@~B?zB{4V4QR~FA90s~- +zTKiB$qixoEEs{0}1i#-YNRS~(W +z+{it>$5Jd1@V3{zx5SblB_?Fu@}BAY>4MKV7)uaD@B0o|-X&pb@X`s!e#p +za}k=v@U~28t!OD3agfASR#`p6-YSbF+?KXJ0zQs@_^7C>zkHs;MZi&y>RCd9LkN`Q +zJRK@|*Nj#HK83KbiBT-{+q#NH`_$I98#vTp~vq(WNYq`8zQ!0Z_?>pjekqE&Jk +z6>6!8e$D3%XC;=(2A4j?jm*Hbk9?*^SL`5wFqWEiFL_TdMyfL_rVlL{*ObwlYlxAh +zVH!e4p`PV~jdCFi4Wy0ML>51?9<~8#Ag!|4C%nF!S_?IAf&I&r4;c`LVFeW9L +zajZu)Pq4%1+b_!)#4 +zV5`C%5cC}Rf|#gMmiRC}MHb?qMNtZR3GT8u8tlCp_pM&%K3$7kGKyx_>gilk&0Q3M +zT52YLk0#NRvl?xnB^|!Sp+lvsiqP|!Ib-zj-ICTVI;u}U6|gJ21zvgZ2=$zd{5n)+ +z+#wEOy%@GD6J$Ub>x~*&iLtd@+xN|4b4G^E7Z`M> +z#%ESGzLCqN5C%_G*l)|8+_vjhJvprN{H*nTr_}qic0be&HDBX%p;jgBt45ycsTmaH +zHCCI~uF*<}&178-@c5V^wC9;K_$vf*&5LQzzkG4x8EchNJ<05*$Y2lF>%8Bi6-Ll3 +zL1S@6ChLdHvxaH50O!`9;4?a5*H@zl$i&e3$gJq@mq`gxNLMvNO&F13kjC?;>M{li +zk=Z4eoD}aXu7wXN8_9U9xV*yZ~=F;wK)KM@Rc?AOHCXJ|CY +z?hshFPBFrd*z49R)eok%Eli-{C7VRFEBcda;lRV{&HVy&#rLoAefF-PkhLtrznBe2 +zw8$Cw=?*e?BZWa})K4;^p7eN&R9? +zD^F$@_(;-5){)bDug8pywS=C!bbs8Lua;QlFY@IN>3*{r-Q|^$znkGx7uVMX8BScP +ztPzM42}my8tn80z=Hu0>>>mx>wV2JJe?0lHyxv3Pcq-XqpFU!^|MPW0f8HR*Xk+tc +z9ZA$WJy9*&Y<`*Q5u05|4Dm!I?>&8PLl=Kw +zn3#G`ePZiV<*SCvxPM|rm6Z(^ca~V%n<_u#Z1K#>weTPYHK#|CUk=>ztkM5^J692u +zu1V2wDf?nTKhM%;JL3=r@9Om&ehaT3>93f-eP0@U0bhv6H{fk>gc1^RB;8GJuMq`$ +zGuTDABs2W={xdz}lR)H-RA?w>z!gCw<)Iq6Cs>ns;?3ED%N7&)3*J{}!^!%Fwy!D_ +zxZk352@DLZ9tFILkv}SmrN;4U_3O1PhBWf&`>!Ba*f*1EJ0_p1=i4D(S0An75>$`? +zYXf~+dXw8CR@fcV;CIp)E-{$7KhB;-ONcxBFH#Mk{@53GWn)9_G*0@dGc8lBV}M`6 +zDfaDB-q?frPE6|Y`t=pRqo^M%4u?m!6ej$OR9taQUN^Qeg)z|a9(9Ybgsc0n +zO!3-sFuAT@F+E{Q_Fbm*`J+i3io#Xi;S@pBadS~cn4y=#-&8mY)LXyf;~Z?N`VrF7 +z+OgDiI)ROBkh-aUwYNihC5Ztm|18l={{Sq5TcPB}Zen?eRb%c)D%^i1@h;-{&5aA}224huiur53b(2brgGoHD-H9le75aejeU-313HaR?~OWN*gVryiulZz4N_cM$T4d2 +zA+Y9$@O{OW8#Q?F&f;}xhswErtv)cVaott4#wF<#+33e}iCk3f7W(?I$&s&f5Kctv`YB@nB{(|$VJQYEmw=-`{@Ovm-R|fzz{$er(YBw3T1F-;98A-h^HA6flZT2^ +z1M`+o$;eAeP)c6GA`JVLLCe^Vuu}X~-^?oWn5)3om2iFfX4#I&F1@Nn)e3z5c&p{H +zuea!<6QIqvzCQ4ISwS(MhSv+li#kOPQDwWB94Sp_=%PC<=NO}KOI`{}4zPswUqVSM +zC{=%Y2_gD2y`N(joV}}xxDJD-j1-3j^>m>!D;M|R=9w$HrK~bIt)`JwerC^&-<-tp +zsuaorH593+$W(nRsr_OP1yejXhwk*Wlt7ZncB8To8ytA +zytu(cLN?tr$G&cJ-`i_ZHH!{i*Q3$*tj>#-h)I=sFYGf%_CoM+pb}bgaf1gpsqXTL +zium=nhJ(3W@O+{*hoF81@>1A?1zHTrIXlxj;_?xqv??})960xo6DbWS54n5pu2NO&g_dytq09An2Sr#iVIwp{9rSUbiGkS +za>{_uZVpZ=8}=vt@c?jc@gEaBRgt>*9*Y`eIB8rF_pPj*9owF%I{n!%b(x=Y%=yS? +zj)*k0vV1mg@<)#cEY7I$*)JUSeKIkb=tcv@LGsNj-7Jlgd7vJ_&M +z3Q#+wbQFNoe68YJu`dGMvIOroH5CN~z8<&CzEKi3@CPC^185`c&4>&pd^klX+Qu@y +zJRaRpVnkMA)x!LyUQtTCE^Zy|3Umy!4-DX^ylKCgZRMW`mLqx6GaiZ(*n90i!{Cbf +z~xJ9nMmsDIjVhJ3*<7BXsV>hyj- +zy=uR_Io=%^227&XsLnC3N^UX*`mOg_^0Er +z<)UdsVJ3V_;yjDM(^rojm)u9rZ9TIrUHL#>t*xQSS3JC{0?n7iGl44it=~yo-HDhK +zZ+xPWZ>9Lfkz7Fwh7nn6#=j0rm1U;b1DWkq(9Sp=N~t|V#+IRYO5fqOHs^6@@l8Xm +ztX>8?ZN2il159#ae47qP8aNPYw!EF#rFEJ)M@JG$&E2c3$gcLC;(*CjB;G89ak=cn +z-d=o*W>B*5Y?pTX7`>SgxV7(EVlR63YBgz!--3of+P3xT?#ty(u1FmZ>>|ZkzygW~ +zEP|kBj$(4T8~kQH!xcrsDG>D;0rHL`7lpJ1O6pa4TM55yLT1Ucyd0=>>R74 +zXqTK2@JrPFQg!rx&&Km`=kO5)Q^RnKk%u^ViQH!u1zUMdZ2Cy*<4Ln7t?ud&+Y3;} +zi{j-TsiYGCfC>joN~+0AO8&>gLdYY++`trJxldxmL#Bph#W+zm=&mDbc}&p?uIUW8 +z0}^1y=3_IrBRnp!oQ4rG-13_+vN&xZo~<#75uErr%;4mt{H(OXqp5}+#_oWt=*GJL +z*B+S1Ne0cye0i}&Cd$0%xQ@qa`~YbH41%PXl0|v`1e47+fA8t`?ul`_t#84DJ2C@Y +z8k9(iPvTLebhkM1TC>O0!yq)HBGLu_MYuQ~RnT7N_n$-ush@)K>G4-Cg&M_}-gfGp +zTIld-?GWrl7u%Nlp0G!(jlxAu$=^QKGez4_Ya=hk%r +zDCp__jHQd^?EdexM+u3z{LqoDp>ZdPG-pun1pCj^SO`)n;q30?q_>{pd+E`_okO3w +z+uE*0RLDY@JX-=_h>4+hsmDxie7sG)onP|7H#acv2HjdCx2GVFaM8hF$irJbWhDVi +zXGa!uD`yL979U3!$Or%+B`# +zXE$3`c7A?-RyGb+4i09B2eZ4clZUwvvy(gJZ-_rJq^#X7-M}s$U}q=LZ%lIwXHO4d +z3JSj(tpCZ${{;lZ{XYS?y!Lee`=UMFt$&;T*!XKJ8&=4!{#g3oCzX{~R{KZH?>yLo +z9bNv2`0f37Pblh +zb6fNN4MNoo3~3+p*Z*oId~7z>JeHQc%}aj&Vg8#cb{=+aE`ByH +zes*47HZI=3#p_tRxkGy6H!3?D3&)>3x3UzFfnb_LY832fZfniz;$-^=?(bv>K(+&+ +z)BJZ!L6HBjLwFUCbh9@1aCXyjc782P@q4wP-;jTFB1q`39urV#QNVk|8Gc|cFx{T|93e5bo~d4xSNN!vzvpeo2rGqwWY^@kMpmb|AC|d +zkw|wBH(&Yx!>0ZhKcPR1R1OmB?B@Ho`8BOw|2+Cr$6kZ~pb7-~Lpub_E&rt7-Q3IC +z>W>sac>Ht7($3t;)*2$xe>K>D*unpzkokBytj*cEd70TPAsWiXZehdB$IZ{f%*AiX +zYt3$H$-~F?zo5H2+jw}JyIG6dLTUh#XNUm&k!KLyU-d`-pI75;XZ^dl*f`jk**KZm +z*|pf&1vuCRIN2%w%r;1f^|vhl$8Lpw>yEOrz~A}!iy|p&H!x(gGH%X}e^Bv<0t#6D +z*WJzY-*z{IEr=uv{3}-eQU~t;GXAeZ{jHY*^1mBI)6>P}HQ3tiKj-g1>iNI$6Z)t9 +zuip7Th5y~{&uB?!7hj0F*muQ{|m=I5tP7|)=uuu|J~C6?(%24{&rS{toctH +zBn{zuOLuC9O6^*>_Z +zf8_k{>iYkUE~J0nKd^R!T#tG~?hf3j&(lKgEWleR%1QxVet#GCRHQ*Hh%R#a?f}3m +ztluvvKz1%Z#0d0|SC$5TgL#F8L)J^q7!3e`0P<4eT0W~k@_Yld7Vlm}555XMDA}Yo +zj3(0A4|WccWH6BHdDkl9XjS8=#~Jo5RKvk0w3M~Bw6ZLAHea8>H^m@j-nX>cw|p@@ +zk%14ldi#BpDeG#(|Mc#O9Sb=#dBryM>%rB|)ehW;b@!9@j$^_tB(^u6k}h_McuCQllCx6WPr8fi`+7H|R$TCT;+X9jpc< +z;Cg{WkWB@B6vLowwmB>?bkK#f5=qg05G@=_d>rk{Z2$W#2y157K;9MfTvKqk99$hGMBYd~U|5{&@WZKL#cU +zRsF5{wriLICSMtb&4Mta{zRXPW9M*LOAY);avRt%Z@$v_5s`iFuCO5la1LV!g@7PY +z=Otwj@Zgiwlec>iGDyyVG}hyU(8vc6!+}HNJl6eOyJRbTQhz*apbukp7@@i5_4bpJ +zQz7m)PqdYg&ri`#sAlTtF$<1PQAgriNqn(R%vo%00)}pr=oGoLm+ESho0!)e8>~cE +z&VD>h$t^m|0Qx5v`(0GwmN-20oQ5}tqbXZ~tL>C`w0Eonc_xETDKOWlN84gl;&|J{ +zIFlxa5=FiBDOn~jkyhOGTGHF1rzo$u0*j$E-V`nDlVHwbImE&sUN2a$rlOej6HvBkUOrN#vK7qhp&|SuQaJd0VAY)C(tz +z(&aZFn|O1f0r}x24<}3e3{fxccloYN!zMZd_%?tby|~+x46FC4WND +z-K(+SUD42K@MhW%KIR4D0~Bg+jENDD;0vfXqns;Vy9cp0UN+93>g~ +zV^td=!tDIKT$Yb0d-6QH9ykQL9|g1&%&53de(TL0X0^5_lu{7^MHNqtAJWo-v)7+Y +zHLr6S{C1)oXk70AW+Vh1^*2uPC%lbejE*rvl~4-n6<7I)K}OfQefV61$dq3)d5SOg +z21fO9Uk%-Qk~z#bbx;*6oX3XikpjZWj}N5KFYYI*IM~Lx31mq +z^#lqH=b*q_6um=#-~Q%sBtCsLf=W2@1mFVdsCfWmX}#T5Glh>n>n{MuhMV8`XhgsQ +zu*;X7H7ACxW5r*bZzAui;YSc!N9%tST6DNjG$Ou_4$X%Xle?SjaiIe`t*coQ760&! +zHG(Fl3fdZQj+faw_W^JxG^)tUu@mN}C=V|x=hR=>g~UtMH!k|JwZwU2P#Ob}+&c*Y +zEXZ`}H!7dE@gGe)=QI1~x!z)b4-4oe#|`_m3vUj7t>PxOh)4>dhQoca(8YcEt(4M&J2s=;GEZo{ASO$i+lo0u`eu>oK#h;xYW31JWJ)V +z&4PzHjj6nLW4TU?ODvKSpUC^L(pV^GHC%fprG(S(8XZ$`k6w!LqK*{SyKVnv0ABz4 +zhl7B;M8`4;gLUBmVhm@PQ^{+d+H{nA*6x$K9YWA2^7Pf_uvCro-tjnDi%A$NQPEn+Gf|E&UkLAH@TN +zz6r68;WK0lqf_dGsk9g4$VO@AdtBdtTiiK@JCgh!WS^fOu+ +zKXQb@8qsFXJ6aT35Rej*mAGYedyR&Q58sM7(%ZNs@y1B)S=9O^|?ChZ9%je +z4&(Xlhtzb$HjP(3IO%+DKZ}0IEhJUoymg1|^^ipU9JdBi&*wWN&l4}KrgL@(70FD) +z3U9B*BC`4bjwoKP^>*$wm;=y9F8c&VsW+UkL`JaC3b^S_<(I*2(xoVu;aL6%-8+hw +zcEZnB5|?*}VpjjSh7>kkkdw?PN+N +z^!L#k?sPfg-sund_})-@h-SvuA +zZ1+uI0k3J~AL|9B6Q4T+b*7c+2OC(|b>O!I0HV-r?6s(gZlIA5$6$vUGzZ1! +zxjHRz7e<@iY3sBj-<#n(a+Ktto?+A+Hihm)y`eHGDy*M6QslJTBQ~D-RIEMV*Q&B3 +zJWxIe{jWk7-8l(iuglhGp!ATo9so7;eT{~2>P`M8?y3+?pByoDeWg{7C;FCoAE#-> +z=*-wZq8JL=?06rZq!Gb(p2Hk&HqCP2mrrA5xVWN~=COR%z>Pko7Ihi(2`EGq`qtX) +z>T3;JMl0*O43rja&x}!N(*!f^ujVG#%U}0nnL4iGU$k!{MoBR +zWLT8Ouu#?`H-s`=h_a~nTP-H&1-vA$sC*fI={v@mKvMCPPQi#%e!zU@mp}Ro}CQ6Ddqh!_uD&6;P +z?AxxOtxGW^)J%#E_$@rZb8^+x2y{AF-r0>BVA;p2&?v-H8a?H(V@Ex;P1;tVSLyc +z{G4Xz=u_{la6_~KQ@1i-GQ?ZYLX~KT)K6W*#v*3tb5rp-@lD<;4+FC|+P#*OfmW+I +zD$r0-b8hPj79hN^ezk_#RGB9qg}jpL^T7Sp7Y`};t@_cdhu!|;4rQBe$@5# +zF|lvll87X_3o%3A3tT7;*>$V(5b;2+laeE0LGv3PG%EDXh~tok@13T88_~dZ@>vrz +zHZ}fq+Gs0g9-}K+4(-knQh}*`4kfCqV}XDvU}01$a*LZCv?bk5C%KFzT#!UDTbR>( +zEB0D5fhXF0l#WGOexh!yg8TGCG@g99?;l}Cxjc%tO31@7SF{W&58mqxcKYoAmOv?F +zi^(U}YtSJS(3Nlv(EW&1Bl1h*y$|}Ly4rYCdFYrHG*2H~-(Qj648u)Q|IXi3%mVp_ +zWc(%Jj8It=vW>Bv7WDp?bh-G4UYB5m#BYEN#F~7;QZyP!quanWpd)*yIC)ysNt#wy +z)1CEHXJZH9h5o*dCM1nrW199|vJQ>`>!`9ho~Q^m&Ye`nJR?vLn5L7`%Cr4M|L!ui +zR?NJP7XCGF8lEhlwYx{UJ1#bxS~kqv88=B3#@TJ^PAX7=uIEjDsR +z*oC$lJL5ZZ_HE*C%qEBav6=pI(G$p>38|w#vhz8c-nM%foL*($b{3^nZE$fCmvTQ9 +zRW>z6L|C}*JkKP2B0vS4i|W6>FQ^N|^P32ZJ`tNumQ)68iesU-t;OAhg65DMoTu?0^2S4-Y9(EitJ1@_gHIp%vqo7S#UXlGZ24ZKiJ-cHzXsGsI?b4 +zqCO;6F$AivQw5mHZfTBp2vuLrqFo}1n#gqg`R1gvoq;|$MRHdELhLp)1@&`p;lgQ1 +zs1XWD2Rb$xIq(i4bZizOiJ;auxtle2FyCgLO>p|1cI)@9Ta`IU;D%AxB +zz=j*s>OCobA3U;mn570!!HOn35(xWkD-TL??x$HpiX1p%Bkh}p%tub#cP1{IK3ZCz +zKyYf$YDDno?Uu7kC~CJFmF=X&t;oCjm+878n$QjohP|)qWe`AN#*Mj{_(FwaV`&Oh +z&kYa5SwB`ArV-L@0N#8~Uw+!swWBfgrp?}N6Bls+`HC7^o00try2`~-tu&Tg=&EaK +z)ITwOjb0{WCb(@={ny!$y!6Y^h5%Fu^5VeaKY#o?9* +zLtURX9Xg+*S(}UUB;`o!wI73P))9oW;4l=$ITU}!=s6x{sjVibn?y^WUokfkP$9Tz +zJDR8dbjOW(neDl)Oyb4oCBqWAgq*Z3yk~tg^Qd^*$gg(d|MSj^E4H4rO +zLq(9KLIf(F1ja(Hw?>Qfl)vJc>~t25m7%Oqi2ame{v~Ss;EnG>D%Hfc-ki}s8+|ID +zHUL*tny<)2z$YR18oK<{n&0*q75AMkeaaVs=}FnoE#?MDg%fd5J_D}1Z<&)DVHupD +zWQVqT5ar-0J1dmLJJz%t1a6A9#ox%CxWLs_lQ1zbr$MSF;T4*{d{_zg)o3>=f)$(; +z$17~skW#O=Wi|69{&U!DJ-t->;vRQ*B7Ovoo6QFj&DjSolTz>U8 +zfEORGa}O?3Fg)^&0Tx_eMLwt +zsZvn+Tyf9?Ls=CXO(fRf-(aFEMG}BT6Pgt-{q$q#0t6d#e=&fDd|;%V$5bZ-x{@+= +zd@jOOKmn_f)+a#u3^&@H*wD4ZFqpz7{tP9@m=!k19Q)8W9T@GKoH($(W%R?0-M3>e +zDR;x?M?4-EUnB-yaHV4yl((d;3UEk~uU{Hl34I0x(mKj-(U +zu5L(LXb)hG8#(KA=2uXZBtt9|;#Y-gn1p2HrpSyOKL|Pz@e8YS4i@xgJ7UADTDJSg +zPsqvQb$a_sSCvf~72y=YZkqv0QbatInCWaGo4@N0En*V|XA`)|>llI6FX~539Nz!s +z2N6w(3$hW#xNqj@7@5A*eU`&)O6@A!?8Hly0c!}EfYX&qD@|3Z03Y0JeT^tuN=$_5;=6PEvo?B~WomoH9H +zivvKf!H+m^Z9xKYm+7Z(}x+@u;#kEL{zv#Ko@|c +zH31KzI{pf$XK3K|+|zLdLc%;cM&!$^rdvx_YvwscW1ZcaVWKS*`Yq(uPe5coJc>Ht +z0E-LA%Dd3^<85xiFZz9ARKzVMM>E&EAZkp*66;m_w|Hup;Hzmth$z8peMKF1Mlu{9 +zyMukh;e2}PKAiwef#QM|n(ERsGN1ucR`9S0eP^XaNigx;7+yr0Z4^fh{4WX{HT8#lV2QOCIHdwNlLz2|-0YjHXf=uStu#ZtS@`K@dG4}+2L=7VCXa)v8V!dYl3xnwN{qDV@) +zr9X+&&ux~oFxXmR0{bQA+^Dewm;Fy%kL93uQ+Ih|g%vzh)M9J0^?9Ce8G62%K2zlX +zqI&3>?a&k*H`2e>$qMThIAOFkW`aMsrnEiBfPyFTE0|~RZ<98N@pgM)BZ!yvq}E|e +z8Trbjxm$C+Sr!qo7>-^EMiSz0;|6>cwi)n~9DpJKyBGz0gtwGSG+iZ0V$$Uk49Ap6 +zY*CGrbPi)L(?mr2CWDZU%Nzc4_S}a60qXm=OBZoaFs?LH?p206oaOE9Cjw3xCBCoE +zAEgQW(UB%m?cU|x!V5oq;jZu)_TNBYm_5LrLM;nVw)~!^W=tjjq5_s +z9^4^}3{`*xD5kT>ACh>s=h$tdJ@Zgm@Y98%Vide-Z_yOkdH($9w&y5sZ3~SKE8*H4 +zh{=Fry|sl7Py(naFk~`v=-HlZJW?e^ksm4wAVYspk#%Y`7>(NW;t%%GkPOp4IT8xj +z`(zaU?uu?!T7=(?cK2Ak=Y<9Rjq?hvMS9U|R&*Qy6E!>nLg0t0I9;yOD +zZ_Bbfm90HMQ{hs7!HjpoWqR2pf>t;tnCwU#7V0yR@*Z~@LH*?F`Z=>Bwq3|&2`5k5 +zha^LHBeB?C(&Q4>vFgt0A3fNnpJtbl0WulcH~q_(47E-nGtcBJ-X;39v3mg@vYvO9 +zZl1}u{MBrQZpTB2eR=B`el=_wZ^QPhk2cu%UFf>)1iY}twfF&A +zofj5f$&b_rm4v@R*YouI3T{jeo^o;vdMPDQ`6Wk2MyV5)YuYnKs9E;@Nn`Ibl5WMd +z;4mOyTm-{19`&+qoV`)2N0t@25h1hhmBi4Yv)GOsrxJO{qDo)NvMihuH^Lsh`ryZT +zFF{=e%#EkRa{?t8E7&g)izh>s4AWPm+c-^QD)=LD8qmbrJZ!`n0EM8tC^fo#8HAEy +z=!f;50sfgii%cQJ1_8U^(pPVyIVUJqYL)Xr6n~S7&Wb$DCvlhNKn={qpillv;!}!fKP_tBqX@0Oh_m&gWHLD2TXVV&OLz^TvenX7gOQ&s(gcUAZf{%5OrjxRvu0!Rwp +z`S@IMlOxyk`1WTzPC>PGO^b~7r`n{esOo%qefh*mnD#P|*qNS}~lpY>&-v+k% +z#ofBbA!)?>d3gVxnXdHrZ76)V{2>{t>$*Ork)UWNoWMU!TtR* +z2J}@!tKQZ|d$rLNzv>Y#F7%e6H5Qv6Kzw7F0g#T;@pfCy@GJTlF_2062TEJ{%(HIW +zy{ihmx-2ddG{1N2_+5@3cGAnQ27=yguJXtPU`L_fQzb&&`ZBfjMjX4ZxPi+TT8sE9 +z;wq{dRl?$g@@Bt&YkIhJ%=d;x0M)xndI|PbGZ5OB*Lyvj*}1y8=ikm}X?NHFpU}0- +z&b&s8AT@$Y4Q67&xMcbEHmpJou@cB;6CI)J5A4MTp1h9YbNaYd|4l4SaUgvZn#n8s +z7zT@BORhxB1YNp2aFz6+seMrN?zZ7mWU~lC6Y1W^UEUe@L__yCrl`&cW3!_43^^&N +z#Odh7)&*`Pwd@>?;T-q=ZwlYrD+G^XDfri{D9-dIFLf)vdn7{c +z5VkMr36j&vlKyy0ad!ERG!==MbDRdbeVRQPh1>NRrSbF(8ZAmQI8Y-k{Ea=0kO8E> +zY#LL8r@Z*_3ckN?unXB+bI_`9o8sRuTYJ>l+eJuo+TQ~63P7>V^N?C5BsOp8qV`9U +zI~hfM-B&&#Hq3x=!jt67)Gw8yry)e%V~@*tzKW-@{n1!}S_>%*SP))8gL;kd_2RFt +zZ!`Yn)X$7%s)T->I@&xU^ds~7FwiuXcHeW+c!V2tc>CuR-)p1^o~9&c9}&z-p}Q0h +z$no%x9mQ1yeG}5aI^u6oRQV!hQGHqKMDJmmOMl>UUPINNV!i8{O#gIzQN%RW2jx$N +zjC-g;#a8Yrl~N6@tc%znXt_)zJ+5s0y}QQ6P60GJ6a8qUcz0uPr~33Lb4zQyQGzWVXH()vr)3Fr%U9+ +zgk&N2lCF_K3(De%=zl%Va#)Hzk0SFkymEI_G*It1jN@D`0wC|6SqNAH@wm-2WnCAAg~7d(dcNiI9zD11;_j{3F^;@ +z8sjNRF0tzmXIk~b5ZxCdOE1Z>%E(i4D~yuos3Eki8S%M3!#ViGV$SSaDak`ycq4&% +zI=UiKsJ@WmFro7^X@%uLxZ=B;fQ%3bkF>AoXN{$a(YJRW&t$WN3dlex!f6f1AWdnV +zq7>5pFXp%=KS;KOgDQ*6yb1=Obg{;%sX(xn$nZZyhLko$OB%OhvAnqsWr;y}Bz*&X +zH<`M1KjML+$n*LDbq3+)BXN<8MlYJRW +zOo(I&O^8T#USwwwDQgXq#*(R|cx7KQjdilk$dYaBV+$#gJrr3cYqC@H{k;Fc_m}h2 +zbDrxt_kEx1xz4$s>n0}Xd&vi}_JC?g#oqjOqtl%DQ^&U2jtjdvhnKk7FH9itn&n8+ +zJo=Z3^3NqpPca6OEqdJ0yYlp_`3T{^;rHe&D2mUS8+KlsFKCYX-DXVRtcF}_yk?y~ +z*>JMqiiWG$E-}J2V?3O~2~bTj=<2{nb^W_kKGt6mmHuPPeUjB#5e}{ixx|rAvx4e) +zmj@jNhM`)`=OnIaKUX>Qh3907B#~2oBO5e4BTI^EixtLRy$UM!Lw_16S%7f^dA$Dt +zmiN)%3xRg;Lwz;r?N_X +zNGRS=Nk#?OBEs@p_N!>c&hzYiL;5m;gFoyh!<&$8pMfPMP*gB$_Ke~D22 +zKc)>tjYcBlQP&9=9~mY1N|sGtm6fh)*^*j+Dk5!p_hxCgiT2C&pz|7iSyRIj!3@2y +zo{Ed($nC9#L^_>5AeZTT;h^Qw|h3%~zez?08A +z4B|%}HL1&~>$yblVv3BJAF}Fwwbdr1LMHawmfZBd=rk|9C1={NF6l0~X#m$= +z$BL$-%lywtgN{ab$8;r?twc-lsDIq2|0bgBuIn +zl}tuOm}@B%TT*b{Rqdo$2Z^GUKThs9nj!x_gE5i2bBR#kA~BH^~LcNOK*u;!u+DLn0p- +z?5pPs{z@oAtr(j?QVJM?jFM*o?oeW;9|zK*EVKr1YHii(Y=nB^7$(n%0fpDE0C1lunj-a!?$m# +zRAHglXz>#~7I@}hI5#WV0Rc#mm$g;71c&`lUCw85M!QebGH#93L1XHEv5gx$OX +zb#=o6U(0Q=vrQ4Q0b;!2!FqOKnfSv^#sx(GkAfK6$Zp(I^|fK1RfRiOtm4Es&Vv3W +z48AGN(9iO{#g08om*p?o23piA=ejmslX5>PPdQL9d7Gmzhd|-P<)5KoxU-*HTsafRtv4%10MRl^oK= +zW2^4_~7F?f6DI+MYJo=+_yB|t#IU_J8GO_uv@sCd}3yecEOFH +zF2SOoc#<0!h6CnJ$fv@)8%j90%gi`o`7sP<_M5&3dGufgp2+kGfpnXx*?TmGXql`Z +zK-mK5TJ{;#bn?y^nE1=PN};IlEYt +z-BHe>`zH(tBjD~}*ad$cldWkY*yGoYa*?&gywKGZ*~xDLnP}ntN+K?$43gz*Len+b +z;Z4J3=$lM<1SS;Cm{_P9+u^}_W)V{dDNmsU^k5>$ncjuS~W07QO~BbZYhP!Wj@Fw_q9yk)?=tjyUU>;!iv~ezvMD3et4m<8rJB*+hxi9 +zOa=%1VW0(nC6IGmRwjs!n8*+*{42-^Fu|55t4mJ0=P8Ull#YHBQp5Y(eg}BKNIMJ0 +z8V0i(4nU0c3dTn5qgZAmfN=oJ-`HJ4*zS1hXi`maIL!~;GK&KCp|ZWok;pGT>oCrva0XmkI$3-QGAjqraem~wa6j_a*bHt^gF>m; +zgE~Om`SF>_bgvsE=>c|epIZJk%Vz^$s}|mrhIKn%@7_9o3N!96qf{(~?2fvjx_{#U +z?JHs%{cF2opyVHUX4`R5Q^uW33xFIbRu2EWMJ-Els>lx}Y6`});R8U3PPFldzy_0& +zT4s&kobB9;9#6|AO^JA+T))d5z#k|r2#8XQ?vagSUS>|iVj_3N0V0EYNcoQOnK){l +z*5~dT@>Zu(O2tD_nm_OtDzPY9;u=^!9OGlvd2Z}Glf#!}grVf+1qNWdMa|YN +z%r8MU3s=4n_wVWoC46Ot5|ut8lL!&4*~Cj!1(*AF)=(`_$BYI7cxMQ48N^gxpy7I_ +zSdw<0D&*iaw`Sp8q)~1fw9;q3TD958x852C{9&URgCh6_a%EC?M1GxEYQB;R%1^e` +zN_)obR!Q%iM%As!e|HnW4;JlDQKrDye#{WKVaz%z=X78~Feb9r#on!jKX$ad2~xAi +zM<=NTx~jb66DlKxL@9PaA{b_afJ_jZlhnbTq*Kfwl7B$1bPOc}7P*kmXU +z14}x`!zEpTP*lh}rn~0pT4`dpjlMEkf~iGl=5m%7;0H3}NF$%v-8?Q|s!Uv$WPjg( +z+HF>|ZO=bAcXf{(-+1J^n}4*z^_@mUU}d0$+I&ah2t@XYXJNxKlBFXhqA*|JeN2Sw +zx5C}g8$jEn((T{E?AXLp)C})MiSF2P{p@Fg<~maRtHGidF=9gSK~?|r&W)9=U#dl@ +z-OR+Z5a%SLd&6Z2cZ6_(!M2p%;!B>Ujh4fzfvJjEj?CL4cJ;{$nR{ig%3E;Jzi2VNm^~p*Q{zrsqZjq^T;;iJYC)?L!{&-#a9vvf^r1In1`EyG +zypR3?eJUjPRI1mR+6ncLX>~W@axRUspViH;TfdCMwLZZ<#4!wR>(9Nol@$!J%zp~D +zJl1s2TcI=657x0Uo)ck7t?F&91n9)!XX5&}GyZlN?f9rmJ1&p&peEYjrQnCCe6NJu +zkgtK}^G3L}Q^8VO`zw^m!`;!SRmK50gN`LP>=J)U~Ti@(v-%DUi +zIsWKyJgW)QLwP8*nDomJBHqP2f5p)|?!T$8MaYX--UME`W>p`HGH*hqHhCa|n!Gbi +zL(BC%)geM0@7`cST}au1`H&RQ0k>6yarWXW-QUd*`p_3Bm&W@Tp*eN^r45F!j<}xO +z>q(*1+!ImgdQV9Tt%v-3OmkG(w6}iivx}5KnY~#0#?D8wSUXx)Nc6>e<9W?;iS};C +zH`uXv5DvY7%vpuV0z}IV_7EH5nfNTMUr#oBi&KTqX}k9I_eU#&cmdb!>SG1XN`0<~ +zrd|djclMo&%FdZqBrngNb@uO0iavJSbwztTTem*$6*Zp57%EifHTj4@ihzmay|FQ{{!khsA>QJ + +literal 14310 +zcmXY21yoy2uugDycPmm{N^yd_Q`}vP7YOd|PH`>8wLo!q*HRn`6f5rV?*HD)IX5{c +zxpy-=`|Zxo_svGBD$Agwkf4A-AaprdNp;|J21vifLD%hMrT$lZeEex|7Xe0mqFAZArC{!qp)zJmbab@utqA`6 +z61Z~|e!k$IbXNT?PvGuuzT7G514$8e!}lsR>%nURMm+~pde``@(!O=ISt0%B93;Ez +za-qRi4n0Q>zQ2#2^_y08QOl3jT*!Ir5@<8VrFx(6f9sP|H8ttjftN;wrX>jP4BcG1;MfU5x^L`zc09u!bDBt#+ll=7@ +zB;}A$BKgu}V?#qfHvm`~pt%wG2y{MOc%B!8I`p|pc +zO#?sq!Zd&j8UPmvY4RQnfo>!6{a}GFV!}g@qu<3Wu$07X(O`vikNW$~q!ngF23Ls2 +z53p8js<-B_Qd?xX6rtq43Mdz(jOg2QXx#Wng_9^1^^~KqFNq{Kvb@Ap9}bf&xFA-C +z5+#cQ`#v$A=kd0O=agATcleBaxXf_(dnqbQz|cL9R&&Ni1omTs+6~YApmk)MCghxj +z1}mq&IU>1nEiF=q=PI`%jQbyRd=hVI83Sm{E-4uTc#w;NNwEW)C(C`xvWzY_%`_MmO +zD&g-sEaE)}6(&g)y-N&rNy;5@+{M`}!{60Y8wMgF5;HmO#B~hG`W$;7xLG*yF((rq +zxP6I#r#o`B3FppK{v(q1!C+YLFSfySDcHyoW!}EfzuCB1B|C5+oP}dtocnwkcNy1EZ6#5JX4=ePl&cu~0tMnt&79+I4%PaK>VqFx;r!QdNmnxlEqdU-QR%Nmu{aWP +zJxwXvt5fFTCOVgB)Zq +z%H0U=9q7Y0lu&1kc4zYT3*lHA@XJfoK>3WFM&WWf2u6^+wCm8##D$x@Gkw+t^HoO( +z4pxDRqg;$5S=t^k22H5^V3V0Qfy%Ogl8I%LD$52=7)J>Ki9Ej1HyEi_ujELlz8$-+?cdD1Zxi02kW0 +zaY=caFq4~s^R?zxcc3Z0X|az}Aww<{P$>6rk+5Di5J7$kWor0{Q&>+DWSBH^Gf`SP +zT{4}IOFh-hB7xwBdewq%de)q6QvxorV(()2>@j8i!kj)=^hN +zl_N{$9xTHHA;V&Zx#tX&1pOO;v^NiOP#_UK@J;;lp+OOhOOO2mlMdxM;Qv-mWG+^vzox|8t`w| +z=gPlM3)y6G*hfV1WwuMe>bO-vP9g`h5BqgO9x{ROBD;aPl>XDmvt(3PUxt|4RFRpK +z5OEtRz{(Oa_W_!Z4XHf#h;Z-~71XM7wlF*L!-#h_Uy2tGuy-rAZ)4{qE~feNkp}qf +zgvBtLkFPI~I7%C=OHZfPZz$j>L9)rb;l +z@J^dxncy52;wmHg=wC3|Xn6jPYCR7xc}~D0wNjoYxmoRh_zh=6@8coM1UQIa_z*1)cZPw4v40qoZQp-uy#DLv=oP +zX9b3vzFA2r8}|_AO8W1(OMG__0{1AUD&Z%&7-(>s+Z-X6Sv}G5QguIbZ3mYa--?09 +z;wNw?n=yAag4%m#w$$-YZ{(ZJUcwHfzu&!gykNjG)e}!=q8xy2_KS=ULsQwv45NK! +zVqqD8#S{vRjg4(Q6HM_F&tihNIQns<%DVjE$cv33ET>Dvc^#{z&#u&&9RgXO?ZLuebczKv#;! +zCS|2lIa37Bp#3RWj0$V3=I2>o40{(J^LD|EUH?!2;Z&HS*>7*V%{v1)wHaUP85mcX +z%q!K}Ntr*IzJD%++btJ;VQO*OjJL1t{GvR3cy@OC-~pe^bV?N`z0QKCr?Tom)4u%A +z3mi2k&eIgh0^rGI#Di+&3lrsy-r+}zwBkDQtswtPbkj!Y^l`{f!# +zLseC0M;DiifDa!({-G4{W$Wxsgv*(NX%HMyXhArVwY105dUHg?+=@6Sy8n@slS76x +zU7%PI8ToKm#qahfR;7kn#|t@9y(0EkooWBDqA1(mpO)>BBz))giBi8xVHlj#dR9U8 +zRo%`iBdlj8%_tRn^qa%T>{nsLLwTNld&WHLyfbPzv2W62m6q=Nsdxnk +z#{P==5!Lidx3bcr_qlUl%BX!xjywA?jv>FU^mJDa0zQT9Kw8RRHq>7B +zb~DXw0(oqBrOQunsm2ghWV2i1VmN{F?)U;0%*j{FEUxazAJ3)KSWomuhklkDi?5h*MTLDS5ma_Nk1sNZYzZ#$maGRyiXBzjG@(G__fuyBl(^A>s&{jF+J%5| +zv#7nD1XK806#_U_4#N2ANAxznk%;U$Y$z#{K*O07mADqx6LjACqwP<`HFV#C6Q*wx +z8JVP_qGF}V7B?^8)f*2F5AON7v$L~Kr?2}oPai_kG!_6MI(U`LS~+Mo*CSyrw>pPE +zllqxy +z^&rnDn4XA@AUY7~`1lwTCrm8KlVRqX&!kZFH&;i9@=R}UDxNSh*)Iq2U+#9}@ag1t +z%KUOEw0DXT)>hQoLTprY^z=BC=8NAyi3pZWT7A`?;rI<3%65Nqb93%pJ=!+dNtB>W +z7f3O-e-S7ZBgBntcyt~wOG_p$AU2zlGH8=%TEm+z8kLYReEMTkIo#2YiA=iKWrH); +zS%uT3xAyyY=!U)0Evpgx{{38MPR2nN<3913M<0O#YCO=TSt^4IzV3^D%2zC>t_OO} +z_h~AVOk+IIi$Ov;-g93a4j@WaekCC#HFm2_Vu9s)8-GbYtr{LgrxnSIN^PW9)!jYX +z?%-yssA~&R3F)C)wj5i|@!atCx?Qy%P1QEGSZm;iUNai`-F(8a%y+_a>CMzx$XEKx +z>sW|JbN36s+Y{4SZsrspH%UH=+Q6J`c&_-JLGL&5|$XUA1vFOC+rgoc&xT{dFT&pMaEBKwyD;plX0>2nla;jTlQ{!fn2M=Ak*=K*g% +zBm0-$ly1~}CT-5gv){jex9)7&b8u!a+vYHXU>=NF2>g3+_rN{(LUMGwRWKk49sS$v +zazyX8zZ1hwZ|U*5{fK@i@hRl*U%Q2cg+!iIfb)6W%S5F{91qinEZE%~4Gl>rBw9S< +zMP5$exl1jESyt}d~jo?hf`z^32b!}UGtJH+w9(0UrI#~Ei*ii&6z(AVE?(}k_A +zE9Z@mj7HF-ch46I0ipe3gapRj{=zk_J1E^b_JwdrhKi4ytBuwP)m>e$@9v`A{1N{h +zwUN6H=_W+h(a?rGaQ%%LP5C4)XiZ*`1uUwgqWvk`LyDD!Ps#Q5oI($KDJ%8n5kBi- +zghsLx`~mf<>WT)6-cJBbp|htk1NfkZ@e#B4@l?UH7!MDMpO?1NETGk_Eg{z!N3!D< +zWg8gtgS%b(0Bg7dw9u35xq)1vNdnM8iu7Eje*u?#sZ~%^q*HDaZC?5z4ZzhSA%ndS +z4&$M&7(|(9nWY%QShCnuN0 +z`n9&UeypypUgx;R+x;XM#8uDM{p`9~j<49)^dotHJVO*A@HL&g7F={FP#trj@{dzm +zeQUiqRWJ&pkKkA1O-|vOf8O1UQ$$0lIExffio|}F@ROV#MXcPH$ +z?$$kxAF@B#KT}u;R@SVyIO>1sw1!i?C(_013w9@?8$bKaLQi34zC$g*^}F&(%NEO6 +zQzD-^6}HQMnGJ{h$J*)HjSxjblWegsW&rLC8Ov_r_20jLjUS$Ptnm|p9fK%r0j+4; +z57^mjL&lISh8>DC;eB$B69$h4XxE3qU4T&zUpDeV@4g>or%D-x@qhie>6mqD959ck74(h?S0BA0}YQ18d?hr6}%}y{%ZNJ^-(?=Op~; +z#2-UNh)jH9>RXmvPJ(Y!8(uhyW|sFpyvv)AaNeljHj^Fx+RC +z!`@c->W1C^FUKHmG2w_atkdsMnzY+l!CV8havQ8-Gu)<8t{#V*2Pwp4h?ayXsi5Z> +zo!guta>TA~iv#iJpQkN>#)QF%As@2WgU&V_Y^qm#E*O}M_ijJfFWq}ts)-l4>D)kCqJJ@MG2$69ph0jzwI8ry1u8D@CyinC$oT?7S*Z}Eg +zYs}PWLqr4u@)w}#!{cMx;KxO6W2H6~3k$laJjAt+C{0mmCRnfs=OJYbh}HMh&e`#> +zj;jrpjqKCh41OK{FOS`@_sPP$iCm46G^EMNk8(l-1f>!gEV+4vMVRZ#8infUenP+k +zL^tBOHF^=)k&U-Tw{gfijqQ&^ +z-RHHII5yp}2|o8pTsf6x7$teW9Em!~iy2DN?D@|U)g%I6VG%JBO$|~;c~1Q^3|x`1 +z6HRbq1#~Ke)wWpALcc&@P;m+*sGavR0{aOx3=IwUE3YPWAwV45pzD$~02inxi7(6X +z$zk683M=_r#M*+6fQ)&FK0y|lm7JLwS)K=t&ZJk!U_-y%_o@fhr{s37MUEQOF*M)3 +zB$;4>Zx;Xk*(hwFjb>1iJ1f*D#nyWL{=>{2|9*^vCNN!%bF8Oe<`xz#s;jFz?;I}4M3lL;!fy_;J-E96Of+;sG%K=fZdR)99pJ}fM( +zq%(s8UrsEL{NrdF`!#RY+VjFyPpE_vtqPMM!MQ+QnE)+_g9Z^{4^;k&Sa^=w*yuxB_*Z!U%!3{_9Qr)Jfz4IeS#io4oj_Kqhq`HCUub|Ke!v$1-$v=kc+O#rlCej?%dhY +zxxKUTsFPG1nfoFp3%7@gh9S?vM0N27#*fpJyaX;Vy{!pt*}!9_mX9uC#J5RyjknW2Dm3dCvZYU +zSW?0kvI9!o2un}*%`AYhr^CQT1aZF=-Nt^atn@Kt%b2!hT(pK!|MclbBv3-<+6{>_ +z8toMfWc9rpOk(8|KW>Z-k>Fr(xc_+q9ocf`8!_n}XYUrW?Ax|*_|=5m*4F0V+46wJ +z1IGS^Z5t=0Zj86J2MfJc +zUq#WKCfhoB<;P2&&`*_G4^_0uqDR20m!>T8ay_rxSzA&9_v5##g6tzXTkx+KRfz32 +z9vvpp?+YxHTxDthCBu7)&Q052y4s9*$M4_2w-OdPyK?F-EBoUuSsIk@@(!gA*A_!0 +z2eu1y;-Q$Ut(M>8FCOtw?vZR-%*ly^x)<95vK@P0tJoZws@+M*NGhg_NU`!}DZnWBHQz%*@6))$BWN;EM0xAF+B4Mph#S??J?K+&viwPmes*n^HGDL9iBf +zCk|mDu46wwughN!isu&G((DO>Ws`(VLY?^#w=RONxUgFGby--Y=5NJ|(>qXOS`;lZhmXyMEyBdVM@jJh71E-})~`?t4w8^Kwy) +z<+KACjs!F^TS-;FT24_iWF+=l(nR}j7U#;Vd +z)IT3=b&}A}1PUKFa6DKfgHkJci!~7u?a%k9h7Rri^{y`|;;xNDoQbV}+oJ=LdApL}|77o@C= +z;~aed)XpbrMtt1x3gHPWxbliQH4nKBCew{9 +z*-_PTyn~`1VrwKcc4ZrhI^!MsZ{D0O0%O2!SHHi^Dfyr9*x*DGFKwc()b;q6nM*M7 +zvA$x_?$BMJJHN5HIn9Ps{_7-sn79~BZegaa5V;s(BA<5BnU?^AeJHXtd)cIj_UCjA +zW|N@MjV~vrJz{sE0Dzv}tXxUDQAXm)1(kX7C_ZVFX%!TlZ850i(P1A0BxaJu)#LcH +zoxMFRzxoxw$bM=B6gpuMD#vcsa^00?%=D+T9-dQqV*=zD|)W!3BLun2&^n)~$ +z2_^{i9~sGXOAsF_S=k&4mWJ@`mD+G%MiPTlhuomboeFNwHb(< +zVpVR!mwf;JmpO3JL|B%L-!;@7TG}+`HZA;-{VIlQGY|T=f|!9!S=!c?sq5|KeEQ*~ +zm!1xeZcJPbSsfjU9e>K|=Ni<+YgrIG!|5@|Z>4bjx+`1j^O-{QK8XARf +zUG$nLRiTEtt;)9F30rvw>nj)@vCF{$d7>o2n>}~Y2^^C79l@s`uXRZOcuy>^%2@t- +zRGv={pKlDXFUgvG_^DWGR==il1rIzn{$p4r(FVOQxZi!_*Ksfl2hR{Aj>01RbFAM= +zpr0wzMwlOwlkt4|JLK)$>VL+{4nv>^`yMa)T;(9f*B(9;{T+)_=M4dN>M&&hS-#(G +z)-sW(WxVkHR)`x#g)25Lu7qnN;~Q-bvKDZ=;^fyLy@okDpvt&ZU{!U)WVtmnp +zAN-CzM{jPFWep9NAKDDq@=kynkGi_GQ@Z2y_Wn)xc_q3-&+9`qdGy_{PF-2c^$)%x +zd0sonEJhtG*2|P*Q-f_3`Akk96HzBz2 +z!5tnJaCcA2hGQrSw*{F)epvfYX?7toP=O0dN +zizY2w`>O@4Vqff!dBhQ^><#TjMP}loM9ProiD-Og@$V=*zQ|Avg0D!+96lr^u(1fl +z3J52PHoJYDdvdiIW?q?JIC*r?88VruLx#bp0lys39v$(c6uC*j}2IFFh +zViOX|K+DH18cd9%Rgjs$*sXuoW<>p^Fv-7CV|zpgTUnj812pyyX-nhA4TZ^UyYY9; +z?}BOarTT1q;0xSTjV_DPWE11?Y2+wSA*ybzebDoy8JwhznKa6SvYxE$WswX7Z6pG$ +zsA2GgHFFL3^zA@XTYK{a+6$Q8di%@1-|q9U15y+~R-L7Kwx8*xr(FP{g*JDPa`e((jSl#~?Rx=3ne(nLfeP9k0grubJK +zU4euzZqt~$Cl%k^{-!e6YQZi|D3#+MUS}VsYZ)0S>y@)kyqRI?A_esvAu-{`1Uq@! +zC+b`wnMK&<_mitl+k@e*$*{&S>vayX*>D>Q5sw2FZ?l(8ff%(8lo<^mBMrwQXOXe+ +z*7sZdWzBTIwZO$y^F)qZL1XbOMY<@M_a56y{({Vg@YN<_y}toq41V%~w=+4ZQvg)X +zVw~l$z-sId^nKU%dlk7W(mG}eS&KV2BdYqNJnX-p=YrG&&`_m0fzA_|iKD${5?oL* +zdS$heR@%Q+(3!!T&k;tIN|v2j=UI))rgkvyC7MTTrKP3g>Fma@_R0`GE5(tL%sS$7 +zG41ag%(Y(xZ5cjlk=R~(3XC+$25r*Fo=G5OhGgR}i!nDoG?^sult?Eo*x$x6CH-3L@LtZ0dfq!Bbbw-S}RwlN%lpH8c=4l2qH +z1wRszHSPh~=esnWvXD8B{D4<}?}6cA+@Ob1760Is6`g!zl@WL(L&={LA}SxAt0>Tw +z%b7i^&yNKM;(vGcNwuxAK{g|S3Y1&pH_6U1G +z3M4zx5FU=O;=l_?VzQ-~bx~xN1axPgYI0am3d25BjYmfSTX7Q}==Vcryl6@Se0(Jv +zxKW_o%H`jdnC7QXlkFbCsACHN1Dx=0gf<~@PW-&<=`1Hd)@#ypH7%OpalDj-P=ts+3^~yWs~TV}BD20HjkW6zc1L +z0#HzMkn3JV%7N-18_@tgE82*YnmEzxirriDSx#_|<|q1vL{k}7>^mRzO(ueTSN2~H +zG}kxp)Qn!&)><3|e>62+GXSpQKcemfqU!&BHZ5Ca;DT<63bBM&uV1BDS?MM$M;x8w>gShAPMxJM^BbMZn}Unm{OC9^4x3%% +zlmX8!km-u$N4fQXQ>jRe`7)3+RFGjhz +z18zf(Fo2<>YV^7LJO^UTZ2Ivd#mpN}o?7pBV&q=f%ID>haV7M8R3jsF*@a%iwIy>| +zsZ!-y{!%&j7`B?W8TcF4NH-RHH1xZ{;7BsA<#APu!;cND)te)FhoXz$BIU}2&^7WP +zT}TX>ZO58$VNPuh6JV7~s(W$vAj`^%AtUamex3YdVl3~4+pqk?G)qUibNMrj0*M25 +zY>5Ac|Dnv6xBQmV#$3JA?&HTN(lYl~J}@$l{*TY^kORrCB)3dDO}^^v!dcLf^CHty +zanjllIQeSLmpuG+h&ae`r*v!C*0A&W^a&q>93?BAXzG7n +z2*3TGPIcN`-_hY9&oaiv#fiv~>}7`T`4=pInEqWX*3e8+yPm^9h-tr&ts55$l+388 +zW)~F}2JH!}VLbQ>?6~H@&k`MnSsTeVj0TRVP4jGbP*!!CwM6`Z11c)yI2w$+R0zxo +zT|obYS1&&`{>>Z9(jnVU&=yI*%PGe*f78ie*_9oap?sd7fx7{r^WT>=XHF +zl`f{=UJEn2?tRw`Fem?eRE6#*nOes(ebRcmaK3~a3{a3EyE1zXSF0p7I_iDJ&%;3V +zU;AS}e?*mH#Yh2P9E3QBigIqu2iXf=@t)2+I~f*_E^JtEP1@IR{CBfTj%T}E3e#n% +zUa{@vU?D$l4DEANwkkK@ruP4ta)E*e^KLGg%$PizyPmHvKNMWtuJQ6sPXY=(1m#>W +z7V?9E!Vj}>a|KfQx5ESpH+q6$@gAp-P#~lbz`aj1_?xinN>3o8b2-Z3w>UZ3QZ}W0 +zWg-!>p>AADDcU^4;0*L4UFgB0QLlXd^y1E&4>txV!T|!`RwjZGl`;-4ZgFf>luHIy +zZ8d8Rh{I3r!g-ht6mAZxMB6VxRqnA0UY`h|mJZy2 +z17BazT$jMKFL3J6Ue_HL1^)4s%$Jj~Qx~1HG#tS@kwL(KP_ZI3dWz0SH(sqj#-*TNGsIWqPj>cj?!GyWvfdEiNOu4$>MIqL=F&Cc0{g*~L5 +zA1wt)=_zMFUkCT5$l!G{1-Y9QtGQ#qm5E(3fYPms_EP*sSVI)bfXN|uNO`BqVuCvd +zv)z8IGRgtM1<_trndVhQ^xA)wn~*W~#d*X@E=W)jcQWI8+?kdzHe;DZ`%+JE%gE}m +z6H=FO8rJxM{N90S=Gi!Mel)TyanxPa;E}C?hJl@e9UWad->;S|v;axgFjrY$z3(rV{MiJ}3M)t;Q?P5wZy0e3G{dcDO7n}3slDXLMrB$;#*W@Qv)D$=?Xs$F(8eTcyGIQ~IWgD%Gn&E>F9y#o>cR-7spE;Rur<_E~Pu)e0I +z#&y1|@8D~8c55<|KMf;&x;hg!A%VOZ38_+uk`jH4#=b9M&xcpxV-7cMN{jXVRnKSe +zlKJJ%=VBV{$DNeI1QkiA;DfdVT?$;O#22z6v6bTK9)fjrfIh!Hq__l~KzuNqT{&kA +zKs@YV6^1ZLGjTgR%(=NHS-DvWnnP)NM#qbHINqmQdCE5??co$3nuikqgm=s7*#Kd*+j_weKrZjMeLeHEoiJm>zuDRU` +zh~ggr^knneWU!Nn}AQt=0Id6Hk; +z4bJqse|V$H`stT?NS0yreYvaZ9YF!fw+N}{3#yXRU!C7?exl35BDC%+!jDMGT^DN# +zN9FGd#5t#;$h}5UgQ?q-Gr15>C6=nLUszle9<+_!!oi_m@_L^-R>_Qty7_g|C%m|5 +z-7^5X5V_ARi?h9_LW%2vByD3X_IvUktqBv{%SYXO1&;e&O#Ll_cfC`Wv1u+l_#RI< +zQ5Kly0;P`%TXaQN(heOg~>V&L{d+ZDA%eq-UKo#1)$rkjSm=nzAE2r +z5--RyKhxfXoGVU3^ab{5XGlyL1+26foG)4HZvN +zG@&I3h0fnK5lIjcrg*XxPy1(gK3_TN`&VYnxP;C|j$~0rT$0f|*#=OzM^NbE-1T5D +z%Csnt)n!sx3N#b(8G&+G3W~Q_B#StA6jZZ=p#wuu`DrAMXm{T@#S;ku4Dme@{Njmk +zCtrh3z6O>o)~o{&Htx+6kn*)$NNBH-biu^aYtWUq +z(G>4rCEKr#tO>!x8A@%W@6g)Xs%2Hq!y#Mbb@9R2@GDWi&!{jhZvzQ1D9nMuPoOS+ +z+cj{9nx5X{jJOIavbFf)Kz5Jnbe5Bu#(XE-z$j&iaP%c9W59OoT0~|N#D*(N2kz={ +zs(|)nH!_+_g1)#ZH2xk>ZTG#6WN#qa3BxZM{NWxq`*#$H255k6Ky?hw*hSA6`c_fl +zT@Ua%E5Ez3;~`kQFmrC#$Nlvc_Uy3#yzhd-6UYuuIwgIBZZC-`dwOBJbfurL(FfhH +z{YkjE+9OrOveY`{t{sGw&51YO1@{iO4)Ki=!Z5#q=m_Hi)_j0`>?;t2j);vv%BUif +z;wpTZdLQLsGvZ()DCdxYudn^Pt;BZ}Rin$4F8h{R`HxT2z`uc&aMXIQOvwgA5%{&) +zFW52MiN!$!EXgx}Px~e1!EMp;#&kY65oDho95j~!qD%YJr`+aK4jCJ4UJ^;q>w@Lf +zvDfg|M`S^@DGxu+7aR3Cx#;%?advj&1~L-m +zJqCP9&TW3migV*`Z$#)Qa>3>Jf)g9D6Ki28P@iX(uso)hic8Dp1F< +zeF;(n8Po8A*~^T{De(J)Z2nqLl@Vv3yoSlGwq0aeOg4ymI(KIkTeur-=J-yp9z?qe)it6gq-wl@I +z0D-_I{|T<5kwD9uH3yf1GWXp5*8eOgJf*q0IRoK|+r{}Fug&0WpNDKMTC@(Xc)9K8 +zy`lByMn!1fnY)1KYP(0Je1)c~WilUuh<&Q8^OE?L9Q^xK*Y@M$`6D6TDCZ^@l8{|} +zxmmNw)mng$hYBii+&ZqedxWT0dnV#LG4zC%+kzcK+-??vEHT>Q-T8zu|s_1IbA#OV)^+1pg1OmmZn` + diff --git a/patches/server/0001-Rebrand.patch b/patches/server/0002-Rebrand.patch similarity index 99% rename from patches/server/0001-Rebrand.patch rename to patches/server/0002-Rebrand.patch index d51517c50..524079a96 100644 --- a/patches/server/0001-Rebrand.patch +++ b/patches/server/0002-Rebrand.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Rebrand diff --git a/build.gradle.kts b/build.gradle.kts -index c2005f46b6cf2d7f80f65dc52f9ff7cd03d0419f..92ec24aefa42e4359391e549c8dcff313c44be71 100644 +index f766183883f009368b79e1201553c184a373e2cf..fba83a632abace5c2b280ea5919175f9574429f6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,8 +30,8 @@ repositories { @@ -63,7 +63,7 @@ index 3789df8ef9c0b4150c3baccf84dbaff57c0fb65a..43a88bc58716eef4040584944f52893c 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 862a48eeb135e7734bafc560782f1bf92b18a020..986a2a05fae2a095b99b2ec330543cd0d2f49cde 100644 +index f3bf6270c7735869083559f907c65d95144dbe6f..4cc099b469a7554e381a6f7aceb686a94f7d6605 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1714,7 +1714,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop type, Level world) { diff --git a/src/main/java/net/pl3x/purpur/PurpurConfig.java b/src/main/java/net/pl3x/purpur/PurpurConfig.java -index 7c5ecdbd5a05ae07a440cadf79d59a439e4db524..bd0807e5647ad4745fa585bf7e6c853239cdd271 100644 +index 32f44983aaec838960e6435580ee261cfa266b53..feceb11ae27bf32db5bb3ea8f02ebaa26dc9466b 100644 --- a/src/main/java/net/pl3x/purpur/PurpurConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurConfig.java @@ -127,4 +127,9 @@ public class PurpurConfig { diff --git a/patches/server/0004-Configurable-entity-base-attributes.patch b/patches/server/0005-Configurable-entity-base-attributes.patch similarity index 100% rename from patches/server/0004-Configurable-entity-base-attributes.patch rename to patches/server/0005-Configurable-entity-base-attributes.patch diff --git a/patches/server/0005-Timings-stuff.patch b/patches/server/0006-Timings-stuff.patch similarity index 97% rename from patches/server/0005-Timings-stuff.patch rename to patches/server/0006-Timings-stuff.patch index 47dcac2a5..4454b21fa 100644 --- a/patches/server/0005-Timings-stuff.patch +++ b/patches/server/0006-Timings-stuff.patch @@ -55,7 +55,7 @@ index 9d920565ff65a84b1b9a2a4777fd8bc8f07e0153..33263af33b4355e5079fadfcfae77d03 String hostName = "BrokenHost"; try { diff --git a/src/main/java/net/pl3x/purpur/PurpurConfig.java b/src/main/java/net/pl3x/purpur/PurpurConfig.java -index bd0807e5647ad4745fa585bf7e6c853239cdd271..eadb1904d431f1820e0bc469361b4dbe18675d47 100644 +index feceb11ae27bf32db5bb3ea8f02ebaa26dc9466b..7c898f58e954692481753238a307ad8e107411c1 100644 --- a/src/main/java/net/pl3x/purpur/PurpurConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurConfig.java @@ -132,4 +132,10 @@ public class PurpurConfig { diff --git a/patches/server/0006-Barrels-and-enderchests-6-rows.patch b/patches/server/0007-Barrels-and-enderchests-6-rows.patch similarity index 99% rename from patches/server/0006-Barrels-and-enderchests-6-rows.patch rename to patches/server/0007-Barrels-and-enderchests-6-rows.patch index 77c271687..0730c7b55 100644 --- a/patches/server/0006-Barrels-and-enderchests-6-rows.patch +++ b/patches/server/0007-Barrels-and-enderchests-6-rows.patch @@ -142,7 +142,7 @@ index f494063ead9c6303fb3ca880aba2a877ae8d83ab..3b027111ed933856ae86ad5f62aac744 } diff --git a/src/main/java/net/pl3x/purpur/PurpurConfig.java b/src/main/java/net/pl3x/purpur/PurpurConfig.java -index 358ce4bb952fbf50bf78a35f19b4c61e6f4d1afd..7bb30fba98b5b217df72e6df5b2dc4e6082d1a6a 100644 +index 7c898f58e954692481753238a307ad8e107411c1..6388f8ebd0b2aede894da39cdab2c2965e0f231f 100644 --- a/src/main/java/net/pl3x/purpur/PurpurConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurConfig.java @@ -138,4 +138,23 @@ public class PurpurConfig { diff --git a/patches/server/0007-Advancement-API.patch b/patches/server/0008-Advancement-API.patch similarity index 100% rename from patches/server/0007-Advancement-API.patch rename to patches/server/0008-Advancement-API.patch diff --git a/patches/server/0008-Llama-API.patch b/patches/server/0009-Llama-API.patch similarity index 100% rename from patches/server/0008-Llama-API.patch rename to patches/server/0009-Llama-API.patch diff --git a/patches/server/0009-AFK-API.patch b/patches/server/0010-AFK-API.patch similarity index 99% rename from patches/server/0009-AFK-API.patch rename to patches/server/0010-AFK-API.patch index 70524f09d..8ec0fcb94 100644 --- a/patches/server/0009-AFK-API.patch +++ b/patches/server/0010-AFK-API.patch @@ -177,7 +177,7 @@ index 6a4e44dd8935018d1b5283761dfb8e855be62987..afe70b0d5bd98d05bbb7afc756108f09 if (range < 0.0D || d < range * range) { return true; diff --git a/src/main/java/net/pl3x/purpur/PurpurConfig.java b/src/main/java/net/pl3x/purpur/PurpurConfig.java -index 409948bf86d7b3a30106598cf9ee3cdff05b928d..3efc51e347ae0d5224eae4727867eaf1e83291b8 100644 +index 6388f8ebd0b2aede894da39cdab2c2965e0f231f..b2b3d591d86f51e5cb118e2334f840c627b5e677 100644 --- a/src/main/java/net/pl3x/purpur/PurpurConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurConfig.java @@ -129,8 +129,16 @@ public class PurpurConfig { diff --git a/patches/server/0010-Bring-back-server-name.patch b/patches/server/0011-Bring-back-server-name.patch similarity index 94% rename from patches/server/0010-Bring-back-server-name.patch rename to patches/server/0011-Bring-back-server-name.patch index 8eba391f0..8f07431ca 100644 --- a/patches/server/0010-Bring-back-server-name.patch +++ b/patches/server/0011-Bring-back-server-name.patch @@ -17,7 +17,7 @@ index c9d80a5430cc66d6189bf337770af43121a5bfd5..329336d413317388455fbbf03036cee1 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 4547dd9d4c1f9a35adf52c1cbbf23be9a9a23d68..67dfc1239d50a3561f5e78aa18cd4b313f58ed03 100644 +index 84858994a92380d71ae9c456853ab3ea54afa98e..8809e738bd0baa6ba18d57df14d9b95624d90d14 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 { diff --git a/patches/server/0011-Configurable-server-mod-name.patch b/patches/server/0012-Configurable-server-mod-name.patch similarity index 89% rename from patches/server/0011-Configurable-server-mod-name.patch rename to patches/server/0012-Configurable-server-mod-name.patch index 641898cdb..d89995e3b 100644 --- a/patches/server/0011-Configurable-server-mod-name.patch +++ b/patches/server/0012-Configurable-server-mod-name.patch @@ -5,7 +5,7 @@ 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 6ca331b3b5ed248c669e221079c798a85b752b68..74fda35fa08d9d0915f724504ae1b79f51e02d2b 100644 +index 06177ec182d79cd101da9cd43a841656d27d6d8b..0e0f7d48337f261481731917aabf45d8445121a0 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1715,7 +1715,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop. Co-authored-by: Mariell Hoversholm 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 c2b0b1adcff5baf169901710d492317d44b93846..0e07c6b4ddb599bd9e32147dda64ea6ac2abacbd 100644 +index c7636191fa2ba92db95a7f779d0e5a1bd45198aa..6cd66a4edbe2010d2a34506baf1ba6fe3de38bb9 100644 --- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java @@ -311,7 +311,14 @@ public abstract class ChunkGenerator { diff --git a/patches/server/0151-Farmland-trampling-changes.patch b/patches/server/0152-Farmland-trampling-changes.patch similarity index 97% rename from patches/server/0151-Farmland-trampling-changes.patch rename to patches/server/0152-Farmland-trampling-changes.patch index c9a675d46..34d2c513c 100644 --- a/patches/server/0151-Farmland-trampling-changes.patch +++ b/patches/server/0152-Farmland-trampling-changes.patch @@ -37,7 +37,7 @@ index aad204181c4f54ee533bfe3fc04a8705b847e371..16eb80e7926cf4fe421c17d7d851d457 if (CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.DIRT.defaultBlockState()).isCancelled()) { return; diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java -index 830077db284376d2ab5987a059d609f160765e03..d360245d421adf0972573a455d00f5b3e1ae46cb 100644 +index b8b58d3ac28d038db9eefa57b46864738e17d8d1..3fc168278c29de591e989e38acf24c39bc6aea3f 100644 --- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java @@ -453,10 +453,16 @@ public class PurpurWorldConfig { diff --git a/patches/server/0152-Movement-options-for-armor-stands.patch b/patches/server/0153-Movement-options-for-armor-stands.patch similarity index 96% rename from patches/server/0152-Movement-options-for-armor-stands.patch rename to patches/server/0153-Movement-options-for-armor-stands.patch index dda5acd33..7d954e641 100644 --- a/patches/server/0152-Movement-options-for-armor-stands.patch +++ b/patches/server/0153-Movement-options-for-armor-stands.patch @@ -17,7 +17,7 @@ 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 7e3270945d60d6b0664020f7988300b5f8e91a20..e3d6be3c3ac94b683acfdba9b1feea88f80c5707 100644 +index 103cac9915855858768c80a05e6e6a73f9cb9402..9a83d2eea22f5602c79a08cf9d3af14a0838f9c9 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -1588,7 +1588,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n @@ -66,7 +66,7 @@ index 796ab61f4513c02b0d55d34044d2f7084c447796..d119f8ab447bc17deabc494463de4961 + // Purpur end } diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java -index d360245d421adf0972573a455d00f5b3e1ae46cb..20ba8faadf79fe8edf18a37579dcda791ea91fa1 100644 +index 3fc168278c29de591e989e38acf24c39bc6aea3f..1ab68e01a3594334ce356200712f64e4e063563e 100644 --- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java @@ -92,10 +92,16 @@ public class PurpurWorldConfig { diff --git a/patches/server/0153-Fix-stuck-in-portals.patch b/patches/server/0154-Fix-stuck-in-portals.patch similarity index 94% rename from patches/server/0153-Fix-stuck-in-portals.patch rename to patches/server/0154-Fix-stuck-in-portals.patch index 873bc6110..fc83f3bff 100644 --- a/patches/server/0153-Fix-stuck-in-portals.patch +++ b/patches/server/0154-Fix-stuck-in-portals.patch @@ -17,7 +17,7 @@ index d362c9efbad593a7cd7efbcdba4acf1457abe4f7..1747d80315d7b11b7ccb21dd41324434 // 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 e3d6be3c3ac94b683acfdba9b1feea88f80c5707..158691a1912155390751698a833b1375a2b8e613 100644 +index 9a83d2eea22f5602c79a08cf9d3af14a0838f9c9..59c2cd680830b87d6f412020672a0a156800b7fc 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -2698,12 +2698,15 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n @@ -37,7 +37,7 @@ index e3d6be3c3ac94b683acfdba9b1feea88f80c5707..158691a1912155390751698a833b1375 this.isInsidePortal = true; diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java -index 20ba8faadf79fe8edf18a37579dcda791ea91fa1..13cf662db2278b321ad9c66b94a440fe3587cf57 100644 +index 1ab68e01a3594334ce356200712f64e4e063563e..a621a705fba7b6cdd255eda900296defbe4476b5 100644 --- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java @@ -285,6 +285,7 @@ public class PurpurWorldConfig { diff --git a/patches/server/0154-Toggle-for-water-sensitive-mob-damage.patch b/patches/server/0155-Toggle-for-water-sensitive-mob-damage.patch similarity index 98% rename from patches/server/0154-Toggle-for-water-sensitive-mob-damage.patch rename to patches/server/0155-Toggle-for-water-sensitive-mob-damage.patch index 98e83c274..271666f0c 100644 --- a/patches/server/0154-Toggle-for-water-sensitive-mob-damage.patch +++ b/patches/server/0155-Toggle-for-water-sensitive-mob-damage.patch @@ -70,7 +70,7 @@ index b3928617f732b49cfc124e9bdb879110413defd7..1c6d29a30df66e9971cd50e264bb4455 @Override diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java -index 13cf662db2278b321ad9c66b94a440fe3587cf57..7e6514fb7060f6221924b3bd9eefc75b8797b801 100644 +index a621a705fba7b6cdd255eda900296defbe4476b5..e67254a644e1af029fe76b6770751bc34f77d64f 100644 --- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java @@ -614,6 +614,7 @@ public class PurpurWorldConfig { diff --git a/patches/server/0155-Config-to-always-tame-in-Creative.patch b/patches/server/0156-Config-to-always-tame-in-Creative.patch similarity index 98% rename from patches/server/0155-Config-to-always-tame-in-Creative.patch rename to patches/server/0156-Config-to-always-tame-in-Creative.patch index d4a02b7ff..55838d834 100644 --- a/patches/server/0155-Config-to-always-tame-in-Creative.patch +++ b/patches/server/0156-Config-to-always-tame-in-Creative.patch @@ -59,7 +59,7 @@ index 518dd0e6b4889c049e438b393baa795a5eac3e7d..21e154c4e7fe261a41c891b481072fbd this.navigation.stop(); this.setTarget((LivingEntity) null); diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java -index 7e6514fb7060f6221924b3bd9eefc75b8797b801..7beffa42708570a99805fa579c0e7c9b3e1c4f17 100644 +index e67254a644e1af029fe76b6770751bc34f77d64f..5a09eff3d6a0c4542cb8f647fb0cee45358bdcdd 100644 --- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java @@ -358,6 +358,7 @@ public class PurpurWorldConfig { diff --git a/patches/server/0156-End-crystal-explosion-options.patch b/patches/server/0157-End-crystal-explosion-options.patch similarity index 98% rename from patches/server/0156-End-crystal-explosion-options.patch rename to patches/server/0157-End-crystal-explosion-options.patch index e9e84011b..d40653d08 100644 --- a/patches/server/0156-End-crystal-explosion-options.patch +++ b/patches/server/0157-End-crystal-explosion-options.patch @@ -52,7 +52,7 @@ index 92e65f3fbc8f5d77bb8cc31e7a7780c2589f4227..0c46507ab0b904fb1f79bc5421c88c03 this.onDestroyedBy(source); diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java -index 7beffa42708570a99805fa579c0e7c9b3e1c4f17..fef45610bdba497f5d37df39292d1ff9aae76a02 100644 +index 5a09eff3d6a0c4542cb8f647fb0cee45358bdcdd..b74cbdc1789b4fd6c6e93f639e25d84829d1d933 100644 --- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java @@ -460,6 +460,35 @@ public class PurpurWorldConfig { diff --git a/patches/server/0157-Add-unsafe-Entity-serialization-API.patch b/patches/server/0158-Add-unsafe-Entity-serialization-API.patch similarity index 100% rename from patches/server/0157-Add-unsafe-Entity-serialization-API.patch rename to patches/server/0158-Add-unsafe-Entity-serialization-API.patch diff --git a/patches/server/0158-Configs-for-if-Wither-Ender-Dragon-can-ride-vehicles.patch b/patches/server/0159-Configs-for-if-Wither-Ender-Dragon-can-ride-vehicles.patch similarity index 97% rename from patches/server/0158-Configs-for-if-Wither-Ender-Dragon-can-ride-vehicles.patch rename to patches/server/0159-Configs-for-if-Wither-Ender-Dragon-can-ride-vehicles.patch index 78c18895a..c0b775ecf 100644 --- a/patches/server/0158-Configs-for-if-Wither-Ender-Dragon-can-ride-vehicles.patch +++ b/patches/server/0159-Configs-for-if-Wither-Ender-Dragon-can-ride-vehicles.patch @@ -29,7 +29,7 @@ index 15d35d325b66e1a417eb7ba699597d627bd4eb54..36745f845f33c877595d572e46bd8e96 } diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java -index fef45610bdba497f5d37df39292d1ff9aae76a02..d2776666a4012a3d1e96ff5ae8659f8195f00755 100644 +index b74cbdc1789b4fd6c6e93f639e25d84829d1d933..e56519d674ef5031c642b99497bf46f9cfab03a0 100644 --- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java @@ -849,6 +849,7 @@ public class PurpurWorldConfig { diff --git a/patches/server/0159-Dont-run-with-scissors.patch b/patches/server/0160-Dont-run-with-scissors.patch similarity index 96% rename from patches/server/0159-Dont-run-with-scissors.patch rename to patches/server/0160-Dont-run-with-scissors.patch index 652d1729c..d94506146 100644 --- a/patches/server/0159-Dont-run-with-scissors.patch +++ b/patches/server/0160-Dont-run-with-scissors.patch @@ -22,7 +22,7 @@ index c65a6ab9a30ad269a2caad9ab5fad77c4c7ce3aa..082deb251fa793561cc4366cc7b03d7b this.lastGoodX = this.player.getX(); this.lastGoodY = this.player.getY(); diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java -index d2776666a4012a3d1e96ff5ae8659f8195f00755..026c4e5ddedcf732e1ef0c66736193a627d6eb25 100644 +index e56519d674ef5031c642b99497bf46f9cfab03a0..f46005346663a7e29b0fa938cf6f469f3a340177 100644 --- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java @@ -146,6 +146,8 @@ public class PurpurWorldConfig { diff --git a/patches/server/0160-One-Punch-Man.patch b/patches/server/0161-One-Punch-Man.patch similarity index 96% rename from patches/server/0160-One-Punch-Man.patch rename to patches/server/0161-One-Punch-Man.patch index 48f88e886..6c25ec3e1 100644 --- a/patches/server/0160-One-Punch-Man.patch +++ b/patches/server/0161-One-Punch-Man.patch @@ -30,7 +30,7 @@ index 5c82dd279fc3a5847e2e0ed6c9cf9e70acfb3bff..c123e723d4fc3202eb7a4c74a356ffcd if (human) { // PAIL: Be sure to drag all this code from the EntityHuman subclass each update. diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java -index 026c4e5ddedcf732e1ef0c66736193a627d6eb25..c3e20c28044eb0fe5e39c68ce635b692ec9f141b 100644 +index f46005346663a7e29b0fa938cf6f469f3a340177..260153f7f32558766b75985e6d7a76c21c2b49ac 100644 --- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java @@ -290,6 +290,7 @@ public class PurpurWorldConfig { diff --git a/patches/server/0161-Add-config-for-snow-on-blue-ice.patch b/patches/server/0162-Add-config-for-snow-on-blue-ice.patch similarity index 96% rename from patches/server/0161-Add-config-for-snow-on-blue-ice.patch rename to patches/server/0162-Add-config-for-snow-on-blue-ice.patch index b7ed9872b..4be45e3b3 100644 --- a/patches/server/0161-Add-config-for-snow-on-blue-ice.patch +++ b/patches/server/0162-Add-config-for-snow-on-blue-ice.patch @@ -29,7 +29,7 @@ index 0169d874247a96c2e10a65ecb9c0c093f5a6ecfb..b760e2d014b3ae70671878082bb853b7 @Override diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java -index c3e20c28044eb0fe5e39c68ce635b692ec9f141b..d5577dc51e984fda8798870ab784064fa2e78d09 100644 +index 260153f7f32558766b75985e6d7a76c21c2b49ac..39b2dcd904a222bdcb4e723af43dfc6762d8518e 100644 --- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java @@ -515,6 +515,11 @@ public class PurpurWorldConfig { diff --git a/patches/server/0162-Configurable-Ender-Pearl-cooldown-damage-and-Endermi.patch b/patches/server/0163-Configurable-Ender-Pearl-cooldown-damage-and-Endermi.patch similarity index 98% rename from patches/server/0162-Configurable-Ender-Pearl-cooldown-damage-and-Endermi.patch rename to patches/server/0163-Configurable-Ender-Pearl-cooldown-damage-and-Endermi.patch index ae9e71d4b..4218210ee 100644 --- a/patches/server/0162-Configurable-Ender-Pearl-cooldown-damage-and-Endermi.patch +++ b/patches/server/0163-Configurable-Ender-Pearl-cooldown-damage-and-Endermi.patch @@ -43,7 +43,7 @@ index 749ab72edc0d2e9c6f1161415ab8d59d3d6ca976..897c202c0905040072a06fdfa2032a7f // Paper end if (user instanceof net.minecraft.server.level.ServerPlayer) { diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java -index d5577dc51e984fda8798870ab784064fa2e78d09..47c68f6655afb759b5028b20ce0c48e40e51a92a 100644 +index 39b2dcd904a222bdcb4e723af43dfc6762d8518e..3045a4fb9c316b36b3b908e6a58510675fa372a6 100644 --- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java @@ -148,6 +148,10 @@ public class PurpurWorldConfig { diff --git a/patches/server/0163-Config-to-ignore-nearby-mobs-when-sleeping.patch b/patches/server/0164-Config-to-ignore-nearby-mobs-when-sleeping.patch similarity index 96% rename from patches/server/0163-Config-to-ignore-nearby-mobs-when-sleeping.patch rename to patches/server/0164-Config-to-ignore-nearby-mobs-when-sleeping.patch index 80e5e77a4..9e1c62f11 100644 --- a/patches/server/0163-Config-to-ignore-nearby-mobs-when-sleeping.patch +++ b/patches/server/0164-Config-to-ignore-nearby-mobs-when-sleeping.patch @@ -18,7 +18,7 @@ index 1747d80315d7b11b7ccb21dd41324434e655b628..d5ee740792699f2eab3965d4920de913 } } diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java -index 47c68f6655afb759b5028b20ce0c48e40e51a92a..95058c3c61275407b9393c3fa874e7993c100ded 100644 +index 3045a4fb9c316b36b3b908e6a58510675fa372a6..3cdf1832a1552e10d8fc36d06200439bc36de7e1 100644 --- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java +++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java @@ -299,6 +299,7 @@ public class PurpurWorldConfig { diff --git a/patches/server/0164-Add-back-player-spawned-endermite-API.patch b/patches/server/0165-Add-back-player-spawned-endermite-API.patch similarity index 100% rename from patches/server/0164-Add-back-player-spawned-endermite-API.patch rename to patches/server/0165-Add-back-player-spawned-endermite-API.patch diff --git a/patches/server/0165-Config-Enderman-aggressiveness-towards-Endermites.patch b/patches/server/0166-Config-Enderman-aggressiveness-towards-Endermites.patch similarity index 100% rename from patches/server/0165-Config-Enderman-aggressiveness-towards-Endermites.patch rename to patches/server/0166-Config-Enderman-aggressiveness-towards-Endermites.patch diff --git a/patches/server/0166-Config-to-ignore-Dragon-Head-wearers-and-stare-aggro.patch b/patches/server/0167-Config-to-ignore-Dragon-Head-wearers-and-stare-aggro.patch similarity index 100% rename from patches/server/0166-Config-to-ignore-Dragon-Head-wearers-and-stare-aggro.patch rename to patches/server/0167-Config-to-ignore-Dragon-Head-wearers-and-stare-aggro.patch diff --git a/patches/server/0167-Tick-fluids-config.patch b/patches/server/0168-Tick-fluids-config.patch similarity index 100% rename from patches/server/0167-Tick-fluids-config.patch rename to patches/server/0168-Tick-fluids-config.patch diff --git a/patches/server/0168-Config-to-disable-Llama-caravans.patch b/patches/server/0169-Config-to-disable-Llama-caravans.patch similarity index 100% rename from patches/server/0168-Config-to-disable-Llama-caravans.patch rename to patches/server/0169-Config-to-disable-Llama-caravans.patch diff --git a/patches/server/0169-Config-to-make-Creepers-explode-on-death.patch b/patches/server/0170-Config-to-make-Creepers-explode-on-death.patch similarity index 100% rename from patches/server/0169-Config-to-make-Creepers-explode-on-death.patch rename to patches/server/0170-Config-to-make-Creepers-explode-on-death.patch diff --git a/patches/server/0170-Configurable-ravager-griefable-blocks-list.patch b/patches/server/0171-Configurable-ravager-griefable-blocks-list.patch similarity index 100% rename from patches/server/0170-Configurable-ravager-griefable-blocks-list.patch rename to patches/server/0171-Configurable-ravager-griefable-blocks-list.patch diff --git a/patches/server/0171-Sneak-to-bulk-process-composter.patch b/patches/server/0172-Sneak-to-bulk-process-composter.patch similarity index 100% rename from patches/server/0171-Sneak-to-bulk-process-composter.patch rename to patches/server/0172-Sneak-to-bulk-process-composter.patch diff --git a/patches/server/0172-Config-for-skipping-night.patch b/patches/server/0173-Config-for-skipping-night.patch similarity index 100% rename from patches/server/0172-Config-for-skipping-night.patch rename to patches/server/0173-Config-for-skipping-night.patch diff --git a/patches/server/0173-Add-config-for-villager-trading.patch b/patches/server/0174-Add-config-for-villager-trading.patch similarity index 100% rename from patches/server/0173-Add-config-for-villager-trading.patch rename to patches/server/0174-Add-config-for-villager-trading.patch diff --git a/patches/server/0174-Allow-infinity-on-crossbows.patch b/patches/server/0175-Allow-infinity-on-crossbows.patch similarity index 100% rename from patches/server/0174-Allow-infinity-on-crossbows.patch rename to patches/server/0175-Allow-infinity-on-crossbows.patch diff --git a/patches/server/0175-Drowning-Settings.patch b/patches/server/0176-Drowning-Settings.patch similarity index 100% rename from patches/server/0175-Drowning-Settings.patch rename to patches/server/0176-Drowning-Settings.patch diff --git a/patches/server/0176-Break-individual-slabs-when-sneaking.patch b/patches/server/0177-Break-individual-slabs-when-sneaking.patch similarity index 100% rename from patches/server/0176-Break-individual-slabs-when-sneaking.patch rename to patches/server/0177-Break-individual-slabs-when-sneaking.patch diff --git a/patches/server/0177-Config-to-disable-hostile-mob-spawn-on-ice.patch b/patches/server/0178-Config-to-disable-hostile-mob-spawn-on-ice.patch similarity index 100% rename from patches/server/0177-Config-to-disable-hostile-mob-spawn-on-ice.patch rename to patches/server/0178-Config-to-disable-hostile-mob-spawn-on-ice.patch diff --git a/patches/server/0178-Config-to-show-Armor-Stand-arms-on-spawn.patch b/patches/server/0179-Config-to-show-Armor-Stand-arms-on-spawn.patch similarity index 100% rename from patches/server/0178-Config-to-show-Armor-Stand-arms-on-spawn.patch rename to patches/server/0179-Config-to-show-Armor-Stand-arms-on-spawn.patch diff --git a/patches/server/0179-Option-to-make-doors-require-redstone.patch b/patches/server/0180-Option-to-make-doors-require-redstone.patch similarity index 100% rename from patches/server/0179-Option-to-make-doors-require-redstone.patch rename to patches/server/0180-Option-to-make-doors-require-redstone.patch diff --git a/patches/server/0180-Config-to-allow-for-unsafe-enchants.patch b/patches/server/0181-Config-to-allow-for-unsafe-enchants.patch similarity index 100% rename from patches/server/0180-Config-to-allow-for-unsafe-enchants.patch rename to patches/server/0181-Config-to-allow-for-unsafe-enchants.patch diff --git a/patches/server/0181-Configurable-sponge-absorption.patch b/patches/server/0182-Configurable-sponge-absorption.patch similarity index 100% rename from patches/server/0181-Configurable-sponge-absorption.patch rename to patches/server/0182-Configurable-sponge-absorption.patch diff --git a/patches/server/0182-Projectile-offset-config.patch b/patches/server/0183-Projectile-offset-config.patch similarity index 100% rename from patches/server/0182-Projectile-offset-config.patch rename to patches/server/0183-Projectile-offset-config.patch diff --git a/patches/server/0183-Config-for-powered-rail-activation-distance.patch b/patches/server/0184-Config-for-powered-rail-activation-distance.patch similarity index 100% rename from patches/server/0183-Config-for-powered-rail-activation-distance.patch rename to patches/server/0184-Config-for-powered-rail-activation-distance.patch diff --git a/patches/server/0184-Piglin-portal-spawn-modifier.patch b/patches/server/0185-Piglin-portal-spawn-modifier.patch similarity index 100% rename from patches/server/0184-Piglin-portal-spawn-modifier.patch rename to patches/server/0185-Piglin-portal-spawn-modifier.patch diff --git a/patches/server/0185-Config-to-change-max-number-of-bees.patch b/patches/server/0186-Config-to-change-max-number-of-bees.patch similarity index 100% rename from patches/server/0185-Config-to-change-max-number-of-bees.patch rename to patches/server/0186-Config-to-change-max-number-of-bees.patch diff --git a/patches/server/0186-Configurable-damage-settings-for-magma-blocks.patch b/patches/server/0187-Configurable-damage-settings-for-magma-blocks.patch similarity index 100% rename from patches/server/0186-Configurable-damage-settings-for-magma-blocks.patch rename to patches/server/0187-Configurable-damage-settings-for-magma-blocks.patch diff --git a/patches/server/0187-Config-for-wither-explosion-radius.patch b/patches/server/0188-Config-for-wither-explosion-radius.patch similarity index 100% rename from patches/server/0187-Config-for-wither-explosion-radius.patch rename to patches/server/0188-Config-for-wither-explosion-radius.patch diff --git a/patches/server/0188-Gamemode-extra-permissions.patch b/patches/server/0189-Gamemode-extra-permissions.patch similarity index 100% rename from patches/server/0188-Gamemode-extra-permissions.patch rename to patches/server/0189-Gamemode-extra-permissions.patch diff --git a/patches/server/0189-Config-for-changing-the-blocks-that-turn-into-dirt-p.patch b/patches/server/0190-Config-for-changing-the-blocks-that-turn-into-dirt-p.patch similarity index 100% rename from patches/server/0189-Config-for-changing-the-blocks-that-turn-into-dirt-p.patch rename to patches/server/0190-Config-for-changing-the-blocks-that-turn-into-dirt-p.patch diff --git a/patches/server/0190-Configurable-piston-push-limit.patch b/patches/server/0191-Configurable-piston-push-limit.patch similarity index 100% rename from patches/server/0190-Configurable-piston-push-limit.patch rename to patches/server/0191-Configurable-piston-push-limit.patch diff --git a/patches/server/0191-Configurable-broadcast-settings.patch b/patches/server/0192-Configurable-broadcast-settings.patch similarity index 100% rename from patches/server/0191-Configurable-broadcast-settings.patch rename to patches/server/0192-Configurable-broadcast-settings.patch diff --git a/patches/server/0192-Fix-SPIGOT-6278.patch b/patches/server/0193-Fix-SPIGOT-6278.patch similarity index 100% rename from patches/server/0192-Fix-SPIGOT-6278.patch rename to patches/server/0193-Fix-SPIGOT-6278.patch diff --git a/patches/server/0193-Configurable-mob-blindness.patch b/patches/server/0194-Configurable-mob-blindness.patch similarity index 100% rename from patches/server/0193-Configurable-mob-blindness.patch rename to patches/server/0194-Configurable-mob-blindness.patch diff --git a/patches/server/0194-Hide-hidden-players-from-entity-selector.patch b/patches/server/0195-Hide-hidden-players-from-entity-selector.patch similarity index 100% rename from patches/server/0194-Hide-hidden-players-from-entity-selector.patch rename to patches/server/0195-Hide-hidden-players-from-entity-selector.patch diff --git a/patches/server/0195-Config-for-health-to-impact-Creeper-explosion-radius.patch b/patches/server/0196-Config-for-health-to-impact-Creeper-explosion-radius.patch similarity index 100% rename from patches/server/0195-Config-for-health-to-impact-Creeper-explosion-radius.patch rename to patches/server/0196-Config-for-health-to-impact-Creeper-explosion-radius.patch diff --git a/patches/server/0196-Iron-golem-poppy-calms-anger.patch b/patches/server/0197-Iron-golem-poppy-calms-anger.patch similarity index 100% rename from patches/server/0196-Iron-golem-poppy-calms-anger.patch rename to patches/server/0197-Iron-golem-poppy-calms-anger.patch diff --git a/patches/server/0197-Breedable-parrots.patch b/patches/server/0198-Breedable-parrots.patch similarity index 100% rename from patches/server/0197-Breedable-parrots.patch rename to patches/server/0198-Breedable-parrots.patch diff --git a/patches/server/0198-Configurable-powered-rail-boost-modifier.patch b/patches/server/0199-Configurable-powered-rail-boost-modifier.patch similarity index 100% rename from patches/server/0198-Configurable-powered-rail-boost-modifier.patch rename to patches/server/0199-Configurable-powered-rail-boost-modifier.patch diff --git a/patches/server/0199-Add-config-change-multiplier-critical-damage-value.patch b/patches/server/0200-Add-config-change-multiplier-critical-damage-value.patch similarity index 100% rename from patches/server/0199-Add-config-change-multiplier-critical-damage-value.patch rename to patches/server/0200-Add-config-change-multiplier-critical-damage-value.patch diff --git a/patches/server/0200-Option-to-disable-dragon-egg-teleporting.patch b/patches/server/0201-Option-to-disable-dragon-egg-teleporting.patch similarity index 100% rename from patches/server/0200-Option-to-disable-dragon-egg-teleporting.patch rename to patches/server/0201-Option-to-disable-dragon-egg-teleporting.patch diff --git a/patches/server/0201-Config-for-unverified-username-message.patch b/patches/server/0202-Config-for-unverified-username-message.patch similarity index 100% rename from patches/server/0201-Config-for-unverified-username-message.patch rename to patches/server/0202-Config-for-unverified-username-message.patch diff --git a/patches/server/0202-Make-anvil-cumulative-cost-configurable.patch b/patches/server/0203-Make-anvil-cumulative-cost-configurable.patch similarity index 100% rename from patches/server/0202-Make-anvil-cumulative-cost-configurable.patch rename to patches/server/0203-Make-anvil-cumulative-cost-configurable.patch diff --git a/patches/server/0203-ShulkerBox-allow-oversized-stacks.patch b/patches/server/0204-ShulkerBox-allow-oversized-stacks.patch similarity index 100% rename from patches/server/0203-ShulkerBox-allow-oversized-stacks.patch rename to patches/server/0204-ShulkerBox-allow-oversized-stacks.patch diff --git a/patches/server/0204-Bee-can-work-when-raining-or-at-night.patch b/patches/server/0205-Bee-can-work-when-raining-or-at-night.patch similarity index 100% rename from patches/server/0204-Bee-can-work-when-raining-or-at-night.patch rename to patches/server/0205-Bee-can-work-when-raining-or-at-night.patch diff --git a/patches/server/0205-API-for-any-mob-to-burn-daylight.patch b/patches/server/0206-API-for-any-mob-to-burn-daylight.patch similarity index 100% rename from patches/server/0205-API-for-any-mob-to-burn-daylight.patch rename to patches/server/0206-API-for-any-mob-to-burn-daylight.patch diff --git a/patches/server/0206-Fix-advancement-triggers-on-entity-death.patch b/patches/server/0207-Fix-advancement-triggers-on-entity-death.patch similarity index 100% rename from patches/server/0206-Fix-advancement-triggers-on-entity-death.patch rename to patches/server/0207-Fix-advancement-triggers-on-entity-death.patch diff --git a/patches/server/0207-Config-MobEffect-by-world.patch b/patches/server/0208-Config-MobEffect-by-world.patch similarity index 100% rename from patches/server/0207-Config-MobEffect-by-world.patch rename to patches/server/0208-Config-MobEffect-by-world.patch diff --git a/patches/server/0208-Beacon-Activation-Range-Configurable.patch b/patches/server/0209-Beacon-Activation-Range-Configurable.patch similarity index 100% rename from patches/server/0208-Beacon-Activation-Range-Configurable.patch rename to patches/server/0209-Beacon-Activation-Range-Configurable.patch diff --git a/patches/server/0209-Add-toggle-for-sand-duping-fix.patch b/patches/server/0210-Add-toggle-for-sand-duping-fix.patch similarity index 100% rename from patches/server/0209-Add-toggle-for-sand-duping-fix.patch rename to patches/server/0210-Add-toggle-for-sand-duping-fix.patch diff --git a/patches/server/0210-Add-toggle-for-end-portal-safe-teleporting.patch b/patches/server/0211-Add-toggle-for-end-portal-safe-teleporting.patch similarity index 100% rename from patches/server/0210-Add-toggle-for-end-portal-safe-teleporting.patch rename to patches/server/0211-Add-toggle-for-end-portal-safe-teleporting.patch diff --git a/patches/server/0211-Flying-Fall-Damage-API.patch b/patches/server/0212-Flying-Fall-Damage-API.patch similarity index 100% rename from patches/server/0211-Flying-Fall-Damage-API.patch rename to patches/server/0212-Flying-Fall-Damage-API.patch diff --git a/patches/server/0212-Make-lightning-rod-range-configurable.patch b/patches/server/0213-Make-lightning-rod-range-configurable.patch similarity index 100% rename from patches/server/0212-Make-lightning-rod-range-configurable.patch rename to patches/server/0213-Make-lightning-rod-range-configurable.patch diff --git a/patches/server/0213-Burp-after-eating-food-fills-hunger-bar-completely.patch b/patches/server/0214-Burp-after-eating-food-fills-hunger-bar-completely.patch similarity index 100% rename from patches/server/0213-Burp-after-eating-food-fills-hunger-bar-completely.patch rename to patches/server/0214-Burp-after-eating-food-fills-hunger-bar-completely.patch diff --git a/patches/server/0214-Allow-player-join-full-server-by-permission.patch b/patches/server/0215-Allow-player-join-full-server-by-permission.patch similarity index 100% rename from patches/server/0214-Allow-player-join-full-server-by-permission.patch rename to patches/server/0215-Allow-player-join-full-server-by-permission.patch diff --git a/patches/server/0215-Populator-seed-controls.patch b/patches/server/0216-Populator-seed-controls.patch similarity index 100% rename from patches/server/0215-Populator-seed-controls.patch rename to patches/server/0216-Populator-seed-controls.patch diff --git a/patches/server/0216-Add-permission-bypass-for-portal-waiting.patch b/patches/server/0217-Add-permission-bypass-for-portal-waiting.patch similarity index 100% rename from patches/server/0216-Add-permission-bypass-for-portal-waiting.patch rename to patches/server/0217-Add-permission-bypass-for-portal-waiting.patch