From d7d2f88893ce4c10d5e77c606b06173463c8613d Mon Sep 17 00:00:00 2001 From: Nassim Jahnke Date: Tue, 3 Dec 2024 21:24:17 +0100 Subject: Apply remaining patches, fix API --- ...e-for-removal-all-OldEnum-related-methods.patch | 6 +- patches/api/0489-Improve-entity-effect-API.patch | 8 +- ...chunk-data-to-disk-if-it-serializes-witho.patch | 129 + .../server/1037-API-for-checking-sent-chunks.patch | 46 + .../1038-Fix-CraftWorld-isChunkGenerated.patch | 44 + ...d-startup-flag-to-disable-gamerule-limits.patch | 66 + .../server/1040-Improved-Watchdog-Support.patch | 475 ++++ ...Detail-more-information-in-watchdog-dumps.patch | 297 +++ .../1042-Entity-load-save-limit-per-chunk.patch | 81 + ...recalculate-regionfile-header-if-it-is-co.patch | 744 ++++++ patches/server/1044-Bundle-spark.patch | 401 ++++ .../1045-Improve-performance-of-mass-crafts.patch | 94 + .../1046-Incremental-chunk-and-player-saving.patch | 134 ++ .../server/1047-Optimise-general-POI-access.patch | 1069 +++++++++ ...tracker-desync-when-new-players-are-added.patch | 107 + patches/server/1049-Lag-compensation-ticks.patch | 131 ++ ...llision-checking-in-player-move-packet-ha.patch | 170 ++ .../1051-Optional-per-player-mob-spawns.patch | 233 ++ ...celling-PreCreatureSpawnEvent-with-per-pl.patch | 89 + ...s-with-certain-tasks-not-processing-durin.patch | 46 + ...1054-Allow-using-old-ender-pearl-behavior.patch | 62 + .../1055-Block-Enderpearl-Travel-Exploit.patch | 49 + ...stencies-in-dispense-events-regarding-sta.patch | 432 ++++ patches/server/1057-Correct-update-cursor.patch | 42 + ...layer-onEntityRemove-for-all-online-playe.patch | 19 + .../1059-Eigencraft-redstone-implementation.patch | 1099 +++++++++ ...ove-performance-of-RecipeMap-removeRecipe.patch | 89 + ...-done-in-CraftMapCanvas.drawImage-by-limi.patch | 75 + ...Alternate-Current-redstone-implementation.patch | 2452 ++++++++++++++++++++ ...ncorrect-invulnerability-damage-reduction.patch | 115 + ...-when-EntityResurrectEvent-is-uncancelled.patch | 23 + ...65-API-to-check-if-the-server-is-sleeping.patch | 37 + .../1066-API-to-allow-disallow-tick-sleeping.patch | 67 + .../1067-Configurable-Entity-Despawn-Time.patch | 39 + patches/server/1068-Expanded-Art-API.patch | 33 + ...t-to-find-spawn-position-if-there-isn-t-a.patch | 28 + ...chunk-data-to-disk-if-it-serializes-witho.patch | 129 - .../server/1039-API-for-checking-sent-chunks.patch | 46 - .../1040-Fix-CraftWorld-isChunkGenerated.patch | 44 - ...d-startup-flag-to-disable-gamerule-limits.patch | 66 - .../server/1042-Improved-Watchdog-Support.patch | 475 ---- ...Detail-more-information-in-watchdog-dumps.patch | 297 --- .../1044-Entity-load-save-limit-per-chunk.patch | 81 - ...recalculate-regionfile-header-if-it-is-co.patch | 744 ------ patches/unapplied/server/1046-Bundle-spark.patch | 401 ---- .../1047-Improve-performance-of-mass-crafts.patch | 94 - .../1048-Incremental-chunk-and-player-saving.patch | 134 -- .../server/1049-Optimise-general-POI-access.patch | 1063 --------- ...tracker-desync-when-new-players-are-added.patch | 107 - .../server/1051-Lag-compensation-ticks.patch | 131 -- ...llision-checking-in-player-move-packet-ha.patch | 170 -- .../1053-Optional-per-player-mob-spawns.patch | 233 -- ...celling-PreCreatureSpawnEvent-with-per-pl.patch | 89 - ...s-with-certain-tasks-not-processing-durin.patch | 46 - ...1056-Allow-using-old-ender-pearl-behavior.patch | 62 - .../1057-Block-Enderpearl-Travel-Exploit.patch | 49 - ...stencies-in-dispense-events-regarding-sta.patch | 432 ---- .../server/1059-Correct-update-cursor.patch | 42 - ...layer-onEntityRemove-for-all-online-playe.patch | 19 - .../1062-Eigencraft-redstone-implementation.patch | 1099 --------- ...ove-performance-of-RecipeMap-removeRecipe.patch | 89 - ...-done-in-CraftMapCanvas.drawImage-by-limi.patch | 75 - ...Alternate-Current-redstone-implementation.patch | 2452 -------------------- ...ncorrect-invulnerability-damage-reduction.patch | 115 - ...-when-EntityResurrectEvent-is-uncancelled.patch | 23 - ...68-API-to-check-if-the-server-is-sleeping.patch | 37 - .../1069-API-to-allow-disallow-tick-sleeping.patch | 67 - .../1070-Configurable-Entity-Despawn-Time.patch | 39 - .../unapplied/server/1071-Expanded-Art-API.patch | 33 - ...t-to-find-spawn-position-if-there-isn-t-a.patch | 28 - 70 files changed, 9023 insertions(+), 9019 deletions(-) create mode 100644 patches/server/1036-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch create mode 100644 patches/server/1037-API-for-checking-sent-chunks.patch create mode 100644 patches/server/1038-Fix-CraftWorld-isChunkGenerated.patch create mode 100644 patches/server/1039-Add-startup-flag-to-disable-gamerule-limits.patch create mode 100644 patches/server/1040-Improved-Watchdog-Support.patch create mode 100644 patches/server/1041-Detail-more-information-in-watchdog-dumps.patch create mode 100644 patches/server/1042-Entity-load-save-limit-per-chunk.patch create mode 100644 patches/server/1043-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch create mode 100644 patches/server/1044-Bundle-spark.patch create mode 100644 patches/server/1045-Improve-performance-of-mass-crafts.patch create mode 100644 patches/server/1046-Incremental-chunk-and-player-saving.patch create mode 100644 patches/server/1047-Optimise-general-POI-access.patch create mode 100644 patches/server/1048-Fix-entity-tracker-desync-when-new-players-are-added.patch create mode 100644 patches/server/1049-Lag-compensation-ticks.patch create mode 100644 patches/server/1050-Optimise-collision-checking-in-player-move-packet-ha.patch create mode 100644 patches/server/1051-Optional-per-player-mob-spawns.patch create mode 100644 patches/server/1052-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch create mode 100644 patches/server/1053-Avoid-issues-with-certain-tasks-not-processing-durin.patch create mode 100644 patches/server/1054-Allow-using-old-ender-pearl-behavior.patch create mode 100644 patches/server/1055-Block-Enderpearl-Travel-Exploit.patch create mode 100644 patches/server/1056-Fix-inconsistencies-in-dispense-events-regarding-sta.patch create mode 100644 patches/server/1057-Correct-update-cursor.patch create mode 100644 patches/server/1058-Call-CraftPlayer-onEntityRemove-for-all-online-playe.patch create mode 100644 patches/server/1059-Eigencraft-redstone-implementation.patch create mode 100644 patches/server/1060-Improve-performance-of-RecipeMap-removeRecipe.patch create mode 100644 patches/server/1061-Reduce-work-done-in-CraftMapCanvas.drawImage-by-limi.patch create mode 100644 patches/server/1062-Add-Alternate-Current-redstone-implementation.patch create mode 100644 patches/server/1063-Fix-incorrect-invulnerability-damage-reduction.patch create mode 100644 patches/server/1064-Fix-NPE-when-EntityResurrectEvent-is-uncancelled.patch create mode 100644 patches/server/1065-API-to-check-if-the-server-is-sleeping.patch create mode 100644 patches/server/1066-API-to-allow-disallow-tick-sleeping.patch create mode 100644 patches/server/1067-Configurable-Entity-Despawn-Time.patch create mode 100644 patches/server/1068-Expanded-Art-API.patch create mode 100644 patches/server/1069-Only-attempt-to-find-spawn-position-if-there-isn-t-a.patch delete mode 100644 patches/unapplied/server/1038-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch delete mode 100644 patches/unapplied/server/1039-API-for-checking-sent-chunks.patch delete mode 100644 patches/unapplied/server/1040-Fix-CraftWorld-isChunkGenerated.patch delete mode 100644 patches/unapplied/server/1041-Add-startup-flag-to-disable-gamerule-limits.patch delete mode 100644 patches/unapplied/server/1042-Improved-Watchdog-Support.patch delete mode 100644 patches/unapplied/server/1043-Detail-more-information-in-watchdog-dumps.patch delete mode 100644 patches/unapplied/server/1044-Entity-load-save-limit-per-chunk.patch delete mode 100644 patches/unapplied/server/1045-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch delete mode 100644 patches/unapplied/server/1046-Bundle-spark.patch delete mode 100644 patches/unapplied/server/1047-Improve-performance-of-mass-crafts.patch delete mode 100644 patches/unapplied/server/1048-Incremental-chunk-and-player-saving.patch delete mode 100644 patches/unapplied/server/1049-Optimise-general-POI-access.patch delete mode 100644 patches/unapplied/server/1050-Fix-entity-tracker-desync-when-new-players-are-added.patch delete mode 100644 patches/unapplied/server/1051-Lag-compensation-ticks.patch delete mode 100644 patches/unapplied/server/1052-Optimise-collision-checking-in-player-move-packet-ha.patch delete mode 100644 patches/unapplied/server/1053-Optional-per-player-mob-spawns.patch delete mode 100644 patches/unapplied/server/1054-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch delete mode 100644 patches/unapplied/server/1055-Avoid-issues-with-certain-tasks-not-processing-durin.patch delete mode 100644 patches/unapplied/server/1056-Allow-using-old-ender-pearl-behavior.patch delete mode 100644 patches/unapplied/server/1057-Block-Enderpearl-Travel-Exploit.patch delete mode 100644 patches/unapplied/server/1058-Fix-inconsistencies-in-dispense-events-regarding-sta.patch delete mode 100644 patches/unapplied/server/1059-Correct-update-cursor.patch delete mode 100644 patches/unapplied/server/1060-Call-CraftPlayer-onEntityRemove-for-all-online-playe.patch delete mode 100644 patches/unapplied/server/1062-Eigencraft-redstone-implementation.patch delete mode 100644 patches/unapplied/server/1063-Improve-performance-of-RecipeMap-removeRecipe.patch delete mode 100644 patches/unapplied/server/1064-Reduce-work-done-in-CraftMapCanvas.drawImage-by-limi.patch delete mode 100644 patches/unapplied/server/1065-Add-Alternate-Current-redstone-implementation.patch delete mode 100644 patches/unapplied/server/1066-Fix-incorrect-invulnerability-damage-reduction.patch delete mode 100644 patches/unapplied/server/1067-Fix-NPE-when-EntityResurrectEvent-is-uncancelled.patch delete mode 100644 patches/unapplied/server/1068-API-to-check-if-the-server-is-sleeping.patch delete mode 100644 patches/unapplied/server/1069-API-to-allow-disallow-tick-sleeping.patch delete mode 100644 patches/unapplied/server/1070-Configurable-Entity-Despawn-Time.patch delete mode 100644 patches/unapplied/server/1071-Expanded-Art-API.patch delete mode 100644 patches/unapplied/server/1072-Only-attempt-to-find-spawn-position-if-there-isn-t-a.patch diff --git a/patches/api/0483-Deprecate-for-removal-all-OldEnum-related-methods.patch b/patches/api/0483-Deprecate-for-removal-all-OldEnum-related-methods.patch index 70a87660d6..9f74aeceee 100644 --- a/patches/api/0483-Deprecate-for-removal-all-OldEnum-related-methods.patch +++ b/patches/api/0483-Deprecate-for-removal-all-OldEnum-related-methods.patch @@ -71,7 +71,7 @@ index 21f9998b472dc18eb308554f5cdf467f6675f2f0..521f035409ee61a9ad73d39bec938f29 return Lists.newArrayList(Registry.ATTRIBUTE).toArray(new Attribute[0]); } diff --git a/src/main/java/org/bukkit/block/Biome.java b/src/main/java/org/bukkit/block/Biome.java -index 739fef949defca7b6bf4e6b3e079446c24d9b34c..1b9f7a7759e59e0294f379dc6388f400010faa2f 100644 +index 739fef949defca7b6bf4e6b3e079446c24d9b34c..20fc2b30fdcdedb012dfe129e746d0b9e162fc36 100644 --- a/src/main/java/org/bukkit/block/Biome.java +++ b/src/main/java/org/bukkit/block/Biome.java @@ -93,7 +93,7 @@ public interface Biome extends OldEnum, Keyed, net.kyori.adventure.transl @@ -79,7 +79,7 @@ index 739fef949defca7b6bf4e6b3e079446c24d9b34c..1b9f7a7759e59e0294f379dc6388f400 * @deprecated Biome is no longer an enum, custom biomes will have their own biome instance. */ - @Deprecated(since = "1.21.3") -+ @Deprecated(since = "1.21.3", forRemoval = true) @ApiStatus.ScheduledForRemoval(inVersion = "1.22") // Paper - will be removed via asm-utils ++ @Deprecated(since = "1.21.3", forRemoval = true) @org.jetbrains.annotations.ApiStatus.ScheduledForRemoval(inVersion = "1.22") // Paper - will be removed via asm-utils Biome CUSTOM = Bukkit.getUnsafe().getCustomBiome(); @NotNull @@ -88,7 +88,7 @@ index 739fef949defca7b6bf4e6b3e079446c24d9b34c..1b9f7a7759e59e0294f379dc6388f400 */ @NotNull - @Deprecated(since = "1.21.3") -+ @Deprecated(since = "1.21.3", forRemoval = true) @ApiStatus.ScheduledForRemoval(inVersion = "1.22") // Paper - will be removed via asm-utils ++ @Deprecated(since = "1.21.3", forRemoval = true) @org.jetbrains.annotations.ApiStatus.ScheduledForRemoval(inVersion = "1.22") // Paper - will be removed via asm-utils static Biome valueOf(@NotNull String name) { if ("CUSTOM".equals(name)) { return Biome.CUSTOM; diff --git a/patches/api/0489-Improve-entity-effect-API.patch b/patches/api/0489-Improve-entity-effect-API.patch index 95d238f818..2f11287f3e 100644 --- a/patches/api/0489-Improve-entity-effect-API.patch +++ b/patches/api/0489-Improve-entity-effect-API.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Improve entity effect API diff --git a/src/main/java/org/bukkit/EntityEffect.java b/src/main/java/org/bukkit/EntityEffect.java -index d7ccccdf3f5e2c572efd528a92e240ec6ea60028..51d51638b7220f886c8415581869df8708e72fef 100644 +index d7ccccdf3f5e2c572efd528a92e240ec6ea60028..37c321067ee25f8a38130eb65ed06e1c986c65b6 100644 --- a/src/main/java/org/bukkit/EntityEffect.java +++ b/src/main/java/org/bukkit/EntityEffect.java @@ -112,11 +112,25 @@ public enum EntityEffect { @@ -76,7 +76,7 @@ index d7ccccdf3f5e2c572efd528a92e240ec6ea60028..51d51638b7220f886c8415581869df87 HURT_BERRY_BUSH(44, LivingEntity.class), /** * Fox chews the food in its mouth -@@ -331,7 +355,25 @@ public enum EntityEffect { +@@ -331,7 +355,23 @@ public enum EntityEffect { * Sniffer must have a target and be in {@link Sniffer.State#SEARCHING} or * {@link Sniffer.State#DIGGING} */ @@ -96,9 +96,7 @@ index d7ccccdf3f5e2c572efd528a92e240ec6ea60028..51d51638b7220f886c8415581869df87 + * Does not apply to plain creaking entities as they are not invulnerable like the transient ones spawned by the + * creaking heart. + */ -+ @MinecraftExperimental(MinecraftExperimental.Requires.WINTER_DROP) -+ @org.jetbrains.annotations.ApiStatus.Experimental -+ SHAKE(66, org.bukkit.entity.CreakingTransient.class); ++ SHAKE(66, org.bukkit.entity.Creaking.class); + // Paper end - add missing EntityEffect private final byte data; diff --git a/patches/server/1036-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch b/patches/server/1036-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch new file mode 100644 index 0000000000..c155a5cca2 --- /dev/null +++ b/patches/server/1036-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch @@ -0,0 +1,129 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sun, 19 Dec 2021 09:13:41 -0800 +Subject: [PATCH] Only write chunk data to disk if it serializes without + throwing + +This ensures at least a valid version of the chunk exists +on disk, even if outdated + +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 057875cbbdc92ba49b429f9a129514760edb32a2..ff092c6d0cd436f14a9a4ff5c8ddbb5538d1a8c5 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 +@@ -539,6 +539,7 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + + } + // Paper end ++ public static final int MAX_CHUNK_SIZE = 500 * 1024 * 1024; // Paper - don't write garbage data to disk if writing serialization fails + private class ChunkBuffer extends ByteArrayOutputStream implements ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkBuffer { // Paper - rewrite chunk system + + private final ChunkPos pos; +@@ -571,6 +572,23 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + super.write(RegionFile.this.version.getId()); + this.pos = chunkcoordintpair; + } ++ // Paper start - don't write garbage data to disk if writing serialization fails ++ @Override ++ public void write(final int b) { ++ if (this.count > MAX_CHUNK_SIZE) { ++ throw new RegionFileStorage.RegionFileSizeException("Region file too large: " + this.count); ++ } ++ super.write(b); ++ } ++ ++ @Override ++ public void write(final byte[] b, final int off, final int len) { ++ if (this.count + len > MAX_CHUNK_SIZE) { ++ throw new RegionFileStorage.RegionFileSizeException("Region file too large: " + (this.count + len)); ++ } ++ super.write(b, off, len); ++ } ++ // Paper end - don't write garbage data to disk if writing serialization fails + + public void close() throws IOException { + ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count); +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 fdf8e18d24442178b52397acb482ffa3306a32e3..8d66d6b7aeb9feb54ebd83f5c73b45d42b9a7034 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 +@@ -19,6 +19,8 @@ import net.minecraft.world.level.ChunkPos; + + public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage { // Paper - rewrite chunk system + ++ private static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger(); // Paper ++ + public static final String ANVIL_EXTENSION = ".mca"; + private static final int MAX_CACHE_SIZE = 256; + public final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap(); +@@ -123,11 +125,24 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + // (and, the regionfile parameter is unused for writing until the write call) + final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData writeData = ((ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile)regionFile).moonrise$startWrite(compound, pos); + ++ try { // Paper - implement RegionFileSizeException + try { + NbtIo.write(compound, writeData.output()); + } finally { + writeData.output().close(); + } ++ // Paper start - implement RegionFileSizeException ++ } catch (final RegionFileSizeException ex) { ++ // note: it's OK if close() is called, as close() here will not issue a write to the RegionFile ++ // see startWrite ++ final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024); ++ LOGGER.error("Chunk at (" + chunkX + "," + chunkZ + ") in regionfile '" + regionFile.getPath().toString() + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk."); ++ return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData( ++ compound, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.DELETE, ++ null, null ++ ); ++ } ++ // Paper end - implement RegionFileSizeException + + return writeData; + } +@@ -378,10 +393,18 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + try { + NbtIo.write(nbt, (DataOutput) dataoutputstream); + regionfile.setOversized(pos.x, pos.z, false); // Paper - We don't do this anymore, mojang stores differently, but clear old meta flag if it exists to get rid of our own meta file once last oversized is gone ++ // Paper start - don't write garbage data to disk if writing serialization fails ++ dataoutputstream.close(); // Only write if successful ++ } catch (final RegionFileSizeException ex) { ++ regionfile.clear(pos); ++ final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024); ++ LOGGER.error("Chunk at (" + pos.x + "," + pos.z + ") in regionfile '" + regionfile.getPath().toString() + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk."); ++ return; ++ // Paper end - don't write garbage data to disk if writing serialization fails + } catch (Throwable throwable) { + if (dataoutputstream != null) { + try { +- dataoutputstream.close(); ++ //dataoutputstream.close(); // Paper - don't write garbage data to disk if writing serialization fails + } catch (Throwable throwable1) { + throwable.addSuppressed(throwable1); + } +@@ -389,10 +412,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + + throw throwable; + } +- +- if (dataoutputstream != null) { +- dataoutputstream.close(); +- } ++ // Paper - don't write garbage data to disk if writing serialization fails; move into try block to only write if successfully serialized + } + + } +@@ -435,4 +455,13 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + public RegionStorageInfo info() { + return this.info; + } ++ ++ // Paper start - don't write garbage data to disk if writing serialization fails ++ public static final class RegionFileSizeException extends RuntimeException { ++ ++ public RegionFileSizeException(String message) { ++ super(message); ++ } ++ } ++ // Paper end - don't write garbage data to disk if writing serialization fails + } diff --git a/patches/server/1037-API-for-checking-sent-chunks.patch b/patches/server/1037-API-for-checking-sent-chunks.patch new file mode 100644 index 0000000000..67f4f99ba6 --- /dev/null +++ b/patches/server/1037-API-for-checking-sent-chunks.patch @@ -0,0 +1,46 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Flo0 +Date: Mon, 8 Apr 2024 16:43:16 +0200 +Subject: [PATCH] API for checking sent chunks + + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 79d72a2f6cd85f18c644bd48801fdda86f9d235a..84c494ca81b8f58604b372ac7236147776817a4f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -3485,6 +3485,35 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + } + // Paper end + ++ // Paper start - Add chunk view API ++ @Override ++ public Set getSentChunkKeys() { ++ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); ++ return it.unimi.dsi.fastutil.longs.LongSets.unmodifiable( ++ this.getHandle().moonrise$getChunkLoader().getSentChunksRaw().clone() ++ ); ++ } ++ ++ @Override ++ public Set getSentChunks() { ++ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); ++ final it.unimi.dsi.fastutil.longs.LongOpenHashSet rawChunkKeys = this.getHandle().moonrise$getChunkLoader().getSentChunksRaw(); ++ final it.unimi.dsi.fastutil.objects.ObjectOpenHashSet chunks = new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(rawChunkKeys.size()); ++ final org.bukkit.World world = this.getWorld(); ++ ++ final it.unimi.dsi.fastutil.longs.LongIterator iter = this.getHandle().moonrise$getChunkLoader().getSentChunksRaw().longIterator(); ++ while (iter.hasNext()) chunks.add(world.getChunkAt(iter.nextLong(), false)); ++ ++ return it.unimi.dsi.fastutil.objects.ObjectSets.unmodifiable(chunks); ++ } ++ ++ @Override ++ public boolean isChunkSent(final long chunkKey) { ++ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); ++ return this.getHandle().moonrise$getChunkLoader().getSentChunksRaw().contains(chunkKey); ++ } ++ // Paper end ++ + public Player.Spigot spigot() + { + return this.spigot; diff --git a/patches/server/1038-Fix-CraftWorld-isChunkGenerated.patch b/patches/server/1038-Fix-CraftWorld-isChunkGenerated.patch new file mode 100644 index 0000000000..1f7efead99 --- /dev/null +++ b/patches/server/1038-Fix-CraftWorld-isChunkGenerated.patch @@ -0,0 +1,44 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> +Date: Tue, 18 Jun 2024 12:43:06 -0700 +Subject: [PATCH] Fix CraftWorld#isChunkGenerated + +The upstream implementation is returning true for non-full chunks. + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 7c936f59ad8fc1fa244adcc19c413d6e0e4f7323..a9f7ac8d1f5e184687d53ab3e8b348bb7f4f2ba0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -398,11 +398,28 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean isChunkGenerated(int x, int z) { +- try { +- return this.isChunkLoaded(x, z) || this.world.getChunkSource().chunkMap.read(new ChunkPos(x, z)).get().isPresent(); +- } catch (InterruptedException | ExecutionException ex) { +- throw new RuntimeException(ex); ++ // Paper start - Fix this method ++ if (!Bukkit.isPrimaryThread()) { ++ return java.util.concurrent.CompletableFuture.supplyAsync(() -> { ++ return CraftWorld.this.isChunkGenerated(x, z); ++ }, world.getChunkSource().mainThreadProcessor).join(); ++ } ++ ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(x, z); ++ if (chunk != null) { ++ return chunk instanceof ImposterProtoChunk || chunk instanceof net.minecraft.world.level.chunk.LevelChunk; + } ++ final java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); ++ ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad( ++ this.world, x, z, false, ChunkStatus.EMPTY, true, ca.spottedleaf.concurrentutil.util.Priority.NORMAL, future::complete ++ ); ++ world.getChunkSource().mainThreadProcessor.managedBlock(future::isDone); ++ return future.thenApply(c -> { ++ if (c != null) { ++ return c.getPersistedStatus() == ChunkStatus.FULL; ++ } ++ return false; ++ }).join(); ++ // Paper end - Fix this method + } + + @Override diff --git a/patches/server/1039-Add-startup-flag-to-disable-gamerule-limits.patch b/patches/server/1039-Add-startup-flag-to-disable-gamerule-limits.patch new file mode 100644 index 0000000000..9ad4196621 --- /dev/null +++ b/patches/server/1039-Add-startup-flag-to-disable-gamerule-limits.patch @@ -0,0 +1,66 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Fri, 25 Oct 2024 14:20:40 -0700 +Subject: [PATCH] Add startup flag to disable gamerule limits + +-DPaper.DisableGameRuleLimits=true will disable gamerule limits + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 5889a47a25abf3494fba74ebb3c5e07a2408f161..e4a50b2f6cdc9daf6a018aaf44bb029c5003fa65 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -2077,13 +2077,21 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + + if (this.lastSpawnChunkRadius > 1) { +- this.getChunkSource().removeRegionTicket(TicketType.START, new ChunkPos(blockposition1), this.lastSpawnChunkRadius, Unit.INSTANCE); ++ // Paper start - allow disabling gamerule limits ++ for (ChunkPos chunkPos : io.papermc.paper.util.MCUtil.getSpiralOutChunks(blockposition1, this.lastSpawnChunkRadius - 2)) { ++ this.getChunkSource().removeTicketAtLevel(TicketType.START, chunkPos, net.minecraft.server.level.ChunkLevel.ENTITY_TICKING_LEVEL, Unit.INSTANCE); ++ } ++ // Paper end - allow disabling gamerule limits + } + + int i = this.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS) + 1; + + if (i > 1) { +- this.getChunkSource().addRegionTicket(TicketType.START, new ChunkPos(pos), i, Unit.INSTANCE); ++ // Paper start - allow disabling gamerule limits ++ for (ChunkPos chunkPos : io.papermc.paper.util.MCUtil.getSpiralOutChunks(pos, i - 2)) { ++ this.getChunkSource().addTicketAtLevel(TicketType.START, chunkPos, net.minecraft.server.level.ChunkLevel.ENTITY_TICKING_LEVEL, Unit.INSTANCE); ++ } ++ // Paper end - allow disabling gamerule limits + } + + this.lastSpawnChunkRadius = i; +diff --git a/src/main/java/net/minecraft/world/level/GameRules.java b/src/main/java/net/minecraft/world/level/GameRules.java +index 4ae47c2c5a6bcfbf932d000a80974463e2d3818d..7c363d59c6567cae8e6caf213be51804efa5a96d 100644 +--- a/src/main/java/net/minecraft/world/level/GameRules.java ++++ b/src/main/java/net/minecraft/world/level/GameRules.java +@@ -36,6 +36,14 @@ import org.slf4j.Logger; + + public class GameRules { + ++ // Paper start - allow disabling gamerule limits ++ private static final boolean DISABLE_LIMITS = Boolean.getBoolean("paper.disableGameRuleLimits"); ++ ++ private static int limit(final int limit, final int unlimited) { ++ return DISABLE_LIMITS ? unlimited : limit; ++ } ++ // Paper end - allow disabling gamerule limits ++ + public static final int DEFAULT_RANDOM_TICK_SPEED = 3; + static final Logger LOGGER = LogUtils.getLogger(); + private static final Map, GameRules.Type> GAME_RULE_TYPES = Maps.newTreeMap(Comparator.comparing((gamerules_gamerulekey) -> { +@@ -120,9 +128,9 @@ public class GameRules { + public static final GameRules.Key RULE_GLOBAL_SOUND_EVENTS = GameRules.register("globalSoundEvents", GameRules.Category.MISC, GameRules.BooleanValue.create(true)); + public static final GameRules.Key RULE_DO_VINES_SPREAD = GameRules.register("doVinesSpread", GameRules.Category.UPDATES, GameRules.BooleanValue.create(true)); + public static final GameRules.Key RULE_ENDER_PEARLS_VANISH_ON_DEATH = GameRules.register("enderPearlsVanishOnDeath", GameRules.Category.PLAYER, GameRules.BooleanValue.create(true)); +- public static final GameRules.Key RULE_MINECART_MAX_SPEED = GameRules.register("minecartMaxSpeed", GameRules.Category.MISC, GameRules.IntegerValue.create(8, 1, 1000, FeatureFlagSet.of(FeatureFlags.MINECART_IMPROVEMENTS), (minecraftserver, gamerules_gameruleint) -> { ++ public static final GameRules.Key RULE_MINECART_MAX_SPEED = GameRules.register("minecartMaxSpeed", GameRules.Category.MISC, GameRules.IntegerValue.create(8, 1, limit(1000, Integer.MAX_VALUE), FeatureFlagSet.of(FeatureFlags.MINECART_IMPROVEMENTS), (minecraftserver, gamerules_gameruleint) -> { // Paper - allow disabling gamerule limits + })); +- public static final GameRules.Key RULE_SPAWN_CHUNK_RADIUS = GameRules.register("spawnChunkRadius", GameRules.Category.MISC, GameRules.IntegerValue.create(2, 0, 32, FeatureFlagSet.of(), (minecraftserver, gamerules_gameruleint) -> { ++ public static final GameRules.Key RULE_SPAWN_CHUNK_RADIUS = GameRules.register("spawnChunkRadius", GameRules.Category.MISC, GameRules.IntegerValue.create(2, 0, limit(32, Integer.MAX_VALUE), FeatureFlagSet.of(), (minecraftserver, gamerules_gameruleint) -> { // Paper - allow disabling gamerule limits + ServerLevel worldserver = minecraftserver; // CraftBukkit - per-world + + worldserver.setDefaultSpawnPos(worldserver.getSharedSpawnPos(), worldserver.getSharedSpawnAngle()); diff --git a/patches/server/1040-Improved-Watchdog-Support.patch b/patches/server/1040-Improved-Watchdog-Support.patch new file mode 100644 index 0000000000..56fc63ce68 --- /dev/null +++ b/patches/server/1040-Improved-Watchdog-Support.patch @@ -0,0 +1,475 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Sun, 12 Apr 2020 15:50:48 -0400 +Subject: [PATCH] Improved Watchdog Support + +Forced Watchdog Crash support and Improve Async Shutdown + +If the request to shut down the server is received while we are in +a watchdog hang, immediately treat it as a crash and begin the shutdown +process. Shutdown process is now improved to also shutdown cleanly when +not using restart scripts either. + +If a server is deadlocked, a server owner can send SIGUP (or any other signal +the JVM understands to shut down as it currently does) and the watchdog +will no longer need to wait until the full timeout, allowing you to trigger +a close process and try to shut the server down gracefully, saving player and +world data. + +Previously there was no way to trigger this outside of waiting for a full watchdog +timeout, which may be set to a really long time... + +Additionally, fix everything to do with shutting the server down asynchronously. + +Previously, nearly everything about the process was fragile and unsafe. Main might +not have actually been frozen, and might still be manipulating state. + +Or, some reuest might ask main to do something in the shutdown but main is dead. + +Or worse, other things might start closing down items such as the Console or Thread Pool +before we are fully shutdown. + +This change tries to resolve all of these issues by moving everything into the stop +method and guaranteeing only one thread is stopping the server. + +We then issue Thread Death to the main thread of another thread initiates the stop process. +We have to ensure Thread Death propagates correctly though to stop main completely. + +This is to ensure that if main isn't truely stuck, it's not manipulating state we are trying to save. + +This also moves all plugins who register "delayed init" tasks to occur just before "Done" so they +are properly accounted for and wont trip watchdog on init. + +Feature patch + +diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java +index 6aaed8e8bf8c721fc834da5c76ac72a4c3e92458..4b002e8b75d117b726b0de274a76d3596fce015b 100644 +--- a/src/main/java/com/destroystokyo/paper/Metrics.java ++++ b/src/main/java/com/destroystokyo/paper/Metrics.java +@@ -92,7 +92,12 @@ public class Metrics { + * Starts the Scheduler which submits our data every 30 minutes. + */ + private void startSubmitting() { +- final Runnable submitTask = this::submitData; ++ final Runnable submitTask = () -> { ++ if (MinecraftServer.getServer().hasStopped()) { ++ return; ++ } ++ submitData(); ++ }; + + // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution of requests on the + // bStats backend. To circumvent this problem, we introduce some randomness into the initial and second delay. +diff --git a/src/main/java/io/papermc/paper/util/LogManagerShutdownThread.java b/src/main/java/io/papermc/paper/util/LogManagerShutdownThread.java +new file mode 100644 +index 0000000000000000000000000000000000000000..183e141d0c13190c6905dc4510d891992afef878 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/LogManagerShutdownThread.java +@@ -0,0 +1,26 @@ ++package io.papermc.paper.util; ++ ++public class LogManagerShutdownThread extends Thread { ++ ++ static LogManagerShutdownThread INSTANCE = new LogManagerShutdownThread(); ++ public static final void hook() { ++ if (INSTANCE == null) { ++ throw new IllegalStateException("Cannot re-hook after being unhooked"); ++ } ++ Runtime.getRuntime().addShutdownHook(INSTANCE); ++ } ++ ++ public static final void unhook() { ++ Runtime.getRuntime().removeShutdownHook(INSTANCE); ++ INSTANCE = null; ++ } ++ ++ private LogManagerShutdownThread() { ++ super("Log4j2 Shutdown Thread"); ++ } ++ ++ @Override ++ public void run() { ++ org.apache.logging.log4j.LogManager.shutdown(); ++ } ++} +diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java +index 589a8bf75be6ccc59f1e5dd5d8d9afed41c4772d..b24265573fdef5d9a964bcd76146f34542c420cf 100644 +--- a/src/main/java/net/minecraft/CrashReport.java ++++ b/src/main/java/net/minecraft/CrashReport.java +@@ -237,6 +237,7 @@ public class CrashReport { + } + + public static CrashReport forThrowable(Throwable cause, String title) { ++ if (cause instanceof ThreadDeath) com.destroystokyo.paper.util.SneakyThrow.sneaky(cause); // Paper + while (cause instanceof CompletionException && cause.getCause() != null) { + cause = cause.getCause(); + } +diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java +index 42d46c7a7437bea5335a23cbee5708ac57131474..300a044bb0f0e377133f24469cea1a9669de6e58 100644 +--- a/src/main/java/net/minecraft/server/Main.java ++++ b/src/main/java/net/minecraft/server/Main.java +@@ -79,6 +79,7 @@ public class Main { + @SuppressForbidden(reason = "System.out needed before bootstrap") // CraftBukkit - decompile error + @DontObfuscate + public static void main(final OptionSet optionset) { // CraftBukkit - replaces main(String[] astring) ++ io.papermc.paper.util.LogManagerShutdownThread.hook(); // Paper + SharedConstants.tryDetectVersion(); + /* CraftBukkit start - Replace everything + OptionParser optionparser = new OptionParser(); +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 6de6f76e8385c50bd18ef9caaca68a79e1e797ca..8549292b4e96c7b09e2a9707f2d8a75b870ee35b 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -317,7 +317,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); + public int autosavePeriod; + // Paper - don't store the vanilla dispatcher +- private boolean forceTicks; ++ public boolean forceTicks; // Paper - Improved watchdog support + // CraftBukkit end + // Spigot start + public static final int TPS = 20; +@@ -329,6 +329,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { + AtomicReference atomicreference = new AtomicReference(); + Thread thread = new ca.spottedleaf.moonrise.common.util.TickThread(() -> { // Paper - rewrite chunk system +@@ -502,6 +505,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {}; ++ } ++ // Paper end + return new TickTask(this.tickCount, runnable); + } + +@@ -2309,7 +2351,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop implements Profiler + public static boolean isNonRecoverable(Throwable exception) { + return exception instanceof ReportedException reportedException + ? isNonRecoverable(reportedException.getCause()) +- : exception instanceof OutOfMemoryError || exception instanceof StackOverflowError; ++ : exception instanceof OutOfMemoryError || exception instanceof StackOverflowError || exception instanceof ThreadDeath; // Paper + } + } +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index f477c5817f022ce7c4ad25e9b827401434bcfff1..d518493ecf3853b9f2aefceb72e1a4d2e9bf1184 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -1489,6 +1489,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + try { + tickConsumer.accept(entity); + } catch (Throwable throwable) { ++ if (throwable instanceof ThreadDeath) throw throwable; // Paper + // Paper start - Prevent block entity and entity crashes + final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ()); + MinecraftServer.LOGGER.error(msg, throwable); +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 134d63076f231791988e67a5bdf191005112080b..97937e3bd211997f0a0a3e9e671a1c59712d0003 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -1083,6 +1083,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + + gameprofilerfiller.pop(); + } catch (Throwable throwable) { ++ if (throwable instanceof ThreadDeath) throw throwable; // Paper + // Paper start - Prevent block entity and entity crashes + final String msg = String.format("BlockEntity threw exception at %s:%s,%s,%s", LevelChunk.this.getLevel().getWorld().getName(), this.getPos().getX(), this.getPos().getY(), this.getPos().getZ()); + net.minecraft.server.MinecraftServer.LOGGER.error(msg, throwable); +diff --git a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java +index c6e8441e299f477ddb22c1ce2618710763978f1a..e8e93538dfd71de86515d9405f728db1631e949a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java +@@ -12,11 +12,27 @@ public class ServerShutdownThread extends Thread { + @Override + public void run() { + try { ++ // Paper start - try to shutdown on main ++ server.safeShutdown(false, false); ++ for (int i = 1000; i > 0 && !server.hasStopped(); i -= 100) { ++ Thread.sleep(100); ++ } ++ if (server.hasStopped()) { ++ while (!server.hasFullyShutdown) Thread.sleep(1000); ++ return; ++ } ++ // Looks stalled, close async + org.spigotmc.AsyncCatcher.enabled = false; // Spigot ++ server.forceTicks = true; + this.server.close(); ++ while (!server.hasFullyShutdown) Thread.sleep(1000); ++ } catch (InterruptedException e) { ++ e.printStackTrace(); ++ // Paper end + } finally { ++ org.apache.logging.log4j.LogManager.shutdown(); // Paper + try { +- net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender ++ //net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Move into stop + } catch (Exception e) { + } + } +diff --git a/src/main/java/org/spigotmc/RestartCommand.java b/src/main/java/org/spigotmc/RestartCommand.java +index 39e56b95aaafbcd8ebe68fdefaace83702e9510d..3ba27955548a26367a87d6b87c3c61beb299dfb9 100644 +--- a/src/main/java/org/spigotmc/RestartCommand.java ++++ b/src/main/java/org/spigotmc/RestartCommand.java +@@ -139,7 +139,7 @@ public class RestartCommand extends Command + // Paper end + + // Paper start - copied from above and modified to return if the hook registered +- private static boolean addShutdownHook(String restartScript) ++ public static boolean addShutdownHook(String restartScript) // Paper + { + String[] split = restartScript.split( " " ); + if ( split.length > 0 && new File( split[0] ).isFile() ) +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index 529df2a41dd93d6e1505053bd04032dbf0cdaa31..c9e17225bc52fe5e7b2dc0908db225a86c6e94d1 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -11,6 +11,7 @@ import org.bukkit.Bukkit; + public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThread // Paper - rewrite chunk system + { + ++ public static final boolean DISABLE_WATCHDOG = Boolean.getBoolean("disable.watchdog"); // Paper - Improved watchdog support + private static WatchdogThread instance; + private long timeoutTime; + private boolean restart; +@@ -39,6 +40,7 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre + { + if ( WatchdogThread.instance == null ) + { ++ if (timeoutTime <= 0) timeoutTime = 300; // Paper + WatchdogThread.instance = new WatchdogThread( timeoutTime * 1000L, restart ); + WatchdogThread.instance.start(); + } else +@@ -70,12 +72,13 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre + // Paper start + Logger log = Bukkit.getServer().getLogger(); + long currentTime = WatchdogThread.monotonicMillis(); +- if ( this.lastTick != 0 && this.timeoutTime > 0 && currentTime > this.lastTick + this.earlyWarningEvery && !Boolean.getBoolean("disable.watchdog")) // Paper - Add property to disable ++ MinecraftServer server = MinecraftServer.getServer(); ++ if ( this.lastTick != 0 && this.timeoutTime > 0 && WatchdogThread.hasStarted && (!server.isRunning() || (currentTime > this.lastTick + this.earlyWarningEvery && !DISABLE_WATCHDOG) )) // Paper - add property to disable + { +- boolean isLongTimeout = currentTime > lastTick + timeoutTime; ++ boolean isLongTimeout = currentTime > lastTick + timeoutTime || (!server.isRunning() && !server.hasStopped() && currentTime > lastTick + 1000); + // Don't spam early warning dumps + if ( !isLongTimeout && (earlyWarningEvery <= 0 || !hasStarted || currentTime < lastEarlyWarning + earlyWarningEvery || currentTime < lastTick + earlyWarningDelay)) continue; +- if ( !isLongTimeout && MinecraftServer.getServer().hasStopped()) continue; // Don't spam early watchdog warnings during shutdown, we'll come back to this... ++ if ( !isLongTimeout && server.hasStopped()) continue; // Don't spam early watchdog warnings during shutdown, we'll come back to this... + lastEarlyWarning = currentTime; + if (isLongTimeout) { + // Paper end +@@ -136,9 +139,24 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre + + if ( isLongTimeout ) + { +- if ( this.restart && !MinecraftServer.getServer().hasStopped() ) ++ if ( !server.hasStopped() ) + { +- RestartCommand.restart(); ++ AsyncCatcher.enabled = false; // Disable async catcher incase it interferes with us ++ server.forceTicks = true; ++ if (restart) { ++ RestartCommand.addShutdownHook( SpigotConfig.restartScript ); ++ } ++ // try one last chance to safe shutdown on main incase it 'comes back' ++ server.abnormalExit = true; ++ server.safeShutdown(false, restart); ++ try { ++ Thread.sleep(1000); ++ } catch (InterruptedException e) { ++ e.printStackTrace(); ++ } ++ if (!server.hasStopped()) { ++ server.close(); ++ } + } + break; + } // Paper end +diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml +index 637d64da9938e51a97338b9253b43889585c67bb..d2a75850af9c6ad2aca66a5f994f1b587d73eac4 100644 +--- a/src/main/resources/log4j2.xml ++++ b/src/main/resources/log4j2.xml +@@ -1,5 +1,5 @@ + +- ++ + + + diff --git a/patches/server/1041-Detail-more-information-in-watchdog-dumps.patch b/patches/server/1041-Detail-more-information-in-watchdog-dumps.patch new file mode 100644 index 0000000000..f667a5bf33 --- /dev/null +++ b/patches/server/1041-Detail-more-information-in-watchdog-dumps.patch @@ -0,0 +1,297 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 26 Mar 2020 21:59:32 -0700 +Subject: [PATCH] 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 + +Feature patch + +diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java +index e300293ba64a8ac54cc9c5348ecc9f3ed2d27e19..3c866432c8a938c677a315612f3e159bda67a2a2 100644 +--- a/src/main/java/net/minecraft/network/Connection.java ++++ b/src/main/java/net/minecraft/network/Connection.java +@@ -632,7 +632,13 @@ public class Connection extends SimpleChannelInboundHandler> { + if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) + || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING + || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) { ++ // Paper start - detailed watchdog information ++ net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener); ++ try { + tickablepacketlistener.tick(); ++ } finally { ++ net.minecraft.network.protocol.PacketUtils.packetProcessing.pop(); ++ } // Paper end - detailed watchdog information + } // Paper end - Buffer joins to world + } + +diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java +index f7197f1347251a37dd0f6d9ffa2f09bc3a4e1233..1f7f68aad97ee73763c042837f239bdc7167db55 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 = LogUtils.getLogger(); + ++ // Paper 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; ++ } ++ // Paper end - detailed watchdog information ++ + public PacketUtils() {} + + public static void ensureRunningOnSameThread(Packet packet, T listener, ServerLevel world) throws RunningOnDifferentThreadException { +@@ -29,6 +47,8 @@ public class PacketUtils { + public static void ensureRunningOnSameThread(Packet packet, T listener, BlockableEventLoop engine) throws RunningOnDifferentThreadException { + if (!engine.isSameThread()) { + engine.executeIfPossible(() -> { ++ packetProcessing.push(listener); // Paper - detailed watchdog information ++ try { // Paper - detailed watchdog information + if (listener instanceof ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // CraftBukkit - Don't handle sync packets for kicked players + if (listener.shouldHandleMessage(packet)) { + try { +@@ -47,6 +67,12 @@ public class PacketUtils { + } else { + PacketUtils.LOGGER.debug("Ignoring packet due to disconnection: {}", packet); + } ++ // Paper start - detailed watchdog information ++ } finally { ++ totalMainThreadPacketsProcessed.getAndIncrement(); ++ packetProcessing.pop(); ++ } ++ // Paper end - detailed watchdog information + + }); + throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD; +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index e4a50b2f6cdc9daf6a018aaf44bb029c5003fa65..abed6e7b92d1472bbbc5bfd60abf4f9052c749c5 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -1254,7 +1254,26 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + } + ++ // Paper 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; ++ } ++ // Paper end - log detailed entity tick information ++ + public void tickNonPassenger(Entity entity) { ++ // Paper start - log detailed entity tick information ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); ++ try { ++ if (currentlyTickingEntity.get() == null) { ++ currentlyTickingEntity.lazySet(entity); ++ } ++ // Paper end - log detailed entity tick information + // Spigot start + /*if (!org.spigotmc.ActivationRange.checkIfActive(entity)) { // Paper - comment out EAR 2 + entity.tickCount++; +@@ -1284,6 +1303,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + this.tickPassenger(entity, entity1, isActive); // Paper - EAR 2 + } + ++ // Paper start - log detailed entity tick information ++ } finally { ++ if (currentlyTickingEntity.get() == entity) { ++ currentlyTickingEntity.lazySet(null); ++ } ++ } ++ // Paper end - log detailed entity tick information + } + + private void tickPassenger(Entity vehicle, Entity passenger, boolean isActive) { // Paper - EAR 2 +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 717500b8b88f123d6b2d3545d33d5c78a1ef7cc1..d7f2950223533c3cc2d182612d4c485edf3fba2b 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -1136,8 +1136,43 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return this.onGround; + } + ++ // Paper start - detailed watchdog information ++ public final Object posLock = new Object(); // Paper - 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; ++ } ++ // Paper end - detailed watchdog information ++ + public void move(MoverType type, Vec3 movement) { + final Vec3 originalMovement = movement; // Paper - Expose pre-collision velocity ++ // Paper start - detailed watchdog information ++ ca.spottedleaf.moonrise.common.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 { ++ // Paper end - detailed watchdog information + if (this.noPhysics) { + this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z); + } else { +@@ -1261,6 +1296,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + gameprofilerfiller.pop(); + } + } ++ // Paper start - detailed watchdog information ++ } finally { ++ synchronized (this.posLock) { // Paper ++ this.moveVector = null; ++ } // Paper ++ } ++ // Paper end - detailed watchdog information + } + + private void applyMovementEmissionAndPlaySound(Entity.MovementEmission moveEffect, Vec3 movement, BlockPos landingPos, BlockState landingState) { +@@ -4902,7 +4944,9 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + public void setDeltaMovement(Vec3 velocity) { ++ synchronized (this.posLock) { // Paper + this.deltaMovement = velocity; ++ } // Paper + } + + public void addDeltaMovement(Vec3 velocity) { +@@ -5008,7 +5052,9 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + // Paper end - Fix MC-4 + if (this.position.x != x || this.position.y != y || this.position.z != z) { ++ synchronized (this.posLock) { // Paper + this.position = new Vec3(x, y, z); ++ } // Paper + int i = Mth.floor(x); + int j = Mth.floor(y); + int k = Mth.floor(z); +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index c9e17225bc52fe5e7b2dc0908db225a86c6e94d1..f7a4fee9bb25ff256dc2e5ea26bfbceca6a49167 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -22,6 +22,78 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre + private volatile long lastTick; + private volatile boolean stopping; + ++ // Paper 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 = net.minecraft.world.entity.EntityType.getKey(entity.getType()).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); ++ } ++ } ++ } ++ // Paper end - log detailed tick information ++ + private WatchdogThread(long timeoutTime, boolean restart) + { + super( "Paper Watchdog Thread" ); +@@ -119,6 +191,7 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre + log.log( Level.SEVERE, "------------------------------" ); + log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(MinecraftServer.getServer(), isLongTimeout); // Paper - rewrite chunk system ++ this.dumpTickingInfo(); // Paper - log detailed tick information + WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log ); + log.log( Level.SEVERE, "------------------------------" ); + // diff --git a/patches/server/1042-Entity-load-save-limit-per-chunk.patch b/patches/server/1042-Entity-load-save-limit-per-chunk.patch new file mode 100644 index 0000000000..e69171236b --- /dev/null +++ b/patches/server/1042-Entity-load-save-limit-per-chunk.patch @@ -0,0 +1,81 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> +Date: Wed, 18 Nov 2020 20:52:25 -0800 +Subject: [PATCH] Entity load/save limit per chunk + +Adds a config option to limit the number of entities saved and loaded +to a chunk. The default values of -1 disable the limit. Although +defaults are only included for certain entites, this allows setting +limits for any entity type. + +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java +index 7aea4e343581b977d11af90f9f65eac3532eade1..d21ce54ebb5724c04eadf56a2cde701d5eeb5db2 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java +@@ -104,7 +104,18 @@ public final class ChunkEntitySlices { + } + + final ListTag entitiesTag = new ListTag(); ++ final java.util.Map, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk + for (final Entity entity : PlatformHooks.get().modifySavedEntities(world, chunkPos.x, chunkPos.z, entities)) { ++ // Paper start - Entity load/save limit per chunk ++ final EntityType entityType = entity.getType(); ++ final int saveLimit = world.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); ++ if (saveLimit > -1) { ++ if (savedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { ++ continue; ++ } ++ savedEntityCounts.merge(entityType, 1, Integer::sum); ++ } ++ // Paper end - Entity load/save limit per chunk + CompoundTag compoundTag = new CompoundTag(); + if (entity.save(compoundTag)) { + entitiesTag.add(compoundTag); +diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java +index 0ec3e1837e36d17e9ff33e7d50c66353aa7539db..d23914a3ab3723d532ae867db6b954c843030f75 100644 +--- a/src/main/java/net/minecraft/world/entity/EntityType.java ++++ b/src/main/java/net/minecraft/world/entity/EntityType.java +@@ -716,9 +716,20 @@ public class EntityType implements FeatureElement, EntityTypeT + final Spliterator spliterator = entityNbtList.spliterator(); + + return StreamSupport.stream(new Spliterator() { ++ final java.util.Map, Integer> loadedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk + public boolean tryAdvance(Consumer consumer) { + return spliterator.tryAdvance((nbtbase) -> { + EntityType.loadEntityRecursive((CompoundTag) nbtbase, world, reason, (entity) -> { ++ // Paper start - Entity load/save limit per chunk ++ final EntityType entityType = entity.getType(); ++ final int saveLimit = world.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); ++ if (saveLimit > -1) { ++ if (this.loadedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { ++ return null; ++ } ++ this.loadedEntityCounts.merge(entityType, 1, Integer::sum); ++ } ++ // Paper end - Entity load/save limit per chunk + consumer.accept(entity); + return entity; + }); +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java +index 16ca1c8672e5f0a27f8a30498c754a81cdec5191..356d010506fd21f3c752e4aa86c46c1106fdde3b 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java +@@ -93,7 +93,18 @@ public class EntityStorage implements EntityPersistentStorage { + } + } else { + ListTag listTag = new ListTag(); ++ final java.util.Map, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk + dataList.getEntities().forEach(entity -> { ++ // Paper start - Entity load/save limit per chunk ++ final EntityType entityType = entity.getType(); ++ final int saveLimit = this.level.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); ++ if (saveLimit > -1) { ++ if (savedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { ++ return; ++ } ++ savedEntityCounts.merge(entityType, 1, Integer::sum); ++ } ++ // Paper end - Entity load/save limit per chunk + CompoundTag compoundTagx = new CompoundTag(); + if (entity.save(compoundTagx)) { + listTag.add(compoundTagx); diff --git a/patches/server/1043-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch b/patches/server/1043-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch new file mode 100644 index 0000000000..16f1024afb --- /dev/null +++ b/patches/server/1043-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch @@ -0,0 +1,744 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sun, 2 Feb 2020 02:25:10 -0800 +Subject: [PATCH] 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. + +Feature patch + +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java +index a23dc2f8f4475de1ee35bf18a7a8a53233ccac12..226af44fd469053451a0403a95ffb446face9530 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java +@@ -9,6 +9,27 @@ import java.util.BitSet; + public class RegionBitmap { + private final BitSet used = new BitSet(); + ++ // Paper start - Attempt to recalculate regionfile header if it is corrupt ++ public final void copyFrom(RegionBitmap other) { ++ BitSet thisBitset = this.used; ++ BitSet otherBitset = other.used; ++ ++ for (int i = 0; i < Math.max(thisBitset.size(), otherBitset.size()); ++i) { ++ thisBitset.set(i, otherBitset.get(i)); ++ } ++ } ++ ++ public final boolean tryAllocate(int from, int length) { ++ BitSet bitset = this.used; ++ int firstSet = bitset.nextSetBit(from); ++ if (firstSet > 0 && firstSet < (from + length)) { ++ return false; ++ } ++ bitset.set(from, from + length); ++ return true; ++ } ++ // Paper end - Attempt to recalculate regionfile header if it is corrupt ++ + public void force(int start, int size) { + this.used.set(start, start + size); + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +index ff092c6d0cd436f14a9a4ff5c8ddbb5538d1a8c5..16f07007a0f73ec0c6f421c9b082518e87e8cc7b 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 +@@ -51,6 +51,354 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + private final IntBuffer timestamps; + @VisibleForTesting + protected final RegionBitmap usedSectors; ++ // Paper start - Attempt to recalculate regionfile header if it is corrupt ++ 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(new DataInputStream(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() { ++ Path backup = this.path.getParent().resolve(this.path.getFileName() + "." + new java.util.Random().nextLong() + ".backup"); ++ this.backupRegionFile(backup); ++ } ++ ++ private void backupRegionFile(Path to) { ++ try { ++ this.file.force(true); ++ LOGGER.warn("Backing up regionfile \"" + this.path.toAbsolutePath() + "\" to " + to.toAbsolutePath()); ++ java.nio.file.Files.copy(this.path, to, java.nio.file.StandardCopyOption.COPY_ATTRIBUTES); ++ LOGGER.warn("Backed up the regionfile to " + to.toAbsolutePath()); ++ } catch (IOException ex) { ++ LOGGER.error("Failed to backup to " + to.toAbsolutePath(), ex); ++ } ++ } ++ ++ private static boolean inSameRegionfile(ChunkPos first, ChunkPos second) { ++ return (first.x & ~31) == (second.x & ~31) && (first.z & ~31) == (second.z & ~31); ++ } ++ ++ // note: only call for CHUNK regionfiles ++ boolean recalculateHeader() throws IOException { ++ if (!this.canRecalcHeader) { ++ return false; ++ } ++ ChunkPos ourLowerLeftPosition = RegionFileStorage.getRegionFileCoordinates(this.path); ++ if (ourLowerLeftPosition == null) { ++ LOGGER.error("Unable to get chunk location of regionfile " + this.path.toAbsolutePath() + ", cannot recover header"); ++ return false; ++ } ++ synchronized (this) { ++ LOGGER.warn("Corrupt regionfile header detected! Attempting to re-calculate header offsets for regionfile " + this.path.toAbsolutePath(), 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 = SerializableChunkData.getChunkCoordinate(compound); ++ if (!inSameRegionfile(ourLowerLeftPosition, chunkPos)) { ++ LOGGER.error("Ignoring absolute chunk " + chunkPos + " in regionfile as it is not contained in the bounds of the regionfile '" + this.path.toAbsolutePath() + "'. It should be in regionfile (" + (chunkPos.x >> 5) + "," + (chunkPos.z >> 5) + ")"); ++ continue; ++ } ++ int location = (chunkPos.x & 31) | ((chunkPos.z & 31) << 5); ++ ++ CompoundTag otherCompound = compounds[location]; ++ ++ if (otherCompound != null && SerializableChunkData.getLastWorldSaveTime(otherCompound) > SerializableChunkData.getLastWorldSaveTime(compound)) { ++ continue; // don't overwrite newer data. ++ } ++ ++ // aikar oversized? ++ Path aikarOversizedFile = this.getOversizedFile(chunkPos.x, chunkPos.z); ++ boolean isAikarOversized = false; ++ if (Files.exists(aikarOversizedFile)) { ++ try { ++ CompoundTag aikarOversizedCompound = this.getOversizedData(chunkPos.x, chunkPos.z); ++ if (SerializableChunkData.getLastWorldSaveTime(compound) == SerializableChunkData.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.path.toAbsolutePath() + ", 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; ++ Path[] regionFiles = Files.list(containingFolder).toArray(Path[]::new); ++ boolean[] oversized = new boolean[32 * 32]; ++ RegionFileVersion[] oversizedCompressionTypes = new RegionFileVersion[32 * 32]; ++ ++ if (regionFiles != null) { ++ 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 (Path 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); ++ } catch (Exception ex) { ++ LOGGER.error("Failed to read oversized chunk data in file " + regionFile.toAbsolutePath() + ", 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(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.toAbsolutePath() + ", it's corrupt. Its data will be lost"); ++ continue; ++ } ++ ++ if (!SerializableChunkData.getChunkCoordinate(compound).equals(oversizedCoords)) { ++ LOGGER.error("Can't use oversized chunk stored in " + regionFile.toAbsolutePath() + ", got absolute chunkpos: " + SerializableChunkData.getChunkCoordinate(compound) + ", expected " + oversizedCoords); ++ continue; ++ } ++ ++ if (compounds[location] == null || SerializableChunkData.getLastWorldSaveTime(compound) > SerializableChunkData.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.path.toAbsolutePath() + ", 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.path.toAbsolutePath() + " 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.path.toAbsolutePath(), ex); ++ Files.deleteIfExists(this.getOversizedMetaFile()); ++ } ++ } else { ++ Files.deleteIfExists(this.getOversizedMetaFile()); ++ } ++ ++ 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.path.toAbsolutePath()); ++ ++ 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.path.toAbsolutePath()); ++ } else if (newOffset == 0) { ++ LOGGER.warn("Data for local chunk (" + chunkX + "," + chunkZ + ") could not be recovered in regionfile " + this.path.toAbsolutePath() + ", it will be regenerated"); ++ } else { ++ LOGGER.info("Local chunk (" + chunkX + "," + chunkZ + ") changed to point to newer data or correct chunk in regionfile " + this.path.toAbsolutePath()); ++ } ++ } ++ } ++ ++ LOGGER.info("End of change summary for regionfile " + this.path.toAbsolutePath()); ++ ++ // simply destroy the timestamp header, it's not used ++ ++ for (int i = 0; i < 32 * 32; ++i) { ++ this.timestamps.put(i, calculatedOffsets[i] != 0 ? RegionFile.getTimestamp() : 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.path.toAbsolutePath()); ++ } catch (IOException ex) { ++ LOGGER.error("Failed to write new header to disk for regionfile " + this.path.toAbsolutePath(), ex); ++ } ++ } ++ ++ return true; ++ } ++ ++ final boolean canRecalcHeader; // final forces compile fail on new constructor ++ // Paper end - Attempt to recalculate regionfile header if it is corrupt + + // Paper start - rewrite chunk system + @Override +@@ -82,6 +430,7 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + throw new IllegalArgumentException("Expected directory, got " + String.valueOf(directory.toAbsolutePath())); + } else { + this.externalFileDir = directory; ++ this.canRecalcHeader = RegionFileStorage.isChunkDataFolder(this.externalFileDir); // Paper - add can recalc flag + this.offsets = this.header.asIntBuffer(); + ((java.nio.Buffer) this.offsets).limit(1024); // CraftBukkit - decompile error + ((java.nio.Buffer) this.header).position(4096); // CraftBukkit - decompile error +@@ -101,14 +450,16 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + RegionFile.LOGGER.warn("Region file {} has truncated header: {}", path, i); + } + +- long j = Files.size(path); ++ final long j = Files.size(path); final long regionFileSize = j; // Paper - recalculate header on header corruption + +- for (int k = 0; k < 1024; ++k) { +- int l = this.offsets.get(k); ++ boolean needsHeaderRecalc = false; // Paper - recalculate header on header corruption ++ boolean hasBackedUp = false; // Paper - recalculate header on header corruption ++ for (int k = 0; k < 1024; ++k) { final int headerLocation = k; // Paper - we expect this to be the header location ++ final int l = this.offsets.get(k); + + if (l != 0) { +- int i1 = RegionFile.getSectorNumber(l); +- int j1 = RegionFile.getNumSectors(l); ++ final int i1 = RegionFile.getSectorNumber(l); final int offset = i1; // Paper - we expect this to be offset in file in sectors ++ int j1 = RegionFile.getNumSectors(l); final int sectorLength; // Paper - 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 +@@ -117,21 +468,66 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + j1 = (realLen.getInt(0) + 4) / 4096 + 1; + } + // Spigot end ++ sectorLength = j1; // Paper - 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", new Object[]{path, k, i1}); +- this.offsets.put(k, 0); ++ //this.offsets.put(k, 0); // Paper - 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", path, k); +- this.offsets.put(k, 0); ++ //this.offsets.put(k, 0); // Paper - 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", new Object[]{path, k, i1}); +- this.offsets.put(k, 0); ++ //this.offsets.put(k, 0); // Paper - we catch this, but need it in the header for the summary change + } else { +- this.usedSectors.force(i1, j1); ++ //this.usedSectors.force(i1, j1); // Paper - move this down so we can check if it fails to allocate ++ } ++ // Paper 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.path.toAbsolutePath() + "! Recalculating header..."); ++ needsHeaderRecalc = true; ++ break; ++ } else { ++ // location = chunkX | (chunkZ << 5); ++ LOGGER.error("Detected invalid header for regionfile " + this.path.toAbsolutePath() + ++ "! 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.path.toAbsolutePath()); + } ++ if (failedToAllocate & !canRecalcHeader) { ++ // location = chunkX | (chunkZ << 5); ++ LOGGER.error("Detected invalid header for regionfile " + this.path.toAbsolutePath() + ++ "! 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; ++ // Paper end - recalculate header on header corruption + } + } ++ // Paper 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.path.toAbsolutePath() + ", header gave erroneous offsets & locations"); ++ this.recalculateHeader(); ++ } ++ // Paper end + } + + } +@@ -142,11 +538,36 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + } + + private Path getExternalChunkPath(ChunkPos chunkPos) { +- String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; ++ String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; // Paper - diff on change + + return this.externalFileDir.resolve(s); + } + ++ // Paper start ++ private static ChunkPos getOversizedChunkPair(Path file) { ++ String fileName = file.getFileName().toString(); ++ ++ 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; ++ } ++ } ++ // Paper end ++ + @Nullable + public synchronized DataInputStream getChunkDataInputStream(ChunkPos pos) throws IOException { + int i = this.getOffset(pos); +@@ -170,6 +591,11 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error + if (bytebuffer.remaining() < 5) { + RegionFile.LOGGER.error("Chunk {} header is truncated: expected {} but read {}", new Object[]{pos, l, bytebuffer.remaining()}); ++ // Paper start - recalculate header on regionfile corruption ++ if (this.canRecalcHeader && this.recalculateHeader()) { ++ return this.getChunkDataInputStream(pos); ++ } ++ // Paper end - recalculate header on regionfile corruption + return null; + } else { + int i1 = bytebuffer.getInt(); +@@ -177,6 +603,11 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + + if (i1 == 0) { + RegionFile.LOGGER.warn("Chunk {} is allocated, but stream is missing", pos); ++ // Paper start - recalculate header on regionfile corruption ++ if (this.canRecalcHeader && this.recalculateHeader()) { ++ return this.getChunkDataInputStream(pos); ++ } ++ // Paper end - recalculate header on regionfile corruption + return null; + } else { + int j1 = i1 - 1; +@@ -184,18 +615,45 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + if (RegionFile.isExternalStreamChunk(b0)) { + if (j1 != 0) { + RegionFile.LOGGER.warn("Chunk has both internal and external streams"); ++ // Paper start - recalculate header on regionfile corruption ++ if (this.canRecalcHeader && this.recalculateHeader()) { ++ return this.getChunkDataInputStream(pos); ++ } ++ // Paper end - recalculate header on regionfile corruption + } + +- return this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0)); ++ // Paper 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; ++ // Paper end - recalculate header on regionfile corruption + } else if (j1 > bytebuffer.remaining()) { + RegionFile.LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", new Object[]{pos, j1, bytebuffer.remaining()}); ++ // Paper start - recalculate header on regionfile corruption ++ if (this.canRecalcHeader && this.recalculateHeader()) { ++ return this.getChunkDataInputStream(pos); ++ } ++ // Paper end - recalculate header on regionfile corruption + return null; + } else if (j1 < 0) { + RegionFile.LOGGER.error("Declared size {} of chunk {} is negative", i1, pos); ++ // Paper start - recalculate header on regionfile corruption ++ if (this.canRecalcHeader && this.recalculateHeader()) { ++ return this.getChunkDataInputStream(pos); ++ } ++ // Paper end - recalculate header on regionfile corruption + return null; + } else { + JvmProfiler.INSTANCE.onRegionFileRead(this.info, pos, this.version, j1); +- return this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1)); ++ // Paper 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; ++ // Paper end - recalculate header on regionfile corruption + } + } + } +@@ -391,10 +849,15 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + } + + private ByteBuffer createExternalStub() { ++ // Paper start - add version param ++ return this.createExternalStub(this.version); ++ } ++ private ByteBuffer createExternalStub(RegionFileVersion version) { ++ // Paper 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)); // Paper - 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 8d66d6b7aeb9feb54ebd83f5c73b45d42b9a7034..e40665cead218502b44dd49051a53326ed94f061 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 +@@ -211,11 +211,42 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + } + } + // Paper end - rewrite chunk system ++ // Paper start - recalculate region file headers ++ private final boolean isChunkData; ++ ++ public static boolean isChunkDataFolder(Path path) { ++ return path.toFile().getName().equalsIgnoreCase("region"); ++ } ++ ++ @Nullable ++ public static ChunkPos getRegionFileCoordinates(Path file) { ++ String fileName = file.getFileName().toString(); ++ if (!fileName.startsWith("r.") || !fileName.endsWith(".mca")) { ++ 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 << 5, z << 5); ++ } catch (NumberFormatException ex) { ++ return null; ++ } ++ } ++ // Paper end + + protected RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { // Paper - protected + this.folder = directory; + this.sync = dsync; + this.info = storageKey; ++ this.isChunkData = isChunkDataFolder(this.folder); // Paper - recalculate region file headers + } + + // Paper start - rewrite chunk system +@@ -315,6 +346,19 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + try { + if (datainputstream != null) { + nbttagcompound = NbtIo.read((DataInput) datainputstream); ++ // Paper start - recover from corrupt regionfile header ++ if (this.isChunkData) { ++ ChunkPos chunkPos = SerializableChunkData.getChunkCoordinate(nbttagcompound); ++ if (!chunkPos.equals(pos)) { ++ net.minecraft.server.MinecraftServer.LOGGER.error("Attempting to read chunk data at " + pos + " but got chunk data for " + chunkPos + " instead! Attempting regionfile recalculation for regionfile " + regionfile.getPath().toAbsolutePath()); ++ if (regionfile.recalculateHeader()) { ++ return this.read(pos); ++ } ++ net.minecraft.server.MinecraftServer.LOGGER.error("Can't recalculate regionfile header, regenerating chunk " + pos + " for " + regionfile.getPath().toAbsolutePath()); ++ return null; ++ } ++ } ++ // Paper end - recover from corrupt regionfile header + break label43; + } + +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java +index ef68b57ef1d8d7cb317c417569dd23a777fba4ad..f4a39f49b354c560d614483db1cd3dfc154e94b4 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java +@@ -21,7 +21,7 @@ import org.slf4j.Logger; + + public class RegionFileVersion { + private static final Logger LOGGER = LogUtils.getLogger(); +- private static final Int2ObjectMap VERSIONS = new Int2ObjectOpenHashMap<>(); ++ public static final Int2ObjectMap VERSIONS = new Int2ObjectOpenHashMap<>(); // Paper - private -> public + private static final Object2ObjectMap VERSIONS_BY_NAME = new Object2ObjectOpenHashMap<>(); + public static final RegionFileVersion VERSION_GZIP = register( + new RegionFileVersion( +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +index 0296f52fb2c871adbf2ce73a64d8f77fab826cd7..018b24d7611c3fd11536441431abf8f125850129 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +@@ -103,6 +103,18 @@ public record SerializableChunkData(Registry biomeRegistry, ChunkPos chun + } + } + // Paper end - guard against serializing mismatching coordinates ++ // Paper start - Attempt to recalculate regionfile header if it is corrupt ++ // TODO: Check on update ++ public static long getLastWorldSaveTime(final CompoundTag chunkData) { ++ final int dataVersion = ChunkStorage.getVersion(chunkData); ++ if (dataVersion < 2842) { // Level tag is removed after this version ++ final CompoundTag levelData = chunkData.getCompound("Level"); ++ return levelData.getLong("LastUpdate"); ++ } else { ++ return chunkData.getLong("LastUpdate"); ++ } ++ } ++ // Paper end - Attempt to recalculate regionfile header if it is corrupt + + // Paper start - Do not let the server load chunks from newer versions + private static final int CURRENT_DATA_VERSION = net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion(); +@@ -575,7 +587,7 @@ public record SerializableChunkData(Registry biomeRegistry, ChunkPos chun + nbttagcompound.putInt("xPos", this.chunkPos.x); + nbttagcompound.putInt("yPos", this.minSectionY); + nbttagcompound.putInt("zPos", this.chunkPos.z); +- nbttagcompound.putLong("LastUpdate", this.lastUpdateTime); ++ nbttagcompound.putLong("LastUpdate", this.lastUpdateTime); // Paper - Diff on change + nbttagcompound.putLong("InhabitedTime", this.inhabitedTime); + nbttagcompound.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(this.chunkStatus).toString()); + DataResult dataresult; // CraftBukkit - decompile error diff --git a/patches/server/1044-Bundle-spark.patch b/patches/server/1044-Bundle-spark.patch new file mode 100644 index 0000000000..a2da3d9ff8 --- /dev/null +++ b/patches/server/1044-Bundle-spark.patch @@ -0,0 +1,401 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Riley Park +Date: Tue, 16 Jul 2024 14:55:23 -0700 +Subject: [PATCH] Bundle spark + + +diff --git a/build.gradle.kts b/build.gradle.kts +index 2ceee9c2f7a237dac1c2e5c3fcc50a869d93d1ac..092a9ee1e862bde04c2025de6f7e25b6ec13760a 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -76,6 +76,10 @@ dependencies { + implementation("io.papermc:reflection-rewriter-runtime:$reflectionRewriterVersion") + implementation("io.papermc:reflection-rewriter-proxy-generator:$reflectionRewriterVersion") + // Paper end - Remap reflection ++ // Paper start - spark ++ implementation("me.lucko:spark-api:0.1-20240720.200737-2") ++ implementation("me.lucko:spark-paper:1.10.119-SNAPSHOT") ++ // Paper end - spark + } + + paperweight { +diff --git a/src/main/java/io/papermc/paper/SparksFly.java b/src/main/java/io/papermc/paper/SparksFly.java +new file mode 100644 +index 0000000000000000000000000000000000000000..62e2d5704c348955bc8284dc2d54c933b7bcdd06 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/SparksFly.java +@@ -0,0 +1,211 @@ ++package io.papermc.paper; ++ ++import io.papermc.paper.configuration.GlobalConfiguration; ++import io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage; ++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; ++import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage; ++import io.papermc.paper.util.MCUtil; ++import java.util.Collection; ++import java.util.List; ++import java.util.concurrent.ConcurrentLinkedQueue; ++import java.util.logging.Level; ++import java.util.logging.Logger; ++import me.lucko.spark.paper.api.Compatibility; ++import me.lucko.spark.paper.api.PaperClassLookup; ++import me.lucko.spark.paper.api.PaperScheduler; ++import me.lucko.spark.paper.api.PaperSparkModule; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.format.TextColor; ++import net.minecraft.util.ExceptionCollector; ++import org.bukkit.Server; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.craftbukkit.CraftServer; ++ ++// It's like electricity. ++public final class SparksFly { ++ public static final String ID = "spark"; ++ public static final String COMMAND_NAME = "spark"; ++ ++ private static final String PREFER_SPARK_PLUGIN_PROPERTY = "paper.preferSparkPlugin"; ++ ++ private static final int SPARK_YELLOW = 0xffc93a; ++ ++ private final Logger logger; ++ private final PaperSparkModule spark; ++ private final ConcurrentLinkedQueue mainThreadTaskQueue; ++ ++ private boolean enabled; ++ private boolean disabledInConfigurationWarningLogged; ++ ++ public SparksFly(final Server server) { ++ this.mainThreadTaskQueue = new ConcurrentLinkedQueue<>(); ++ this.logger = Logger.getLogger(ID); ++ this.logger.log(Level.INFO, "This server bundles the spark profiler. For more information please visit https://docs.papermc.io/paper/profiling"); ++ this.spark = PaperSparkModule.create(Compatibility.VERSION_1_0, server, this.logger, new PaperScheduler() { ++ @Override ++ public void executeAsync(final Runnable runnable) { ++ MCUtil.scheduleAsyncTask(this.catching(runnable, "asynchronous")); ++ } ++ ++ @Override ++ public void executeSync(final Runnable runnable) { ++ SparksFly.this.mainThreadTaskQueue.offer(this.catching(runnable, "synchronous")); ++ } ++ ++ private Runnable catching(final Runnable runnable, final String type) { ++ return () -> { ++ try { ++ runnable.run(); ++ } catch (final Throwable t) { ++ SparksFly.this.logger.log(Level.SEVERE, "An exception was encountered while executing a " + type + " spark task", t); ++ } ++ }; ++ } ++ }, new PaperClassLookup() { ++ @Override ++ public Class lookup(final String className) throws Exception { ++ final ExceptionCollector exceptions = new ExceptionCollector<>(); ++ try { ++ return Class.forName(className); ++ } catch (final ClassNotFoundException e) { ++ exceptions.add(e); ++ for (final ConfiguredPluginClassLoader loader : ((PaperPluginClassLoaderStorage) PaperClassLoaderStorage.instance()).getGlobalGroup().getClassLoaders()) { ++ try { ++ final Class loadedClass = loader.loadClass(className, true, false, true); ++ if (loadedClass != null) { ++ return loadedClass; ++ } ++ } catch (final ClassNotFoundException exception) { ++ exceptions.add(exception); ++ } ++ } ++ exceptions.throwIfPresent(); ++ return null; ++ } ++ } ++ }); ++ } ++ ++ public void executeMainThreadTasks() { ++ Runnable task; ++ while ((task = this.mainThreadTaskQueue.poll()) != null) { ++ task.run(); ++ } ++ } ++ ++ public void enableEarlyIfRequested() { ++ if (!isPluginPreferred() && shouldEnableImmediately()) { ++ this.enable(); ++ } ++ } ++ ++ public void enableBeforePlugins() { ++ if (!isPluginPreferred()) { ++ this.enable(); ++ } ++ } ++ ++ public void enableAfterPlugins(final Server server) { ++ final boolean isPluginPreferred = isPluginPreferred(); ++ final boolean isPluginEnabled = isPluginEnabled(server); ++ if (!isPluginPreferred || !isPluginEnabled) { ++ if (isPluginPreferred && !this.enabled) { ++ this.logger.log(Level.INFO, "The spark plugin has been preferred but was not loaded. The bundled spark profiler will enabled instead."); ++ } ++ this.enable(); ++ } ++ } ++ ++ private void enable() { ++ if (!this.enabled) { ++ if (GlobalConfiguration.get().spark.enabled) { ++ this.enabled = true; ++ this.spark.enable(); ++ } else { ++ if (!this.disabledInConfigurationWarningLogged) { ++ this.logger.log(Level.INFO, "The spark profiler will not be enabled because it is currently disabled in the configuration."); ++ this.disabledInConfigurationWarningLogged = true; ++ } ++ } ++ } ++ } ++ ++ public void disable() { ++ if (this.enabled) { ++ this.spark.disable(); ++ this.enabled = false; ++ } ++ } ++ ++ public void registerCommandBeforePlugins(final Server server) { ++ if (!isPluginPreferred()) { ++ this.registerCommand(server); ++ } ++ } ++ ++ public void registerCommandAfterPlugins(final Server server) { ++ if ((!isPluginPreferred() || !isPluginEnabled(server)) && server.getCommandMap().getCommand(COMMAND_NAME) == null) { ++ this.registerCommand(server); ++ } ++ } ++ ++ private void registerCommand(final Server server) { ++ server.getCommandMap().register(COMMAND_NAME, "paper", new CommandImpl(COMMAND_NAME, this.spark.getPermissions())); ++ } ++ ++ public void tickStart() { ++ this.spark.onServerTickStart(); ++ } ++ ++ public void tickEnd(final double duration) { ++ this.spark.onServerTickEnd(duration); ++ } ++ ++ void executeCommand(final CommandSender sender, final String[] args) { ++ this.spark.executeCommand(sender, args); ++ } ++ ++ List tabComplete(final CommandSender sender, final String[] args) { ++ return this.spark.tabComplete(sender, args); ++ } ++ ++ public static boolean isPluginPreferred() { ++ return Boolean.getBoolean(PREFER_SPARK_PLUGIN_PROPERTY); ++ } ++ ++ private static boolean isPluginEnabled(final Server server) { ++ return server.getPluginManager().isPluginEnabled(ID); ++ } ++ ++ private static boolean shouldEnableImmediately() { ++ return GlobalConfiguration.get().spark.enableImmediately; ++ } ++ ++ public static final class CommandImpl extends Command { ++ CommandImpl(final String name, final Collection permissions) { ++ super(name); ++ this.setPermission(String.join(";", permissions)); ++ } ++ ++ @Override ++ public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) { ++ final SparksFly spark = ((CraftServer) sender.getServer()).spark; ++ if (spark.enabled) { ++ spark.executeCommand(sender, args); ++ } else { ++ sender.sendMessage(Component.text("The spark profiler is currently disabled.", TextColor.color(SPARK_YELLOW))); ++ } ++ return true; ++ } ++ ++ @Override ++ public List tabComplete(final CommandSender sender, final String alias, final String[] args) throws IllegalArgumentException { ++ final SparksFly spark = ((CraftServer) sender.getServer()).spark; ++ if (spark.enabled) { ++ return spark.tabComplete(sender, args); ++ } ++ return List.of(); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java +index 6b8ed8a0baaf4a57d20e57cec3400af5561ddd79..48604e7f96adc9e226e034054c5e2bad0b024eb5 100644 +--- a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java ++++ b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java +@@ -1,6 +1,9 @@ + package io.papermc.paper.plugin.provider.source; + ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.SparksFly; + import io.papermc.paper.plugin.PluginInitializerManager; ++import io.papermc.paper.plugin.configuration.PluginMeta; + import io.papermc.paper.plugin.entrypoint.EntrypointHandler; + import io.papermc.paper.plugin.provider.type.PluginFileType; + import org.bukkit.plugin.InvalidPluginException; +@@ -17,12 +20,14 @@ import java.nio.file.attribute.BasicFileAttributes; + import java.util.Set; + import java.util.function.Function; + import java.util.jar.JarFile; ++import org.slf4j.Logger; + + /** + * Loads a plugin provider at the given plugin jar file path. + */ + public class FileProviderSource implements ProviderSource { + ++ private static final Logger LOGGER = LogUtils.getClassLogger(); + private final Function contextChecker; + private final boolean applyRemap; + +@@ -82,6 +87,12 @@ public class FileProviderSource implements ProviderSource { + ); + } + ++ final PluginMeta config = type.getConfig(file); ++ if ((config.getName().equals("spark") && config.getMainClass().equals("me.lucko.spark.bukkit.BukkitSparkPlugin")) && !SparksFly.isPluginPreferred()) { ++ LOGGER.info("The spark plugin will not be loaded as this server bundles the spark profiler."); ++ return; ++ } ++ + type.register(entrypointHandler, file, context); + } + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 8549292b4e96c7b09e2a9707f2d8a75b870ee35b..aa0a693af442a791ad8e5ec5a9e11594e6b42419 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -764,6 +764,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= j) { ++ this.server.spark.tickStart(); // Paper - spark + if (this.emptyTicks == j) { + MinecraftServer.LOGGER.info("Server empty for {} seconds, pausing", this.pauseWhileEmptySeconds()); + this.autoSave(); + } + + this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit ++ this.server.spark.executeMainThreadTasks(); // Paper - spark + this.tickConnection(); ++ this.server.spark.tickEnd(((double)(System.nanoTime() - lastTick) / 1000000D)); // Paper - spark + return; + } + } + ++ this.server.spark.tickStart(); // Paper - spark + new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper - Server Tick Events + ++this.tickCount; + this.tickRateManager.tick(); +@@ -1654,11 +1662,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Sun, 13 Aug 2023 15:41:52 -0700 +Subject: [PATCH] Improve performance of mass crafts + +When the server crafts all available items in CraftingMenu or InventoryMenu the game +checks either 4 or 9 times for each individual craft for a matching recipe for that container. +This check can be expensive if 64 total crafts are being performed with the recipe matching logic +being run 64 * 9 + 64 times. A breakdown of those times is below. This patch caches the last matching +recipe so that it is checked first and only if it doesn't match does the rest of the matching logic run. + +Shift-click crafts are processed one at a time, so shift clicking on an item in the result of a iron block craft +where all the 9 inputs are full stacks of iron will run 64 iron block crafts. For each of those crafts, the +'remaining' blocks are calculated. This is due to recipes that have leftover items like buckets. This is done +for each craft, and done once to get the full 9 leftover items which are usually air. Then 1 item is removed +from each of the 9 inputs and each time that happens, logic is triggered to update the result itemstack. So +for each craft, that logic is run 9 times (hence the 64 * 9). The + 64 is from the 64 checks for remaining items. + +After this patch, the full iteration over all recipes checking for a match should run once for a full craft to find the +initial recipe match. Then that recipe will be checked first for all future recipe match checks. + +Feature patch + +diff --git a/src/main/java/net/minecraft/world/inventory/CraftingContainer.java b/src/main/java/net/minecraft/world/inventory/CraftingContainer.java +index 779d107a4d07820529273af5931421c09d1dc27f..4f6c8c43f5150e340704682accfbe2a5b1c5db19 100644 +--- a/src/main/java/net/minecraft/world/inventory/CraftingContainer.java ++++ b/src/main/java/net/minecraft/world/inventory/CraftingContainer.java +@@ -18,11 +18,11 @@ public interface CraftingContainer extends Container, StackedContentsCompatible + List getItems(); + + // CraftBukkit start +- default RecipeHolder getCurrentRecipe() { ++ default RecipeHolder getCurrentRecipe() { // Paper - use correct generic + return null; + } + +- default void setCurrentRecipe(RecipeHolder recipe) { ++ default void setCurrentRecipe(RecipeHolder recipe) { // Paper - use correct generic + } + // CraftBukkit end + +diff --git a/src/main/java/net/minecraft/world/inventory/CraftingMenu.java b/src/main/java/net/minecraft/world/inventory/CraftingMenu.java +index 6b3006a8543265664a2e54898ece92c66afb9c21..2e4043248c3ac7a54d894d76b99adc26518d3866 100644 +--- a/src/main/java/net/minecraft/world/inventory/CraftingMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/CraftingMenu.java +@@ -56,6 +56,7 @@ public class CraftingMenu extends AbstractCraftingMenu { + CraftingInput craftinginput = craftingInventory.asCraftInput(); + ServerPlayer entityplayer = (ServerPlayer) player; + ItemStack itemstack = ItemStack.EMPTY; ++ if (recipe == null) recipe = craftingInventory.getCurrentRecipe(); // Paper - Perf: Improve mass crafting; check last recipe used first + Optional> optional = world.getServer().getRecipeManager().getRecipeFor(RecipeType.CRAFTING, craftinginput, world, recipe); + craftingInventory.setCurrentRecipe(optional.orElse(null)); // CraftBukkit + +diff --git a/src/main/java/net/minecraft/world/inventory/ResultSlot.java b/src/main/java/net/minecraft/world/inventory/ResultSlot.java +index 1ea4f0800598a75ba74ce033378749d1abe4009b..ff30071f3ef37d1b28cf86e26ce4f7477335a07a 100644 +--- a/src/main/java/net/minecraft/world/inventory/ResultSlot.java ++++ b/src/main/java/net/minecraft/world/inventory/ResultSlot.java +@@ -72,7 +72,7 @@ public class ResultSlot extends Slot { + private NonNullList getRemainingItems(CraftingInput input, Level world) { + return world instanceof ServerLevel serverLevel + ? serverLevel.recipeAccess() +- .getRecipeFor(RecipeType.CRAFTING, input, serverLevel) ++ .getRecipeFor(RecipeType.CRAFTING, input, serverLevel, this.craftSlots.getCurrentRecipe()) // Paper - Perf: Improve mass crafting; check last recipe used first + .map(recipe -> recipe.value().getRemainingItems(input)) + .orElseGet(() -> copyAllInputItems(input)) + : CraftingRecipe.defaultCraftingReminder(input); +diff --git a/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java b/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java +index 32d49d759f95d27ca04b843bbb2c2fd22ebd7e4a..a458f7b8270dc1d5902e0d131dbf9e66209e26be 100644 +--- a/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java ++++ b/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java +@@ -27,7 +27,7 @@ public class TransientCraftingContainer implements CraftingContainer { + + // CraftBukkit start - add fields + public List transaction = new java.util.ArrayList(); +- private RecipeHolder currentRecipe; ++ private RecipeHolder currentRecipe; // Paper - use correct generic + public Container resultInventory; + private Player owner; + private int maxStack = MAX_STACK; +@@ -72,12 +72,12 @@ public class TransientCraftingContainer implements CraftingContainer { + } + + @Override +- public RecipeHolder getCurrentRecipe() { ++ public RecipeHolder getCurrentRecipe() { // Paper - use correct generic + return this.currentRecipe; + } + + @Override +- public void setCurrentRecipe(RecipeHolder currentRecipe) { ++ public void setCurrentRecipe(RecipeHolder currentRecipe) { // Paper - use correct generic + this.currentRecipe = currentRecipe; + } + diff --git a/patches/server/1046-Incremental-chunk-and-player-saving.patch b/patches/server/1046-Incremental-chunk-and-player-saving.patch new file mode 100644 index 0000000000..2755cd14ca --- /dev/null +++ b/patches/server/1046-Incremental-chunk-and-player-saving.patch @@ -0,0 +1,134 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Shane Freeder +Date: Sun, 9 Jun 2019 03:53:22 +0100 +Subject: [PATCH] Incremental chunk and player saving + +Feature patch + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index aa0a693af442a791ad8e5ec5a9e11594e6b42419..0af2e1f951683023124f1733a6079e4eaa5deb48 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -1007,7 +1007,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.ticksUntilAutosave <= 0) { // CraftBukkit +- this.autoSave(); ++ // Paper start - Incremental chunk and player saving ++ final ProfilerFiller profiler = Profiler.get(); ++ int playerSaveInterval = io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.rate; ++ if (playerSaveInterval < 0) { ++ playerSaveInterval = autosavePeriod; ++ } ++ profiler.push("save"); ++ final boolean fullSave = autosavePeriod > 0 && this.tickCount % autosavePeriod == 0; ++ try { ++ this.isSaving = true; ++ if (playerSaveInterval > 0) { ++ this.playerList.saveAll(playerSaveInterval); ++ } ++ for (final ServerLevel level : this.getAllLevels()) { ++ if (level.paperConfig().chunks.autoSaveInterval.value() > 0) { ++ level.saveIncrementally(fullSave); ++ } ++ } ++ } finally { ++ this.isSaving = false; + } ++ profiler.pop(); ++ // Paper end - Incremental chunk and player saving + + ProfilerFiller gameprofilerfiller = Profiler.get(); + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index abed6e7b92d1472bbbc5bfd60abf4f9052c749c5..03ff07e4047fcf5e2cad7be998b76b4fcef6b49a 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -1354,6 +1354,30 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + return !this.server.isUnderSpawnProtection(this, pos, player) && this.getWorldBorder().isWithinBounds(pos); + } + ++ // Paper start - Incremental chunk and player saving ++ public void saveIncrementally(boolean doFull) { ++ ServerChunkCache chunkproviderserver = this.getChunkSource(); ++ ++ if (doFull) { ++ org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(getWorld())); ++ } ++ ++ if (doFull) { ++ this.saveLevelData(true); ++ } ++ // chunk autosave is already called by the ChunkSystem during unload processing (ChunkMap#processUnloads) ++ // Copied from save() ++ // CraftBukkit start - moved from MinecraftServer.saveChunks ++ if (doFull) { // Paper ++ ServerLevel worldserver1 = this; ++ this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings()); ++ this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess())); ++ this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); ++ } ++ // CraftBukkit end ++ } ++ // Paper end - Incremental chunk and player saving ++ + public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) { + // Paper start - add close param + this.save(progressListener, flush, savingDisabled, false); +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 9b61fb06875c8070dba30ee541f85b4eed589681..c7c637fcaf02bf5a0861c9ffaca2b473fdeceddb 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -220,6 +220,7 @@ import org.bukkit.inventory.MainHand; + public class ServerPlayer extends net.minecraft.world.entity.player.Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system + + private static final Logger LOGGER = LogUtils.getLogger(); ++ public long lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving + private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32; + private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10; + private static final int FLY_STAT_RECORDING_SPEED = 25; +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 532e4c1dac20d7481557bb8c84f81c30994ae4d5..8de23b39806734c9a413b6d98dbfff25888c1798 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -518,6 +518,7 @@ public abstract class PlayerList { + + protected void save(ServerPlayer player) { + if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit ++ player.lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving + this.playerIo.save(player); + ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit + +@@ -1152,9 +1153,21 @@ public abstract class PlayerList { + } + + public void saveAll() { ++ // Paper start - Incremental chunk and player saving ++ this.saveAll(-1); ++ } ++ ++ public void saveAll(int interval) { + io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main ++ int numSaved = 0; ++ long now = MinecraftServer.currentTick; + for (int i = 0; i < this.players.size(); ++i) { +- this.save((ServerPlayer) this.players.get(i)); ++ final ServerPlayer player = this.players.get(i); ++ if (interval == -1 || now - player.lastSave >= interval) { ++ this.save(player); ++ if (interval != -1 && ++numSaved >= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) { break; } ++ } ++ // Paper end - Incremental chunk and player saving + } + + return null; }); // Paper - ensure main diff --git a/patches/server/1047-Optimise-general-POI-access.patch b/patches/server/1047-Optimise-general-POI-access.patch new file mode 100644 index 0000000000..d93eda2cf3 --- /dev/null +++ b/patches/server/1047-Optimise-general-POI-access.patch @@ -0,0 +1,1069 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sun, 31 Jan 2021 02:29:24 -0800 +Subject: [PATCH] Optimise general POI access + +There are a couple of problems with mojang's POI code. +Firstly, it's all streams. Unsurprisingly, stacking +streams on top of each other is horrible for performance +and ultimately took up half of a villager's tick! + +Secondly, sometime's the search radius is large and there are +a significant number of poi entries per chunk section. Even +removing streams at this point doesn't help much. The only solution +is to start at the search point and iterate outwards. This +type of approach shows massive gains for portals, simply because +we can avoid sync loading a large area of chunks. I also tested +a massive farm I found in JellySquid's discord, which showed +to benefit significantly simply because the farm had so many +portal blocks that searching through them all was very slow. + +Great care has been taken so that behavior remains identical to +vanilla, however I cannot account for oddball Stream API +implementations, if they even exist (streams can technically +be loose with iteration order in a sorted stream given its +source stream is not tagged with ordered, and mojang does not +tag the source stream as ordered). However in my testing on openjdk +there showed no difference, as expected. + +This patch also specifically optimises other areas of code to +use PoiAccess. For example, some villager AI and portaling code +had to be specifically modified. + +Feature patch + +diff --git a/src/main/java/io/papermc/paper/util/PoiAccess.java b/src/main/java/io/papermc/paper/util/PoiAccess.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f39294b1f83c4022be5ced4da781103a1eee2daf +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/PoiAccess.java +@@ -0,0 +1,806 @@ ++package io.papermc.paper.util; ++ ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import com.mojang.datafixers.util.Pair; ++import it.unimi.dsi.fastutil.doubles.Double2ObjectMap; ++import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap; ++import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; ++import it.unimi.dsi.fastutil.longs.LongOpenHashSet; ++import java.util.function.BiPredicate; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Holder; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.ai.village.poi.PoiManager; ++import net.minecraft.world.entity.ai.village.poi.PoiRecord; ++import net.minecraft.world.entity.ai.village.poi.PoiSection; ++import net.minecraft.world.entity.ai.village.poi.PoiType; ++import java.util.ArrayList; ++import java.util.HashSet; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Map; ++import java.util.Optional; ++import java.util.Set; ++import java.util.function.Predicate; ++ ++/** ++ * Provides optimised access to POI data. All returned values will be identical to vanilla. ++ */ ++public final class PoiAccess { ++ ++ protected static double clamp(final double val, final double min, final double max) { ++ return (val < min ? min : (val > max ? max : val)); ++ } ++ ++ protected static double getSmallestDistanceSquared(final double boxMinX, final double boxMinY, final double boxMinZ, ++ final double boxMaxX, final double boxMaxY, final double boxMaxZ, ++ ++ final double circleX, final double circleY, final double circleZ) { ++ // is the circle center inside the box? ++ if (circleX >= boxMinX && circleX <= boxMaxX && circleY >= boxMinY && circleY <= boxMaxY && circleZ >= boxMinZ && circleZ <= boxMaxZ) { ++ return 0.0; ++ } ++ ++ final double boxWidthX = (boxMaxX - boxMinX) / 2.0; ++ final double boxWidthY = (boxMaxY - boxMinY) / 2.0; ++ final double boxWidthZ = (boxMaxZ - boxMinZ) / 2.0; ++ ++ final double boxCenterX = (boxMinX + boxMaxX) / 2.0; ++ final double boxCenterY = (boxMinY + boxMaxY) / 2.0; ++ final double boxCenterZ = (boxMinZ + boxMaxZ) / 2.0; ++ ++ double centerDiffX = circleX - boxCenterX; ++ double centerDiffY = circleY - boxCenterY; ++ double centerDiffZ = circleZ - boxCenterZ; ++ ++ centerDiffX = circleX - (clamp(centerDiffX, -boxWidthX, boxWidthX) + boxCenterX); ++ centerDiffY = circleY - (clamp(centerDiffY, -boxWidthY, boxWidthY) + boxCenterY); ++ centerDiffZ = circleZ - (clamp(centerDiffZ, -boxWidthZ, boxWidthZ) + boxCenterZ); ++ ++ return (centerDiffX * centerDiffX) + (centerDiffY * centerDiffY) + (centerDiffZ * centerDiffZ); ++ } ++ ++ ++ // key is: ++ // upper 32 bits: ++ // upper 16 bits: max y section ++ // lower 16 bits: min y section ++ // lower 32 bits: ++ // upper 16 bits: section ++ // lower 16 bits: radius ++ protected static long getKey(final int minSection, final int maxSection, final int section, final int radius) { ++ return ( ++ (maxSection & 0xFFFFL) << (64 - 16) ++ | (minSection & 0xFFFFL) << (64 - 32) ++ | (section & 0xFFFFL) << (64 - 48) ++ | (radius & 0xFFFFL) << (64 - 64) ++ ); ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. ++ public static BlockPos findClosestPoiDataPosition(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final PoiRecord ret = findClosestPoiDataRecord( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load ++ ); ++ ++ return ret == null ? null : ret.getPos(); ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. ++ public static Pair, BlockPos> findClosestPoiDataTypeAndPosition(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final PoiRecord ret = findClosestPoiDataRecord( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load ++ ); ++ ++ return ret == null ? null : Pair.of(ret.getPoiType(), ret.getPos()); ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. ++ public static void findClosestPoiDataPositions(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final Set ret) { ++ final Set positions = new HashSet<>(); ++ // pos predicate is last thing that runs before adding to ret. ++ final Predicate newPredicate = (final BlockPos pos) -> { ++ if (positionPredicate != null && !positionPredicate.test(pos)) { ++ return false; ++ } ++ return positions.add(pos.immutable()); ++ }; ++ ++ final List toConvert = new ArrayList<>(); ++ findClosestPoiDataRecords( ++ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, toConvert ++ ); ++ ++ for (final PoiRecord record : toConvert) { ++ ret.add(record.getPos()); ++ } ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. ++ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final List ret = new ArrayList<>(); ++ findClosestPoiDataRecords( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret ++ ); ++ return ret.isEmpty() ? null : ret.get(0); ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. ++ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final BiPredicate, BlockPos> predicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final List ret = new ArrayList<>(); ++ findClosestPoiDataRecords( ++ poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret ++ ); ++ return ret.isEmpty() ? null : ret.get(0); ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. ++ public static void findClosestPoiDataRecords(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final List ret) { ++ final BiPredicate, BlockPos> predicate = positionPredicate != null ? (type, pos) -> positionPredicate.test(pos) : null; ++ findClosestPoiDataRecords(poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret); ++ } ++ ++ public static void findClosestPoiDataRecords(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final BiPredicate, BlockPos> predicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final List ret) { ++ final Predicate occupancyFilter = occupancy.getTest(); ++ ++ final List closestRecords = new ArrayList<>(); ++ double closestDistanceSquared = maxDistanceSquared; ++ ++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; ++ final int lowerY = WorldUtil.getMinSection(poiStorage.moonrise$getWorld()); ++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; ++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; ++ final int upperY = WorldUtil.getMaxSection(poiStorage.moonrise$getWorld()); ++ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; ++ ++ final int centerX = sourcePosition.getX() >> 4; ++ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); ++ final int centerZ = sourcePosition.getZ() >> 4; ++ final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ); ++ ++ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); ++ final LongOpenHashSet seen = new LongOpenHashSet(); ++ seen.add(centerKey); ++ queue.enqueue(centerKey); ++ ++ while (!queue.isEmpty()) { ++ final long key = queue.dequeueLong(); ++ final int sectionX = CoordinateUtils.getChunkSectionX(key); ++ final int sectionY = CoordinateUtils.getChunkSectionY(key); ++ final int sectionZ = CoordinateUtils.getChunkSectionZ(key); ++ ++ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { ++ // out of bound chunk ++ continue; ++ } ++ ++ final double sectionDistanceSquared = getSmallestDistanceSquared( ++ (sectionX << 4) + 0.5, ++ (sectionY << 4) + 0.5, ++ (sectionZ << 4) + 0.5, ++ (sectionX << 4) + 15.5, ++ (sectionY << 4) + 15.5, ++ (sectionZ << 4) + 15.5, ++ (double)sourcePosition.getX(), (double)sourcePosition.getY(), (double)sourcePosition.getZ() ++ ); ++ if (sectionDistanceSquared > closestDistanceSquared) { ++ continue; ++ } ++ ++ // queue all neighbours ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ for (int dy = -1; dy <= 1; ++dy) { ++ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many ++ // values are set. we only care about cardinal neighbours, so, we only care if one value is set ++ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { ++ continue; ++ } ++ ++ final int neighbourX = sectionX + dx; ++ final int neighbourY = sectionY + dy; ++ final int neighbourZ = sectionZ + dz; ++ ++ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); ++ if (seen.add(neighbourKey)) { ++ queue.enqueue(neighbourKey); ++ } ++ } ++ } ++ } ++ ++ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); ++ ++ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { ++ continue; ++ } ++ ++ final PoiSection poiSection = poiSectionOptional.get(); ++ ++ final Map, Set> sectionData = poiSection.getData(); ++ if (sectionData.isEmpty()) { ++ continue; ++ } ++ ++ // now we search the section data ++ for (final Map.Entry, Set> entry : sectionData.entrySet()) { ++ if (!villagePlaceType.test(entry.getKey())) { ++ // filter out by poi type ++ continue; ++ } ++ ++ // now we can look at the poi data ++ for (final PoiRecord poiData : entry.getValue()) { ++ if (!occupancyFilter.test(poiData)) { ++ // filter by occupancy ++ continue; ++ } ++ ++ final BlockPos poiPosition = poiData.getPos(); ++ ++ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range ++ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { ++ // out of range for square radius ++ continue; ++ } ++ ++ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! ++ final double dataRange = poiPosition.distSqr(sourcePosition); ++ ++ if (dataRange > closestDistanceSquared) { ++ // out of range for distance check ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test(poiData.getPoiType(), poiPosition)) { ++ // filter by position ++ continue; ++ } ++ ++ if (dataRange < closestDistanceSquared) { ++ closestRecords.clear(); ++ closestDistanceSquared = dataRange; ++ } ++ closestRecords.add(poiData); ++ } ++ } ++ } ++ ++ // uh oh! we might have multiple records that match the distance sorting! ++ // we need to re-order our results by the way vanilla would have iterated over them. ++ closestRecords.sort((record1, record2) -> { ++ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section ++ // is fine and should be preserved (this sort is stable so we're good there) ++ // but they iterate sections by x then by z (like the following) ++ // for (int x = -dx; x <= dx; ++x) ++ // for (int z = -dz; z <= dz; ++z) ++ // .... ++ // so we need to reorder such that records with lower chunk z, then lower chunk x come first ++ final BlockPos pos1 = record1.getPos(); ++ final BlockPos pos2 = record2.getPos(); ++ ++ final int cx1 = pos1.getX() >> 4; ++ final int cz1 = pos1.getZ() >> 4; ++ ++ final int cx2 = pos2.getX() >> 4; ++ final int cz2 = pos2.getZ() >> 4; ++ ++ if (cz2 != cz1) { ++ // want smaller z ++ return Integer.compare(cz1, cz2); ++ } ++ ++ if (cx2 != cx1) { ++ // want smaller x ++ return Integer.compare(cx1, cx2); ++ } ++ ++ // same chunk ++ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y ++ // so now we just compare section y, wanting smaller y ++ ++ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); ++ }); ++ ++ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). ++ ret.addAll(closestRecords); ++ } ++ ++ // finds the closest poi entry pos. ++ public static BlockPos findNearestPoiPosition(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final PoiRecord ret = findNearestPoiRecord( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load ++ ); ++ return ret == null ? null : ret.getPos(); ++ } ++ ++ // finds the closest `max` poi entry positions. ++ public static void findNearestPoiPositions(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final int max, ++ final List, BlockPos>> ret) { ++ final Set positions = new HashSet<>(); ++ // pos predicate is last thing that runs before adding to ret. ++ final Predicate newPredicate = (final BlockPos pos) -> { ++ if (positionPredicate != null && !positionPredicate.test(pos)) { ++ return false; ++ } ++ return positions.add(pos.immutable()); ++ }; ++ ++ final List toConvert = new ArrayList<>(); ++ findNearestPoiRecords( ++ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, max, toConvert ++ ); ++ ++ for (final PoiRecord record : toConvert) { ++ ret.add(Pair.of(record.getPoiType(), record.getPos())); ++ } ++ } ++ ++ // finds the closest poi entry. ++ public static PoiRecord findNearestPoiRecord(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final List ret = new ArrayList<>(); ++ findNearestPoiRecords( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ++ 1, ret ++ ); ++ return ret.isEmpty() ? null : ret.get(0); ++ } ++ ++ // finds the closest `max` poi entries. ++ public static void findNearestPoiRecords(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final int max, ++ final List ret) { ++ final Predicate occupancyFilter = occupancy.getTest(); ++ ++ final Double2ObjectRBTreeMap> closestRecords = new Double2ObjectRBTreeMap<>(); ++ int totalRecords = 0; ++ double furthestDistanceSquared = maxDistanceSquared; ++ ++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; ++ final int lowerY = WorldUtil.getMinSection(poiStorage.moonrise$getWorld()); ++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; ++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; ++ final int upperY = WorldUtil.getMaxSection(poiStorage.moonrise$getWorld()); ++ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; ++ ++ final int centerX = sourcePosition.getX() >> 4; ++ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); ++ final int centerZ = sourcePosition.getZ() >> 4; ++ final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ); ++ ++ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); ++ final LongOpenHashSet seen = new LongOpenHashSet(); ++ seen.add(centerKey); ++ queue.enqueue(centerKey); ++ ++ while (!queue.isEmpty()) { ++ final long key = queue.dequeueLong(); ++ final int sectionX = CoordinateUtils.getChunkSectionX(key); ++ final int sectionY = CoordinateUtils.getChunkSectionY(key); ++ final int sectionZ = CoordinateUtils.getChunkSectionZ(key); ++ ++ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { ++ // out of bound chunk ++ continue; ++ } ++ ++ final double sectionDistanceSquared = getSmallestDistanceSquared( ++ (sectionX << 4) + 0.5, ++ (sectionY << 4) + 0.5, ++ (sectionZ << 4) + 0.5, ++ (sectionX << 4) + 15.5, ++ (sectionY << 4) + 15.5, ++ (sectionZ << 4) + 15.5, ++ (double) sourcePosition.getX(), (double) sourcePosition.getY(), (double) sourcePosition.getZ() ++ ); ++ ++ if (sectionDistanceSquared > (totalRecords >= max ? furthestDistanceSquared : maxDistanceSquared)) { ++ continue; ++ } ++ ++ // queue all neighbours ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ for (int dy = -1; dy <= 1; ++dy) { ++ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many ++ // values are set. we only care about cardinal neighbours, so, we only care if one value is set ++ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { ++ continue; ++ } ++ ++ final int neighbourX = sectionX + dx; ++ final int neighbourY = sectionY + dy; ++ final int neighbourZ = sectionZ + dz; ++ ++ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); ++ if (seen.add(neighbourKey)) { ++ queue.enqueue(neighbourKey); ++ } ++ } ++ } ++ } ++ ++ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); ++ ++ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { ++ continue; ++ } ++ ++ final PoiSection poiSection = poiSectionOptional.get(); ++ ++ final Map, Set> sectionData = poiSection.getData(); ++ if (sectionData.isEmpty()) { ++ continue; ++ } ++ ++ // now we search the section data ++ for (final Map.Entry, Set> entry : sectionData.entrySet()) { ++ if (!villagePlaceType.test(entry.getKey())) { ++ // filter out by poi type ++ continue; ++ } ++ ++ // now we can look at the poi data ++ for (final PoiRecord poiData : entry.getValue()) { ++ if (!occupancyFilter.test(poiData)) { ++ // filter by occupancy ++ continue; ++ } ++ ++ final BlockPos poiPosition = poiData.getPos(); ++ ++ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range ++ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { ++ // out of range for square radius ++ continue; ++ } ++ ++ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! ++ final double dataRange = poiPosition.distSqr(sourcePosition); ++ ++ if (dataRange > maxDistanceSquared) { ++ // out of range for distance check ++ continue; ++ } ++ ++ if (dataRange > furthestDistanceSquared && totalRecords >= max) { ++ // out of range for distance check ++ continue; ++ } ++ ++ if (positionPredicate != null && !positionPredicate.test(poiPosition)) { ++ // filter by position ++ continue; ++ } ++ ++ if (dataRange > furthestDistanceSquared) { ++ // we know totalRecords < max, so this entry is now our furthest ++ furthestDistanceSquared = dataRange; ++ } ++ ++ closestRecords.computeIfAbsent(dataRange, (final double unused) -> { ++ return new ArrayList<>(); ++ }).add(poiData); ++ ++ if (++totalRecords >= max) { ++ if (closestRecords.size() >= 2) { ++ int entriesInClosest = 0; ++ final Iterator>> iterator = closestRecords.double2ObjectEntrySet().iterator(); ++ double nextFurthestDistanceSquared = 0.0; ++ ++ for (int i = 0, len = closestRecords.size() - 1; i < len; ++i) { ++ final Double2ObjectMap.Entry> recordEntry = iterator.next(); ++ entriesInClosest += recordEntry.getValue().size(); ++ nextFurthestDistanceSquared = recordEntry.getDoubleKey(); ++ } ++ ++ if (entriesInClosest >= max) { ++ // the last set of entries at range wont even be considered for sure... nuke em ++ final Double2ObjectMap.Entry> recordEntry = iterator.next(); ++ totalRecords -= recordEntry.getValue().size(); ++ iterator.remove(); ++ ++ furthestDistanceSquared = nextFurthestDistanceSquared; ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ final List closestRecordsUnsorted = new ArrayList<>(); ++ ++ // we're done here, so now just flatten the map and sort it. ++ ++ for (final List records : closestRecords.values()) { ++ closestRecordsUnsorted.addAll(records); ++ } ++ ++ // uh oh! we might have multiple records that match the distance sorting! ++ // we need to re-order our results by the way vanilla would have iterated over them. ++ closestRecordsUnsorted.sort((record1, record2) -> { ++ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section ++ // is fine and should be preserved (this sort is stable so we're good there) ++ // but they iterate sections by x then by z (like the following) ++ // for (int x = -dx; x <= dx; ++x) ++ // for (int z = -dz; z <= dz; ++z) ++ // .... ++ // so we need to reorder such that records with lower chunk z, then lower chunk x come first ++ final BlockPos pos1 = record1.getPos(); ++ final BlockPos pos2 = record2.getPos(); ++ ++ final int cx1 = pos1.getX() >> 4; ++ final int cz1 = pos1.getZ() >> 4; ++ ++ final int cx2 = pos2.getX() >> 4; ++ final int cz2 = pos2.getZ() >> 4; ++ ++ if (cz2 != cz1) { ++ // want smaller z ++ return Integer.compare(cz1, cz2); ++ } ++ ++ if (cx2 != cx1) { ++ // want smaller x ++ return Integer.compare(cx1, cx2); ++ } ++ ++ // same chunk ++ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y ++ // so now we just compare section y, wanting smaller section y ++ ++ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); ++ }); ++ ++ // trim out any entries exceeding our maximum ++ for (int i = closestRecordsUnsorted.size() - 1; i >= max; --i) { ++ closestRecordsUnsorted.remove(i); ++ } ++ ++ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). ++ ret.addAll(closestRecordsUnsorted); ++ } ++ ++ public static BlockPos findAnyPoiPosition(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final PoiRecord ret = findAnyPoiRecord( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load ++ ); ++ ++ return ret == null ? null : ret.getPos(); ++ } ++ ++ public static void findAnyPoiPositions(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final int max, ++ final List, BlockPos>> ret) { ++ final Set positions = new HashSet<>(); ++ // pos predicate is last thing that runs before adding to ret. ++ final Predicate newPredicate = (final BlockPos pos) -> { ++ if (positionPredicate != null && !positionPredicate.test(pos)) { ++ return false; ++ } ++ return positions.add(pos.immutable()); ++ }; ++ ++ final List toConvert = new ArrayList<>(); ++ findAnyPoiRecords( ++ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, occupancy, load, max, toConvert ++ ); ++ ++ for (final PoiRecord record : toConvert) { ++ ret.add(Pair.of(record.getPoiType(), record.getPos())); ++ } ++ } ++ ++ public static PoiRecord findAnyPoiRecord(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final List ret = new ArrayList<>(); ++ findAnyPoiRecords(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load, 1, ret); ++ return ret.isEmpty() ? null : ret.get(0); ++ } ++ ++ public static void findAnyPoiRecords(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final int max, ++ final List ret) { ++ // the biggest issue with the original mojang implementation is that they chain so many streams together ++ // the amount of streams chained just rolls performance, even if nothing is iterated over ++ final Predicate occupancyFilter = occupancy.getTest(); ++ final double rangeSquared = range * range; ++ ++ int added = 0; ++ ++ // First up, we need to iterate the chunks ++ // all the values here are in chunk sections ++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; ++ final int lowerY = Math.max(WorldUtil.getMinSection(poiStorage.moonrise$getWorld()), Mth.floor(sourcePosition.getY() - range) >> 4); ++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; ++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; ++ final int upperY = Math.min(WorldUtil.getMaxSection(poiStorage.moonrise$getWorld()), Mth.floor(sourcePosition.getY() + range) >> 4); ++ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; ++ ++ // Vanilla iterates by x until max is reached then increases z ++ // vanilla also searches by increasing Y section value ++ for (int currZ = lowerZ; currZ <= upperZ; ++currZ) { ++ for (int currX = lowerX; currX <= upperX; ++currX) { ++ for (int currY = lowerY; currY <= upperY; ++currY) { // vanilla searches the entire chunk because they're actually stupid. just search the sections we need ++ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)) : ++ poiStorage.get(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)); ++ final PoiSection poiSection = poiSectionOptional == null ? null : poiSectionOptional.orElse(null); ++ if (poiSection == null) { ++ continue; ++ } ++ ++ final Map, Set> sectionData = poiSection.getData(); ++ if (sectionData.isEmpty()) { ++ continue; ++ } ++ ++ // now we search the section data ++ for (final Map.Entry, Set> entry : sectionData.entrySet()) { ++ if (!villagePlaceType.test(entry.getKey())) { ++ // filter out by poi type ++ continue; ++ } ++ ++ // now we can look at the poi data ++ for (final PoiRecord poiData : entry.getValue()) { ++ if (!occupancyFilter.test(poiData)) { ++ // filter by occupancy ++ continue; ++ } ++ ++ final BlockPos poiPosition = poiData.getPos(); ++ ++ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range ++ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { ++ // out of range for square radius ++ continue; ++ } ++ ++ if (poiPosition.distSqr(sourcePosition) > rangeSquared) { ++ // out of range for distance check ++ continue; ++ } ++ ++ if (positionPredicate != null && !positionPredicate.test(poiPosition)) { ++ // filter by position ++ continue; ++ } ++ ++ // found one! ++ ret.add(poiData); ++ if (++added >= max) { ++ return; ++ } ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ private PoiAccess() { ++ throw new RuntimeException(); ++ } ++} +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java +index 45974895c464b4c0186ab9add5eacb98abe90e09..0d177e828c2b338ce93c58aaef04df326e1eb0b2 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java +@@ -84,12 +84,16 @@ public class AcquirePoi { + return true; + } + }; +- Set, BlockPos>> set = poiManager.findAllClosestFirstWithType( +- poiPredicate, predicate2, entity.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE +- ) +- .limit(5L) +- .filter(pairx -> worldPosBiPredicate.test(world, (BlockPos)pairx.getSecond())) +- .collect(Collectors.toSet()); ++ // Paper start - optimise POI access ++ final java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); ++ io.papermc.paper.util.PoiAccess.findNearestPoiPositions(poiManager, poiPredicate, predicate2, entity.blockPosition(), 48, 48*48, PoiManager.Occupancy.HAS_SPACE, false, 5, poiposes); ++ final Set, BlockPos>> set = new java.util.HashSet<>(poiposes.size()); ++ for (final Pair, BlockPos> poiPose : poiposes) { ++ if (worldPosBiPredicate.test(world, poiPose.getSecond())) { ++ set.add(poiPose); ++ } ++ } ++ // Paper end - optimise POI access + Path path = findPathToPois(entity, set); + if (path != null && path.canReach()) { + BlockPos blockPos = path.getTarget(); +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java +index d5a549f08b98c80a5cf0eef02cb8a389c32dfecb..92731b6b593289e9f583c9b705b219e81fcd8e73 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java +@@ -53,11 +53,12 @@ public class NearestBedSensor extends Sensor { + return true; + } + }; +- Set, BlockPos>> set = poiManager.findAllWithType( +- holder -> holder.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY +- ) +- .collect(Collectors.toSet()); +- Path path = AcquirePoi.findPathToPois(entity, set); ++ // Paper start - optimise POI access ++ java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); ++ // don't ask me why it's unbounded. ask mojang. ++ io.papermc.paper.util.PoiAccess.findAnyPoiPositions(poiManager, type -> type.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes); ++ Path path = AcquirePoi.findPathToPois(entity, new java.util.HashSet<>(poiposes)); ++ // Paper end - optimise POI access + if (path != null && path.canReach()) { + BlockPos blockPos = path.getTarget(); + Optional> optional = poiManager.getType(blockPos); +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +index 5930a430983061afddf20e3208ff2462ca1b78cd..63a94b6068fdaef8bb26675c2927cb729ced1dac 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 +@@ -254,36 +254,45 @@ public class PoiManager extends SectionStorage im + public Optional find( + Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus + ) { +- return this.findAll(typePredicate, posPredicate, pos, radius, occupationStatus).findFirst(); ++ // Paper start - re-route to faster logic ++ BlockPos ret = io.papermc.paper.util.PoiAccess.findAnyPoiPosition(this, typePredicate, posPredicate, pos, radius, occupationStatus, false); ++ return Optional.ofNullable(ret); ++ // Paper end + } + + public Optional findClosest(Predicate> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) { +- return this.getInRange(typePredicate, pos, radius, occupationStatus) +- .map(PoiRecord::getPos) +- .min(Comparator.comparingDouble(poiPos -> poiPos.distSqr(pos))); ++ // Paper start - re-route to faster logic ++ BlockPos closestPos = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, null, pos, radius, radius * radius, occupationStatus, false); ++ return Optional.ofNullable(closestPos); ++ // Paper end - re-route to faster logic + } + + public Optional, BlockPos>> findClosestWithType( + Predicate> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus + ) { +- return this.getInRange(typePredicate, pos, radius, occupationStatus) +- .min(Comparator.comparingDouble(poi -> poi.getPos().distSqr(pos))) +- .map(poi -> Pair.of(poi.getPoiType(), poi.getPos())); ++ // Paper start - re-route to faster logic ++ return Optional.ofNullable(io.papermc.paper.util.PoiAccess.findClosestPoiDataTypeAndPosition( ++ this, typePredicate, null, pos, radius, radius * radius, occupationStatus, false ++ )); ++ // Paper end - re-route to faster logic + } + + public Optional findClosest( + Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus + ) { +- return this.getInRange(typePredicate, pos, radius, occupationStatus) +- .map(PoiRecord::getPos) +- .filter(posPredicate) +- .min(Comparator.comparingDouble(poiPos -> poiPos.distSqr(pos))); ++ // Paper start - re-route to faster logic ++ BlockPos closestPos = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, posPredicate, pos, radius, radius * radius, occupationStatus, false); ++ return Optional.ofNullable(closestPos); ++ // Paper end - re-route to faster logic + } + + public Optional take(Predicate> typePredicate, BiPredicate, BlockPos> posPredicate, BlockPos pos, int radius) { +- return this.getInRange(typePredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE) +- .filter(poi -> posPredicate.test(poi.getPoiType(), poi.getPos())) +- .findFirst() ++ // Paper start - re-route to faster logic ++ final @javax.annotation.Nullable PoiRecord closest = io.papermc.paper.util.PoiAccess.findClosestPoiDataRecord( ++ this, typePredicate, posPredicate, pos, radius, radius * radius, Occupancy.HAS_SPACE, false ++ ); ++ return Optional.ofNullable(closest) ++ // Paper end - re-route to faster logic + .map(poi -> { + poi.acquireTicket(); + return poi.getPos(); +@@ -298,8 +307,21 @@ public class PoiManager extends SectionStorage im + int radius, + RandomSource random + ) { +- List list = Util.toShuffledList(this.getInRange(typePredicate, pos, radius, occupationStatus), random); +- return list.stream().filter(poi -> positionPredicate.test(poi.getPos())).findFirst().map(PoiRecord::getPos); ++ // Paper start - re-route to faster logic ++ List list = new java.util.ArrayList<>(); ++ io.papermc.paper.util.PoiAccess.findAnyPoiRecords( ++ this, typePredicate, positionPredicate, pos, radius, occupationStatus, false, Integer.MAX_VALUE, list ++ ); ++ ++ // the old method shuffled the list and then tried to find the first element in it that ++ // matched positionPredicate, however we moved positionPredicate into the poi search. This means we can avoid a ++ // shuffle entirely, and just pick a random element from list ++ if (list.isEmpty()) { ++ return Optional.empty(); ++ } ++ ++ return Optional.of(list.get(random.nextInt(list.size())).getPos()); ++ // Paper end - re-route to faster logic + } + + public boolean release(BlockPos pos) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java +index 712cbfc100e8aaf612d1d651dae64f57f892a768..827991ee61406bcda3f4794dcc735c0e2e0e09af 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java +@@ -26,7 +26,7 @@ import org.slf4j.Logger; + public class PoiSection implements ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection { // Paper - rewrite chunk system + private static final Logger LOGGER = LogUtils.getLogger(); + private final Short2ObjectMap records = new Short2ObjectOpenHashMap<>(); +- private final Map, Set> byType = Maps.newHashMap(); ++ private final Map, Set> byType = Maps.newHashMap(); public final Map, Set> getData() { return this.byType; } // Paper - public accessor + private final Runnable setDirty; + private boolean isValid; + +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java +index c3beb7fcad46a917d2b61bd0a0e98e5106056728..9b97fb2d125df4df715599aab27e074707731466 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java +@@ -131,11 +131,11 @@ public class SectionStorage implements AutoCloseable, ca.spottedleaf.moonr + } + + @Nullable +- protected Optional get(long pos) { ++ public Optional get(long pos) { // Paper - public + return this.storage.get(pos); + } + +- protected Optional getOrLoad(long pos) { ++ public Optional getOrLoad(long pos) { // Paper - public + if (this.outsideStoredRange(pos)) { + return Optional.empty(); + } else { +diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +index 83d294f6f48b867d09ea0d339c779011bf4138a5..9204bb0538297f233442a86733a33e6d0eea8114 100644 +--- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java ++++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +@@ -53,17 +53,39 @@ public class PortalForcer { + // int i = flag ? 16 : 128; + // CraftBukkit end + +- villageplace.ensureLoadedAndValid(this.level, blockposition, i); +- Stream stream = villageplace.getInSquare((holder) -> { // CraftBukkit - decompile error +- return holder.is(PoiTypes.NETHER_PORTAL); +- }, blockposition, i, PoiManager.Occupancy.ANY).map(PoiRecord::getPos); +- +- Objects.requireNonNull(worldborder); +- return stream.filter(worldborder::isWithinBounds).filter(pos -> !(this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> pos.getY() >= v))).filter((blockposition1) -> { // Paper - Configurable nether ceiling damage +- return this.level.getBlockState(blockposition1).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); +- }).min(Comparator.comparingDouble((BlockPos blockposition1) -> { // CraftBukkit - decompile error +- return blockposition1.distSqr(blockposition); +- }).thenComparingInt(Vec3i::getY)); ++ // Paper start - optimise portals ++ Optional optional; ++ java.util.List records = new java.util.ArrayList<>(); ++ io.papermc.paper.util.PoiAccess.findClosestPoiDataRecords( ++ villageplace, ++ type -> type.is(PoiTypes.NETHER_PORTAL), ++ (BlockPos pos) -> { ++ net.minecraft.world.level.chunk.ChunkAccess lowest = this.level.getChunk(pos.getX() >> 4, pos.getZ() >> 4, net.minecraft.world.level.chunk.status.ChunkStatus.EMPTY); ++ if (!lowest.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.FULL) ++ && (lowest.getBelowZeroRetrogen() == null || !lowest.getBelowZeroRetrogen().targetStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.SPAWN))) { ++ // why would we generate the chunk? ++ return false; ++ } ++ if (!worldborder.isWithinBounds(pos) || (this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> pos.getY() >= v))) { // Paper - Configurable nether ceiling damage ++ return false; ++ } ++ return lowest.getBlockState(pos).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); ++ }, ++ blockposition, i, Double.MAX_VALUE, PoiManager.Occupancy.ANY, true, records ++ ); ++ ++ // this gets us most of the way there, but we bias towards lower y values. ++ BlockPos lowestPos = null; ++ for (PoiRecord record : records) { ++ if (lowestPos == null) { ++ lowestPos = record.getPos(); ++ } else if (lowestPos.getY() > record.getPos().getY()) { ++ lowestPos = record.getPos(); ++ } ++ } ++ // now we're done ++ return Optional.ofNullable(lowestPos); ++ // Paper end - optimise portals + } + + public Optional createPortal(BlockPos pos, Direction.Axis axis) { diff --git a/patches/server/1048-Fix-entity-tracker-desync-when-new-players-are-added.patch b/patches/server/1048-Fix-entity-tracker-desync-when-new-players-are-added.patch new file mode 100644 index 0000000000..5fb7dcd463 --- /dev/null +++ b/patches/server/1048-Fix-entity-tracker-desync-when-new-players-are-added.patch @@ -0,0 +1,107 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Tue, 20 Feb 2024 18:24:16 -0800 +Subject: [PATCH] Fix entity tracker desync when new players are added to the + tracker + +The delta position packet instructs the client to update +the entity position by a position difference. However, this position +difference is relative to the last position in the entity tracker +state, not the last position which has been sent to the player. As +a result, if the last position the player has recorded is different +than the one stored in the entity tracker (which occurs when a new +player is added to an existing entity tracker state) then the sent +position difference will cause a position desync for the client. + +We can resolve this problem by either tracking the last position +sent per-player, or by simply resetting the last sent position +in the entity tracker state every time a new player is added. +Resetting the last sent position every time a new player is +added to the tracker is just easier to do, so that is what +this patch does. + +This patch also fixes entities appearing to disappear when +teleporting to players by changing the initial position +in the spawn packet to the entities current tracking position. +When teleporting, the spawn packet will contain the old position +which is most likely in an unloaded chunk - which means that the +client will not tick the entity and thus not lerp the entity +from its old position to its new position. + +Feature patch + +diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java +index f6e1deb2f849d8b01b15cfa69e2f6cd5f2b1512b..f66e40326c510aa3267542b1a24ed75d1ed6d3f1 100644 +--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java ++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java +@@ -42,9 +42,11 @@ public class ClientboundAddEntityPacket implements Packet= 1 || Math.abs(b1 - this.lastSentXRot) >= 1; +@@ -199,7 +206,7 @@ public class ServerEntity { + long k = this.positionCodec.encodeZ(vec3d); + boolean flag5 = i < -32768L || i > 32767L || j < -32768L || j > 32767L || k < -32768L || k > 32767L; + +- if (!flag5 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.onGround()) { ++ if (!this.forceStateResync && !flag5 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.onGround()) { // Paper - fix desync when a player is added to the tracker + if ((!flag2 || !flag) && !(this.entity instanceof AbstractArrow)) { + if (flag2) { + packet1 = new ClientboundMoveEntityPacket.Pos(this.entity.getId(), (short) ((int) i), (short) ((int) j), (short) ((int) k), this.entity.onGround()); +@@ -265,6 +272,7 @@ public class ServerEntity { + } + + this.entity.hasImpulse = false; ++ this.forceStateResync = false; // Paper - fix desync when a player is added to the tracker + } + + ++this.tickCount; diff --git a/patches/server/1049-Lag-compensation-ticks.patch b/patches/server/1049-Lag-compensation-ticks.patch new file mode 100644 index 0000000000..917ace263a --- /dev/null +++ b/patches/server/1049-Lag-compensation-ticks.patch @@ -0,0 +1,131 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sat, 23 Sep 2023 22:05:35 -0700 +Subject: [PATCH] Lag compensation ticks + +Areas affected by lag comepnsation: + - Block breaking and destroying + - Eating food items + +Feature patch + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 0af2e1f951683023124f1733a6079e4eaa5deb48..b5c5e9d9279e61e2476319e9ce8a829743d56267 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -331,6 +331,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { + AtomicReference atomicreference = new AtomicReference(); +@@ -1841,6 +1842,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - BlockPhysicsEvent + worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent + net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers ++ worldserver.updateLagCompensationTick(); // Paper - lag compensation + + gameprofilerfiller.push(() -> { + String s = String.valueOf(worldserver); +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 03ff07e4047fcf5e2cad7be998b76b4fcef6b49a..020ef251fd3d03c9e8fb9fc859d4ef9fc04cd3ba 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -583,6 +583,17 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + ); + } + // Paper end - chunk tick iteration ++ // Paper start - lag compensation ++ private long lagCompensationTick = net.minecraft.server.MinecraftServer.SERVER_INIT; ++ ++ public long getLagCompensationTick() { ++ return this.lagCompensationTick; ++ } ++ ++ public void updateLagCompensationTick() { ++ this.lagCompensationTick = (System.nanoTime() - net.minecraft.server.MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L)); ++ } ++ // Paper end - lag compensation + + // Add env and gen to constructor, IWorldDataServer -> WorldDataServer + public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +index 504c996220b278c194c93e001a3b326d549868ec..a96f859a5d0c6ec692d4627a69f3c9ee49199dbc 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +@@ -127,7 +127,7 @@ public class ServerPlayerGameMode { + } + + public void tick() { +- this.gameTicks = MinecraftServer.currentTick; // CraftBukkit; ++ this.gameTicks = (int)this.level.getLagCompensationTick(); // CraftBukkit; // Paper - lag compensation + BlockState iblockdata; + + if (this.hasDelayedDestroy) { +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 02b412dcad5c8df6e14e92166b3bea629d640680..91735414a81c40861315ea2d4ca6fdce64b2c228 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -4058,6 +4058,10 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.resendPossiblyDesyncedDataValues(java.util.List.of(DATA_LIVING_ENTITY_FLAGS), serverPlayer); + } + // Paper end - Properly cancel usable items ++ // Paper start - lag compensate eating ++ protected long eatStartTime; ++ protected int totalEatTimeTicks; ++ // Paper end - lag compensate eating + private void updatingUsingItem() { + if (this.isUsingItem()) { + if (ItemStack.isSameItem(this.getItemInHand(this.getUsedItemHand()), this.useItem)) { +@@ -4072,7 +4076,12 @@ public abstract class LivingEntity extends Entity implements Attackable { + + protected void updateUsingItem(ItemStack stack) { + stack.onUseTick(this.level(), this, this.getUseItemRemainingTicks()); +- if (--this.useItemRemaining == 0 && !this.level().isClientSide && !stack.useOnRelease()) { ++ // Paper start - lag compensate eating ++ // we add 1 to the expected time to avoid lag compensating when we should not ++ final boolean shouldLagCompensate = this.useItem.has(DataComponents.FOOD) && this.eatStartTime != -1 && (System.nanoTime() - this.eatStartTime) > ((1L + this.totalEatTimeTicks) * 50L * (1000L * 1000L)); ++ if ((--this.useItemRemaining == 0 || shouldLagCompensate) && !this.level().isClientSide && !stack.useOnRelease()) { ++ this.useItemRemaining = 0; ++ // Paper end - lag compensate eating + this.completeUsingItem(); + } + +@@ -4110,7 +4119,10 @@ public abstract class LivingEntity extends Entity implements Attackable { + + if (!itemstack.isEmpty() && !this.isUsingItem() || forceUpdate) { // Paper - Prevent consuming the wrong itemstack + this.useItem = itemstack; +- this.useItemRemaining = itemstack.getUseDuration(this); ++ // Paper start - lag compensate eating ++ this.useItemRemaining = this.totalEatTimeTicks = itemstack.getUseDuration(this); ++ this.eatStartTime = System.nanoTime(); ++ // Paper end - lag compensate eating + if (!this.level().isClientSide) { + this.setLivingEntityFlag(1, true); + this.setLivingEntityFlag(2, hand == InteractionHand.OFF_HAND); +@@ -4135,7 +4147,10 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + } else if (!this.isUsingItem() && !this.useItem.isEmpty()) { + this.useItem = ItemStack.EMPTY; +- this.useItemRemaining = 0; ++ // Paper start - lag compensate eating ++ this.useItemRemaining = this.totalEatTimeTicks = 0; ++ this.eatStartTime = -1L; ++ // Paper end - lag compensate eating + } + } + +@@ -4266,7 +4281,10 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + this.useItem = ItemStack.EMPTY; +- this.useItemRemaining = 0; ++ // Paper start - lag compensate eating ++ this.useItemRemaining = this.totalEatTimeTicks = 0; ++ this.eatStartTime = -1L; ++ // Paper end - lag compensate eating + } + + public boolean isBlocking() { diff --git a/patches/server/1050-Optimise-collision-checking-in-player-move-packet-ha.patch b/patches/server/1050-Optimise-collision-checking-in-player-move-packet-ha.patch new file mode 100644 index 0000000000..dcc4506fa6 --- /dev/null +++ b/patches/server/1050-Optimise-collision-checking-in-player-move-packet-ha.patch @@ -0,0 +1,170 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 2 Jul 2020 12:02:43 -0700 +Subject: [PATCH] Optimise collision checking in player move packet handling + +Move collision logic to just the hasNewCollision call instead of getCubes + hasNewCollision + +Feature patch + +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index f0200e2e68e3ec88b82d337a76e22a6e80419b6f..a3d8d5d735272ae2a67536e4d5bbcdb5d2e4bf8b 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -579,7 +579,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + return; + } + +- boolean flag = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); ++ AABB oldBox = entity.getBoundingBox(); // Paper - copy from player movement packet + + d6 = d3 - this.vehicleLastGoodX; // Paper - diff on change, used for checking large move vectors above + d7 = d4 - this.vehicleLastGoodY; // Paper - diff on change, used for checking large move vectors above +@@ -595,6 +595,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + entity.move(MoverType.PLAYER, new Vec3(d6, d7, d8)); ++ boolean didCollide = toX != entity.getX() || toY != entity.getY() || toZ != entity.getZ(); // Paper - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be... + double d11 = d7; + + d6 = d3 - entity.getX(); +@@ -608,15 +609,23 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + boolean flag2 = false; + + if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot +- flag2 = true; ++ flag2 = true; // Paper - diff on change, this should be moved wrongly + ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", new Object[]{entity.getName().getString(), this.player.getName().getString(), Math.sqrt(d10)}); + } + + entity.absMoveTo(d3, d4, d5, f, f1); + this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit +- boolean flag3 = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); + +- if (flag && (flag2 || !flag3)) { ++ // Paper start - optimise out extra getCubes ++ boolean teleportBack = flag2; // violating this is always a fail ++ if (!teleportBack) { ++ // note: only call after setLocation, or else getBoundingBox is wrong ++ AABB newBox = entity.getBoundingBox(); ++ if (didCollide || !oldBox.equals(newBox)) { ++ teleportBack = this.hasNewCollision(worldserver, entity, oldBox, newBox); ++ } // else: no collision at all detected, why do we care? ++ } ++ if (teleportBack) { // Paper end - optimise out extra getCubes + entity.absMoveTo(d0, d1, d2, f, f1); + this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit + this.send(ClientboundMoveVehiclePacket.fromEntity(entity)); +@@ -692,7 +701,32 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + private boolean noBlocksAround(Entity entity) { +- return entity.level().getBlockStates(entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D)).allMatch(BlockBehaviour.BlockStateBase::isAir); ++ // Paper start - stop using streams, this is already a known fixed problem in Entity#move ++ AABB box = entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D); ++ int minX = Mth.floor(box.minX); ++ int minY = Mth.floor(box.minY); ++ int minZ = Mth.floor(box.minZ); ++ int maxX = Mth.floor(box.maxX); ++ int maxY = Mth.floor(box.maxY); ++ int maxZ = Mth.floor(box.maxZ); ++ ++ Level world = entity.level(); ++ BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); ++ ++ for (int y = minY; y <= maxY; ++y) { ++ for (int z = minZ; z <= maxZ; ++z) { ++ for (int x = minX; x <= maxX; ++x) { ++ pos.set(x, y, z); ++ BlockState type = world.getBlockStateIfLoaded(pos); ++ if (type != null && !type.isAir()) { ++ return false; ++ } ++ } ++ } ++ } ++ ++ return true; ++ // Paper end - stop using streams, this is already a known fixed problem in Entity#move + } + + @Override +@@ -1451,7 +1485,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + } + +- AABB axisalignedbb = this.player.getBoundingBox(); ++ AABB axisalignedbb = this.player.getBoundingBox(); // Paper - diff on change, should be old AABB + + d6 = d0 - this.lastGoodX; // Paper - diff on change, used for checking large move vectors above + d7 = d1 - this.lastGoodY; // Paper - diff on change, used for checking large move vectors above +@@ -1493,6 +1527,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + this.player.move(MoverType.PLAYER, new Vec3(d6, d7, d8)); + this.player.onGround = packet.isOnGround(); // CraftBukkit - SPIGOT-5810, SPIGOT-5835, SPIGOT-6828: reset by this.player.move ++ boolean didCollide = toX != this.player.getX() || toY != this.player.getY() || toZ != this.player.getZ(); // Paper - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be... + // Paper start - prevent position desync + if (this.awaitingPositionFromClient != null) { + return; // ... thanks Mojang for letting move calls teleport across dimensions. +@@ -1523,7 +1558,17 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + // Paper start - Add fail move event +- boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && (movedWrongly && worldserver.noCollision(this.player, axisalignedbb) || this.isPlayerCollidingWithAnythingNew(worldserver, axisalignedbb, d0, d1, d2)); ++ // Paper start - optimise out extra getCubes ++ boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && movedWrongly; ++ this.player.absMoveTo(d0, d1, d2, f, f1); // prevent desync by tping to the set position, dropped for unknown reasons by mojang ++ 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? ++ } ++ // Paper end - optimise out extra getCubes + if (teleportBack) { + io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.CLIPPED_INTO_BLOCK, + toX, toY, toZ, toYaw, toPitch, false); +@@ -1647,7 +1692,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + private boolean updateAwaitingTeleport() { + if (this.awaitingPositionFromClient != null) { +- if (this.tickCount - this.awaitingTeleportTime > 20) { ++ if (false && this.tickCount - this.awaitingTeleportTime > 20) { // Paper - this will greatly screw with clients with > 1000ms RTT + this.awaitingTeleportTime = this.tickCount; + this.teleport(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot()); + } +@@ -1660,6 +1705,33 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + } + ++ // Paper start - optimise out extra getCubes ++ private boolean hasNewCollision(final ServerLevel world, final Entity entity, final AABB oldBox, final AABB newBox) { ++ final List collisionsBB = new java.util.ArrayList<>(); ++ final List collisionsVoxel = new java.util.ArrayList<>(); ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisions( ++ world, entity, newBox, collisionsVoxel, collisionsBB, ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS | ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, ++ null, null ++ ); ++ ++ for (int i = 0, len = collisionsBB.size(); i < len; ++i) { ++ final AABB box = collisionsBB.get(i); ++ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(box, oldBox)) { ++ return true; ++ } ++ } ++ ++ for (int i = 0, len = collisionsVoxel.size(); i < len; ++i) { ++ final VoxelShape voxel = collisionsVoxel.get(i); ++ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(voxel, oldBox)) { ++ return true; ++ } ++ } ++ ++ return false; ++ } ++ // Paper end - optimise out extra getCubes + private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box, double newX, double newY, double newZ) { + AABB axisalignedbb1 = this.player.getBoundingBox().move(newX - this.player.getX(), newY - this.player.getY(), newZ - this.player.getZ()); + Iterable iterable = world.getCollisions(this.player, axisalignedbb1.deflate(9.999999747378752E-6D)); diff --git a/patches/server/1051-Optional-per-player-mob-spawns.patch b/patches/server/1051-Optional-per-player-mob-spawns.patch new file mode 100644 index 0000000000..c6fad8c0e5 --- /dev/null +++ b/patches/server/1051-Optional-per-player-mob-spawns.patch @@ -0,0 +1,233 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: kickash32 +Date: Mon, 19 Aug 2019 01:27:58 +0500 +Subject: [PATCH] Optional per player mob spawns + +Feature patch + +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 7833c53b4eff67f2ff37c091b5926cb081205921..094096bd08450e5d656ce2c442757cbc63ffb090 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -229,8 +229,26 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + // Paper start ++ // Paper start - Optional per player mob spawns ++ public void updatePlayerMobTypeMap(final Entity entity) { ++ if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { ++ return; ++ } ++ final int index = entity.getType().getCategory().ordinal(); ++ ++ final ca.spottedleaf.moonrise.common.list.ReferenceList inRange = ++ this.level.moonrise$getNearbyPlayers().getPlayers(entity.chunkPosition(), ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE); ++ if (inRange == null) { ++ return; ++ } ++ final ServerPlayer[] backingSet = inRange.getRawDataUnchecked(); ++ for (int i = 0, len = inRange.size(); i < len; i++) { ++ ++(backingSet[i].mobCounts[index]); ++ } ++ } + public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) { +- return -1; ++ return player.mobCounts[mobCategory.ordinal()]; ++ // Paper end - Optional per player mob spawns + } + // Paper end + +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index b3ce572547535001959d9bcc6cb567da552c6539..8e96905fa93b02623f16feb4369a45b175031ebf 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -492,7 +492,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + gameprofilerfiller.popPush("shuffleChunks"); + // Paper start - chunk tick iteration optimisation + this.shuffleRandom.setSeed(this.level.random.nextLong()); +- Util.shuffle(list, this.shuffleRandom); ++ if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) Util.shuffle(list, this.shuffleRandom); // Paper - Optional per player mob spawns; do not need this when per-player is enabled + // Paper end - chunk tick iteration optimisation + this.tickChunks(gameprofilerfiller, j, list); + gameprofilerfiller.pop(); +@@ -549,7 +549,19 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + private void tickChunks(ProfilerFiller profiler, long timeDelta, List chunks) { + profiler.popPush("naturalSpawnCount"); + int j = this.distanceManager.getNaturalSpawnChunkCount(); +- NaturalSpawner.SpawnState spawnercreature_d = NaturalSpawner.createState(j, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap)); ++ // Paper start - Optional per player mob spawns ++ final int naturalSpawnChunkCount = j; ++ NaturalSpawner.SpawnState spawnercreature_d; // moved down ++ if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled ++ // re-set mob counts ++ for (ServerPlayer player : this.level.players) { ++ Arrays.fill(player.mobCounts, 0); ++ } ++ spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); ++ } else { ++ spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); ++ } ++ // Paper end - Optional per player mob spawns + + this.lastSpawnState = spawnercreature_d; + profiler.popPush("spawnAndTick"); +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index c7c637fcaf02bf5a0861c9ffaca2b473fdeceddb..1ac3f820d2f7c1bd29e2f2a323747f8262a57d89 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -301,6 +301,10 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple + public boolean queueHealthUpdatePacket; + public net.minecraft.network.protocol.game.ClientboundSetHealthPacket queuedHealthUpdatePacket; + // Paper end - cancellable death event ++ // Paper start - Optional per player mob spawns ++ public static final int MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length; ++ public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper ++ // Paper end - Optional per player mob spawns + + // CraftBukkit start + public CraftPlayer.TransferCookieConnection transferCookieConnection; +diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +index bf943feca387b77a3154773a59da7190d38d8621..12ebd7829c7f6814ccd79ae96aa9023afcc64696 100644 +--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java ++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +@@ -71,6 +71,12 @@ public final class NaturalSpawner { + private NaturalSpawner() {} + + public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable entities, NaturalSpawner.ChunkGetter chunkSource, LocalMobCapCalculator densityCapper) { ++ // Paper start - Optional per player mob spawns ++ return createState(spawningChunkCount, entities, chunkSource, densityCapper, false); ++ } ++ ++ public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable entities, NaturalSpawner.ChunkGetter chunkSource, LocalMobCapCalculator densityCapper, boolean countMobs) { ++ // Paper end - Optional per player mob spawns + PotentialCalculator spawnercreatureprobabilities = new PotentialCalculator(); + Object2IntOpenHashMap object2intopenhashmap = new Object2IntOpenHashMap(); + Iterator iterator = entities.iterator(); +@@ -103,11 +109,16 @@ public final class NaturalSpawner { + spawnercreatureprobabilities.addCharge(entity.blockPosition(), biomesettingsmobs_b.charge()); + } + +- if (entity instanceof Mob) { ++ if (densityCapper != null && entity instanceof Mob) { // Paper - Optional per player mob spawns + densityCapper.addMob(chunk.getPos(), enumcreaturetype); + } + + object2intopenhashmap.addTo(enumcreaturetype, 1); ++ // Paper start - Optional per player mob spawns ++ if (countMobs) { ++ chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity); ++ } ++ // Paper end - Optional per player mob spawns + }); + } + } +@@ -142,7 +153,7 @@ public final class NaturalSpawner { + continue; + } + +- if ((flag || !enumcreaturetype.isFriendly()) && (flag1 || enumcreaturetype.isFriendly()) && (flag2 || !enumcreaturetype.isPersistent()) && spawnercreature_d.canSpawnForCategoryGlobal(enumcreaturetype, limit)) { ++ if ((flag || !enumcreaturetype.isFriendly()) && (flag1 || enumcreaturetype.isFriendly()) && (flag2 || !enumcreaturetype.isPersistent()) && (worldserver.paperConfig().entities.spawning.perPlayerMobSpawns || spawnercreature_d.canSpawnForCategoryGlobal(enumcreaturetype, limit))) { // Paper - Optional per player mob spawns; remove global check, check later during the local one + // CraftBukkit end + list.add(enumcreaturetype); + } +@@ -160,12 +171,43 @@ public final class NaturalSpawner { + while (iterator.hasNext()) { + MobCategory enumcreaturetype = (MobCategory) iterator.next(); + +- if (info.canSpawnForCategoryLocal(enumcreaturetype, chunk.getPos())) { ++ // Paper start - Optional per player mob spawns ++ final boolean canSpawn; ++ int maxSpawns = Integer.MAX_VALUE; ++ if (world.paperConfig().entities.spawning.perPlayerMobSpawns) { ++ // Copied from getFilteredSpawningCategories ++ int limit = enumcreaturetype.getMaxInstancesPerChunk(); ++ SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(enumcreaturetype); ++ if (CraftSpawnCategory.isValidForLimits(spawnCategory)) { ++ limit = world.getWorld().getSpawnLimit(spawnCategory); ++ } ++ ++ // Apply per-player limit ++ int minDiff = Integer.MAX_VALUE; ++ final ca.spottedleaf.moonrise.common.list.ReferenceList inRange = ++ world.moonrise$getNearbyPlayers().getPlayers(chunk.getPos(), ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE); ++ if (inRange != null) { ++ final net.minecraft.server.level.ServerPlayer[] backingSet = inRange.getRawDataUnchecked(); ++ for (int k = 0, len = inRange.size(); k < len; k++) { ++ minDiff = Math.min(limit - world.getChunkSource().chunkMap.getMobCountNear(backingSet[k], enumcreaturetype), minDiff); ++ } ++ } ++ ++ maxSpawns = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff; ++ canSpawn = maxSpawns > 0; ++ } else { ++ canSpawn = info.canSpawnForCategoryLocal(enumcreaturetype, chunk.getPos()); ++ } ++ if (canSpawn) { ++ // Paper end - Optional per player mob spawns + Objects.requireNonNull(info); + NaturalSpawner.SpawnPredicate spawnercreature_c = info::canSpawn; + + Objects.requireNonNull(info); +- NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn); ++ // Paper start - Optional per player mob spawns ++ NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn, ++ maxSpawns, world.paperConfig().entities.spawning.perPlayerMobSpawns ? world.getChunkSource().chunkMap::updatePlayerMobTypeMap : null); ++ // Paper end - Optional per player mob spawns + } + } + +@@ -183,10 +225,15 @@ public final class NaturalSpawner { + // Paper end - Add mobcaps commands + + public static void spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) { ++ // Paper start - Optional per player mob spawns ++ spawnCategoryForChunk(group, world, chunk, checker, runner, Integer.MAX_VALUE, null); ++ } ++ public static void spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner, int maxSpawns, Consumer trackEntity) { ++ // Paper end - Optional per player mob spawns + BlockPos blockposition = NaturalSpawner.getRandomPosWithin(world, chunk); + + if (blockposition.getY() >= world.getMinY() + 1) { +- NaturalSpawner.spawnCategoryForPosition(group, world, chunk, blockposition, checker, runner); ++ NaturalSpawner.spawnCategoryForPosition(group, world, chunk, blockposition, checker, runner, maxSpawns, trackEntity); // Paper - Optional per player mob spawns + } + } + +@@ -198,7 +245,12 @@ public final class NaturalSpawner { + }); + } + ++ // Paper start - Optional per player mob spawns + public static void spawnCategoryForPosition(MobCategory group, ServerLevel world, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) { ++ spawnCategoryForPosition(group, world,chunk, pos, checker, runner, Integer.MAX_VALUE, null); ++ } ++ public static void spawnCategoryForPosition(MobCategory group, ServerLevel world, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner, int maxSpawns, Consumer trackEntity) { ++ // Paper end - Optional per player mob spawns + StructureManager structuremanager = world.structureManager(); + ChunkGenerator chunkgenerator = world.getChunkSource().getGenerator(); + int i = pos.getY(); +@@ -268,9 +320,14 @@ public final class NaturalSpawner { + ++j; + ++k1; + runner.run(entityinsentient, chunk); ++ // Paper start - Optional per player mob spawns ++ if (trackEntity != null) { ++ trackEntity.accept(entityinsentient); ++ } ++ // Paper end - Optional per player mob spawns + } + // CraftBukkit end +- if (j >= entityinsentient.getMaxSpawnClusterSize()) { ++ if (j >= entityinsentient.getMaxSpawnClusterSize() || j >= maxSpawns) { // Paper - Optional per player mob spawns + return; + } + +@@ -543,7 +600,7 @@ public final class NaturalSpawner { + MobCategory enumcreaturetype = entitytypes.getCategory(); + + this.mobCategoryCounts.addTo(enumcreaturetype, 1); +- this.localMobCapCalculator.addMob(new ChunkPos(blockposition), enumcreaturetype); ++ if (this.localMobCapCalculator != null) this.localMobCapCalculator.addMob(new ChunkPos(blockposition), enumcreaturetype); // Paper - Optional per player mob spawns + } + + public int getSpawnableChunkCount() { diff --git a/patches/server/1052-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch b/patches/server/1052-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch new file mode 100644 index 0000000000..e1710b40b0 --- /dev/null +++ b/patches/server/1052-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch @@ -0,0 +1,89 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: kickash32 +Date: Mon, 5 Apr 2021 01:42:35 -0400 +Subject: [PATCH] Improve cancelling PreCreatureSpawnEvent with per player mob + spawns + + +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 094096bd08450e5d656ce2c442757cbc63ffb090..cfeeddf2cb4ff50dbc29c6913e78ca1dee076790 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -246,8 +246,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + ++(backingSet[i].mobCounts[index]); + } + } ++ // Paper start - per player mob count backoff ++ public void updateFailurePlayerMobTypeMap(int chunkX, int chunkZ, net.minecraft.world.entity.MobCategory mobCategory) { ++ if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { ++ return; ++ } ++ int idx = mobCategory.ordinal(); ++ final ca.spottedleaf.moonrise.common.list.ReferenceList inRange = ++ this.level.moonrise$getNearbyPlayers().getPlayersByChunk(chunkX, chunkZ, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE); ++ if (inRange == null) { ++ return; ++ } ++ final ServerPlayer[] backingSet = inRange.getRawDataUnchecked(); ++ for (int i = 0, len = inRange.size(); i < len; i++) { ++ ++(backingSet[i].mobBackoffCounts[idx]); ++ } ++ } ++ // Paper end - per player mob count backoff + public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) { +- return player.mobCounts[mobCategory.ordinal()]; ++ return player.mobCounts[mobCategory.ordinal()] + player.mobBackoffCounts[mobCategory.ordinal()]; // Paper - per player mob count backoff + // Paper end - Optional per player mob spawns + } + // Paper end +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index 8e96905fa93b02623f16feb4369a45b175031ebf..d021cd5b6136f0125076513977f430c6d4dd4f9f 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -555,7 +555,17 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled + // re-set mob counts + for (ServerPlayer player : this.level.players) { +- Arrays.fill(player.mobCounts, 0); ++ // Paper start - per player mob spawning backoff ++ for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) { ++ player.mobCounts[ii] = 0; ++ ++ int newBackoff = player.mobBackoffCounts[ii] - 1; // TODO make configurable bleed // TODO use nonlinear algorithm? ++ if (newBackoff < 0) { ++ newBackoff = 0; ++ } ++ player.mobBackoffCounts[ii] = newBackoff; ++ } ++ // Paper end - per player mob spawning backoff + } + spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); + } else { +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 1ac3f820d2f7c1bd29e2f2a323747f8262a57d89..a2cd4385fca0cf2ec164d06be2732755506c0249 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -305,6 +305,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple + public static final int MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length; + public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper + // Paper end - Optional per player mob spawns ++ public final int[] mobBackoffCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper - per player mob count backoff + + // CraftBukkit start + public CraftPlayer.TransferCookieConnection transferCookieConnection; +diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +index 12ebd7829c7f6814ccd79ae96aa9023afcc64696..c1b76a1ebc1eea7ab70cf61d8175a31794dd122a 100644 +--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java ++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +@@ -299,6 +299,11 @@ public final class NaturalSpawner { + + // Paper start - PreCreatureSpawnEvent + PreSpawnStatus doSpawning = isValidSpawnPostitionForType(world, group, structuremanager, chunkgenerator, biomesettingsmobs_c, blockposition_mutableblockposition, d2); ++ // Paper start - per player mob count backoff ++ if (doSpawning == PreSpawnStatus.ABORT || doSpawning == PreSpawnStatus.CANCELLED) { ++ world.getChunkSource().chunkMap.updateFailurePlayerMobTypeMap(blockposition_mutableblockposition.getX() >> 4, blockposition_mutableblockposition.getZ() >> 4, group); ++ } ++ // Paper end - per player mob count backoff + if (doSpawning == PreSpawnStatus.ABORT) { + return; + } diff --git a/patches/server/1053-Avoid-issues-with-certain-tasks-not-processing-durin.patch b/patches/server/1053-Avoid-issues-with-certain-tasks-not-processing-durin.patch new file mode 100644 index 0000000000..e52ca4da86 --- /dev/null +++ b/patches/server/1053-Avoid-issues-with-certain-tasks-not-processing-durin.patch @@ -0,0 +1,46 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> +Date: Sun, 27 Oct 2024 14:18:28 -0700 +Subject: [PATCH] Avoid issues with certain tasks not processing during sleep + +Execute processQueue tasks during sleep: needed for console tab completions, pre join event, etc. + +Upstream has set precedent that the bukkit scheduler will still tick during sleep, which avoids some problems +with plugins not accounting for the new sleep feature, but can still lead to others. Because of this we have disabled +sleep by default, which avoids the problem and makes it more obvious to check if this is the cause of issues when +enabled. We also unload chunks during sleep to prevent memory leaks caused by plugin chunk loads. + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index b5c5e9d9279e61e2476319e9ce8a829743d56267..6a4f99c56f8f49f5087a582a8c77be2c261537bb 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -1638,6 +1638,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop true, false); ++ } ++ // Paper end - avoid issues with certain tasks not processing during sleep + this.server.spark.executeMainThreadTasks(); // Paper - spark + this.tickConnection(); + this.server.spark.tickEnd(((double)(System.nanoTime() - lastTick) / 1000000D)); // Paper - spark +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java +index a2633780619d73c29a23cb8b6a208ca9ba549fb0..c3ec370b83b895be0f03662e3884fa4a2442a2a6 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java +@@ -160,7 +160,7 @@ public class DedicatedServerProperties extends Settings +Date: Sun, 27 Oct 2024 12:36:53 -0700 +Subject: [PATCH] Allow using old ender pearl behavior + +When enabled, ender pearls will not load chunks and will save to the world instead of the player. + +== AT == +public net.minecraft.world.entity.projectile.Projectile cachedOwner + +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index a2cd4385fca0cf2ec164d06be2732755506c0249..8aff5b7dd14b835788348b22b1fec4d381df816f 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -834,6 +834,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple + + while (iterator.hasNext()) { + ThrownEnderpearl entityenderpearl = (ThrownEnderpearl) iterator.next(); ++ if (entityenderpearl.level().paperConfig().misc.legacyEnderPearlBehavior) continue; // Paper - Allow using old ender pearl behavior + + if (entityenderpearl.isRemoved()) { + ServerPlayer.LOGGER.warn("Trying to save removed ender pearl, skipping"); +@@ -3123,7 +3124,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple + } + + public static long placeEnderPearlTicket(ServerLevel world, ChunkPos chunkPos) { +- world.getChunkSource().addRegionTicket(TicketType.ENDER_PEARL, chunkPos, 2, chunkPos); ++ if (!world.paperConfig().misc.legacyEnderPearlBehavior) world.getChunkSource().addRegionTicket(TicketType.ENDER_PEARL, chunkPos, 2, chunkPos); // Paper - Allow using old ender pearl behavior + return TicketType.ENDER_PEARL.timeout(); + } + +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 8de23b39806734c9a413b6d98dbfff25888c1798..1a956249828156fdc273888de59128c3d1a0b898 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -602,7 +602,13 @@ public abstract class PlayerList { + while (iterator.hasNext()) { + ThrownEnderpearl entityenderpearl = (ThrownEnderpearl) iterator.next(); + ++ // Paper start - Allow using old ender pearl behavior ++ if (!entityenderpearl.level().paperConfig().misc.legacyEnderPearlBehavior) { + entityenderpearl.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER, EntityRemoveEvent.Cause.PLAYER_QUIT); // CraftBukkit - add Bukkit remove cause ++ } else { ++ entityenderpearl.cachedOwner = null; ++ } ++ // Paper end - Allow using old ender pearl behavior + } + + worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +index 5f790dd24f2bdae827c6dc597064b9b265089751..bd2684528157f928460f2143dd71a48e11983123 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +@@ -252,7 +252,7 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + Entity entity = super.teleport(teleportTarget); + + if (entity != null) { +- entity.placePortalTicket(BlockPos.containing(entity.position())); ++ if (!this.level().paperConfig().misc.legacyEnderPearlBehavior) entity.placePortalTicket(BlockPos.containing(entity.position())); // Paper - Allow using old ender pearl behavior + } + + return entity; diff --git a/patches/server/1055-Block-Enderpearl-Travel-Exploit.patch b/patches/server/1055-Block-Enderpearl-Travel-Exploit.patch new file mode 100644 index 0000000000..ca3570dc03 --- /dev/null +++ b/patches/server/1055-Block-Enderpearl-Travel-Exploit.patch @@ -0,0 +1,49 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Mon, 30 Apr 2018 17:15:26 -0400 +Subject: [PATCH] Block Enderpearl Travel Exploit + +Players are able to use alt accounts and enderpearls to travel +long distances utilizing the pearls in unloaded chunks and loading +the chunk later when convenient. + +This disables that by not saving the thrower when the chunk is unloaded. + +This is mainly useful for survival servers that do not allow freeform teleporting. + +Note: Currently removed as enderpearls are ticked as long as their owner is online in 1.21.2. +Might be worth to re-add once an option to disable the above vanilla mechanic is added, to +fully prevent enderpearl travel exploits. + +== AT == +public net.minecraft.world.entity.projectile.Projectile ownerUUID + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 020ef251fd3d03c9e8fb9fc859d4ef9fc04cd3ba..17ddec036cb6135c7489efbd76121304e76c32c0 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -2678,6 +2678,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + public void onTickingEnd(Entity entity) { + ServerLevel.this.entityTickList.remove(entity); ++ // Paper start - Reset pearls when they stop being ticked ++ if (ServerLevel.this.paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && ServerLevel.this.paperConfig().misc.legacyEnderPearlBehavior && entity instanceof net.minecraft.world.entity.projectile.ThrownEnderpearl pearl) { ++ pearl.cachedOwner = null; ++ pearl.ownerUUID = null; ++ } ++ // Paper end - Reset pearls when they stop being ticked + } + + public void onTrackingStart(Entity entity) { +diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +index 4c71642474d97d1943db302947a4566a326b9ac3..9a7b56b653848974e1194eb4f6d40cb99a96ff57 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +@@ -134,6 +134,7 @@ public abstract class Projectile extends Entity implements TraceableEntity { + protected void readAdditionalSaveData(CompoundTag nbt) { + if (nbt.hasUUID("Owner")) { + this.setOwnerThroughUUID(nbt.getUUID("Owner")); ++ if (this instanceof ThrownEnderpearl && this.level() != null && this.level().paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && this.level().paperConfig().misc.legacyEnderPearlBehavior) { this.ownerUUID = null; } // Paper - Reset pearls when they stop being ticked; Don't store shooter name for pearls to block enderpearl travel exploit + } + + this.leftOwner = nbt.getBoolean("LeftOwner"); diff --git a/patches/server/1056-Fix-inconsistencies-in-dispense-events-regarding-sta.patch b/patches/server/1056-Fix-inconsistencies-in-dispense-events-regarding-sta.patch new file mode 100644 index 0000000000..564f0bb367 --- /dev/null +++ b/patches/server/1056-Fix-inconsistencies-in-dispense-events-regarding-sta.patch @@ -0,0 +1,432 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Sun, 11 Dec 2022 23:47:22 -0800 +Subject: [PATCH] Fix inconsistencies in dispense events regarding stack size + +The javadocs for BlockDispenseEvent suggest the ItemStack is a single +item which is being dispensed. Before this fix, sometimes it was the whole +stack before a single item had been taken. This fixes that so the stack size +is always 1. + +diff --git a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java +index dff30954e4c588ee4cc79d3f6dab6fb456934d65..ddb264443f2e38b6348226016f9139727c588898 100644 +--- a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java +@@ -49,7 +49,7 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior { + } + + // CraftBukkit start +- ItemStack itemstack1 = stack.split(1); ++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink at end and single item in event + org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos()); + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + +@@ -59,12 +59,13 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior { + } + + if (event.isCancelled()) { +- stack.grow(1); ++ // stack.grow(1); // Paper - shrink below + return stack; + } + ++ boolean shrink = true; // Paper + if (!event.getItem().equals(craftItem)) { +- stack.grow(1); ++ shrink = false; // Paper - shrink below + // Chain to handler for new item + ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); + DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior +@@ -80,8 +81,7 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior { + abstractboat.setInitialPos(event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ()); // CraftBukkit + EntityType.createDefaultStackConfig(worldserver, stack, (Player) null).accept(abstractboat); + abstractboat.setYRot(enumdirection.toYRot()); +- if (!worldserver.addFreshEntity(abstractboat)) stack.grow(1); // CraftBukkit +- // itemstack.shrink(1); // CraftBukkit - handled during event processing ++ if (worldserver.addFreshEntity(abstractboat) && shrink) stack.shrink(1); // Paper - if entity add was successful and supposed to shrink + } + + return stack; +diff --git a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +index 9246ec011c7d94618c0aa73792d1bef8f447c88c..c81050f58a8c75f7f3b16ab466d8d87edd83ea31 100644 +--- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +@@ -106,7 +106,7 @@ public interface DispenseItemBehavior { + + // CraftBukkit start + ServerLevel worldserver = pointer.level(); +- ItemStack itemstack1 = stack.split(1); ++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event + org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos()); + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + +@@ -116,12 +116,13 @@ public interface DispenseItemBehavior { + } + + if (event.isCancelled()) { +- stack.grow(1); ++ // stack.grow(1); // Paper - shrink below + return stack; + } + ++ boolean shrink = true; // Paper + if (!event.getItem().equals(craftItem)) { +- stack.grow(1); ++ shrink = false; // Paper - shrink below + // Chain to handler for new item + ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); + DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior +@@ -142,7 +143,7 @@ public interface DispenseItemBehavior { + return ItemStack.EMPTY; + } + +- // itemstack.shrink(1); // Handled during event processing ++ if (shrink) stack.shrink(1); // Paper - actually handle here + // CraftBukkit end + pointer.level().gameEvent((Entity) null, (Holder) GameEvent.ENTITY_PLACE, pointer.pos()); + return stack; +@@ -164,7 +165,7 @@ public interface DispenseItemBehavior { + ServerLevel worldserver = pointer.level(); + + // CraftBukkit start +- ItemStack itemstack1 = stack.split(1); ++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event + org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos()); + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + +@@ -174,12 +175,13 @@ public interface DispenseItemBehavior { + } + + if (event.isCancelled()) { +- stack.grow(1); ++ // stack.grow(1); // Paper - shrink below + return stack; + } + ++ boolean shrink = true; // Paper + if (!event.getItem().equals(craftItem)) { +- stack.grow(1); ++ shrink = false; // Paper - shrink below + // Chain to handler for new item + ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); + DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior +@@ -197,7 +199,7 @@ public interface DispenseItemBehavior { + ArmorStand entityarmorstand = (ArmorStand) EntityType.ARMOR_STAND.spawn(worldserver, consumer, blockposition, EntitySpawnReason.DISPENSER, false, false); + + if (entityarmorstand != null) { +- // itemstack.shrink(1); // CraftBukkit - Handled during event processing ++ if (shrink) stack.shrink(1); // Paper - actually handle here + } + + return stack; +@@ -217,7 +219,7 @@ public interface DispenseItemBehavior { + + if (!list.isEmpty()) { + // CraftBukkit start +- ItemStack itemstack1 = stack.split(1); ++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event + ServerLevel world = pointer.level(); + org.bukkit.block.Block block = CraftBlock.at(world, pointer.pos()); + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); +@@ -228,12 +230,13 @@ public interface DispenseItemBehavior { + } + + if (event.isCancelled()) { +- stack.grow(1); ++ // stack.grow(1); // Paper - shrink below + return stack; + } + ++ boolean shrink = true; // Paper + if (!event.getItem().equals(craftItem)) { +- stack.grow(1); ++ shrink = false; // Paper - shrink below + // Chain to handler for new item + ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); + DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior +@@ -244,6 +247,7 @@ public interface DispenseItemBehavior { + } + ((Saddleable) list.get(0)).equipSaddle(CraftItemStack.asNMSCopy(event.getItem()), SoundSource.BLOCKS); // Paper - track changed items in dispense event + // CraftBukkit end ++ if (shrink) stack.shrink(1); // Paper - actually handle here + this.setSuccess(true); + return stack; + } else { +@@ -270,7 +274,7 @@ public interface DispenseItemBehavior { + entityhorsechestedabstract = (AbstractChestedHorse) iterator1.next(); + // CraftBukkit start + } while (!entityhorsechestedabstract.isTamed()); +- ItemStack itemstack1 = stack.split(1); ++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below + ServerLevel world = pointer.level(); + org.bukkit.block.Block block = CraftBlock.at(world, pointer.pos()); + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); +@@ -281,10 +285,13 @@ public interface DispenseItemBehavior { + } + + if (event.isCancelled()) { ++ // stack.grow(1); // Paper - shrink below (this was actually missing and should be here, added it commented out to be consistent) + return stack; + } + ++ boolean shrink = true; // Paper + if (!event.getItem().equals(craftItem)) { ++ shrink = false; // Paper - shrink below + // Chain to handler for new item + ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); + DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior +@@ -296,7 +303,7 @@ public interface DispenseItemBehavior { + entityhorsechestedabstract.getSlot(499).set(CraftItemStack.asNMSCopy(event.getItem())); + // CraftBukkit end + +- // itemstack.shrink(1); // CraftBukkit - handled above ++ if (shrink) stack.shrink(1); // Paper - actually handle here + this.setSuccess(true); + return stack; + } +@@ -344,7 +351,7 @@ public interface DispenseItemBehavior { + if (willEmptyContentsSolidBucketItem || willEmptyBucketItem) { + // Paper end - correctly check if the bucket place will succeed + org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos()); +- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); ++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(x, y, z)); + if (!DispenserBlock.eventFired) { +@@ -409,7 +416,7 @@ public interface DispenseItemBehavior { + + // CraftBukkit start + org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos()); +- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); ++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); + if (!DispenserBlock.eventFired) { +@@ -447,7 +454,7 @@ public interface DispenseItemBehavior { + + // CraftBukkit start + org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos()); +- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); ++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); + if (!DispenserBlock.eventFired) { +@@ -509,7 +516,7 @@ public interface DispenseItemBehavior { + BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING)); + // CraftBukkit start + org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos()); +- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); ++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); + if (!DispenserBlock.eventFired) { +@@ -576,7 +583,7 @@ public interface DispenseItemBehavior { + // CraftBukkit start + // EntityTNTPrimed entitytntprimed = new EntityTNTPrimed(worldserver, (double) blockposition.getX() + 0.5D, (double) blockposition.getY(), (double) blockposition.getZ() + 0.5D, (EntityLiving) null); + +- ItemStack itemstack1 = stack.split(1); ++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink at end and single item in event + org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos()); + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + +@@ -586,12 +593,13 @@ public interface DispenseItemBehavior { + } + + if (event.isCancelled()) { +- stack.grow(1); ++ // stack.grow(1); // Paper - shrink below + return stack; + } + ++ boolean shrink = true; // Paper + if (!event.getItem().equals(craftItem)) { +- stack.grow(1); ++ shrink = false; // Paper - shrink below + // Chain to handler for new item + ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); + DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior +@@ -607,7 +615,7 @@ public interface DispenseItemBehavior { + worldserver.addFreshEntity(entitytntprimed); + worldserver.playSound((Player) null, entitytntprimed.getX(), entitytntprimed.getY(), entitytntprimed.getZ(), SoundEvents.TNT_PRIMED, SoundSource.BLOCKS, 1.0F, 1.0F); + worldserver.gameEvent((Entity) null, (Holder) GameEvent.ENTITY_PLACE, blockposition); +- // itemstack.shrink(1); // CraftBukkit - handled above ++ if (shrink) stack.shrink(1); // Paper - actually handle here + return stack; + } + }); +@@ -620,7 +628,7 @@ public interface DispenseItemBehavior { + + // CraftBukkit start + org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos()); +- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); ++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); + if (!DispenserBlock.eventFired) { +@@ -669,7 +677,7 @@ public interface DispenseItemBehavior { + + // CraftBukkit start + org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos()); +- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); ++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); + if (!DispenserBlock.eventFired) { +@@ -731,7 +739,7 @@ public interface DispenseItemBehavior { + + // CraftBukkit start + org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos()); +- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); ++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - only single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); + if (!DispenserBlock.eventFired) { +@@ -813,7 +821,7 @@ public interface DispenseItemBehavior { + ItemStack itemstack1 = stack; + ServerLevel world = pointer.level(); + org.bukkit.block.Block block = CraftBlock.at(world, pointer.pos()); +- CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); ++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); // Paper - ignore stack size on damageable items + + BlockDispenseEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) list.get(0).getBukkitEntity()); + if (!DispenserBlock.eventFired) { +diff --git a/src/main/java/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java +index a43ea83dbbd5946096cdde31af766674bda6c3be..bf8c511739265c6a9cd277752e844481598f8966 100644 +--- a/src/main/java/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java +@@ -42,7 +42,7 @@ public class EquipmentDispenseItemBehavior extends DefaultDispenseItemBehavior { + } else { + LivingEntity entityliving = (LivingEntity) list.getFirst(); + EquipmentSlot enumitemslot = entityliving.getEquipmentSlotForItem(stack); +- ItemStack itemstack1 = stack.split(1); ++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event + + // CraftBukkit start + Level world = pointer.level(); +@@ -55,12 +55,13 @@ public class EquipmentDispenseItemBehavior extends DefaultDispenseItemBehavior { + } + + if (event.isCancelled()) { +- stack.grow(1); ++ // stack.grow(1); // Paper - shrink below + return false; + } + ++ boolean shrink = true; // Paper + if (!event.getItem().equals(craftItem)) { +- stack.grow(1); ++ shrink = false; // Paper - shrink below + // Chain to handler for new item + ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); + DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior +@@ -79,6 +80,7 @@ public class EquipmentDispenseItemBehavior extends DefaultDispenseItemBehavior { + entityinsentient.setPersistenceRequired(); + } + ++ if (shrink) stack.shrink(1); // Paper - shrink here + return true; + } + } +diff --git a/src/main/java/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java +index aae9ec8f3bd39685b37251bef3f9ac846d65c192..3588896b7413be73ade6b3f8fd111d02c48ec550 100644 +--- a/src/main/java/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java +@@ -69,7 +69,7 @@ public class MinecartDispenseItemBehavior extends DefaultDispenseItemBehavior { + Vec3 vec3d1 = new Vec3(d0, d1 + d3, d2); + // CraftBukkit start + // EntityMinecartAbstract entityminecartabstract = EntityMinecartAbstract.createMinecart(worldserver, vec3d1.x, vec3d1.y, vec3d1.z, this.entityType, EntitySpawnReason.DISPENSER, itemstack, (EntityHuman) null); +- ItemStack itemstack1 = stack.split(1); ++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event + org.bukkit.block.Block block2 = CraftBlock.at(worldserver, pointer.pos()); + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + +@@ -79,12 +79,13 @@ public class MinecartDispenseItemBehavior extends DefaultDispenseItemBehavior { + } + + if (event.isCancelled()) { +- stack.grow(1); ++ // stack.grow(1); // Paper - shrink below + return stack; + } + ++ boolean shrink = true; // Paper + if (!event.getItem().equals(craftItem)) { +- stack.grow(1); ++ shrink = false; // Paper - shrink below + // Chain to handler for new item + ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); + DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior +@@ -98,8 +99,7 @@ public class MinecartDispenseItemBehavior extends DefaultDispenseItemBehavior { + AbstractMinecart entityminecartabstract = AbstractMinecart.createMinecart(worldserver, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), this.entityType, EntitySpawnReason.DISPENSER, itemstack1, (Player) null); + + if (entityminecartabstract != null) { +- if (!worldserver.addFreshEntity(entityminecartabstract)) stack.grow(1); +- // itemstack.shrink(1); // CraftBukkit - handled during event processing ++ if (worldserver.addFreshEntity(entityminecartabstract) && shrink) stack.shrink(1); // Paper - if entity add was successful and supposed to shrink + // CraftBukkit end + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java +index 281439e430fb8e587549da783bdd93432f8f957f..54c72cf472e06e214eb61bd8615a0bb27690c807 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java +@@ -38,7 +38,7 @@ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior { + + // CraftBukkit start + // IProjectile.spawnProjectileUsingShoot(this.projectileItem.asProjectile(worldserver, iposition, itemstack, enumdirection), worldserver, itemstack, (double) enumdirection.getStepX(), (double) enumdirection.getStepY(), (double) enumdirection.getStepZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty()); // CraftBukkit - call when finish the BlockDispenseEvent +- ItemStack itemstack1 = stack.split(1); ++ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event + org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos()); + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + +@@ -48,12 +48,13 @@ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior { + } + + if (event.isCancelled()) { +- stack.grow(1); ++ // stack.grow(1); // Paper - shrink below + return stack; + } + ++ boolean shrink = true; // Paper + if (!event.getItem().equals(craftItem)) { +- stack.grow(1); ++ shrink = false; // Paper - shrink below + // Chain to handler for new item + ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); + DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior +@@ -68,7 +69,7 @@ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior { + Projectile iprojectile = Projectile.spawnProjectileUsingShoot(this.projectileItem.asProjectile(worldserver, iposition, CraftItemStack.unwrap(event.getItem()), enumdirection), worldserver, itemstack1, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty()); // Paper - track changed items in the dispense event; unwrap is safe here because all uses of the stack make their own copies + iprojectile.projectileSource = new org.bukkit.craftbukkit.projectiles.CraftBlockProjectileSource(pointer.blockEntity()); + } +- // itemstack.shrink(1); // CraftBukkit - Handled during event processing ++ if (shrink) stack.shrink(1); // Paper - actually handle here + // CraftBukkit end + return stack; + } +diff --git a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +index afad4fa3ca1a3186c4569ea073f776dac16817e1..65ed3d77a51b8299517e0c165403b0c5ac413475 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +@@ -38,7 +38,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { + ServerLevel worldserver = pointer.level(); + // CraftBukkit start + org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos()); +- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); ++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); + if (!DispenserBlock.eventFired) { +diff --git a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java +index 16b435216dc7c6a3f8c1c0f9e2323e6afb3a6cb9..8f9fde5489c0e1d0a91203536caddec5a9c96f6c 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java +@@ -34,7 +34,7 @@ public class ShulkerBoxDispenseBehavior extends OptionalDispenseItemBehavior { + + // CraftBukkit start + org.bukkit.block.Block bukkitBlock = CraftBlock.at(pointer.level(), pointer.pos()); +- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); ++ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); + if (!DispenserBlock.eventFired) { diff --git a/patches/server/1057-Correct-update-cursor.patch b/patches/server/1057-Correct-update-cursor.patch new file mode 100644 index 0000000000..1ae9d46f9c --- /dev/null +++ b/patches/server/1057-Correct-update-cursor.patch @@ -0,0 +1,42 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Bjarne Koll +Date: Fri, 1 Nov 2024 14:58:57 +0100 +Subject: [PATCH] Correct update cursor + +Spigot uses a no longer valid ClientboundContainerSetSlotPacket with the +slot -1, which would update the carried stack in versions <=1.21.1 but +now leads to an IOOB. +1.21.2 instead introduced the ClientboundSetCursorItemPacket, which this +patch uses instead. + +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index a3d8d5d735272ae2a67536e4d5bbcdb5d2e4bf8b..cdee55bece5e64a88051ecc0c43f446b50076ed3 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -3353,7 +3353,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + case PLACE_SOME: + case PLACE_ONE: + case SWAP_WITH_CURSOR: +- this.player.connection.send(new ClientboundContainerSetSlotPacket(-1, -1, this.player.inventoryMenu.incrementStateId(), this.player.containerMenu.getCarried())); ++ this.player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCursorItemPacket(this.player.containerMenu.getCarried().copy())); // Paper - correctly set cursor + this.player.connection.send(new ClientboundContainerSetSlotPacket(this.player.containerMenu.containerId, this.player.inventoryMenu.incrementStateId(), packet.getSlotNum(), this.player.containerMenu.getSlot(packet.getSlotNum()).getItem())); + break; + // Modified clicked only +@@ -3365,7 +3365,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + case DROP_ALL_CURSOR: + case DROP_ONE_CURSOR: + case CLONE_STACK: +- this.player.connection.send(new ClientboundContainerSetSlotPacket(-1, -1, this.player.inventoryMenu.incrementStateId(), this.player.containerMenu.getCarried())); ++ this.player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCursorItemPacket(this.player.containerMenu.getCarried().copy())); // Paper - correctly set cursor + break; + // Nothing + case NOTHING: +@@ -3543,7 +3543,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + // Reset the slot + if (packet.slotNum() >= 0) { + this.player.connection.send(new ClientboundContainerSetSlotPacket(this.player.inventoryMenu.containerId, this.player.inventoryMenu.incrementStateId(), packet.slotNum(), this.player.inventoryMenu.getSlot(packet.slotNum()).getItem())); +- this.player.connection.send(new ClientboundContainerSetSlotPacket(-1, this.player.inventoryMenu.incrementStateId(), -1, ItemStack.EMPTY)); ++ this.player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCursorItemPacket(ItemStack.EMPTY.copy())); // Paper - correctly set cursor + } + return; + } diff --git a/patches/server/1058-Call-CraftPlayer-onEntityRemove-for-all-online-playe.patch b/patches/server/1058-Call-CraftPlayer-onEntityRemove-for-all-online-playe.patch new file mode 100644 index 0000000000..a03d8a4094 --- /dev/null +++ b/patches/server/1058-Call-CraftPlayer-onEntityRemove-for-all-online-playe.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Gero +Date: Sat, 9 Nov 2024 22:27:58 +0100 +Subject: [PATCH] Call CraftPlayer#onEntityRemove for all online players + + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 17ddec036cb6135c7489efbd76121304e76c32c0..7e8713373315eebf57541f8afe10902681449ad9 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -2800,7 +2800,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + // CraftBukkit start + entity.valid = false; + if (!(entity instanceof ServerPlayer)) { +- for (ServerPlayer player : ServerLevel.this.players) { ++ for (ServerPlayer player : ServerLevel.this.server.getPlayerList().players) { // Paper - call onEntityRemove for all online players + player.getBukkitEntity().onEntityRemove(entity); + } + } diff --git a/patches/server/1059-Eigencraft-redstone-implementation.patch b/patches/server/1059-Eigencraft-redstone-implementation.patch new file mode 100644 index 0000000000..85700695b3 --- /dev/null +++ b/patches/server/1059-Eigencraft-redstone-implementation.patch @@ -0,0 +1,1099 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: theosib +Date: Thu, 27 Sep 2018 01:43:35 -0600 +Subject: [PATCH] Eigencraft redstone implementation + +Author: theosib + +Original license: MIT + +This patch implements theosib's redstone algorithms to completely overhaul the way redstone works. +The new algorithms should be many times faster than current vanilla ones. +From the original author's comments, it looks like it shouldn't interfere with any redstone save for very extreme edge-cases. + +Surprisingly, not a lot was touched aside from a few obfuscation helpers and BlockRedstoneWire. +A lot of this code is self-contained in a helper class. + +Aside from making the obvious class/function renames and obfhelpers I didn't need to modify much. +Just added Bukkit's event system and took a few liberties with dead code and comment misspellings. + +Feature patch + +== AT == +public net.minecraft.world.level.block.RedStoneWireBlock shouldSignal +public net.minecraft.world.level.block.RedStoneWireBlock canSurvive(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/LevelReader;Lnet/minecraft/core/BlockPos;)Z + +Co-authored-by: egg82 + +diff --git a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e7d510af3e415064fd483f0220d5f6a4cd0b9f63 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java +@@ -0,0 +1,961 @@ ++package com.destroystokyo.paper.util; ++ ++import java.util.List; ++import java.util.Map; ++import java.util.concurrent.ThreadLocalRandom; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Direction; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.item.Items; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.RedStoneWireBlock; ++import net.minecraft.world.level.block.state.BlockState; ++import org.bukkit.craftbukkit.block.CraftBlock; ++import org.bukkit.event.block.BlockRedstoneEvent; ++ ++import com.google.common.collect.Lists; ++import com.google.common.collect.Maps; ++ ++/** ++ * Used for the faster redstone algorithm. ++ * Original author: theosib ++ * Original license: MIT ++ * ++ * Ported to Paper and updated to 1.13 by egg82 ++ */ ++public class RedstoneWireTurbo { ++ /* ++ * This is Helper class for BlockRedstoneWire. It implements a minimally-invasive ++ * bolt-on accelerator that performs a breadth-first search through redstone wire blocks ++ * in order to more efficiently and deterministically compute new redstone wire power levels ++ * and determine the order in which other blocks should be updated. ++ * ++ * Features: ++ * - Changes to BlockRedstoneWire are very limited, no other classes are affected, and the ++ * choice between old and new redstone wire update algorithms is switchable on-line. ++ * - The vanilla implementation relied on World.notifyNeighborsOfStateChange for redstone ++ * wire blocks to communicate power level changes to each other, generating 36 block ++ * updates per call. This improved implementation propagates power level changes directly ++ * between redstone wire blocks. Redstone wire power levels are therefore computed more quickly, ++ * and block updates are sent only to non-redstone blocks, many of which may perform an ++ * action when informed of a change in redstone power level. (Note: Block updates are not ++ * the same as state changes to redstone wire. Wire block states are updated as soon ++ * as they are computed.) ++ * - Of the 36 block updates generated by a call to World.notifyNeighborsOfStateChange, ++ * 12 of them are obviously redundant (e.g. the west neighbor of the east neighbor). ++ * These are eliminated. ++ * - Updates to redstone wire and other connected blocks are propagated in a breath-first ++ * manner, radiating out from the initial trigger (a block update to a redstone wire ++ * from something other than redstone wire). ++ * - Updates are scheduled both deterministically and in an intuitive order, addressing bug ++ * MC-11193. ++ * - All redstone behavior that used to be locational now works the same in all locations. ++ * - All behaviors of redstone wire that used to be orientational now work the same in all ++ * orientations, as long as orientation can be determined; random otherwise. Some other ++ * redstone components still update directionally (e.g. switches), and this code can't ++ * compensate for that. ++ * - Information that is otherwise computed over and over again or which is expensive to ++ * to compute is cached for faster lookup. This includes coordinates of block position ++ * neighbors and block states that won't change behind our backs during the execution of ++ * this search algorithm. ++ * - Redundant block updates (both to redstone wire and to other blocks) are heavily ++ * consolidated. For worst-case scenarios (depowering of redstone wire) this results ++ * in a reduction of block updates by as much as 95% (factor of 1/21). Due to overheads, ++ * empirical testing shows a speedup better than 10x. This addresses bug MC-81098. ++ * ++ * Extensive testing has been performed to ensure that existing redstone contraptions still ++ * behave as expected. Results of early testing that identified undesirable behavior changes ++ * were addressed. Additionally, real-time performance testing revealed compute inefficiencies ++ * With earlier implementations of this accelerator. Some compatibility adjustments and ++ * performance optimizations resulted in harmless increases in block updates above the ++ * theoretical minimum. ++ * ++ * Only a single redstone machine was found to break: An instant dropper line hack that ++ * relies on powered rails and quasi-connectivity but doesn't work in all directions. The ++ * replacement is to lay redstone wire directly on top of the dropper line, which now works ++ * reliably in any direction. ++ * ++ * There are numerous other optimization that can be made, but those will be provided later in ++ * separate updates. This version is designed to be minimalistic. ++ * ++ * Many thanks to the following individuals for their help in testing this functionality: ++ * - pokechu22, _MethodZz_, WARBEN, NarcolepticFrog, CommandHelper (nessie), ilmango, ++ * OreoLamp, Xcom6000, tryashtar, RedCMD, Smokey95Dog, EDDxample, Rays Works, ++ * Nodnam, BlockyPlays, Grumm, NeunEinser, HelVince. ++ */ ++ ++ /* Reference to BlockRedstoneWire object, which uses this accelerator */ ++ private final RedStoneWireBlock wire; ++ ++ /* ++ * Implementation: ++ * ++ * RedstoneWire Blocks are updated in concentric rings or "layers" radiating out from the ++ * initial block update that came from a call to BlockRedstoneWire.neighborChanged(). ++ * All nodes put in Layer N are those with Manhattan distance N from the trigger ++ * position, reachable through connected redstone wire blocks. ++ * ++ * Layer 0 represents the trigger block position that was input to neighborChanged. ++ * Layer 1 contains the immediate neighbors of that position. ++ * Layer N contains the neighbors of blocks in layer N-1, not including ++ * those in previous layers. ++ * ++ * Layers enforce an update order that is a function of Manhattan distance ++ * from the initial coordinates input to neighborChanged. The same ++ * coordinates may appear in multiple layers, but redundant updates are minimized. ++ * Block updates are sent layer-by-layer. If multiple of a block's neighbors experience ++ * redstone wire changes before its layer is processed, then those updates will be merged. ++ * If a block's update has been sent, but its neighboring redstone changes ++ * after that, then another update will be sent. This preserves compatibility with ++ * machines that rely on zero-tick behavior, except that the new functionality is non- ++ * locational. ++ * ++ * Within each layer, updates are ordered left-to-right relative to the direction of ++ * information flow. This makes the implementation non-orientational. Only when ++ * this direction is ambiguous is randomness applied (intentionally). ++ */ ++ private List updateQueue0 = Lists.newArrayList(); ++ private List updateQueue1 = Lists.newArrayList(); ++ private List updateQueue2 = Lists.newArrayList(); ++ ++ public RedstoneWireTurbo(RedStoneWireBlock wire) { ++ this.wire = wire; ++ } ++ ++ /* ++ * Compute neighbors of a block. When a redstone wire value changes, previously it called ++ * World.notifyNeighborsOfStateChange. That lists immediately neighboring blocks in ++ * west, east, down, up, north, south order. For each of those neighbors, their own ++ * neighbors are updated in the same order. This generates 36 updates, but 12 of them are ++ * redundant; for instance the west neighbor of a block's east neighbor. ++ * ++ * Note that this ordering is only used to create the initial list of neighbors. Once ++ * the direction of signal flow is identified, the ordering of updates is completely ++ * reorganized. ++ */ ++ public static BlockPos[] computeAllNeighbors(final BlockPos pos) { ++ final int x = pos.getX(); ++ final int y = pos.getY(); ++ final int z = pos.getZ(); ++ final BlockPos[] n = new BlockPos[24]; ++ ++ // Immediate neighbors, in the same order as ++ // World.notifyNeighborsOfStateChange, etc.: ++ // west, east, down, up, north, south ++ n[0] = new BlockPos(x - 1, y, z); ++ n[1] = new BlockPos(x + 1, y, z); ++ n[2] = new BlockPos(x, y - 1, z); ++ n[3] = new BlockPos(x, y + 1, z); ++ n[4] = new BlockPos(x, y, z - 1); ++ n[5] = new BlockPos(x, y, z + 1); ++ ++ // Neighbors of neighbors, in the same order, ++ // except that duplicates are not included ++ n[6] = new BlockPos(x - 2, y, z); ++ n[7] = new BlockPos(x - 1, y - 1, z); ++ n[8] = new BlockPos(x - 1, y + 1, z); ++ n[9] = new BlockPos(x - 1, y, z - 1); ++ n[10] = new BlockPos(x - 1, y, z + 1); ++ n[11] = new BlockPos(x + 2, y, z); ++ n[12] = new BlockPos(x + 1, y - 1, z); ++ n[13] = new BlockPos(x + 1, y + 1, z); ++ n[14] = new BlockPos(x + 1, y, z - 1); ++ n[15] = new BlockPos(x + 1, y, z + 1); ++ n[16] = new BlockPos(x, y - 2, z); ++ n[17] = new BlockPos(x, y - 1, z - 1); ++ n[18] = new BlockPos(x, y - 1, z + 1); ++ n[19] = new BlockPos(x, y + 2, z); ++ n[20] = new BlockPos(x, y + 1, z - 1); ++ n[21] = new BlockPos(x, y + 1, z + 1); ++ n[22] = new BlockPos(x, y, z - 2); ++ n[23] = new BlockPos(x, y, z + 2); ++ return n; ++ } ++ ++ /* ++ * We only want redstone wires to update redstone wires that are ++ * immediately adjacent. Some more distant updates can result ++ * in cross-talk that (a) wastes time and (b) can make the update ++ * order unintuitive. Therefore (relative to the neighbor order ++ * computed by computeAllNeighbors), updates are not scheduled ++ * for redstone wire in those non-connecting positions. On the ++ * other hand, updates will always be sent to *other* types of blocks ++ * in any of the 24 neighboring positions. ++ */ ++ private static final boolean[] update_redstone = { ++ true, true, false, false, true, true, // 0 to 5 ++ false, true, true, false, false, false, // 6 to 11 ++ true, true, false, false, false, true, // 12 to 17 ++ true, false, true, true, false, false // 18 to 23 ++ }; ++ ++ // Internal numbering for cardinal directions ++ private static final int North = 0; ++ private static final int East = 1; ++ private static final int South = 2; ++ private static final int West = 3; ++ ++ /* ++ * These lookup tables completely remap neighbor positions into a left-to-right ++ * ordering, based on the cardinal direction that is determined to be forward. ++ * See below for more explanation. ++ */ ++ private static final int[] forward_is_north = {2, 3, 16, 19, 0, 4, 1, 5, 7, 8, 17, 20, 12, 13, 18, 21, 6, 9, 22, 14, 11, 10, 23, 15}; ++ private static final int[] forward_is_east = {2, 3, 16, 19, 4, 1, 5, 0, 17, 20, 12, 13, 18, 21, 7, 8, 22, 14, 11, 15, 23, 9, 6, 10}; ++ private static final int[] forward_is_south = {2, 3, 16, 19, 1, 5, 0, 4, 12, 13, 18, 21, 7, 8, 17, 20, 11, 15, 23, 10, 6, 14, 22, 9}; ++ private static final int[] forward_is_west = {2, 3, 16, 19, 5, 0, 4, 1, 18, 21, 7, 8, 17, 20, 12, 13, 23, 10, 6, 9, 22, 15, 11, 14}; ++ ++ /* For any orientation, we end up with the update order defined below. This order is relative to any redstone wire block ++ * that is itself having an update computed, and this center position is marked with C. ++ * - The update position marked 0 is computed first, and the one marked 23 is last. ++ * - Forward is determined by the local direction of information flow into position C from prior updates. ++ * - The first updates are scheduled for the four positions below and above C. ++ * - Then updates are scheduled for the four horizontal neighbors of C, followed by the positions below and above those neighbors. ++ * - Finally, updates are scheduled for the remaining positions with Manhattan distance 2 from C (at the same Y coordinate). ++ * - For a given horizontal distance from C, updates are scheduled starting from directly left and stepping clockwise to directly ++ * right. The remaining positions behind C are scheduled counterclockwise so as to maintain the left-to-right ordering. ++ * - If C is in layer N of the update schedule, then all 24 positions may be scheduled for layer N+1. For redstone wire, no ++ * updates are scheduled for positions that cannot directly connect. Additionally, the four positions above and below C ++ * are ALSO scheduled for layer N+2. ++ * - This update order was selected after experimenting with a number of alternative schedules, based on its compatibility ++ * with existing redstone designs and behaviors that were considered to be intuitive by various testers. WARBEN in particular ++ * made some of the most challenging test cases, but the 3-tick clocks (made by RedCMD) were also challenging to fix, ++ * along with the rail-based instant dropper line built by ilmango. Numerous others made test cases as well, including ++ * NarcolepticFrog, nessie, and Pokechu22. ++ * ++ * - The forward direction is determined locally. So when there are branches in the redstone wire, the left one will get updated ++ * before the right one. Each branch can have its own relative forward direction, resulting in the left side of a left branch ++ * having priority over the right branch of a left branch, which has priority over the left branch of a right branch, followed ++ * by the right branch of a right branch. And so forth. Since redstone power reduces to zero after a path distance of 15, ++ * that imposes a practical limit on the branching. Note that the branching is not tracked explicitly -- relative forward ++ * directions dictate relative sort order, which maintains the proper global ordering. This also makes it unnecessary to be ++ * concerned about branches meeting up with each other. ++ * ++ * ^ ++ * | ++ * Forward ++ * <-- Left Right --> ++ * ++ * 18 ++ * 10 17 5 19 11 ++ * 2 8 0 12 16 4 C 6 20 9 1 13 3 ++ * 14 21 7 23 15 ++ * Further 22 Further ++ * Down Down Up Up ++ * ++ * Backward ++ * | ++ * V ++ */ ++ ++ // This allows the above remapping tables to be looked up by cardial direction index ++ private static final int[][] reordering = { forward_is_north, forward_is_east, forward_is_south, forward_is_west }; ++ ++ /* ++ * Input: Array of UpdateNode objects in an order corresponding to the positions ++ * computed by computeAllNeighbors above. ++ * Output: Array of UpdateNode objects oriented using the above remapping tables ++ * corresponding to the identified heading (direction of information flow). ++ */ ++ private static void orientNeighbors(final UpdateNode[] src, final UpdateNode[] dst, final int heading) { ++ final int[] re = reordering[heading]; ++ for (int i = 0; i < 24; i++) { ++ dst[i] = src[re[i]]; ++ } ++ } ++ ++ /* ++ * Structure to keep track of redstone wire blocks and ++ * neighbors that will receive updates. ++ */ ++ private static class UpdateNode { ++ public static enum Type { ++ UNKNOWN, REDSTONE, OTHER ++ } ++ ++ BlockState currentState; // Keep track of redstone wire value ++ UpdateNode[] neighbor_nodes; // References to neighbors (directed graph edges) ++ BlockPos self; // UpdateNode's own position ++ BlockPos parent; // Which block pos spawned/updated this node ++ Type type = Type.UNKNOWN; // unknown, redstone wire, other type of block ++ int layer; // Highest layer this node is scheduled in ++ boolean visited; // To keep track of information flow direction, visited restone wire is marked ++ int xbias, zbias; // Remembers directionality of ancestor nodes; helps eliminate directional ambiguities. ++ } ++ ++ /* ++ * Keep track of all block positions discovered during search and their current states. ++ * We want to remember one entry for each position. ++ */ ++ private final Map nodeCache = Maps.newHashMap(); ++ ++ /* ++ * For a newly created UpdateNode object, determine what type of block it is. ++ */ ++ private void identifyNode(final Level worldIn, final UpdateNode upd1) { ++ final BlockPos pos = upd1.self; ++ final BlockState oldState = worldIn.getBlockState(pos); ++ upd1.currentState = oldState; ++ ++ // Some neighbors of redstone wire are other kinds of blocks. ++ // These need to receive block updates to inform them that ++ // redstone wire values have changed. ++ final Block block = oldState.getBlock(); ++ if (block != wire) { ++ // Mark this block as not redstone wire and therefore ++ // requiring updates ++ upd1.type = UpdateNode.Type.OTHER; ++ ++ // Non-redstone blocks may propagate updates, but those updates ++ // are not handled by this accelerator. Therefore, we do not ++ // expand this position's neighbors. ++ return; ++ } ++ ++ // One job of BlockRedstoneWire.neighborChanged is to convert ++ // redstone wires to items if the block beneath was removed. ++ // With this accelerator, BlockRedstoneWire.neighborChanged ++ // is only typically called for a single wire block, while ++ // others are processed internally by the breadth first search ++ // algorithm. To preserve this game behavior, this check must ++ // be replicated here. ++ if (!wire.canSurvive(null, worldIn, pos)) { ++ // Pop off the redstone dust ++ Block.popResource(worldIn, pos, new ItemStack(Items.REDSTONE)); // TODO ++ worldIn.removeBlock(pos, false); ++ ++ // Mark this position as not being redstone wire ++ upd1.type = UpdateNode.Type.OTHER; ++ ++ // Note: Sending updates to air blocks leads to an empty method. ++ // Testing shows this to be faster than explicitly avoiding updates to ++ // air blocks. ++ return; ++ } ++ ++ // If the above conditions fail, then this is a redstone wire block. ++ upd1.type = UpdateNode.Type.REDSTONE; ++ } ++ ++ /* ++ * Given which redstone wire blocks have been visited and not visited ++ * around the position currently being updated, compute the cardinal ++ * direction that is "forward." ++ * ++ * rx is the forward direction along the West/East axis ++ * rz is the forward direction along the North/South axis ++ */ ++ static private int computeHeading(final int rx, final int rz) { ++ // rx and rz can only take on values -1, 0, and 1, so we can ++ // compute a code number that allows us to use a single switch ++ // to determine the heading. ++ final int code = (rx + 1) + 3 * (rz + 1); ++ switch (code) { ++ case 0: { ++ // Both rx and rz are -1 (northwest) ++ // Randomly choose one to be forward. ++ final int j = ThreadLocalRandom.current().nextInt(0, 1); ++ return (j == 0) ? North : West; ++ } ++ case 1: { ++ // rx=0, rz=-1 ++ // Definitively North ++ return North; ++ } ++ case 2: { ++ // rx=1, rz=-1 (northeast) ++ // Choose randomly between north and east ++ final int j = ThreadLocalRandom.current().nextInt(0, 1); ++ return (j == 0) ? North : East; ++ } ++ case 3: { ++ // rx=-1, rz=0 ++ // Definitively West ++ return West; ++ } ++ case 4: { ++ // rx=0, rz=0 ++ // Heading is completely ambiguous. Choose ++ // randomly among the four cardinal directions. ++ return ThreadLocalRandom.current().nextInt(0, 4); ++ } ++ case 5: { ++ // rx=1, rz=0 ++ // Definitively East ++ return East; ++ } ++ case 6: { ++ // rx=-1, rz=1 (southwest) ++ // Choose randomly between south and west ++ final int j = ThreadLocalRandom.current().nextInt(0, 1); ++ return (j == 0) ? South : West; ++ } ++ case 7: { ++ // rx=0, rz=1 ++ // Definitively South ++ return South; ++ } ++ case 8: { ++ // rx=1, rz=1 (southeast) ++ // Choose randomly between south and east ++ final int j = ThreadLocalRandom.current().nextInt(0, 1); ++ return (j == 0) ? South : East; ++ } ++ } ++ ++ // We should never get here ++ return ThreadLocalRandom.current().nextInt(0, 4); ++ } ++ ++ // Select whether to use updateSurroundingRedstone from BlockRedstoneWire (old) ++ // or this helper class (new) ++ private static final boolean old_current_change = false; ++ ++ /* ++ * Process a node whose neighboring redstone wire has experienced value changes. ++ */ ++ private void updateNode(final Level worldIn, final UpdateNode upd1, final int layer) { ++ final BlockPos pos = upd1.self; ++ ++ // Mark this redstone wire as having been visited so that it can be used ++ // to calculate direction of information flow. ++ upd1.visited = true; ++ ++ // Look up the last known state. ++ // Due to the way other redstone components are updated, we do not ++ // have to worry about a state changing behind our backs. The rare ++ // exception is handled by scheduleReentrantNeighborChanged. ++ final BlockState oldState = upd1.currentState; ++ ++ // Ask the wire block to compute its power level from its neighbors. ++ // This will also update the wire's power level and return a new ++ // state if it has changed. When a wire power level is changed, ++ // calculateCurrentChanges will immediately update the block state in the world ++ // and return the same value here to be cached in the corresponding ++ // UpdateNode object. ++ BlockState newState; ++ if (old_current_change) { ++ newState = wire.calculateCurrentChanges(worldIn, pos, oldState); ++ } else { ++ // Looking up block state is slow. This accelerator includes a version of ++ // calculateCurrentChanges that uses cahed wire values for a ++ // significant performance boost. ++ newState = this.calculateCurrentChanges(worldIn, upd1); ++ } ++ ++ // Only inform neighbors if the state has changed ++ if (newState != oldState) { ++ // Store the new state ++ upd1.currentState = newState; ++ ++ // Inform neighbors of the change ++ propagateChanges(worldIn, upd1, layer); ++ } ++ } ++ ++ /* ++ * This identifies the neighboring positions of a new UpdateNode object, ++ * determines their types, and links those to into the graph. Then based on ++ * what nodes in the redstone wire graph have been visited, the neighbors ++ * are reordered left-to-right relative to the direction of information flow. ++ */ ++ private void findNeighbors(final Level worldIn, final UpdateNode upd1) { ++ final BlockPos pos = upd1.self; ++ ++ // Get the list of neighbor coordinates ++ final BlockPos[] neighbors = computeAllNeighbors(pos); ++ ++ // Temporary array of neighbors in cardinal ordering ++ final UpdateNode[] neighbor_nodes = new UpdateNode[24]; ++ ++ // Target array of neighbors sorted left-to-right ++ upd1.neighbor_nodes = new UpdateNode[24]; ++ ++ for (int i=0; i<24; i++) { ++ // Look up each neighbor in the node cache ++ final BlockPos pos2 = neighbors[i]; ++ UpdateNode upd2 = nodeCache.get(pos2); ++ if (upd2 == null) { ++ // If this is a previously unreached position, create ++ // a new update node, add it to the cache, and identify what it is. ++ upd2 = new UpdateNode(); ++ upd2.self = pos2; ++ upd2.parent = pos; ++ nodeCache.put(pos2, upd2); ++ identifyNode(worldIn, upd2); ++ } ++ ++ // For non-redstone blocks, any of the 24 neighboring positions ++ // should receive a block update. However, some block coordinates ++ // may contain a redstone wire that does not directly connect to the ++ // one being expanded. To avoid redundant calculations and confusing ++ // cross-talk, those neighboring positions are not included. ++ if (update_redstone[i] || upd2.type != UpdateNode.Type.REDSTONE) { ++ neighbor_nodes[i] = upd2; ++ } ++ } ++ ++ // Determine the directions from which the redstone signal may have come from. This ++ // checks for redstone wire at the same Y level and also Y+1 and Y-1, relative to the ++ // block being expanded. ++ final boolean fromWest = (neighbor_nodes[0].visited || neighbor_nodes[7].visited || neighbor_nodes[8].visited); ++ final boolean fromEast = (neighbor_nodes[1].visited || neighbor_nodes[12].visited || neighbor_nodes[13].visited); ++ final boolean fromNorth = (neighbor_nodes[4].visited || neighbor_nodes[17].visited || neighbor_nodes[20].visited); ++ final boolean fromSouth = (neighbor_nodes[5].visited || neighbor_nodes[18].visited || neighbor_nodes[21].visited); ++ ++ int cx = 0, cz = 0; ++ if (fromWest) cx += 1; ++ if (fromEast) cx -= 1; ++ if (fromNorth) cz += 1; ++ if (fromSouth) cz -= 1; ++ ++ int heading; ++ if (cx==0 && cz==0) { ++ // If there is no clear direction, try to inherit the heading from ancestor nodes. ++ heading = computeHeading(upd1.xbias, upd1.zbias); ++ ++ // Propagate that heading to descendant nodes. ++ for (int i=0; i<24; i++) { ++ final UpdateNode nn = neighbor_nodes[i]; ++ if (nn != null) { ++ nn.xbias = upd1.xbias; ++ nn.zbias = upd1.zbias; ++ } ++ } ++ } else { ++ if (cx != 0 && cz != 0) { ++ // If the heading is somewhat ambiguous, try to disambiguate based on ++ // ancestor nodes. ++ if (upd1.xbias != 0) cz = 0; ++ if (upd1.zbias != 0) cx = 0; ++ } ++ heading = computeHeading(cx, cz); ++ ++ // Propagate that heading to descendant nodes. ++ for (int i=0; i<24; i++) { ++ final UpdateNode nn = neighbor_nodes[i]; ++ if (nn != null) { ++ nn.xbias = cx; ++ nn.zbias = cz; ++ } ++ } ++ } ++ ++ // Reorder neighboring UpdateNode objects according to the forward direction ++ // determined above. ++ orientNeighbors(neighbor_nodes, upd1.neighbor_nodes, heading); ++ } ++ ++ /* ++ * For any redstone wire block in layer N, inform neighbors to recompute their states ++ * in layers N+1 and N+2; ++ */ ++ private void propagateChanges(final Level worldIn, final UpdateNode upd1, final int layer) { ++ if (upd1.neighbor_nodes == null) { ++ // If this node has not been expanded yet, find its neighbors ++ findNeighbors(worldIn, upd1); ++ } ++ ++ final BlockPos pos = upd1.self; ++ ++ // All neighbors may be scheduled for layer N+1 ++ final int layer1 = layer + 1; ++ ++ // If the node being updated (upd1) has already been expanded, then merely ++ // schedule updates to its neighbors. ++ for (int i = 0; i < 24; i++) { ++ final UpdateNode upd2 = upd1.neighbor_nodes[i]; ++ ++ // This test ensures that an UpdateNode is never scheduled to the same layer ++ // more than once. Also, skip non-connecting redstone wire blocks ++ if (upd2 != null && layer1 > upd2.layer) { ++ upd2.layer = layer1; ++ updateQueue1.add(upd2); ++ ++ // Keep track of which block updated this neighbor ++ upd2.parent = pos; ++ } ++ } ++ ++ // Nodes above and below are scheduled ALSO for layer N+2 ++ final int layer2 = layer + 2; ++ ++ // Repeat of the loop above, but only for the first four (above and below) neighbors ++ // and for layer N+2; ++ for (int i = 0; i < 4; i++) { ++ final UpdateNode upd2 = upd1.neighbor_nodes[i]; ++ if (upd2 != null && layer2 > upd2.layer) { ++ upd2.layer = layer2; ++ updateQueue2.add(upd2); ++ upd2.parent = pos; ++ } ++ } ++ } ++ ++ // The breadth-first search below will send block updates to blocks ++ // that are not redstone wire. If one of those updates results in ++ // a distant redstone wire getting an update, then this.neighborChanged ++ // will get called. This would be a reentrant call, and ++ // it is necessary to properly integrate those updates into the ++ // on-going search through redstone wire. Thus, we make the layer ++ // currently being processed visible at the object level. ++ ++ // The current layer being processed by the breadth-first search ++ private int currentWalkLayer = 0; ++ ++ private void shiftQueue() { ++ final List t = updateQueue0; ++ t.clear(); ++ updateQueue0 = updateQueue1; ++ updateQueue1 = updateQueue2; ++ updateQueue2 = t; ++ } ++ ++ /* ++ * Perform a breadth-first (layer by layer) traversal through redstone ++ * wire blocks, propagating value changes to neighbors in an order ++ * that is a function of distance from the initial call to ++ * this.neighborChanged. ++ */ ++ private void breadthFirstWalk(final Level worldIn) { ++ shiftQueue(); ++ currentWalkLayer = 1; ++ ++ // Loop over all layers ++ while (updateQueue0.size()>0 || updateQueue1.size()>0) { ++ // Get the set of blocks in this layer ++ final List thisLayer = updateQueue0; ++ ++ // Loop over all blocks in the layer. Recall that ++ // this is a List, preserving the insertion order of ++ // left-to-right based on direction of information flow. ++ for (UpdateNode upd : thisLayer) { ++ if (upd.type == UpdateNode.Type.REDSTONE) { ++ // If the node is is redstone wire, ++ // schedule updates to neighbors if its value ++ // has changed. ++ updateNode(worldIn, upd, currentWalkLayer); ++ } else { ++ // If this block is not redstone wire, send a block update. ++ // Redstone wire blocks get state updates, but they don't ++ // need block updates. Only non-redstone neighbors need updates. ++ ++ // World.neighborChanged is called from ++ // World.notifyNeighborsOfStateChange, and ++ // notifyNeighborsOfStateExcept. We don't use ++ // World.notifyNeighborsOfStateChange here, since we are ++ // already keeping track of all of the neighbor positions ++ // that need to be updated. All on its own, handling neighbors ++ // this way reduces block updates by 1/3 (24 instead of 36). ++// worldIn.neighborChanged(upd.self, wire, upd.parent); ++ ++ // [Space Walker] ++ // The neighbor update system got a significant overhaul in 1.19. ++ // Shape and block updates are now added to a stack before being ++ // processed. These changes make it so any neighbor updates emitted ++ // by this accelerator will not be processed until after the entire ++ // wire network has updated. This has a significant impact on the ++ // behavior and introduces Vanilla parity issues. ++ // To circumvent this issue we bypass the neighbor update stack and ++ // call BlockStateBase#neighborChanged directly. This change mostly ++ // restores old behavior, at the cost of bypassing the ++ // max-chained-neighbor-updates server property. ++ // The Orientation parameter is (for now) only used by redstone wire ++ // while these updates are dispatched to non-wires only, so we can ++ // pass null. ++ worldIn.getBlockState(upd.self).handleNeighborChanged(worldIn, upd.self, wire, null, false); ++ } ++ } ++ ++ // Move on to the next layer ++ shiftQueue(); ++ currentWalkLayer++; ++ } ++ ++ currentWalkLayer = 0; ++ } ++ ++ /* ++ * Normally, when Minecraft is computing redstone wire power changes, and a wire power level ++ * change sends a block update to a neighboring functional component (e.g. piston, repeater, etc.), ++ * those updates are queued. Only once all redstone wire updates are complete will any component ++ * action generate any further block updates to redstone wire. Instant repeater lines, for instance, ++ * will process all wire updates for one redstone line, after which the pistons will zero-tick, ++ * after which the next redstone line performs all of its updates. Thus, each wire is processed in its ++ * own discrete wave. ++ * ++ * However, there are some corner cases where this pattern breaks, with a proof of concept discovered ++ * by Rays Works, which works the same in vanilla. The scenario is as follows: ++ * (1) A redstone wire is conducting a signal. ++ * (2) Part-way through that wave of updates, a neighbor is updated that causes an update to a completely ++ * separate redstone wire. ++ * (3) This results in a call to BlockRedstoneWire.neighborChanged for that other wire, in the middle of ++ * an already on-going propagation through the first wire. ++ * ++ * The vanilla code, being depth-first, would end up fully processing the second wire before going back ++ * to finish processing the first one. (Although technically, vanilla has no special concept of "being ++ * in the middle" of processing updates to a wire.) For the breadth-first algorithm, we give this ++ * situation special handling, where the updates for the second wire are incorporated into the schedule ++ * for the first wire, and then the callstack is allowed to unwind back to the on-going search loop in ++ * order to continue processing both the first and second wire in the order of distance from the initial ++ * trigger. ++ */ ++ private BlockState scheduleReentrantNeighborChanged(final Level worldIn, final BlockPos pos, final BlockState newState, final BlockPos source) { ++ if (source != null) { ++ // If the cause of the redstone wire update is known, we can use that to help determine ++ // direction of information flow. ++ UpdateNode src = nodeCache.get(source); ++ if (src == null) { ++ src = new UpdateNode(); ++ src.self = source; ++ src.parent = source; ++ src.visited = true; ++ identifyNode(worldIn, src); ++ nodeCache.put(source, src); ++ } ++ } ++ ++ // Find or generate a node for the redstone block position receiving the update ++ UpdateNode upd = nodeCache.get(pos); ++ if (upd == null) { ++ upd = new UpdateNode(); ++ upd.self = pos; ++ upd.parent = pos; ++ upd.visited = true; ++ identifyNode(worldIn, upd); ++ nodeCache.put(pos, upd); ++ } ++ upd.currentState = newState; ++ ++ // Receiving this block update may mean something in the world changed. ++ // Therefore we clear the cached block info about all neighbors of ++ // the position receiving the update and then re-identify what they are. ++ if (upd.neighbor_nodes != null) { ++ for (int i=0; i<24; i++) { ++ final UpdateNode upd2 = upd.neighbor_nodes[i]; ++ if (upd2 == null) continue; ++ upd2.type = UpdateNode.Type.UNKNOWN; ++ upd2.currentState = null; ++ identifyNode(worldIn, upd2); ++ } ++ } ++ ++ // The block at 'pos' is a redstone wire and has been updated already by calling ++ // wire.calculateCurrentChanges, so we don't schedule that. However, we do need ++ // to schedule its neighbors. By passing the current value of 'currentWalkLayer' to ++ // propagateChanges, the neighbors of 'pos' are scheduled for layers currentWalkLayer+1 ++ // and currentWalkLayer+2. ++ propagateChanges(worldIn, upd, currentWalkLayer); ++ ++ // Return here. The call stack will unwind back to the first call to ++ // updateSurroundingRedstone, whereupon the new updates just scheduled will ++ // be propagated. This also facilitates elimination of superfluous and ++ // redundant block updates. ++ return newState; ++ } ++ ++ /* ++ * New version of pre-existing updateSurroundingRedstone, which is called from ++ * wire.updateSurroundingRedstone, which is called from wire.neighborChanged and a ++ * few other methods in BlockRedstoneWire. This sets off the breadth-first ++ * walk through all redstone dust connected to the initial position triggered. ++ */ ++ public BlockState updateSurroundingRedstone(final Level worldIn, final BlockPos pos, final BlockState state, final BlockPos source) { ++ // Check this block's neighbors and see if its power level needs to change ++ // Use the calculateCurrentChanges method in BlockRedstoneWire since we have no ++ // cached block states at this point. ++ final BlockState newState = wire.calculateCurrentChanges(worldIn, pos, state); ++ ++ // If no change, exit ++ if (newState == state) { ++ return state; ++ } ++ ++ // Check to see if this update was received during an on-going breadth first search ++ if (currentWalkLayer > 0 || nodeCache.size() > 0) { ++ // As breadthFirstWalk progresses, it sends block updates to neighbors. Some of those ++ // neighbors may affect the world so as to cause yet another redstone wire block to receive ++ // an update. If that happens, we need to integrate those redstone wire updates into the ++ // already on-going graph walk being performed by breadthFirstWalk. ++ return scheduleReentrantNeighborChanged(worldIn, pos, newState, source); ++ } ++ // If there are no on-going walks through redstone wire, then start a new walk. ++ ++ // If the source of the block update to the redstone wire at 'pos' is known, we can use ++ // that to help determine the direction of information flow. ++ if (source != null) { ++ final UpdateNode src = new UpdateNode(); ++ src.self = source; ++ src.parent = source; ++ src.visited = true; ++ nodeCache.put(source, src); ++ identifyNode(worldIn, src); ++ } ++ ++ // Create a node representing the block at 'pos', and then propagate updates ++ // to its neighbors. As stated above, the call to wire.calculateCurrentChanges ++ // already performs the update to the block at 'pos', so it is not added to the schedule. ++ final UpdateNode upd = new UpdateNode(); ++ upd.self = pos; ++ upd.parent = source!=null ? source : pos; ++ upd.currentState = newState; ++ upd.type = UpdateNode.Type.REDSTONE; ++ upd.visited = true; ++ nodeCache.put(pos, upd); ++ propagateChanges(worldIn, upd, 0); ++ ++ // Perform the walk over all directly reachable redstone wire blocks, propagating wire value ++ // updates in a breadth first order out from the initial update received for the block at 'pos'. ++ breadthFirstWalk(worldIn); ++ ++ // With the whole search completed, clear the list of all known blocks. ++ // We do not want to keep around state information that may be changed by other code. ++ // In theory, we could cache the neighbor block positions, but that is a separate ++ // optimization. ++ nodeCache.clear(); ++ ++ return newState; ++ } ++ ++ // For any array of neighbors in an UpdateNode object, these are always ++ // the indices of the four immediate neighbors at the same Y coordinate. ++ private static final int[] rs_neighbors = {4, 5, 6, 7}; ++ private static final int[] rs_neighbors_up = {9, 11, 13, 15}; ++ private static final int[] rs_neighbors_dn = {8, 10, 12, 14}; ++ ++ /* ++ * Updated calculateCurrentChanges that is optimized for speed and uses ++ * the UpdateNode's neighbor array to find the redstone states of neighbors ++ * that might power it. ++ */ ++ private BlockState calculateCurrentChanges(final Level worldIn, final UpdateNode upd) { ++ BlockState state = upd.currentState; ++ final int i = state.getValue(RedStoneWireBlock.POWER).intValue(); ++ int j = 0; ++ j = getMaxCurrentStrength(upd, j); ++ int l = 0; ++ ++ wire.shouldSignal = false; ++ // Unfortunately, World.isBlockIndirectlyGettingPowered is complicated, ++ // and I'm not ready to try to replicate even more functionality from ++ // elsewhere in Minecraft into this accelerator. So sadly, we must ++ // suffer the performance hit of this very expensive call. If there ++ // is consistency to what this call returns, we may be able to cache it. ++ final int k = worldIn.getBestNeighborSignal(upd.self); ++ wire.shouldSignal = true; ++ ++ // The variable 'k' holds the maximum redstone power value of any adjacent blocks. ++ // If 'k' has the highest level of all neighbors, then the power level of this ++ // redstone wire will be set to 'k'. If 'k' is already 15, then nothing inside the ++ // following loop can affect the power level of the wire. Therefore, the loop is ++ // skipped if k is already 15. ++ if (k < 15) { ++ if (upd.neighbor_nodes == null) { ++ // If this node's neighbors are not known, expand the node ++ findNeighbors(worldIn, upd); ++ } ++ ++ // These remain constant, so pull them out of the loop. ++ // Regardless of which direction is forward, the UpdateNode for the ++ // position directly above the node being calculated is always ++ // at index 1. ++ UpdateNode center_up = upd.neighbor_nodes[1]; ++ boolean center_up_is_cube = center_up.currentState.isRedstoneConductor(worldIn, center_up.self); // TODO ++ ++ for (int m = 0; m < 4; m++) { ++ // Get the neighbor array index of each of the four cardinal ++ // neighbors. ++ int n = rs_neighbors[m]; ++ ++ // Get the max redstone power level of each of the cardinal ++ // neighbors ++ UpdateNode neighbor = upd.neighbor_nodes[n]; ++ l = getMaxCurrentStrength(neighbor, l); ++ ++ // Also check the positions above and below the cardinal ++ // neighbors ++ boolean neighbor_is_cube = neighbor.currentState.isRedstoneConductor(worldIn, neighbor.self); // TODO ++ if (!neighbor_is_cube) { ++ UpdateNode neighbor_down = upd.neighbor_nodes[rs_neighbors_dn[m]]; ++ l = getMaxCurrentStrength(neighbor_down, l); ++ } else ++ if (!center_up_is_cube) { ++ UpdateNode neighbor_up = upd.neighbor_nodes[rs_neighbors_up[m]]; ++ l = getMaxCurrentStrength(neighbor_up, l); ++ } ++ } ++ } ++ ++ // The new code sets this RedstoneWire block's power level to the highest neighbor ++ // minus 1. This usually results in wire power levels dropping by 2 at a time. ++ // This optimization alone has no impact on update order, only the number of updates. ++ j = l - 1; ++ ++ // If 'l' turns out to be zero, then j will be set to -1, but then since 'k' will ++ // always be in the range of 0 to 15, the following if will correct that. ++ if (k > j) j = k; ++ ++ // egg82's amendment ++ // Adding Bukkit's BlockRedstoneEvent - er.. event. ++ if (i != j) { ++ BlockRedstoneEvent event = new BlockRedstoneEvent(CraftBlock.at(worldIn, upd.self), i, j); ++ worldIn.getCraftServer().getPluginManager().callEvent(event); ++ j = event.getNewCurrent(); ++ } ++ ++ if (i != j) { ++ // If the power level has changed from its previous value, compute a new state ++ // and set it in the world. ++ // Possible optimization: Don't commit state changes to the world until they ++ // need to be known by some nearby non-redstone-wire block. ++ BlockPos pos = new BlockPos(upd.self.getX(), upd.self.getY(), upd.self.getZ()); ++ if (wire.canSurvive(null, worldIn, pos)) { ++ state = state.setValue(RedStoneWireBlock.POWER, Integer.valueOf(j)); ++ // [Space Walker] suppress shape updates and emit those manually to ++ // bypass the new neighbor update stack. ++ if (worldIn.setBlock(upd.self, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) ++ updateNeighborShapes(worldIn, upd.self, state); ++ } ++ } ++ ++ return state; ++ } ++ ++ private static final Direction[] UPDATE_SHAPE_ORDER = { Direction.WEST, Direction.EAST, Direction.NORTH, Direction.SOUTH, Direction.DOWN, Direction.UP }; ++ ++ /* ++ * [Space Walker] ++ * This method emits shape updates around the given block, ++ * bypassing the new neighbor update stack. Diagonal shape ++ * updates are omitted, as they are mostly unnecessary. ++ * Diagonal shape updates are emitted exclusively to other ++ * redstone wires, in order to update their connection properties. ++ * Wire connections should never change as a result of power ++ * changes, so the only behavioral change will be in scenarios ++ * where earlier shape updates have been suppressed to keep a ++ * redstone wire in an invalid state. ++ */ ++ public void updateNeighborShapes(Level level, BlockPos pos, BlockState state) { ++ // these updates will be added to the stack and processed after the entire network has updated ++ state.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS); ++ ++ for (Direction dir : UPDATE_SHAPE_ORDER) { ++ BlockPos neighborPos = pos.relative(dir); ++ BlockState neighborState = level.getBlockState(neighborPos); ++ ++ BlockState newState = neighborState.updateShape(level, level, neighborPos, dir.getOpposite(), pos, state, level.getRandom()); ++ Block.updateOrDestroy(neighborState, newState, level, neighborPos, Block.UPDATE_CLIENTS); ++ } ++ } ++ ++ /* ++ * Optimized function to compute a redstone wire's power level based on cached ++ * state. ++ */ ++ private static int getMaxCurrentStrength(final UpdateNode upd, final int strength) { ++ if (upd.type != UpdateNode.Type.REDSTONE) return strength; ++ final int i = upd.currentState.getValue(RedStoneWireBlock.POWER).intValue(); ++ return i > strength ? i : strength; ++ } ++} +diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +index 2a3be00d41eda68f7d5383b240759561c4663f8d..09b8f5335cb7651d90f4d1ca61b2ec5aa324e443 100644 +--- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +@@ -290,6 +290,60 @@ public class RedStoneWireBlock extends Block { + return floor.isFaceSturdy(world, pos, Direction.UP) || floor.is(Blocks.HOPPER); + } + ++ // Paper start - Optimize redstone ++ // The bulk of the new functionality is found in RedstoneWireTurbo.java ++ com.destroystokyo.paper.util.RedstoneWireTurbo turbo = new com.destroystokyo.paper.util.RedstoneWireTurbo(this); ++ ++ /* ++ * Modified version of pre-existing updateSurroundingRedstone, which is called from ++ * this.neighborChanged and a few other methods in this class. ++ * Note: Added 'source' argument so as to help determine direction of information flow ++ */ ++ private void updateSurroundingRedstone(Level worldIn, BlockPos pos, BlockState state, @Nullable Orientation orientation, boolean blockAdded) { ++ if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT) { ++ // since 24w33a the source pos is no longer given, but instead an Orientation parameter ++ // when this is not null, it can be used to find the source pos, which the turbo uses ++ // to find the direction of information flow ++ BlockPos source = null; ++ if (orientation != null) { ++ source = pos.relative(orientation.getFront().getOpposite()); ++ } ++ turbo.updateSurroundingRedstone(worldIn, pos, state, source); ++ return; ++ } ++ updatePowerStrength(worldIn, pos, state, orientation, blockAdded); ++ } ++ ++ /* ++ * This method computes a wire's target strength and updates the given block state. ++ * It uses the DefaultRedstoneWireEvaluator for this, which is identical to code ++ * that was present in this class prior to the introduction of the experimental redstone ++ * changes in 24w33a. ++ * The previous implementation of this method in this patch had optimizations that have ++ * not been relevant since 1.13, thus it has been greatly simplified. ++ */ ++ public BlockState calculateCurrentChanges(Level level, BlockPos pos, BlockState state) { ++ int oldPower = state.getValue(POWER); ++ int newPower = ((DefaultRedstoneWireEvaluator) evaluator).calculateTargetStrength(level, pos); ++ if (oldPower != newPower) { ++ org.bukkit.event.block.BlockRedstoneEvent event = new org.bukkit.event.block.BlockRedstoneEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), oldPower, newPower); ++ level.getCraftServer().getPluginManager().callEvent(event); ++ ++ newPower = event.getNewCurrent(); ++ ++ if (level.getBlockState(pos) == state) { ++ state = state.setValue(POWER, newPower); ++ // [Space Walker] suppress shape updates and emit those manually to ++ // bypass the new neighbor update stack. ++ if (level.setBlock(pos, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) { ++ turbo.updateNeighborShapes(level, pos, state); ++ } ++ } ++ } ++ return state; ++ } ++ // Paper end ++ + private void updatePowerStrength(Level world, BlockPos pos, BlockState state, @Nullable Orientation orientation, boolean blockAdded) { + if (useExperimentalEvaluator(world)) { + new ExperimentalRedstoneWireEvaluator(this).updatePowerStrength(world, pos, state, orientation, blockAdded); +@@ -318,7 +372,7 @@ public class RedStoneWireBlock extends Block { + @Override + protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) { + if (!oldState.is(state.getBlock()) && !world.isClientSide) { +- this.updatePowerStrength(world, pos, state, null, true); ++ this.updateSurroundingRedstone(world, pos, state, null, true); // Paper - Optimize redstone + + for (Direction direction : Direction.Plane.VERTICAL) { + world.updateNeighborsAt(pos.relative(direction), this); +@@ -337,7 +391,7 @@ public class RedStoneWireBlock extends Block { + world.updateNeighborsAt(pos.relative(direction), this); + } + +- this.updatePowerStrength(world, pos, state, null, false); ++ this.updateSurroundingRedstone(world, pos, state, null, false); // Paper - Optimize redstone + this.updateNeighborsOfNeighboringWires(world, pos); + } + } +@@ -363,7 +417,7 @@ public class RedStoneWireBlock extends Block { + if (!world.isClientSide) { + if (sourceBlock != this || !useExperimentalEvaluator(world)) { + if (state.canSurvive(world, pos)) { +- this.updatePowerStrength(world, pos, state, wireOrientation, false); ++ this.updateSurroundingRedstone(world, pos, state, wireOrientation, false); // Paper - Optimize redstone + } else { + dropResources(state, world, pos); + world.removeBlock(pos, false); +diff --git a/src/main/java/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java b/src/main/java/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java +index f8be1f6bc6f144db5265844f46f0a2cb8cc213fe..3df778f3c9a633f07c7bd1736423384afce3edf7 100644 +--- a/src/main/java/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java ++++ b/src/main/java/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java +@@ -61,7 +61,7 @@ public class DefaultRedstoneWireEvaluator extends RedstoneWireEvaluator { + + } + +- private int calculateTargetStrength(Level world, BlockPos pos) { ++ public int calculateTargetStrength(Level world, BlockPos pos) { // Paper - Optimize redstone + int i = this.getBlockSignal(world, pos); + + return i == 15 ? i : Math.max(i, this.getIncomingWireSignal(world, pos)); diff --git a/patches/server/1060-Improve-performance-of-RecipeMap-removeRecipe.patch b/patches/server/1060-Improve-performance-of-RecipeMap-removeRecipe.patch new file mode 100644 index 0000000000..ced8d08b40 --- /dev/null +++ b/patches/server/1060-Improve-performance-of-RecipeMap-removeRecipe.patch @@ -0,0 +1,89 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Thu, 31 Oct 2024 20:36:41 -0700 +Subject: [PATCH] Improve performance of RecipeMap#removeRecipe + + +diff --git a/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java b/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java +index 6fe39c9910c09aa47cf7b130e8f3aeec6d036013..2483627f807d7a3907f6848a8bc45d7a798e746d 100644 +--- a/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java ++++ b/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java +@@ -260,7 +260,7 @@ public class RecipeManager extends SimplePreparableReloadListener imp + + // CraftBukkit start + public boolean removeRecipe(ResourceKey> mcKey) { +- boolean removed = this.recipes.removeRecipe(mcKey); ++ boolean removed = this.recipes.removeRecipe((ResourceKey>) (ResourceKey) mcKey); // Paper - generic fix + if (removed) { + this.finalizeRecipeLoading(); + } +diff --git a/src/main/java/net/minecraft/world/item/crafting/RecipeMap.java b/src/main/java/net/minecraft/world/item/crafting/RecipeMap.java +index 5d842d7e774564143f9f3be6c2628d54595a235b..c4067fbf827fed882772962a0e4b3ead0d642e62 100644 +--- a/src/main/java/net/minecraft/world/item/crafting/RecipeMap.java ++++ b/src/main/java/net/minecraft/world/item/crafting/RecipeMap.java +@@ -54,21 +54,38 @@ public class RecipeMap { + } + } + +- public boolean removeRecipe(ResourceKey> mcKey) { +- boolean removed = false; +- Iterator> iter = this.byType.values().iterator(); +- while (iter.hasNext()) { +- RecipeHolder recipe = iter.next(); +- if (recipe.id().equals(mcKey)) { +- iter.remove(); +- removed = true; +- } +- } +- removed |= this.byKey.remove(mcKey) != null; ++ // public boolean removeRecipe(ResourceKey> mcKey) { ++ // boolean removed = false; ++ // Iterator> iter = this.byType.values().iterator(); ++ // while (iter.hasNext()) { ++ // RecipeHolder recipe = iter.next(); ++ // if (recipe.id().equals(mcKey)) { ++ // iter.remove(); ++ // removed = true; ++ // } ++ // } ++ // removed |= this.byKey.remove(mcKey) != null; ++ // ++ // return removed; ++ // } ++ // CraftBukkit end ++ + +- return removed; ++ // Paper start - replace removeRecipe implementation ++ public boolean removeRecipe(ResourceKey> mcKey) { ++ //noinspection unchecked ++ final RecipeHolder> remove = (RecipeHolder>) this.byKey.remove(mcKey); ++ if (remove == null) { ++ return false; ++ } ++ final Collection>> recipes = this.byType(remove.value().getType()); ++ if (recipes.remove(remove)) { ++ return true; ++ } ++ return false; ++ // Paper end - why are you using a loop??? + } +- // CraftBukkit end ++ // Paper end - replace removeRecipe implementation + + public > Collection> byType(RecipeType type) { + return (Collection) this.byType.get(type); // CraftBukkit - decompile error +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/RecipeIterator.java b/src/main/java/org/bukkit/craftbukkit/inventory/RecipeIterator.java +index c0433e054e64c329dff670c8f7ca21c4a4133c6f..a20b471389474244ef20bf42d4085dcf9dd122a5 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/RecipeIterator.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/RecipeIterator.java +@@ -32,5 +32,9 @@ public class RecipeIterator implements Iterator { + public void remove() { + MinecraftServer.getServer().getRecipeManager().recipes.byKey.remove(this.currentRecipe.id()); // Paper - fix removing recipes from RecipeIterator + this.recipes.remove(); ++ // Paper start - correctly reload recipes ++ MinecraftServer.getServer().getRecipeManager().finalizeRecipeLoading(); ++ MinecraftServer.getServer().getPlayerList().reloadRecipes(); ++ // Paper end - correctly reload recipes + } + } diff --git a/patches/server/1061-Reduce-work-done-in-CraftMapCanvas.drawImage-by-limi.patch b/patches/server/1061-Reduce-work-done-in-CraftMapCanvas.drawImage-by-limi.patch new file mode 100644 index 0000000000..81c7b52685 --- /dev/null +++ b/patches/server/1061-Reduce-work-done-in-CraftMapCanvas.drawImage-by-limi.patch @@ -0,0 +1,75 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Barnaby <22575741+barnabwhy@users.noreply.github.com> +Date: Sat, 29 Jun 2024 12:06:51 +0100 +Subject: [PATCH] Reduce work done in CraftMapCanvas.drawImage by limiting size + of image and using System.arraycopy instead of for loops and use bitwise + operations to do bounds checks. + + +diff --git a/src/main/java/org/bukkit/craftbukkit/map/CraftMapCanvas.java b/src/main/java/org/bukkit/craftbukkit/map/CraftMapCanvas.java +index ff59f759669620795ef355c988b664bdcda39f52..a5e98571d6d83390761c11e28a0bc3c4415799cd 100644 +--- a/src/main/java/org/bukkit/craftbukkit/map/CraftMapCanvas.java ++++ b/src/main/java/org/bukkit/craftbukkit/map/CraftMapCanvas.java +@@ -91,12 +91,41 @@ public class CraftMapCanvas implements MapCanvas { + + @Override + public void drawImage(int x, int y, Image image) { +- byte[] bytes = MapPalette.imageToBytes(image); +- for (int x2 = 0; x2 < image.getWidth(null); ++x2) { +- for (int y2 = 0; y2 < image.getHeight(null); ++y2) { +- this.setPixel(x + x2, y + y2, bytes[y2 * image.getWidth(null) + x2]); ++ // Paper start - Reduce work done by limiting size of image and using System.arraycopy ++ int width = 128 - x; ++ int height = 128 - y; ++ if (image.getHeight(null) < height) ++ height = image.getHeight(null); ++ ++ // Create a subimage if the image is larger than the max allowed size ++ java.awt.image.BufferedImage temp; ++ if (image.getWidth(null) >= width && image instanceof java.awt.image.BufferedImage bImage) { ++ // If the image is larger than the max allowed size, get a subimage, otherwise use the image as is ++ if (image.getWidth(null) > width || image.getHeight(null) > height) { ++ temp = bImage.getSubimage(0, 0, width, height); ++ } else { ++ temp = bImage; + } ++ } else { ++ temp = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB); ++ java.awt.Graphics2D graphics = temp.createGraphics(); ++ graphics.drawImage(image, 0, 0, null); ++ graphics.dispose(); + } ++ ++ byte[] bytes = MapPalette.imageToBytes(temp); ++ ++ // Since we now control the size of the image, we can safely use System.arraycopy ++ // If x is 0, we can just copy the entire image as width is 128 and height is <=(128-y) ++ if (x == 0) { ++ System.arraycopy(bytes, 0, this.buffer, y * 128, width * height); ++ return; ++ } ++ ++ for (int y2 = 0; y2 < height; ++y2) { ++ System.arraycopy(bytes, 0, this.buffer, (y + y2) * 128 + x, width); ++ } ++ // Paper end + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java b/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java +index 0cbbd915631904fe8c6effefb92895422b33eff6..cf0920e5f84b35647882fb963e9972af4e8427e0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java ++++ b/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java +@@ -23,8 +23,10 @@ public class CraftMapRenderer extends MapRenderer { + @Override + public void render(MapView map, MapCanvas canvas, Player player) { + // Map +- for (int x = 0; x < 128; ++x) { +- for (int y = 0; y < 128; ++y) { ++ // Paper start - Swap inner and outer loops here to (theoretically) improve cache locality ++ for (int y = 0; y < 128; ++y) { ++ for (int x = 0; x < 128; ++x) { ++ // Paper end + canvas.setPixel(x, y, this.worldMap.colors[y * 128 + x]); + } + } diff --git a/patches/server/1062-Add-Alternate-Current-redstone-implementation.patch b/patches/server/1062-Add-Alternate-Current-redstone-implementation.patch new file mode 100644 index 0000000000..125b43d205 --- /dev/null +++ b/patches/server/1062-Add-Alternate-Current-redstone-implementation.patch @@ -0,0 +1,2452 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Space Walker +Date: Wed, 8 Jun 2022 18:47:18 +0200 +Subject: [PATCH] Add Alternate Current redstone implementation + +Author: Space Walker + +Original license: MIT +Original project: https://github.com/SpaceWalkerRS/alternate-current + +This patch adds Alternate Current's redstone implementation as an alternative to vanilla and Eigencraft's. +Performance of (de)powering redstone dust is many times faster than vanilla, and even exceeds Eigencraft. +Similar to Eigencraft, Alternate Current heavily changes the update order of redstone dust. This means any contraption that +is location dependent in vanilla will either work everywhere or nowhere when using Alternate Current/Eigencraft. Beyond that +parity issues should be rare for both implementations, though Alternate Current has not been tested as thoroughly, so I +cannot comment on how the two compare in that aspect. + +Alternate Current needs the following modifications: +* Level/ServerLevel: Each level has its own 'wire handler' that handles redstone dust power changes. +* RedStoneWireBlock: Replace calls to vanilla's or Eigencraft's methods for handling power changes with calls to +Alternate Current's wire handler. + +Feature patch + +diff --git a/src/main/java/alternate/current/wire/LevelHelper.java b/src/main/java/alternate/current/wire/LevelHelper.java +new file mode 100644 +index 0000000000000000000000000000000000000000..eda108e2df9bf7d1ddd89287b8d2c2d7f1637c96 +--- /dev/null ++++ b/src/main/java/alternate/current/wire/LevelHelper.java +@@ -0,0 +1,66 @@ ++package alternate.current.wire; ++ ++import org.bukkit.craftbukkit.block.CraftBlock; ++import org.bukkit.event.block.BlockRedstoneEvent; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.LevelChunkSection; ++ ++class LevelHelper { ++ ++ static int doRedstoneEvent(ServerLevel level, BlockPos pos, int prevPower, int newPower) { ++ BlockRedstoneEvent event = new BlockRedstoneEvent(CraftBlock.at(level, pos), prevPower, newPower); ++ level.getCraftServer().getPluginManager().callEvent(event); ++ ++ return event.getNewCurrent(); ++ } ++ ++ /** ++ * An optimized version of {@link net.minecraft.world.level.Level#setBlock ++ * Level.setBlock}. Since this method is only used to update redstone wire block ++ * states, lighting checks, height map updates, and block entity updates are ++ * omitted. ++ */ ++ static boolean setWireState(ServerLevel level, BlockPos pos, BlockState state, boolean updateNeighborShapes) { ++ int y = pos.getY(); ++ ++ if (y < level.getMinY() || y >= level.getMaxY()) { ++ return false; ++ } ++ ++ int x = pos.getX(); ++ int z = pos.getZ(); ++ int index = level.getSectionIndex(y); ++ ++ ChunkAccess chunk = level.getChunk(x >> 4, z >> 4, ChunkStatus.FULL, true); ++ LevelChunkSection section = chunk.getSections()[index]; ++ ++ if (section == null) { ++ return false; // we should never get here ++ } ++ ++ BlockState prevState = section.setBlockState(x & 15, y & 15, z & 15, state); ++ ++ if (state == prevState) { ++ return false; ++ } ++ ++ // notify clients of the BlockState change ++ level.getChunkSource().blockChanged(pos); ++ // mark the chunk for saving ++ chunk.markUnsaved(); ++ ++ if (updateNeighborShapes) { ++ prevState.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_CLIENTS); ++ state.updateNeighbourShapes(level, pos, Block.UPDATE_CLIENTS); ++ state.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_CLIENTS); ++ } ++ ++ return true; ++ } ++} +diff --git a/src/main/java/alternate/current/wire/Node.java b/src/main/java/alternate/current/wire/Node.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8af6c69098e64945361d116b5fd6ac21e97fcd8d +--- /dev/null ++++ b/src/main/java/alternate/current/wire/Node.java +@@ -0,0 +1,113 @@ ++package alternate.current.wire; ++ ++import java.util.Arrays; ++ ++import alternate.current.wire.WireHandler.Directions; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.state.BlockState; ++ ++/** ++ * A Node represents a block in the world. It also holds a few other pieces of ++ * information that speed up the calculations in the WireHandler class. ++ * ++ * @author Space Walker ++ */ ++public class Node { ++ ++ // flags that encode the Node type ++ private static final int CONDUCTOR = 0b01; ++ private static final int SOURCE = 0b10; ++ ++ final ServerLevel level; ++ final Node[] neighbors; ++ ++ BlockPos pos; ++ BlockState state; ++ boolean invalid; ++ ++ private int flags; ++ ++ /** The previous node in the priority queue. */ ++ Node prev_node; ++ /** The next node in the priority queue. */ ++ Node next_node; ++ /** The priority with which this node was queued. */ ++ int priority; ++ /** The wire that queued this node for an update. */ ++ WireNode neighborWire; ++ ++ Node(ServerLevel level) { ++ this.level = level; ++ this.neighbors = new Node[Directions.ALL.length]; ++ } ++ ++ @Override ++ public boolean equals(Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ if (!(obj instanceof Node)) { ++ return false; ++ } ++ ++ Node node = (Node)obj; ++ ++ return level == node.level && pos.equals(node.pos); ++ } ++ ++ @Override ++ public int hashCode() { ++ return pos.hashCode(); ++ } ++ ++ Node set(BlockPos pos, BlockState state, boolean clearNeighbors) { ++ if (state.is(Blocks.REDSTONE_WIRE)) { ++ throw new IllegalStateException("Cannot update a regular Node to a WireNode!"); ++ } ++ ++ if (clearNeighbors) { ++ Arrays.fill(neighbors, null); ++ } ++ ++ this.pos = pos.immutable(); ++ this.state = state; ++ this.invalid = false; ++ ++ this.flags = 0; ++ ++ if (this.state.isRedstoneConductor(this.level, this.pos)) { ++ this.flags |= CONDUCTOR; ++ } ++ if (this.state.isSignalSource()) { ++ this.flags |= SOURCE; ++ } ++ ++ return this; ++ } ++ ++ /** ++ * Determine the priority with which this node should be queued. ++ */ ++ int priority() { ++ return neighborWire.priority; ++ } ++ ++ public boolean isWire() { ++ return false; ++ } ++ ++ public boolean isConductor() { ++ return (flags & CONDUCTOR) != 0; ++ } ++ ++ public boolean isSignalSource() { ++ return (flags & SOURCE) != 0; ++ } ++ ++ public WireNode asWire() { ++ throw new UnsupportedOperationException("Not a WireNode!"); ++ } ++} +diff --git a/src/main/java/alternate/current/wire/PriorityQueue.java b/src/main/java/alternate/current/wire/PriorityQueue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d71b4d0e4c44a2620b41b89475412db53bea20ed +--- /dev/null ++++ b/src/main/java/alternate/current/wire/PriorityQueue.java +@@ -0,0 +1,211 @@ ++package alternate.current.wire; ++ ++import java.util.AbstractQueue; ++import java.util.Arrays; ++import java.util.Iterator; ++ ++import net.minecraft.world.level.redstone.Redstone; ++ ++public class PriorityQueue extends AbstractQueue { ++ ++ private static final int OFFSET = -Redstone.SIGNAL_MIN; ++ ++ /** The last node for each priority value. */ ++ private final Node[] tails; ++ ++ private Node head; ++ private Node tail; ++ ++ private int size; ++ ++ PriorityQueue() { ++ this.tails = new Node[(Redstone.SIGNAL_MAX + OFFSET) + 1]; ++ } ++ ++ @Override ++ public boolean offer(Node node) { ++ if (node == null) { ++ throw new NullPointerException(); ++ } ++ ++ int priority = node.priority(); ++ ++ if (contains(node)) { ++ if (node.priority == priority) { ++ // already queued with this priority; exit ++ return false; ++ } else { ++ // already queued with different priority; move it ++ move(node, priority); ++ } ++ } else { ++ insert(node, priority); ++ } ++ ++ return true; ++ } ++ ++ @Override ++ public Node poll() { ++ if (head == null) { ++ return null; ++ } ++ ++ Node node = head; ++ Node next = node.next_node; ++ ++ if (next == null) { ++ clear(); // reset the tails array ++ } else { ++ if (node.priority != next.priority) { ++ // If the head is also a tail, its entry in the array ++ // can be cleared; there is no previous node with the ++ // same priority to take its place. ++ tails[node.priority + OFFSET] = null; ++ } ++ ++ node.next_node = null; ++ next.prev_node = null; ++ head = next; ++ ++ size--; ++ } ++ ++ return node; ++ } ++ ++ @Override ++ public Node peek() { ++ return head; ++ } ++ ++ @Override ++ public void clear() { ++ for (Node node = head; node != null; ) { ++ Node n = node; ++ node = node.next_node; ++ ++ n.prev_node = null; ++ n.next_node = null; ++ } ++ ++ Arrays.fill(tails, null); ++ ++ head = null; ++ tail = null; ++ ++ size = 0; ++ } ++ ++ @Override ++ public Iterator iterator() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public int size() { ++ return size; ++ } ++ ++ public boolean contains(Node node) { ++ return node == head || node.prev_node != null; ++ } ++ ++ private void move(Node node, int priority) { ++ remove(node); ++ insert(node, priority); ++ } ++ ++ private void remove(Node node) { ++ Node prev = node.prev_node; ++ Node next = node.next_node; ++ ++ if (node == tail || node.priority != next.priority) { ++ // assign a new tail for this node's priority ++ if (node == head || node.priority != prev.priority) { ++ // there is no other node with the same priority; clear ++ tails[node.priority + OFFSET] = null; ++ } else { ++ // the previous node in the queue becomes the tail ++ tails[node.priority + OFFSET] = prev; ++ } ++ } ++ ++ if (node == head) { ++ head = next; ++ } else { ++ prev.next_node = next; ++ } ++ if (node == tail) { ++ tail = prev; ++ } else { ++ next.prev_node = prev; ++ } ++ ++ node.prev_node = null; ++ node.next_node = null; ++ ++ size--; ++ } ++ ++ private void insert(Node node, int priority) { ++ node.priority = priority; ++ ++ // nodes are sorted by priority (highest to lowest) ++ // nodes with the same priority are ordered FIFO ++ if (head == null) { ++ // first element in this queue \o/ ++ head = tail = node; ++ } else if (priority > head.priority) { ++ linkHead(node); ++ } else if (priority <= tail.priority) { ++ linkTail(node); ++ } else { ++ // since the node is neither the head nor the tail ++ // findPrev is guaranteed to find a non-null element ++ linkAfter(findPrev(node), node); ++ } ++ ++ tails[priority + OFFSET] = node; ++ ++ size++; ++ } ++ ++ private void linkHead(Node node) { ++ node.next_node = head; ++ head.prev_node = node; ++ head = node; ++ } ++ ++ private void linkTail(Node node) { ++ tail.next_node = node; ++ node.prev_node = tail; ++ tail = node; ++ } ++ ++ private void linkAfter(Node prev, Node node) { ++ linkBetween(prev, node, prev.next_node); ++ } ++ ++ private void linkBetween(Node prev, Node node, Node next) { ++ prev.next_node = node; ++ node.prev_node = prev; ++ ++ node.next_node = next; ++ next.prev_node = node; ++ } ++ ++ private Node findPrev(Node node) { ++ Node prev = null; ++ ++ for (int i = node.priority + OFFSET; i < tails.length; i++) { ++ prev = tails[i]; ++ ++ if (prev != null) { ++ break; ++ } ++ } ++ ++ return prev; ++ } ++} +diff --git a/src/main/java/alternate/current/wire/SimpleQueue.java b/src/main/java/alternate/current/wire/SimpleQueue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2b30074252551e1dc55d5be17d26fb4a2d8eb2e4 +--- /dev/null ++++ b/src/main/java/alternate/current/wire/SimpleQueue.java +@@ -0,0 +1,112 @@ ++package alternate.current.wire; ++ ++import java.util.AbstractQueue; ++import java.util.Iterator; ++ ++public class SimpleQueue extends AbstractQueue { ++ ++ private WireNode head; ++ private WireNode tail; ++ ++ private int size; ++ ++ SimpleQueue() { ++ ++ } ++ ++ @Override ++ public boolean offer(WireNode node) { ++ if (node == null) { ++ throw new NullPointerException(); ++ } ++ ++ if (tail == null) { ++ head = tail = node; ++ } else { ++ tail.next_wire = node; ++ tail = node; ++ } ++ ++ size++; ++ ++ return true; ++ } ++ ++ @Override ++ public WireNode poll() { ++ if (head == null) { ++ return null; ++ } ++ ++ WireNode node = head; ++ WireNode next = node.next_wire; ++ ++ if (next == null) { ++ head = tail = null; ++ } else { ++ node.next_wire = null; ++ head = next; ++ } ++ ++ size--; ++ ++ return node; ++ } ++ ++ @Override ++ public WireNode peek() { ++ return head; ++ } ++ ++ @Override ++ public void clear() { ++ for (WireNode node = head; node != null; ) { ++ WireNode n = node; ++ node = node.next_wire; ++ ++ n.next_wire = null; ++ } ++ ++ head = null; ++ tail = null; ++ ++ size = 0; ++ } ++ ++ @Override ++ public Iterator iterator() { ++ return new SimpleIterator(); ++ } ++ ++ @Override ++ public int size() { ++ return size; ++ } ++ ++ private class SimpleIterator implements Iterator { ++ ++ private WireNode curr; ++ private WireNode next; ++ ++ private SimpleIterator() { ++ next = head; ++ } ++ ++ @Override ++ public boolean hasNext() { ++ if (next == null && curr != null) { ++ next = curr.next_wire; ++ } ++ ++ return next != null; ++ } ++ ++ @Override ++ public WireNode next() { ++ curr = next; ++ next = curr.next_wire; ++ ++ return curr; ++ } ++ } ++} +diff --git a/src/main/java/alternate/current/wire/UpdateOrder.java b/src/main/java/alternate/current/wire/UpdateOrder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..29338efd16cf62bb49e81cce09fbafd9b4319e7c +--- /dev/null ++++ b/src/main/java/alternate/current/wire/UpdateOrder.java +@@ -0,0 +1,390 @@ ++package alternate.current.wire; ++ ++import java.util.Locale; ++import java.util.function.Consumer; ++ ++import alternate.current.wire.WireHandler.Directions; ++import alternate.current.wire.WireHandler.NodeProvider; ++ ++public enum UpdateOrder { ++ ++ HORIZONTAL_FIRST_OUTWARD( ++ new int[][] { ++ new int[] { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH, Directions.DOWN, Directions.UP }, ++ new int[] { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST , Directions.DOWN, Directions.UP }, ++ new int[] { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH, Directions.DOWN, Directions.UP }, ++ new int[] { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST , Directions.DOWN, Directions.UP } ++ ++ }, ++ new int[][] { ++ new int[] { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH }, ++ new int[] { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST }, ++ new int[] { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH }, ++ new int[] { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST } ++ } ++ ) { ++ ++ @Override ++ public void forEachNeighbor(NodeProvider nodes, Node source, int forward, Consumer action) { ++ /* ++ * This iteration order is designed to be an extension of the Vanilla shape ++ * update order, and is determined as follows: ++ *
++ * 1. Each neighbor is identified by the step(s) you must take, starting at the ++ * source, to reach it. Each step is 1 block, thus the position of a neighbor is ++ * encoded by the direction(s) of the step(s), e.g. (right), (down), (up, left), ++ * etc. ++ *
++ * 2. Neighbors are iterated over in pairs that lie on opposite sides of the ++ * source. ++ *
++ * 3. Neighbors are iterated over in order of their distance from the source, ++ * moving outward. This means they are iterated over in 3 groups: direct ++ * neighbors first, then diagonal neighbors, and last are the far neighbors that ++ * are 2 blocks directly out. ++ *
++ * 4. The order within each group is determined using the following basic order: ++ * { front, back, right, left, down, up }. This order was chosen because it ++ * converts to the following order of absolute directions when west is said to ++ * be 'forward': { west, east, north, south, down, up } - this is the order of ++ * shape updates. ++ */ ++ ++ int rightward = (forward + 1) & 0b11; ++ int backward = (forward + 2) & 0b11; ++ int leftward = (forward + 3) & 0b11; ++ int downward = Directions.DOWN; ++ int upward = Directions.UP; ++ ++ Node front = nodes.getNeighbor(source, forward); ++ Node right = nodes.getNeighbor(source, rightward); ++ Node back = nodes.getNeighbor(source, backward); ++ Node left = nodes.getNeighbor(source, leftward); ++ Node below = nodes.getNeighbor(source, downward); ++ Node above = nodes.getNeighbor(source, upward); ++ ++ // direct neighbors (6) ++ action.accept(front); ++ action.accept(back); ++ action.accept(right); ++ action.accept(left); ++ action.accept(below); ++ action.accept(above); ++ ++ // diagonal neighbors (12) ++ action.accept(nodes.getNeighbor(front, rightward)); ++ action.accept(nodes.getNeighbor(back, leftward)); ++ action.accept(nodes.getNeighbor(front, leftward)); ++ action.accept(nodes.getNeighbor(back, rightward)); ++ action.accept(nodes.getNeighbor(front, downward)); ++ action.accept(nodes.getNeighbor(back, upward)); ++ action.accept(nodes.getNeighbor(front, upward)); ++ action.accept(nodes.getNeighbor(back, downward)); ++ action.accept(nodes.getNeighbor(right, downward)); ++ action.accept(nodes.getNeighbor(left, upward)); ++ action.accept(nodes.getNeighbor(right, upward)); ++ action.accept(nodes.getNeighbor(left, downward)); ++ ++ // far neighbors (6) ++ action.accept(nodes.getNeighbor(front, forward)); ++ action.accept(nodes.getNeighbor(back, backward)); ++ action.accept(nodes.getNeighbor(right, rightward)); ++ action.accept(nodes.getNeighbor(left, leftward)); ++ action.accept(nodes.getNeighbor(below, downward)); ++ action.accept(nodes.getNeighbor(above, upward)); ++ } ++ }, ++ HORIZONTAL_FIRST_INWARD( ++ new int[][] { ++ new int[] { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH, Directions.DOWN, Directions.UP }, ++ new int[] { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST , Directions.DOWN, Directions.UP }, ++ new int[] { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH, Directions.DOWN, Directions.UP }, ++ new int[] { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST , Directions.DOWN, Directions.UP } ++ }, ++ new int[][] { ++ new int[] { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH }, ++ new int[] { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST }, ++ new int[] { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH }, ++ new int[] { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST } ++ } ++ ) { ++ ++ @Override ++ public void forEachNeighbor(NodeProvider nodes, Node source, int forward, Consumer action) { ++ /* ++ * This iteration order is designed to be an inversion of the above update ++ * order, and is determined as follows: ++ *
++ * 1. Each neighbor is identified by the step(s) you must take, starting at the ++ * source, to reach it. Each step is 1 block, thus the position of a neighbor is ++ * encoded by the direction(s) of the step(s), e.g. (right), (down), (up, left), ++ * etc. ++ *
++ * 2. Neighbors are iterated over in pairs that lie on opposite sides of the ++ * source. ++ *
++ * 3. Neighbors are iterated over in order of their distance from the source, ++ * moving inward. This means they are iterated over in 3 groups: neighbors that ++ * are 2 blocks directly out first, then diagonal neighbors, and last are direct ++ * neighbors. ++ *
++ * 4. The order within each group is determined using the following basic order: ++ * { front, back, right, left, down, up }. This order was chosen because it ++ * converts to the following order of absolute directions when west is said to ++ * be 'forward': { west, east, north, south, down, up } - this is the order of ++ * shape updates. ++ */ ++ ++ int rightward = (forward + 1) & 0b11; ++ int backward = (forward + 2) & 0b11; ++ int leftward = (forward + 3) & 0b11; ++ int downward = Directions.DOWN; ++ int upward = Directions.UP; ++ ++ Node front = nodes.getNeighbor(source, forward); ++ Node right = nodes.getNeighbor(source, rightward); ++ Node back = nodes.getNeighbor(source, backward); ++ Node left = nodes.getNeighbor(source, leftward); ++ Node below = nodes.getNeighbor(source, downward); ++ Node above = nodes.getNeighbor(source, upward); ++ ++ // far neighbors (6) ++ action.accept(nodes.getNeighbor(front, forward)); ++ action.accept(nodes.getNeighbor(back, backward)); ++ action.accept(nodes.getNeighbor(right, rightward)); ++ action.accept(nodes.getNeighbor(left, leftward)); ++ action.accept(nodes.getNeighbor(below, downward)); ++ action.accept(nodes.getNeighbor(above, upward)); ++ ++ // diagonal neighbors (12) ++ action.accept(nodes.getNeighbor(front, rightward)); ++ action.accept(nodes.getNeighbor(back, leftward)); ++ action.accept(nodes.getNeighbor(front, leftward)); ++ action.accept(nodes.getNeighbor(back, rightward)); ++ action.accept(nodes.getNeighbor(front, downward)); ++ action.accept(nodes.getNeighbor(back, upward)); ++ action.accept(nodes.getNeighbor(front, upward)); ++ action.accept(nodes.getNeighbor(back, downward)); ++ action.accept(nodes.getNeighbor(right, downward)); ++ action.accept(nodes.getNeighbor(left, upward)); ++ action.accept(nodes.getNeighbor(right, upward)); ++ action.accept(nodes.getNeighbor(left, downward)); ++ ++ ++ // direct neighbors (6) ++ action.accept(front); ++ action.accept(back); ++ action.accept(right); ++ action.accept(left); ++ action.accept(below); ++ action.accept(above); ++ } ++ }, ++ VERTICAL_FIRST_OUTWARD( ++ new int[][] { ++ new int[] { Directions.DOWN, Directions.UP, Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH }, ++ new int[] { Directions.DOWN, Directions.UP, Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST }, ++ new int[] { Directions.DOWN, Directions.UP, Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH }, ++ new int[] { Directions.DOWN, Directions.UP, Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST } ++ }, ++ new int[][] { ++ new int[] { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH }, ++ new int[] { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST }, ++ new int[] { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH }, ++ new int[] { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST } ++ } ++ ) { ++ ++ @Override ++ public void forEachNeighbor(NodeProvider nodes, Node source, int forward, Consumer action) { ++ /* ++ * This iteration order is designed to be the opposite of the Vanilla shape ++ * update order, and is determined as follows: ++ *
++ * 1. Each neighbor is identified by the step(s) you must take, starting at the ++ * source, to reach it. Each step is 1 block, thus the position of a neighbor is ++ * encoded by the direction(s) of the step(s), e.g. (right), (down), (up, left), ++ * etc. ++ *
++ * 2. Neighbors are iterated over in pairs that lie on opposite sides of the ++ * source. ++ *
++ * 3. Neighbors are iterated over in order of their distance from the source, ++ * moving outward. This means they are iterated over in 3 groups: direct ++ * neighbors first, then diagonal neighbors, and last are the far neighbors that ++ * are 2 blocks directly out. ++ *
++ * 4. The order within each group is determined using the following basic order: ++ * { down, up, front, back, right, left }. This order was chosen because it ++ * converts to the following order of absolute directions when west is said to ++ * be 'forward': { down, up west, east, north, south } - this is the order of ++ * shape updates, with the vertical directions moved to the front. ++ */ ++ ++ int rightward = (forward + 1) & 0b11; ++ int backward = (forward + 2) & 0b11; ++ int leftward = (forward + 3) & 0b11; ++ int downward = Directions.DOWN; ++ int upward = Directions.UP; ++ ++ Node front = nodes.getNeighbor(source, forward); ++ Node right = nodes.getNeighbor(source, rightward); ++ Node back = nodes.getNeighbor(source, backward); ++ Node left = nodes.getNeighbor(source, leftward); ++ Node below = nodes.getNeighbor(source, downward); ++ Node above = nodes.getNeighbor(source, upward); ++ ++ // direct neighbors (6) ++ action.accept(below); ++ action.accept(above); ++ action.accept(front); ++ action.accept(back); ++ action.accept(right); ++ action.accept(left); ++ ++ // diagonal neighbors (12) ++ action.accept(nodes.getNeighbor(below, forward)); ++ action.accept(nodes.getNeighbor(above, backward)); ++ action.accept(nodes.getNeighbor(below, backward)); ++ action.accept(nodes.getNeighbor(above, forward)); ++ action.accept(nodes.getNeighbor(below, rightward)); ++ action.accept(nodes.getNeighbor(above, leftward)); ++ action.accept(nodes.getNeighbor(below, leftward)); ++ action.accept(nodes.getNeighbor(above, rightward)); ++ action.accept(nodes.getNeighbor(front, rightward)); ++ action.accept(nodes.getNeighbor(back, leftward)); ++ action.accept(nodes.getNeighbor(front, leftward)); ++ action.accept(nodes.getNeighbor(back, rightward)); ++ ++ // far neighbors (6) ++ action.accept(nodes.getNeighbor(below, downward)); ++ action.accept(nodes.getNeighbor(above, upward)); ++ action.accept(nodes.getNeighbor(front, forward)); ++ action.accept(nodes.getNeighbor(back, backward)); ++ action.accept(nodes.getNeighbor(right, rightward)); ++ action.accept(nodes.getNeighbor(left, leftward)); ++ } ++ }, ++ VERTICAL_FIRST_INWARD( ++ new int[][] { ++ new int[] { Directions.DOWN, Directions.UP, Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH }, ++ new int[] { Directions.DOWN, Directions.UP, Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST }, ++ new int[] { Directions.DOWN, Directions.UP, Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH }, ++ new int[] { Directions.DOWN, Directions.UP, Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST } ++ }, ++ new int[][] { ++ new int[] { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH }, ++ new int[] { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST }, ++ new int[] { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH }, ++ new int[] { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST } ++ } ++ ) { ++ ++ @Override ++ public void forEachNeighbor(NodeProvider nodes, Node source, int forward, Consumer action) { ++ /* ++ * This iteration order is designed to be an inversion of the above update ++ * order, and is determined as follows: ++ *
++ * 1. Each neighbor is identified by the step(s) you must take, starting at the ++ * source, to reach it. Each step is 1 block, thus the position of a neighbor is ++ * encoded by the direction(s) of the step(s), e.g. (right), (down), (up, left), ++ * etc. ++ *
++ * 2. Neighbors are iterated over in pairs that lie on opposite sides of the ++ * source. ++ *
++ * 3. Neighbors are iterated over in order of their distance from the source, ++ * moving inward. This means they are iterated over in 3 groups: neighbors that ++ * are 2 blocks directly out first, then diagonal neighbors, and last are direct ++ * neighbors. ++ *
++ * 4. The order within each group is determined using the following basic order: ++ * { down, up, front, back, right, left }. This order was chosen because it ++ * converts to the following order of absolute directions when west is said to ++ * be 'forward': { down, up west, east, north, south } - this is the order of ++ * shape updates, with the vertical directions moved to the front. ++ */ ++ ++ int rightward = (forward + 1) & 0b11; ++ int backward = (forward + 2) & 0b11; ++ int leftward = (forward + 3) & 0b11; ++ int downward = Directions.DOWN; ++ int upward = Directions.UP; ++ ++ Node front = nodes.getNeighbor(source, forward); ++ Node right = nodes.getNeighbor(source, rightward); ++ Node back = nodes.getNeighbor(source, backward); ++ Node left = nodes.getNeighbor(source, leftward); ++ Node below = nodes.getNeighbor(source, downward); ++ Node above = nodes.getNeighbor(source, upward); ++ ++ // far neighbors (6) ++ action.accept(nodes.getNeighbor(below, downward)); ++ action.accept(nodes.getNeighbor(above, upward)); ++ action.accept(nodes.getNeighbor(front, forward)); ++ action.accept(nodes.getNeighbor(back, backward)); ++ action.accept(nodes.getNeighbor(right, rightward)); ++ action.accept(nodes.getNeighbor(left, leftward)); ++ ++ // diagonal neighbors (12) ++ action.accept(nodes.getNeighbor(below, forward)); ++ action.accept(nodes.getNeighbor(above, backward)); ++ action.accept(nodes.getNeighbor(below, backward)); ++ action.accept(nodes.getNeighbor(above, forward)); ++ action.accept(nodes.getNeighbor(below, rightward)); ++ action.accept(nodes.getNeighbor(above, leftward)); ++ action.accept(nodes.getNeighbor(below, leftward)); ++ action.accept(nodes.getNeighbor(above, rightward)); ++ action.accept(nodes.getNeighbor(front, rightward)); ++ action.accept(nodes.getNeighbor(back, leftward)); ++ action.accept(nodes.getNeighbor(front, leftward)); ++ action.accept(nodes.getNeighbor(back, rightward)); ++ ++ // direct neighbors (6) ++ action.accept(below); ++ action.accept(above); ++ action.accept(front); ++ action.accept(back); ++ action.accept(right); ++ action.accept(left); ++ } ++ }; ++ ++ private final int[][] directNeighbors; ++ private final int[][] cardinalNeighbors; ++ ++ private UpdateOrder(int[][] directNeighbors, int[][] cardinalNeighbors) { ++ this.directNeighbors = directNeighbors; ++ this.cardinalNeighbors = cardinalNeighbors; ++ } ++ ++ public String id() { ++ return name().toLowerCase(Locale.ENGLISH); ++ } ++ ++ public static UpdateOrder byId(String id) { ++ return valueOf(id.toUpperCase(Locale.ENGLISH)); ++ } ++ ++ public int[] directNeighbors(int forward) { ++ return directNeighbors[forward]; ++ } ++ ++ public int[] cardinalNeighbors(int forward) { ++ return cardinalNeighbors[forward]; ++ } ++ ++ /** ++ * Iterate over all neighboring nodes of the given source node. The iteration ++ * order is built from relative directions around the source, depending on the ++ * given 'forward' direction. This is an effort to eliminate any directional ++ * biases that would be emerge in rotationally symmetric circuits if the update ++ * order was built from absolute directions around the source. ++ *
++ * Each update order must include the source's direct neighbors, but further ++ * neighbors may not be included. ++ */ ++ public abstract void forEachNeighbor(NodeProvider nodes, Node source, int forward, Consumer action); ++ ++} +diff --git a/src/main/java/alternate/current/wire/WireConnection.java b/src/main/java/alternate/current/wire/WireConnection.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4fd8cb29024330397cfe4cbc1f237d285bfb7b3e +--- /dev/null ++++ b/src/main/java/alternate/current/wire/WireConnection.java +@@ -0,0 +1,30 @@ ++package alternate.current.wire; ++ ++/** ++ * This class represents a connection between some WireNode (the 'owner') and a ++ * neighboring WireNode. Two wires are considered to be connected if power can ++ * flow from one wire to the other (and/or vice versa). ++ * ++ * @author Space Walker ++ */ ++public class WireConnection { ++ ++ /** The connected wire. */ ++ final WireNode wire; ++ /** Cardinal direction to the connected wire. */ ++ final int iDir; ++ /** True if the owner of the connection can provide power to the connected wire. */ ++ final boolean offer; ++ /** True if the connected wire can provide power to the owner of the connection. */ ++ final boolean accept; ++ ++ /** The next connection in the sequence. */ ++ WireConnection next; ++ ++ WireConnection(WireNode wire, int iDir, boolean offer, boolean accept) { ++ this.wire = wire; ++ this.iDir = iDir; ++ this.offer = offer; ++ this.accept = accept; ++ } ++} +diff --git a/src/main/java/alternate/current/wire/WireConnectionManager.java b/src/main/java/alternate/current/wire/WireConnectionManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f03b313e58385d626490a9e64c9616fd08aa951e +--- /dev/null ++++ b/src/main/java/alternate/current/wire/WireConnectionManager.java +@@ -0,0 +1,134 @@ ++package alternate.current.wire; ++ ++import java.util.Arrays; ++import java.util.function.Consumer; ++ ++import alternate.current.wire.WireHandler.Directions; ++import alternate.current.wire.WireHandler.NodeProvider; ++ ++public class WireConnectionManager { ++ ++ /** The owner of these connections. */ ++ final WireNode owner; ++ ++ /** The first connection for each cardinal direction. */ ++ private final WireConnection[] heads; ++ ++ private WireConnection head; ++ private WireConnection tail; ++ ++ /** The total number of connections. */ ++ int total; ++ ++ /** ++ * A 4 bit number that encodes in which direction(s) the owner has connections ++ * to other wires. ++ */ ++ private int flowTotal; ++ /** The direction of flow based connections to other wires. */ ++ int iFlowDir; ++ ++ WireConnectionManager(WireNode owner) { ++ this.owner = owner; ++ ++ this.heads = new WireConnection[Directions.HORIZONTAL.length]; ++ ++ this.total = 0; ++ ++ this.flowTotal = 0; ++ this.iFlowDir = -1; ++ } ++ ++ void set(NodeProvider nodes) { ++ if (total > 0) { ++ clear(); ++ } ++ ++ boolean belowIsConductor = nodes.getNeighbor(owner, Directions.DOWN).isConductor(); ++ boolean aboveIsConductor = nodes.getNeighbor(owner, Directions.UP).isConductor(); ++ ++ for (int iDir = 0; iDir < Directions.HORIZONTAL.length; iDir++) { ++ Node neighbor = nodes.getNeighbor(owner, iDir); ++ ++ if (neighbor.isWire()) { ++ add(neighbor.asWire(), iDir, true, true); ++ } else { ++ boolean sideIsConductor = neighbor.isConductor(); ++ ++ if (!sideIsConductor) { ++ Node node = nodes.getNeighbor(neighbor, Directions.DOWN); ++ ++ if (node.isWire()) { ++ add(node.asWire(), iDir, belowIsConductor, true); ++ } ++ } ++ if (!aboveIsConductor) { ++ Node node = nodes.getNeighbor(neighbor, Directions.UP); ++ ++ if (node.isWire()) { ++ add(node.asWire(), iDir, true, sideIsConductor); ++ } ++ } ++ } ++ } ++ ++ if (total > 0) { ++ iFlowDir = WireHandler.FLOW_IN_TO_FLOW_OUT[flowTotal]; ++ } ++ } ++ ++ private void clear() { ++ Arrays.fill(heads, null); ++ ++ head = null; ++ tail = null; ++ ++ total = 0; ++ ++ flowTotal = 0; ++ iFlowDir = -1; ++ } ++ ++ private void add(WireNode wire, int iDir, boolean offer, boolean accept) { ++ add(new WireConnection(wire, iDir, offer, accept)); ++ } ++ ++ private void add(WireConnection connection) { ++ if (head == null) { ++ head = connection; ++ tail = connection; ++ } else { ++ tail.next = connection; ++ tail = connection; ++ } ++ ++ total++; ++ ++ if (heads[connection.iDir] == null) { ++ heads[connection.iDir] = connection; ++ flowTotal |= (1 << connection.iDir); ++ } ++ } ++ ++ /** ++ * Iterate over all connections. Use this method if the iteration order is not ++ * important. ++ */ ++ void forEach(Consumer consumer) { ++ for (WireConnection c = head; c != null; c = c.next) { ++ consumer.accept(c); ++ } ++ } ++ ++ /** ++ * Iterate over all connections. Use this method if the iteration order is ++ * important. ++ */ ++ void forEach(Consumer consumer, UpdateOrder updateOrder, int iFlowDir) { ++ for (int iDir : updateOrder.cardinalNeighbors(iFlowDir)) { ++ for (WireConnection c = heads[iDir]; c != null && c.iDir == iDir; c = c.next) { ++ consumer.accept(c); ++ } ++ } ++ } ++} +diff --git a/src/main/java/alternate/current/wire/WireHandler.java b/src/main/java/alternate/current/wire/WireHandler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..259b301b2c8b64cb7974a235afb260e0e991af54 +--- /dev/null ++++ b/src/main/java/alternate/current/wire/WireHandler.java +@@ -0,0 +1,1073 @@ ++package alternate.current.wire; ++ ++import java.util.Iterator; ++import java.util.Queue; ++ ++import it.unimi.dsi.fastutil.longs.Long2ObjectMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry; ++import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Direction; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.redstone.InstantNeighborUpdater; ++import net.minecraft.world.level.redstone.NeighborUpdater; ++import net.minecraft.world.level.redstone.Orientation; ++import net.minecraft.world.level.redstone.Redstone; ++ ++/** ++ * This class handles power changes for redstone wire. The algorithm was ++ * designed with the following goals in mind: ++ *
++ * 1. Minimize the number of times a wire checks its surroundings to determine ++ * its power level. ++ *
++ * 2. Minimize the number of block and shape updates emitted. ++ *
++ * 3. Emit block and shape updates in a deterministic, non-locational order, ++ * fixing bug MC-11193. ++ * ++ *

++ * In Vanilla redstone wire is laggy because it fails on points 1 and 2. ++ * ++ *

++ * Redstone wire updates recursively and each wire calculates its power level in ++ * isolation rather than in the context of the network it is a part of. This ++ * means a wire in a grid can change its power level over half a dozen times ++ * before settling on its final value. This problem used to be worse in 1.13 and ++ * below, where a wire would only decrease its power level by 1 at a time. ++ * ++ *

++ * In addition to this, a wire emits 42 block updates and up to 22 shape updates ++ * each time it changes its power level. ++ * ++ *

++ * Of those 42 block updates, 6 are to itself, which are thus not only ++ * redundant, but a big source of lag, since those cause the wire to ++ * unnecessarily re-calculate its power level. A block only has 24 neighbors ++ * within a Manhattan distance of 2, meaning 12 of the remaining 36 block ++ * updates are duplicates and thus also redundant. ++ * ++ *

++ * Of the 22 shape updates, only 6 are strictly necessary. The other 16 are sent ++ * to blocks diagonally above and below. These are necessary if a wire changes ++ * its connections, but not when it changes its power level. ++ * ++ *

++ * Redstone wire in Vanilla also fails on point 3, though this is more of a ++ * quality-of-life issue than a lag issue. The recursive nature in which it ++ * updates, combined with the location-dependent order in which each wire ++ * updates its neighbors, makes the order in which neighbors of a wire network ++ * are updated incredibly inconsistent and seemingly random. ++ * ++ *

++ * Alternate Current fixes each of these problems as follows. ++ * ++ *

++ * 1. To make sure a wire calculates its power level as little as possible, we ++ * remove the recursive nature in which redstone wire updates in Vanilla. ++ * Instead, we build a network of connected wires, find those wires that receive ++ * redstone power from "outside" the network, and spread the power from there. ++ * This has a few advantages: ++ *
++ * - Each wire checks for power from non-wire components at most once, and from ++ * nearby wires just twice. ++ *
++ * - Each wire only sets its power level in the world once. This is important, ++ * because calls to Level.setBlock are even more expensive than calls to ++ * Level.getBlockState. ++ * ++ *

++ * 2. There are 2 obvious ways in which we can reduce the number of block and ++ * shape updates. ++ *
++ * - Get rid of the 18 redundant block updates and 16 redundant shape updates, ++ * so each wire only emits 24 block updates and 6 shape updates whenever it ++ * changes its power level. ++ *
++ * - Only emit block updates and shape updates once a wire reaches its final ++ * power level, rather than at each intermediary stage. ++ *
++ * For an individual wire, these two optimizations are the best you can do, but ++ * for an entire grid, you can do better! ++ * ++ *

++ * Since we calculate the power of the entire network, sending block and shape ++ * updates to the wires in it is redundant. Removing those updates can reduce ++ * the number of block and shape updates by up to 20%. ++ * ++ *

++ * 3. To make the order of block updates to neighbors of a network ++ * deterministic, the first thing we must do is to replace the location- ++ * dependent order in which a wire updates its neighbors. Instead, we base it on ++ * the direction of power flow. This part of the algorithm was heavily inspired ++ * by theosib's 'RedstoneWireTurbo', which you can read more about in theosib's ++ * comment on Mojira here ++ * or by checking out its implementation in carpet mod here. ++ * ++ *

++ * The idea is to determine the direction of power flow through a wire based on ++ * the power it receives from neighboring wires. For example, if the only power ++ * a wire receives is from a neighboring wire to its west, it can be said that ++ * the direction of power flow through the wire is east. ++ * ++ *

++ * We make the order of block updates to neighbors of a wire depend on what is ++ * determined to be the direction of power flow. This not only removes ++ * locationality entirely, it even removes directionality in a large number of ++ * cases. Unlike in 'RedstoneWireTurbo', however, I have decided to keep a ++ * directional element in ambiguous cases, rather than to introduce randomness, ++ * though this is trivial to change. ++ * ++ *

++ * While this change fixes the block update order of individual wires, we must ++ * still address the overall block update order of a network. This turns out to ++ * be a simple fix, because of a change we made earlier: we search through the ++ * network for wires that receive power from outside it, and spread the power ++ * from there. If we make each wire transmit its power to neighboring wires in ++ * an order dependent on the direction of power flow, we end up with a ++ * non-locational and largely non-directional wire update order. ++ * ++ * @author Space Walker ++ */ ++public class WireHandler { ++ ++ public static class Directions { ++ ++ public static final Direction[] ALL = { Direction.WEST, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.DOWN, Direction.UP }; ++ public static final Direction[] HORIZONTAL = { Direction.WEST, Direction.NORTH, Direction.EAST, Direction.SOUTH }; ++ ++ // Indices for the arrays above. ++ // The cardinal directions are ordered clockwise. This allows ++ // for conversion between relative and absolute directions ++ // ('left' 'right' vs 'east' 'west') with simple arithmetic: ++ // If some Direction index 'iDir' is considered 'forward', then ++ // '(iDir + 1) & 0b11' is 'right', '(iDir + 2) & 0b11' is 'backward', etc. ++ public static final int WEST = 0b000; // 0 ++ public static final int NORTH = 0b001; // 1 ++ public static final int EAST = 0b010; // 2 ++ public static final int SOUTH = 0b011; // 3 ++ public static final int DOWN = 0b100; // 4 ++ public static final int UP = 0b101; // 5 ++ ++ public static int iOpposite(int iDir) { ++ return iDir ^ (0b10 >>> (iDir >>> 2)); ++ } ++ ++ public static int index(Direction dir) { ++ for (int i = 0; i < ALL.length; i++) { ++ if (dir == ALL[i]) { ++ return i; ++ } ++ } ++ ++ return -1; ++ } ++ } ++ ++ /** ++ * This conversion table takes in information about incoming flow, and outputs ++ * the determined outgoing flow. ++ * ++ *

++ * The input is a 4 bit number that encodes the incoming flow. Each bit ++ * represents a cardinal direction, and when it is 'on', there is flow in that ++ * direction. ++ * ++ *

++ * The output is a single Direction index, or -1 for ambiguous cases. ++ * ++ *

++ * The outgoing flow is determined as follows: ++ * ++ *

++ * If there is just 1 direction of incoming flow, that direction will be the ++ * direction of outgoing flow. ++ * ++ *

++ * If there are 2 directions of incoming flow, and these directions are not each ++ * other's opposites, the direction that is 'more clockwise' will be the ++ * direction of outgoing flow. More precisely, the direction that is 1 clockwise ++ * turn from the other is picked. ++ * ++ *

++ * If there are 3 directions of incoming flow, the two opposing directions ++ * cancel each other out, and the remaining direction will be the direction of ++ * outgoing flow. ++ * ++ *

++ * In all other cases, the flow is completely ambiguous. ++ */ ++ static final int[] FLOW_IN_TO_FLOW_OUT = { ++ -1, // 0b0000: - -> x ++ Directions.WEST, // 0b0001: west -> west ++ Directions.NORTH, // 0b0010: north -> north ++ Directions.NORTH, // 0b0011: west/north -> north ++ Directions.EAST, // 0b0100: east -> east ++ -1, // 0b0101: west/east -> x ++ Directions.EAST, // 0b0110: north/east -> east ++ Directions.NORTH, // 0b0111: west/north/east -> north ++ Directions.SOUTH, // 0b1000: south -> south ++ Directions.WEST, // 0b1001: west/south -> west ++ -1, // 0b1010: north/south -> x ++ Directions.WEST, // 0b1011: west/north/south -> west ++ Directions.SOUTH, // 0b1100: east/south -> south ++ Directions.SOUTH, // 0b1101: west/east/south -> south ++ Directions.EAST, // 0b1110: north/east/south -> east ++ -1, // 0b1111: west/north/east/south -> x ++ }; ++ /** ++ * Update order of shape updates, matching that of Vanilla. ++ */ ++ static final int[] SHAPE_UPDATE_ORDER = { Directions.WEST, Directions.EAST, Directions.NORTH, Directions.SOUTH, Directions.DOWN, Directions.UP }; ++ ++ private static final int POWER_MIN = Redstone.SIGNAL_MIN; ++ private static final int POWER_MAX = Redstone.SIGNAL_MAX; ++ private static final int POWER_STEP = 1; ++ ++ // If Vanilla will ever multi-thread the ticking of levels, there should ++ // be only one WireHandler per level, in case redstone updates in multiple ++ // levels at the same time. There are already mods that add multi-threading ++ // as well. ++ private final ServerLevel level; ++ ++ /** Map of wires and neighboring blocks. */ ++ private final Long2ObjectMap nodes; ++ /** Queue for the breadth-first search through the network. */ ++ private final Queue search; ++ /** Queue of updates to wires and neighboring blocks. */ ++ private final Queue updates; ++ ++ private final NeighborUpdater neighborUpdater; ++ ++ // Rather than creating new nodes every time a network is updated we keep ++ // a cache of nodes that can be re-used. ++ private Node[] nodeCache; ++ private int nodeCount; ++ ++ /** Is this WireHandler currently working through the update queue? */ ++ private boolean updating; ++ /** The update order currently in use. */ ++ private UpdateOrder updateOrder; ++ ++ public WireHandler(ServerLevel level) { ++ this.level = level; ++ ++ this.nodes = new Long2ObjectOpenHashMap<>(); ++ this.search = new SimpleQueue(); ++ this.updates = new PriorityQueue(); ++ ++ this.neighborUpdater = new InstantNeighborUpdater(this.level); ++ ++ this.nodeCache = new Node[16]; ++ this.fillNodeCache(0, 16); ++ } ++ ++ private Node getOrAddNode(BlockPos pos) { ++ // just pass in null, then the state will only be retrieved ++ // if there is no node as this position yet ++ return getOrAddNode(pos, null); ++ } ++ ++ /** ++ * Retrieve the {@link alternate.current.wire.Node Node} that represents the ++ * block at the given position in the level. ++ */ ++ private Node getOrAddNode(BlockPos pos, BlockState state) { ++ return nodes.compute(pos.asLong(), (key, node) -> { ++ if (node == null) { ++ // If there is not yet a node at this position, retrieve and ++ // update one from the cache. ++ return getNextNode(pos, state != null ? state : level.getBlockState(pos)); ++ } ++ if (node.invalid) { ++ return revalidateNode(node); ++ } ++ ++ return node; ++ }); ++ } ++ ++ /** ++ * Remove and return the {@link alternate.current.wire.Node Node} at the given ++ * position. ++ */ ++ private Node removeNode(BlockPos pos) { ++ return nodes.remove(pos.asLong()); ++ } ++ ++ /** ++ * Return a node that represents the given position and block state. If it is a ++ * wire, then create a new {@link alternate.current.wire.WireNode WireNode}. ++ * Otherwise, grab the next {@link alternate.current.wire.Node Node} from the ++ * cache and update it. ++ */ ++ private Node getNextNode(BlockPos pos, BlockState state) { ++ return state.is(Blocks.REDSTONE_WIRE) ? new WireNode(level, pos, state) : getNextNode().set(pos, state, true); ++ } ++ ++ /** ++ * Grab the first unused node from the cache. If all of the cache is already in ++ * use, increase it in size first. ++ */ ++ private Node getNextNode() { ++ if (nodeCount == nodeCache.length) { ++ increaseNodeCache(); ++ } ++ ++ return nodeCache[nodeCount++]; ++ } ++ ++ private void increaseNodeCache() { ++ Node[] oldCache = nodeCache; ++ nodeCache = new Node[oldCache.length << 1]; ++ ++ for (int index = 0; index < oldCache.length; index++) { ++ nodeCache[index] = oldCache[index]; ++ } ++ ++ fillNodeCache(oldCache.length, nodeCache.length); ++ } ++ ++ private void fillNodeCache(int start, int end) { ++ for (int index = start; index < end; index++) { ++ nodeCache[index] = new Node(level); ++ } ++ } ++ ++ /** ++ * Try to revalidate the given node by looking at the block state that is ++ * occupying its position. If the given node is a wire but the block state is ++ * not, or vice versa, a new node must be created/grabbed from the cache. ++ * Otherwise, the node can be quickly revalidated with the new block state. ++ */ ++ private Node revalidateNode(Node node) { ++ if (!node.invalid) { ++ return node; ++ } ++ ++ BlockPos pos = node.pos; ++ BlockState state = level.getBlockState(pos); ++ ++ boolean wasWire = node.isWire(); ++ boolean isWire = state.is(Blocks.REDSTONE_WIRE); ++ ++ if (wasWire != isWire) { ++ return getNextNode(pos, state); ++ } ++ ++ node.invalid = false; ++ ++ if (isWire) { ++ // No need to update the block state of this wire - it will grab ++ // the current block state just before setting power anyway. ++ WireNode wire = node.asWire(); ++ ++ wire.root = false; ++ wire.discovered = false; ++ wire.searched = false; ++ } else { ++ node.set(pos, state, false); ++ } ++ ++ return node; ++ } ++ ++ /** ++ * Retrieve the neighbor of a node in the given direction and create a link ++ * between the two nodes if they are not yet linked. This link makes accessing ++ * neighbors of a node signficantly faster. ++ */ ++ private Node getNeighbor(Node node, int iDir) { ++ Node neighbor = node.neighbors[iDir]; ++ ++ if (neighbor == null || neighbor.invalid) { ++ Direction dir = Directions.ALL[iDir]; ++ BlockPos pos = node.pos.relative(dir); ++ ++ Node oldNeighbor = neighbor; ++ neighbor = getOrAddNode(pos); ++ ++ if (neighbor != oldNeighbor) { ++ int iOpp = Directions.iOpposite(iDir); ++ ++ node.neighbors[iDir] = neighbor; ++ neighbor.neighbors[iOpp] = node; ++ } ++ } ++ ++ return neighbor; ++ } ++ ++ /** ++ * This method should be called whenever a wire receives a block update. ++ */ ++ public boolean onWireUpdated(BlockPos pos, BlockState state, Orientation orientation) { ++ Node node = getOrAddNode(pos, state); ++ ++ if (!node.isWire()) { ++ return false; // we should never get here ++ } ++ ++ WireNode wire = node.asWire(); ++ ++ invalidate(); ++ revalidateNode(wire); ++ findRoots(wire, orientation); ++ tryUpdate(); ++ ++ return true; ++ } ++ ++ /** ++ * This method should be called whenever a wire is placed. ++ */ ++ public void onWireAdded(BlockPos pos, BlockState state) { ++ Node node = getOrAddNode(pos, state); ++ ++ if (!node.isWire()) { ++ return; // we should never get here ++ } ++ ++ WireNode wire = node.asWire(); ++ wire.added = true; ++ ++ invalidate(); ++ revalidateNode(wire); ++ findRoot(wire); ++ tryUpdate(); ++ } ++ ++ /** ++ * This method should be called whenever a wire is removed. ++ */ ++ public void onWireRemoved(BlockPos pos, BlockState state) { ++ Node node = removeNode(pos); ++ WireNode wire; ++ ++ if (node == null || !node.isWire()) { ++ wire = new WireNode(level, pos, state); ++ } else { ++ wire = node.asWire(); ++ } ++ ++ wire.invalid = true; ++ wire.removed = true; ++ ++ // If these fields are set to 'true', the removal of this wire was part of ++ // already ongoing power changes, so we can exit early here. ++ if (updating && wire.shouldBreak) { ++ return; ++ } ++ ++ invalidate(); ++ revalidateNode(wire); ++ findRoot(wire); ++ tryUpdate(); ++ } ++ ++ /** ++ * The nodes map is a snapshot of the state of the world. It becomes invalid ++ * when power changes are carried out, since the block and shape updates can ++ * lead to block changes. If these block changes cause the network to be updated ++ * again every node must be invalidated, and revalidated before it is used ++ * again. This ensures the power calculations of the network are accurate. ++ */ ++ private void invalidate() { ++ if (updating && !nodes.isEmpty()) { ++ Iterator> it = Long2ObjectMaps.fastIterator(nodes); ++ ++ while (it.hasNext()) { ++ Entry entry = it.next(); ++ Node node = entry.getValue(); ++ ++ node.invalid = true; ++ } ++ } ++ ++ updateOrder = UpdateOrder.values()[level.paperConfig().misc.alternateCurrentUpdateOrder.ordinal()]; ++ } ++ ++ /** ++ * Look for wires at and around the given position that are in an invalid state ++ * and require power changes. These wires are called 'roots' because it is only ++ * when these wires change power level that neighboring wires must adjust as ++ * well. ++ * ++ *

++ * While it is strictly only necessary to check the wire at the given position, ++ * if that wire is part of a network, it is beneficial to check its surroundings ++ * for other wires that require power changes. This is because a network can ++ * receive power at multiple points. Consider the following setup: ++ * ++ *

++ * (top-down view, W = wire, L = lever, _ = air/other) ++ *
{@code _ _ W _ _ } ++ *
{@code _ W W W _ } ++ *
{@code W W L W W } ++ *
{@code _ W W W _ } ++ *
{@code _ _ W _ _ } ++ * ++ *

++ * The lever powers four wires in the network at once. If this is identified ++ * correctly, the entire network can (un)power at once. While it is not ++ * practical to cover every possible situation where a network is (un)powered ++ * from multiple points at once, checking for common cases like the one ++ * described above is relatively straight-forward. ++ */ ++ private void findRoots(WireNode wire, Orientation orientation) { ++ // horizontal direction bias for update order purposes ++ int iDirBias = -1; ++ ++ if (orientation != null) { ++ Direction dir = orientation.getFront().getAxis().isHorizontal() ++ ? orientation.getFront() ++ : orientation.getUp(); ++ ++ iDirBias = Directions.index(dir); ++ } ++ ++ findRoot(wire, iDirBias); ++ ++ // If the wire at the given position is not in an invalid state ++ // we can exit early. ++ if (!wire.searched) { ++ return; ++ } ++ ++ if (orientation == null) { ++ // no neighborChanged orientation present, look around in all sides ++ for (int iDir : updateOrder.directNeighbors(wire.iFlowDir)) { ++ findRootsAround(wire, iDir); ++ } ++ } else { ++ // use the orientation from the neighborChanged update to look for roots only behind ++ findRootsAround(wire, Directions.index(orientation.getFront().getOpposite())); ++ } ++ } ++ ++ /** ++ * Look for wires around a neighbor of the given wire that require power changes. ++ */ ++ private void findRootsAround(WireNode wire, int iDir) { ++ Node node = getNeighbor(wire, iDir); ++ ++ if (node.isConductor() || node.isSignalSource()) { ++ for (int iSide : updateOrder.cardinalNeighbors(wire.iFlowDir)) { ++ Node neighbor = getNeighbor(node, iSide); ++ ++ if (neighbor.isWire()) { ++ findRoot(neighbor.asWire(), iSide); ++ } ++ } ++ } ++ } ++ ++ private void findRoot(WireNode wire) { ++ findRoot(wire, -1); ++ } ++ ++ /** ++ * Check if the given wire requires power changes. If it does, queue it for the ++ * breadth-first search as a root. ++ */ ++ private void findRoot(WireNode wire, int iDiscoveryDir) { ++ // Each wire only needs to be checked once. ++ if (wire.discovered) { ++ return; ++ } ++ ++ discover(wire); ++ findExternalPower(wire); ++ findPower(wire, false); ++ ++ if (needsUpdate(wire)) { ++ searchRoot(wire, iDiscoveryDir); ++ } ++ } ++ ++ /** ++ * Prepare the given wire for the breadth-first search. This means: ++ *
++ * - Check if the wire should break. Rather than breaking the wire right away, ++ * its effects are integrated into the power calculations. ++ *
++ * - Reset the virtual and external power. ++ *
++ * - Find connections to neighboring wires. ++ */ ++ private void discover(WireNode wire) { ++ if (wire.discovered) { ++ return; ++ } ++ ++ wire.discovered = true; ++ wire.searched = false; ++ ++ if (!wire.removed && !wire.shouldBreak && !wire.state.canSurvive(level, wire.pos)) { ++ wire.shouldBreak = true; ++ } ++ ++ wire.virtualPower = wire.currentPower; ++ wire.externalPower = POWER_MIN - 1; ++ ++ wire.connections.set(this::getNeighbor); ++ } ++ ++ /** ++ * Determine the power level the given wire receives from the blocks around it. ++ * Power from non-wire components only needs to be computed if power from ++ * neighboring wires has decreased, so as to determine how low the power of the ++ * wire can fall. ++ */ ++ private void findPower(WireNode wire, boolean ignoreSearched) { ++ // As wire power is (re-)computed, flow information must be reset. ++ wire.virtualPower = wire.externalPower; ++ wire.flowIn = 0; ++ ++ // If the wire is removed or going to break, its power level should always be ++ // the minimum value. This is because it (effectively) no longer exists, so ++ // cannot provide any power to neighboring wires. ++ if (wire.removed || wire.shouldBreak) { ++ return; ++ } ++ ++ // Power received from neighboring wires will never exceed POWER_MAX - ++ // POWER_STEP, so if the external power is already larger than or equal to ++ // that, there is no need to check for power from neighboring wires. ++ if (wire.externalPower < (POWER_MAX - POWER_STEP)) { ++ findWirePower(wire, ignoreSearched); ++ } ++ } ++ ++ /** ++ * Determine the power the given wire receives from connected neighboring wires ++ * and update the virtual power accordingly. ++ */ ++ private void findWirePower(WireNode wire, boolean ignoreSearched) { ++ wire.connections.forEach(connection -> { ++ if (!connection.accept) { ++ return; ++ } ++ ++ WireNode neighbor = connection.wire; ++ ++ if (!ignoreSearched || !neighbor.searched) { ++ int power = Math.max(POWER_MIN, neighbor.virtualPower - POWER_STEP); ++ int iOpp = Directions.iOpposite(connection.iDir); ++ ++ wire.offerPower(power, iOpp); ++ } ++ }); ++ } ++ ++ /** ++ * Determine the redstone signal the given wire receives from non-wire ++ * components and update the virtual power accordingly. ++ */ ++ private void findExternalPower(WireNode wire) { ++ // If the wire is removed or going to break, its power level should always be ++ // the minimum value. Thus external power need not be computed. ++ // In other cases external power need only be computed once. ++ if (wire.removed || wire.shouldBreak || wire.externalPower >= POWER_MIN) { ++ return; ++ } ++ ++ wire.externalPower = getExternalPower(wire); ++ ++ if (wire.externalPower > wire.virtualPower) { ++ wire.virtualPower = wire.externalPower; ++ } ++ } ++ ++ /** ++ * Determine the redstone signal the given wire receives from non-wire ++ * components. ++ */ ++ private int getExternalPower(WireNode wire) { ++ int power = POWER_MIN; ++ ++ for (int iDir = 0; iDir < Directions.ALL.length; iDir++) { ++ Node neighbor = getNeighbor(wire, iDir); ++ ++ // Power from wires is handled separately. ++ if (neighbor.isWire()) { ++ continue; ++ } ++ ++ // Since 1.16 there is a block that is both a conductor and a signal ++ // source: the target block! ++ if (neighbor.isConductor()) { ++ power = Math.max(power, getDirectSignalTo(wire, neighbor)); ++ } ++ if (neighbor.isSignalSource()) { ++ power = Math.max(power, neighbor.state.getSignal(level, neighbor.pos, Directions.ALL[iDir])); ++ } ++ ++ if (power >= POWER_MAX) { ++ return POWER_MAX; ++ } ++ } ++ ++ return power; ++ } ++ ++ /** ++ * Determine the direct signal the given wire receives from neighboring blocks ++ * through the given conductor node. ++ */ ++ private int getDirectSignalTo(WireNode wire, Node node) { ++ int power = POWER_MIN; ++ ++ for (int iDir = 0; iDir < Directions.ALL.length; iDir++) { ++ Node neighbor = getNeighbor(node, iDir); ++ ++ if (neighbor.isSignalSource()) { ++ power = Math.max(power, neighbor.state.getDirectSignal(level, neighbor.pos, Directions.ALL[iDir])); ++ ++ if (power >= POWER_MAX) { ++ return POWER_MAX; ++ } ++ } ++ } ++ ++ return power; ++ } ++ ++ /** ++ * Check if the given wire needs to update its state in the world. ++ */ ++ private boolean needsUpdate(WireNode wire) { ++ return wire.removed || wire.shouldBreak || wire.virtualPower != wire.currentPower; ++ } ++ ++ /** ++ * Queue the given wire for the breadth-first search as a root. ++ */ ++ private void searchRoot(WireNode wire, int iBackupFlowDir) { ++ if (wire.connections.iFlowDir >= 0) { ++ // power flow direction takes precedent ++ iBackupFlowDir = wire.connections.iFlowDir; ++ } else if (iBackupFlowDir < 0) { ++ // use default value if none is given ++ iBackupFlowDir = 0; ++ } ++ ++ search(wire, true, iBackupFlowDir); ++ } ++ ++ /** ++ * Queue the given wire for the breadth-first search and set a backup flow ++ * direction. ++ */ ++ private void search(WireNode wire, boolean root, int iBackupFlowDir) { ++ search.offer(wire); ++ ++ wire.root = root; ++ wire.searched = true; ++ // Normally the flow is not set until the power level is updated. However, ++ // in networks with multiple power sources the update order between them ++ // depends on which was discovered first. To make this less prone to ++ // directionality, each wire node is given a 'backup' flow. For roots, this ++ // is the determined flow of their connections. For non-roots this is the ++ // direction from which they were discovered. ++ wire.iFlowDir = iBackupFlowDir; ++ } ++ ++ private void tryUpdate() { ++ if (!search.isEmpty()) { ++ update(); ++ } ++ if (!updating) { ++ nodes.clear(); ++ nodeCount = 0; ++ } ++ } ++ ++ /** ++ * Update the network and neighboring blocks. This is done in 3 steps. ++ * ++ *

++ * 1. Search through the network ++ *
++ * Conduct a breadth-first search around the roots to find wires that are in an ++ * invalid state and need power changes. ++ * ++ *

++ * 2. Depower the network ++ *
++ * Depower all wires in the network. This allows power to be spread most ++ * efficiently. ++ * ++ *

++ * 3. Power the network ++ *
++ * Work through the update queue, setting the new power level of each wire and ++ * updating neighboring blocks. After a wire has updated its power level, it ++ * will emit shape updates and queue updates for neighboring wires and blocks. ++ */ ++ private void update() { ++ // Search through the network for wires that need power changes. This includes ++ // the roots as well as any wires that will be affected by power changes to ++ // those roots. ++ searchNetwork(); ++ ++ // Depower all the wires in the network. ++ depowerNetwork(); ++ ++ // Bring each wire up to its new power level and update neighboring blocks. ++ try { ++ powerNetwork(); ++ } catch (Throwable t) { ++ // If anything goes wrong while carrying out power changes, this field must ++ // be reset to 'false', or the wire handler will be locked out of carrying ++ // out power changes until the world is reloaded. ++ updating = false; ++ ++ throw t; ++ } ++ } ++ ++ /** ++ * Search through the network for wires that are in an invalid state and need ++ * power changes. These wires are added to the end of the queue, so that their ++ * neighbors can be searched next. ++ */ ++ private void searchNetwork() { ++ for (WireNode wire : search) { ++ // The order in which wires are searched will influence the order in ++ // which they update their power levels. ++ wire.connections.forEach(connection -> { ++ if (!connection.offer) { ++ return; ++ } ++ ++ WireNode neighbor = connection.wire; ++ ++ if (neighbor.searched) { ++ return; ++ } ++ ++ discover(neighbor); ++ findPower(neighbor, false); ++ ++ // If power from neighboring wires has decreased, check for power ++ // from non-wire components so as to determine how low power can ++ // fall. ++ if (neighbor.virtualPower < neighbor.currentPower) { ++ findExternalPower(neighbor); ++ } ++ ++ if (needsUpdate(neighbor)) { ++ search(neighbor, false, connection.iDir); ++ } ++ }, updateOrder, wire.iFlowDir); ++ } ++ } ++ ++ /** ++ * Depower all wires in the network so that power can be spread from the power ++ * sources. ++ */ ++ private void depowerNetwork() { ++ while (!search.isEmpty()) { ++ WireNode wire = search.poll(); ++ findPower(wire, true); ++ ++ if (wire.root || wire.removed || wire.shouldBreak || wire.virtualPower > POWER_MIN) { ++ queueWire(wire); ++ } else { ++ // Wires that do not receive any power do not queue power changes ++ // until they are offered power from a neighboring wire. To ensure ++ // that they accept any power from neighboring wires and thus queue ++ // their power changes, their virtual power is set to below the ++ // minimum. ++ wire.virtualPower--; ++ } ++ } ++ } ++ ++ /** ++ * Work through the update queue, setting the new power level of each wire, then ++ * queueing updates to connected wires and neighboring blocks. ++ */ ++ private void powerNetwork() { ++ // If an instantaneous update chain causes updates to another network ++ // (or the same network in another place), new power changes will be ++ // integrated into the already ongoing power queue, so we can exit early ++ // here. ++ if (updating) { ++ return; ++ } ++ ++ updating = true; ++ ++ while (!updates.isEmpty()) { ++ Node node = updates.poll(); ++ ++ if (node.isWire()) { ++ WireNode wire = node.asWire(); ++ ++ if (!needsUpdate(wire)) { ++ continue; ++ } ++ ++ findPowerFlow(wire); ++ transmitPower(wire); ++ ++ if (wire.setPower()) { ++ queueNeighbors(wire); ++ ++ // If the wire was newly placed or removed, shape updates have ++ // already been emitted. However, unlike before 1.19, neighbor ++ // updates are now queued, so to preserve behavior parity with ++ // previous versions, we emit extra shape updates here to ++ // notify neighboring observers. ++ updateNeighborShapes(wire); ++ } ++ } else { ++ WireNode neighborWire = node.neighborWire; ++ ++ if (neighborWire != null) { ++ BlockPos neighborPos = neighborWire.pos; ++ Block neighborBlock = neighborWire.state.getBlock(); ++ ++ updateBlock(node, neighborPos, neighborBlock); ++ } ++ } ++ } ++ ++ updating = false; ++ } ++ ++ /** ++ * Use the information of incoming power flow to determine the direction of ++ * power flow through this wire. If that flow is ambiguous, try to use a flow ++ * direction based on connections to neighboring wires. If that is also ++ * ambiguous, use the backup value that was set when the wire was first added to ++ * the network. ++ */ ++ private void findPowerFlow(WireNode wire) { ++ int flow = FLOW_IN_TO_FLOW_OUT[wire.flowIn]; ++ ++ if (flow >= 0) { ++ wire.iFlowDir = flow; ++ } else if (wire.connections.iFlowDir >= 0) { ++ wire.iFlowDir = wire.connections.iFlowDir; ++ } ++ } ++ ++ /** ++ * Transmit power from the given wire to neighboring wires and queue updates to ++ * those wires. ++ */ ++ private void transmitPower(WireNode wire) { ++ wire.connections.forEach(connection -> { ++ if (!connection.offer) { ++ return; ++ } ++ ++ WireNode neighbor = connection.wire; ++ ++ int power = Math.max(POWER_MIN, wire.virtualPower - POWER_STEP); ++ int iDir = connection.iDir; ++ ++ if (neighbor.offerPower(power, iDir)) { ++ queueWire(neighbor); ++ } ++ }, updateOrder, wire.iFlowDir); ++ } ++ ++ /** ++ * Emit shape updates around the given wire. ++ */ ++ private void updateNeighborShapes(WireNode wire) { ++ BlockPos wirePos = wire.pos; ++ BlockState wireState = wire.state; ++ ++ for (int iDir : SHAPE_UPDATE_ORDER) { ++ Node neighbor = getNeighbor(wire, iDir); ++ ++ // Shape updates to redstone wire are very expensive, and should never happen ++ // as a result of power changes anyway, while shape updates to air do nothing. ++ // The current block state at this position *could* be wrong, but if you somehow ++ // manage to place a block where air used to be during the execution of a shape ++ // update I am very impressed and you deserve to have some broken behavior. ++ if (!neighbor.isWire() && !neighbor.state.isAir()) { ++ int iOpp = Directions.iOpposite(iDir); ++ Direction opp = Directions.ALL[iOpp]; ++ ++ updateShape(neighbor, opp, wirePos, wireState); ++ } ++ } ++ } ++ ++ private void updateShape(Node node, Direction dir, BlockPos neighborPos, BlockState neighborState) { ++ neighborUpdater.shapeUpdate(dir, neighborState, node.pos, neighborPos, Block.UPDATE_CLIENTS, 512); ++ } ++ ++ /** ++ * Queue block updates to nodes around the given wire. ++ */ ++ private void queueNeighbors(WireNode wire) { ++ updateOrder.forEachNeighbor(this::getNeighbor, wire, wire.iFlowDir, neighbor -> queueNeighbor(neighbor, wire)); ++ } ++ ++ /** ++ * Queue the given node for an update from the given neighboring wire. ++ */ ++ private void queueNeighbor(Node node, WireNode neighborWire) { ++ // Updates to wires are queued when power is transmitted. ++ // While this check makes sure wires in the network are not given block ++ // updates, it also prevents block updates to wires in neighboring networks. ++ // While this should not make a difference in theory, in practice, it is ++ // possible to force a network into an invalid state without updating it, even ++ // if it is relatively obscure. ++ // While I was willing to make this compromise in return for some significant ++ // performance gains in certain setups, if you are not, you can add all the ++ // positions of the network to a set and filter out block updates to wires in ++ // the network that way. ++ // Block updates to air do nothing, so those are skipped as well. ++ // The current block state at this position *could* be wrong, but if you somehow ++ // manage to place a block where air used to be during the execution of a block ++ // update I am very impressed and you deserve to have some broken behavior. ++ if (!node.isWire() && !node.state.isAir()) { ++ node.neighborWire = neighborWire; ++ updates.offer(node); ++ } ++ } ++ ++ /** ++ * Queue the given wire for a power change. If the wire does not need a power ++ * change (perhaps because its power has already changed), transmit power to ++ * neighboring wires. ++ */ ++ private void queueWire(WireNode wire) { ++ if (needsUpdate(wire)) { ++ updates.offer(wire); ++ } else { ++ findPowerFlow(wire); ++ transmitPower(wire); ++ } ++ } ++ ++ /** ++ * Emit a block update to the given node. ++ */ ++ private void updateBlock(Node node, BlockPos neighborPos, Block neighborBlock) { ++ // redstone wire is the only block that uses the neighborChanged orientation ++ // so leaving it as null should not be an issue ++ neighborUpdater.neighborChanged(node.pos, neighborBlock, null); ++ } ++ ++ @FunctionalInterface ++ public static interface NodeProvider { ++ ++ public Node getNeighbor(Node node, int iDir); ++ ++ } ++} +diff --git a/src/main/java/alternate/current/wire/WireNode.java b/src/main/java/alternate/current/wire/WireNode.java +new file mode 100644 +index 0000000000000000000000000000000000000000..298076a0db4e6ee6e4775ac43bf749d9f5689bdb +--- /dev/null ++++ b/src/main/java/alternate/current/wire/WireNode.java +@@ -0,0 +1,122 @@ ++package alternate.current.wire; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.Mth; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.RedStoneWireBlock; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.redstone.Redstone; ++ ++/** ++ * A WireNode is a Node that represents a wire in the world. It stores all the ++ * information about the wire that the WireHandler needs to calculate power ++ * changes. ++ * ++ * @author Space Walker ++ */ ++public class WireNode extends Node { ++ ++ final WireConnectionManager connections; ++ ++ /** The power level this wire currently holds in the world. */ ++ int currentPower; ++ /** ++ * While calculating power changes for a network, this field is used to keep ++ * track of the power level this wire should have. ++ */ ++ int virtualPower; ++ /** The power level received from non-wire components. */ ++ int externalPower; ++ /** ++ * A 4-bit number that keeps track of the power flow of the wires that give this ++ * wire its power level. ++ */ ++ int flowIn; ++ /** The direction of power flow, based on the incoming flow. */ ++ int iFlowDir; ++ boolean added; ++ boolean removed; ++ boolean shouldBreak; ++ boolean root; ++ boolean discovered; ++ boolean searched; ++ ++ /** The next wire in the simple queue. */ ++ WireNode next_wire; ++ ++ WireNode(ServerLevel level, BlockPos pos, BlockState state) { ++ super(level); ++ ++ this.pos = pos.immutable(); ++ this.state = state; ++ ++ this.connections = new WireConnectionManager(this); ++ ++ this.virtualPower = this.currentPower = this.state.getValue(RedStoneWireBlock.POWER); ++ this.priority = priority(); ++ } ++ ++ @Override ++ Node set(BlockPos pos, BlockState state, boolean clearNeighbors) { ++ throw new UnsupportedOperationException("Cannot update a WireNode!"); ++ } ++ ++ @Override ++ int priority() { ++ return Mth.clamp(virtualPower, Redstone.SIGNAL_MIN, Redstone.SIGNAL_MAX); ++ } ++ ++ @Override ++ public boolean isWire() { ++ return true; ++ } ++ ++ @Override ++ public WireNode asWire() { ++ return this; ++ } ++ ++ boolean offerPower(int power, int iDir) { ++ if (removed || shouldBreak) { ++ return false; ++ } ++ if (power == virtualPower) { ++ flowIn |= (1 << iDir); ++ return false; ++ } ++ if (power > virtualPower) { ++ virtualPower = power; ++ flowIn = (1 << iDir); ++ ++ return true; ++ } ++ ++ return false; ++ } ++ ++ boolean setPower() { ++ if (removed) { ++ return true; ++ } ++ ++ state = level.getBlockState(pos); ++ ++ if (!state.is(Blocks.REDSTONE_WIRE)) { ++ return false; // we should never get here ++ } ++ ++ if (shouldBreak) { ++ Block.dropResources(state, level, pos); ++ level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_CLIENTS); ++ ++ return true; ++ } ++ ++ currentPower = LevelHelper.doRedstoneEvent(level, pos, currentPower, Mth.clamp(virtualPower, Redstone.SIGNAL_MIN, Redstone.SIGNAL_MAX));; ++ state = state.setValue(RedStoneWireBlock.POWER, currentPower); ++ ++ return LevelHelper.setWireState(level, pos, state, added); ++ } ++} +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 7e8713373315eebf57541f8afe10902681449ad9..7270a94246df73ee195156fc7b62470d090a337a 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -231,6 +231,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + public final UUID uuid; + public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent + public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent ++ private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current) + + public LevelChunk getChunkIfLoaded(int x, int z) { + return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately +@@ -2661,6 +2662,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + return this.chunkSource.getGenerator().getSeaLevel(); + } + ++ // Paper start - optimize redstone (Alternate Current) ++ @Override ++ public alternate.current.wire.WireHandler getWireHandler() { ++ return wireHandler; ++ } ++ // Paper end - optimize redstone (Alternate Current) ++ + private final class EntityCallbacks implements LevelCallback { + + EntityCallbacks() {} +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index d518493ecf3853b9f2aefceb72e1a4d2e9bf1184..27f9d167b5ae9ce5117798ea44324107df59425f 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -2018,6 +2018,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + public abstract FuelValues fuelValues(); + ++ // Paper start - optimize redstone (Alternate Current) ++ public alternate.current.wire.WireHandler getWireHandler() { ++ // This method is overridden in ServerLevel. ++ // Since Paper is a server platform there is no risk ++ // of this implementation being called. It is here ++ // only so this method can be called without casting ++ // an instance of Level to ServerLevel. ++ return null; ++ } ++ // Paper end - optimize redstone (Alternate Current) ++ + public static enum ExplosionInteraction implements StringRepresentable { + + NONE("none"), BLOCK("block"), MOB("mob"), TNT("tnt"), TRIGGER("trigger"), STANDARD("standard"); // CraftBukkit - Add STANDARD which will always use Explosion.Effect.DESTROY +diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +index 09b8f5335cb7651d90f4d1ca61b2ec5aa324e443..21f2c61023fadcce30452a02f067cd5d87e5d8dc 100644 +--- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +@@ -290,7 +290,7 @@ public class RedStoneWireBlock extends Block { + return floor.isFaceSturdy(world, pos, Direction.UP) || floor.is(Blocks.HOPPER); + } + +- // Paper start - Optimize redstone ++ // Paper start - Optimize redstone (Eigencraft) + // The bulk of the new functionality is found in RedstoneWireTurbo.java + com.destroystokyo.paper.util.RedstoneWireTurbo turbo = new com.destroystokyo.paper.util.RedstoneWireTurbo(this); + +@@ -372,7 +372,13 @@ public class RedStoneWireBlock extends Block { + @Override + protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) { + if (!oldState.is(state.getBlock()) && !world.isClientSide) { +- this.updateSurroundingRedstone(world, pos, state, null, true); // Paper - Optimize redstone ++ // Paper start - optimize redstone - replace call to updatePowerStrength ++ if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { ++ world.getWireHandler().onWireAdded(pos, state); // Alternate Current ++ } else { ++ this.updateSurroundingRedstone(world, pos, state, null, true); // Vanilla/Eigencraft ++ } ++ // Paper end + + for (Direction direction : Direction.Plane.VERTICAL) { + world.updateNeighborsAt(pos.relative(direction), this); +@@ -391,7 +397,12 @@ public class RedStoneWireBlock extends Block { + world.updateNeighborsAt(pos.relative(direction), this); + } + +- this.updateSurroundingRedstone(world, pos, state, null, false); // Paper - Optimize redstone ++ // Paper start - optimize redstone - replace call to updatePowerStrength ++ if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { ++ world.getWireHandler().onWireRemoved(pos, state); // Alternate Current ++ } else { ++ this.updateSurroundingRedstone(world, pos, state, null, false); // Vanilla/Eigencraft ++ } + this.updateNeighborsOfNeighboringWires(world, pos); + } + } +@@ -415,9 +426,15 @@ public class RedStoneWireBlock extends Block { + @Override + protected void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, @Nullable Orientation wireOrientation, boolean notify) { + if (!world.isClientSide) { ++ // Paper start - optimize redstone (Alternate Current) ++ // Alternate Current handles breaking of redstone wires in the WireHandler. ++ if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { ++ world.getWireHandler().onWireUpdated(pos, state, wireOrientation); ++ } else ++ // Paper end - optimize redstone (Alternate Current) + if (sourceBlock != this || !useExperimentalEvaluator(world)) { + if (state.canSurvive(world, pos)) { +- this.updateSurroundingRedstone(world, pos, state, wireOrientation, false); // Paper - Optimize redstone ++ this.updateSurroundingRedstone(world, pos, state, wireOrientation, false); // Paper - Optimize redstone (Eigencraft) + } else { + dropResources(state, world, pos); + world.removeBlock(pos, false); +diff --git a/src/main/java/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java b/src/main/java/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java +index 4a9dc307106687bec084244c0a76e3e30f244fe2..8342dd636531729a187aff1bd69878d7aef9d3eb 100644 +--- a/src/main/java/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java ++++ b/src/main/java/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java +@@ -17,6 +17,11 @@ public class ExperimentalRedstoneUtils { + if (up != null) { + orientation = orientation.withFront(up); + } ++ // Paper start - Optimize redstone (Alternate Current) - use default front instead of random ++ else if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { ++ orientation = orientation.withFront(Direction.WEST); ++ } ++ // Paper end - Optimize redstone (Alternate Current) + + return orientation; + } else { diff --git a/patches/server/1063-Fix-incorrect-invulnerability-damage-reduction.patch b/patches/server/1063-Fix-incorrect-invulnerability-damage-reduction.patch new file mode 100644 index 0000000000..2b5598fa46 --- /dev/null +++ b/patches/server/1063-Fix-incorrect-invulnerability-damage-reduction.patch @@ -0,0 +1,115 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Bjarne Koll +Date: Mon, 11 Nov 2024 21:35:27 +0100 +Subject: [PATCH] Fix incorrect invulnerability damage reduction + +Fixes incorrect spigot handling of the invulnerability damage +reduction applied when an already invulnerable entity is damaged with a +larger damage amount than the initial damage. +Vanilla still damages entities even if invulnerable if the damage to be +applied is larger than the previous damage taken. In that case, vanilla +applies the difference between the previous damage taken and the +proposed damage. + +Spigot's damage modifier API takes over the computation of damage +reducing effects, however spigot invokes this handling with the initial +damage before computing the difference to the previous damage amount. +This leads to the reduction values to generally be larger than expected, +as they are computed on the not-yet-reduced value. +Spigot applies these reductions after calling the EntityDamageEvent and +*then* subtracts the previous damage point, leading to the final damage +amount being smaller than expected. + +This patch cannot simply call the EntityDamageEvent with the reduced +damage, as that would lead to EntityDamageEvent#getDamage() returning +the already reduced damage, which breaks its method contract. +Instead, this patch makes use of the DamageModifier API, implementing +the last-damage-reduction as a DamageModifier. + +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 91735414a81c40861315ea2d4ca6fdce64b2c228..cc4cc42adc95fb9357d4cf94d81b6c0c109879c1 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -1505,12 +1505,12 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + // Paper start - only call damage event when actuallyHurt will be called - move call logic down +- event = this.handleEntityDamage(source, amount); ++ event = this.handleEntityDamage(source, amount, this.lastHurt); // Paper - fix invulnerability reduction in EntityDamageEvent - pass lastDamage reduction + amount = computeAmountFromEntityDamageEvent(event); + // Paper end - only call damage event when actuallyHurt will be called - move call logic down + + // CraftBukkit start +- if (!this.actuallyHurt(world, source, (float) event.getFinalDamage() - this.lastHurt, event)) { ++ if (!this.actuallyHurt(world, source, (float) event.getFinalDamage(), event)) { // Paper - fix invulnerability reduction in EntityDamageEvent - no longer subtract lastHurt, that is part of the damage event calc now + return false; + } + if (this instanceof ServerPlayer && event.getDamage() == 0 && originalAmount == 0) return false; // Paper - revert to vanilla damage - players are not affected by damage that is 0 - skip damage if the vanilla damage is 0 and was not modified by plugins in the event. +@@ -1519,7 +1519,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + flag1 = false; + } else { + // Paper start - only call damage event when actuallyHurt will be called - move call logic down +- event = this.handleEntityDamage(source, amount); ++ event = this.handleEntityDamage(source, amount, 0); // Paper - fix invulnerability reduction in EntityDamageEvent - pass lastDamage reduction (none in this branch) + amount = computeAmountFromEntityDamageEvent(event); + // Paper end - only call damage event when actuallyHurt will be called - move call logic down + // CraftBukkit start +@@ -2333,8 +2333,19 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + // CraftBukkit start +- private EntityDamageEvent handleEntityDamage(final DamageSource damagesource, float f) { ++ private EntityDamageEvent handleEntityDamage(final DamageSource damagesource, float f, final float invulnerabilityRelatedLastDamage) { // Paper - fix invulnerability reduction in EntityDamageEvent + float originalDamage = f; ++ // Paper start - fix invulnerability reduction in EntityDamageEvent ++ final com.google.common.base.Function invulnerabilityReductionEquation = d -> { ++ if (invulnerabilityRelatedLastDamage == 0) return 0D; // no last damage, no reduction ++ // last damage existed, this means the reduction *technically* is (new damage - last damage). ++ // If the event damage was changed to something less than invul damage, hard lock it at 0. ++ if (d < invulnerabilityRelatedLastDamage) return 0D; ++ return (double) -invulnerabilityRelatedLastDamage; ++ }; ++ final float originalInvulnerabilityReduction = invulnerabilityReductionEquation.apply((double) f).floatValue(); ++ f += originalInvulnerabilityReduction; ++ // Paper end - fix invulnerability reduction in EntityDamageEvent + + com.google.common.base.Function freezing = new com.google.common.base.Function() { + @Override +@@ -2411,7 +2422,12 @@ public abstract class LivingEntity extends Entity implements Attackable { + }; + float absorptionModifier = absorption.apply((double) f).floatValue(); + +- return CraftEventFactory.handleLivingEntityDamageEvent(this, damagesource, originalDamage, freezingModifier, hardHatModifier, blockingModifier, armorModifier, resistanceModifier, magicModifier, absorptionModifier, freezing, hardHat, blocking, armor, resistance, magic, absorption); ++ // Paper start - fix invulnerability reduction in EntityDamageEvent ++ return CraftEventFactory.handleLivingEntityDamageEvent(this, damagesource, originalDamage, freezingModifier, hardHatModifier, blockingModifier, armorModifier, resistanceModifier, magicModifier, absorptionModifier, freezing, hardHat, blocking, armor, resistance, magic, absorption, (damageModifierDoubleMap, damageModifierFunctionMap) -> { ++ damageModifierFunctionMap.put(DamageModifier.INVULNERABILITY_REDUCTION, invulnerabilityReductionEquation); ++ damageModifierDoubleMap.put(DamageModifier.INVULNERABILITY_REDUCTION, (double) originalInvulnerabilityReduction); ++ }); ++ // Paper end - fix invulnerability reduction in EntityDamageEvent + } + + protected boolean actuallyHurt(ServerLevel worldserver, final DamageSource damagesource, float f, final EntityDamageEvent event) { // void -> boolean, add final +diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +index deba03eb37012c638e08e20cd1c98e9db190c790..e37aaf77f94b97b736cc20ef070cefdff0400188 100644 +--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +@@ -1218,6 +1218,11 @@ public class CraftEventFactory { + private static final Function ZERO = Functions.constant(-0.0); + + public static EntityDamageEvent handleLivingEntityDamageEvent(Entity damagee, DamageSource source, double rawDamage, double freezingModifier, double hardHatModifier, double blockingModifier, double armorModifier, double resistanceModifier, double magicModifier, double absorptionModifier, Function freezing, Function hardHat, Function blocking, Function armor, Function resistance, Function magic, Function absorption) { ++ // Paper start - fix invulnerability reduction in EntityDamageEvent ++ return handleLivingEntityDamageEvent(damagee, source, rawDamage, freezingModifier, hardHatModifier, blockingModifier, armorModifier, resistanceModifier, magicModifier, absorptionModifier, freezing, hardHat, blocking, armor, resistance, magic, absorption, null); ++ } ++ public static EntityDamageEvent handleLivingEntityDamageEvent(Entity damagee, DamageSource source, double rawDamage, double freezingModifier, double hardHatModifier, double blockingModifier, double armorModifier, double resistanceModifier, double magicModifier, double absorptionModifier, Function freezing, Function hardHat, Function blocking, Function armor, Function resistance, Function magic, Function absorption, java.util.function.BiConsumer, Map>> callback) { ++ // Paper end - fix invulnerability reduction in EntityDamageEvent + Map modifiers = new EnumMap<>(DamageModifier.class); + Map> modifierFunctions = new EnumMap<>(DamageModifier.class); + modifiers.put(DamageModifier.BASE, rawDamage); +@@ -1242,6 +1247,7 @@ public class CraftEventFactory { + modifierFunctions.put(DamageModifier.MAGIC, magic); + modifiers.put(DamageModifier.ABSORPTION, absorptionModifier); + modifierFunctions.put(DamageModifier.ABSORPTION, absorption); ++ if (callback != null) callback.accept(modifiers, modifierFunctions); // Paper - fix invulnerability reduction in EntityDamageEvent + return CraftEventFactory.handleEntityDamageEvent(damagee, source, modifiers, modifierFunctions); + } + diff --git a/patches/server/1064-Fix-NPE-when-EntityResurrectEvent-is-uncancelled.patch b/patches/server/1064-Fix-NPE-when-EntityResurrectEvent-is-uncancelled.patch new file mode 100644 index 0000000000..e4f75e957c --- /dev/null +++ b/patches/server/1064-Fix-NPE-when-EntityResurrectEvent-is-uncancelled.patch @@ -0,0 +1,23 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Lulu13022002 <41980282+Lulu13022002@users.noreply.github.com> +Date: Mon, 18 Nov 2024 20:27:58 +0100 +Subject: [PATCH] Fix NPE when EntityResurrectEvent is uncancelled + + +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index cc4cc42adc95fb9357d4cf94d81b6c0c109879c1..a542bde48edd91929cb7e3dc62c425507a8118fa 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -1722,6 +1722,12 @@ public abstract class LivingEntity extends Entity implements Attackable { + if (!itemstack1.isEmpty() && itemstack != null) { // Paper - only reduce item if actual totem was found + itemstack1.shrink(1); + } ++ // Paper start - fix NPE when pre-cancelled EntityResurrectEvent is uncancelled ++ // restore the previous behavior in that case by defaulting to vanillas totem of undying efect ++ if (deathprotection == null) { ++ deathprotection = DeathProtection.TOTEM_OF_UNDYING; ++ } ++ // Paper end - fix NPE when pre-cancelled EntityResurrectEvent is uncancelled + if (itemstack != null && this instanceof ServerPlayer) { + // CraftBukkit end + ServerPlayer entityplayer = (ServerPlayer) this; diff --git a/patches/server/1065-API-to-check-if-the-server-is-sleeping.patch b/patches/server/1065-API-to-check-if-the-server-is-sleeping.patch new file mode 100644 index 0000000000..9f542e8138 --- /dev/null +++ b/patches/server/1065-API-to-check-if-the-server-is-sleeping.patch @@ -0,0 +1,37 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Abel +Date: Sun, 10 Nov 2024 16:32:34 +0100 +Subject: [PATCH] API to check if the server is sleeping + + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 6a4f99c56f8f49f5087a582a8c77be2c261537bb..78ec2c6d4546bc4eaedd64fa8340f5654876f65c 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -3186,4 +3186,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.emptyTicks >= this.pauseWhileEmptySeconds() * 20; ++ } ++ // Paper end - API to check if the server is sleeping + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 11ff5f3b5cd25f0ad6ca944d59bca8434f8510d8..7afc3d4244c096f78d48338da2eb65c4e834b6f1 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -3258,4 +3258,11 @@ public final class CraftServer implements Server { + return this.potionBrewer; + } + // Paper end ++ ++ // Paper start - API to check if the server is sleeping ++ @Override ++ public boolean isPaused() { ++ return this.console.isTickPaused(); ++ } ++ // Paper end - API to check if the server is sleeping + } diff --git a/patches/server/1066-API-to-allow-disallow-tick-sleeping.patch b/patches/server/1066-API-to-allow-disallow-tick-sleeping.patch new file mode 100644 index 0000000000..ffcfe813b3 --- /dev/null +++ b/patches/server/1066-API-to-allow-disallow-tick-sleeping.patch @@ -0,0 +1,67 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Abel +Date: Tue, 12 Nov 2024 22:25:20 +0100 +Subject: [PATCH] API to allow/disallow tick sleeping + + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 78ec2c6d4546bc4eaedd64fa8340f5654876f65c..c352c6717835d92cadc2bd131fba432714fe56e5 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -332,6 +332,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop pluginsBlockingSleep = new java.util.HashSet<>(); // Paper - API to allow/disallow tick sleeping + + public static S spin(Function serverFactory) { + AtomicReference atomicreference = new AtomicReference(); +@@ -1623,8 +1624,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0) { +- if (this.playerList.getPlayerCount() == 0 && !this.tickRateManager.isSprinting()) { ++ if (this.playerList.getPlayerCount() == 0 && !this.tickRateManager.isSprinting() && this.pluginsBlockingSleep.isEmpty()) { // Paper - API to allow/disallow tick sleeping + ++this.emptyTicks; + } else { + this.emptyTicks = 0; +@@ -3191,5 +3193,22 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.emptyTicks >= this.pauseWhileEmptySeconds() * 20; + } ++ ++ public void addPluginAllowingSleep(final String pluginName, final boolean value) { ++ if (!value) { ++ this.pluginsBlockingSleep.add(pluginName); ++ } else { ++ this.pluginsBlockingSleep.remove(pluginName); ++ } ++ } ++ ++ private void removeDisabledPluginsBlockingSleep() { ++ if (this.pluginsBlockingSleep.isEmpty()) { ++ return; ++ } ++ this.pluginsBlockingSleep.removeIf(plugin -> ( ++ !io.papermc.paper.plugin.manager.PaperPluginManagerImpl.getInstance().isPluginEnabled(plugin) ++ )); ++ } + // Paper end - API to check if the server is sleeping + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 7afc3d4244c096f78d48338da2eb65c4e834b6f1..ac8af406180bc680d46e8edc3da0fc2e5211345a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -3264,5 +3264,10 @@ public final class CraftServer implements Server { + public boolean isPaused() { + return this.console.isTickPaused(); + } ++ ++ @Override ++ public void allowPausing(final Plugin plugin, final boolean value) { ++ this.console.addPluginAllowingSleep(plugin.getName(), value); ++ } + // Paper end - API to check if the server is sleeping + } diff --git a/patches/server/1067-Configurable-Entity-Despawn-Time.patch b/patches/server/1067-Configurable-Entity-Despawn-Time.patch new file mode 100644 index 0000000000..85007eaf71 --- /dev/null +++ b/patches/server/1067-Configurable-Entity-Despawn-Time.patch @@ -0,0 +1,39 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Kevin Raneri +Date: Mon, 30 Sep 2024 09:50:55 -0700 +Subject: [PATCH] Configurable Entity Despawn Time + + +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index d7f2950223533c3cc2d182612d4c485edf3fba2b..6e01c521cdc6e7d18643248a352a7ad058dab294 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -388,6 +388,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + private UUID originWorld; + public boolean freezeLocked = false; // Paper - Freeze Tick Lock API + public boolean fixedPose = false; // Paper - Expand Pose API ++ private final int despawnTime; // Paper - entity despawn time limit + + public void setOrigin(@javax.annotation.Nonnull Location location) { + this.origin = location.toVector(); +@@ -570,6 +571,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + + public Entity(EntityType type, Level world) { + this.id = Entity.ENTITY_COUNTER.incrementAndGet(); ++ this.despawnTime = type == EntityType.PLAYER ? -1 : world.paperConfig().entities.spawning.despawnTime.getOrDefault(type, io.papermc.paper.configuration.type.number.IntOr.Disabled.DISABLED).or(-1); // Paper - entity despawn time limit + this.passengers = ImmutableList.of(); + this.deltaMovement = Vec3.ZERO; + this.bb = Entity.INITIAL_AABB; +@@ -872,6 +874,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + public void tick() { ++ // Paper start - entity despawn time limit ++ if (this.despawnTime >= 0 && this.tickCount >= this.despawnTime) { ++ this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); ++ return; ++ } ++ // Paper end - entity despawn time limit + this.baseTick(); + } + diff --git a/patches/server/1068-Expanded-Art-API.patch b/patches/server/1068-Expanded-Art-API.patch new file mode 100644 index 0000000000..56e4179720 --- /dev/null +++ b/patches/server/1068-Expanded-Art-API.patch @@ -0,0 +1,33 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: kokiriglade <60290002+celerry@users.noreply.github.com> +Date: Sat, 23 Nov 2024 18:58:49 +0000 +Subject: [PATCH] Expanded Art API + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftArt.java b/src/main/java/org/bukkit/craftbukkit/CraftArt.java +index 40af940193d0df66bbcdcf5f46132e304016a4d7..9d73954282104a6e315c1840feb7d6034d27cfbe 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftArt.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftArt.java +@@ -75,6 +75,22 @@ public class CraftArt implements Art, Handleable { + return this.paintingVariant.height(); + } + ++ // Paper start - Expand Art API ++ @Override ++ public net.kyori.adventure.text.Component title() { ++ return this.paintingVariant.title().map(io.papermc.paper.adventure.PaperAdventure::asAdventure).orElse(null); ++ } ++ ++ @Override ++ public net.kyori.adventure.text.Component author() { ++ return this.paintingVariant.author().map(io.papermc.paper.adventure.PaperAdventure::asAdventure).orElse(null); ++ } ++ ++ public net.kyori.adventure.key.Key assetId() { ++ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.paintingVariant.assetId()); ++ } ++ // Paper end - Expand Art API ++ + @Override + public int getId() { + return CraftRegistry.getMinecraftRegistry(Registries.PAINTING_VARIANT).getId(this.paintingVariant); diff --git a/patches/server/1069-Only-attempt-to-find-spawn-position-if-there-isn-t-a.patch b/patches/server/1069-Only-attempt-to-find-spawn-position-if-there-isn-t-a.patch new file mode 100644 index 0000000000..7f878f33b1 --- /dev/null +++ b/patches/server/1069-Only-attempt-to-find-spawn-position-if-there-isn-t-a.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrPowerGamerBR +Date: Thu, 28 Nov 2024 15:20:25 -0300 +Subject: [PATCH] Only attempt to find spawn position if there isn't a fixed + spawn position set + + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index c352c6717835d92cadc2bd131fba432714fe56e5..40eddfb94d85834b384ae34445c6159f904ae577 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -816,7 +816,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop -Date: Sun, 19 Dec 2021 09:13:41 -0800 -Subject: [PATCH] Only write chunk data to disk if it serializes without - throwing - -This ensures at least a valid version of the chunk exists -on disk, even if outdated - -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 057875cbbdc92ba49b429f9a129514760edb32a2..ff092c6d0cd436f14a9a4ff5c8ddbb5538d1a8c5 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 -@@ -539,6 +539,7 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche - - } - // Paper end -+ public static final int MAX_CHUNK_SIZE = 500 * 1024 * 1024; // Paper - don't write garbage data to disk if writing serialization fails - private class ChunkBuffer extends ByteArrayOutputStream implements ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkBuffer { // Paper - rewrite chunk system - - private final ChunkPos pos; -@@ -571,6 +572,23 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche - super.write(RegionFile.this.version.getId()); - this.pos = chunkcoordintpair; - } -+ // Paper start - don't write garbage data to disk if writing serialization fails -+ @Override -+ public void write(final int b) { -+ if (this.count > MAX_CHUNK_SIZE) { -+ throw new RegionFileStorage.RegionFileSizeException("Region file too large: " + this.count); -+ } -+ super.write(b); -+ } -+ -+ @Override -+ public void write(final byte[] b, final int off, final int len) { -+ if (this.count + len > MAX_CHUNK_SIZE) { -+ throw new RegionFileStorage.RegionFileSizeException("Region file too large: " + (this.count + len)); -+ } -+ super.write(b, off, len); -+ } -+ // Paper end - don't write garbage data to disk if writing serialization fails - - public void close() throws IOException { - ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count); -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 fdf8e18d24442178b52397acb482ffa3306a32e3..8d66d6b7aeb9feb54ebd83f5c73b45d42b9a7034 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 -@@ -19,6 +19,8 @@ import net.minecraft.world.level.ChunkPos; - - public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage { // Paper - rewrite chunk system - -+ private static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger(); // Paper -+ - public static final String ANVIL_EXTENSION = ".mca"; - private static final int MAX_CACHE_SIZE = 256; - public final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap(); -@@ -123,11 +125,24 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - // (and, the regionfile parameter is unused for writing until the write call) - final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData writeData = ((ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile)regionFile).moonrise$startWrite(compound, pos); - -+ try { // Paper - implement RegionFileSizeException - try { - NbtIo.write(compound, writeData.output()); - } finally { - writeData.output().close(); - } -+ // Paper start - implement RegionFileSizeException -+ } catch (final RegionFileSizeException ex) { -+ // note: it's OK if close() is called, as close() here will not issue a write to the RegionFile -+ // see startWrite -+ final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024); -+ LOGGER.error("Chunk at (" + chunkX + "," + chunkZ + ") in regionfile '" + regionFile.getPath().toString() + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk."); -+ return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData( -+ compound, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.DELETE, -+ null, null -+ ); -+ } -+ // Paper end - implement RegionFileSizeException - - return writeData; - } -@@ -378,10 +393,18 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - try { - NbtIo.write(nbt, (DataOutput) dataoutputstream); - regionfile.setOversized(pos.x, pos.z, false); // Paper - We don't do this anymore, mojang stores differently, but clear old meta flag if it exists to get rid of our own meta file once last oversized is gone -+ // Paper start - don't write garbage data to disk if writing serialization fails -+ dataoutputstream.close(); // Only write if successful -+ } catch (final RegionFileSizeException ex) { -+ regionfile.clear(pos); -+ final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024); -+ LOGGER.error("Chunk at (" + pos.x + "," + pos.z + ") in regionfile '" + regionfile.getPath().toString() + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk."); -+ return; -+ // Paper end - don't write garbage data to disk if writing serialization fails - } catch (Throwable throwable) { - if (dataoutputstream != null) { - try { -- dataoutputstream.close(); -+ //dataoutputstream.close(); // Paper - don't write garbage data to disk if writing serialization fails - } catch (Throwable throwable1) { - throwable.addSuppressed(throwable1); - } -@@ -389,10 +412,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - - throw throwable; - } -- -- if (dataoutputstream != null) { -- dataoutputstream.close(); -- } -+ // Paper - don't write garbage data to disk if writing serialization fails; move into try block to only write if successfully serialized - } - - } -@@ -435,4 +455,13 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - public RegionStorageInfo info() { - return this.info; - } -+ -+ // Paper start - don't write garbage data to disk if writing serialization fails -+ public static final class RegionFileSizeException extends RuntimeException { -+ -+ public RegionFileSizeException(String message) { -+ super(message); -+ } -+ } -+ // Paper end - don't write garbage data to disk if writing serialization fails - } diff --git a/patches/unapplied/server/1039-API-for-checking-sent-chunks.patch b/patches/unapplied/server/1039-API-for-checking-sent-chunks.patch deleted file mode 100644 index fc8b212100..0000000000 --- a/patches/unapplied/server/1039-API-for-checking-sent-chunks.patch +++ /dev/null @@ -1,46 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Flo0 -Date: Mon, 8 Apr 2024 16:43:16 +0200 -Subject: [PATCH] API for checking sent chunks - - -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index 71ed0230baf3115a53a8ce8f0a5c72f01954fffc..d4e497961578bb693275cdf95915b60b2cc76eb7 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -3500,6 +3500,35 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - } - // Paper end - -+ // Paper start - Add chunk view API -+ @Override -+ public Set getSentChunkKeys() { -+ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); -+ return it.unimi.dsi.fastutil.longs.LongSets.unmodifiable( -+ this.getHandle().moonrise$getChunkLoader().getSentChunksRaw().clone() -+ ); -+ } -+ -+ @Override -+ public Set getSentChunks() { -+ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); -+ final it.unimi.dsi.fastutil.longs.LongOpenHashSet rawChunkKeys = this.getHandle().moonrise$getChunkLoader().getSentChunksRaw(); -+ final it.unimi.dsi.fastutil.objects.ObjectOpenHashSet chunks = new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(rawChunkKeys.size()); -+ final org.bukkit.World world = this.getWorld(); -+ -+ final it.unimi.dsi.fastutil.longs.LongIterator iter = this.getHandle().moonrise$getChunkLoader().getSentChunksRaw().longIterator(); -+ while (iter.hasNext()) chunks.add(world.getChunkAt(iter.nextLong(), false)); -+ -+ return it.unimi.dsi.fastutil.objects.ObjectSets.unmodifiable(chunks); -+ } -+ -+ @Override -+ public boolean isChunkSent(final long chunkKey) { -+ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); -+ return this.getHandle().moonrise$getChunkLoader().getSentChunksRaw().contains(chunkKey); -+ } -+ // Paper end -+ - public Player.Spigot spigot() - { - return this.spigot; diff --git a/patches/unapplied/server/1040-Fix-CraftWorld-isChunkGenerated.patch b/patches/unapplied/server/1040-Fix-CraftWorld-isChunkGenerated.patch deleted file mode 100644 index ab99075727..0000000000 --- a/patches/unapplied/server/1040-Fix-CraftWorld-isChunkGenerated.patch +++ /dev/null @@ -1,44 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> -Date: Tue, 18 Jun 2024 12:43:06 -0700 -Subject: [PATCH] Fix CraftWorld#isChunkGenerated - -The upstream implementation is returning true for non-full chunks. - -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index ddf6403682025e544ab4060c32ff089ed11ffe0a..57da11c0da7322e74810e7108e9c8000b0c36520 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -398,11 +398,28 @@ public class CraftWorld extends CraftRegionAccessor implements World { - - @Override - public boolean isChunkGenerated(int x, int z) { -- try { -- return this.isChunkLoaded(x, z) || this.world.getChunkSource().chunkMap.read(new ChunkPos(x, z)).get().isPresent(); -- } catch (InterruptedException | ExecutionException ex) { -- throw new RuntimeException(ex); -+ // Paper start - Fix this method -+ if (!Bukkit.isPrimaryThread()) { -+ return java.util.concurrent.CompletableFuture.supplyAsync(() -> { -+ return CraftWorld.this.isChunkGenerated(x, z); -+ }, world.getChunkSource().mainThreadProcessor).join(); -+ } -+ ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(x, z); -+ if (chunk != null) { -+ return chunk instanceof ImposterProtoChunk || chunk instanceof net.minecraft.world.level.chunk.LevelChunk; - } -+ final java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); -+ ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad( -+ this.world, x, z, false, ChunkStatus.EMPTY, true, ca.spottedleaf.concurrentutil.util.Priority.NORMAL, future::complete -+ ); -+ world.getChunkSource().mainThreadProcessor.managedBlock(future::isDone); -+ return future.thenApply(c -> { -+ if (c != null) { -+ return c.getPersistedStatus() == ChunkStatus.FULL; -+ } -+ return false; -+ }).join(); -+ // Paper end - Fix this method - } - - @Override diff --git a/patches/unapplied/server/1041-Add-startup-flag-to-disable-gamerule-limits.patch b/patches/unapplied/server/1041-Add-startup-flag-to-disable-gamerule-limits.patch deleted file mode 100644 index 83ad17bfc0..0000000000 --- a/patches/unapplied/server/1041-Add-startup-flag-to-disable-gamerule-limits.patch +++ /dev/null @@ -1,66 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Fri, 25 Oct 2024 14:20:40 -0700 -Subject: [PATCH] Add startup flag to disable gamerule limits - --DPaper.DisableGameRuleLimits=true will disable gamerule limits - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 9928e14a5a42a2f0deba86e9dcb1f6f9f59412ef..c5eed86a8982466fd8302c678f0f041db1b24029 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -2071,13 +2071,21 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - } - - if (this.lastSpawnChunkRadius > 1) { -- this.getChunkSource().removeRegionTicket(TicketType.START, new ChunkPos(blockposition1), this.lastSpawnChunkRadius, Unit.INSTANCE); -+ // Paper start - allow disabling gamerule limits -+ for (ChunkPos chunkPos : io.papermc.paper.util.MCUtil.getSpiralOutChunks(blockposition1, this.lastSpawnChunkRadius - 2)) { -+ this.getChunkSource().removeTicketAtLevel(TicketType.START, chunkPos, net.minecraft.server.level.ChunkLevel.ENTITY_TICKING_LEVEL, Unit.INSTANCE); -+ } -+ // Paper end - allow disabling gamerule limits - } - - int i = this.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS) + 1; - - if (i > 1) { -- this.getChunkSource().addRegionTicket(TicketType.START, new ChunkPos(pos), i, Unit.INSTANCE); -+ // Paper start - allow disabling gamerule limits -+ for (ChunkPos chunkPos : io.papermc.paper.util.MCUtil.getSpiralOutChunks(pos, i - 2)) { -+ this.getChunkSource().addTicketAtLevel(TicketType.START, chunkPos, net.minecraft.server.level.ChunkLevel.ENTITY_TICKING_LEVEL, Unit.INSTANCE); -+ } -+ // Paper end - allow disabling gamerule limits - } - - this.lastSpawnChunkRadius = i; -diff --git a/src/main/java/net/minecraft/world/level/GameRules.java b/src/main/java/net/minecraft/world/level/GameRules.java -index 4ae47c2c5a6bcfbf932d000a80974463e2d3818d..7c363d59c6567cae8e6caf213be51804efa5a96d 100644 ---- a/src/main/java/net/minecraft/world/level/GameRules.java -+++ b/src/main/java/net/minecraft/world/level/GameRules.java -@@ -36,6 +36,14 @@ import org.slf4j.Logger; - - public class GameRules { - -+ // Paper start - allow disabling gamerule limits -+ private static final boolean DISABLE_LIMITS = Boolean.getBoolean("paper.disableGameRuleLimits"); -+ -+ private static int limit(final int limit, final int unlimited) { -+ return DISABLE_LIMITS ? unlimited : limit; -+ } -+ // Paper end - allow disabling gamerule limits -+ - public static final int DEFAULT_RANDOM_TICK_SPEED = 3; - static final Logger LOGGER = LogUtils.getLogger(); - private static final Map, GameRules.Type> GAME_RULE_TYPES = Maps.newTreeMap(Comparator.comparing((gamerules_gamerulekey) -> { -@@ -120,9 +128,9 @@ public class GameRules { - public static final GameRules.Key RULE_GLOBAL_SOUND_EVENTS = GameRules.register("globalSoundEvents", GameRules.Category.MISC, GameRules.BooleanValue.create(true)); - public static final GameRules.Key RULE_DO_VINES_SPREAD = GameRules.register("doVinesSpread", GameRules.Category.UPDATES, GameRules.BooleanValue.create(true)); - public static final GameRules.Key RULE_ENDER_PEARLS_VANISH_ON_DEATH = GameRules.register("enderPearlsVanishOnDeath", GameRules.Category.PLAYER, GameRules.BooleanValue.create(true)); -- public static final GameRules.Key RULE_MINECART_MAX_SPEED = GameRules.register("minecartMaxSpeed", GameRules.Category.MISC, GameRules.IntegerValue.create(8, 1, 1000, FeatureFlagSet.of(FeatureFlags.MINECART_IMPROVEMENTS), (minecraftserver, gamerules_gameruleint) -> { -+ public static final GameRules.Key RULE_MINECART_MAX_SPEED = GameRules.register("minecartMaxSpeed", GameRules.Category.MISC, GameRules.IntegerValue.create(8, 1, limit(1000, Integer.MAX_VALUE), FeatureFlagSet.of(FeatureFlags.MINECART_IMPROVEMENTS), (minecraftserver, gamerules_gameruleint) -> { // Paper - allow disabling gamerule limits - })); -- public static final GameRules.Key RULE_SPAWN_CHUNK_RADIUS = GameRules.register("spawnChunkRadius", GameRules.Category.MISC, GameRules.IntegerValue.create(2, 0, 32, FeatureFlagSet.of(), (minecraftserver, gamerules_gameruleint) -> { -+ public static final GameRules.Key RULE_SPAWN_CHUNK_RADIUS = GameRules.register("spawnChunkRadius", GameRules.Category.MISC, GameRules.IntegerValue.create(2, 0, limit(32, Integer.MAX_VALUE), FeatureFlagSet.of(), (minecraftserver, gamerules_gameruleint) -> { // Paper - allow disabling gamerule limits - ServerLevel worldserver = minecraftserver; // CraftBukkit - per-world - - worldserver.setDefaultSpawnPos(worldserver.getSharedSpawnPos(), worldserver.getSharedSpawnAngle()); diff --git a/patches/unapplied/server/1042-Improved-Watchdog-Support.patch b/patches/unapplied/server/1042-Improved-Watchdog-Support.patch deleted file mode 100644 index 7e6f9c63e0..0000000000 --- a/patches/unapplied/server/1042-Improved-Watchdog-Support.patch +++ /dev/null @@ -1,475 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Sun, 12 Apr 2020 15:50:48 -0400 -Subject: [PATCH] Improved Watchdog Support - -Forced Watchdog Crash support and Improve Async Shutdown - -If the request to shut down the server is received while we are in -a watchdog hang, immediately treat it as a crash and begin the shutdown -process. Shutdown process is now improved to also shutdown cleanly when -not using restart scripts either. - -If a server is deadlocked, a server owner can send SIGUP (or any other signal -the JVM understands to shut down as it currently does) and the watchdog -will no longer need to wait until the full timeout, allowing you to trigger -a close process and try to shut the server down gracefully, saving player and -world data. - -Previously there was no way to trigger this outside of waiting for a full watchdog -timeout, which may be set to a really long time... - -Additionally, fix everything to do with shutting the server down asynchronously. - -Previously, nearly everything about the process was fragile and unsafe. Main might -not have actually been frozen, and might still be manipulating state. - -Or, some reuest might ask main to do something in the shutdown but main is dead. - -Or worse, other things might start closing down items such as the Console or Thread Pool -before we are fully shutdown. - -This change tries to resolve all of these issues by moving everything into the stop -method and guaranteeing only one thread is stopping the server. - -We then issue Thread Death to the main thread of another thread initiates the stop process. -We have to ensure Thread Death propagates correctly though to stop main completely. - -This is to ensure that if main isn't truely stuck, it's not manipulating state we are trying to save. - -This also moves all plugins who register "delayed init" tasks to occur just before "Done" so they -are properly accounted for and wont trip watchdog on init. - -Feature patch - -diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java -index 6aaed8e8bf8c721fc834da5c76ac72a4c3e92458..4b002e8b75d117b726b0de274a76d3596fce015b 100644 ---- a/src/main/java/com/destroystokyo/paper/Metrics.java -+++ b/src/main/java/com/destroystokyo/paper/Metrics.java -@@ -92,7 +92,12 @@ public class Metrics { - * Starts the Scheduler which submits our data every 30 minutes. - */ - private void startSubmitting() { -- final Runnable submitTask = this::submitData; -+ final Runnable submitTask = () -> { -+ if (MinecraftServer.getServer().hasStopped()) { -+ return; -+ } -+ submitData(); -+ }; - - // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution of requests on the - // bStats backend. To circumvent this problem, we introduce some randomness into the initial and second delay. -diff --git a/src/main/java/io/papermc/paper/util/LogManagerShutdownThread.java b/src/main/java/io/papermc/paper/util/LogManagerShutdownThread.java -new file mode 100644 -index 0000000000000000000000000000000000000000..183e141d0c13190c6905dc4510d891992afef878 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/LogManagerShutdownThread.java -@@ -0,0 +1,26 @@ -+package io.papermc.paper.util; -+ -+public class LogManagerShutdownThread extends Thread { -+ -+ static LogManagerShutdownThread INSTANCE = new LogManagerShutdownThread(); -+ public static final void hook() { -+ if (INSTANCE == null) { -+ throw new IllegalStateException("Cannot re-hook after being unhooked"); -+ } -+ Runtime.getRuntime().addShutdownHook(INSTANCE); -+ } -+ -+ public static final void unhook() { -+ Runtime.getRuntime().removeShutdownHook(INSTANCE); -+ INSTANCE = null; -+ } -+ -+ private LogManagerShutdownThread() { -+ super("Log4j2 Shutdown Thread"); -+ } -+ -+ @Override -+ public void run() { -+ org.apache.logging.log4j.LogManager.shutdown(); -+ } -+} -diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java -index 589a8bf75be6ccc59f1e5dd5d8d9afed41c4772d..b24265573fdef5d9a964bcd76146f34542c420cf 100644 ---- a/src/main/java/net/minecraft/CrashReport.java -+++ b/src/main/java/net/minecraft/CrashReport.java -@@ -237,6 +237,7 @@ public class CrashReport { - } - - public static CrashReport forThrowable(Throwable cause, String title) { -+ if (cause instanceof ThreadDeath) com.destroystokyo.paper.util.SneakyThrow.sneaky(cause); // Paper - while (cause instanceof CompletionException && cause.getCause() != null) { - cause = cause.getCause(); - } -diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java -index e114e687f2f4503546687fd6792226a643af8793..90ca25c4aaf92a5639839a7cdaee2ffcdb75efa7 100644 ---- a/src/main/java/net/minecraft/server/Main.java -+++ b/src/main/java/net/minecraft/server/Main.java -@@ -77,6 +77,7 @@ public class Main { - - @DontObfuscate - public static void main(final OptionSet optionset) { // CraftBukkit - replaces main(String[] astring) -+ io.papermc.paper.util.LogManagerShutdownThread.hook(); // Paper - SharedConstants.tryDetectVersion(); - /* CraftBukkit start - Replace everything - OptionParser optionparser = new OptionParser(); -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 9cb435573d0b1bdf5488bd5b9cef5d2aba6a1c2d..4ac8bc8dc326ef12c4ffdfdf8325f3111ca5b665 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -317,7 +317,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); - public int autosavePeriod; - // Paper - don't store the vanilla dispatcher -- private boolean forceTicks; -+ public boolean forceTicks; // Paper - Improved watchdog support - // CraftBukkit end - // Spigot start - public static final int TPS = 20; -@@ -329,6 +329,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { - AtomicReference atomicreference = new AtomicReference(); - Thread thread = new ca.spottedleaf.moonrise.common.util.TickThread(() -> { // Paper - rewrite chunk system -@@ -502,6 +505,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {}; -+ } -+ // Paper end - return new TickTask(this.tickCount, runnable); - } - -@@ -2309,7 +2351,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop implements Profiler - public static boolean isNonRecoverable(Throwable exception) { - return exception instanceof ReportedException reportedException - ? isNonRecoverable(reportedException.getCause()) -- : exception instanceof OutOfMemoryError || exception instanceof StackOverflowError; -+ : exception instanceof OutOfMemoryError || exception instanceof StackOverflowError || exception instanceof ThreadDeath; // Paper - } - } -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 061d3e77fe8d9322eb660ac1995e025aba51ae1a..3f69dfe877a6c3a362a28c29f556b7b9b2ad19b0 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -1488,6 +1488,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - try { - tickConsumer.accept(entity); - } catch (Throwable throwable) { -+ if (throwable instanceof ThreadDeath) throw throwable; // Paper - // Paper start - Prevent block entity and entity crashes - final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ()); - MinecraftServer.LOGGER.error(msg, throwable); -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 01cfb478764b8deb38be5692390dd9f014b8999f..4640baec5bed6c2d53cc0f8ca1d273cc115abe9b 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -@@ -1078,6 +1078,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p - - gameprofilerfiller.pop(); - } catch (Throwable throwable) { -+ if (throwable instanceof ThreadDeath) throw throwable; // Paper - // Paper start - Prevent block entity and entity crashes - final String msg = String.format("BlockEntity threw exception at %s:%s,%s,%s", LevelChunk.this.getLevel().getWorld().getName(), this.getPos().getX(), this.getPos().getY(), this.getPos().getZ()); - net.minecraft.server.MinecraftServer.LOGGER.error(msg, throwable); -diff --git a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java -index c6e8441e299f477ddb22c1ce2618710763978f1a..e8e93538dfd71de86515d9405f728db1631e949a 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java -@@ -12,11 +12,27 @@ public class ServerShutdownThread extends Thread { - @Override - public void run() { - try { -+ // Paper start - try to shutdown on main -+ server.safeShutdown(false, false); -+ for (int i = 1000; i > 0 && !server.hasStopped(); i -= 100) { -+ Thread.sleep(100); -+ } -+ if (server.hasStopped()) { -+ while (!server.hasFullyShutdown) Thread.sleep(1000); -+ return; -+ } -+ // Looks stalled, close async - org.spigotmc.AsyncCatcher.enabled = false; // Spigot -+ server.forceTicks = true; - this.server.close(); -+ while (!server.hasFullyShutdown) Thread.sleep(1000); -+ } catch (InterruptedException e) { -+ e.printStackTrace(); -+ // Paper end - } finally { -+ org.apache.logging.log4j.LogManager.shutdown(); // Paper - try { -- net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender -+ //net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Move into stop - } catch (Exception e) { - } - } -diff --git a/src/main/java/org/spigotmc/RestartCommand.java b/src/main/java/org/spigotmc/RestartCommand.java -index 39e56b95aaafbcd8ebe68fdefaace83702e9510d..3ba27955548a26367a87d6b87c3c61beb299dfb9 100644 ---- a/src/main/java/org/spigotmc/RestartCommand.java -+++ b/src/main/java/org/spigotmc/RestartCommand.java -@@ -139,7 +139,7 @@ public class RestartCommand extends Command - // Paper end - - // Paper start - copied from above and modified to return if the hook registered -- private static boolean addShutdownHook(String restartScript) -+ public static boolean addShutdownHook(String restartScript) // Paper - { - String[] split = restartScript.split( " " ); - if ( split.length > 0 && new File( split[0] ).isFile() ) -diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java -index 529df2a41dd93d6e1505053bd04032dbf0cdaa31..c9e17225bc52fe5e7b2dc0908db225a86c6e94d1 100644 ---- a/src/main/java/org/spigotmc/WatchdogThread.java -+++ b/src/main/java/org/spigotmc/WatchdogThread.java -@@ -11,6 +11,7 @@ import org.bukkit.Bukkit; - public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThread // Paper - rewrite chunk system - { - -+ public static final boolean DISABLE_WATCHDOG = Boolean.getBoolean("disable.watchdog"); // Paper - Improved watchdog support - private static WatchdogThread instance; - private long timeoutTime; - private boolean restart; -@@ -39,6 +40,7 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre - { - if ( WatchdogThread.instance == null ) - { -+ if (timeoutTime <= 0) timeoutTime = 300; // Paper - WatchdogThread.instance = new WatchdogThread( timeoutTime * 1000L, restart ); - WatchdogThread.instance.start(); - } else -@@ -70,12 +72,13 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre - // Paper start - Logger log = Bukkit.getServer().getLogger(); - long currentTime = WatchdogThread.monotonicMillis(); -- if ( this.lastTick != 0 && this.timeoutTime > 0 && currentTime > this.lastTick + this.earlyWarningEvery && !Boolean.getBoolean("disable.watchdog")) // Paper - Add property to disable -+ MinecraftServer server = MinecraftServer.getServer(); -+ if ( this.lastTick != 0 && this.timeoutTime > 0 && WatchdogThread.hasStarted && (!server.isRunning() || (currentTime > this.lastTick + this.earlyWarningEvery && !DISABLE_WATCHDOG) )) // Paper - add property to disable - { -- boolean isLongTimeout = currentTime > lastTick + timeoutTime; -+ boolean isLongTimeout = currentTime > lastTick + timeoutTime || (!server.isRunning() && !server.hasStopped() && currentTime > lastTick + 1000); - // Don't spam early warning dumps - if ( !isLongTimeout && (earlyWarningEvery <= 0 || !hasStarted || currentTime < lastEarlyWarning + earlyWarningEvery || currentTime < lastTick + earlyWarningDelay)) continue; -- if ( !isLongTimeout && MinecraftServer.getServer().hasStopped()) continue; // Don't spam early watchdog warnings during shutdown, we'll come back to this... -+ if ( !isLongTimeout && server.hasStopped()) continue; // Don't spam early watchdog warnings during shutdown, we'll come back to this... - lastEarlyWarning = currentTime; - if (isLongTimeout) { - // Paper end -@@ -136,9 +139,24 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre - - if ( isLongTimeout ) - { -- if ( this.restart && !MinecraftServer.getServer().hasStopped() ) -+ if ( !server.hasStopped() ) - { -- RestartCommand.restart(); -+ AsyncCatcher.enabled = false; // Disable async catcher incase it interferes with us -+ server.forceTicks = true; -+ if (restart) { -+ RestartCommand.addShutdownHook( SpigotConfig.restartScript ); -+ } -+ // try one last chance to safe shutdown on main incase it 'comes back' -+ server.abnormalExit = true; -+ server.safeShutdown(false, restart); -+ try { -+ Thread.sleep(1000); -+ } catch (InterruptedException e) { -+ e.printStackTrace(); -+ } -+ if (!server.hasStopped()) { -+ server.close(); -+ } - } - break; - } // Paper end -diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml -index 637d64da9938e51a97338b9253b43889585c67bb..d2a75850af9c6ad2aca66a5f994f1b587d73eac4 100644 ---- a/src/main/resources/log4j2.xml -+++ b/src/main/resources/log4j2.xml -@@ -1,5 +1,5 @@ - -- -+ - - - diff --git a/patches/unapplied/server/1043-Detail-more-information-in-watchdog-dumps.patch b/patches/unapplied/server/1043-Detail-more-information-in-watchdog-dumps.patch deleted file mode 100644 index 2ec02452ea..0000000000 --- a/patches/unapplied/server/1043-Detail-more-information-in-watchdog-dumps.patch +++ /dev/null @@ -1,297 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 26 Mar 2020 21:59:32 -0700 -Subject: [PATCH] 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 - -Feature patch - -diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java -index d18af548fa6e979267347443b61efc58b271dfcf..e693a003ea8f022eef8b49e4332025b769333b30 100644 ---- a/src/main/java/net/minecraft/network/Connection.java -+++ b/src/main/java/net/minecraft/network/Connection.java -@@ -632,7 +632,13 @@ public class Connection extends SimpleChannelInboundHandler> { - if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) - || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING - || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) { -+ // Paper start - detailed watchdog information -+ net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener); -+ try { - tickablepacketlistener.tick(); -+ } finally { -+ net.minecraft.network.protocol.PacketUtils.packetProcessing.pop(); -+ } // Paper end - detailed watchdog information - } // Paper end - Buffer joins to world - } - -diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java -index f7197f1347251a37dd0f6d9ffa2f09bc3a4e1233..1f7f68aad97ee73763c042837f239bdc7167db55 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 = LogUtils.getLogger(); - -+ // Paper 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; -+ } -+ // Paper end - detailed watchdog information -+ - public PacketUtils() {} - - public static void ensureRunningOnSameThread(Packet packet, T listener, ServerLevel world) throws RunningOnDifferentThreadException { -@@ -29,6 +47,8 @@ public class PacketUtils { - public static void ensureRunningOnSameThread(Packet packet, T listener, BlockableEventLoop engine) throws RunningOnDifferentThreadException { - if (!engine.isSameThread()) { - engine.executeIfPossible(() -> { -+ packetProcessing.push(listener); // Paper - detailed watchdog information -+ try { // Paper - detailed watchdog information - if (listener instanceof ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // CraftBukkit - Don't handle sync packets for kicked players - if (listener.shouldHandleMessage(packet)) { - try { -@@ -47,6 +67,12 @@ public class PacketUtils { - } else { - PacketUtils.LOGGER.debug("Ignoring packet due to disconnection: {}", packet); - } -+ // Paper start - detailed watchdog information -+ } finally { -+ totalMainThreadPacketsProcessed.getAndIncrement(); -+ packetProcessing.pop(); -+ } -+ // Paper end - detailed watchdog information - - }); - throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD; -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index c5eed86a8982466fd8302c678f0f041db1b24029..2e0dcac7642d899efd60cf70fb0ad0336e1923da 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -1253,7 +1253,26 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - } - -+ // Paper 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; -+ } -+ // Paper end - log detailed entity tick information -+ - public void tickNonPassenger(Entity entity) { -+ // Paper start - log detailed entity tick information -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); -+ try { -+ if (currentlyTickingEntity.get() == null) { -+ currentlyTickingEntity.lazySet(entity); -+ } -+ // Paper end - log detailed entity tick information - // Spigot start - /*if (!org.spigotmc.ActivationRange.checkIfActive(entity)) { // Paper - comment out EAR 2 - entity.tickCount++; -@@ -1283,6 +1302,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - this.tickPassenger(entity, entity1, isActive); // Paper - EAR 2 - } - -+ // Paper start - log detailed entity tick information -+ } finally { -+ if (currentlyTickingEntity.get() == entity) { -+ currentlyTickingEntity.lazySet(null); -+ } -+ } -+ // Paper end - log detailed entity tick information - } - - private void tickPassenger(Entity vehicle, Entity passenger, boolean isActive) { // Paper - EAR 2 -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index b810f887e536af938f978ca2af068e6ae89b5e60..8c62d1aa5c8a062685474dca7e91bf9f8b004ca5 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -1128,8 +1128,43 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - return this.onGround; - } - -+ // Paper start - detailed watchdog information -+ public final Object posLock = new Object(); // Paper - 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; -+ } -+ // Paper end - detailed watchdog information -+ - public void move(MoverType type, Vec3 movement) { - final Vec3 originalMovement = movement; // Paper - Expose pre-collision velocity -+ // Paper start - detailed watchdog information -+ ca.spottedleaf.moonrise.common.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 { -+ // Paper end - detailed watchdog information - if (this.noPhysics) { - this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z); - } else { -@@ -1250,6 +1285,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - gameprofilerfiller.pop(); - } - } -+ // Paper start - detailed watchdog information -+ } finally { -+ synchronized (this.posLock) { // Paper -+ this.moveVector = null; -+ } // Paper -+ } -+ // Paper end - detailed watchdog information - } - - private void applyMovementEmissionAndPlaySound(Entity.MovementEmission moveEffect, Vec3 movement, BlockPos landingPos, BlockState landingState) { -@@ -4872,7 +4914,9 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - - public void setDeltaMovement(Vec3 velocity) { -+ synchronized (this.posLock) { // Paper - this.deltaMovement = velocity; -+ } // Paper - } - - public void addDeltaMovement(Vec3 velocity) { -@@ -4978,7 +5022,9 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - // Paper end - Fix MC-4 - if (this.position.x != x || this.position.y != y || this.position.z != z) { -+ synchronized (this.posLock) { // Paper - this.position = new Vec3(x, y, z); -+ } // Paper - int i = Mth.floor(x); - int j = Mth.floor(y); - int k = Mth.floor(z); -diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java -index c9e17225bc52fe5e7b2dc0908db225a86c6e94d1..f7a4fee9bb25ff256dc2e5ea26bfbceca6a49167 100644 ---- a/src/main/java/org/spigotmc/WatchdogThread.java -+++ b/src/main/java/org/spigotmc/WatchdogThread.java -@@ -22,6 +22,78 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre - private volatile long lastTick; - private volatile boolean stopping; - -+ // Paper 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 = net.minecraft.world.entity.EntityType.getKey(entity.getType()).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); -+ } -+ } -+ } -+ // Paper end - log detailed tick information -+ - private WatchdogThread(long timeoutTime, boolean restart) - { - super( "Paper Watchdog Thread" ); -@@ -119,6 +191,7 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre - log.log( Level.SEVERE, "------------------------------" ); - log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper - ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(MinecraftServer.getServer(), isLongTimeout); // Paper - rewrite chunk system -+ this.dumpTickingInfo(); // Paper - log detailed tick information - WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log ); - log.log( Level.SEVERE, "------------------------------" ); - // diff --git a/patches/unapplied/server/1044-Entity-load-save-limit-per-chunk.patch b/patches/unapplied/server/1044-Entity-load-save-limit-per-chunk.patch deleted file mode 100644 index 7a75cf646d..0000000000 --- a/patches/unapplied/server/1044-Entity-load-save-limit-per-chunk.patch +++ /dev/null @@ -1,81 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> -Date: Wed, 18 Nov 2020 20:52:25 -0800 -Subject: [PATCH] Entity load/save limit per chunk - -Adds a config option to limit the number of entities saved and loaded -to a chunk. The default values of -1 disable the limit. Although -defaults are only included for certain entites, this allows setting -limits for any entity type. - -diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java -index 5ed6599d1f9a2edf8c904f3602b06d26d857600c..b3c993a790fc3fab6a408c731deb297f74c959ce 100644 ---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java -+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java -@@ -104,7 +104,18 @@ public final class ChunkEntitySlices { - } - - final ListTag entitiesTag = new ListTag(); -+ final java.util.Map, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk - for (final Entity entity : PlatformHooks.get().modifySavedEntities(world, chunkPos.x, chunkPos.z, entities)) { -+ // Paper start - Entity load/save limit per chunk -+ final EntityType entityType = entity.getType(); -+ final int saveLimit = world.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); -+ if (saveLimit > -1) { -+ if (savedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { -+ continue; -+ } -+ savedEntityCounts.merge(entityType, 1, Integer::sum); -+ } -+ // Paper end - Entity load/save limit per chunk - CompoundTag compoundTag = new CompoundTag(); - if (entity.save(compoundTag)) { - entitiesTag.add(compoundTag); -diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java -index 629c1316920ad4c111fff489f8c3ea0ed39d0099..c8c2394558952d7ca57d29874485251b8f2b3400 100644 ---- a/src/main/java/net/minecraft/world/entity/EntityType.java -+++ b/src/main/java/net/minecraft/world/entity/EntityType.java -@@ -706,9 +706,20 @@ public class EntityType implements FeatureElement, EntityTypeT - final Spliterator spliterator = entityNbtList.spliterator(); - - return StreamSupport.stream(new Spliterator() { -+ final java.util.Map, Integer> loadedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk - public boolean tryAdvance(Consumer consumer) { - return spliterator.tryAdvance((nbtbase) -> { - EntityType.loadEntityRecursive((CompoundTag) nbtbase, world, reason, (entity) -> { -+ // Paper start - Entity load/save limit per chunk -+ final EntityType entityType = entity.getType(); -+ final int saveLimit = world.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); -+ if (saveLimit > -1) { -+ if (this.loadedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { -+ return null; -+ } -+ this.loadedEntityCounts.merge(entityType, 1, Integer::sum); -+ } -+ // Paper end - Entity load/save limit per chunk - consumer.accept(entity); - return entity; - }); -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java -index 16ca1c8672e5f0a27f8a30498c754a81cdec5191..356d010506fd21f3c752e4aa86c46c1106fdde3b 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java -@@ -93,7 +93,18 @@ public class EntityStorage implements EntityPersistentStorage { - } - } else { - ListTag listTag = new ListTag(); -+ final java.util.Map, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk - dataList.getEntities().forEach(entity -> { -+ // Paper start - Entity load/save limit per chunk -+ final EntityType entityType = entity.getType(); -+ final int saveLimit = this.level.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); -+ if (saveLimit > -1) { -+ if (savedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { -+ return; -+ } -+ savedEntityCounts.merge(entityType, 1, Integer::sum); -+ } -+ // Paper end - Entity load/save limit per chunk - CompoundTag compoundTagx = new CompoundTag(); - if (entity.save(compoundTagx)) { - listTag.add(compoundTagx); diff --git a/patches/unapplied/server/1045-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch b/patches/unapplied/server/1045-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch deleted file mode 100644 index 16f1024afb..0000000000 --- a/patches/unapplied/server/1045-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch +++ /dev/null @@ -1,744 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sun, 2 Feb 2020 02:25:10 -0800 -Subject: [PATCH] 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. - -Feature patch - -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java -index a23dc2f8f4475de1ee35bf18a7a8a53233ccac12..226af44fd469053451a0403a95ffb446face9530 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java -@@ -9,6 +9,27 @@ import java.util.BitSet; - public class RegionBitmap { - private final BitSet used = new BitSet(); - -+ // Paper start - Attempt to recalculate regionfile header if it is corrupt -+ public final void copyFrom(RegionBitmap other) { -+ BitSet thisBitset = this.used; -+ BitSet otherBitset = other.used; -+ -+ for (int i = 0; i < Math.max(thisBitset.size(), otherBitset.size()); ++i) { -+ thisBitset.set(i, otherBitset.get(i)); -+ } -+ } -+ -+ public final boolean tryAllocate(int from, int length) { -+ BitSet bitset = this.used; -+ int firstSet = bitset.nextSetBit(from); -+ if (firstSet > 0 && firstSet < (from + length)) { -+ return false; -+ } -+ bitset.set(from, from + length); -+ return true; -+ } -+ // Paper end - Attempt to recalculate regionfile header if it is corrupt -+ - public void force(int start, int size) { - this.used.set(start, start + size); - } -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -index ff092c6d0cd436f14a9a4ff5c8ddbb5538d1a8c5..16f07007a0f73ec0c6f421c9b082518e87e8cc7b 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 -@@ -51,6 +51,354 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche - private final IntBuffer timestamps; - @VisibleForTesting - protected final RegionBitmap usedSectors; -+ // Paper start - Attempt to recalculate regionfile header if it is corrupt -+ 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(new DataInputStream(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() { -+ Path backup = this.path.getParent().resolve(this.path.getFileName() + "." + new java.util.Random().nextLong() + ".backup"); -+ this.backupRegionFile(backup); -+ } -+ -+ private void backupRegionFile(Path to) { -+ try { -+ this.file.force(true); -+ LOGGER.warn("Backing up regionfile \"" + this.path.toAbsolutePath() + "\" to " + to.toAbsolutePath()); -+ java.nio.file.Files.copy(this.path, to, java.nio.file.StandardCopyOption.COPY_ATTRIBUTES); -+ LOGGER.warn("Backed up the regionfile to " + to.toAbsolutePath()); -+ } catch (IOException ex) { -+ LOGGER.error("Failed to backup to " + to.toAbsolutePath(), ex); -+ } -+ } -+ -+ private static boolean inSameRegionfile(ChunkPos first, ChunkPos second) { -+ return (first.x & ~31) == (second.x & ~31) && (first.z & ~31) == (second.z & ~31); -+ } -+ -+ // note: only call for CHUNK regionfiles -+ boolean recalculateHeader() throws IOException { -+ if (!this.canRecalcHeader) { -+ return false; -+ } -+ ChunkPos ourLowerLeftPosition = RegionFileStorage.getRegionFileCoordinates(this.path); -+ if (ourLowerLeftPosition == null) { -+ LOGGER.error("Unable to get chunk location of regionfile " + this.path.toAbsolutePath() + ", cannot recover header"); -+ return false; -+ } -+ synchronized (this) { -+ LOGGER.warn("Corrupt regionfile header detected! Attempting to re-calculate header offsets for regionfile " + this.path.toAbsolutePath(), 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 = SerializableChunkData.getChunkCoordinate(compound); -+ if (!inSameRegionfile(ourLowerLeftPosition, chunkPos)) { -+ LOGGER.error("Ignoring absolute chunk " + chunkPos + " in regionfile as it is not contained in the bounds of the regionfile '" + this.path.toAbsolutePath() + "'. It should be in regionfile (" + (chunkPos.x >> 5) + "," + (chunkPos.z >> 5) + ")"); -+ continue; -+ } -+ int location = (chunkPos.x & 31) | ((chunkPos.z & 31) << 5); -+ -+ CompoundTag otherCompound = compounds[location]; -+ -+ if (otherCompound != null && SerializableChunkData.getLastWorldSaveTime(otherCompound) > SerializableChunkData.getLastWorldSaveTime(compound)) { -+ continue; // don't overwrite newer data. -+ } -+ -+ // aikar oversized? -+ Path aikarOversizedFile = this.getOversizedFile(chunkPos.x, chunkPos.z); -+ boolean isAikarOversized = false; -+ if (Files.exists(aikarOversizedFile)) { -+ try { -+ CompoundTag aikarOversizedCompound = this.getOversizedData(chunkPos.x, chunkPos.z); -+ if (SerializableChunkData.getLastWorldSaveTime(compound) == SerializableChunkData.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.path.toAbsolutePath() + ", 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; -+ Path[] regionFiles = Files.list(containingFolder).toArray(Path[]::new); -+ boolean[] oversized = new boolean[32 * 32]; -+ RegionFileVersion[] oversizedCompressionTypes = new RegionFileVersion[32 * 32]; -+ -+ if (regionFiles != null) { -+ 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 (Path 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); -+ } catch (Exception ex) { -+ LOGGER.error("Failed to read oversized chunk data in file " + regionFile.toAbsolutePath() + ", 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(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.toAbsolutePath() + ", it's corrupt. Its data will be lost"); -+ continue; -+ } -+ -+ if (!SerializableChunkData.getChunkCoordinate(compound).equals(oversizedCoords)) { -+ LOGGER.error("Can't use oversized chunk stored in " + regionFile.toAbsolutePath() + ", got absolute chunkpos: " + SerializableChunkData.getChunkCoordinate(compound) + ", expected " + oversizedCoords); -+ continue; -+ } -+ -+ if (compounds[location] == null || SerializableChunkData.getLastWorldSaveTime(compound) > SerializableChunkData.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.path.toAbsolutePath() + ", 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.path.toAbsolutePath() + " 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.path.toAbsolutePath(), ex); -+ Files.deleteIfExists(this.getOversizedMetaFile()); -+ } -+ } else { -+ Files.deleteIfExists(this.getOversizedMetaFile()); -+ } -+ -+ 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.path.toAbsolutePath()); -+ -+ 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.path.toAbsolutePath()); -+ } else if (newOffset == 0) { -+ LOGGER.warn("Data for local chunk (" + chunkX + "," + chunkZ + ") could not be recovered in regionfile " + this.path.toAbsolutePath() + ", it will be regenerated"); -+ } else { -+ LOGGER.info("Local chunk (" + chunkX + "," + chunkZ + ") changed to point to newer data or correct chunk in regionfile " + this.path.toAbsolutePath()); -+ } -+ } -+ } -+ -+ LOGGER.info("End of change summary for regionfile " + this.path.toAbsolutePath()); -+ -+ // simply destroy the timestamp header, it's not used -+ -+ for (int i = 0; i < 32 * 32; ++i) { -+ this.timestamps.put(i, calculatedOffsets[i] != 0 ? RegionFile.getTimestamp() : 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.path.toAbsolutePath()); -+ } catch (IOException ex) { -+ LOGGER.error("Failed to write new header to disk for regionfile " + this.path.toAbsolutePath(), ex); -+ } -+ } -+ -+ return true; -+ } -+ -+ final boolean canRecalcHeader; // final forces compile fail on new constructor -+ // Paper end - Attempt to recalculate regionfile header if it is corrupt - - // Paper start - rewrite chunk system - @Override -@@ -82,6 +430,7 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche - throw new IllegalArgumentException("Expected directory, got " + String.valueOf(directory.toAbsolutePath())); - } else { - this.externalFileDir = directory; -+ this.canRecalcHeader = RegionFileStorage.isChunkDataFolder(this.externalFileDir); // Paper - add can recalc flag - this.offsets = this.header.asIntBuffer(); - ((java.nio.Buffer) this.offsets).limit(1024); // CraftBukkit - decompile error - ((java.nio.Buffer) this.header).position(4096); // CraftBukkit - decompile error -@@ -101,14 +450,16 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche - RegionFile.LOGGER.warn("Region file {} has truncated header: {}", path, i); - } - -- long j = Files.size(path); -+ final long j = Files.size(path); final long regionFileSize = j; // Paper - recalculate header on header corruption - -- for (int k = 0; k < 1024; ++k) { -- int l = this.offsets.get(k); -+ boolean needsHeaderRecalc = false; // Paper - recalculate header on header corruption -+ boolean hasBackedUp = false; // Paper - recalculate header on header corruption -+ for (int k = 0; k < 1024; ++k) { final int headerLocation = k; // Paper - we expect this to be the header location -+ final int l = this.offsets.get(k); - - if (l != 0) { -- int i1 = RegionFile.getSectorNumber(l); -- int j1 = RegionFile.getNumSectors(l); -+ final int i1 = RegionFile.getSectorNumber(l); final int offset = i1; // Paper - we expect this to be offset in file in sectors -+ int j1 = RegionFile.getNumSectors(l); final int sectorLength; // Paper - 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 -@@ -117,21 +468,66 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche - j1 = (realLen.getInt(0) + 4) / 4096 + 1; - } - // Spigot end -+ sectorLength = j1; // Paper - 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", new Object[]{path, k, i1}); -- this.offsets.put(k, 0); -+ //this.offsets.put(k, 0); // Paper - 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", path, k); -- this.offsets.put(k, 0); -+ //this.offsets.put(k, 0); // Paper - 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", new Object[]{path, k, i1}); -- this.offsets.put(k, 0); -+ //this.offsets.put(k, 0); // Paper - we catch this, but need it in the header for the summary change - } else { -- this.usedSectors.force(i1, j1); -+ //this.usedSectors.force(i1, j1); // Paper - move this down so we can check if it fails to allocate -+ } -+ // Paper 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.path.toAbsolutePath() + "! Recalculating header..."); -+ needsHeaderRecalc = true; -+ break; -+ } else { -+ // location = chunkX | (chunkZ << 5); -+ LOGGER.error("Detected invalid header for regionfile " + this.path.toAbsolutePath() + -+ "! 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.path.toAbsolutePath()); - } -+ if (failedToAllocate & !canRecalcHeader) { -+ // location = chunkX | (chunkZ << 5); -+ LOGGER.error("Detected invalid header for regionfile " + this.path.toAbsolutePath() + -+ "! 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; -+ // Paper end - recalculate header on header corruption - } - } -+ // Paper 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.path.toAbsolutePath() + ", header gave erroneous offsets & locations"); -+ this.recalculateHeader(); -+ } -+ // Paper end - } - - } -@@ -142,11 +538,36 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche - } - - private Path getExternalChunkPath(ChunkPos chunkPos) { -- String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; -+ String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; // Paper - diff on change - - return this.externalFileDir.resolve(s); - } - -+ // Paper start -+ private static ChunkPos getOversizedChunkPair(Path file) { -+ String fileName = file.getFileName().toString(); -+ -+ 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; -+ } -+ } -+ // Paper end -+ - @Nullable - public synchronized DataInputStream getChunkDataInputStream(ChunkPos pos) throws IOException { - int i = this.getOffset(pos); -@@ -170,6 +591,11 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche - ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error - if (bytebuffer.remaining() < 5) { - RegionFile.LOGGER.error("Chunk {} header is truncated: expected {} but read {}", new Object[]{pos, l, bytebuffer.remaining()}); -+ // Paper start - recalculate header on regionfile corruption -+ if (this.canRecalcHeader && this.recalculateHeader()) { -+ return this.getChunkDataInputStream(pos); -+ } -+ // Paper end - recalculate header on regionfile corruption - return null; - } else { - int i1 = bytebuffer.getInt(); -@@ -177,6 +603,11 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche - - if (i1 == 0) { - RegionFile.LOGGER.warn("Chunk {} is allocated, but stream is missing", pos); -+ // Paper start - recalculate header on regionfile corruption -+ if (this.canRecalcHeader && this.recalculateHeader()) { -+ return this.getChunkDataInputStream(pos); -+ } -+ // Paper end - recalculate header on regionfile corruption - return null; - } else { - int j1 = i1 - 1; -@@ -184,18 +615,45 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche - if (RegionFile.isExternalStreamChunk(b0)) { - if (j1 != 0) { - RegionFile.LOGGER.warn("Chunk has both internal and external streams"); -+ // Paper start - recalculate header on regionfile corruption -+ if (this.canRecalcHeader && this.recalculateHeader()) { -+ return this.getChunkDataInputStream(pos); -+ } -+ // Paper end - recalculate header on regionfile corruption - } - -- return this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0)); -+ // Paper 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; -+ // Paper end - recalculate header on regionfile corruption - } else if (j1 > bytebuffer.remaining()) { - RegionFile.LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", new Object[]{pos, j1, bytebuffer.remaining()}); -+ // Paper start - recalculate header on regionfile corruption -+ if (this.canRecalcHeader && this.recalculateHeader()) { -+ return this.getChunkDataInputStream(pos); -+ } -+ // Paper end - recalculate header on regionfile corruption - return null; - } else if (j1 < 0) { - RegionFile.LOGGER.error("Declared size {} of chunk {} is negative", i1, pos); -+ // Paper start - recalculate header on regionfile corruption -+ if (this.canRecalcHeader && this.recalculateHeader()) { -+ return this.getChunkDataInputStream(pos); -+ } -+ // Paper end - recalculate header on regionfile corruption - return null; - } else { - JvmProfiler.INSTANCE.onRegionFileRead(this.info, pos, this.version, j1); -- return this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1)); -+ // Paper 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; -+ // Paper end - recalculate header on regionfile corruption - } - } - } -@@ -391,10 +849,15 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche - } - - private ByteBuffer createExternalStub() { -+ // Paper start - add version param -+ return this.createExternalStub(this.version); -+ } -+ private ByteBuffer createExternalStub(RegionFileVersion version) { -+ // Paper 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)); // Paper - 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 8d66d6b7aeb9feb54ebd83f5c73b45d42b9a7034..e40665cead218502b44dd49051a53326ed94f061 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 -@@ -211,11 +211,42 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - } - } - // Paper end - rewrite chunk system -+ // Paper start - recalculate region file headers -+ private final boolean isChunkData; -+ -+ public static boolean isChunkDataFolder(Path path) { -+ return path.toFile().getName().equalsIgnoreCase("region"); -+ } -+ -+ @Nullable -+ public static ChunkPos getRegionFileCoordinates(Path file) { -+ String fileName = file.getFileName().toString(); -+ if (!fileName.startsWith("r.") || !fileName.endsWith(".mca")) { -+ 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 << 5, z << 5); -+ } catch (NumberFormatException ex) { -+ return null; -+ } -+ } -+ // Paper end - - protected RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { // Paper - protected - this.folder = directory; - this.sync = dsync; - this.info = storageKey; -+ this.isChunkData = isChunkDataFolder(this.folder); // Paper - recalculate region file headers - } - - // Paper start - rewrite chunk system -@@ -315,6 +346,19 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - try { - if (datainputstream != null) { - nbttagcompound = NbtIo.read((DataInput) datainputstream); -+ // Paper start - recover from corrupt regionfile header -+ if (this.isChunkData) { -+ ChunkPos chunkPos = SerializableChunkData.getChunkCoordinate(nbttagcompound); -+ if (!chunkPos.equals(pos)) { -+ net.minecraft.server.MinecraftServer.LOGGER.error("Attempting to read chunk data at " + pos + " but got chunk data for " + chunkPos + " instead! Attempting regionfile recalculation for regionfile " + regionfile.getPath().toAbsolutePath()); -+ if (regionfile.recalculateHeader()) { -+ return this.read(pos); -+ } -+ net.minecraft.server.MinecraftServer.LOGGER.error("Can't recalculate regionfile header, regenerating chunk " + pos + " for " + regionfile.getPath().toAbsolutePath()); -+ return null; -+ } -+ } -+ // Paper end - recover from corrupt regionfile header - break label43; - } - -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java -index ef68b57ef1d8d7cb317c417569dd23a777fba4ad..f4a39f49b354c560d614483db1cd3dfc154e94b4 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java -@@ -21,7 +21,7 @@ import org.slf4j.Logger; - - public class RegionFileVersion { - private static final Logger LOGGER = LogUtils.getLogger(); -- private static final Int2ObjectMap VERSIONS = new Int2ObjectOpenHashMap<>(); -+ public static final Int2ObjectMap VERSIONS = new Int2ObjectOpenHashMap<>(); // Paper - private -> public - private static final Object2ObjectMap VERSIONS_BY_NAME = new Object2ObjectOpenHashMap<>(); - public static final RegionFileVersion VERSION_GZIP = register( - new RegionFileVersion( -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java -index 0296f52fb2c871adbf2ce73a64d8f77fab826cd7..018b24d7611c3fd11536441431abf8f125850129 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java -@@ -103,6 +103,18 @@ public record SerializableChunkData(Registry biomeRegistry, ChunkPos chun - } - } - // Paper end - guard against serializing mismatching coordinates -+ // Paper start - Attempt to recalculate regionfile header if it is corrupt -+ // TODO: Check on update -+ public static long getLastWorldSaveTime(final CompoundTag chunkData) { -+ final int dataVersion = ChunkStorage.getVersion(chunkData); -+ if (dataVersion < 2842) { // Level tag is removed after this version -+ final CompoundTag levelData = chunkData.getCompound("Level"); -+ return levelData.getLong("LastUpdate"); -+ } else { -+ return chunkData.getLong("LastUpdate"); -+ } -+ } -+ // Paper end - Attempt to recalculate regionfile header if it is corrupt - - // Paper start - Do not let the server load chunks from newer versions - private static final int CURRENT_DATA_VERSION = net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion(); -@@ -575,7 +587,7 @@ public record SerializableChunkData(Registry biomeRegistry, ChunkPos chun - nbttagcompound.putInt("xPos", this.chunkPos.x); - nbttagcompound.putInt("yPos", this.minSectionY); - nbttagcompound.putInt("zPos", this.chunkPos.z); -- nbttagcompound.putLong("LastUpdate", this.lastUpdateTime); -+ nbttagcompound.putLong("LastUpdate", this.lastUpdateTime); // Paper - Diff on change - nbttagcompound.putLong("InhabitedTime", this.inhabitedTime); - nbttagcompound.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(this.chunkStatus).toString()); - DataResult dataresult; // CraftBukkit - decompile error diff --git a/patches/unapplied/server/1046-Bundle-spark.patch b/patches/unapplied/server/1046-Bundle-spark.patch deleted file mode 100644 index 84624b4705..0000000000 --- a/patches/unapplied/server/1046-Bundle-spark.patch +++ /dev/null @@ -1,401 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Riley Park -Date: Tue, 16 Jul 2024 14:55:23 -0700 -Subject: [PATCH] Bundle spark - - -diff --git a/build.gradle.kts b/build.gradle.kts -index 9e6c2a4630ce75e4115f76b5e7a1e0b50e8b3197..faf3e3fd72e8c915e7a4803dacbe1bb576c6663e 100644 ---- a/build.gradle.kts -+++ b/build.gradle.kts -@@ -76,6 +76,10 @@ dependencies { - implementation("io.papermc:reflection-rewriter-runtime:$reflectionRewriterVersion") - implementation("io.papermc:reflection-rewriter-proxy-generator:$reflectionRewriterVersion") - // Paper end - Remap reflection -+ // Paper start - spark -+ implementation("me.lucko:spark-api:0.1-20240720.200737-2") -+ implementation("me.lucko:spark-paper:1.10.119-SNAPSHOT") -+ // Paper end - spark - } - - paperweight { -diff --git a/src/main/java/io/papermc/paper/SparksFly.java b/src/main/java/io/papermc/paper/SparksFly.java -new file mode 100644 -index 0000000000000000000000000000000000000000..62e2d5704c348955bc8284dc2d54c933b7bcdd06 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/SparksFly.java -@@ -0,0 +1,211 @@ -+package io.papermc.paper; -+ -+import io.papermc.paper.configuration.GlobalConfiguration; -+import io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage; -+import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; -+import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage; -+import io.papermc.paper.util.MCUtil; -+import java.util.Collection; -+import java.util.List; -+import java.util.concurrent.ConcurrentLinkedQueue; -+import java.util.logging.Level; -+import java.util.logging.Logger; -+import me.lucko.spark.paper.api.Compatibility; -+import me.lucko.spark.paper.api.PaperClassLookup; -+import me.lucko.spark.paper.api.PaperScheduler; -+import me.lucko.spark.paper.api.PaperSparkModule; -+import net.kyori.adventure.text.Component; -+import net.kyori.adventure.text.format.TextColor; -+import net.minecraft.util.ExceptionCollector; -+import org.bukkit.Server; -+import org.bukkit.command.Command; -+import org.bukkit.command.CommandSender; -+import org.bukkit.craftbukkit.CraftServer; -+ -+// It's like electricity. -+public final class SparksFly { -+ public static final String ID = "spark"; -+ public static final String COMMAND_NAME = "spark"; -+ -+ private static final String PREFER_SPARK_PLUGIN_PROPERTY = "paper.preferSparkPlugin"; -+ -+ private static final int SPARK_YELLOW = 0xffc93a; -+ -+ private final Logger logger; -+ private final PaperSparkModule spark; -+ private final ConcurrentLinkedQueue mainThreadTaskQueue; -+ -+ private boolean enabled; -+ private boolean disabledInConfigurationWarningLogged; -+ -+ public SparksFly(final Server server) { -+ this.mainThreadTaskQueue = new ConcurrentLinkedQueue<>(); -+ this.logger = Logger.getLogger(ID); -+ this.logger.log(Level.INFO, "This server bundles the spark profiler. For more information please visit https://docs.papermc.io/paper/profiling"); -+ this.spark = PaperSparkModule.create(Compatibility.VERSION_1_0, server, this.logger, new PaperScheduler() { -+ @Override -+ public void executeAsync(final Runnable runnable) { -+ MCUtil.scheduleAsyncTask(this.catching(runnable, "asynchronous")); -+ } -+ -+ @Override -+ public void executeSync(final Runnable runnable) { -+ SparksFly.this.mainThreadTaskQueue.offer(this.catching(runnable, "synchronous")); -+ } -+ -+ private Runnable catching(final Runnable runnable, final String type) { -+ return () -> { -+ try { -+ runnable.run(); -+ } catch (final Throwable t) { -+ SparksFly.this.logger.log(Level.SEVERE, "An exception was encountered while executing a " + type + " spark task", t); -+ } -+ }; -+ } -+ }, new PaperClassLookup() { -+ @Override -+ public Class lookup(final String className) throws Exception { -+ final ExceptionCollector exceptions = new ExceptionCollector<>(); -+ try { -+ return Class.forName(className); -+ } catch (final ClassNotFoundException e) { -+ exceptions.add(e); -+ for (final ConfiguredPluginClassLoader loader : ((PaperPluginClassLoaderStorage) PaperClassLoaderStorage.instance()).getGlobalGroup().getClassLoaders()) { -+ try { -+ final Class loadedClass = loader.loadClass(className, true, false, true); -+ if (loadedClass != null) { -+ return loadedClass; -+ } -+ } catch (final ClassNotFoundException exception) { -+ exceptions.add(exception); -+ } -+ } -+ exceptions.throwIfPresent(); -+ return null; -+ } -+ } -+ }); -+ } -+ -+ public void executeMainThreadTasks() { -+ Runnable task; -+ while ((task = this.mainThreadTaskQueue.poll()) != null) { -+ task.run(); -+ } -+ } -+ -+ public void enableEarlyIfRequested() { -+ if (!isPluginPreferred() && shouldEnableImmediately()) { -+ this.enable(); -+ } -+ } -+ -+ public void enableBeforePlugins() { -+ if (!isPluginPreferred()) { -+ this.enable(); -+ } -+ } -+ -+ public void enableAfterPlugins(final Server server) { -+ final boolean isPluginPreferred = isPluginPreferred(); -+ final boolean isPluginEnabled = isPluginEnabled(server); -+ if (!isPluginPreferred || !isPluginEnabled) { -+ if (isPluginPreferred && !this.enabled) { -+ this.logger.log(Level.INFO, "The spark plugin has been preferred but was not loaded. The bundled spark profiler will enabled instead."); -+ } -+ this.enable(); -+ } -+ } -+ -+ private void enable() { -+ if (!this.enabled) { -+ if (GlobalConfiguration.get().spark.enabled) { -+ this.enabled = true; -+ this.spark.enable(); -+ } else { -+ if (!this.disabledInConfigurationWarningLogged) { -+ this.logger.log(Level.INFO, "The spark profiler will not be enabled because it is currently disabled in the configuration."); -+ this.disabledInConfigurationWarningLogged = true; -+ } -+ } -+ } -+ } -+ -+ public void disable() { -+ if (this.enabled) { -+ this.spark.disable(); -+ this.enabled = false; -+ } -+ } -+ -+ public void registerCommandBeforePlugins(final Server server) { -+ if (!isPluginPreferred()) { -+ this.registerCommand(server); -+ } -+ } -+ -+ public void registerCommandAfterPlugins(final Server server) { -+ if ((!isPluginPreferred() || !isPluginEnabled(server)) && server.getCommandMap().getCommand(COMMAND_NAME) == null) { -+ this.registerCommand(server); -+ } -+ } -+ -+ private void registerCommand(final Server server) { -+ server.getCommandMap().register(COMMAND_NAME, "paper", new CommandImpl(COMMAND_NAME, this.spark.getPermissions())); -+ } -+ -+ public void tickStart() { -+ this.spark.onServerTickStart(); -+ } -+ -+ public void tickEnd(final double duration) { -+ this.spark.onServerTickEnd(duration); -+ } -+ -+ void executeCommand(final CommandSender sender, final String[] args) { -+ this.spark.executeCommand(sender, args); -+ } -+ -+ List tabComplete(final CommandSender sender, final String[] args) { -+ return this.spark.tabComplete(sender, args); -+ } -+ -+ public static boolean isPluginPreferred() { -+ return Boolean.getBoolean(PREFER_SPARK_PLUGIN_PROPERTY); -+ } -+ -+ private static boolean isPluginEnabled(final Server server) { -+ return server.getPluginManager().isPluginEnabled(ID); -+ } -+ -+ private static boolean shouldEnableImmediately() { -+ return GlobalConfiguration.get().spark.enableImmediately; -+ } -+ -+ public static final class CommandImpl extends Command { -+ CommandImpl(final String name, final Collection permissions) { -+ super(name); -+ this.setPermission(String.join(";", permissions)); -+ } -+ -+ @Override -+ public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) { -+ final SparksFly spark = ((CraftServer) sender.getServer()).spark; -+ if (spark.enabled) { -+ spark.executeCommand(sender, args); -+ } else { -+ sender.sendMessage(Component.text("The spark profiler is currently disabled.", TextColor.color(SPARK_YELLOW))); -+ } -+ return true; -+ } -+ -+ @Override -+ public List tabComplete(final CommandSender sender, final String alias, final String[] args) throws IllegalArgumentException { -+ final SparksFly spark = ((CraftServer) sender.getServer()).spark; -+ if (spark.enabled) { -+ return spark.tabComplete(sender, args); -+ } -+ return List.of(); -+ } -+ } -+} -diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java -index 6b8ed8a0baaf4a57d20e57cec3400af5561ddd79..48604e7f96adc9e226e034054c5e2bad0b024eb5 100644 ---- a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java -+++ b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java -@@ -1,6 +1,9 @@ - package io.papermc.paper.plugin.provider.source; - -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.SparksFly; - import io.papermc.paper.plugin.PluginInitializerManager; -+import io.papermc.paper.plugin.configuration.PluginMeta; - import io.papermc.paper.plugin.entrypoint.EntrypointHandler; - import io.papermc.paper.plugin.provider.type.PluginFileType; - import org.bukkit.plugin.InvalidPluginException; -@@ -17,12 +20,14 @@ import java.nio.file.attribute.BasicFileAttributes; - import java.util.Set; - import java.util.function.Function; - import java.util.jar.JarFile; -+import org.slf4j.Logger; - - /** - * Loads a plugin provider at the given plugin jar file path. - */ - public class FileProviderSource implements ProviderSource { - -+ private static final Logger LOGGER = LogUtils.getClassLogger(); - private final Function contextChecker; - private final boolean applyRemap; - -@@ -82,6 +87,12 @@ public class FileProviderSource implements ProviderSource { - ); - } - -+ final PluginMeta config = type.getConfig(file); -+ if ((config.getName().equals("spark") && config.getMainClass().equals("me.lucko.spark.bukkit.BukkitSparkPlugin")) && !SparksFly.isPluginPreferred()) { -+ LOGGER.info("The spark plugin will not be loaded as this server bundles the spark profiler."); -+ return; -+ } -+ - type.register(entrypointHandler, file, context); - } - -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 4ac8bc8dc326ef12c4ffdfdf8325f3111ca5b665..ca70815b73199835b88c9d68c8a01699536d320f 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -764,6 +764,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= j) { -+ this.server.spark.tickStart(); // Paper - spark - if (this.emptyTicks == j) { - MinecraftServer.LOGGER.info("Server empty for {} seconds, pausing", this.pauseWhileEmptySeconds()); - this.autoSave(); - } - - this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit -+ this.server.spark.executeMainThreadTasks(); // Paper - spark - this.tickConnection(); -+ this.server.spark.tickEnd(((double)(System.nanoTime() - lastTick) / 1000000D)); // Paper - spark - return; - } - } - -+ this.server.spark.tickStart(); // Paper - spark - new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper - Server Tick Events - ++this.tickCount; - this.tickRateManager.tick(); -@@ -1654,11 +1662,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop -Date: Sun, 13 Aug 2023 15:41:52 -0700 -Subject: [PATCH] Improve performance of mass crafts - -When the server crafts all available items in CraftingMenu or InventoryMenu the game -checks either 4 or 9 times for each individual craft for a matching recipe for that container. -This check can be expensive if 64 total crafts are being performed with the recipe matching logic -being run 64 * 9 + 64 times. A breakdown of those times is below. This patch caches the last matching -recipe so that it is checked first and only if it doesn't match does the rest of the matching logic run. - -Shift-click crafts are processed one at a time, so shift clicking on an item in the result of a iron block craft -where all the 9 inputs are full stacks of iron will run 64 iron block crafts. For each of those crafts, the -'remaining' blocks are calculated. This is due to recipes that have leftover items like buckets. This is done -for each craft, and done once to get the full 9 leftover items which are usually air. Then 1 item is removed -from each of the 9 inputs and each time that happens, logic is triggered to update the result itemstack. So -for each craft, that logic is run 9 times (hence the 64 * 9). The + 64 is from the 64 checks for remaining items. - -After this patch, the full iteration over all recipes checking for a match should run once for a full craft to find the -initial recipe match. Then that recipe will be checked first for all future recipe match checks. - -Feature patch - -diff --git a/src/main/java/net/minecraft/world/inventory/CraftingContainer.java b/src/main/java/net/minecraft/world/inventory/CraftingContainer.java -index 779d107a4d07820529273af5931421c09d1dc27f..4f6c8c43f5150e340704682accfbe2a5b1c5db19 100644 ---- a/src/main/java/net/minecraft/world/inventory/CraftingContainer.java -+++ b/src/main/java/net/minecraft/world/inventory/CraftingContainer.java -@@ -18,11 +18,11 @@ public interface CraftingContainer extends Container, StackedContentsCompatible - List getItems(); - - // CraftBukkit start -- default RecipeHolder getCurrentRecipe() { -+ default RecipeHolder getCurrentRecipe() { // Paper - use correct generic - return null; - } - -- default void setCurrentRecipe(RecipeHolder recipe) { -+ default void setCurrentRecipe(RecipeHolder recipe) { // Paper - use correct generic - } - // CraftBukkit end - -diff --git a/src/main/java/net/minecraft/world/inventory/CraftingMenu.java b/src/main/java/net/minecraft/world/inventory/CraftingMenu.java -index 6b3006a8543265664a2e54898ece92c66afb9c21..2e4043248c3ac7a54d894d76b99adc26518d3866 100644 ---- a/src/main/java/net/minecraft/world/inventory/CraftingMenu.java -+++ b/src/main/java/net/minecraft/world/inventory/CraftingMenu.java -@@ -56,6 +56,7 @@ public class CraftingMenu extends AbstractCraftingMenu { - CraftingInput craftinginput = craftingInventory.asCraftInput(); - ServerPlayer entityplayer = (ServerPlayer) player; - ItemStack itemstack = ItemStack.EMPTY; -+ if (recipe == null) recipe = craftingInventory.getCurrentRecipe(); // Paper - Perf: Improve mass crafting; check last recipe used first - Optional> optional = world.getServer().getRecipeManager().getRecipeFor(RecipeType.CRAFTING, craftinginput, world, recipe); - craftingInventory.setCurrentRecipe(optional.orElse(null)); // CraftBukkit - -diff --git a/src/main/java/net/minecraft/world/inventory/ResultSlot.java b/src/main/java/net/minecraft/world/inventory/ResultSlot.java -index 1ea4f0800598a75ba74ce033378749d1abe4009b..ff30071f3ef37d1b28cf86e26ce4f7477335a07a 100644 ---- a/src/main/java/net/minecraft/world/inventory/ResultSlot.java -+++ b/src/main/java/net/minecraft/world/inventory/ResultSlot.java -@@ -72,7 +72,7 @@ public class ResultSlot extends Slot { - private NonNullList getRemainingItems(CraftingInput input, Level world) { - return world instanceof ServerLevel serverLevel - ? serverLevel.recipeAccess() -- .getRecipeFor(RecipeType.CRAFTING, input, serverLevel) -+ .getRecipeFor(RecipeType.CRAFTING, input, serverLevel, this.craftSlots.getCurrentRecipe()) // Paper - Perf: Improve mass crafting; check last recipe used first - .map(recipe -> recipe.value().getRemainingItems(input)) - .orElseGet(() -> copyAllInputItems(input)) - : CraftingRecipe.defaultCraftingReminder(input); -diff --git a/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java b/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java -index 32d49d759f95d27ca04b843bbb2c2fd22ebd7e4a..a458f7b8270dc1d5902e0d131dbf9e66209e26be 100644 ---- a/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java -+++ b/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java -@@ -27,7 +27,7 @@ public class TransientCraftingContainer implements CraftingContainer { - - // CraftBukkit start - add fields - public List transaction = new java.util.ArrayList(); -- private RecipeHolder currentRecipe; -+ private RecipeHolder currentRecipe; // Paper - use correct generic - public Container resultInventory; - private Player owner; - private int maxStack = MAX_STACK; -@@ -72,12 +72,12 @@ public class TransientCraftingContainer implements CraftingContainer { - } - - @Override -- public RecipeHolder getCurrentRecipe() { -+ public RecipeHolder getCurrentRecipe() { // Paper - use correct generic - return this.currentRecipe; - } - - @Override -- public void setCurrentRecipe(RecipeHolder currentRecipe) { -+ public void setCurrentRecipe(RecipeHolder currentRecipe) { // Paper - use correct generic - this.currentRecipe = currentRecipe; - } - diff --git a/patches/unapplied/server/1048-Incremental-chunk-and-player-saving.patch b/patches/unapplied/server/1048-Incremental-chunk-and-player-saving.patch deleted file mode 100644 index 321440c65f..0000000000 --- a/patches/unapplied/server/1048-Incremental-chunk-and-player-saving.patch +++ /dev/null @@ -1,134 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Shane Freeder -Date: Sun, 9 Jun 2019 03:53:22 +0100 -Subject: [PATCH] Incremental chunk and player saving - -Feature patch - -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index ca70815b73199835b88c9d68c8a01699536d320f..be6e64d5c858961b19eb7b1b028530c1eb4c68d7 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1007,7 +1007,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.ticksUntilAutosave <= 0) { // CraftBukkit -- this.autoSave(); -+ // Paper start - Incremental chunk and player saving -+ final ProfilerFiller profiler = Profiler.get(); -+ int playerSaveInterval = io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.rate; -+ if (playerSaveInterval < 0) { -+ playerSaveInterval = autosavePeriod; -+ } -+ profiler.push("save"); -+ final boolean fullSave = autosavePeriod > 0 && this.tickCount % autosavePeriod == 0; -+ try { -+ this.isSaving = true; -+ if (playerSaveInterval > 0) { -+ this.playerList.saveAll(playerSaveInterval); -+ } -+ for (final ServerLevel level : this.getAllLevels()) { -+ if (level.paperConfig().chunks.autoSaveInterval.value() > 0) { -+ level.saveIncrementally(fullSave); -+ } -+ } -+ } finally { -+ this.isSaving = false; - } -+ profiler.pop(); -+ // Paper end - Incremental chunk and player saving - - ProfilerFiller gameprofilerfiller = Profiler.get(); - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 2e0dcac7642d899efd60cf70fb0ad0336e1923da..b5f0ce0869c0ea6ad478bddddfc463ec42a5bef7 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -1353,6 +1353,30 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - return !this.server.isUnderSpawnProtection(this, pos, player) && this.getWorldBorder().isWithinBounds(pos); - } - -+ // Paper start - Incremental chunk and player saving -+ public void saveIncrementally(boolean doFull) { -+ ServerChunkCache chunkproviderserver = this.getChunkSource(); -+ -+ if (doFull) { -+ org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(getWorld())); -+ } -+ -+ if (doFull) { -+ this.saveLevelData(true); -+ } -+ // chunk autosave is already called by the ChunkSystem during unload processing (ChunkMap#processUnloads) -+ // Copied from save() -+ // CraftBukkit start - moved from MinecraftServer.saveChunks -+ if (doFull) { // Paper -+ ServerLevel worldserver1 = this; -+ this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings()); -+ this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess())); -+ this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); -+ } -+ // CraftBukkit end -+ } -+ // Paper end - Incremental chunk and player saving -+ - public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) { - // Paper start - add close param - this.save(progressListener, flush, savingDisabled, false); -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 2b6c5b2387b67f25d8877849ccbfaaa77eab51d3..05981a075898794b899f1327bff1e7ca8ef8fc13 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -221,6 +221,7 @@ import org.bukkit.inventory.MainHand; - public class ServerPlayer extends net.minecraft.world.entity.player.Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system - - private static final Logger LOGGER = LogUtils.getLogger(); -+ public long lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving - private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32; - private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10; - private static final int FLY_STAT_RECORDING_SPEED = 25; -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 532e4c1dac20d7481557bb8c84f81c30994ae4d5..8de23b39806734c9a413b6d98dbfff25888c1798 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -518,6 +518,7 @@ public abstract class PlayerList { - - protected void save(ServerPlayer player) { - if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit -+ player.lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving - this.playerIo.save(player); - ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit - -@@ -1152,9 +1153,21 @@ public abstract class PlayerList { - } - - public void saveAll() { -+ // Paper start - Incremental chunk and player saving -+ this.saveAll(-1); -+ } -+ -+ public void saveAll(int interval) { - io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main -+ int numSaved = 0; -+ long now = MinecraftServer.currentTick; - for (int i = 0; i < this.players.size(); ++i) { -- this.save((ServerPlayer) this.players.get(i)); -+ final ServerPlayer player = this.players.get(i); -+ if (interval == -1 || now - player.lastSave >= interval) { -+ this.save(player); -+ if (interval != -1 && ++numSaved >= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) { break; } -+ } -+ // Paper end - Incremental chunk and player saving - } - - return null; }); // Paper - ensure main diff --git a/patches/unapplied/server/1049-Optimise-general-POI-access.patch b/patches/unapplied/server/1049-Optimise-general-POI-access.patch deleted file mode 100644 index 2fa0832c02..0000000000 --- a/patches/unapplied/server/1049-Optimise-general-POI-access.patch +++ /dev/null @@ -1,1063 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sun, 31 Jan 2021 02:29:24 -0800 -Subject: [PATCH] Optimise general POI access - -There are a couple of problems with mojang's POI code. -Firstly, it's all streams. Unsurprisingly, stacking -streams on top of each other is horrible for performance -and ultimately took up half of a villager's tick! - -Secondly, sometime's the search radius is large and there are -a significant number of poi entries per chunk section. Even -removing streams at this point doesn't help much. The only solution -is to start at the search point and iterate outwards. This -type of approach shows massive gains for portals, simply because -we can avoid sync loading a large area of chunks. I also tested -a massive farm I found in JellySquid's discord, which showed -to benefit significantly simply because the farm had so many -portal blocks that searching through them all was very slow. - -Great care has been taken so that behavior remains identical to -vanilla, however I cannot account for oddball Stream API -implementations, if they even exist (streams can technically -be loose with iteration order in a sorted stream given its -source stream is not tagged with ordered, and mojang does not -tag the source stream as ordered). However in my testing on openjdk -there showed no difference, as expected. - -This patch also specifically optimises other areas of code to -use PoiAccess. For example, some villager AI and portaling code -had to be specifically modified. - -Feature patch - -diff --git a/src/main/java/io/papermc/paper/util/PoiAccess.java b/src/main/java/io/papermc/paper/util/PoiAccess.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f39294b1f83c4022be5ced4da781103a1eee2daf ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/PoiAccess.java -@@ -0,0 +1,806 @@ -+package io.papermc.paper.util; -+ -+import ca.spottedleaf.moonrise.common.util.CoordinateUtils; -+import ca.spottedleaf.moonrise.common.util.WorldUtil; -+import com.mojang.datafixers.util.Pair; -+import it.unimi.dsi.fastutil.doubles.Double2ObjectMap; -+import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap; -+import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; -+import it.unimi.dsi.fastutil.longs.LongOpenHashSet; -+import java.util.function.BiPredicate; -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Holder; -+import net.minecraft.util.Mth; -+import net.minecraft.world.entity.ai.village.poi.PoiManager; -+import net.minecraft.world.entity.ai.village.poi.PoiRecord; -+import net.minecraft.world.entity.ai.village.poi.PoiSection; -+import net.minecraft.world.entity.ai.village.poi.PoiType; -+import java.util.ArrayList; -+import java.util.HashSet; -+import java.util.Iterator; -+import java.util.List; -+import java.util.Map; -+import java.util.Optional; -+import java.util.Set; -+import java.util.function.Predicate; -+ -+/** -+ * Provides optimised access to POI data. All returned values will be identical to vanilla. -+ */ -+public final class PoiAccess { -+ -+ protected static double clamp(final double val, final double min, final double max) { -+ return (val < min ? min : (val > max ? max : val)); -+ } -+ -+ protected static double getSmallestDistanceSquared(final double boxMinX, final double boxMinY, final double boxMinZ, -+ final double boxMaxX, final double boxMaxY, final double boxMaxZ, -+ -+ final double circleX, final double circleY, final double circleZ) { -+ // is the circle center inside the box? -+ if (circleX >= boxMinX && circleX <= boxMaxX && circleY >= boxMinY && circleY <= boxMaxY && circleZ >= boxMinZ && circleZ <= boxMaxZ) { -+ return 0.0; -+ } -+ -+ final double boxWidthX = (boxMaxX - boxMinX) / 2.0; -+ final double boxWidthY = (boxMaxY - boxMinY) / 2.0; -+ final double boxWidthZ = (boxMaxZ - boxMinZ) / 2.0; -+ -+ final double boxCenterX = (boxMinX + boxMaxX) / 2.0; -+ final double boxCenterY = (boxMinY + boxMaxY) / 2.0; -+ final double boxCenterZ = (boxMinZ + boxMaxZ) / 2.0; -+ -+ double centerDiffX = circleX - boxCenterX; -+ double centerDiffY = circleY - boxCenterY; -+ double centerDiffZ = circleZ - boxCenterZ; -+ -+ centerDiffX = circleX - (clamp(centerDiffX, -boxWidthX, boxWidthX) + boxCenterX); -+ centerDiffY = circleY - (clamp(centerDiffY, -boxWidthY, boxWidthY) + boxCenterY); -+ centerDiffZ = circleZ - (clamp(centerDiffZ, -boxWidthZ, boxWidthZ) + boxCenterZ); -+ -+ return (centerDiffX * centerDiffX) + (centerDiffY * centerDiffY) + (centerDiffZ * centerDiffZ); -+ } -+ -+ -+ // key is: -+ // upper 32 bits: -+ // upper 16 bits: max y section -+ // lower 16 bits: min y section -+ // lower 32 bits: -+ // upper 16 bits: section -+ // lower 16 bits: radius -+ protected static long getKey(final int minSection, final int maxSection, final int section, final int radius) { -+ return ( -+ (maxSection & 0xFFFFL) << (64 - 16) -+ | (minSection & 0xFFFFL) << (64 - 32) -+ | (section & 0xFFFFL) << (64 - 48) -+ | (radius & 0xFFFFL) << (64 - 64) -+ ); -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. -+ public static BlockPos findClosestPoiDataPosition(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final PoiRecord ret = findClosestPoiDataRecord( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load -+ ); -+ -+ return ret == null ? null : ret.getPos(); -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. -+ public static Pair, BlockPos> findClosestPoiDataTypeAndPosition(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final PoiRecord ret = findClosestPoiDataRecord( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load -+ ); -+ -+ return ret == null ? null : Pair.of(ret.getPoiType(), ret.getPos()); -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. -+ public static void findClosestPoiDataPositions(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final Set ret) { -+ final Set positions = new HashSet<>(); -+ // pos predicate is last thing that runs before adding to ret. -+ final Predicate newPredicate = (final BlockPos pos) -> { -+ if (positionPredicate != null && !positionPredicate.test(pos)) { -+ return false; -+ } -+ return positions.add(pos.immutable()); -+ }; -+ -+ final List toConvert = new ArrayList<>(); -+ findClosestPoiDataRecords( -+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, toConvert -+ ); -+ -+ for (final PoiRecord record : toConvert) { -+ ret.add(record.getPos()); -+ } -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. -+ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final List ret = new ArrayList<>(); -+ findClosestPoiDataRecords( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret -+ ); -+ return ret.isEmpty() ? null : ret.get(0); -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. -+ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final BiPredicate, BlockPos> predicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final List ret = new ArrayList<>(); -+ findClosestPoiDataRecords( -+ poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret -+ ); -+ return ret.isEmpty() ? null : ret.get(0); -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. -+ public static void findClosestPoiDataRecords(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final List ret) { -+ final BiPredicate, BlockPos> predicate = positionPredicate != null ? (type, pos) -> positionPredicate.test(pos) : null; -+ findClosestPoiDataRecords(poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret); -+ } -+ -+ public static void findClosestPoiDataRecords(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final BiPredicate, BlockPos> predicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final List ret) { -+ final Predicate occupancyFilter = occupancy.getTest(); -+ -+ final List closestRecords = new ArrayList<>(); -+ double closestDistanceSquared = maxDistanceSquared; -+ -+ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; -+ final int lowerY = WorldUtil.getMinSection(poiStorage.moonrise$getWorld()); -+ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; -+ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; -+ final int upperY = WorldUtil.getMaxSection(poiStorage.moonrise$getWorld()); -+ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; -+ -+ final int centerX = sourcePosition.getX() >> 4; -+ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); -+ final int centerZ = sourcePosition.getZ() >> 4; -+ final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ); -+ -+ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); -+ final LongOpenHashSet seen = new LongOpenHashSet(); -+ seen.add(centerKey); -+ queue.enqueue(centerKey); -+ -+ while (!queue.isEmpty()) { -+ final long key = queue.dequeueLong(); -+ final int sectionX = CoordinateUtils.getChunkSectionX(key); -+ final int sectionY = CoordinateUtils.getChunkSectionY(key); -+ final int sectionZ = CoordinateUtils.getChunkSectionZ(key); -+ -+ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { -+ // out of bound chunk -+ continue; -+ } -+ -+ final double sectionDistanceSquared = getSmallestDistanceSquared( -+ (sectionX << 4) + 0.5, -+ (sectionY << 4) + 0.5, -+ (sectionZ << 4) + 0.5, -+ (sectionX << 4) + 15.5, -+ (sectionY << 4) + 15.5, -+ (sectionZ << 4) + 15.5, -+ (double)sourcePosition.getX(), (double)sourcePosition.getY(), (double)sourcePosition.getZ() -+ ); -+ if (sectionDistanceSquared > closestDistanceSquared) { -+ continue; -+ } -+ -+ // queue all neighbours -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ for (int dy = -1; dy <= 1; ++dy) { -+ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many -+ // values are set. we only care about cardinal neighbours, so, we only care if one value is set -+ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { -+ continue; -+ } -+ -+ final int neighbourX = sectionX + dx; -+ final int neighbourY = sectionY + dy; -+ final int neighbourZ = sectionZ + dz; -+ -+ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); -+ if (seen.add(neighbourKey)) { -+ queue.enqueue(neighbourKey); -+ } -+ } -+ } -+ } -+ -+ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); -+ -+ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { -+ continue; -+ } -+ -+ final PoiSection poiSection = poiSectionOptional.get(); -+ -+ final Map, Set> sectionData = poiSection.getData(); -+ if (sectionData.isEmpty()) { -+ continue; -+ } -+ -+ // now we search the section data -+ for (final Map.Entry, Set> entry : sectionData.entrySet()) { -+ if (!villagePlaceType.test(entry.getKey())) { -+ // filter out by poi type -+ continue; -+ } -+ -+ // now we can look at the poi data -+ for (final PoiRecord poiData : entry.getValue()) { -+ if (!occupancyFilter.test(poiData)) { -+ // filter by occupancy -+ continue; -+ } -+ -+ final BlockPos poiPosition = poiData.getPos(); -+ -+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range -+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { -+ // out of range for square radius -+ continue; -+ } -+ -+ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! -+ final double dataRange = poiPosition.distSqr(sourcePosition); -+ -+ if (dataRange > closestDistanceSquared) { -+ // out of range for distance check -+ continue; -+ } -+ -+ if (predicate != null && !predicate.test(poiData.getPoiType(), poiPosition)) { -+ // filter by position -+ continue; -+ } -+ -+ if (dataRange < closestDistanceSquared) { -+ closestRecords.clear(); -+ closestDistanceSquared = dataRange; -+ } -+ closestRecords.add(poiData); -+ } -+ } -+ } -+ -+ // uh oh! we might have multiple records that match the distance sorting! -+ // we need to re-order our results by the way vanilla would have iterated over them. -+ closestRecords.sort((record1, record2) -> { -+ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section -+ // is fine and should be preserved (this sort is stable so we're good there) -+ // but they iterate sections by x then by z (like the following) -+ // for (int x = -dx; x <= dx; ++x) -+ // for (int z = -dz; z <= dz; ++z) -+ // .... -+ // so we need to reorder such that records with lower chunk z, then lower chunk x come first -+ final BlockPos pos1 = record1.getPos(); -+ final BlockPos pos2 = record2.getPos(); -+ -+ final int cx1 = pos1.getX() >> 4; -+ final int cz1 = pos1.getZ() >> 4; -+ -+ final int cx2 = pos2.getX() >> 4; -+ final int cz2 = pos2.getZ() >> 4; -+ -+ if (cz2 != cz1) { -+ // want smaller z -+ return Integer.compare(cz1, cz2); -+ } -+ -+ if (cx2 != cx1) { -+ // want smaller x -+ return Integer.compare(cx1, cx2); -+ } -+ -+ // same chunk -+ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y -+ // so now we just compare section y, wanting smaller y -+ -+ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); -+ }); -+ -+ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). -+ ret.addAll(closestRecords); -+ } -+ -+ // finds the closest poi entry pos. -+ public static BlockPos findNearestPoiPosition(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final PoiRecord ret = findNearestPoiRecord( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load -+ ); -+ return ret == null ? null : ret.getPos(); -+ } -+ -+ // finds the closest `max` poi entry positions. -+ public static void findNearestPoiPositions(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final int max, -+ final List, BlockPos>> ret) { -+ final Set positions = new HashSet<>(); -+ // pos predicate is last thing that runs before adding to ret. -+ final Predicate newPredicate = (final BlockPos pos) -> { -+ if (positionPredicate != null && !positionPredicate.test(pos)) { -+ return false; -+ } -+ return positions.add(pos.immutable()); -+ }; -+ -+ final List toConvert = new ArrayList<>(); -+ findNearestPoiRecords( -+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, max, toConvert -+ ); -+ -+ for (final PoiRecord record : toConvert) { -+ ret.add(Pair.of(record.getPoiType(), record.getPos())); -+ } -+ } -+ -+ // finds the closest poi entry. -+ public static PoiRecord findNearestPoiRecord(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final List ret = new ArrayList<>(); -+ findNearestPoiRecords( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, -+ 1, ret -+ ); -+ return ret.isEmpty() ? null : ret.get(0); -+ } -+ -+ // finds the closest `max` poi entries. -+ public static void findNearestPoiRecords(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final int max, -+ final List ret) { -+ final Predicate occupancyFilter = occupancy.getTest(); -+ -+ final Double2ObjectRBTreeMap> closestRecords = new Double2ObjectRBTreeMap<>(); -+ int totalRecords = 0; -+ double furthestDistanceSquared = maxDistanceSquared; -+ -+ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; -+ final int lowerY = WorldUtil.getMinSection(poiStorage.moonrise$getWorld()); -+ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; -+ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; -+ final int upperY = WorldUtil.getMaxSection(poiStorage.moonrise$getWorld()); -+ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; -+ -+ final int centerX = sourcePosition.getX() >> 4; -+ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); -+ final int centerZ = sourcePosition.getZ() >> 4; -+ final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ); -+ -+ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); -+ final LongOpenHashSet seen = new LongOpenHashSet(); -+ seen.add(centerKey); -+ queue.enqueue(centerKey); -+ -+ while (!queue.isEmpty()) { -+ final long key = queue.dequeueLong(); -+ final int sectionX = CoordinateUtils.getChunkSectionX(key); -+ final int sectionY = CoordinateUtils.getChunkSectionY(key); -+ final int sectionZ = CoordinateUtils.getChunkSectionZ(key); -+ -+ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { -+ // out of bound chunk -+ continue; -+ } -+ -+ final double sectionDistanceSquared = getSmallestDistanceSquared( -+ (sectionX << 4) + 0.5, -+ (sectionY << 4) + 0.5, -+ (sectionZ << 4) + 0.5, -+ (sectionX << 4) + 15.5, -+ (sectionY << 4) + 15.5, -+ (sectionZ << 4) + 15.5, -+ (double) sourcePosition.getX(), (double) sourcePosition.getY(), (double) sourcePosition.getZ() -+ ); -+ -+ if (sectionDistanceSquared > (totalRecords >= max ? furthestDistanceSquared : maxDistanceSquared)) { -+ continue; -+ } -+ -+ // queue all neighbours -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ for (int dy = -1; dy <= 1; ++dy) { -+ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many -+ // values are set. we only care about cardinal neighbours, so, we only care if one value is set -+ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { -+ continue; -+ } -+ -+ final int neighbourX = sectionX + dx; -+ final int neighbourY = sectionY + dy; -+ final int neighbourZ = sectionZ + dz; -+ -+ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); -+ if (seen.add(neighbourKey)) { -+ queue.enqueue(neighbourKey); -+ } -+ } -+ } -+ } -+ -+ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); -+ -+ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { -+ continue; -+ } -+ -+ final PoiSection poiSection = poiSectionOptional.get(); -+ -+ final Map, Set> sectionData = poiSection.getData(); -+ if (sectionData.isEmpty()) { -+ continue; -+ } -+ -+ // now we search the section data -+ for (final Map.Entry, Set> entry : sectionData.entrySet()) { -+ if (!villagePlaceType.test(entry.getKey())) { -+ // filter out by poi type -+ continue; -+ } -+ -+ // now we can look at the poi data -+ for (final PoiRecord poiData : entry.getValue()) { -+ if (!occupancyFilter.test(poiData)) { -+ // filter by occupancy -+ continue; -+ } -+ -+ final BlockPos poiPosition = poiData.getPos(); -+ -+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range -+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { -+ // out of range for square radius -+ continue; -+ } -+ -+ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! -+ final double dataRange = poiPosition.distSqr(sourcePosition); -+ -+ if (dataRange > maxDistanceSquared) { -+ // out of range for distance check -+ continue; -+ } -+ -+ if (dataRange > furthestDistanceSquared && totalRecords >= max) { -+ // out of range for distance check -+ continue; -+ } -+ -+ if (positionPredicate != null && !positionPredicate.test(poiPosition)) { -+ // filter by position -+ continue; -+ } -+ -+ if (dataRange > furthestDistanceSquared) { -+ // we know totalRecords < max, so this entry is now our furthest -+ furthestDistanceSquared = dataRange; -+ } -+ -+ closestRecords.computeIfAbsent(dataRange, (final double unused) -> { -+ return new ArrayList<>(); -+ }).add(poiData); -+ -+ if (++totalRecords >= max) { -+ if (closestRecords.size() >= 2) { -+ int entriesInClosest = 0; -+ final Iterator>> iterator = closestRecords.double2ObjectEntrySet().iterator(); -+ double nextFurthestDistanceSquared = 0.0; -+ -+ for (int i = 0, len = closestRecords.size() - 1; i < len; ++i) { -+ final Double2ObjectMap.Entry> recordEntry = iterator.next(); -+ entriesInClosest += recordEntry.getValue().size(); -+ nextFurthestDistanceSquared = recordEntry.getDoubleKey(); -+ } -+ -+ if (entriesInClosest >= max) { -+ // the last set of entries at range wont even be considered for sure... nuke em -+ final Double2ObjectMap.Entry> recordEntry = iterator.next(); -+ totalRecords -= recordEntry.getValue().size(); -+ iterator.remove(); -+ -+ furthestDistanceSquared = nextFurthestDistanceSquared; -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ final List closestRecordsUnsorted = new ArrayList<>(); -+ -+ // we're done here, so now just flatten the map and sort it. -+ -+ for (final List records : closestRecords.values()) { -+ closestRecordsUnsorted.addAll(records); -+ } -+ -+ // uh oh! we might have multiple records that match the distance sorting! -+ // we need to re-order our results by the way vanilla would have iterated over them. -+ closestRecordsUnsorted.sort((record1, record2) -> { -+ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section -+ // is fine and should be preserved (this sort is stable so we're good there) -+ // but they iterate sections by x then by z (like the following) -+ // for (int x = -dx; x <= dx; ++x) -+ // for (int z = -dz; z <= dz; ++z) -+ // .... -+ // so we need to reorder such that records with lower chunk z, then lower chunk x come first -+ final BlockPos pos1 = record1.getPos(); -+ final BlockPos pos2 = record2.getPos(); -+ -+ final int cx1 = pos1.getX() >> 4; -+ final int cz1 = pos1.getZ() >> 4; -+ -+ final int cx2 = pos2.getX() >> 4; -+ final int cz2 = pos2.getZ() >> 4; -+ -+ if (cz2 != cz1) { -+ // want smaller z -+ return Integer.compare(cz1, cz2); -+ } -+ -+ if (cx2 != cx1) { -+ // want smaller x -+ return Integer.compare(cx1, cx2); -+ } -+ -+ // same chunk -+ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y -+ // so now we just compare section y, wanting smaller section y -+ -+ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); -+ }); -+ -+ // trim out any entries exceeding our maximum -+ for (int i = closestRecordsUnsorted.size() - 1; i >= max; --i) { -+ closestRecordsUnsorted.remove(i); -+ } -+ -+ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). -+ ret.addAll(closestRecordsUnsorted); -+ } -+ -+ public static BlockPos findAnyPoiPosition(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final PoiRecord ret = findAnyPoiRecord( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load -+ ); -+ -+ return ret == null ? null : ret.getPos(); -+ } -+ -+ public static void findAnyPoiPositions(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final int max, -+ final List, BlockPos>> ret) { -+ final Set positions = new HashSet<>(); -+ // pos predicate is last thing that runs before adding to ret. -+ final Predicate newPredicate = (final BlockPos pos) -> { -+ if (positionPredicate != null && !positionPredicate.test(pos)) { -+ return false; -+ } -+ return positions.add(pos.immutable()); -+ }; -+ -+ final List toConvert = new ArrayList<>(); -+ findAnyPoiRecords( -+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, occupancy, load, max, toConvert -+ ); -+ -+ for (final PoiRecord record : toConvert) { -+ ret.add(Pair.of(record.getPoiType(), record.getPos())); -+ } -+ } -+ -+ public static PoiRecord findAnyPoiRecord(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final List ret = new ArrayList<>(); -+ findAnyPoiRecords(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load, 1, ret); -+ return ret.isEmpty() ? null : ret.get(0); -+ } -+ -+ public static void findAnyPoiRecords(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final int max, -+ final List ret) { -+ // the biggest issue with the original mojang implementation is that they chain so many streams together -+ // the amount of streams chained just rolls performance, even if nothing is iterated over -+ final Predicate occupancyFilter = occupancy.getTest(); -+ final double rangeSquared = range * range; -+ -+ int added = 0; -+ -+ // First up, we need to iterate the chunks -+ // all the values here are in chunk sections -+ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; -+ final int lowerY = Math.max(WorldUtil.getMinSection(poiStorage.moonrise$getWorld()), Mth.floor(sourcePosition.getY() - range) >> 4); -+ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; -+ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; -+ final int upperY = Math.min(WorldUtil.getMaxSection(poiStorage.moonrise$getWorld()), Mth.floor(sourcePosition.getY() + range) >> 4); -+ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; -+ -+ // Vanilla iterates by x until max is reached then increases z -+ // vanilla also searches by increasing Y section value -+ for (int currZ = lowerZ; currZ <= upperZ; ++currZ) { -+ for (int currX = lowerX; currX <= upperX; ++currX) { -+ for (int currY = lowerY; currY <= upperY; ++currY) { // vanilla searches the entire chunk because they're actually stupid. just search the sections we need -+ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)) : -+ poiStorage.get(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)); -+ final PoiSection poiSection = poiSectionOptional == null ? null : poiSectionOptional.orElse(null); -+ if (poiSection == null) { -+ continue; -+ } -+ -+ final Map, Set> sectionData = poiSection.getData(); -+ if (sectionData.isEmpty()) { -+ continue; -+ } -+ -+ // now we search the section data -+ for (final Map.Entry, Set> entry : sectionData.entrySet()) { -+ if (!villagePlaceType.test(entry.getKey())) { -+ // filter out by poi type -+ continue; -+ } -+ -+ // now we can look at the poi data -+ for (final PoiRecord poiData : entry.getValue()) { -+ if (!occupancyFilter.test(poiData)) { -+ // filter by occupancy -+ continue; -+ } -+ -+ final BlockPos poiPosition = poiData.getPos(); -+ -+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range -+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { -+ // out of range for square radius -+ continue; -+ } -+ -+ if (poiPosition.distSqr(sourcePosition) > rangeSquared) { -+ // out of range for distance check -+ continue; -+ } -+ -+ if (positionPredicate != null && !positionPredicate.test(poiPosition)) { -+ // filter by position -+ continue; -+ } -+ -+ // found one! -+ ret.add(poiData); -+ if (++added >= max) { -+ return; -+ } -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ private PoiAccess() { -+ throw new RuntimeException(); -+ } -+} -diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java -index e8aa27547e3fa1a42720889c7038d4fb0273e7b5..e1b6fe9ecda25f86431baf414f1bfd3a26a8b2bd 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java -+++ b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java -@@ -71,11 +71,11 @@ public class AcquirePoi { - return true; - } - }; -- Set, BlockPos>> set = poiManager.findAllClosestFirstWithType( -- poiPredicate, predicate2, entity.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE -- ) -- .limit(5L) -- .collect(Collectors.toSet()); -+ // Paper start - optimise POI access -+ java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); -+ io.papermc.paper.util.PoiAccess.findNearestPoiPositions(poiManager, poiPredicate, predicate2, entity.blockPosition(), 48, 48*48, PoiManager.Occupancy.HAS_SPACE, false, 5, poiposes); -+ Set, BlockPos>> set = new java.util.HashSet<>(poiposes); -+ // Paper end - optimise POI access - Path path = findPathToPois(entity, set); - if (path != null && path.canReach()) { - BlockPos blockPos = path.getTarget(); -diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java -index d5a549f08b98c80a5cf0eef02cb8a389c32dfecb..92731b6b593289e9f583c9b705b219e81fcd8e73 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java -+++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java -@@ -53,11 +53,12 @@ public class NearestBedSensor extends Sensor { - return true; - } - }; -- Set, BlockPos>> set = poiManager.findAllWithType( -- holder -> holder.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY -- ) -- .collect(Collectors.toSet()); -- Path path = AcquirePoi.findPathToPois(entity, set); -+ // Paper start - optimise POI access -+ java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); -+ // don't ask me why it's unbounded. ask mojang. -+ io.papermc.paper.util.PoiAccess.findAnyPoiPositions(poiManager, type -> type.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes); -+ Path path = AcquirePoi.findPathToPois(entity, new java.util.HashSet<>(poiposes)); -+ // Paper end - optimise POI access - if (path != null && path.canReach()) { - BlockPos blockPos = path.getTarget(); - Optional> optional = poiManager.getType(blockPos); -diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java -index 5930a430983061afddf20e3208ff2462ca1b78cd..63a94b6068fdaef8bb26675c2927cb729ced1dac 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 -@@ -254,36 +254,45 @@ public class PoiManager extends SectionStorage im - public Optional find( - Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus - ) { -- return this.findAll(typePredicate, posPredicate, pos, radius, occupationStatus).findFirst(); -+ // Paper start - re-route to faster logic -+ BlockPos ret = io.papermc.paper.util.PoiAccess.findAnyPoiPosition(this, typePredicate, posPredicate, pos, radius, occupationStatus, false); -+ return Optional.ofNullable(ret); -+ // Paper end - } - - public Optional findClosest(Predicate> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) { -- return this.getInRange(typePredicate, pos, radius, occupationStatus) -- .map(PoiRecord::getPos) -- .min(Comparator.comparingDouble(poiPos -> poiPos.distSqr(pos))); -+ // Paper start - re-route to faster logic -+ BlockPos closestPos = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, null, pos, radius, radius * radius, occupationStatus, false); -+ return Optional.ofNullable(closestPos); -+ // Paper end - re-route to faster logic - } - - public Optional, BlockPos>> findClosestWithType( - Predicate> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus - ) { -- return this.getInRange(typePredicate, pos, radius, occupationStatus) -- .min(Comparator.comparingDouble(poi -> poi.getPos().distSqr(pos))) -- .map(poi -> Pair.of(poi.getPoiType(), poi.getPos())); -+ // Paper start - re-route to faster logic -+ return Optional.ofNullable(io.papermc.paper.util.PoiAccess.findClosestPoiDataTypeAndPosition( -+ this, typePredicate, null, pos, radius, radius * radius, occupationStatus, false -+ )); -+ // Paper end - re-route to faster logic - } - - public Optional findClosest( - Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus - ) { -- return this.getInRange(typePredicate, pos, radius, occupationStatus) -- .map(PoiRecord::getPos) -- .filter(posPredicate) -- .min(Comparator.comparingDouble(poiPos -> poiPos.distSqr(pos))); -+ // Paper start - re-route to faster logic -+ BlockPos closestPos = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, posPredicate, pos, radius, radius * radius, occupationStatus, false); -+ return Optional.ofNullable(closestPos); -+ // Paper end - re-route to faster logic - } - - public Optional take(Predicate> typePredicate, BiPredicate, BlockPos> posPredicate, BlockPos pos, int radius) { -- return this.getInRange(typePredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE) -- .filter(poi -> posPredicate.test(poi.getPoiType(), poi.getPos())) -- .findFirst() -+ // Paper start - re-route to faster logic -+ final @javax.annotation.Nullable PoiRecord closest = io.papermc.paper.util.PoiAccess.findClosestPoiDataRecord( -+ this, typePredicate, posPredicate, pos, radius, radius * radius, Occupancy.HAS_SPACE, false -+ ); -+ return Optional.ofNullable(closest) -+ // Paper end - re-route to faster logic - .map(poi -> { - poi.acquireTicket(); - return poi.getPos(); -@@ -298,8 +307,21 @@ public class PoiManager extends SectionStorage im - int radius, - RandomSource random - ) { -- List list = Util.toShuffledList(this.getInRange(typePredicate, pos, radius, occupationStatus), random); -- return list.stream().filter(poi -> positionPredicate.test(poi.getPos())).findFirst().map(PoiRecord::getPos); -+ // Paper start - re-route to faster logic -+ List list = new java.util.ArrayList<>(); -+ io.papermc.paper.util.PoiAccess.findAnyPoiRecords( -+ this, typePredicate, positionPredicate, pos, radius, occupationStatus, false, Integer.MAX_VALUE, list -+ ); -+ -+ // the old method shuffled the list and then tried to find the first element in it that -+ // matched positionPredicate, however we moved positionPredicate into the poi search. This means we can avoid a -+ // shuffle entirely, and just pick a random element from list -+ if (list.isEmpty()) { -+ return Optional.empty(); -+ } -+ -+ return Optional.of(list.get(random.nextInt(list.size())).getPos()); -+ // Paper end - re-route to faster logic - } - - public boolean release(BlockPos pos) { -diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java -index 712cbfc100e8aaf612d1d651dae64f57f892a768..827991ee61406bcda3f4794dcc735c0e2e0e09af 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java -+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java -@@ -26,7 +26,7 @@ import org.slf4j.Logger; - public class PoiSection implements ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection { // Paper - rewrite chunk system - private static final Logger LOGGER = LogUtils.getLogger(); - private final Short2ObjectMap records = new Short2ObjectOpenHashMap<>(); -- private final Map, Set> byType = Maps.newHashMap(); -+ private final Map, Set> byType = Maps.newHashMap(); public final Map, Set> getData() { return this.byType; } // Paper - public accessor - private final Runnable setDirty; - private boolean isValid; - -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java -index c3beb7fcad46a917d2b61bd0a0e98e5106056728..9b97fb2d125df4df715599aab27e074707731466 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java -@@ -131,11 +131,11 @@ public class SectionStorage implements AutoCloseable, ca.spottedleaf.moonr - } - - @Nullable -- protected Optional get(long pos) { -+ public Optional get(long pos) { // Paper - public - return this.storage.get(pos); - } - -- protected Optional getOrLoad(long pos) { -+ public Optional getOrLoad(long pos) { // Paper - public - if (this.outsideStoredRange(pos)) { - return Optional.empty(); - } else { -diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java -index 83d294f6f48b867d09ea0d339c779011bf4138a5..9204bb0538297f233442a86733a33e6d0eea8114 100644 ---- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java -+++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java -@@ -53,17 +53,39 @@ public class PortalForcer { - // int i = flag ? 16 : 128; - // CraftBukkit end - -- villageplace.ensureLoadedAndValid(this.level, blockposition, i); -- Stream stream = villageplace.getInSquare((holder) -> { // CraftBukkit - decompile error -- return holder.is(PoiTypes.NETHER_PORTAL); -- }, blockposition, i, PoiManager.Occupancy.ANY).map(PoiRecord::getPos); -- -- Objects.requireNonNull(worldborder); -- return stream.filter(worldborder::isWithinBounds).filter(pos -> !(this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> pos.getY() >= v))).filter((blockposition1) -> { // Paper - Configurable nether ceiling damage -- return this.level.getBlockState(blockposition1).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); -- }).min(Comparator.comparingDouble((BlockPos blockposition1) -> { // CraftBukkit - decompile error -- return blockposition1.distSqr(blockposition); -- }).thenComparingInt(Vec3i::getY)); -+ // Paper start - optimise portals -+ Optional optional; -+ java.util.List records = new java.util.ArrayList<>(); -+ io.papermc.paper.util.PoiAccess.findClosestPoiDataRecords( -+ villageplace, -+ type -> type.is(PoiTypes.NETHER_PORTAL), -+ (BlockPos pos) -> { -+ net.minecraft.world.level.chunk.ChunkAccess lowest = this.level.getChunk(pos.getX() >> 4, pos.getZ() >> 4, net.minecraft.world.level.chunk.status.ChunkStatus.EMPTY); -+ if (!lowest.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.FULL) -+ && (lowest.getBelowZeroRetrogen() == null || !lowest.getBelowZeroRetrogen().targetStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.SPAWN))) { -+ // why would we generate the chunk? -+ return false; -+ } -+ if (!worldborder.isWithinBounds(pos) || (this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> pos.getY() >= v))) { // Paper - Configurable nether ceiling damage -+ return false; -+ } -+ return lowest.getBlockState(pos).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); -+ }, -+ blockposition, i, Double.MAX_VALUE, PoiManager.Occupancy.ANY, true, records -+ ); -+ -+ // this gets us most of the way there, but we bias towards lower y values. -+ BlockPos lowestPos = null; -+ for (PoiRecord record : records) { -+ if (lowestPos == null) { -+ lowestPos = record.getPos(); -+ } else if (lowestPos.getY() > record.getPos().getY()) { -+ lowestPos = record.getPos(); -+ } -+ } -+ // now we're done -+ return Optional.ofNullable(lowestPos); -+ // Paper end - optimise portals - } - - public Optional createPortal(BlockPos pos, Direction.Axis axis) { diff --git a/patches/unapplied/server/1050-Fix-entity-tracker-desync-when-new-players-are-added.patch b/patches/unapplied/server/1050-Fix-entity-tracker-desync-when-new-players-are-added.patch deleted file mode 100644 index 0b5b0d3c49..0000000000 --- a/patches/unapplied/server/1050-Fix-entity-tracker-desync-when-new-players-are-added.patch +++ /dev/null @@ -1,107 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Tue, 20 Feb 2024 18:24:16 -0800 -Subject: [PATCH] Fix entity tracker desync when new players are added to the - tracker - -The delta position packet instructs the client to update -the entity position by a position difference. However, this position -difference is relative to the last position in the entity tracker -state, not the last position which has been sent to the player. As -a result, if the last position the player has recorded is different -than the one stored in the entity tracker (which occurs when a new -player is added to an existing entity tracker state) then the sent -position difference will cause a position desync for the client. - -We can resolve this problem by either tracking the last position -sent per-player, or by simply resetting the last sent position -in the entity tracker state every time a new player is added. -Resetting the last sent position every time a new player is -added to the tracker is just easier to do, so that is what -this patch does. - -This patch also fixes entities appearing to disappear when -teleporting to players by changing the initial position -in the spawn packet to the entities current tracking position. -When teleporting, the spawn packet will contain the old position -which is most likely in an unloaded chunk - which means that the -client will not tick the entity and thus not lerp the entity -from its old position to its new position. - -Feature patch - -diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java -index f6e1deb2f849d8b01b15cfa69e2f6cd5f2b1512b..f66e40326c510aa3267542b1a24ed75d1ed6d3f1 100644 ---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java -+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java -@@ -42,9 +42,11 @@ public class ClientboundAddEntityPacket implements Packet= 1 || Math.abs(b1 - this.lastSentXRot) >= 1; -@@ -199,7 +206,7 @@ public class ServerEntity { - long k = this.positionCodec.encodeZ(vec3d); - boolean flag5 = i < -32768L || i > 32767L || j < -32768L || j > 32767L || k < -32768L || k > 32767L; - -- if (!flag5 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.onGround()) { -+ if (!this.forceStateResync && !flag5 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.onGround()) { // Paper - fix desync when a player is added to the tracker - if ((!flag2 || !flag) && !(this.entity instanceof AbstractArrow)) { - if (flag2) { - packet1 = new ClientboundMoveEntityPacket.Pos(this.entity.getId(), (short) ((int) i), (short) ((int) j), (short) ((int) k), this.entity.onGround()); -@@ -265,6 +272,7 @@ public class ServerEntity { - } - - this.entity.hasImpulse = false; -+ this.forceStateResync = false; // Paper - fix desync when a player is added to the tracker - } - - ++this.tickCount; diff --git a/patches/unapplied/server/1051-Lag-compensation-ticks.patch b/patches/unapplied/server/1051-Lag-compensation-ticks.patch deleted file mode 100644 index 373bf30ab1..0000000000 --- a/patches/unapplied/server/1051-Lag-compensation-ticks.patch +++ /dev/null @@ -1,131 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sat, 23 Sep 2023 22:05:35 -0700 -Subject: [PATCH] Lag compensation ticks - -Areas affected by lag comepnsation: - - Block breaking and destroying - - Eating food items - -Feature patch - -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index be6e64d5c858961b19eb7b1b028530c1eb4c68d7..11a0bf52d891d79e3520de91d270b876871510f7 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -331,6 +331,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { - AtomicReference atomicreference = new AtomicReference(); -@@ -1841,6 +1842,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - BlockPhysicsEvent - worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent - net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers -+ worldserver.updateLagCompensationTick(); // Paper - lag compensation - - gameprofilerfiller.push(() -> { - String s = String.valueOf(worldserver); -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index b5f0ce0869c0ea6ad478bddddfc463ec42a5bef7..06ae6347d2c9666cb64aea2bea9ff946324015d9 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -582,6 +582,17 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - ); - } - // Paper end - chunk tick iteration -+ // Paper start - lag compensation -+ private long lagCompensationTick = net.minecraft.server.MinecraftServer.SERVER_INIT; -+ -+ public long getLagCompensationTick() { -+ return this.lagCompensationTick; -+ } -+ -+ public void updateLagCompensationTick() { -+ this.lagCompensationTick = (System.nanoTime() - net.minecraft.server.MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L)); -+ } -+ // Paper end - lag compensation - - // Add env and gen to constructor, IWorldDataServer -> WorldDataServer - public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -index 504c996220b278c194c93e001a3b326d549868ec..a96f859a5d0c6ec692d4627a69f3c9ee49199dbc 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -@@ -127,7 +127,7 @@ public class ServerPlayerGameMode { - } - - public void tick() { -- this.gameTicks = MinecraftServer.currentTick; // CraftBukkit; -+ this.gameTicks = (int)this.level.getLagCompensationTick(); // CraftBukkit; // Paper - lag compensation - BlockState iblockdata; - - if (this.hasDelayedDestroy) { -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 59c992173fda6153c58722caae061b0e6bee86a1..6a3a8f0466998409a01223bc0c16d92b96e50118 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -4051,6 +4051,10 @@ public abstract class LivingEntity extends Entity implements Attackable { - this.resendPossiblyDesyncedDataValues(java.util.List.of(DATA_LIVING_ENTITY_FLAGS), serverPlayer); - } - // Paper end - Properly cancel usable items -+ // Paper start - lag compensate eating -+ protected long eatStartTime; -+ protected int totalEatTimeTicks; -+ // Paper end - lag compensate eating - private void updatingUsingItem() { - if (this.isUsingItem()) { - if (ItemStack.isSameItem(this.getItemInHand(this.getUsedItemHand()), this.useItem)) { -@@ -4065,7 +4069,12 @@ public abstract class LivingEntity extends Entity implements Attackable { - - protected void updateUsingItem(ItemStack stack) { - stack.onUseTick(this.level(), this, this.getUseItemRemainingTicks()); -- if (--this.useItemRemaining == 0 && !this.level().isClientSide && !stack.useOnRelease()) { -+ // Paper start - lag compensate eating -+ // we add 1 to the expected time to avoid lag compensating when we should not -+ final boolean shouldLagCompensate = this.useItem.has(DataComponents.FOOD) && this.eatStartTime != -1 && (System.nanoTime() - this.eatStartTime) > ((1L + this.totalEatTimeTicks) * 50L * (1000L * 1000L)); -+ if ((--this.useItemRemaining == 0 || shouldLagCompensate) && !this.level().isClientSide && !stack.useOnRelease()) { -+ this.useItemRemaining = 0; -+ // Paper end - lag compensate eating - this.completeUsingItem(); - } - -@@ -4103,7 +4112,10 @@ public abstract class LivingEntity extends Entity implements Attackable { - - if (!itemstack.isEmpty() && !this.isUsingItem() || forceUpdate) { // Paper - Prevent consuming the wrong itemstack - this.useItem = itemstack; -- this.useItemRemaining = itemstack.getUseDuration(this); -+ // Paper start - lag compensate eating -+ this.useItemRemaining = this.totalEatTimeTicks = itemstack.getUseDuration(this); -+ this.eatStartTime = System.nanoTime(); -+ // Paper end - lag compensate eating - if (!this.level().isClientSide) { - this.setLivingEntityFlag(1, true); - this.setLivingEntityFlag(2, hand == InteractionHand.OFF_HAND); -@@ -4128,7 +4140,10 @@ public abstract class LivingEntity extends Entity implements Attackable { - } - } else if (!this.isUsingItem() && !this.useItem.isEmpty()) { - this.useItem = ItemStack.EMPTY; -- this.useItemRemaining = 0; -+ // Paper start - lag compensate eating -+ this.useItemRemaining = this.totalEatTimeTicks = 0; -+ this.eatStartTime = -1L; -+ // Paper end - lag compensate eating - } - } - -@@ -4259,7 +4274,10 @@ public abstract class LivingEntity extends Entity implements Attackable { - } - - this.useItem = ItemStack.EMPTY; -- this.useItemRemaining = 0; -+ // Paper start - lag compensate eating -+ this.useItemRemaining = this.totalEatTimeTicks = 0; -+ this.eatStartTime = -1L; -+ // Paper end - lag compensate eating - } - - public boolean isBlocking() { diff --git a/patches/unapplied/server/1052-Optimise-collision-checking-in-player-move-packet-ha.patch b/patches/unapplied/server/1052-Optimise-collision-checking-in-player-move-packet-ha.patch deleted file mode 100644 index 82b8f60c95..0000000000 --- a/patches/unapplied/server/1052-Optimise-collision-checking-in-player-move-packet-ha.patch +++ /dev/null @@ -1,170 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 2 Jul 2020 12:02:43 -0700 -Subject: [PATCH] Optimise collision checking in player move packet handling - -Move collision logic to just the hasNewCollision call instead of getCubes + hasNewCollision - -Feature patch - -diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index e3458038d56b7133f991a5198db26398a299bf30..d7ac001d53a083e9881f2320eb7fd5dcbd20416e 100644 ---- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -577,7 +577,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - return; - } - -- boolean flag = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); -+ AABB oldBox = entity.getBoundingBox(); // Paper - copy from player movement packet - - d6 = d3 - this.vehicleLastGoodX; // Paper - diff on change, used for checking large move vectors above - d7 = d4 - this.vehicleLastGoodY - 1.0E-6D; // Paper - diff on change, used for checking large move vectors above -@@ -593,6 +593,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - - entity.move(MoverType.PLAYER, new Vec3(d6, d7, d8)); -+ boolean didCollide = toX != entity.getX() || toY != entity.getY() || toZ != entity.getZ(); // Paper - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be... - double d11 = d7; - - d6 = d3 - entity.getX(); -@@ -606,15 +607,23 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - boolean flag2 = false; - - if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot -- flag2 = true; -+ flag2 = true; // Paper - diff on change, this should be moved wrongly - ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", new Object[]{entity.getName().getString(), this.player.getName().getString(), Math.sqrt(d10)}); - } - - entity.absMoveTo(d3, d4, d5, f, f1); - this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit -- boolean flag3 = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); - -- if (flag && (flag2 || !flag3)) { -+ // Paper start - optimise out extra getCubes -+ boolean teleportBack = flag2; // violating this is always a fail -+ if (!teleportBack) { -+ // note: only call after setLocation, or else getBoundingBox is wrong -+ AABB newBox = entity.getBoundingBox(); -+ if (didCollide || !oldBox.equals(newBox)) { -+ teleportBack = this.hasNewCollision(worldserver, entity, oldBox, newBox); -+ } // else: no collision at all detected, why do we care? -+ } -+ if (teleportBack) { // Paper end - optimise out extra getCubes - entity.absMoveTo(d0, d1, d2, f, f1); - this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit - this.send(new ClientboundMoveVehiclePacket(entity)); -@@ -697,7 +706,32 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - - private boolean noBlocksAround(Entity entity) { -- return entity.level().getBlockStates(entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D)).allMatch(BlockBehaviour.BlockStateBase::isAir); -+ // Paper start - stop using streams, this is already a known fixed problem in Entity#move -+ AABB box = entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D); -+ int minX = Mth.floor(box.minX); -+ int minY = Mth.floor(box.minY); -+ int minZ = Mth.floor(box.minZ); -+ int maxX = Mth.floor(box.maxX); -+ int maxY = Mth.floor(box.maxY); -+ int maxZ = Mth.floor(box.maxZ); -+ -+ Level world = entity.level(); -+ BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); -+ -+ for (int y = minY; y <= maxY; ++y) { -+ for (int z = minZ; z <= maxZ; ++z) { -+ for (int x = minX; x <= maxX; ++x) { -+ pos.set(x, y, z); -+ BlockState type = world.getBlockStateIfLoaded(pos); -+ if (type != null && !type.isAir()) { -+ return false; -+ } -+ } -+ } -+ } -+ -+ return true; -+ // Paper end - stop using streams, this is already a known fixed problem in Entity#move - } - - @Override -@@ -1398,7 +1432,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - } - -- AABB axisalignedbb = this.player.getBoundingBox(); -+ AABB axisalignedbb = this.player.getBoundingBox(); // Paper - diff on change, should be old AABB - - d6 = d0 - this.lastGoodX; // Paper - diff on change, used for checking large move vectors above - d7 = d1 - this.lastGoodY; // Paper - diff on change, used for checking large move vectors above -@@ -1440,6 +1474,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - - this.player.move(MoverType.PLAYER, new Vec3(d6, d7, d8)); - this.player.onGround = packet.isOnGround(); // CraftBukkit - SPIGOT-5810, SPIGOT-5835, SPIGOT-6828: reset by this.player.move -+ boolean didCollide = toX != this.player.getX() || toY != this.player.getY() || toZ != this.player.getZ(); // Paper - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be... - // Paper start - prevent position desync - if (this.awaitingPositionFromClient != null) { - return; // ... thanks Mojang for letting move calls teleport across dimensions. -@@ -1470,7 +1505,17 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - - // Paper start - Add fail move event -- boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && (movedWrongly && worldserver.noCollision(this.player, axisalignedbb) || this.isPlayerCollidingWithAnythingNew(worldserver, axisalignedbb, d0, d1, d2)); -+ // Paper start - optimise out extra getCubes -+ boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && movedWrongly; -+ this.player.absMoveTo(d0, d1, d2, f, f1); // prevent desync by tping to the set position, dropped for unknown reasons by mojang -+ 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? -+ } -+ // Paper end - optimise out extra getCubes - if (teleportBack) { - io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.CLIPPED_INTO_BLOCK, - toX, toY, toZ, toYaw, toPitch, false); -@@ -1594,7 +1639,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - - private boolean updateAwaitingTeleport() { - if (this.awaitingPositionFromClient != null) { -- if (this.tickCount - this.awaitingTeleportTime > 20) { -+ if (false && this.tickCount - this.awaitingTeleportTime > 20) { // Paper - this will greatly screw with clients with > 1000ms RTT - this.awaitingTeleportTime = this.tickCount; - this.teleport(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot()); - } -@@ -1607,6 +1652,33 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - } - -+ // Paper start - optimise out extra getCubes -+ private boolean hasNewCollision(final ServerLevel world, final Entity entity, final AABB oldBox, final AABB newBox) { -+ final List collisionsBB = new java.util.ArrayList<>(); -+ final List collisionsVoxel = new java.util.ArrayList<>(); -+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisions( -+ world, entity, newBox, collisionsVoxel, collisionsBB, -+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS | ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, -+ null, null -+ ); -+ -+ for (int i = 0, len = collisionsBB.size(); i < len; ++i) { -+ final AABB box = collisionsBB.get(i); -+ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(box, oldBox)) { -+ return true; -+ } -+ } -+ -+ for (int i = 0, len = collisionsVoxel.size(); i < len; ++i) { -+ final VoxelShape voxel = collisionsVoxel.get(i); -+ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(voxel, oldBox)) { -+ return true; -+ } -+ } -+ -+ return false; -+ } -+ // Paper end - optimise out extra getCubes - private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box, double newX, double newY, double newZ) { - AABB axisalignedbb1 = this.player.getBoundingBox().move(newX - this.player.getX(), newY - this.player.getY(), newZ - this.player.getZ()); - Iterable iterable = world.getCollisions(this.player, axisalignedbb1.deflate(9.999999747378752E-6D)); diff --git a/patches/unapplied/server/1053-Optional-per-player-mob-spawns.patch b/patches/unapplied/server/1053-Optional-per-player-mob-spawns.patch deleted file mode 100644 index 9e2c455dcc..0000000000 --- a/patches/unapplied/server/1053-Optional-per-player-mob-spawns.patch +++ /dev/null @@ -1,233 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: kickash32 -Date: Mon, 19 Aug 2019 01:27:58 +0500 -Subject: [PATCH] Optional per player mob spawns - -Feature patch - -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 7833c53b4eff67f2ff37c091b5926cb081205921..094096bd08450e5d656ce2c442757cbc63ffb090 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkMap.java -+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -229,8 +229,26 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - // Paper start -+ // Paper start - Optional per player mob spawns -+ public void updatePlayerMobTypeMap(final Entity entity) { -+ if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { -+ return; -+ } -+ final int index = entity.getType().getCategory().ordinal(); -+ -+ final ca.spottedleaf.moonrise.common.list.ReferenceList inRange = -+ this.level.moonrise$getNearbyPlayers().getPlayers(entity.chunkPosition(), ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE); -+ if (inRange == null) { -+ return; -+ } -+ final ServerPlayer[] backingSet = inRange.getRawDataUnchecked(); -+ for (int i = 0, len = inRange.size(); i < len; i++) { -+ ++(backingSet[i].mobCounts[index]); -+ } -+ } - public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) { -- return -1; -+ return player.mobCounts[mobCategory.ordinal()]; -+ // Paper end - Optional per player mob spawns - } - // Paper end - -diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index b3ce572547535001959d9bcc6cb567da552c6539..8e96905fa93b02623f16feb4369a45b175031ebf 100644 ---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java -+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -@@ -492,7 +492,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - gameprofilerfiller.popPush("shuffleChunks"); - // Paper start - chunk tick iteration optimisation - this.shuffleRandom.setSeed(this.level.random.nextLong()); -- Util.shuffle(list, this.shuffleRandom); -+ if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) Util.shuffle(list, this.shuffleRandom); // Paper - Optional per player mob spawns; do not need this when per-player is enabled - // Paper end - chunk tick iteration optimisation - this.tickChunks(gameprofilerfiller, j, list); - gameprofilerfiller.pop(); -@@ -549,7 +549,19 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - private void tickChunks(ProfilerFiller profiler, long timeDelta, List chunks) { - profiler.popPush("naturalSpawnCount"); - int j = this.distanceManager.getNaturalSpawnChunkCount(); -- NaturalSpawner.SpawnState spawnercreature_d = NaturalSpawner.createState(j, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap)); -+ // Paper start - Optional per player mob spawns -+ final int naturalSpawnChunkCount = j; -+ NaturalSpawner.SpawnState spawnercreature_d; // moved down -+ if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled -+ // re-set mob counts -+ for (ServerPlayer player : this.level.players) { -+ Arrays.fill(player.mobCounts, 0); -+ } -+ spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); -+ } else { -+ spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); -+ } -+ // Paper end - Optional per player mob spawns - - this.lastSpawnState = spawnercreature_d; - profiler.popPush("spawnAndTick"); -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 05981a075898794b899f1327bff1e7ca8ef8fc13..2b40896483ffbba2c84dbaaae3194342ed5d2170 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -303,6 +303,10 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple - public boolean queueHealthUpdatePacket; - public net.minecraft.network.protocol.game.ClientboundSetHealthPacket queuedHealthUpdatePacket; - // Paper end - cancellable death event -+ // Paper start - Optional per player mob spawns -+ public static final int MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length; -+ public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper -+ // Paper end - Optional per player mob spawns - - // CraftBukkit start - public CraftPlayer.TransferCookieConnection transferCookieConnection; -diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java -index bf943feca387b77a3154773a59da7190d38d8621..12ebd7829c7f6814ccd79ae96aa9023afcc64696 100644 ---- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java -+++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java -@@ -71,6 +71,12 @@ public final class NaturalSpawner { - private NaturalSpawner() {} - - public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable entities, NaturalSpawner.ChunkGetter chunkSource, LocalMobCapCalculator densityCapper) { -+ // Paper start - Optional per player mob spawns -+ return createState(spawningChunkCount, entities, chunkSource, densityCapper, false); -+ } -+ -+ public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable entities, NaturalSpawner.ChunkGetter chunkSource, LocalMobCapCalculator densityCapper, boolean countMobs) { -+ // Paper end - Optional per player mob spawns - PotentialCalculator spawnercreatureprobabilities = new PotentialCalculator(); - Object2IntOpenHashMap object2intopenhashmap = new Object2IntOpenHashMap(); - Iterator iterator = entities.iterator(); -@@ -103,11 +109,16 @@ public final class NaturalSpawner { - spawnercreatureprobabilities.addCharge(entity.blockPosition(), biomesettingsmobs_b.charge()); - } - -- if (entity instanceof Mob) { -+ if (densityCapper != null && entity instanceof Mob) { // Paper - Optional per player mob spawns - densityCapper.addMob(chunk.getPos(), enumcreaturetype); - } - - object2intopenhashmap.addTo(enumcreaturetype, 1); -+ // Paper start - Optional per player mob spawns -+ if (countMobs) { -+ chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity); -+ } -+ // Paper end - Optional per player mob spawns - }); - } - } -@@ -142,7 +153,7 @@ public final class NaturalSpawner { - continue; - } - -- if ((flag || !enumcreaturetype.isFriendly()) && (flag1 || enumcreaturetype.isFriendly()) && (flag2 || !enumcreaturetype.isPersistent()) && spawnercreature_d.canSpawnForCategoryGlobal(enumcreaturetype, limit)) { -+ if ((flag || !enumcreaturetype.isFriendly()) && (flag1 || enumcreaturetype.isFriendly()) && (flag2 || !enumcreaturetype.isPersistent()) && (worldserver.paperConfig().entities.spawning.perPlayerMobSpawns || spawnercreature_d.canSpawnForCategoryGlobal(enumcreaturetype, limit))) { // Paper - Optional per player mob spawns; remove global check, check later during the local one - // CraftBukkit end - list.add(enumcreaturetype); - } -@@ -160,12 +171,43 @@ public final class NaturalSpawner { - while (iterator.hasNext()) { - MobCategory enumcreaturetype = (MobCategory) iterator.next(); - -- if (info.canSpawnForCategoryLocal(enumcreaturetype, chunk.getPos())) { -+ // Paper start - Optional per player mob spawns -+ final boolean canSpawn; -+ int maxSpawns = Integer.MAX_VALUE; -+ if (world.paperConfig().entities.spawning.perPlayerMobSpawns) { -+ // Copied from getFilteredSpawningCategories -+ int limit = enumcreaturetype.getMaxInstancesPerChunk(); -+ SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(enumcreaturetype); -+ if (CraftSpawnCategory.isValidForLimits(spawnCategory)) { -+ limit = world.getWorld().getSpawnLimit(spawnCategory); -+ } -+ -+ // Apply per-player limit -+ int minDiff = Integer.MAX_VALUE; -+ final ca.spottedleaf.moonrise.common.list.ReferenceList inRange = -+ world.moonrise$getNearbyPlayers().getPlayers(chunk.getPos(), ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE); -+ if (inRange != null) { -+ final net.minecraft.server.level.ServerPlayer[] backingSet = inRange.getRawDataUnchecked(); -+ for (int k = 0, len = inRange.size(); k < len; k++) { -+ minDiff = Math.min(limit - world.getChunkSource().chunkMap.getMobCountNear(backingSet[k], enumcreaturetype), minDiff); -+ } -+ } -+ -+ maxSpawns = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff; -+ canSpawn = maxSpawns > 0; -+ } else { -+ canSpawn = info.canSpawnForCategoryLocal(enumcreaturetype, chunk.getPos()); -+ } -+ if (canSpawn) { -+ // Paper end - Optional per player mob spawns - Objects.requireNonNull(info); - NaturalSpawner.SpawnPredicate spawnercreature_c = info::canSpawn; - - Objects.requireNonNull(info); -- NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn); -+ // Paper start - Optional per player mob spawns -+ NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn, -+ maxSpawns, world.paperConfig().entities.spawning.perPlayerMobSpawns ? world.getChunkSource().chunkMap::updatePlayerMobTypeMap : null); -+ // Paper end - Optional per player mob spawns - } - } - -@@ -183,10 +225,15 @@ public final class NaturalSpawner { - // Paper end - Add mobcaps commands - - public static void spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) { -+ // Paper start - Optional per player mob spawns -+ spawnCategoryForChunk(group, world, chunk, checker, runner, Integer.MAX_VALUE, null); -+ } -+ public static void spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner, int maxSpawns, Consumer trackEntity) { -+ // Paper end - Optional per player mob spawns - BlockPos blockposition = NaturalSpawner.getRandomPosWithin(world, chunk); - - if (blockposition.getY() >= world.getMinY() + 1) { -- NaturalSpawner.spawnCategoryForPosition(group, world, chunk, blockposition, checker, runner); -+ NaturalSpawner.spawnCategoryForPosition(group, world, chunk, blockposition, checker, runner, maxSpawns, trackEntity); // Paper - Optional per player mob spawns - } - } - -@@ -198,7 +245,12 @@ public final class NaturalSpawner { - }); - } - -+ // Paper start - Optional per player mob spawns - public static void spawnCategoryForPosition(MobCategory group, ServerLevel world, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) { -+ spawnCategoryForPosition(group, world,chunk, pos, checker, runner, Integer.MAX_VALUE, null); -+ } -+ public static void spawnCategoryForPosition(MobCategory group, ServerLevel world, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner, int maxSpawns, Consumer trackEntity) { -+ // Paper end - Optional per player mob spawns - StructureManager structuremanager = world.structureManager(); - ChunkGenerator chunkgenerator = world.getChunkSource().getGenerator(); - int i = pos.getY(); -@@ -268,9 +320,14 @@ public final class NaturalSpawner { - ++j; - ++k1; - runner.run(entityinsentient, chunk); -+ // Paper start - Optional per player mob spawns -+ if (trackEntity != null) { -+ trackEntity.accept(entityinsentient); -+ } -+ // Paper end - Optional per player mob spawns - } - // CraftBukkit end -- if (j >= entityinsentient.getMaxSpawnClusterSize()) { -+ if (j >= entityinsentient.getMaxSpawnClusterSize() || j >= maxSpawns) { // Paper - Optional per player mob spawns - return; - } - -@@ -543,7 +600,7 @@ public final class NaturalSpawner { - MobCategory enumcreaturetype = entitytypes.getCategory(); - - this.mobCategoryCounts.addTo(enumcreaturetype, 1); -- this.localMobCapCalculator.addMob(new ChunkPos(blockposition), enumcreaturetype); -+ if (this.localMobCapCalculator != null) this.localMobCapCalculator.addMob(new ChunkPos(blockposition), enumcreaturetype); // Paper - Optional per player mob spawns - } - - public int getSpawnableChunkCount() { diff --git a/patches/unapplied/server/1054-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch b/patches/unapplied/server/1054-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch deleted file mode 100644 index 68c09ef04f..0000000000 --- a/patches/unapplied/server/1054-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch +++ /dev/null @@ -1,89 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: kickash32 -Date: Mon, 5 Apr 2021 01:42:35 -0400 -Subject: [PATCH] Improve cancelling PreCreatureSpawnEvent with per player mob - spawns - - -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 094096bd08450e5d656ce2c442757cbc63ffb090..cfeeddf2cb4ff50dbc29c6913e78ca1dee076790 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkMap.java -+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -246,8 +246,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - ++(backingSet[i].mobCounts[index]); - } - } -+ // Paper start - per player mob count backoff -+ public void updateFailurePlayerMobTypeMap(int chunkX, int chunkZ, net.minecraft.world.entity.MobCategory mobCategory) { -+ if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { -+ return; -+ } -+ int idx = mobCategory.ordinal(); -+ final ca.spottedleaf.moonrise.common.list.ReferenceList inRange = -+ this.level.moonrise$getNearbyPlayers().getPlayersByChunk(chunkX, chunkZ, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE); -+ if (inRange == null) { -+ return; -+ } -+ final ServerPlayer[] backingSet = inRange.getRawDataUnchecked(); -+ for (int i = 0, len = inRange.size(); i < len; i++) { -+ ++(backingSet[i].mobBackoffCounts[idx]); -+ } -+ } -+ // Paper end - per player mob count backoff - public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) { -- return player.mobCounts[mobCategory.ordinal()]; -+ return player.mobCounts[mobCategory.ordinal()] + player.mobBackoffCounts[mobCategory.ordinal()]; // Paper - per player mob count backoff - // Paper end - Optional per player mob spawns - } - // Paper end -diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index 8e96905fa93b02623f16feb4369a45b175031ebf..d021cd5b6136f0125076513977f430c6d4dd4f9f 100644 ---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java -+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -@@ -555,7 +555,17 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled - // re-set mob counts - for (ServerPlayer player : this.level.players) { -- Arrays.fill(player.mobCounts, 0); -+ // Paper start - per player mob spawning backoff -+ for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) { -+ player.mobCounts[ii] = 0; -+ -+ int newBackoff = player.mobBackoffCounts[ii] - 1; // TODO make configurable bleed // TODO use nonlinear algorithm? -+ if (newBackoff < 0) { -+ newBackoff = 0; -+ } -+ player.mobBackoffCounts[ii] = newBackoff; -+ } -+ // Paper end - per player mob spawning backoff - } - spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); - } else { -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 2b40896483ffbba2c84dbaaae3194342ed5d2170..a755a2742f18ed55adc1fc735d995c9874b1e62e 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -307,6 +307,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple - public static final int MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length; - public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper - // Paper end - Optional per player mob spawns -+ public final int[] mobBackoffCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper - per player mob count backoff - - // CraftBukkit start - public CraftPlayer.TransferCookieConnection transferCookieConnection; -diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java -index 12ebd7829c7f6814ccd79ae96aa9023afcc64696..c1b76a1ebc1eea7ab70cf61d8175a31794dd122a 100644 ---- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java -+++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java -@@ -299,6 +299,11 @@ public final class NaturalSpawner { - - // Paper start - PreCreatureSpawnEvent - PreSpawnStatus doSpawning = isValidSpawnPostitionForType(world, group, structuremanager, chunkgenerator, biomesettingsmobs_c, blockposition_mutableblockposition, d2); -+ // Paper start - per player mob count backoff -+ if (doSpawning == PreSpawnStatus.ABORT || doSpawning == PreSpawnStatus.CANCELLED) { -+ world.getChunkSource().chunkMap.updateFailurePlayerMobTypeMap(blockposition_mutableblockposition.getX() >> 4, blockposition_mutableblockposition.getZ() >> 4, group); -+ } -+ // Paper end - per player mob count backoff - if (doSpawning == PreSpawnStatus.ABORT) { - return; - } diff --git a/patches/unapplied/server/1055-Avoid-issues-with-certain-tasks-not-processing-durin.patch b/patches/unapplied/server/1055-Avoid-issues-with-certain-tasks-not-processing-durin.patch deleted file mode 100644 index 5da4e0387b..0000000000 --- a/patches/unapplied/server/1055-Avoid-issues-with-certain-tasks-not-processing-durin.patch +++ /dev/null @@ -1,46 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> -Date: Sun, 27 Oct 2024 14:18:28 -0700 -Subject: [PATCH] Avoid issues with certain tasks not processing during sleep - -Execute processQueue tasks during sleep: needed for console tab completions, pre join event, etc. - -Upstream has set precedent that the bukkit scheduler will still tick during sleep, which avoids some problems -with plugins not accounting for the new sleep feature, but can still lead to others. Because of this we have disabled -sleep by default, which avoids the problem and makes it more obvious to check if this is the cause of issues when -enabled. We also unload chunks during sleep to prevent memory leaks caused by plugin chunk loads. - -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 11a0bf52d891d79e3520de91d270b876871510f7..317bb0bd16d8125a40c37b75be1d4d0461bcf9ce 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1638,6 +1638,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop true, false); -+ } -+ // Paper end - avoid issues with certain tasks not processing during sleep - this.server.spark.executeMainThreadTasks(); // Paper - spark - this.tickConnection(); - this.server.spark.tickEnd(((double)(System.nanoTime() - lastTick) / 1000000D)); // Paper - spark -diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java -index a2633780619d73c29a23cb8b6a208ca9ba549fb0..c3ec370b83b895be0f03662e3884fa4a2442a2a6 100644 ---- a/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java -+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServerProperties.java -@@ -160,7 +160,7 @@ public class DedicatedServerProperties extends Settings -Date: Sun, 27 Oct 2024 12:36:53 -0700 -Subject: [PATCH] Allow using old ender pearl behavior - -When enabled, ender pearls will not load chunks and will save to the world instead of the player. - -== AT == -public net.minecraft.world.entity.projectile.Projectile cachedOwner - -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index a755a2742f18ed55adc1fc735d995c9874b1e62e..5a8f396d47577f087abb415c972fd4f51e50faba 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -836,6 +836,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple - - while (iterator.hasNext()) { - ThrownEnderpearl entityenderpearl = (ThrownEnderpearl) iterator.next(); -+ if (entityenderpearl.level().paperConfig().misc.legacyEnderPearlBehavior) continue; // Paper - Allow using old ender pearl behavior - - if (entityenderpearl.isRemoved()) { - ServerPlayer.LOGGER.warn("Trying to save removed ender pearl, skipping"); -@@ -3143,7 +3144,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple - } - - public static long placeEnderPearlTicket(ServerLevel world, ChunkPos chunkPos) { -- world.getChunkSource().addRegionTicket(TicketType.ENDER_PEARL, chunkPos, 2, chunkPos); -+ if (!world.paperConfig().misc.legacyEnderPearlBehavior) world.getChunkSource().addRegionTicket(TicketType.ENDER_PEARL, chunkPos, 2, chunkPos); // Paper - Allow using old ender pearl behavior - return TicketType.ENDER_PEARL.timeout(); - } - -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 8de23b39806734c9a413b6d98dbfff25888c1798..1a956249828156fdc273888de59128c3d1a0b898 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -602,7 +602,13 @@ public abstract class PlayerList { - while (iterator.hasNext()) { - ThrownEnderpearl entityenderpearl = (ThrownEnderpearl) iterator.next(); - -+ // Paper start - Allow using old ender pearl behavior -+ if (!entityenderpearl.level().paperConfig().misc.legacyEnderPearlBehavior) { - entityenderpearl.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER, EntityRemoveEvent.Cause.PLAYER_QUIT); // CraftBukkit - add Bukkit remove cause -+ } else { -+ entityenderpearl.cachedOwner = null; -+ } -+ // Paper end - Allow using old ender pearl behavior - } - - worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER); -diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java -index 5f790dd24f2bdae827c6dc597064b9b265089751..bd2684528157f928460f2143dd71a48e11983123 100644 ---- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java -+++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java -@@ -252,7 +252,7 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { - Entity entity = super.teleport(teleportTarget); - - if (entity != null) { -- entity.placePortalTicket(BlockPos.containing(entity.position())); -+ if (!this.level().paperConfig().misc.legacyEnderPearlBehavior) entity.placePortalTicket(BlockPos.containing(entity.position())); // Paper - Allow using old ender pearl behavior - } - - return entity; diff --git a/patches/unapplied/server/1057-Block-Enderpearl-Travel-Exploit.patch b/patches/unapplied/server/1057-Block-Enderpearl-Travel-Exploit.patch deleted file mode 100644 index 6c0ae6175b..0000000000 --- a/patches/unapplied/server/1057-Block-Enderpearl-Travel-Exploit.patch +++ /dev/null @@ -1,49 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Mon, 30 Apr 2018 17:15:26 -0400 -Subject: [PATCH] Block Enderpearl Travel Exploit - -Players are able to use alt accounts and enderpearls to travel -long distances utilizing the pearls in unloaded chunks and loading -the chunk later when convenient. - -This disables that by not saving the thrower when the chunk is unloaded. - -This is mainly useful for survival servers that do not allow freeform teleporting. - -Note: Currently removed as enderpearls are ticked as long as their owner is online in 1.21.2. -Might be worth to re-add once an option to disable the above vanilla mechanic is added, to -fully prevent enderpearl travel exploits. - -== AT == -public net.minecraft.world.entity.projectile.Projectile ownerUUID - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 06ae6347d2c9666cb64aea2bea9ff946324015d9..7db77a36701e766c148e91d8313838d307855d8a 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -2672,6 +2672,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - public void onTickingEnd(Entity entity) { - ServerLevel.this.entityTickList.remove(entity); -+ // Paper start - Reset pearls when they stop being ticked -+ if (ServerLevel.this.paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && ServerLevel.this.paperConfig().misc.legacyEnderPearlBehavior && entity instanceof net.minecraft.world.entity.projectile.ThrownEnderpearl pearl) { -+ pearl.cachedOwner = null; -+ pearl.ownerUUID = null; -+ } -+ // Paper end - Reset pearls when they stop being ticked - } - - public void onTrackingStart(Entity entity) { -diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java -index bf2e79c50092acd13e97ab6e32471a9c527a524e..6c2d4d6f3a36ab452dfd3c33f66e54f152906639 100644 ---- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java -+++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java -@@ -134,6 +134,7 @@ public abstract class Projectile extends Entity implements TraceableEntity { - protected void readAdditionalSaveData(CompoundTag nbt) { - if (nbt.hasUUID("Owner")) { - this.setOwnerThroughUUID(nbt.getUUID("Owner")); -+ if (this instanceof ThrownEnderpearl && this.level() != null && this.level().paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && this.level().paperConfig().misc.legacyEnderPearlBehavior) { this.ownerUUID = null; } // Paper - Reset pearls when they stop being ticked; Don't store shooter name for pearls to block enderpearl travel exploit - } - - this.leftOwner = nbt.getBoolean("LeftOwner"); diff --git a/patches/unapplied/server/1058-Fix-inconsistencies-in-dispense-events-regarding-sta.patch b/patches/unapplied/server/1058-Fix-inconsistencies-in-dispense-events-regarding-sta.patch deleted file mode 100644 index 30f853ff98..0000000000 --- a/patches/unapplied/server/1058-Fix-inconsistencies-in-dispense-events-regarding-sta.patch +++ /dev/null @@ -1,432 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Sun, 11 Dec 2022 23:47:22 -0800 -Subject: [PATCH] Fix inconsistencies in dispense events regarding stack size - -The javadocs for BlockDispenseEvent suggest the ItemStack is a single -item which is being dispensed. Before this fix, sometimes it was the whole -stack before a single item had been taken. This fixes that so the stack size -is always 1. - -diff --git a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java -index dff30954e4c588ee4cc79d3f6dab6fb456934d65..ddb264443f2e38b6348226016f9139727c588898 100644 ---- a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java -+++ b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java -@@ -49,7 +49,7 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior { - } - - // CraftBukkit start -- ItemStack itemstack1 = stack.split(1); -+ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink at end and single item in event - org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos()); - CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); - -@@ -59,12 +59,13 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior { - } - - if (event.isCancelled()) { -- stack.grow(1); -+ // stack.grow(1); // Paper - shrink below - return stack; - } - -+ boolean shrink = true; // Paper - if (!event.getItem().equals(craftItem)) { -- stack.grow(1); -+ shrink = false; // Paper - shrink below - // Chain to handler for new item - ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); - DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior -@@ -80,8 +81,7 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior { - abstractboat.setInitialPos(event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ()); // CraftBukkit - EntityType.createDefaultStackConfig(worldserver, stack, (Player) null).accept(abstractboat); - abstractboat.setYRot(enumdirection.toYRot()); -- if (!worldserver.addFreshEntity(abstractboat)) stack.grow(1); // CraftBukkit -- // itemstack.shrink(1); // CraftBukkit - handled during event processing -+ if (worldserver.addFreshEntity(abstractboat) && shrink) stack.shrink(1); // Paper - if entity add was successful and supposed to shrink - } - - return stack; -diff --git a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java -index 8f9b86e50717746e55232293d9e5ac05b8616aa0..0d12605dc84dad49faa18bf1fd058c3c168623ee 100644 ---- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java -+++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java -@@ -106,7 +106,7 @@ public interface DispenseItemBehavior { - - // CraftBukkit start - ServerLevel worldserver = pointer.level(); -- ItemStack itemstack1 = stack.split(1); -+ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event - org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos()); - CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); - -@@ -116,12 +116,13 @@ public interface DispenseItemBehavior { - } - - if (event.isCancelled()) { -- stack.grow(1); -+ // stack.grow(1); // Paper - shrink below - return stack; - } - -+ boolean shrink = true; // Paper - if (!event.getItem().equals(craftItem)) { -- stack.grow(1); -+ shrink = false; // Paper - shrink below - // Chain to handler for new item - ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); - DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior -@@ -142,7 +143,7 @@ public interface DispenseItemBehavior { - return ItemStack.EMPTY; - } - -- // itemstack.shrink(1); // Handled during event processing -+ if (shrink) stack.shrink(1); // Paper - actually handle here - // CraftBukkit end - pointer.level().gameEvent((Entity) null, (Holder) GameEvent.ENTITY_PLACE, pointer.pos()); - return stack; -@@ -164,7 +165,7 @@ public interface DispenseItemBehavior { - ServerLevel worldserver = pointer.level(); - - // CraftBukkit start -- ItemStack itemstack1 = stack.split(1); -+ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event - org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos()); - CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); - -@@ -174,12 +175,13 @@ public interface DispenseItemBehavior { - } - - if (event.isCancelled()) { -- stack.grow(1); -+ // stack.grow(1); // Paper - shrink below - return stack; - } - -+ boolean shrink = true; // Paper - if (!event.getItem().equals(craftItem)) { -- stack.grow(1); -+ shrink = false; // Paper - shrink below - // Chain to handler for new item - ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); - DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior -@@ -197,7 +199,7 @@ public interface DispenseItemBehavior { - ArmorStand entityarmorstand = (ArmorStand) EntityType.ARMOR_STAND.spawn(worldserver, consumer, blockposition, EntitySpawnReason.DISPENSER, false, false); - - if (entityarmorstand != null) { -- // itemstack.shrink(1); // CraftBukkit - Handled during event processing -+ if (shrink) stack.shrink(1); // Paper - actually handle here - } - - return stack; -@@ -217,7 +219,7 @@ public interface DispenseItemBehavior { - - if (!list.isEmpty()) { - // CraftBukkit start -- ItemStack itemstack1 = stack.split(1); -+ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event - ServerLevel world = pointer.level(); - org.bukkit.block.Block block = CraftBlock.at(world, pointer.pos()); - CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); -@@ -228,12 +230,13 @@ public interface DispenseItemBehavior { - } - - if (event.isCancelled()) { -- stack.grow(1); -+ // stack.grow(1); // Paper - shrink below - return stack; - } - -+ boolean shrink = true; // Paper - if (!event.getItem().equals(craftItem)) { -- stack.grow(1); -+ shrink = false; // Paper - shrink below - // Chain to handler for new item - ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); - DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior -@@ -244,6 +247,7 @@ public interface DispenseItemBehavior { - } - ((Saddleable) list.get(0)).equipSaddle(CraftItemStack.asNMSCopy(event.getItem()), SoundSource.BLOCKS); // Paper - track changed items in dispense event - // CraftBukkit end -+ if (shrink) stack.shrink(1); // Paper - actually handle here - this.setSuccess(true); - return stack; - } else { -@@ -270,7 +274,7 @@ public interface DispenseItemBehavior { - entityhorsechestedabstract = (AbstractChestedHorse) iterator1.next(); - // CraftBukkit start - } while (!entityhorsechestedabstract.isTamed()); -- ItemStack itemstack1 = stack.split(1); -+ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below - ServerLevel world = pointer.level(); - org.bukkit.block.Block block = CraftBlock.at(world, pointer.pos()); - CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); -@@ -281,10 +285,13 @@ public interface DispenseItemBehavior { - } - - if (event.isCancelled()) { -+ // stack.grow(1); // Paper - shrink below (this was actually missing and should be here, added it commented out to be consistent) - return stack; - } - -+ boolean shrink = true; // Paper - if (!event.getItem().equals(craftItem)) { -+ shrink = false; // Paper - shrink below - // Chain to handler for new item - ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); - DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior -@@ -296,7 +303,7 @@ public interface DispenseItemBehavior { - entityhorsechestedabstract.getSlot(499).set(CraftItemStack.asNMSCopy(event.getItem())); - // CraftBukkit end - -- // itemstack.shrink(1); // CraftBukkit - handled above -+ if (shrink) stack.shrink(1); // Paper - actually handle here - this.setSuccess(true); - return stack; - } -@@ -344,7 +351,7 @@ public interface DispenseItemBehavior { - if (willEmptyContentsSolidBucketItem || willEmptyBucketItem) { - // Paper end - correctly check if the bucket place will succeed - org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos()); -- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); -+ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event - - BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(x, y, z)); - if (!DispenserBlock.eventFired) { -@@ -409,7 +416,7 @@ public interface DispenseItemBehavior { - - // CraftBukkit start - org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos()); -- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); -+ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event - - BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); - if (!DispenserBlock.eventFired) { -@@ -447,7 +454,7 @@ public interface DispenseItemBehavior { - - // CraftBukkit start - org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos()); -- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); -+ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items - - BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { -@@ -509,7 +516,7 @@ public interface DispenseItemBehavior { - BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING)); - // CraftBukkit start - org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos()); -- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); -+ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event - - BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { -@@ -576,7 +583,7 @@ public interface DispenseItemBehavior { - // CraftBukkit start - // EntityTNTPrimed entitytntprimed = new EntityTNTPrimed(worldserver, (double) blockposition.getX() + 0.5D, (double) blockposition.getY(), (double) blockposition.getZ() + 0.5D, (EntityLiving) null); - -- ItemStack itemstack1 = stack.split(1); -+ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink at end and single item in event - org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos()); - CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); - -@@ -586,12 +593,13 @@ public interface DispenseItemBehavior { - } - - if (event.isCancelled()) { -- stack.grow(1); -+ // stack.grow(1); // Paper - shrink below - return stack; - } - -+ boolean shrink = true; // Paper - if (!event.getItem().equals(craftItem)) { -- stack.grow(1); -+ shrink = false; // Paper - shrink below - // Chain to handler for new item - ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); - DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior -@@ -607,7 +615,7 @@ public interface DispenseItemBehavior { - worldserver.addFreshEntity(entitytntprimed); - worldserver.playSound((Player) null, entitytntprimed.getX(), entitytntprimed.getY(), entitytntprimed.getZ(), SoundEvents.TNT_PRIMED, SoundSource.BLOCKS, 1.0F, 1.0F); - worldserver.gameEvent((Entity) null, (Holder) GameEvent.ENTITY_PLACE, blockposition); -- // itemstack.shrink(1); // CraftBukkit - handled above -+ if (shrink) stack.shrink(1); // Paper - actually handle here - return stack; - } - }); -@@ -620,7 +628,7 @@ public interface DispenseItemBehavior { - - // CraftBukkit start - org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos()); -- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); -+ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event - - BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); - if (!DispenserBlock.eventFired) { -@@ -669,7 +677,7 @@ public interface DispenseItemBehavior { - - // CraftBukkit start - org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos()); -- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); -+ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event - - BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); - if (!DispenserBlock.eventFired) { -@@ -731,7 +739,7 @@ public interface DispenseItemBehavior { - - // CraftBukkit start - org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos()); -- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); -+ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - only single item in event - - BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); - if (!DispenserBlock.eventFired) { -@@ -813,7 +821,7 @@ public interface DispenseItemBehavior { - ItemStack itemstack1 = stack; - ServerLevel world = pointer.level(); - org.bukkit.block.Block block = CraftBlock.at(world, pointer.pos()); -- CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); -+ CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); // Paper - ignore stack size on damageable items - - BlockDispenseEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) list.get(0).getBukkitEntity()); - if (!DispenserBlock.eventFired) { -diff --git a/src/main/java/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java -index a43ea83dbbd5946096cdde31af766674bda6c3be..bf8c511739265c6a9cd277752e844481598f8966 100644 ---- a/src/main/java/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java -+++ b/src/main/java/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java -@@ -42,7 +42,7 @@ public class EquipmentDispenseItemBehavior extends DefaultDispenseItemBehavior { - } else { - LivingEntity entityliving = (LivingEntity) list.getFirst(); - EquipmentSlot enumitemslot = entityliving.getEquipmentSlotForItem(stack); -- ItemStack itemstack1 = stack.split(1); -+ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event - - // CraftBukkit start - Level world = pointer.level(); -@@ -55,12 +55,13 @@ public class EquipmentDispenseItemBehavior extends DefaultDispenseItemBehavior { - } - - if (event.isCancelled()) { -- stack.grow(1); -+ // stack.grow(1); // Paper - shrink below - return false; - } - -+ boolean shrink = true; // Paper - if (!event.getItem().equals(craftItem)) { -- stack.grow(1); -+ shrink = false; // Paper - shrink below - // Chain to handler for new item - ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); - DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior -@@ -79,6 +80,7 @@ public class EquipmentDispenseItemBehavior extends DefaultDispenseItemBehavior { - entityinsentient.setPersistenceRequired(); - } - -+ if (shrink) stack.shrink(1); // Paper - shrink here - return true; - } - } -diff --git a/src/main/java/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java -index aae9ec8f3bd39685b37251bef3f9ac846d65c192..3588896b7413be73ade6b3f8fd111d02c48ec550 100644 ---- a/src/main/java/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java -+++ b/src/main/java/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java -@@ -69,7 +69,7 @@ public class MinecartDispenseItemBehavior extends DefaultDispenseItemBehavior { - Vec3 vec3d1 = new Vec3(d0, d1 + d3, d2); - // CraftBukkit start - // EntityMinecartAbstract entityminecartabstract = EntityMinecartAbstract.createMinecart(worldserver, vec3d1.x, vec3d1.y, vec3d1.z, this.entityType, EntitySpawnReason.DISPENSER, itemstack, (EntityHuman) null); -- ItemStack itemstack1 = stack.split(1); -+ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event - org.bukkit.block.Block block2 = CraftBlock.at(worldserver, pointer.pos()); - CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); - -@@ -79,12 +79,13 @@ public class MinecartDispenseItemBehavior extends DefaultDispenseItemBehavior { - } - - if (event.isCancelled()) { -- stack.grow(1); -+ // stack.grow(1); // Paper - shrink below - return stack; - } - -+ boolean shrink = true; // Paper - if (!event.getItem().equals(craftItem)) { -- stack.grow(1); -+ shrink = false; // Paper - shrink below - // Chain to handler for new item - ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); - DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior -@@ -98,8 +99,7 @@ public class MinecartDispenseItemBehavior extends DefaultDispenseItemBehavior { - AbstractMinecart entityminecartabstract = AbstractMinecart.createMinecart(worldserver, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), this.entityType, EntitySpawnReason.DISPENSER, itemstack1, (Player) null); - - if (entityminecartabstract != null) { -- if (!worldserver.addFreshEntity(entityminecartabstract)) stack.grow(1); -- // itemstack.shrink(1); // CraftBukkit - handled during event processing -+ if (worldserver.addFreshEntity(entityminecartabstract) && shrink) stack.shrink(1); // Paper - if entity add was successful and supposed to shrink - // CraftBukkit end - } - -diff --git a/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java -index 281439e430fb8e587549da783bdd93432f8f957f..54c72cf472e06e214eb61bd8615a0bb27690c807 100644 ---- a/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java -+++ b/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java -@@ -38,7 +38,7 @@ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior { - - // CraftBukkit start - // IProjectile.spawnProjectileUsingShoot(this.projectileItem.asProjectile(worldserver, iposition, itemstack, enumdirection), worldserver, itemstack, (double) enumdirection.getStepX(), (double) enumdirection.getStepY(), (double) enumdirection.getStepZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty()); // CraftBukkit - call when finish the BlockDispenseEvent -- ItemStack itemstack1 = stack.split(1); -+ ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event - org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos()); - CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); - -@@ -48,12 +48,13 @@ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior { - } - - if (event.isCancelled()) { -- stack.grow(1); -+ // stack.grow(1); // Paper - shrink below - return stack; - } - -+ boolean shrink = true; // Paper - if (!event.getItem().equals(craftItem)) { -- stack.grow(1); -+ shrink = false; // Paper - shrink below - // Chain to handler for new item - ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); - DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior -@@ -68,7 +69,7 @@ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior { - Projectile iprojectile = Projectile.spawnProjectileUsingShoot(this.projectileItem.asProjectile(worldserver, iposition, CraftItemStack.unwrap(event.getItem()), enumdirection), worldserver, itemstack1, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty()); // Paper - track changed items in the dispense event; unwrap is safe here because all uses of the stack make their own copies - iprojectile.projectileSource = new org.bukkit.craftbukkit.projectiles.CraftBlockProjectileSource(pointer.blockEntity()); - } -- // itemstack.shrink(1); // CraftBukkit - Handled during event processing -+ if (shrink) stack.shrink(1); // Paper - actually handle here - // CraftBukkit end - return stack; - } -diff --git a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java -index afad4fa3ca1a3186c4569ea073f776dac16817e1..65ed3d77a51b8299517e0c165403b0c5ac413475 100644 ---- a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java -+++ b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java -@@ -38,7 +38,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { - ServerLevel worldserver = pointer.level(); - // CraftBukkit start - org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos()); -- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); -+ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items - - BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { -diff --git a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java -index 16b435216dc7c6a3f8c1c0f9e2323e6afb3a6cb9..8f9fde5489c0e1d0a91203536caddec5a9c96f6c 100644 ---- a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java -+++ b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java -@@ -34,7 +34,7 @@ public class ShulkerBoxDispenseBehavior extends OptionalDispenseItemBehavior { - - // CraftBukkit start - org.bukkit.block.Block bukkitBlock = CraftBlock.at(pointer.level(), pointer.pos()); -- CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); -+ CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event - - BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); - if (!DispenserBlock.eventFired) { diff --git a/patches/unapplied/server/1059-Correct-update-cursor.patch b/patches/unapplied/server/1059-Correct-update-cursor.patch deleted file mode 100644 index 5a1e3ce282..0000000000 --- a/patches/unapplied/server/1059-Correct-update-cursor.patch +++ /dev/null @@ -1,42 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Bjarne Koll -Date: Fri, 1 Nov 2024 14:58:57 +0100 -Subject: [PATCH] Correct update cursor - -Spigot uses a no longer valid ClientboundContainerSetSlotPacket with the -slot -1, which would update the carried stack in versions <=1.21.1 but -now leads to an IOOB. -1.21.2 instead introduced the ClientboundSetCursorItemPacket, which this -patch uses instead. - -diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index d7ac001d53a083e9881f2320eb7fd5dcbd20416e..cd1b6b539a62fa5237d6dab2d1c09a2e631d9941 100644 ---- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -3287,7 +3287,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - case PLACE_SOME: - case PLACE_ONE: - case SWAP_WITH_CURSOR: -- this.player.connection.send(new ClientboundContainerSetSlotPacket(-1, -1, this.player.inventoryMenu.incrementStateId(), this.player.containerMenu.getCarried())); -+ this.player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCursorItemPacket(this.player.containerMenu.getCarried().copy())); // Paper - correctly set cursor - this.player.connection.send(new ClientboundContainerSetSlotPacket(this.player.containerMenu.containerId, this.player.inventoryMenu.incrementStateId(), packet.getSlotNum(), this.player.containerMenu.getSlot(packet.getSlotNum()).getItem())); - break; - // Modified clicked only -@@ -3299,7 +3299,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - case DROP_ALL_CURSOR: - case DROP_ONE_CURSOR: - case CLONE_STACK: -- this.player.connection.send(new ClientboundContainerSetSlotPacket(-1, -1, this.player.inventoryMenu.incrementStateId(), this.player.containerMenu.getCarried())); -+ this.player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCursorItemPacket(this.player.containerMenu.getCarried().copy())); // Paper - correctly set cursor - break; - // Nothing - case NOTHING: -@@ -3497,7 +3497,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - // Reset the slot - if (packet.slotNum() >= 0) { - this.player.connection.send(new ClientboundContainerSetSlotPacket(this.player.inventoryMenu.containerId, this.player.inventoryMenu.incrementStateId(), packet.slotNum(), this.player.inventoryMenu.getSlot(packet.slotNum()).getItem())); -- this.player.connection.send(new ClientboundContainerSetSlotPacket(-1, this.player.inventoryMenu.incrementStateId(), -1, ItemStack.EMPTY)); -+ this.player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCursorItemPacket(ItemStack.EMPTY.copy())); // Paper - correctly set cursor - } - return; - } diff --git a/patches/unapplied/server/1060-Call-CraftPlayer-onEntityRemove-for-all-online-playe.patch b/patches/unapplied/server/1060-Call-CraftPlayer-onEntityRemove-for-all-online-playe.patch deleted file mode 100644 index 0e72daf994..0000000000 --- a/patches/unapplied/server/1060-Call-CraftPlayer-onEntityRemove-for-all-online-playe.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Gero -Date: Sat, 9 Nov 2024 22:27:58 +0100 -Subject: [PATCH] Call CraftPlayer#onEntityRemove for all online players - - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 7db77a36701e766c148e91d8313838d307855d8a..657d8af7cc104962ee46ad1a7dc88b13c24262db 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -2794,7 +2794,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - // CraftBukkit start - entity.valid = false; - if (!(entity instanceof ServerPlayer)) { -- for (ServerPlayer player : ServerLevel.this.players) { -+ for (ServerPlayer player : ServerLevel.this.server.getPlayerList().players) { // Paper - call onEntityRemove for all online players - player.getBukkitEntity().onEntityRemove(entity); - } - } diff --git a/patches/unapplied/server/1062-Eigencraft-redstone-implementation.patch b/patches/unapplied/server/1062-Eigencraft-redstone-implementation.patch deleted file mode 100644 index 85700695b3..0000000000 --- a/patches/unapplied/server/1062-Eigencraft-redstone-implementation.patch +++ /dev/null @@ -1,1099 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: theosib -Date: Thu, 27 Sep 2018 01:43:35 -0600 -Subject: [PATCH] Eigencraft redstone implementation - -Author: theosib - -Original license: MIT - -This patch implements theosib's redstone algorithms to completely overhaul the way redstone works. -The new algorithms should be many times faster than current vanilla ones. -From the original author's comments, it looks like it shouldn't interfere with any redstone save for very extreme edge-cases. - -Surprisingly, not a lot was touched aside from a few obfuscation helpers and BlockRedstoneWire. -A lot of this code is self-contained in a helper class. - -Aside from making the obvious class/function renames and obfhelpers I didn't need to modify much. -Just added Bukkit's event system and took a few liberties with dead code and comment misspellings. - -Feature patch - -== AT == -public net.minecraft.world.level.block.RedStoneWireBlock shouldSignal -public net.minecraft.world.level.block.RedStoneWireBlock canSurvive(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/LevelReader;Lnet/minecraft/core/BlockPos;)Z - -Co-authored-by: egg82 - -diff --git a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e7d510af3e415064fd483f0220d5f6a4cd0b9f63 ---- /dev/null -+++ b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java -@@ -0,0 +1,961 @@ -+package com.destroystokyo.paper.util; -+ -+import java.util.List; -+import java.util.Map; -+import java.util.concurrent.ThreadLocalRandom; -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Direction; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.item.Items; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.RedStoneWireBlock; -+import net.minecraft.world.level.block.state.BlockState; -+import org.bukkit.craftbukkit.block.CraftBlock; -+import org.bukkit.event.block.BlockRedstoneEvent; -+ -+import com.google.common.collect.Lists; -+import com.google.common.collect.Maps; -+ -+/** -+ * Used for the faster redstone algorithm. -+ * Original author: theosib -+ * Original license: MIT -+ * -+ * Ported to Paper and updated to 1.13 by egg82 -+ */ -+public class RedstoneWireTurbo { -+ /* -+ * This is Helper class for BlockRedstoneWire. It implements a minimally-invasive -+ * bolt-on accelerator that performs a breadth-first search through redstone wire blocks -+ * in order to more efficiently and deterministically compute new redstone wire power levels -+ * and determine the order in which other blocks should be updated. -+ * -+ * Features: -+ * - Changes to BlockRedstoneWire are very limited, no other classes are affected, and the -+ * choice between old and new redstone wire update algorithms is switchable on-line. -+ * - The vanilla implementation relied on World.notifyNeighborsOfStateChange for redstone -+ * wire blocks to communicate power level changes to each other, generating 36 block -+ * updates per call. This improved implementation propagates power level changes directly -+ * between redstone wire blocks. Redstone wire power levels are therefore computed more quickly, -+ * and block updates are sent only to non-redstone blocks, many of which may perform an -+ * action when informed of a change in redstone power level. (Note: Block updates are not -+ * the same as state changes to redstone wire. Wire block states are updated as soon -+ * as they are computed.) -+ * - Of the 36 block updates generated by a call to World.notifyNeighborsOfStateChange, -+ * 12 of them are obviously redundant (e.g. the west neighbor of the east neighbor). -+ * These are eliminated. -+ * - Updates to redstone wire and other connected blocks are propagated in a breath-first -+ * manner, radiating out from the initial trigger (a block update to a redstone wire -+ * from something other than redstone wire). -+ * - Updates are scheduled both deterministically and in an intuitive order, addressing bug -+ * MC-11193. -+ * - All redstone behavior that used to be locational now works the same in all locations. -+ * - All behaviors of redstone wire that used to be orientational now work the same in all -+ * orientations, as long as orientation can be determined; random otherwise. Some other -+ * redstone components still update directionally (e.g. switches), and this code can't -+ * compensate for that. -+ * - Information that is otherwise computed over and over again or which is expensive to -+ * to compute is cached for faster lookup. This includes coordinates of block position -+ * neighbors and block states that won't change behind our backs during the execution of -+ * this search algorithm. -+ * - Redundant block updates (both to redstone wire and to other blocks) are heavily -+ * consolidated. For worst-case scenarios (depowering of redstone wire) this results -+ * in a reduction of block updates by as much as 95% (factor of 1/21). Due to overheads, -+ * empirical testing shows a speedup better than 10x. This addresses bug MC-81098. -+ * -+ * Extensive testing has been performed to ensure that existing redstone contraptions still -+ * behave as expected. Results of early testing that identified undesirable behavior changes -+ * were addressed. Additionally, real-time performance testing revealed compute inefficiencies -+ * With earlier implementations of this accelerator. Some compatibility adjustments and -+ * performance optimizations resulted in harmless increases in block updates above the -+ * theoretical minimum. -+ * -+ * Only a single redstone machine was found to break: An instant dropper line hack that -+ * relies on powered rails and quasi-connectivity but doesn't work in all directions. The -+ * replacement is to lay redstone wire directly on top of the dropper line, which now works -+ * reliably in any direction. -+ * -+ * There are numerous other optimization that can be made, but those will be provided later in -+ * separate updates. This version is designed to be minimalistic. -+ * -+ * Many thanks to the following individuals for their help in testing this functionality: -+ * - pokechu22, _MethodZz_, WARBEN, NarcolepticFrog, CommandHelper (nessie), ilmango, -+ * OreoLamp, Xcom6000, tryashtar, RedCMD, Smokey95Dog, EDDxample, Rays Works, -+ * Nodnam, BlockyPlays, Grumm, NeunEinser, HelVince. -+ */ -+ -+ /* Reference to BlockRedstoneWire object, which uses this accelerator */ -+ private final RedStoneWireBlock wire; -+ -+ /* -+ * Implementation: -+ * -+ * RedstoneWire Blocks are updated in concentric rings or "layers" radiating out from the -+ * initial block update that came from a call to BlockRedstoneWire.neighborChanged(). -+ * All nodes put in Layer N are those with Manhattan distance N from the trigger -+ * position, reachable through connected redstone wire blocks. -+ * -+ * Layer 0 represents the trigger block position that was input to neighborChanged. -+ * Layer 1 contains the immediate neighbors of that position. -+ * Layer N contains the neighbors of blocks in layer N-1, not including -+ * those in previous layers. -+ * -+ * Layers enforce an update order that is a function of Manhattan distance -+ * from the initial coordinates input to neighborChanged. The same -+ * coordinates may appear in multiple layers, but redundant updates are minimized. -+ * Block updates are sent layer-by-layer. If multiple of a block's neighbors experience -+ * redstone wire changes before its layer is processed, then those updates will be merged. -+ * If a block's update has been sent, but its neighboring redstone changes -+ * after that, then another update will be sent. This preserves compatibility with -+ * machines that rely on zero-tick behavior, except that the new functionality is non- -+ * locational. -+ * -+ * Within each layer, updates are ordered left-to-right relative to the direction of -+ * information flow. This makes the implementation non-orientational. Only when -+ * this direction is ambiguous is randomness applied (intentionally). -+ */ -+ private List updateQueue0 = Lists.newArrayList(); -+ private List updateQueue1 = Lists.newArrayList(); -+ private List updateQueue2 = Lists.newArrayList(); -+ -+ public RedstoneWireTurbo(RedStoneWireBlock wire) { -+ this.wire = wire; -+ } -+ -+ /* -+ * Compute neighbors of a block. When a redstone wire value changes, previously it called -+ * World.notifyNeighborsOfStateChange. That lists immediately neighboring blocks in -+ * west, east, down, up, north, south order. For each of those neighbors, their own -+ * neighbors are updated in the same order. This generates 36 updates, but 12 of them are -+ * redundant; for instance the west neighbor of a block's east neighbor. -+ * -+ * Note that this ordering is only used to create the initial list of neighbors. Once -+ * the direction of signal flow is identified, the ordering of updates is completely -+ * reorganized. -+ */ -+ public static BlockPos[] computeAllNeighbors(final BlockPos pos) { -+ final int x = pos.getX(); -+ final int y = pos.getY(); -+ final int z = pos.getZ(); -+ final BlockPos[] n = new BlockPos[24]; -+ -+ // Immediate neighbors, in the same order as -+ // World.notifyNeighborsOfStateChange, etc.: -+ // west, east, down, up, north, south -+ n[0] = new BlockPos(x - 1, y, z); -+ n[1] = new BlockPos(x + 1, y, z); -+ n[2] = new BlockPos(x, y - 1, z); -+ n[3] = new BlockPos(x, y + 1, z); -+ n[4] = new BlockPos(x, y, z - 1); -+ n[5] = new BlockPos(x, y, z + 1); -+ -+ // Neighbors of neighbors, in the same order, -+ // except that duplicates are not included -+ n[6] = new BlockPos(x - 2, y, z); -+ n[7] = new BlockPos(x - 1, y - 1, z); -+ n[8] = new BlockPos(x - 1, y + 1, z); -+ n[9] = new BlockPos(x - 1, y, z - 1); -+ n[10] = new BlockPos(x - 1, y, z + 1); -+ n[11] = new BlockPos(x + 2, y, z); -+ n[12] = new BlockPos(x + 1, y - 1, z); -+ n[13] = new BlockPos(x + 1, y + 1, z); -+ n[14] = new BlockPos(x + 1, y, z - 1); -+ n[15] = new BlockPos(x + 1, y, z + 1); -+ n[16] = new BlockPos(x, y - 2, z); -+ n[17] = new BlockPos(x, y - 1, z - 1); -+ n[18] = new BlockPos(x, y - 1, z + 1); -+ n[19] = new BlockPos(x, y + 2, z); -+ n[20] = new BlockPos(x, y + 1, z - 1); -+ n[21] = new BlockPos(x, y + 1, z + 1); -+ n[22] = new BlockPos(x, y, z - 2); -+ n[23] = new BlockPos(x, y, z + 2); -+ return n; -+ } -+ -+ /* -+ * We only want redstone wires to update redstone wires that are -+ * immediately adjacent. Some more distant updates can result -+ * in cross-talk that (a) wastes time and (b) can make the update -+ * order unintuitive. Therefore (relative to the neighbor order -+ * computed by computeAllNeighbors), updates are not scheduled -+ * for redstone wire in those non-connecting positions. On the -+ * other hand, updates will always be sent to *other* types of blocks -+ * in any of the 24 neighboring positions. -+ */ -+ private static final boolean[] update_redstone = { -+ true, true, false, false, true, true, // 0 to 5 -+ false, true, true, false, false, false, // 6 to 11 -+ true, true, false, false, false, true, // 12 to 17 -+ true, false, true, true, false, false // 18 to 23 -+ }; -+ -+ // Internal numbering for cardinal directions -+ private static final int North = 0; -+ private static final int East = 1; -+ private static final int South = 2; -+ private static final int West = 3; -+ -+ /* -+ * These lookup tables completely remap neighbor positions into a left-to-right -+ * ordering, based on the cardinal direction that is determined to be forward. -+ * See below for more explanation. -+ */ -+ private static final int[] forward_is_north = {2, 3, 16, 19, 0, 4, 1, 5, 7, 8, 17, 20, 12, 13, 18, 21, 6, 9, 22, 14, 11, 10, 23, 15}; -+ private static final int[] forward_is_east = {2, 3, 16, 19, 4, 1, 5, 0, 17, 20, 12, 13, 18, 21, 7, 8, 22, 14, 11, 15, 23, 9, 6, 10}; -+ private static final int[] forward_is_south = {2, 3, 16, 19, 1, 5, 0, 4, 12, 13, 18, 21, 7, 8, 17, 20, 11, 15, 23, 10, 6, 14, 22, 9}; -+ private static final int[] forward_is_west = {2, 3, 16, 19, 5, 0, 4, 1, 18, 21, 7, 8, 17, 20, 12, 13, 23, 10, 6, 9, 22, 15, 11, 14}; -+ -+ /* For any orientation, we end up with the update order defined below. This order is relative to any redstone wire block -+ * that is itself having an update computed, and this center position is marked with C. -+ * - The update position marked 0 is computed first, and the one marked 23 is last. -+ * - Forward is determined by the local direction of information flow into position C from prior updates. -+ * - The first updates are scheduled for the four positions below and above C. -+ * - Then updates are scheduled for the four horizontal neighbors of C, followed by the positions below and above those neighbors. -+ * - Finally, updates are scheduled for the remaining positions with Manhattan distance 2 from C (at the same Y coordinate). -+ * - For a given horizontal distance from C, updates are scheduled starting from directly left and stepping clockwise to directly -+ * right. The remaining positions behind C are scheduled counterclockwise so as to maintain the left-to-right ordering. -+ * - If C is in layer N of the update schedule, then all 24 positions may be scheduled for layer N+1. For redstone wire, no -+ * updates are scheduled for positions that cannot directly connect. Additionally, the four positions above and below C -+ * are ALSO scheduled for layer N+2. -+ * - This update order was selected after experimenting with a number of alternative schedules, based on its compatibility -+ * with existing redstone designs and behaviors that were considered to be intuitive by various testers. WARBEN in particular -+ * made some of the most challenging test cases, but the 3-tick clocks (made by RedCMD) were also challenging to fix, -+ * along with the rail-based instant dropper line built by ilmango. Numerous others made test cases as well, including -+ * NarcolepticFrog, nessie, and Pokechu22. -+ * -+ * - The forward direction is determined locally. So when there are branches in the redstone wire, the left one will get updated -+ * before the right one. Each branch can have its own relative forward direction, resulting in the left side of a left branch -+ * having priority over the right branch of a left branch, which has priority over the left branch of a right branch, followed -+ * by the right branch of a right branch. And so forth. Since redstone power reduces to zero after a path distance of 15, -+ * that imposes a practical limit on the branching. Note that the branching is not tracked explicitly -- relative forward -+ * directions dictate relative sort order, which maintains the proper global ordering. This also makes it unnecessary to be -+ * concerned about branches meeting up with each other. -+ * -+ * ^ -+ * | -+ * Forward -+ * <-- Left Right --> -+ * -+ * 18 -+ * 10 17 5 19 11 -+ * 2 8 0 12 16 4 C 6 20 9 1 13 3 -+ * 14 21 7 23 15 -+ * Further 22 Further -+ * Down Down Up Up -+ * -+ * Backward -+ * | -+ * V -+ */ -+ -+ // This allows the above remapping tables to be looked up by cardial direction index -+ private static final int[][] reordering = { forward_is_north, forward_is_east, forward_is_south, forward_is_west }; -+ -+ /* -+ * Input: Array of UpdateNode objects in an order corresponding to the positions -+ * computed by computeAllNeighbors above. -+ * Output: Array of UpdateNode objects oriented using the above remapping tables -+ * corresponding to the identified heading (direction of information flow). -+ */ -+ private static void orientNeighbors(final UpdateNode[] src, final UpdateNode[] dst, final int heading) { -+ final int[] re = reordering[heading]; -+ for (int i = 0; i < 24; i++) { -+ dst[i] = src[re[i]]; -+ } -+ } -+ -+ /* -+ * Structure to keep track of redstone wire blocks and -+ * neighbors that will receive updates. -+ */ -+ private static class UpdateNode { -+ public static enum Type { -+ UNKNOWN, REDSTONE, OTHER -+ } -+ -+ BlockState currentState; // Keep track of redstone wire value -+ UpdateNode[] neighbor_nodes; // References to neighbors (directed graph edges) -+ BlockPos self; // UpdateNode's own position -+ BlockPos parent; // Which block pos spawned/updated this node -+ Type type = Type.UNKNOWN; // unknown, redstone wire, other type of block -+ int layer; // Highest layer this node is scheduled in -+ boolean visited; // To keep track of information flow direction, visited restone wire is marked -+ int xbias, zbias; // Remembers directionality of ancestor nodes; helps eliminate directional ambiguities. -+ } -+ -+ /* -+ * Keep track of all block positions discovered during search and their current states. -+ * We want to remember one entry for each position. -+ */ -+ private final Map nodeCache = Maps.newHashMap(); -+ -+ /* -+ * For a newly created UpdateNode object, determine what type of block it is. -+ */ -+ private void identifyNode(final Level worldIn, final UpdateNode upd1) { -+ final BlockPos pos = upd1.self; -+ final BlockState oldState = worldIn.getBlockState(pos); -+ upd1.currentState = oldState; -+ -+ // Some neighbors of redstone wire are other kinds of blocks. -+ // These need to receive block updates to inform them that -+ // redstone wire values have changed. -+ final Block block = oldState.getBlock(); -+ if (block != wire) { -+ // Mark this block as not redstone wire and therefore -+ // requiring updates -+ upd1.type = UpdateNode.Type.OTHER; -+ -+ // Non-redstone blocks may propagate updates, but those updates -+ // are not handled by this accelerator. Therefore, we do not -+ // expand this position's neighbors. -+ return; -+ } -+ -+ // One job of BlockRedstoneWire.neighborChanged is to convert -+ // redstone wires to items if the block beneath was removed. -+ // With this accelerator, BlockRedstoneWire.neighborChanged -+ // is only typically called for a single wire block, while -+ // others are processed internally by the breadth first search -+ // algorithm. To preserve this game behavior, this check must -+ // be replicated here. -+ if (!wire.canSurvive(null, worldIn, pos)) { -+ // Pop off the redstone dust -+ Block.popResource(worldIn, pos, new ItemStack(Items.REDSTONE)); // TODO -+ worldIn.removeBlock(pos, false); -+ -+ // Mark this position as not being redstone wire -+ upd1.type = UpdateNode.Type.OTHER; -+ -+ // Note: Sending updates to air blocks leads to an empty method. -+ // Testing shows this to be faster than explicitly avoiding updates to -+ // air blocks. -+ return; -+ } -+ -+ // If the above conditions fail, then this is a redstone wire block. -+ upd1.type = UpdateNode.Type.REDSTONE; -+ } -+ -+ /* -+ * Given which redstone wire blocks have been visited and not visited -+ * around the position currently being updated, compute the cardinal -+ * direction that is "forward." -+ * -+ * rx is the forward direction along the West/East axis -+ * rz is the forward direction along the North/South axis -+ */ -+ static private int computeHeading(final int rx, final int rz) { -+ // rx and rz can only take on values -1, 0, and 1, so we can -+ // compute a code number that allows us to use a single switch -+ // to determine the heading. -+ final int code = (rx + 1) + 3 * (rz + 1); -+ switch (code) { -+ case 0: { -+ // Both rx and rz are -1 (northwest) -+ // Randomly choose one to be forward. -+ final int j = ThreadLocalRandom.current().nextInt(0, 1); -+ return (j == 0) ? North : West; -+ } -+ case 1: { -+ // rx=0, rz=-1 -+ // Definitively North -+ return North; -+ } -+ case 2: { -+ // rx=1, rz=-1 (northeast) -+ // Choose randomly between north and east -+ final int j = ThreadLocalRandom.current().nextInt(0, 1); -+ return (j == 0) ? North : East; -+ } -+ case 3: { -+ // rx=-1, rz=0 -+ // Definitively West -+ return West; -+ } -+ case 4: { -+ // rx=0, rz=0 -+ // Heading is completely ambiguous. Choose -+ // randomly among the four cardinal directions. -+ return ThreadLocalRandom.current().nextInt(0, 4); -+ } -+ case 5: { -+ // rx=1, rz=0 -+ // Definitively East -+ return East; -+ } -+ case 6: { -+ // rx=-1, rz=1 (southwest) -+ // Choose randomly between south and west -+ final int j = ThreadLocalRandom.current().nextInt(0, 1); -+ return (j == 0) ? South : West; -+ } -+ case 7: { -+ // rx=0, rz=1 -+ // Definitively South -+ return South; -+ } -+ case 8: { -+ // rx=1, rz=1 (southeast) -+ // Choose randomly between south and east -+ final int j = ThreadLocalRandom.current().nextInt(0, 1); -+ return (j == 0) ? South : East; -+ } -+ } -+ -+ // We should never get here -+ return ThreadLocalRandom.current().nextInt(0, 4); -+ } -+ -+ // Select whether to use updateSurroundingRedstone from BlockRedstoneWire (old) -+ // or this helper class (new) -+ private static final boolean old_current_change = false; -+ -+ /* -+ * Process a node whose neighboring redstone wire has experienced value changes. -+ */ -+ private void updateNode(final Level worldIn, final UpdateNode upd1, final int layer) { -+ final BlockPos pos = upd1.self; -+ -+ // Mark this redstone wire as having been visited so that it can be used -+ // to calculate direction of information flow. -+ upd1.visited = true; -+ -+ // Look up the last known state. -+ // Due to the way other redstone components are updated, we do not -+ // have to worry about a state changing behind our backs. The rare -+ // exception is handled by scheduleReentrantNeighborChanged. -+ final BlockState oldState = upd1.currentState; -+ -+ // Ask the wire block to compute its power level from its neighbors. -+ // This will also update the wire's power level and return a new -+ // state if it has changed. When a wire power level is changed, -+ // calculateCurrentChanges will immediately update the block state in the world -+ // and return the same value here to be cached in the corresponding -+ // UpdateNode object. -+ BlockState newState; -+ if (old_current_change) { -+ newState = wire.calculateCurrentChanges(worldIn, pos, oldState); -+ } else { -+ // Looking up block state is slow. This accelerator includes a version of -+ // calculateCurrentChanges that uses cahed wire values for a -+ // significant performance boost. -+ newState = this.calculateCurrentChanges(worldIn, upd1); -+ } -+ -+ // Only inform neighbors if the state has changed -+ if (newState != oldState) { -+ // Store the new state -+ upd1.currentState = newState; -+ -+ // Inform neighbors of the change -+ propagateChanges(worldIn, upd1, layer); -+ } -+ } -+ -+ /* -+ * This identifies the neighboring positions of a new UpdateNode object, -+ * determines their types, and links those to into the graph. Then based on -+ * what nodes in the redstone wire graph have been visited, the neighbors -+ * are reordered left-to-right relative to the direction of information flow. -+ */ -+ private void findNeighbors(final Level worldIn, final UpdateNode upd1) { -+ final BlockPos pos = upd1.self; -+ -+ // Get the list of neighbor coordinates -+ final BlockPos[] neighbors = computeAllNeighbors(pos); -+ -+ // Temporary array of neighbors in cardinal ordering -+ final UpdateNode[] neighbor_nodes = new UpdateNode[24]; -+ -+ // Target array of neighbors sorted left-to-right -+ upd1.neighbor_nodes = new UpdateNode[24]; -+ -+ for (int i=0; i<24; i++) { -+ // Look up each neighbor in the node cache -+ final BlockPos pos2 = neighbors[i]; -+ UpdateNode upd2 = nodeCache.get(pos2); -+ if (upd2 == null) { -+ // If this is a previously unreached position, create -+ // a new update node, add it to the cache, and identify what it is. -+ upd2 = new UpdateNode(); -+ upd2.self = pos2; -+ upd2.parent = pos; -+ nodeCache.put(pos2, upd2); -+ identifyNode(worldIn, upd2); -+ } -+ -+ // For non-redstone blocks, any of the 24 neighboring positions -+ // should receive a block update. However, some block coordinates -+ // may contain a redstone wire that does not directly connect to the -+ // one being expanded. To avoid redundant calculations and confusing -+ // cross-talk, those neighboring positions are not included. -+ if (update_redstone[i] || upd2.type != UpdateNode.Type.REDSTONE) { -+ neighbor_nodes[i] = upd2; -+ } -+ } -+ -+ // Determine the directions from which the redstone signal may have come from. This -+ // checks for redstone wire at the same Y level and also Y+1 and Y-1, relative to the -+ // block being expanded. -+ final boolean fromWest = (neighbor_nodes[0].visited || neighbor_nodes[7].visited || neighbor_nodes[8].visited); -+ final boolean fromEast = (neighbor_nodes[1].visited || neighbor_nodes[12].visited || neighbor_nodes[13].visited); -+ final boolean fromNorth = (neighbor_nodes[4].visited || neighbor_nodes[17].visited || neighbor_nodes[20].visited); -+ final boolean fromSouth = (neighbor_nodes[5].visited || neighbor_nodes[18].visited || neighbor_nodes[21].visited); -+ -+ int cx = 0, cz = 0; -+ if (fromWest) cx += 1; -+ if (fromEast) cx -= 1; -+ if (fromNorth) cz += 1; -+ if (fromSouth) cz -= 1; -+ -+ int heading; -+ if (cx==0 && cz==0) { -+ // If there is no clear direction, try to inherit the heading from ancestor nodes. -+ heading = computeHeading(upd1.xbias, upd1.zbias); -+ -+ // Propagate that heading to descendant nodes. -+ for (int i=0; i<24; i++) { -+ final UpdateNode nn = neighbor_nodes[i]; -+ if (nn != null) { -+ nn.xbias = upd1.xbias; -+ nn.zbias = upd1.zbias; -+ } -+ } -+ } else { -+ if (cx != 0 && cz != 0) { -+ // If the heading is somewhat ambiguous, try to disambiguate based on -+ // ancestor nodes. -+ if (upd1.xbias != 0) cz = 0; -+ if (upd1.zbias != 0) cx = 0; -+ } -+ heading = computeHeading(cx, cz); -+ -+ // Propagate that heading to descendant nodes. -+ for (int i=0; i<24; i++) { -+ final UpdateNode nn = neighbor_nodes[i]; -+ if (nn != null) { -+ nn.xbias = cx; -+ nn.zbias = cz; -+ } -+ } -+ } -+ -+ // Reorder neighboring UpdateNode objects according to the forward direction -+ // determined above. -+ orientNeighbors(neighbor_nodes, upd1.neighbor_nodes, heading); -+ } -+ -+ /* -+ * For any redstone wire block in layer N, inform neighbors to recompute their states -+ * in layers N+1 and N+2; -+ */ -+ private void propagateChanges(final Level worldIn, final UpdateNode upd1, final int layer) { -+ if (upd1.neighbor_nodes == null) { -+ // If this node has not been expanded yet, find its neighbors -+ findNeighbors(worldIn, upd1); -+ } -+ -+ final BlockPos pos = upd1.self; -+ -+ // All neighbors may be scheduled for layer N+1 -+ final int layer1 = layer + 1; -+ -+ // If the node being updated (upd1) has already been expanded, then merely -+ // schedule updates to its neighbors. -+ for (int i = 0; i < 24; i++) { -+ final UpdateNode upd2 = upd1.neighbor_nodes[i]; -+ -+ // This test ensures that an UpdateNode is never scheduled to the same layer -+ // more than once. Also, skip non-connecting redstone wire blocks -+ if (upd2 != null && layer1 > upd2.layer) { -+ upd2.layer = layer1; -+ updateQueue1.add(upd2); -+ -+ // Keep track of which block updated this neighbor -+ upd2.parent = pos; -+ } -+ } -+ -+ // Nodes above and below are scheduled ALSO for layer N+2 -+ final int layer2 = layer + 2; -+ -+ // Repeat of the loop above, but only for the first four (above and below) neighbors -+ // and for layer N+2; -+ for (int i = 0; i < 4; i++) { -+ final UpdateNode upd2 = upd1.neighbor_nodes[i]; -+ if (upd2 != null && layer2 > upd2.layer) { -+ upd2.layer = layer2; -+ updateQueue2.add(upd2); -+ upd2.parent = pos; -+ } -+ } -+ } -+ -+ // The breadth-first search below will send block updates to blocks -+ // that are not redstone wire. If one of those updates results in -+ // a distant redstone wire getting an update, then this.neighborChanged -+ // will get called. This would be a reentrant call, and -+ // it is necessary to properly integrate those updates into the -+ // on-going search through redstone wire. Thus, we make the layer -+ // currently being processed visible at the object level. -+ -+ // The current layer being processed by the breadth-first search -+ private int currentWalkLayer = 0; -+ -+ private void shiftQueue() { -+ final List t = updateQueue0; -+ t.clear(); -+ updateQueue0 = updateQueue1; -+ updateQueue1 = updateQueue2; -+ updateQueue2 = t; -+ } -+ -+ /* -+ * Perform a breadth-first (layer by layer) traversal through redstone -+ * wire blocks, propagating value changes to neighbors in an order -+ * that is a function of distance from the initial call to -+ * this.neighborChanged. -+ */ -+ private void breadthFirstWalk(final Level worldIn) { -+ shiftQueue(); -+ currentWalkLayer = 1; -+ -+ // Loop over all layers -+ while (updateQueue0.size()>0 || updateQueue1.size()>0) { -+ // Get the set of blocks in this layer -+ final List thisLayer = updateQueue0; -+ -+ // Loop over all blocks in the layer. Recall that -+ // this is a List, preserving the insertion order of -+ // left-to-right based on direction of information flow. -+ for (UpdateNode upd : thisLayer) { -+ if (upd.type == UpdateNode.Type.REDSTONE) { -+ // If the node is is redstone wire, -+ // schedule updates to neighbors if its value -+ // has changed. -+ updateNode(worldIn, upd, currentWalkLayer); -+ } else { -+ // If this block is not redstone wire, send a block update. -+ // Redstone wire blocks get state updates, but they don't -+ // need block updates. Only non-redstone neighbors need updates. -+ -+ // World.neighborChanged is called from -+ // World.notifyNeighborsOfStateChange, and -+ // notifyNeighborsOfStateExcept. We don't use -+ // World.notifyNeighborsOfStateChange here, since we are -+ // already keeping track of all of the neighbor positions -+ // that need to be updated. All on its own, handling neighbors -+ // this way reduces block updates by 1/3 (24 instead of 36). -+// worldIn.neighborChanged(upd.self, wire, upd.parent); -+ -+ // [Space Walker] -+ // The neighbor update system got a significant overhaul in 1.19. -+ // Shape and block updates are now added to a stack before being -+ // processed. These changes make it so any neighbor updates emitted -+ // by this accelerator will not be processed until after the entire -+ // wire network has updated. This has a significant impact on the -+ // behavior and introduces Vanilla parity issues. -+ // To circumvent this issue we bypass the neighbor update stack and -+ // call BlockStateBase#neighborChanged directly. This change mostly -+ // restores old behavior, at the cost of bypassing the -+ // max-chained-neighbor-updates server property. -+ // The Orientation parameter is (for now) only used by redstone wire -+ // while these updates are dispatched to non-wires only, so we can -+ // pass null. -+ worldIn.getBlockState(upd.self).handleNeighborChanged(worldIn, upd.self, wire, null, false); -+ } -+ } -+ -+ // Move on to the next layer -+ shiftQueue(); -+ currentWalkLayer++; -+ } -+ -+ currentWalkLayer = 0; -+ } -+ -+ /* -+ * Normally, when Minecraft is computing redstone wire power changes, and a wire power level -+ * change sends a block update to a neighboring functional component (e.g. piston, repeater, etc.), -+ * those updates are queued. Only once all redstone wire updates are complete will any component -+ * action generate any further block updates to redstone wire. Instant repeater lines, for instance, -+ * will process all wire updates for one redstone line, after which the pistons will zero-tick, -+ * after which the next redstone line performs all of its updates. Thus, each wire is processed in its -+ * own discrete wave. -+ * -+ * However, there are some corner cases where this pattern breaks, with a proof of concept discovered -+ * by Rays Works, which works the same in vanilla. The scenario is as follows: -+ * (1) A redstone wire is conducting a signal. -+ * (2) Part-way through that wave of updates, a neighbor is updated that causes an update to a completely -+ * separate redstone wire. -+ * (3) This results in a call to BlockRedstoneWire.neighborChanged for that other wire, in the middle of -+ * an already on-going propagation through the first wire. -+ * -+ * The vanilla code, being depth-first, would end up fully processing the second wire before going back -+ * to finish processing the first one. (Although technically, vanilla has no special concept of "being -+ * in the middle" of processing updates to a wire.) For the breadth-first algorithm, we give this -+ * situation special handling, where the updates for the second wire are incorporated into the schedule -+ * for the first wire, and then the callstack is allowed to unwind back to the on-going search loop in -+ * order to continue processing both the first and second wire in the order of distance from the initial -+ * trigger. -+ */ -+ private BlockState scheduleReentrantNeighborChanged(final Level worldIn, final BlockPos pos, final BlockState newState, final BlockPos source) { -+ if (source != null) { -+ // If the cause of the redstone wire update is known, we can use that to help determine -+ // direction of information flow. -+ UpdateNode src = nodeCache.get(source); -+ if (src == null) { -+ src = new UpdateNode(); -+ src.self = source; -+ src.parent = source; -+ src.visited = true; -+ identifyNode(worldIn, src); -+ nodeCache.put(source, src); -+ } -+ } -+ -+ // Find or generate a node for the redstone block position receiving the update -+ UpdateNode upd = nodeCache.get(pos); -+ if (upd == null) { -+ upd = new UpdateNode(); -+ upd.self = pos; -+ upd.parent = pos; -+ upd.visited = true; -+ identifyNode(worldIn, upd); -+ nodeCache.put(pos, upd); -+ } -+ upd.currentState = newState; -+ -+ // Receiving this block update may mean something in the world changed. -+ // Therefore we clear the cached block info about all neighbors of -+ // the position receiving the update and then re-identify what they are. -+ if (upd.neighbor_nodes != null) { -+ for (int i=0; i<24; i++) { -+ final UpdateNode upd2 = upd.neighbor_nodes[i]; -+ if (upd2 == null) continue; -+ upd2.type = UpdateNode.Type.UNKNOWN; -+ upd2.currentState = null; -+ identifyNode(worldIn, upd2); -+ } -+ } -+ -+ // The block at 'pos' is a redstone wire and has been updated already by calling -+ // wire.calculateCurrentChanges, so we don't schedule that. However, we do need -+ // to schedule its neighbors. By passing the current value of 'currentWalkLayer' to -+ // propagateChanges, the neighbors of 'pos' are scheduled for layers currentWalkLayer+1 -+ // and currentWalkLayer+2. -+ propagateChanges(worldIn, upd, currentWalkLayer); -+ -+ // Return here. The call stack will unwind back to the first call to -+ // updateSurroundingRedstone, whereupon the new updates just scheduled will -+ // be propagated. This also facilitates elimination of superfluous and -+ // redundant block updates. -+ return newState; -+ } -+ -+ /* -+ * New version of pre-existing updateSurroundingRedstone, which is called from -+ * wire.updateSurroundingRedstone, which is called from wire.neighborChanged and a -+ * few other methods in BlockRedstoneWire. This sets off the breadth-first -+ * walk through all redstone dust connected to the initial position triggered. -+ */ -+ public BlockState updateSurroundingRedstone(final Level worldIn, final BlockPos pos, final BlockState state, final BlockPos source) { -+ // Check this block's neighbors and see if its power level needs to change -+ // Use the calculateCurrentChanges method in BlockRedstoneWire since we have no -+ // cached block states at this point. -+ final BlockState newState = wire.calculateCurrentChanges(worldIn, pos, state); -+ -+ // If no change, exit -+ if (newState == state) { -+ return state; -+ } -+ -+ // Check to see if this update was received during an on-going breadth first search -+ if (currentWalkLayer > 0 || nodeCache.size() > 0) { -+ // As breadthFirstWalk progresses, it sends block updates to neighbors. Some of those -+ // neighbors may affect the world so as to cause yet another redstone wire block to receive -+ // an update. If that happens, we need to integrate those redstone wire updates into the -+ // already on-going graph walk being performed by breadthFirstWalk. -+ return scheduleReentrantNeighborChanged(worldIn, pos, newState, source); -+ } -+ // If there are no on-going walks through redstone wire, then start a new walk. -+ -+ // If the source of the block update to the redstone wire at 'pos' is known, we can use -+ // that to help determine the direction of information flow. -+ if (source != null) { -+ final UpdateNode src = new UpdateNode(); -+ src.self = source; -+ src.parent = source; -+ src.visited = true; -+ nodeCache.put(source, src); -+ identifyNode(worldIn, src); -+ } -+ -+ // Create a node representing the block at 'pos', and then propagate updates -+ // to its neighbors. As stated above, the call to wire.calculateCurrentChanges -+ // already performs the update to the block at 'pos', so it is not added to the schedule. -+ final UpdateNode upd = new UpdateNode(); -+ upd.self = pos; -+ upd.parent = source!=null ? source : pos; -+ upd.currentState = newState; -+ upd.type = UpdateNode.Type.REDSTONE; -+ upd.visited = true; -+ nodeCache.put(pos, upd); -+ propagateChanges(worldIn, upd, 0); -+ -+ // Perform the walk over all directly reachable redstone wire blocks, propagating wire value -+ // updates in a breadth first order out from the initial update received for the block at 'pos'. -+ breadthFirstWalk(worldIn); -+ -+ // With the whole search completed, clear the list of all known blocks. -+ // We do not want to keep around state information that may be changed by other code. -+ // In theory, we could cache the neighbor block positions, but that is a separate -+ // optimization. -+ nodeCache.clear(); -+ -+ return newState; -+ } -+ -+ // For any array of neighbors in an UpdateNode object, these are always -+ // the indices of the four immediate neighbors at the same Y coordinate. -+ private static final int[] rs_neighbors = {4, 5, 6, 7}; -+ private static final int[] rs_neighbors_up = {9, 11, 13, 15}; -+ private static final int[] rs_neighbors_dn = {8, 10, 12, 14}; -+ -+ /* -+ * Updated calculateCurrentChanges that is optimized for speed and uses -+ * the UpdateNode's neighbor array to find the redstone states of neighbors -+ * that might power it. -+ */ -+ private BlockState calculateCurrentChanges(final Level worldIn, final UpdateNode upd) { -+ BlockState state = upd.currentState; -+ final int i = state.getValue(RedStoneWireBlock.POWER).intValue(); -+ int j = 0; -+ j = getMaxCurrentStrength(upd, j); -+ int l = 0; -+ -+ wire.shouldSignal = false; -+ // Unfortunately, World.isBlockIndirectlyGettingPowered is complicated, -+ // and I'm not ready to try to replicate even more functionality from -+ // elsewhere in Minecraft into this accelerator. So sadly, we must -+ // suffer the performance hit of this very expensive call. If there -+ // is consistency to what this call returns, we may be able to cache it. -+ final int k = worldIn.getBestNeighborSignal(upd.self); -+ wire.shouldSignal = true; -+ -+ // The variable 'k' holds the maximum redstone power value of any adjacent blocks. -+ // If 'k' has the highest level of all neighbors, then the power level of this -+ // redstone wire will be set to 'k'. If 'k' is already 15, then nothing inside the -+ // following loop can affect the power level of the wire. Therefore, the loop is -+ // skipped if k is already 15. -+ if (k < 15) { -+ if (upd.neighbor_nodes == null) { -+ // If this node's neighbors are not known, expand the node -+ findNeighbors(worldIn, upd); -+ } -+ -+ // These remain constant, so pull them out of the loop. -+ // Regardless of which direction is forward, the UpdateNode for the -+ // position directly above the node being calculated is always -+ // at index 1. -+ UpdateNode center_up = upd.neighbor_nodes[1]; -+ boolean center_up_is_cube = center_up.currentState.isRedstoneConductor(worldIn, center_up.self); // TODO -+ -+ for (int m = 0; m < 4; m++) { -+ // Get the neighbor array index of each of the four cardinal -+ // neighbors. -+ int n = rs_neighbors[m]; -+ -+ // Get the max redstone power level of each of the cardinal -+ // neighbors -+ UpdateNode neighbor = upd.neighbor_nodes[n]; -+ l = getMaxCurrentStrength(neighbor, l); -+ -+ // Also check the positions above and below the cardinal -+ // neighbors -+ boolean neighbor_is_cube = neighbor.currentState.isRedstoneConductor(worldIn, neighbor.self); // TODO -+ if (!neighbor_is_cube) { -+ UpdateNode neighbor_down = upd.neighbor_nodes[rs_neighbors_dn[m]]; -+ l = getMaxCurrentStrength(neighbor_down, l); -+ } else -+ if (!center_up_is_cube) { -+ UpdateNode neighbor_up = upd.neighbor_nodes[rs_neighbors_up[m]]; -+ l = getMaxCurrentStrength(neighbor_up, l); -+ } -+ } -+ } -+ -+ // The new code sets this RedstoneWire block's power level to the highest neighbor -+ // minus 1. This usually results in wire power levels dropping by 2 at a time. -+ // This optimization alone has no impact on update order, only the number of updates. -+ j = l - 1; -+ -+ // If 'l' turns out to be zero, then j will be set to -1, but then since 'k' will -+ // always be in the range of 0 to 15, the following if will correct that. -+ if (k > j) j = k; -+ -+ // egg82's amendment -+ // Adding Bukkit's BlockRedstoneEvent - er.. event. -+ if (i != j) { -+ BlockRedstoneEvent event = new BlockRedstoneEvent(CraftBlock.at(worldIn, upd.self), i, j); -+ worldIn.getCraftServer().getPluginManager().callEvent(event); -+ j = event.getNewCurrent(); -+ } -+ -+ if (i != j) { -+ // If the power level has changed from its previous value, compute a new state -+ // and set it in the world. -+ // Possible optimization: Don't commit state changes to the world until they -+ // need to be known by some nearby non-redstone-wire block. -+ BlockPos pos = new BlockPos(upd.self.getX(), upd.self.getY(), upd.self.getZ()); -+ if (wire.canSurvive(null, worldIn, pos)) { -+ state = state.setValue(RedStoneWireBlock.POWER, Integer.valueOf(j)); -+ // [Space Walker] suppress shape updates and emit those manually to -+ // bypass the new neighbor update stack. -+ if (worldIn.setBlock(upd.self, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) -+ updateNeighborShapes(worldIn, upd.self, state); -+ } -+ } -+ -+ return state; -+ } -+ -+ private static final Direction[] UPDATE_SHAPE_ORDER = { Direction.WEST, Direction.EAST, Direction.NORTH, Direction.SOUTH, Direction.DOWN, Direction.UP }; -+ -+ /* -+ * [Space Walker] -+ * This method emits shape updates around the given block, -+ * bypassing the new neighbor update stack. Diagonal shape -+ * updates are omitted, as they are mostly unnecessary. -+ * Diagonal shape updates are emitted exclusively to other -+ * redstone wires, in order to update their connection properties. -+ * Wire connections should never change as a result of power -+ * changes, so the only behavioral change will be in scenarios -+ * where earlier shape updates have been suppressed to keep a -+ * redstone wire in an invalid state. -+ */ -+ public void updateNeighborShapes(Level level, BlockPos pos, BlockState state) { -+ // these updates will be added to the stack and processed after the entire network has updated -+ state.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS); -+ -+ for (Direction dir : UPDATE_SHAPE_ORDER) { -+ BlockPos neighborPos = pos.relative(dir); -+ BlockState neighborState = level.getBlockState(neighborPos); -+ -+ BlockState newState = neighborState.updateShape(level, level, neighborPos, dir.getOpposite(), pos, state, level.getRandom()); -+ Block.updateOrDestroy(neighborState, newState, level, neighborPos, Block.UPDATE_CLIENTS); -+ } -+ } -+ -+ /* -+ * Optimized function to compute a redstone wire's power level based on cached -+ * state. -+ */ -+ private static int getMaxCurrentStrength(final UpdateNode upd, final int strength) { -+ if (upd.type != UpdateNode.Type.REDSTONE) return strength; -+ final int i = upd.currentState.getValue(RedStoneWireBlock.POWER).intValue(); -+ return i > strength ? i : strength; -+ } -+} -diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java -index 2a3be00d41eda68f7d5383b240759561c4663f8d..09b8f5335cb7651d90f4d1ca61b2ec5aa324e443 100644 ---- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java -@@ -290,6 +290,60 @@ public class RedStoneWireBlock extends Block { - return floor.isFaceSturdy(world, pos, Direction.UP) || floor.is(Blocks.HOPPER); - } - -+ // Paper start - Optimize redstone -+ // The bulk of the new functionality is found in RedstoneWireTurbo.java -+ com.destroystokyo.paper.util.RedstoneWireTurbo turbo = new com.destroystokyo.paper.util.RedstoneWireTurbo(this); -+ -+ /* -+ * Modified version of pre-existing updateSurroundingRedstone, which is called from -+ * this.neighborChanged and a few other methods in this class. -+ * Note: Added 'source' argument so as to help determine direction of information flow -+ */ -+ private void updateSurroundingRedstone(Level worldIn, BlockPos pos, BlockState state, @Nullable Orientation orientation, boolean blockAdded) { -+ if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT) { -+ // since 24w33a the source pos is no longer given, but instead an Orientation parameter -+ // when this is not null, it can be used to find the source pos, which the turbo uses -+ // to find the direction of information flow -+ BlockPos source = null; -+ if (orientation != null) { -+ source = pos.relative(orientation.getFront().getOpposite()); -+ } -+ turbo.updateSurroundingRedstone(worldIn, pos, state, source); -+ return; -+ } -+ updatePowerStrength(worldIn, pos, state, orientation, blockAdded); -+ } -+ -+ /* -+ * This method computes a wire's target strength and updates the given block state. -+ * It uses the DefaultRedstoneWireEvaluator for this, which is identical to code -+ * that was present in this class prior to the introduction of the experimental redstone -+ * changes in 24w33a. -+ * The previous implementation of this method in this patch had optimizations that have -+ * not been relevant since 1.13, thus it has been greatly simplified. -+ */ -+ public BlockState calculateCurrentChanges(Level level, BlockPos pos, BlockState state) { -+ int oldPower = state.getValue(POWER); -+ int newPower = ((DefaultRedstoneWireEvaluator) evaluator).calculateTargetStrength(level, pos); -+ if (oldPower != newPower) { -+ org.bukkit.event.block.BlockRedstoneEvent event = new org.bukkit.event.block.BlockRedstoneEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), oldPower, newPower); -+ level.getCraftServer().getPluginManager().callEvent(event); -+ -+ newPower = event.getNewCurrent(); -+ -+ if (level.getBlockState(pos) == state) { -+ state = state.setValue(POWER, newPower); -+ // [Space Walker] suppress shape updates and emit those manually to -+ // bypass the new neighbor update stack. -+ if (level.setBlock(pos, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) { -+ turbo.updateNeighborShapes(level, pos, state); -+ } -+ } -+ } -+ return state; -+ } -+ // Paper end -+ - private void updatePowerStrength(Level world, BlockPos pos, BlockState state, @Nullable Orientation orientation, boolean blockAdded) { - if (useExperimentalEvaluator(world)) { - new ExperimentalRedstoneWireEvaluator(this).updatePowerStrength(world, pos, state, orientation, blockAdded); -@@ -318,7 +372,7 @@ public class RedStoneWireBlock extends Block { - @Override - protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) { - if (!oldState.is(state.getBlock()) && !world.isClientSide) { -- this.updatePowerStrength(world, pos, state, null, true); -+ this.updateSurroundingRedstone(world, pos, state, null, true); // Paper - Optimize redstone - - for (Direction direction : Direction.Plane.VERTICAL) { - world.updateNeighborsAt(pos.relative(direction), this); -@@ -337,7 +391,7 @@ public class RedStoneWireBlock extends Block { - world.updateNeighborsAt(pos.relative(direction), this); - } - -- this.updatePowerStrength(world, pos, state, null, false); -+ this.updateSurroundingRedstone(world, pos, state, null, false); // Paper - Optimize redstone - this.updateNeighborsOfNeighboringWires(world, pos); - } - } -@@ -363,7 +417,7 @@ public class RedStoneWireBlock extends Block { - if (!world.isClientSide) { - if (sourceBlock != this || !useExperimentalEvaluator(world)) { - if (state.canSurvive(world, pos)) { -- this.updatePowerStrength(world, pos, state, wireOrientation, false); -+ this.updateSurroundingRedstone(world, pos, state, wireOrientation, false); // Paper - Optimize redstone - } else { - dropResources(state, world, pos); - world.removeBlock(pos, false); -diff --git a/src/main/java/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java b/src/main/java/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java -index f8be1f6bc6f144db5265844f46f0a2cb8cc213fe..3df778f3c9a633f07c7bd1736423384afce3edf7 100644 ---- a/src/main/java/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java -+++ b/src/main/java/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java -@@ -61,7 +61,7 @@ public class DefaultRedstoneWireEvaluator extends RedstoneWireEvaluator { - - } - -- private int calculateTargetStrength(Level world, BlockPos pos) { -+ public int calculateTargetStrength(Level world, BlockPos pos) { // Paper - Optimize redstone - int i = this.getBlockSignal(world, pos); - - return i == 15 ? i : Math.max(i, this.getIncomingWireSignal(world, pos)); diff --git a/patches/unapplied/server/1063-Improve-performance-of-RecipeMap-removeRecipe.patch b/patches/unapplied/server/1063-Improve-performance-of-RecipeMap-removeRecipe.patch deleted file mode 100644 index 08bbad66b6..0000000000 --- a/patches/unapplied/server/1063-Improve-performance-of-RecipeMap-removeRecipe.patch +++ /dev/null @@ -1,89 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Thu, 31 Oct 2024 20:36:41 -0700 -Subject: [PATCH] Improve performance of RecipeMap#removeRecipe - - -diff --git a/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java b/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java -index d376d89b479a4d5cc9ac568d23c7603a9355d580..f6dd363ececf967d282f5ba713013085da1ddf37 100644 ---- a/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java -+++ b/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java -@@ -258,7 +258,7 @@ public class RecipeManager extends SimplePreparableReloadListener imp - - // CraftBukkit start - public boolean removeRecipe(ResourceKey> mcKey) { -- boolean removed = this.recipes.removeRecipe(mcKey); -+ boolean removed = this.recipes.removeRecipe((ResourceKey>) (ResourceKey) mcKey); // Paper - generic fix - if (removed) { - this.finalizeRecipeLoading(); - } -diff --git a/src/main/java/net/minecraft/world/item/crafting/RecipeMap.java b/src/main/java/net/minecraft/world/item/crafting/RecipeMap.java -index 5d842d7e774564143f9f3be6c2628d54595a235b..c4067fbf827fed882772962a0e4b3ead0d642e62 100644 ---- a/src/main/java/net/minecraft/world/item/crafting/RecipeMap.java -+++ b/src/main/java/net/minecraft/world/item/crafting/RecipeMap.java -@@ -54,21 +54,38 @@ public class RecipeMap { - } - } - -- public boolean removeRecipe(ResourceKey> mcKey) { -- boolean removed = false; -- Iterator> iter = this.byType.values().iterator(); -- while (iter.hasNext()) { -- RecipeHolder recipe = iter.next(); -- if (recipe.id().equals(mcKey)) { -- iter.remove(); -- removed = true; -- } -- } -- removed |= this.byKey.remove(mcKey) != null; -+ // public boolean removeRecipe(ResourceKey> mcKey) { -+ // boolean removed = false; -+ // Iterator> iter = this.byType.values().iterator(); -+ // while (iter.hasNext()) { -+ // RecipeHolder recipe = iter.next(); -+ // if (recipe.id().equals(mcKey)) { -+ // iter.remove(); -+ // removed = true; -+ // } -+ // } -+ // removed |= this.byKey.remove(mcKey) != null; -+ // -+ // return removed; -+ // } -+ // CraftBukkit end -+ - -- return removed; -+ // Paper start - replace removeRecipe implementation -+ public boolean removeRecipe(ResourceKey> mcKey) { -+ //noinspection unchecked -+ final RecipeHolder> remove = (RecipeHolder>) this.byKey.remove(mcKey); -+ if (remove == null) { -+ return false; -+ } -+ final Collection>> recipes = this.byType(remove.value().getType()); -+ if (recipes.remove(remove)) { -+ return true; -+ } -+ return false; -+ // Paper end - why are you using a loop??? - } -- // CraftBukkit end -+ // Paper end - replace removeRecipe implementation - - public > Collection> byType(RecipeType type) { - return (Collection) this.byType.get(type); // CraftBukkit - decompile error -diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/RecipeIterator.java b/src/main/java/org/bukkit/craftbukkit/inventory/RecipeIterator.java -index c0433e054e64c329dff670c8f7ca21c4a4133c6f..a20b471389474244ef20bf42d4085dcf9dd122a5 100644 ---- a/src/main/java/org/bukkit/craftbukkit/inventory/RecipeIterator.java -+++ b/src/main/java/org/bukkit/craftbukkit/inventory/RecipeIterator.java -@@ -32,5 +32,9 @@ public class RecipeIterator implements Iterator { - public void remove() { - MinecraftServer.getServer().getRecipeManager().recipes.byKey.remove(this.currentRecipe.id()); // Paper - fix removing recipes from RecipeIterator - this.recipes.remove(); -+ // Paper start - correctly reload recipes -+ MinecraftServer.getServer().getRecipeManager().finalizeRecipeLoading(); -+ MinecraftServer.getServer().getPlayerList().reloadRecipes(); -+ // Paper end - correctly reload recipes - } - } diff --git a/patches/unapplied/server/1064-Reduce-work-done-in-CraftMapCanvas.drawImage-by-limi.patch b/patches/unapplied/server/1064-Reduce-work-done-in-CraftMapCanvas.drawImage-by-limi.patch deleted file mode 100644 index 81c7b52685..0000000000 --- a/patches/unapplied/server/1064-Reduce-work-done-in-CraftMapCanvas.drawImage-by-limi.patch +++ /dev/null @@ -1,75 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Barnaby <22575741+barnabwhy@users.noreply.github.com> -Date: Sat, 29 Jun 2024 12:06:51 +0100 -Subject: [PATCH] Reduce work done in CraftMapCanvas.drawImage by limiting size - of image and using System.arraycopy instead of for loops and use bitwise - operations to do bounds checks. - - -diff --git a/src/main/java/org/bukkit/craftbukkit/map/CraftMapCanvas.java b/src/main/java/org/bukkit/craftbukkit/map/CraftMapCanvas.java -index ff59f759669620795ef355c988b664bdcda39f52..a5e98571d6d83390761c11e28a0bc3c4415799cd 100644 ---- a/src/main/java/org/bukkit/craftbukkit/map/CraftMapCanvas.java -+++ b/src/main/java/org/bukkit/craftbukkit/map/CraftMapCanvas.java -@@ -91,12 +91,41 @@ public class CraftMapCanvas implements MapCanvas { - - @Override - public void drawImage(int x, int y, Image image) { -- byte[] bytes = MapPalette.imageToBytes(image); -- for (int x2 = 0; x2 < image.getWidth(null); ++x2) { -- for (int y2 = 0; y2 < image.getHeight(null); ++y2) { -- this.setPixel(x + x2, y + y2, bytes[y2 * image.getWidth(null) + x2]); -+ // Paper start - Reduce work done by limiting size of image and using System.arraycopy -+ int width = 128 - x; -+ int height = 128 - y; -+ if (image.getHeight(null) < height) -+ height = image.getHeight(null); -+ -+ // Create a subimage if the image is larger than the max allowed size -+ java.awt.image.BufferedImage temp; -+ if (image.getWidth(null) >= width && image instanceof java.awt.image.BufferedImage bImage) { -+ // If the image is larger than the max allowed size, get a subimage, otherwise use the image as is -+ if (image.getWidth(null) > width || image.getHeight(null) > height) { -+ temp = bImage.getSubimage(0, 0, width, height); -+ } else { -+ temp = bImage; - } -+ } else { -+ temp = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB); -+ java.awt.Graphics2D graphics = temp.createGraphics(); -+ graphics.drawImage(image, 0, 0, null); -+ graphics.dispose(); - } -+ -+ byte[] bytes = MapPalette.imageToBytes(temp); -+ -+ // Since we now control the size of the image, we can safely use System.arraycopy -+ // If x is 0, we can just copy the entire image as width is 128 and height is <=(128-y) -+ if (x == 0) { -+ System.arraycopy(bytes, 0, this.buffer, y * 128, width * height); -+ return; -+ } -+ -+ for (int y2 = 0; y2 < height; ++y2) { -+ System.arraycopy(bytes, 0, this.buffer, (y + y2) * 128 + x, width); -+ } -+ // Paper end - } - - @Override -diff --git a/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java b/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java -index 0cbbd915631904fe8c6effefb92895422b33eff6..cf0920e5f84b35647882fb963e9972af4e8427e0 100644 ---- a/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java -+++ b/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java -@@ -23,8 +23,10 @@ public class CraftMapRenderer extends MapRenderer { - @Override - public void render(MapView map, MapCanvas canvas, Player player) { - // Map -- for (int x = 0; x < 128; ++x) { -- for (int y = 0; y < 128; ++y) { -+ // Paper start - Swap inner and outer loops here to (theoretically) improve cache locality -+ for (int y = 0; y < 128; ++y) { -+ for (int x = 0; x < 128; ++x) { -+ // Paper end - canvas.setPixel(x, y, this.worldMap.colors[y * 128 + x]); - } - } diff --git a/patches/unapplied/server/1065-Add-Alternate-Current-redstone-implementation.patch b/patches/unapplied/server/1065-Add-Alternate-Current-redstone-implementation.patch deleted file mode 100644 index 6d9bd74276..0000000000 --- a/patches/unapplied/server/1065-Add-Alternate-Current-redstone-implementation.patch +++ /dev/null @@ -1,2452 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Space Walker -Date: Wed, 8 Jun 2022 18:47:18 +0200 -Subject: [PATCH] Add Alternate Current redstone implementation - -Author: Space Walker - -Original license: MIT -Original project: https://github.com/SpaceWalkerRS/alternate-current - -This patch adds Alternate Current's redstone implementation as an alternative to vanilla and Eigencraft's. -Performance of (de)powering redstone dust is many times faster than vanilla, and even exceeds Eigencraft. -Similar to Eigencraft, Alternate Current heavily changes the update order of redstone dust. This means any contraption that -is location dependent in vanilla will either work everywhere or nowhere when using Alternate Current/Eigencraft. Beyond that -parity issues should be rare for both implementations, though Alternate Current has not been tested as thoroughly, so I -cannot comment on how the two compare in that aspect. - -Alternate Current needs the following modifications: -* Level/ServerLevel: Each level has its own 'wire handler' that handles redstone dust power changes. -* RedStoneWireBlock: Replace calls to vanilla's or Eigencraft's methods for handling power changes with calls to -Alternate Current's wire handler. - -Feature patch - -diff --git a/src/main/java/alternate/current/wire/LevelHelper.java b/src/main/java/alternate/current/wire/LevelHelper.java -new file mode 100644 -index 0000000000000000000000000000000000000000..eda108e2df9bf7d1ddd89287b8d2c2d7f1637c96 ---- /dev/null -+++ b/src/main/java/alternate/current/wire/LevelHelper.java -@@ -0,0 +1,66 @@ -+package alternate.current.wire; -+ -+import org.bukkit.craftbukkit.block.CraftBlock; -+import org.bukkit.event.block.BlockRedstoneEvent; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import net.minecraft.world.level.chunk.LevelChunkSection; -+ -+class LevelHelper { -+ -+ static int doRedstoneEvent(ServerLevel level, BlockPos pos, int prevPower, int newPower) { -+ BlockRedstoneEvent event = new BlockRedstoneEvent(CraftBlock.at(level, pos), prevPower, newPower); -+ level.getCraftServer().getPluginManager().callEvent(event); -+ -+ return event.getNewCurrent(); -+ } -+ -+ /** -+ * An optimized version of {@link net.minecraft.world.level.Level#setBlock -+ * Level.setBlock}. Since this method is only used to update redstone wire block -+ * states, lighting checks, height map updates, and block entity updates are -+ * omitted. -+ */ -+ static boolean setWireState(ServerLevel level, BlockPos pos, BlockState state, boolean updateNeighborShapes) { -+ int y = pos.getY(); -+ -+ if (y < level.getMinY() || y >= level.getMaxY()) { -+ return false; -+ } -+ -+ int x = pos.getX(); -+ int z = pos.getZ(); -+ int index = level.getSectionIndex(y); -+ -+ ChunkAccess chunk = level.getChunk(x >> 4, z >> 4, ChunkStatus.FULL, true); -+ LevelChunkSection section = chunk.getSections()[index]; -+ -+ if (section == null) { -+ return false; // we should never get here -+ } -+ -+ BlockState prevState = section.setBlockState(x & 15, y & 15, z & 15, state); -+ -+ if (state == prevState) { -+ return false; -+ } -+ -+ // notify clients of the BlockState change -+ level.getChunkSource().blockChanged(pos); -+ // mark the chunk for saving -+ chunk.markUnsaved(); -+ -+ if (updateNeighborShapes) { -+ prevState.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_CLIENTS); -+ state.updateNeighbourShapes(level, pos, Block.UPDATE_CLIENTS); -+ state.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_CLIENTS); -+ } -+ -+ return true; -+ } -+} -diff --git a/src/main/java/alternate/current/wire/Node.java b/src/main/java/alternate/current/wire/Node.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8af6c69098e64945361d116b5fd6ac21e97fcd8d ---- /dev/null -+++ b/src/main/java/alternate/current/wire/Node.java -@@ -0,0 +1,113 @@ -+package alternate.current.wire; -+ -+import java.util.Arrays; -+ -+import alternate.current.wire.WireHandler.Directions; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.block.state.BlockState; -+ -+/** -+ * A Node represents a block in the world. It also holds a few other pieces of -+ * information that speed up the calculations in the WireHandler class. -+ * -+ * @author Space Walker -+ */ -+public class Node { -+ -+ // flags that encode the Node type -+ private static final int CONDUCTOR = 0b01; -+ private static final int SOURCE = 0b10; -+ -+ final ServerLevel level; -+ final Node[] neighbors; -+ -+ BlockPos pos; -+ BlockState state; -+ boolean invalid; -+ -+ private int flags; -+ -+ /** The previous node in the priority queue. */ -+ Node prev_node; -+ /** The next node in the priority queue. */ -+ Node next_node; -+ /** The priority with which this node was queued. */ -+ int priority; -+ /** The wire that queued this node for an update. */ -+ WireNode neighborWire; -+ -+ Node(ServerLevel level) { -+ this.level = level; -+ this.neighbors = new Node[Directions.ALL.length]; -+ } -+ -+ @Override -+ public boolean equals(Object obj) { -+ if (this == obj) { -+ return true; -+ } -+ if (!(obj instanceof Node)) { -+ return false; -+ } -+ -+ Node node = (Node)obj; -+ -+ return level == node.level && pos.equals(node.pos); -+ } -+ -+ @Override -+ public int hashCode() { -+ return pos.hashCode(); -+ } -+ -+ Node set(BlockPos pos, BlockState state, boolean clearNeighbors) { -+ if (state.is(Blocks.REDSTONE_WIRE)) { -+ throw new IllegalStateException("Cannot update a regular Node to a WireNode!"); -+ } -+ -+ if (clearNeighbors) { -+ Arrays.fill(neighbors, null); -+ } -+ -+ this.pos = pos.immutable(); -+ this.state = state; -+ this.invalid = false; -+ -+ this.flags = 0; -+ -+ if (this.state.isRedstoneConductor(this.level, this.pos)) { -+ this.flags |= CONDUCTOR; -+ } -+ if (this.state.isSignalSource()) { -+ this.flags |= SOURCE; -+ } -+ -+ return this; -+ } -+ -+ /** -+ * Determine the priority with which this node should be queued. -+ */ -+ int priority() { -+ return neighborWire.priority; -+ } -+ -+ public boolean isWire() { -+ return false; -+ } -+ -+ public boolean isConductor() { -+ return (flags & CONDUCTOR) != 0; -+ } -+ -+ public boolean isSignalSource() { -+ return (flags & SOURCE) != 0; -+ } -+ -+ public WireNode asWire() { -+ throw new UnsupportedOperationException("Not a WireNode!"); -+ } -+} -diff --git a/src/main/java/alternate/current/wire/PriorityQueue.java b/src/main/java/alternate/current/wire/PriorityQueue.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d71b4d0e4c44a2620b41b89475412db53bea20ed ---- /dev/null -+++ b/src/main/java/alternate/current/wire/PriorityQueue.java -@@ -0,0 +1,211 @@ -+package alternate.current.wire; -+ -+import java.util.AbstractQueue; -+import java.util.Arrays; -+import java.util.Iterator; -+ -+import net.minecraft.world.level.redstone.Redstone; -+ -+public class PriorityQueue extends AbstractQueue { -+ -+ private static final int OFFSET = -Redstone.SIGNAL_MIN; -+ -+ /** The last node for each priority value. */ -+ private final Node[] tails; -+ -+ private Node head; -+ private Node tail; -+ -+ private int size; -+ -+ PriorityQueue() { -+ this.tails = new Node[(Redstone.SIGNAL_MAX + OFFSET) + 1]; -+ } -+ -+ @Override -+ public boolean offer(Node node) { -+ if (node == null) { -+ throw new NullPointerException(); -+ } -+ -+ int priority = node.priority(); -+ -+ if (contains(node)) { -+ if (node.priority == priority) { -+ // already queued with this priority; exit -+ return false; -+ } else { -+ // already queued with different priority; move it -+ move(node, priority); -+ } -+ } else { -+ insert(node, priority); -+ } -+ -+ return true; -+ } -+ -+ @Override -+ public Node poll() { -+ if (head == null) { -+ return null; -+ } -+ -+ Node node = head; -+ Node next = node.next_node; -+ -+ if (next == null) { -+ clear(); // reset the tails array -+ } else { -+ if (node.priority != next.priority) { -+ // If the head is also a tail, its entry in the array -+ // can be cleared; there is no previous node with the -+ // same priority to take its place. -+ tails[node.priority + OFFSET] = null; -+ } -+ -+ node.next_node = null; -+ next.prev_node = null; -+ head = next; -+ -+ size--; -+ } -+ -+ return node; -+ } -+ -+ @Override -+ public Node peek() { -+ return head; -+ } -+ -+ @Override -+ public void clear() { -+ for (Node node = head; node != null; ) { -+ Node n = node; -+ node = node.next_node; -+ -+ n.prev_node = null; -+ n.next_node = null; -+ } -+ -+ Arrays.fill(tails, null); -+ -+ head = null; -+ tail = null; -+ -+ size = 0; -+ } -+ -+ @Override -+ public Iterator iterator() { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public int size() { -+ return size; -+ } -+ -+ public boolean contains(Node node) { -+ return node == head || node.prev_node != null; -+ } -+ -+ private void move(Node node, int priority) { -+ remove(node); -+ insert(node, priority); -+ } -+ -+ private void remove(Node node) { -+ Node prev = node.prev_node; -+ Node next = node.next_node; -+ -+ if (node == tail || node.priority != next.priority) { -+ // assign a new tail for this node's priority -+ if (node == head || node.priority != prev.priority) { -+ // there is no other node with the same priority; clear -+ tails[node.priority + OFFSET] = null; -+ } else { -+ // the previous node in the queue becomes the tail -+ tails[node.priority + OFFSET] = prev; -+ } -+ } -+ -+ if (node == head) { -+ head = next; -+ } else { -+ prev.next_node = next; -+ } -+ if (node == tail) { -+ tail = prev; -+ } else { -+ next.prev_node = prev; -+ } -+ -+ node.prev_node = null; -+ node.next_node = null; -+ -+ size--; -+ } -+ -+ private void insert(Node node, int priority) { -+ node.priority = priority; -+ -+ // nodes are sorted by priority (highest to lowest) -+ // nodes with the same priority are ordered FIFO -+ if (head == null) { -+ // first element in this queue \o/ -+ head = tail = node; -+ } else if (priority > head.priority) { -+ linkHead(node); -+ } else if (priority <= tail.priority) { -+ linkTail(node); -+ } else { -+ // since the node is neither the head nor the tail -+ // findPrev is guaranteed to find a non-null element -+ linkAfter(findPrev(node), node); -+ } -+ -+ tails[priority + OFFSET] = node; -+ -+ size++; -+ } -+ -+ private void linkHead(Node node) { -+ node.next_node = head; -+ head.prev_node = node; -+ head = node; -+ } -+ -+ private void linkTail(Node node) { -+ tail.next_node = node; -+ node.prev_node = tail; -+ tail = node; -+ } -+ -+ private void linkAfter(Node prev, Node node) { -+ linkBetween(prev, node, prev.next_node); -+ } -+ -+ private void linkBetween(Node prev, Node node, Node next) { -+ prev.next_node = node; -+ node.prev_node = prev; -+ -+ node.next_node = next; -+ next.prev_node = node; -+ } -+ -+ private Node findPrev(Node node) { -+ Node prev = null; -+ -+ for (int i = node.priority + OFFSET; i < tails.length; i++) { -+ prev = tails[i]; -+ -+ if (prev != null) { -+ break; -+ } -+ } -+ -+ return prev; -+ } -+} -diff --git a/src/main/java/alternate/current/wire/SimpleQueue.java b/src/main/java/alternate/current/wire/SimpleQueue.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2b30074252551e1dc55d5be17d26fb4a2d8eb2e4 ---- /dev/null -+++ b/src/main/java/alternate/current/wire/SimpleQueue.java -@@ -0,0 +1,112 @@ -+package alternate.current.wire; -+ -+import java.util.AbstractQueue; -+import java.util.Iterator; -+ -+public class SimpleQueue extends AbstractQueue { -+ -+ private WireNode head; -+ private WireNode tail; -+ -+ private int size; -+ -+ SimpleQueue() { -+ -+ } -+ -+ @Override -+ public boolean offer(WireNode node) { -+ if (node == null) { -+ throw new NullPointerException(); -+ } -+ -+ if (tail == null) { -+ head = tail = node; -+ } else { -+ tail.next_wire = node; -+ tail = node; -+ } -+ -+ size++; -+ -+ return true; -+ } -+ -+ @Override -+ public WireNode poll() { -+ if (head == null) { -+ return null; -+ } -+ -+ WireNode node = head; -+ WireNode next = node.next_wire; -+ -+ if (next == null) { -+ head = tail = null; -+ } else { -+ node.next_wire = null; -+ head = next; -+ } -+ -+ size--; -+ -+ return node; -+ } -+ -+ @Override -+ public WireNode peek() { -+ return head; -+ } -+ -+ @Override -+ public void clear() { -+ for (WireNode node = head; node != null; ) { -+ WireNode n = node; -+ node = node.next_wire; -+ -+ n.next_wire = null; -+ } -+ -+ head = null; -+ tail = null; -+ -+ size = 0; -+ } -+ -+ @Override -+ public Iterator iterator() { -+ return new SimpleIterator(); -+ } -+ -+ @Override -+ public int size() { -+ return size; -+ } -+ -+ private class SimpleIterator implements Iterator { -+ -+ private WireNode curr; -+ private WireNode next; -+ -+ private SimpleIterator() { -+ next = head; -+ } -+ -+ @Override -+ public boolean hasNext() { -+ if (next == null && curr != null) { -+ next = curr.next_wire; -+ } -+ -+ return next != null; -+ } -+ -+ @Override -+ public WireNode next() { -+ curr = next; -+ next = curr.next_wire; -+ -+ return curr; -+ } -+ } -+} -diff --git a/src/main/java/alternate/current/wire/UpdateOrder.java b/src/main/java/alternate/current/wire/UpdateOrder.java -new file mode 100644 -index 0000000000000000000000000000000000000000..29338efd16cf62bb49e81cce09fbafd9b4319e7c ---- /dev/null -+++ b/src/main/java/alternate/current/wire/UpdateOrder.java -@@ -0,0 +1,390 @@ -+package alternate.current.wire; -+ -+import java.util.Locale; -+import java.util.function.Consumer; -+ -+import alternate.current.wire.WireHandler.Directions; -+import alternate.current.wire.WireHandler.NodeProvider; -+ -+public enum UpdateOrder { -+ -+ HORIZONTAL_FIRST_OUTWARD( -+ new int[][] { -+ new int[] { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH, Directions.DOWN, Directions.UP }, -+ new int[] { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST , Directions.DOWN, Directions.UP }, -+ new int[] { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH, Directions.DOWN, Directions.UP }, -+ new int[] { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST , Directions.DOWN, Directions.UP } -+ -+ }, -+ new int[][] { -+ new int[] { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH }, -+ new int[] { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST }, -+ new int[] { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH }, -+ new int[] { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST } -+ } -+ ) { -+ -+ @Override -+ public void forEachNeighbor(NodeProvider nodes, Node source, int forward, Consumer action) { -+ /* -+ * This iteration order is designed to be an extension of the Vanilla shape -+ * update order, and is determined as follows: -+ *
-+ * 1. Each neighbor is identified by the step(s) you must take, starting at the -+ * source, to reach it. Each step is 1 block, thus the position of a neighbor is -+ * encoded by the direction(s) of the step(s), e.g. (right), (down), (up, left), -+ * etc. -+ *
-+ * 2. Neighbors are iterated over in pairs that lie on opposite sides of the -+ * source. -+ *
-+ * 3. Neighbors are iterated over in order of their distance from the source, -+ * moving outward. This means they are iterated over in 3 groups: direct -+ * neighbors first, then diagonal neighbors, and last are the far neighbors that -+ * are 2 blocks directly out. -+ *
-+ * 4. The order within each group is determined using the following basic order: -+ * { front, back, right, left, down, up }. This order was chosen because it -+ * converts to the following order of absolute directions when west is said to -+ * be 'forward': { west, east, north, south, down, up } - this is the order of -+ * shape updates. -+ */ -+ -+ int rightward = (forward + 1) & 0b11; -+ int backward = (forward + 2) & 0b11; -+ int leftward = (forward + 3) & 0b11; -+ int downward = Directions.DOWN; -+ int upward = Directions.UP; -+ -+ Node front = nodes.getNeighbor(source, forward); -+ Node right = nodes.getNeighbor(source, rightward); -+ Node back = nodes.getNeighbor(source, backward); -+ Node left = nodes.getNeighbor(source, leftward); -+ Node below = nodes.getNeighbor(source, downward); -+ Node above = nodes.getNeighbor(source, upward); -+ -+ // direct neighbors (6) -+ action.accept(front); -+ action.accept(back); -+ action.accept(right); -+ action.accept(left); -+ action.accept(below); -+ action.accept(above); -+ -+ // diagonal neighbors (12) -+ action.accept(nodes.getNeighbor(front, rightward)); -+ action.accept(nodes.getNeighbor(back, leftward)); -+ action.accept(nodes.getNeighbor(front, leftward)); -+ action.accept(nodes.getNeighbor(back, rightward)); -+ action.accept(nodes.getNeighbor(front, downward)); -+ action.accept(nodes.getNeighbor(back, upward)); -+ action.accept(nodes.getNeighbor(front, upward)); -+ action.accept(nodes.getNeighbor(back, downward)); -+ action.accept(nodes.getNeighbor(right, downward)); -+ action.accept(nodes.getNeighbor(left, upward)); -+ action.accept(nodes.getNeighbor(right, upward)); -+ action.accept(nodes.getNeighbor(left, downward)); -+ -+ // far neighbors (6) -+ action.accept(nodes.getNeighbor(front, forward)); -+ action.accept(nodes.getNeighbor(back, backward)); -+ action.accept(nodes.getNeighbor(right, rightward)); -+ action.accept(nodes.getNeighbor(left, leftward)); -+ action.accept(nodes.getNeighbor(below, downward)); -+ action.accept(nodes.getNeighbor(above, upward)); -+ } -+ }, -+ HORIZONTAL_FIRST_INWARD( -+ new int[][] { -+ new int[] { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH, Directions.DOWN, Directions.UP }, -+ new int[] { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST , Directions.DOWN, Directions.UP }, -+ new int[] { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH, Directions.DOWN, Directions.UP }, -+ new int[] { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST , Directions.DOWN, Directions.UP } -+ }, -+ new int[][] { -+ new int[] { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH }, -+ new int[] { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST }, -+ new int[] { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH }, -+ new int[] { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST } -+ } -+ ) { -+ -+ @Override -+ public void forEachNeighbor(NodeProvider nodes, Node source, int forward, Consumer action) { -+ /* -+ * This iteration order is designed to be an inversion of the above update -+ * order, and is determined as follows: -+ *
-+ * 1. Each neighbor is identified by the step(s) you must take, starting at the -+ * source, to reach it. Each step is 1 block, thus the position of a neighbor is -+ * encoded by the direction(s) of the step(s), e.g. (right), (down), (up, left), -+ * etc. -+ *
-+ * 2. Neighbors are iterated over in pairs that lie on opposite sides of the -+ * source. -+ *
-+ * 3. Neighbors are iterated over in order of their distance from the source, -+ * moving inward. This means they are iterated over in 3 groups: neighbors that -+ * are 2 blocks directly out first, then diagonal neighbors, and last are direct -+ * neighbors. -+ *
-+ * 4. The order within each group is determined using the following basic order: -+ * { front, back, right, left, down, up }. This order was chosen because it -+ * converts to the following order of absolute directions when west is said to -+ * be 'forward': { west, east, north, south, down, up } - this is the order of -+ * shape updates. -+ */ -+ -+ int rightward = (forward + 1) & 0b11; -+ int backward = (forward + 2) & 0b11; -+ int leftward = (forward + 3) & 0b11; -+ int downward = Directions.DOWN; -+ int upward = Directions.UP; -+ -+ Node front = nodes.getNeighbor(source, forward); -+ Node right = nodes.getNeighbor(source, rightward); -+ Node back = nodes.getNeighbor(source, backward); -+ Node left = nodes.getNeighbor(source, leftward); -+ Node below = nodes.getNeighbor(source, downward); -+ Node above = nodes.getNeighbor(source, upward); -+ -+ // far neighbors (6) -+ action.accept(nodes.getNeighbor(front, forward)); -+ action.accept(nodes.getNeighbor(back, backward)); -+ action.accept(nodes.getNeighbor(right, rightward)); -+ action.accept(nodes.getNeighbor(left, leftward)); -+ action.accept(nodes.getNeighbor(below, downward)); -+ action.accept(nodes.getNeighbor(above, upward)); -+ -+ // diagonal neighbors (12) -+ action.accept(nodes.getNeighbor(front, rightward)); -+ action.accept(nodes.getNeighbor(back, leftward)); -+ action.accept(nodes.getNeighbor(front, leftward)); -+ action.accept(nodes.getNeighbor(back, rightward)); -+ action.accept(nodes.getNeighbor(front, downward)); -+ action.accept(nodes.getNeighbor(back, upward)); -+ action.accept(nodes.getNeighbor(front, upward)); -+ action.accept(nodes.getNeighbor(back, downward)); -+ action.accept(nodes.getNeighbor(right, downward)); -+ action.accept(nodes.getNeighbor(left, upward)); -+ action.accept(nodes.getNeighbor(right, upward)); -+ action.accept(nodes.getNeighbor(left, downward)); -+ -+ -+ // direct neighbors (6) -+ action.accept(front); -+ action.accept(back); -+ action.accept(right); -+ action.accept(left); -+ action.accept(below); -+ action.accept(above); -+ } -+ }, -+ VERTICAL_FIRST_OUTWARD( -+ new int[][] { -+ new int[] { Directions.DOWN, Directions.UP, Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH }, -+ new int[] { Directions.DOWN, Directions.UP, Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST }, -+ new int[] { Directions.DOWN, Directions.UP, Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH }, -+ new int[] { Directions.DOWN, Directions.UP, Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST } -+ }, -+ new int[][] { -+ new int[] { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH }, -+ new int[] { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST }, -+ new int[] { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH }, -+ new int[] { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST } -+ } -+ ) { -+ -+ @Override -+ public void forEachNeighbor(NodeProvider nodes, Node source, int forward, Consumer action) { -+ /* -+ * This iteration order is designed to be the opposite of the Vanilla shape -+ * update order, and is determined as follows: -+ *
-+ * 1. Each neighbor is identified by the step(s) you must take, starting at the -+ * source, to reach it. Each step is 1 block, thus the position of a neighbor is -+ * encoded by the direction(s) of the step(s), e.g. (right), (down), (up, left), -+ * etc. -+ *
-+ * 2. Neighbors are iterated over in pairs that lie on opposite sides of the -+ * source. -+ *
-+ * 3. Neighbors are iterated over in order of their distance from the source, -+ * moving outward. This means they are iterated over in 3 groups: direct -+ * neighbors first, then diagonal neighbors, and last are the far neighbors that -+ * are 2 blocks directly out. -+ *
-+ * 4. The order within each group is determined using the following basic order: -+ * { down, up, front, back, right, left }. This order was chosen because it -+ * converts to the following order of absolute directions when west is said to -+ * be 'forward': { down, up west, east, north, south } - this is the order of -+ * shape updates, with the vertical directions moved to the front. -+ */ -+ -+ int rightward = (forward + 1) & 0b11; -+ int backward = (forward + 2) & 0b11; -+ int leftward = (forward + 3) & 0b11; -+ int downward = Directions.DOWN; -+ int upward = Directions.UP; -+ -+ Node front = nodes.getNeighbor(source, forward); -+ Node right = nodes.getNeighbor(source, rightward); -+ Node back = nodes.getNeighbor(source, backward); -+ Node left = nodes.getNeighbor(source, leftward); -+ Node below = nodes.getNeighbor(source, downward); -+ Node above = nodes.getNeighbor(source, upward); -+ -+ // direct neighbors (6) -+ action.accept(below); -+ action.accept(above); -+ action.accept(front); -+ action.accept(back); -+ action.accept(right); -+ action.accept(left); -+ -+ // diagonal neighbors (12) -+ action.accept(nodes.getNeighbor(below, forward)); -+ action.accept(nodes.getNeighbor(above, backward)); -+ action.accept(nodes.getNeighbor(below, backward)); -+ action.accept(nodes.getNeighbor(above, forward)); -+ action.accept(nodes.getNeighbor(below, rightward)); -+ action.accept(nodes.getNeighbor(above, leftward)); -+ action.accept(nodes.getNeighbor(below, leftward)); -+ action.accept(nodes.getNeighbor(above, rightward)); -+ action.accept(nodes.getNeighbor(front, rightward)); -+ action.accept(nodes.getNeighbor(back, leftward)); -+ action.accept(nodes.getNeighbor(front, leftward)); -+ action.accept(nodes.getNeighbor(back, rightward)); -+ -+ // far neighbors (6) -+ action.accept(nodes.getNeighbor(below, downward)); -+ action.accept(nodes.getNeighbor(above, upward)); -+ action.accept(nodes.getNeighbor(front, forward)); -+ action.accept(nodes.getNeighbor(back, backward)); -+ action.accept(nodes.getNeighbor(right, rightward)); -+ action.accept(nodes.getNeighbor(left, leftward)); -+ } -+ }, -+ VERTICAL_FIRST_INWARD( -+ new int[][] { -+ new int[] { Directions.DOWN, Directions.UP, Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH }, -+ new int[] { Directions.DOWN, Directions.UP, Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST }, -+ new int[] { Directions.DOWN, Directions.UP, Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH }, -+ new int[] { Directions.DOWN, Directions.UP, Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST } -+ }, -+ new int[][] { -+ new int[] { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH }, -+ new int[] { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST }, -+ new int[] { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH }, -+ new int[] { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST } -+ } -+ ) { -+ -+ @Override -+ public void forEachNeighbor(NodeProvider nodes, Node source, int forward, Consumer action) { -+ /* -+ * This iteration order is designed to be an inversion of the above update -+ * order, and is determined as follows: -+ *
-+ * 1. Each neighbor is identified by the step(s) you must take, starting at the -+ * source, to reach it. Each step is 1 block, thus the position of a neighbor is -+ * encoded by the direction(s) of the step(s), e.g. (right), (down), (up, left), -+ * etc. -+ *
-+ * 2. Neighbors are iterated over in pairs that lie on opposite sides of the -+ * source. -+ *
-+ * 3. Neighbors are iterated over in order of their distance from the source, -+ * moving inward. This means they are iterated over in 3 groups: neighbors that -+ * are 2 blocks directly out first, then diagonal neighbors, and last are direct -+ * neighbors. -+ *
-+ * 4. The order within each group is determined using the following basic order: -+ * { down, up, front, back, right, left }. This order was chosen because it -+ * converts to the following order of absolute directions when west is said to -+ * be 'forward': { down, up west, east, north, south } - this is the order of -+ * shape updates, with the vertical directions moved to the front. -+ */ -+ -+ int rightward = (forward + 1) & 0b11; -+ int backward = (forward + 2) & 0b11; -+ int leftward = (forward + 3) & 0b11; -+ int downward = Directions.DOWN; -+ int upward = Directions.UP; -+ -+ Node front = nodes.getNeighbor(source, forward); -+ Node right = nodes.getNeighbor(source, rightward); -+ Node back = nodes.getNeighbor(source, backward); -+ Node left = nodes.getNeighbor(source, leftward); -+ Node below = nodes.getNeighbor(source, downward); -+ Node above = nodes.getNeighbor(source, upward); -+ -+ // far neighbors (6) -+ action.accept(nodes.getNeighbor(below, downward)); -+ action.accept(nodes.getNeighbor(above, upward)); -+ action.accept(nodes.getNeighbor(front, forward)); -+ action.accept(nodes.getNeighbor(back, backward)); -+ action.accept(nodes.getNeighbor(right, rightward)); -+ action.accept(nodes.getNeighbor(left, leftward)); -+ -+ // diagonal neighbors (12) -+ action.accept(nodes.getNeighbor(below, forward)); -+ action.accept(nodes.getNeighbor(above, backward)); -+ action.accept(nodes.getNeighbor(below, backward)); -+ action.accept(nodes.getNeighbor(above, forward)); -+ action.accept(nodes.getNeighbor(below, rightward)); -+ action.accept(nodes.getNeighbor(above, leftward)); -+ action.accept(nodes.getNeighbor(below, leftward)); -+ action.accept(nodes.getNeighbor(above, rightward)); -+ action.accept(nodes.getNeighbor(front, rightward)); -+ action.accept(nodes.getNeighbor(back, leftward)); -+ action.accept(nodes.getNeighbor(front, leftward)); -+ action.accept(nodes.getNeighbor(back, rightward)); -+ -+ // direct neighbors (6) -+ action.accept(below); -+ action.accept(above); -+ action.accept(front); -+ action.accept(back); -+ action.accept(right); -+ action.accept(left); -+ } -+ }; -+ -+ private final int[][] directNeighbors; -+ private final int[][] cardinalNeighbors; -+ -+ private UpdateOrder(int[][] directNeighbors, int[][] cardinalNeighbors) { -+ this.directNeighbors = directNeighbors; -+ this.cardinalNeighbors = cardinalNeighbors; -+ } -+ -+ public String id() { -+ return name().toLowerCase(Locale.ENGLISH); -+ } -+ -+ public static UpdateOrder byId(String id) { -+ return valueOf(id.toUpperCase(Locale.ENGLISH)); -+ } -+ -+ public int[] directNeighbors(int forward) { -+ return directNeighbors[forward]; -+ } -+ -+ public int[] cardinalNeighbors(int forward) { -+ return cardinalNeighbors[forward]; -+ } -+ -+ /** -+ * Iterate over all neighboring nodes of the given source node. The iteration -+ * order is built from relative directions around the source, depending on the -+ * given 'forward' direction. This is an effort to eliminate any directional -+ * biases that would be emerge in rotationally symmetric circuits if the update -+ * order was built from absolute directions around the source. -+ *
-+ * Each update order must include the source's direct neighbors, but further -+ * neighbors may not be included. -+ */ -+ public abstract void forEachNeighbor(NodeProvider nodes, Node source, int forward, Consumer action); -+ -+} -diff --git a/src/main/java/alternate/current/wire/WireConnection.java b/src/main/java/alternate/current/wire/WireConnection.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4fd8cb29024330397cfe4cbc1f237d285bfb7b3e ---- /dev/null -+++ b/src/main/java/alternate/current/wire/WireConnection.java -@@ -0,0 +1,30 @@ -+package alternate.current.wire; -+ -+/** -+ * This class represents a connection between some WireNode (the 'owner') and a -+ * neighboring WireNode. Two wires are considered to be connected if power can -+ * flow from one wire to the other (and/or vice versa). -+ * -+ * @author Space Walker -+ */ -+public class WireConnection { -+ -+ /** The connected wire. */ -+ final WireNode wire; -+ /** Cardinal direction to the connected wire. */ -+ final int iDir; -+ /** True if the owner of the connection can provide power to the connected wire. */ -+ final boolean offer; -+ /** True if the connected wire can provide power to the owner of the connection. */ -+ final boolean accept; -+ -+ /** The next connection in the sequence. */ -+ WireConnection next; -+ -+ WireConnection(WireNode wire, int iDir, boolean offer, boolean accept) { -+ this.wire = wire; -+ this.iDir = iDir; -+ this.offer = offer; -+ this.accept = accept; -+ } -+} -diff --git a/src/main/java/alternate/current/wire/WireConnectionManager.java b/src/main/java/alternate/current/wire/WireConnectionManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f03b313e58385d626490a9e64c9616fd08aa951e ---- /dev/null -+++ b/src/main/java/alternate/current/wire/WireConnectionManager.java -@@ -0,0 +1,134 @@ -+package alternate.current.wire; -+ -+import java.util.Arrays; -+import java.util.function.Consumer; -+ -+import alternate.current.wire.WireHandler.Directions; -+import alternate.current.wire.WireHandler.NodeProvider; -+ -+public class WireConnectionManager { -+ -+ /** The owner of these connections. */ -+ final WireNode owner; -+ -+ /** The first connection for each cardinal direction. */ -+ private final WireConnection[] heads; -+ -+ private WireConnection head; -+ private WireConnection tail; -+ -+ /** The total number of connections. */ -+ int total; -+ -+ /** -+ * A 4 bit number that encodes in which direction(s) the owner has connections -+ * to other wires. -+ */ -+ private int flowTotal; -+ /** The direction of flow based connections to other wires. */ -+ int iFlowDir; -+ -+ WireConnectionManager(WireNode owner) { -+ this.owner = owner; -+ -+ this.heads = new WireConnection[Directions.HORIZONTAL.length]; -+ -+ this.total = 0; -+ -+ this.flowTotal = 0; -+ this.iFlowDir = -1; -+ } -+ -+ void set(NodeProvider nodes) { -+ if (total > 0) { -+ clear(); -+ } -+ -+ boolean belowIsConductor = nodes.getNeighbor(owner, Directions.DOWN).isConductor(); -+ boolean aboveIsConductor = nodes.getNeighbor(owner, Directions.UP).isConductor(); -+ -+ for (int iDir = 0; iDir < Directions.HORIZONTAL.length; iDir++) { -+ Node neighbor = nodes.getNeighbor(owner, iDir); -+ -+ if (neighbor.isWire()) { -+ add(neighbor.asWire(), iDir, true, true); -+ } else { -+ boolean sideIsConductor = neighbor.isConductor(); -+ -+ if (!sideIsConductor) { -+ Node node = nodes.getNeighbor(neighbor, Directions.DOWN); -+ -+ if (node.isWire()) { -+ add(node.asWire(), iDir, belowIsConductor, true); -+ } -+ } -+ if (!aboveIsConductor) { -+ Node node = nodes.getNeighbor(neighbor, Directions.UP); -+ -+ if (node.isWire()) { -+ add(node.asWire(), iDir, true, sideIsConductor); -+ } -+ } -+ } -+ } -+ -+ if (total > 0) { -+ iFlowDir = WireHandler.FLOW_IN_TO_FLOW_OUT[flowTotal]; -+ } -+ } -+ -+ private void clear() { -+ Arrays.fill(heads, null); -+ -+ head = null; -+ tail = null; -+ -+ total = 0; -+ -+ flowTotal = 0; -+ iFlowDir = -1; -+ } -+ -+ private void add(WireNode wire, int iDir, boolean offer, boolean accept) { -+ add(new WireConnection(wire, iDir, offer, accept)); -+ } -+ -+ private void add(WireConnection connection) { -+ if (head == null) { -+ head = connection; -+ tail = connection; -+ } else { -+ tail.next = connection; -+ tail = connection; -+ } -+ -+ total++; -+ -+ if (heads[connection.iDir] == null) { -+ heads[connection.iDir] = connection; -+ flowTotal |= (1 << connection.iDir); -+ } -+ } -+ -+ /** -+ * Iterate over all connections. Use this method if the iteration order is not -+ * important. -+ */ -+ void forEach(Consumer consumer) { -+ for (WireConnection c = head; c != null; c = c.next) { -+ consumer.accept(c); -+ } -+ } -+ -+ /** -+ * Iterate over all connections. Use this method if the iteration order is -+ * important. -+ */ -+ void forEach(Consumer consumer, UpdateOrder updateOrder, int iFlowDir) { -+ for (int iDir : updateOrder.cardinalNeighbors(iFlowDir)) { -+ for (WireConnection c = heads[iDir]; c != null && c.iDir == iDir; c = c.next) { -+ consumer.accept(c); -+ } -+ } -+ } -+} -diff --git a/src/main/java/alternate/current/wire/WireHandler.java b/src/main/java/alternate/current/wire/WireHandler.java -new file mode 100644 -index 0000000000000000000000000000000000000000..259b301b2c8b64cb7974a235afb260e0e991af54 ---- /dev/null -+++ b/src/main/java/alternate/current/wire/WireHandler.java -@@ -0,0 +1,1073 @@ -+package alternate.current.wire; -+ -+import java.util.Iterator; -+import java.util.Queue; -+ -+import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -+import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry; -+import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; -+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Direction; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.redstone.InstantNeighborUpdater; -+import net.minecraft.world.level.redstone.NeighborUpdater; -+import net.minecraft.world.level.redstone.Orientation; -+import net.minecraft.world.level.redstone.Redstone; -+ -+/** -+ * This class handles power changes for redstone wire. The algorithm was -+ * designed with the following goals in mind: -+ *
-+ * 1. Minimize the number of times a wire checks its surroundings to determine -+ * its power level. -+ *
-+ * 2. Minimize the number of block and shape updates emitted. -+ *
-+ * 3. Emit block and shape updates in a deterministic, non-locational order, -+ * fixing bug MC-11193. -+ * -+ *

-+ * In Vanilla redstone wire is laggy because it fails on points 1 and 2. -+ * -+ *

-+ * Redstone wire updates recursively and each wire calculates its power level in -+ * isolation rather than in the context of the network it is a part of. This -+ * means a wire in a grid can change its power level over half a dozen times -+ * before settling on its final value. This problem used to be worse in 1.13 and -+ * below, where a wire would only decrease its power level by 1 at a time. -+ * -+ *

-+ * In addition to this, a wire emits 42 block updates and up to 22 shape updates -+ * each time it changes its power level. -+ * -+ *

-+ * Of those 42 block updates, 6 are to itself, which are thus not only -+ * redundant, but a big source of lag, since those cause the wire to -+ * unnecessarily re-calculate its power level. A block only has 24 neighbors -+ * within a Manhattan distance of 2, meaning 12 of the remaining 36 block -+ * updates are duplicates and thus also redundant. -+ * -+ *

-+ * Of the 22 shape updates, only 6 are strictly necessary. The other 16 are sent -+ * to blocks diagonally above and below. These are necessary if a wire changes -+ * its connections, but not when it changes its power level. -+ * -+ *

-+ * Redstone wire in Vanilla also fails on point 3, though this is more of a -+ * quality-of-life issue than a lag issue. The recursive nature in which it -+ * updates, combined with the location-dependent order in which each wire -+ * updates its neighbors, makes the order in which neighbors of a wire network -+ * are updated incredibly inconsistent and seemingly random. -+ * -+ *

-+ * Alternate Current fixes each of these problems as follows. -+ * -+ *

-+ * 1. To make sure a wire calculates its power level as little as possible, we -+ * remove the recursive nature in which redstone wire updates in Vanilla. -+ * Instead, we build a network of connected wires, find those wires that receive -+ * redstone power from "outside" the network, and spread the power from there. -+ * This has a few advantages: -+ *
-+ * - Each wire checks for power from non-wire components at most once, and from -+ * nearby wires just twice. -+ *
-+ * - Each wire only sets its power level in the world once. This is important, -+ * because calls to Level.setBlock are even more expensive than calls to -+ * Level.getBlockState. -+ * -+ *

-+ * 2. There are 2 obvious ways in which we can reduce the number of block and -+ * shape updates. -+ *
-+ * - Get rid of the 18 redundant block updates and 16 redundant shape updates, -+ * so each wire only emits 24 block updates and 6 shape updates whenever it -+ * changes its power level. -+ *
-+ * - Only emit block updates and shape updates once a wire reaches its final -+ * power level, rather than at each intermediary stage. -+ *
-+ * For an individual wire, these two optimizations are the best you can do, but -+ * for an entire grid, you can do better! -+ * -+ *

-+ * Since we calculate the power of the entire network, sending block and shape -+ * updates to the wires in it is redundant. Removing those updates can reduce -+ * the number of block and shape updates by up to 20%. -+ * -+ *

-+ * 3. To make the order of block updates to neighbors of a network -+ * deterministic, the first thing we must do is to replace the location- -+ * dependent order in which a wire updates its neighbors. Instead, we base it on -+ * the direction of power flow. This part of the algorithm was heavily inspired -+ * by theosib's 'RedstoneWireTurbo', which you can read more about in theosib's -+ * comment on Mojira here -+ * or by checking out its implementation in carpet mod here. -+ * -+ *

-+ * The idea is to determine the direction of power flow through a wire based on -+ * the power it receives from neighboring wires. For example, if the only power -+ * a wire receives is from a neighboring wire to its west, it can be said that -+ * the direction of power flow through the wire is east. -+ * -+ *

-+ * We make the order of block updates to neighbors of a wire depend on what is -+ * determined to be the direction of power flow. This not only removes -+ * locationality entirely, it even removes directionality in a large number of -+ * cases. Unlike in 'RedstoneWireTurbo', however, I have decided to keep a -+ * directional element in ambiguous cases, rather than to introduce randomness, -+ * though this is trivial to change. -+ * -+ *

-+ * While this change fixes the block update order of individual wires, we must -+ * still address the overall block update order of a network. This turns out to -+ * be a simple fix, because of a change we made earlier: we search through the -+ * network for wires that receive power from outside it, and spread the power -+ * from there. If we make each wire transmit its power to neighboring wires in -+ * an order dependent on the direction of power flow, we end up with a -+ * non-locational and largely non-directional wire update order. -+ * -+ * @author Space Walker -+ */ -+public class WireHandler { -+ -+ public static class Directions { -+ -+ public static final Direction[] ALL = { Direction.WEST, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.DOWN, Direction.UP }; -+ public static final Direction[] HORIZONTAL = { Direction.WEST, Direction.NORTH, Direction.EAST, Direction.SOUTH }; -+ -+ // Indices for the arrays above. -+ // The cardinal directions are ordered clockwise. This allows -+ // for conversion between relative and absolute directions -+ // ('left' 'right' vs 'east' 'west') with simple arithmetic: -+ // If some Direction index 'iDir' is considered 'forward', then -+ // '(iDir + 1) & 0b11' is 'right', '(iDir + 2) & 0b11' is 'backward', etc. -+ public static final int WEST = 0b000; // 0 -+ public static final int NORTH = 0b001; // 1 -+ public static final int EAST = 0b010; // 2 -+ public static final int SOUTH = 0b011; // 3 -+ public static final int DOWN = 0b100; // 4 -+ public static final int UP = 0b101; // 5 -+ -+ public static int iOpposite(int iDir) { -+ return iDir ^ (0b10 >>> (iDir >>> 2)); -+ } -+ -+ public static int index(Direction dir) { -+ for (int i = 0; i < ALL.length; i++) { -+ if (dir == ALL[i]) { -+ return i; -+ } -+ } -+ -+ return -1; -+ } -+ } -+ -+ /** -+ * This conversion table takes in information about incoming flow, and outputs -+ * the determined outgoing flow. -+ * -+ *

-+ * The input is a 4 bit number that encodes the incoming flow. Each bit -+ * represents a cardinal direction, and when it is 'on', there is flow in that -+ * direction. -+ * -+ *

-+ * The output is a single Direction index, or -1 for ambiguous cases. -+ * -+ *

-+ * The outgoing flow is determined as follows: -+ * -+ *

-+ * If there is just 1 direction of incoming flow, that direction will be the -+ * direction of outgoing flow. -+ * -+ *

-+ * If there are 2 directions of incoming flow, and these directions are not each -+ * other's opposites, the direction that is 'more clockwise' will be the -+ * direction of outgoing flow. More precisely, the direction that is 1 clockwise -+ * turn from the other is picked. -+ * -+ *

-+ * If there are 3 directions of incoming flow, the two opposing directions -+ * cancel each other out, and the remaining direction will be the direction of -+ * outgoing flow. -+ * -+ *

-+ * In all other cases, the flow is completely ambiguous. -+ */ -+ static final int[] FLOW_IN_TO_FLOW_OUT = { -+ -1, // 0b0000: - -> x -+ Directions.WEST, // 0b0001: west -> west -+ Directions.NORTH, // 0b0010: north -> north -+ Directions.NORTH, // 0b0011: west/north -> north -+ Directions.EAST, // 0b0100: east -> east -+ -1, // 0b0101: west/east -> x -+ Directions.EAST, // 0b0110: north/east -> east -+ Directions.NORTH, // 0b0111: west/north/east -> north -+ Directions.SOUTH, // 0b1000: south -> south -+ Directions.WEST, // 0b1001: west/south -> west -+ -1, // 0b1010: north/south -> x -+ Directions.WEST, // 0b1011: west/north/south -> west -+ Directions.SOUTH, // 0b1100: east/south -> south -+ Directions.SOUTH, // 0b1101: west/east/south -> south -+ Directions.EAST, // 0b1110: north/east/south -> east -+ -1, // 0b1111: west/north/east/south -> x -+ }; -+ /** -+ * Update order of shape updates, matching that of Vanilla. -+ */ -+ static final int[] SHAPE_UPDATE_ORDER = { Directions.WEST, Directions.EAST, Directions.NORTH, Directions.SOUTH, Directions.DOWN, Directions.UP }; -+ -+ private static final int POWER_MIN = Redstone.SIGNAL_MIN; -+ private static final int POWER_MAX = Redstone.SIGNAL_MAX; -+ private static final int POWER_STEP = 1; -+ -+ // If Vanilla will ever multi-thread the ticking of levels, there should -+ // be only one WireHandler per level, in case redstone updates in multiple -+ // levels at the same time. There are already mods that add multi-threading -+ // as well. -+ private final ServerLevel level; -+ -+ /** Map of wires and neighboring blocks. */ -+ private final Long2ObjectMap nodes; -+ /** Queue for the breadth-first search through the network. */ -+ private final Queue search; -+ /** Queue of updates to wires and neighboring blocks. */ -+ private final Queue updates; -+ -+ private final NeighborUpdater neighborUpdater; -+ -+ // Rather than creating new nodes every time a network is updated we keep -+ // a cache of nodes that can be re-used. -+ private Node[] nodeCache; -+ private int nodeCount; -+ -+ /** Is this WireHandler currently working through the update queue? */ -+ private boolean updating; -+ /** The update order currently in use. */ -+ private UpdateOrder updateOrder; -+ -+ public WireHandler(ServerLevel level) { -+ this.level = level; -+ -+ this.nodes = new Long2ObjectOpenHashMap<>(); -+ this.search = new SimpleQueue(); -+ this.updates = new PriorityQueue(); -+ -+ this.neighborUpdater = new InstantNeighborUpdater(this.level); -+ -+ this.nodeCache = new Node[16]; -+ this.fillNodeCache(0, 16); -+ } -+ -+ private Node getOrAddNode(BlockPos pos) { -+ // just pass in null, then the state will only be retrieved -+ // if there is no node as this position yet -+ return getOrAddNode(pos, null); -+ } -+ -+ /** -+ * Retrieve the {@link alternate.current.wire.Node Node} that represents the -+ * block at the given position in the level. -+ */ -+ private Node getOrAddNode(BlockPos pos, BlockState state) { -+ return nodes.compute(pos.asLong(), (key, node) -> { -+ if (node == null) { -+ // If there is not yet a node at this position, retrieve and -+ // update one from the cache. -+ return getNextNode(pos, state != null ? state : level.getBlockState(pos)); -+ } -+ if (node.invalid) { -+ return revalidateNode(node); -+ } -+ -+ return node; -+ }); -+ } -+ -+ /** -+ * Remove and return the {@link alternate.current.wire.Node Node} at the given -+ * position. -+ */ -+ private Node removeNode(BlockPos pos) { -+ return nodes.remove(pos.asLong()); -+ } -+ -+ /** -+ * Return a node that represents the given position and block state. If it is a -+ * wire, then create a new {@link alternate.current.wire.WireNode WireNode}. -+ * Otherwise, grab the next {@link alternate.current.wire.Node Node} from the -+ * cache and update it. -+ */ -+ private Node getNextNode(BlockPos pos, BlockState state) { -+ return state.is(Blocks.REDSTONE_WIRE) ? new WireNode(level, pos, state) : getNextNode().set(pos, state, true); -+ } -+ -+ /** -+ * Grab the first unused node from the cache. If all of the cache is already in -+ * use, increase it in size first. -+ */ -+ private Node getNextNode() { -+ if (nodeCount == nodeCache.length) { -+ increaseNodeCache(); -+ } -+ -+ return nodeCache[nodeCount++]; -+ } -+ -+ private void increaseNodeCache() { -+ Node[] oldCache = nodeCache; -+ nodeCache = new Node[oldCache.length << 1]; -+ -+ for (int index = 0; index < oldCache.length; index++) { -+ nodeCache[index] = oldCache[index]; -+ } -+ -+ fillNodeCache(oldCache.length, nodeCache.length); -+ } -+ -+ private void fillNodeCache(int start, int end) { -+ for (int index = start; index < end; index++) { -+ nodeCache[index] = new Node(level); -+ } -+ } -+ -+ /** -+ * Try to revalidate the given node by looking at the block state that is -+ * occupying its position. If the given node is a wire but the block state is -+ * not, or vice versa, a new node must be created/grabbed from the cache. -+ * Otherwise, the node can be quickly revalidated with the new block state. -+ */ -+ private Node revalidateNode(Node node) { -+ if (!node.invalid) { -+ return node; -+ } -+ -+ BlockPos pos = node.pos; -+ BlockState state = level.getBlockState(pos); -+ -+ boolean wasWire = node.isWire(); -+ boolean isWire = state.is(Blocks.REDSTONE_WIRE); -+ -+ if (wasWire != isWire) { -+ return getNextNode(pos, state); -+ } -+ -+ node.invalid = false; -+ -+ if (isWire) { -+ // No need to update the block state of this wire - it will grab -+ // the current block state just before setting power anyway. -+ WireNode wire = node.asWire(); -+ -+ wire.root = false; -+ wire.discovered = false; -+ wire.searched = false; -+ } else { -+ node.set(pos, state, false); -+ } -+ -+ return node; -+ } -+ -+ /** -+ * Retrieve the neighbor of a node in the given direction and create a link -+ * between the two nodes if they are not yet linked. This link makes accessing -+ * neighbors of a node signficantly faster. -+ */ -+ private Node getNeighbor(Node node, int iDir) { -+ Node neighbor = node.neighbors[iDir]; -+ -+ if (neighbor == null || neighbor.invalid) { -+ Direction dir = Directions.ALL[iDir]; -+ BlockPos pos = node.pos.relative(dir); -+ -+ Node oldNeighbor = neighbor; -+ neighbor = getOrAddNode(pos); -+ -+ if (neighbor != oldNeighbor) { -+ int iOpp = Directions.iOpposite(iDir); -+ -+ node.neighbors[iDir] = neighbor; -+ neighbor.neighbors[iOpp] = node; -+ } -+ } -+ -+ return neighbor; -+ } -+ -+ /** -+ * This method should be called whenever a wire receives a block update. -+ */ -+ public boolean onWireUpdated(BlockPos pos, BlockState state, Orientation orientation) { -+ Node node = getOrAddNode(pos, state); -+ -+ if (!node.isWire()) { -+ return false; // we should never get here -+ } -+ -+ WireNode wire = node.asWire(); -+ -+ invalidate(); -+ revalidateNode(wire); -+ findRoots(wire, orientation); -+ tryUpdate(); -+ -+ return true; -+ } -+ -+ /** -+ * This method should be called whenever a wire is placed. -+ */ -+ public void onWireAdded(BlockPos pos, BlockState state) { -+ Node node = getOrAddNode(pos, state); -+ -+ if (!node.isWire()) { -+ return; // we should never get here -+ } -+ -+ WireNode wire = node.asWire(); -+ wire.added = true; -+ -+ invalidate(); -+ revalidateNode(wire); -+ findRoot(wire); -+ tryUpdate(); -+ } -+ -+ /** -+ * This method should be called whenever a wire is removed. -+ */ -+ public void onWireRemoved(BlockPos pos, BlockState state) { -+ Node node = removeNode(pos); -+ WireNode wire; -+ -+ if (node == null || !node.isWire()) { -+ wire = new WireNode(level, pos, state); -+ } else { -+ wire = node.asWire(); -+ } -+ -+ wire.invalid = true; -+ wire.removed = true; -+ -+ // If these fields are set to 'true', the removal of this wire was part of -+ // already ongoing power changes, so we can exit early here. -+ if (updating && wire.shouldBreak) { -+ return; -+ } -+ -+ invalidate(); -+ revalidateNode(wire); -+ findRoot(wire); -+ tryUpdate(); -+ } -+ -+ /** -+ * The nodes map is a snapshot of the state of the world. It becomes invalid -+ * when power changes are carried out, since the block and shape updates can -+ * lead to block changes. If these block changes cause the network to be updated -+ * again every node must be invalidated, and revalidated before it is used -+ * again. This ensures the power calculations of the network are accurate. -+ */ -+ private void invalidate() { -+ if (updating && !nodes.isEmpty()) { -+ Iterator> it = Long2ObjectMaps.fastIterator(nodes); -+ -+ while (it.hasNext()) { -+ Entry entry = it.next(); -+ Node node = entry.getValue(); -+ -+ node.invalid = true; -+ } -+ } -+ -+ updateOrder = UpdateOrder.values()[level.paperConfig().misc.alternateCurrentUpdateOrder.ordinal()]; -+ } -+ -+ /** -+ * Look for wires at and around the given position that are in an invalid state -+ * and require power changes. These wires are called 'roots' because it is only -+ * when these wires change power level that neighboring wires must adjust as -+ * well. -+ * -+ *

-+ * While it is strictly only necessary to check the wire at the given position, -+ * if that wire is part of a network, it is beneficial to check its surroundings -+ * for other wires that require power changes. This is because a network can -+ * receive power at multiple points. Consider the following setup: -+ * -+ *

-+ * (top-down view, W = wire, L = lever, _ = air/other) -+ *
{@code _ _ W _ _ } -+ *
{@code _ W W W _ } -+ *
{@code W W L W W } -+ *
{@code _ W W W _ } -+ *
{@code _ _ W _ _ } -+ * -+ *

-+ * The lever powers four wires in the network at once. If this is identified -+ * correctly, the entire network can (un)power at once. While it is not -+ * practical to cover every possible situation where a network is (un)powered -+ * from multiple points at once, checking for common cases like the one -+ * described above is relatively straight-forward. -+ */ -+ private void findRoots(WireNode wire, Orientation orientation) { -+ // horizontal direction bias for update order purposes -+ int iDirBias = -1; -+ -+ if (orientation != null) { -+ Direction dir = orientation.getFront().getAxis().isHorizontal() -+ ? orientation.getFront() -+ : orientation.getUp(); -+ -+ iDirBias = Directions.index(dir); -+ } -+ -+ findRoot(wire, iDirBias); -+ -+ // If the wire at the given position is not in an invalid state -+ // we can exit early. -+ if (!wire.searched) { -+ return; -+ } -+ -+ if (orientation == null) { -+ // no neighborChanged orientation present, look around in all sides -+ for (int iDir : updateOrder.directNeighbors(wire.iFlowDir)) { -+ findRootsAround(wire, iDir); -+ } -+ } else { -+ // use the orientation from the neighborChanged update to look for roots only behind -+ findRootsAround(wire, Directions.index(orientation.getFront().getOpposite())); -+ } -+ } -+ -+ /** -+ * Look for wires around a neighbor of the given wire that require power changes. -+ */ -+ private void findRootsAround(WireNode wire, int iDir) { -+ Node node = getNeighbor(wire, iDir); -+ -+ if (node.isConductor() || node.isSignalSource()) { -+ for (int iSide : updateOrder.cardinalNeighbors(wire.iFlowDir)) { -+ Node neighbor = getNeighbor(node, iSide); -+ -+ if (neighbor.isWire()) { -+ findRoot(neighbor.asWire(), iSide); -+ } -+ } -+ } -+ } -+ -+ private void findRoot(WireNode wire) { -+ findRoot(wire, -1); -+ } -+ -+ /** -+ * Check if the given wire requires power changes. If it does, queue it for the -+ * breadth-first search as a root. -+ */ -+ private void findRoot(WireNode wire, int iDiscoveryDir) { -+ // Each wire only needs to be checked once. -+ if (wire.discovered) { -+ return; -+ } -+ -+ discover(wire); -+ findExternalPower(wire); -+ findPower(wire, false); -+ -+ if (needsUpdate(wire)) { -+ searchRoot(wire, iDiscoveryDir); -+ } -+ } -+ -+ /** -+ * Prepare the given wire for the breadth-first search. This means: -+ *
-+ * - Check if the wire should break. Rather than breaking the wire right away, -+ * its effects are integrated into the power calculations. -+ *
-+ * - Reset the virtual and external power. -+ *
-+ * - Find connections to neighboring wires. -+ */ -+ private void discover(WireNode wire) { -+ if (wire.discovered) { -+ return; -+ } -+ -+ wire.discovered = true; -+ wire.searched = false; -+ -+ if (!wire.removed && !wire.shouldBreak && !wire.state.canSurvive(level, wire.pos)) { -+ wire.shouldBreak = true; -+ } -+ -+ wire.virtualPower = wire.currentPower; -+ wire.externalPower = POWER_MIN - 1; -+ -+ wire.connections.set(this::getNeighbor); -+ } -+ -+ /** -+ * Determine the power level the given wire receives from the blocks around it. -+ * Power from non-wire components only needs to be computed if power from -+ * neighboring wires has decreased, so as to determine how low the power of the -+ * wire can fall. -+ */ -+ private void findPower(WireNode wire, boolean ignoreSearched) { -+ // As wire power is (re-)computed, flow information must be reset. -+ wire.virtualPower = wire.externalPower; -+ wire.flowIn = 0; -+ -+ // If the wire is removed or going to break, its power level should always be -+ // the minimum value. This is because it (effectively) no longer exists, so -+ // cannot provide any power to neighboring wires. -+ if (wire.removed || wire.shouldBreak) { -+ return; -+ } -+ -+ // Power received from neighboring wires will never exceed POWER_MAX - -+ // POWER_STEP, so if the external power is already larger than or equal to -+ // that, there is no need to check for power from neighboring wires. -+ if (wire.externalPower < (POWER_MAX - POWER_STEP)) { -+ findWirePower(wire, ignoreSearched); -+ } -+ } -+ -+ /** -+ * Determine the power the given wire receives from connected neighboring wires -+ * and update the virtual power accordingly. -+ */ -+ private void findWirePower(WireNode wire, boolean ignoreSearched) { -+ wire.connections.forEach(connection -> { -+ if (!connection.accept) { -+ return; -+ } -+ -+ WireNode neighbor = connection.wire; -+ -+ if (!ignoreSearched || !neighbor.searched) { -+ int power = Math.max(POWER_MIN, neighbor.virtualPower - POWER_STEP); -+ int iOpp = Directions.iOpposite(connection.iDir); -+ -+ wire.offerPower(power, iOpp); -+ } -+ }); -+ } -+ -+ /** -+ * Determine the redstone signal the given wire receives from non-wire -+ * components and update the virtual power accordingly. -+ */ -+ private void findExternalPower(WireNode wire) { -+ // If the wire is removed or going to break, its power level should always be -+ // the minimum value. Thus external power need not be computed. -+ // In other cases external power need only be computed once. -+ if (wire.removed || wire.shouldBreak || wire.externalPower >= POWER_MIN) { -+ return; -+ } -+ -+ wire.externalPower = getExternalPower(wire); -+ -+ if (wire.externalPower > wire.virtualPower) { -+ wire.virtualPower = wire.externalPower; -+ } -+ } -+ -+ /** -+ * Determine the redstone signal the given wire receives from non-wire -+ * components. -+ */ -+ private int getExternalPower(WireNode wire) { -+ int power = POWER_MIN; -+ -+ for (int iDir = 0; iDir < Directions.ALL.length; iDir++) { -+ Node neighbor = getNeighbor(wire, iDir); -+ -+ // Power from wires is handled separately. -+ if (neighbor.isWire()) { -+ continue; -+ } -+ -+ // Since 1.16 there is a block that is both a conductor and a signal -+ // source: the target block! -+ if (neighbor.isConductor()) { -+ power = Math.max(power, getDirectSignalTo(wire, neighbor)); -+ } -+ if (neighbor.isSignalSource()) { -+ power = Math.max(power, neighbor.state.getSignal(level, neighbor.pos, Directions.ALL[iDir])); -+ } -+ -+ if (power >= POWER_MAX) { -+ return POWER_MAX; -+ } -+ } -+ -+ return power; -+ } -+ -+ /** -+ * Determine the direct signal the given wire receives from neighboring blocks -+ * through the given conductor node. -+ */ -+ private int getDirectSignalTo(WireNode wire, Node node) { -+ int power = POWER_MIN; -+ -+ for (int iDir = 0; iDir < Directions.ALL.length; iDir++) { -+ Node neighbor = getNeighbor(node, iDir); -+ -+ if (neighbor.isSignalSource()) { -+ power = Math.max(power, neighbor.state.getDirectSignal(level, neighbor.pos, Directions.ALL[iDir])); -+ -+ if (power >= POWER_MAX) { -+ return POWER_MAX; -+ } -+ } -+ } -+ -+ return power; -+ } -+ -+ /** -+ * Check if the given wire needs to update its state in the world. -+ */ -+ private boolean needsUpdate(WireNode wire) { -+ return wire.removed || wire.shouldBreak || wire.virtualPower != wire.currentPower; -+ } -+ -+ /** -+ * Queue the given wire for the breadth-first search as a root. -+ */ -+ private void searchRoot(WireNode wire, int iBackupFlowDir) { -+ if (wire.connections.iFlowDir >= 0) { -+ // power flow direction takes precedent -+ iBackupFlowDir = wire.connections.iFlowDir; -+ } else if (iBackupFlowDir < 0) { -+ // use default value if none is given -+ iBackupFlowDir = 0; -+ } -+ -+ search(wire, true, iBackupFlowDir); -+ } -+ -+ /** -+ * Queue the given wire for the breadth-first search and set a backup flow -+ * direction. -+ */ -+ private void search(WireNode wire, boolean root, int iBackupFlowDir) { -+ search.offer(wire); -+ -+ wire.root = root; -+ wire.searched = true; -+ // Normally the flow is not set until the power level is updated. However, -+ // in networks with multiple power sources the update order between them -+ // depends on which was discovered first. To make this less prone to -+ // directionality, each wire node is given a 'backup' flow. For roots, this -+ // is the determined flow of their connections. For non-roots this is the -+ // direction from which they were discovered. -+ wire.iFlowDir = iBackupFlowDir; -+ } -+ -+ private void tryUpdate() { -+ if (!search.isEmpty()) { -+ update(); -+ } -+ if (!updating) { -+ nodes.clear(); -+ nodeCount = 0; -+ } -+ } -+ -+ /** -+ * Update the network and neighboring blocks. This is done in 3 steps. -+ * -+ *

-+ * 1. Search through the network -+ *
-+ * Conduct a breadth-first search around the roots to find wires that are in an -+ * invalid state and need power changes. -+ * -+ *

-+ * 2. Depower the network -+ *
-+ * Depower all wires in the network. This allows power to be spread most -+ * efficiently. -+ * -+ *

-+ * 3. Power the network -+ *
-+ * Work through the update queue, setting the new power level of each wire and -+ * updating neighboring blocks. After a wire has updated its power level, it -+ * will emit shape updates and queue updates for neighboring wires and blocks. -+ */ -+ private void update() { -+ // Search through the network for wires that need power changes. This includes -+ // the roots as well as any wires that will be affected by power changes to -+ // those roots. -+ searchNetwork(); -+ -+ // Depower all the wires in the network. -+ depowerNetwork(); -+ -+ // Bring each wire up to its new power level and update neighboring blocks. -+ try { -+ powerNetwork(); -+ } catch (Throwable t) { -+ // If anything goes wrong while carrying out power changes, this field must -+ // be reset to 'false', or the wire handler will be locked out of carrying -+ // out power changes until the world is reloaded. -+ updating = false; -+ -+ throw t; -+ } -+ } -+ -+ /** -+ * Search through the network for wires that are in an invalid state and need -+ * power changes. These wires are added to the end of the queue, so that their -+ * neighbors can be searched next. -+ */ -+ private void searchNetwork() { -+ for (WireNode wire : search) { -+ // The order in which wires are searched will influence the order in -+ // which they update their power levels. -+ wire.connections.forEach(connection -> { -+ if (!connection.offer) { -+ return; -+ } -+ -+ WireNode neighbor = connection.wire; -+ -+ if (neighbor.searched) { -+ return; -+ } -+ -+ discover(neighbor); -+ findPower(neighbor, false); -+ -+ // If power from neighboring wires has decreased, check for power -+ // from non-wire components so as to determine how low power can -+ // fall. -+ if (neighbor.virtualPower < neighbor.currentPower) { -+ findExternalPower(neighbor); -+ } -+ -+ if (needsUpdate(neighbor)) { -+ search(neighbor, false, connection.iDir); -+ } -+ }, updateOrder, wire.iFlowDir); -+ } -+ } -+ -+ /** -+ * Depower all wires in the network so that power can be spread from the power -+ * sources. -+ */ -+ private void depowerNetwork() { -+ while (!search.isEmpty()) { -+ WireNode wire = search.poll(); -+ findPower(wire, true); -+ -+ if (wire.root || wire.removed || wire.shouldBreak || wire.virtualPower > POWER_MIN) { -+ queueWire(wire); -+ } else { -+ // Wires that do not receive any power do not queue power changes -+ // until they are offered power from a neighboring wire. To ensure -+ // that they accept any power from neighboring wires and thus queue -+ // their power changes, their virtual power is set to below the -+ // minimum. -+ wire.virtualPower--; -+ } -+ } -+ } -+ -+ /** -+ * Work through the update queue, setting the new power level of each wire, then -+ * queueing updates to connected wires and neighboring blocks. -+ */ -+ private void powerNetwork() { -+ // If an instantaneous update chain causes updates to another network -+ // (or the same network in another place), new power changes will be -+ // integrated into the already ongoing power queue, so we can exit early -+ // here. -+ if (updating) { -+ return; -+ } -+ -+ updating = true; -+ -+ while (!updates.isEmpty()) { -+ Node node = updates.poll(); -+ -+ if (node.isWire()) { -+ WireNode wire = node.asWire(); -+ -+ if (!needsUpdate(wire)) { -+ continue; -+ } -+ -+ findPowerFlow(wire); -+ transmitPower(wire); -+ -+ if (wire.setPower()) { -+ queueNeighbors(wire); -+ -+ // If the wire was newly placed or removed, shape updates have -+ // already been emitted. However, unlike before 1.19, neighbor -+ // updates are now queued, so to preserve behavior parity with -+ // previous versions, we emit extra shape updates here to -+ // notify neighboring observers. -+ updateNeighborShapes(wire); -+ } -+ } else { -+ WireNode neighborWire = node.neighborWire; -+ -+ if (neighborWire != null) { -+ BlockPos neighborPos = neighborWire.pos; -+ Block neighborBlock = neighborWire.state.getBlock(); -+ -+ updateBlock(node, neighborPos, neighborBlock); -+ } -+ } -+ } -+ -+ updating = false; -+ } -+ -+ /** -+ * Use the information of incoming power flow to determine the direction of -+ * power flow through this wire. If that flow is ambiguous, try to use a flow -+ * direction based on connections to neighboring wires. If that is also -+ * ambiguous, use the backup value that was set when the wire was first added to -+ * the network. -+ */ -+ private void findPowerFlow(WireNode wire) { -+ int flow = FLOW_IN_TO_FLOW_OUT[wire.flowIn]; -+ -+ if (flow >= 0) { -+ wire.iFlowDir = flow; -+ } else if (wire.connections.iFlowDir >= 0) { -+ wire.iFlowDir = wire.connections.iFlowDir; -+ } -+ } -+ -+ /** -+ * Transmit power from the given wire to neighboring wires and queue updates to -+ * those wires. -+ */ -+ private void transmitPower(WireNode wire) { -+ wire.connections.forEach(connection -> { -+ if (!connection.offer) { -+ return; -+ } -+ -+ WireNode neighbor = connection.wire; -+ -+ int power = Math.max(POWER_MIN, wire.virtualPower - POWER_STEP); -+ int iDir = connection.iDir; -+ -+ if (neighbor.offerPower(power, iDir)) { -+ queueWire(neighbor); -+ } -+ }, updateOrder, wire.iFlowDir); -+ } -+ -+ /** -+ * Emit shape updates around the given wire. -+ */ -+ private void updateNeighborShapes(WireNode wire) { -+ BlockPos wirePos = wire.pos; -+ BlockState wireState = wire.state; -+ -+ for (int iDir : SHAPE_UPDATE_ORDER) { -+ Node neighbor = getNeighbor(wire, iDir); -+ -+ // Shape updates to redstone wire are very expensive, and should never happen -+ // as a result of power changes anyway, while shape updates to air do nothing. -+ // The current block state at this position *could* be wrong, but if you somehow -+ // manage to place a block where air used to be during the execution of a shape -+ // update I am very impressed and you deserve to have some broken behavior. -+ if (!neighbor.isWire() && !neighbor.state.isAir()) { -+ int iOpp = Directions.iOpposite(iDir); -+ Direction opp = Directions.ALL[iOpp]; -+ -+ updateShape(neighbor, opp, wirePos, wireState); -+ } -+ } -+ } -+ -+ private void updateShape(Node node, Direction dir, BlockPos neighborPos, BlockState neighborState) { -+ neighborUpdater.shapeUpdate(dir, neighborState, node.pos, neighborPos, Block.UPDATE_CLIENTS, 512); -+ } -+ -+ /** -+ * Queue block updates to nodes around the given wire. -+ */ -+ private void queueNeighbors(WireNode wire) { -+ updateOrder.forEachNeighbor(this::getNeighbor, wire, wire.iFlowDir, neighbor -> queueNeighbor(neighbor, wire)); -+ } -+ -+ /** -+ * Queue the given node for an update from the given neighboring wire. -+ */ -+ private void queueNeighbor(Node node, WireNode neighborWire) { -+ // Updates to wires are queued when power is transmitted. -+ // While this check makes sure wires in the network are not given block -+ // updates, it also prevents block updates to wires in neighboring networks. -+ // While this should not make a difference in theory, in practice, it is -+ // possible to force a network into an invalid state without updating it, even -+ // if it is relatively obscure. -+ // While I was willing to make this compromise in return for some significant -+ // performance gains in certain setups, if you are not, you can add all the -+ // positions of the network to a set and filter out block updates to wires in -+ // the network that way. -+ // Block updates to air do nothing, so those are skipped as well. -+ // The current block state at this position *could* be wrong, but if you somehow -+ // manage to place a block where air used to be during the execution of a block -+ // update I am very impressed and you deserve to have some broken behavior. -+ if (!node.isWire() && !node.state.isAir()) { -+ node.neighborWire = neighborWire; -+ updates.offer(node); -+ } -+ } -+ -+ /** -+ * Queue the given wire for a power change. If the wire does not need a power -+ * change (perhaps because its power has already changed), transmit power to -+ * neighboring wires. -+ */ -+ private void queueWire(WireNode wire) { -+ if (needsUpdate(wire)) { -+ updates.offer(wire); -+ } else { -+ findPowerFlow(wire); -+ transmitPower(wire); -+ } -+ } -+ -+ /** -+ * Emit a block update to the given node. -+ */ -+ private void updateBlock(Node node, BlockPos neighborPos, Block neighborBlock) { -+ // redstone wire is the only block that uses the neighborChanged orientation -+ // so leaving it as null should not be an issue -+ neighborUpdater.neighborChanged(node.pos, neighborBlock, null); -+ } -+ -+ @FunctionalInterface -+ public static interface NodeProvider { -+ -+ public Node getNeighbor(Node node, int iDir); -+ -+ } -+} -diff --git a/src/main/java/alternate/current/wire/WireNode.java b/src/main/java/alternate/current/wire/WireNode.java -new file mode 100644 -index 0000000000000000000000000000000000000000..298076a0db4e6ee6e4775ac43bf749d9f5689bdb ---- /dev/null -+++ b/src/main/java/alternate/current/wire/WireNode.java -@@ -0,0 +1,122 @@ -+package alternate.current.wire; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.util.Mth; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.block.RedStoneWireBlock; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.redstone.Redstone; -+ -+/** -+ * A WireNode is a Node that represents a wire in the world. It stores all the -+ * information about the wire that the WireHandler needs to calculate power -+ * changes. -+ * -+ * @author Space Walker -+ */ -+public class WireNode extends Node { -+ -+ final WireConnectionManager connections; -+ -+ /** The power level this wire currently holds in the world. */ -+ int currentPower; -+ /** -+ * While calculating power changes for a network, this field is used to keep -+ * track of the power level this wire should have. -+ */ -+ int virtualPower; -+ /** The power level received from non-wire components. */ -+ int externalPower; -+ /** -+ * A 4-bit number that keeps track of the power flow of the wires that give this -+ * wire its power level. -+ */ -+ int flowIn; -+ /** The direction of power flow, based on the incoming flow. */ -+ int iFlowDir; -+ boolean added; -+ boolean removed; -+ boolean shouldBreak; -+ boolean root; -+ boolean discovered; -+ boolean searched; -+ -+ /** The next wire in the simple queue. */ -+ WireNode next_wire; -+ -+ WireNode(ServerLevel level, BlockPos pos, BlockState state) { -+ super(level); -+ -+ this.pos = pos.immutable(); -+ this.state = state; -+ -+ this.connections = new WireConnectionManager(this); -+ -+ this.virtualPower = this.currentPower = this.state.getValue(RedStoneWireBlock.POWER); -+ this.priority = priority(); -+ } -+ -+ @Override -+ Node set(BlockPos pos, BlockState state, boolean clearNeighbors) { -+ throw new UnsupportedOperationException("Cannot update a WireNode!"); -+ } -+ -+ @Override -+ int priority() { -+ return Mth.clamp(virtualPower, Redstone.SIGNAL_MIN, Redstone.SIGNAL_MAX); -+ } -+ -+ @Override -+ public boolean isWire() { -+ return true; -+ } -+ -+ @Override -+ public WireNode asWire() { -+ return this; -+ } -+ -+ boolean offerPower(int power, int iDir) { -+ if (removed || shouldBreak) { -+ return false; -+ } -+ if (power == virtualPower) { -+ flowIn |= (1 << iDir); -+ return false; -+ } -+ if (power > virtualPower) { -+ virtualPower = power; -+ flowIn = (1 << iDir); -+ -+ return true; -+ } -+ -+ return false; -+ } -+ -+ boolean setPower() { -+ if (removed) { -+ return true; -+ } -+ -+ state = level.getBlockState(pos); -+ -+ if (!state.is(Blocks.REDSTONE_WIRE)) { -+ return false; // we should never get here -+ } -+ -+ if (shouldBreak) { -+ Block.dropResources(state, level, pos); -+ level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_CLIENTS); -+ -+ return true; -+ } -+ -+ currentPower = LevelHelper.doRedstoneEvent(level, pos, currentPower, Mth.clamp(virtualPower, Redstone.SIGNAL_MIN, Redstone.SIGNAL_MAX));; -+ state = state.setValue(RedStoneWireBlock.POWER, currentPower); -+ -+ return LevelHelper.setWireState(level, pos, state, added); -+ } -+} -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 657d8af7cc104962ee46ad1a7dc88b13c24262db..585e2b43a0326f0b81597fa1234d3c67c76af550 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -230,6 +230,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - public final UUID uuid; - public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent - public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent -+ private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current) - - public LevelChunk getChunkIfLoaded(int x, int z) { - return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately -@@ -2655,6 +2656,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - return this.chunkSource.getGenerator().getSeaLevel(); - } - -+ // Paper start - optimize redstone (Alternate Current) -+ @Override -+ public alternate.current.wire.WireHandler getWireHandler() { -+ return wireHandler; -+ } -+ // Paper end - optimize redstone (Alternate Current) -+ - private final class EntityCallbacks implements LevelCallback { - - EntityCallbacks() {} -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 3f69dfe877a6c3a362a28c29f556b7b9b2ad19b0..0de2b79481352b52438dde284262019b29949ad8 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -2015,6 +2015,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - - public abstract FuelValues fuelValues(); - -+ // Paper start - optimize redstone (Alternate Current) -+ public alternate.current.wire.WireHandler getWireHandler() { -+ // This method is overridden in ServerLevel. -+ // Since Paper is a server platform there is no risk -+ // of this implementation being called. It is here -+ // only so this method can be called without casting -+ // an instance of Level to ServerLevel. -+ return null; -+ } -+ // Paper end - optimize redstone (Alternate Current) -+ - public static enum ExplosionInteraction implements StringRepresentable { - - NONE("none"), BLOCK("block"), MOB("mob"), TNT("tnt"), TRIGGER("trigger"), STANDARD("standard"); // CraftBukkit - Add STANDARD which will always use Explosion.Effect.DESTROY -diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java -index 09b8f5335cb7651d90f4d1ca61b2ec5aa324e443..21f2c61023fadcce30452a02f067cd5d87e5d8dc 100644 ---- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java -@@ -290,7 +290,7 @@ public class RedStoneWireBlock extends Block { - return floor.isFaceSturdy(world, pos, Direction.UP) || floor.is(Blocks.HOPPER); - } - -- // Paper start - Optimize redstone -+ // Paper start - Optimize redstone (Eigencraft) - // The bulk of the new functionality is found in RedstoneWireTurbo.java - com.destroystokyo.paper.util.RedstoneWireTurbo turbo = new com.destroystokyo.paper.util.RedstoneWireTurbo(this); - -@@ -372,7 +372,13 @@ public class RedStoneWireBlock extends Block { - @Override - protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) { - if (!oldState.is(state.getBlock()) && !world.isClientSide) { -- this.updateSurroundingRedstone(world, pos, state, null, true); // Paper - Optimize redstone -+ // Paper start - optimize redstone - replace call to updatePowerStrength -+ if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { -+ world.getWireHandler().onWireAdded(pos, state); // Alternate Current -+ } else { -+ this.updateSurroundingRedstone(world, pos, state, null, true); // Vanilla/Eigencraft -+ } -+ // Paper end - - for (Direction direction : Direction.Plane.VERTICAL) { - world.updateNeighborsAt(pos.relative(direction), this); -@@ -391,7 +397,12 @@ public class RedStoneWireBlock extends Block { - world.updateNeighborsAt(pos.relative(direction), this); - } - -- this.updateSurroundingRedstone(world, pos, state, null, false); // Paper - Optimize redstone -+ // Paper start - optimize redstone - replace call to updatePowerStrength -+ if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { -+ world.getWireHandler().onWireRemoved(pos, state); // Alternate Current -+ } else { -+ this.updateSurroundingRedstone(world, pos, state, null, false); // Vanilla/Eigencraft -+ } - this.updateNeighborsOfNeighboringWires(world, pos); - } - } -@@ -415,9 +426,15 @@ public class RedStoneWireBlock extends Block { - @Override - protected void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, @Nullable Orientation wireOrientation, boolean notify) { - if (!world.isClientSide) { -+ // Paper start - optimize redstone (Alternate Current) -+ // Alternate Current handles breaking of redstone wires in the WireHandler. -+ if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { -+ world.getWireHandler().onWireUpdated(pos, state, wireOrientation); -+ } else -+ // Paper end - optimize redstone (Alternate Current) - if (sourceBlock != this || !useExperimentalEvaluator(world)) { - if (state.canSurvive(world, pos)) { -- this.updateSurroundingRedstone(world, pos, state, wireOrientation, false); // Paper - Optimize redstone -+ this.updateSurroundingRedstone(world, pos, state, wireOrientation, false); // Paper - Optimize redstone (Eigencraft) - } else { - dropResources(state, world, pos); - world.removeBlock(pos, false); -diff --git a/src/main/java/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java b/src/main/java/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java -index 4a9dc307106687bec084244c0a76e3e30f244fe2..8342dd636531729a187aff1bd69878d7aef9d3eb 100644 ---- a/src/main/java/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java -+++ b/src/main/java/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java -@@ -17,6 +17,11 @@ public class ExperimentalRedstoneUtils { - if (up != null) { - orientation = orientation.withFront(up); - } -+ // Paper start - Optimize redstone (Alternate Current) - use default front instead of random -+ else if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { -+ orientation = orientation.withFront(Direction.WEST); -+ } -+ // Paper end - Optimize redstone (Alternate Current) - - return orientation; - } else { diff --git a/patches/unapplied/server/1066-Fix-incorrect-invulnerability-damage-reduction.patch b/patches/unapplied/server/1066-Fix-incorrect-invulnerability-damage-reduction.patch deleted file mode 100644 index 3b3dd3d144..0000000000 --- a/patches/unapplied/server/1066-Fix-incorrect-invulnerability-damage-reduction.patch +++ /dev/null @@ -1,115 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Bjarne Koll -Date: Mon, 11 Nov 2024 21:35:27 +0100 -Subject: [PATCH] Fix incorrect invulnerability damage reduction - -Fixes incorrect spigot handling of the invulnerability damage -reduction applied when an already invulnerable entity is damaged with a -larger damage amount than the initial damage. -Vanilla still damages entities even if invulnerable if the damage to be -applied is larger than the previous damage taken. In that case, vanilla -applies the difference between the previous damage taken and the -proposed damage. - -Spigot's damage modifier API takes over the computation of damage -reducing effects, however spigot invokes this handling with the initial -damage before computing the difference to the previous damage amount. -This leads to the reduction values to generally be larger than expected, -as they are computed on the not-yet-reduced value. -Spigot applies these reductions after calling the EntityDamageEvent and -*then* subtracts the previous damage point, leading to the final damage -amount being smaller than expected. - -This patch cannot simply call the EntityDamageEvent with the reduced -damage, as that would lead to EntityDamageEvent#getDamage() returning -the already reduced damage, which breaks its method contract. -Instead, this patch makes use of the DamageModifier API, implementing -the last-damage-reduction as a DamageModifier. - -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 6a3a8f0466998409a01223bc0c16d92b96e50118..51f913a495e7fda7e0e72439c6d7cc9607bd4af8 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -1505,12 +1505,12 @@ public abstract class LivingEntity extends Entity implements Attackable { - } - - // Paper start - only call damage event when actuallyHurt will be called - move call logic down -- event = this.handleEntityDamage(source, amount); -+ event = this.handleEntityDamage(source, amount, this.lastHurt); // Paper - fix invulnerability reduction in EntityDamageEvent - pass lastDamage reduction - amount = computeAmountFromEntityDamageEvent(event); - // Paper end - only call damage event when actuallyHurt will be called - move call logic down - - // CraftBukkit start -- if (!this.actuallyHurt(world, source, (float) event.getFinalDamage() - this.lastHurt, event)) { -+ if (!this.actuallyHurt(world, source, (float) event.getFinalDamage(), event)) { // Paper - fix invulnerability reduction in EntityDamageEvent - no longer subtract lastHurt, that is part of the damage event calc now - return false; - } - if (this instanceof ServerPlayer && event.getDamage() == 0 && originalAmount == 0) return false; // Paper - revert to vanilla damage - players are not affected by damage that is 0 - skip damage if the vanilla damage is 0 and was not modified by plugins in the event. -@@ -1519,7 +1519,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - flag1 = false; - } else { - // Paper start - only call damage event when actuallyHurt will be called - move call logic down -- event = this.handleEntityDamage(source, amount); -+ event = this.handleEntityDamage(source, amount, 0); // Paper - fix invulnerability reduction in EntityDamageEvent - pass lastDamage reduction (none in this branch) - amount = computeAmountFromEntityDamageEvent(event); - // Paper end - only call damage event when actuallyHurt will be called - move call logic down - // CraftBukkit start -@@ -2322,8 +2322,19 @@ public abstract class LivingEntity extends Entity implements Attackable { - } - - // CraftBukkit start -- private EntityDamageEvent handleEntityDamage(final DamageSource damagesource, float f) { -+ private EntityDamageEvent handleEntityDamage(final DamageSource damagesource, float f, final float invulnerabilityRelatedLastDamage) { // Paper - fix invulnerability reduction in EntityDamageEvent - float originalDamage = f; -+ // Paper start - fix invulnerability reduction in EntityDamageEvent -+ final com.google.common.base.Function invulnerabilityReductionEquation = d -> { -+ if (invulnerabilityRelatedLastDamage == 0) return 0D; // no last damage, no reduction -+ // last damage existed, this means the reduction *technically* is (new damage - last damage). -+ // If the event damage was changed to something less than invul damage, hard lock it at 0. -+ if (d < invulnerabilityRelatedLastDamage) return 0D; -+ return (double) -invulnerabilityRelatedLastDamage; -+ }; -+ final float originalInvulnerabilityReduction = invulnerabilityReductionEquation.apply((double) f).floatValue(); -+ f += originalInvulnerabilityReduction; -+ // Paper end - fix invulnerability reduction in EntityDamageEvent - - com.google.common.base.Function freezing = new com.google.common.base.Function() { - @Override -@@ -2400,7 +2411,12 @@ public abstract class LivingEntity extends Entity implements Attackable { - }; - float absorptionModifier = absorption.apply((double) f).floatValue(); - -- return CraftEventFactory.handleLivingEntityDamageEvent(this, damagesource, originalDamage, freezingModifier, hardHatModifier, blockingModifier, armorModifier, resistanceModifier, magicModifier, absorptionModifier, freezing, hardHat, blocking, armor, resistance, magic, absorption); -+ // Paper start - fix invulnerability reduction in EntityDamageEvent -+ return CraftEventFactory.handleLivingEntityDamageEvent(this, damagesource, originalDamage, freezingModifier, hardHatModifier, blockingModifier, armorModifier, resistanceModifier, magicModifier, absorptionModifier, freezing, hardHat, blocking, armor, resistance, magic, absorption, (damageModifierDoubleMap, damageModifierFunctionMap) -> { -+ damageModifierFunctionMap.put(DamageModifier.INVULNERABILITY_REDUCTION, invulnerabilityReductionEquation); -+ damageModifierDoubleMap.put(DamageModifier.INVULNERABILITY_REDUCTION, (double) originalInvulnerabilityReduction); -+ }); -+ // Paper end - fix invulnerability reduction in EntityDamageEvent - } - - protected boolean actuallyHurt(ServerLevel worldserver, final DamageSource damagesource, float f, final EntityDamageEvent event) { // void -> boolean, add final -diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -index deba03eb37012c638e08e20cd1c98e9db190c790..e37aaf77f94b97b736cc20ef070cefdff0400188 100644 ---- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -+++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -@@ -1218,6 +1218,11 @@ public class CraftEventFactory { - private static final Function ZERO = Functions.constant(-0.0); - - public static EntityDamageEvent handleLivingEntityDamageEvent(Entity damagee, DamageSource source, double rawDamage, double freezingModifier, double hardHatModifier, double blockingModifier, double armorModifier, double resistanceModifier, double magicModifier, double absorptionModifier, Function freezing, Function hardHat, Function blocking, Function armor, Function resistance, Function magic, Function absorption) { -+ // Paper start - fix invulnerability reduction in EntityDamageEvent -+ return handleLivingEntityDamageEvent(damagee, source, rawDamage, freezingModifier, hardHatModifier, blockingModifier, armorModifier, resistanceModifier, magicModifier, absorptionModifier, freezing, hardHat, blocking, armor, resistance, magic, absorption, null); -+ } -+ public static EntityDamageEvent handleLivingEntityDamageEvent(Entity damagee, DamageSource source, double rawDamage, double freezingModifier, double hardHatModifier, double blockingModifier, double armorModifier, double resistanceModifier, double magicModifier, double absorptionModifier, Function freezing, Function hardHat, Function blocking, Function armor, Function resistance, Function magic, Function absorption, java.util.function.BiConsumer, Map>> callback) { -+ // Paper end - fix invulnerability reduction in EntityDamageEvent - Map modifiers = new EnumMap<>(DamageModifier.class); - Map> modifierFunctions = new EnumMap<>(DamageModifier.class); - modifiers.put(DamageModifier.BASE, rawDamage); -@@ -1242,6 +1247,7 @@ public class CraftEventFactory { - modifierFunctions.put(DamageModifier.MAGIC, magic); - modifiers.put(DamageModifier.ABSORPTION, absorptionModifier); - modifierFunctions.put(DamageModifier.ABSORPTION, absorption); -+ if (callback != null) callback.accept(modifiers, modifierFunctions); // Paper - fix invulnerability reduction in EntityDamageEvent - return CraftEventFactory.handleEntityDamageEvent(damagee, source, modifiers, modifierFunctions); - } - diff --git a/patches/unapplied/server/1067-Fix-NPE-when-EntityResurrectEvent-is-uncancelled.patch b/patches/unapplied/server/1067-Fix-NPE-when-EntityResurrectEvent-is-uncancelled.patch deleted file mode 100644 index 511cccc084..0000000000 --- a/patches/unapplied/server/1067-Fix-NPE-when-EntityResurrectEvent-is-uncancelled.patch +++ /dev/null @@ -1,23 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Lulu13022002 <41980282+Lulu13022002@users.noreply.github.com> -Date: Mon, 18 Nov 2024 20:27:58 +0100 -Subject: [PATCH] Fix NPE when EntityResurrectEvent is uncancelled - - -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 51f913a495e7fda7e0e72439c6d7cc9607bd4af8..f36a075dbee2b96d01899e02460b1d8443e91749 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -1711,6 +1711,12 @@ public abstract class LivingEntity extends Entity implements Attackable { - if (!itemstack1.isEmpty() && itemstack != null) { // Paper - only reduce item if actual totem was found - itemstack1.shrink(1); - } -+ // Paper start - fix NPE when pre-cancelled EntityResurrectEvent is uncancelled -+ // restore the previous behavior in that case by defaulting to vanillas totem of undying efect -+ if (deathprotection == null) { -+ deathprotection = DeathProtection.TOTEM_OF_UNDYING; -+ } -+ // Paper end - fix NPE when pre-cancelled EntityResurrectEvent is uncancelled - if (itemstack != null && this instanceof ServerPlayer) { - // CraftBukkit end - ServerPlayer entityplayer = (ServerPlayer) this; diff --git a/patches/unapplied/server/1068-API-to-check-if-the-server-is-sleeping.patch b/patches/unapplied/server/1068-API-to-check-if-the-server-is-sleeping.patch deleted file mode 100644 index c19dba5aeb..0000000000 --- a/patches/unapplied/server/1068-API-to-check-if-the-server-is-sleeping.patch +++ /dev/null @@ -1,37 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Abel -Date: Sun, 10 Nov 2024 16:32:34 +0100 -Subject: [PATCH] API to check if the server is sleeping - - -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 317bb0bd16d8125a40c37b75be1d4d0461bcf9ce..135fb7f722947a57169f0ce584cb031f4c54c854 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -3186,4 +3186,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.emptyTicks >= this.pauseWhileEmptySeconds() * 20; -+ } -+ // Paper end - API to check if the server is sleeping - } -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index 11ff5f3b5cd25f0ad6ca944d59bca8434f8510d8..7afc3d4244c096f78d48338da2eb65c4e834b6f1 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -3258,4 +3258,11 @@ public final class CraftServer implements Server { - return this.potionBrewer; - } - // Paper end -+ -+ // Paper start - API to check if the server is sleeping -+ @Override -+ public boolean isPaused() { -+ return this.console.isTickPaused(); -+ } -+ // Paper end - API to check if the server is sleeping - } diff --git a/patches/unapplied/server/1069-API-to-allow-disallow-tick-sleeping.patch b/patches/unapplied/server/1069-API-to-allow-disallow-tick-sleeping.patch deleted file mode 100644 index 1aff91abe2..0000000000 --- a/patches/unapplied/server/1069-API-to-allow-disallow-tick-sleeping.patch +++ /dev/null @@ -1,67 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Abel -Date: Tue, 12 Nov 2024 22:25:20 +0100 -Subject: [PATCH] API to allow/disallow tick sleeping - - -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 135fb7f722947a57169f0ce584cb031f4c54c854..780582ebaa8deb0c0b0c8de17de5abcebafa4bd3 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -332,6 +332,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop pluginsBlockingSleep = new java.util.HashSet<>(); // Paper - API to allow/disallow tick sleeping - - public static S spin(Function serverFactory) { - AtomicReference atomicreference = new AtomicReference(); -@@ -1623,8 +1624,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0) { -- if (this.playerList.getPlayerCount() == 0 && !this.tickRateManager.isSprinting()) { -+ if (this.playerList.getPlayerCount() == 0 && !this.tickRateManager.isSprinting() && this.pluginsBlockingSleep.isEmpty()) { // Paper - API to allow/disallow tick sleeping - ++this.emptyTicks; - } else { - this.emptyTicks = 0; -@@ -3191,5 +3193,22 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.emptyTicks >= this.pauseWhileEmptySeconds() * 20; - } -+ -+ public void addPluginAllowingSleep(final String pluginName, final boolean value) { -+ if (!value) { -+ this.pluginsBlockingSleep.add(pluginName); -+ } else { -+ this.pluginsBlockingSleep.remove(pluginName); -+ } -+ } -+ -+ private void removeDisabledPluginsBlockingSleep() { -+ if (this.pluginsBlockingSleep.isEmpty()) { -+ return; -+ } -+ this.pluginsBlockingSleep.removeIf(plugin -> ( -+ !io.papermc.paper.plugin.manager.PaperPluginManagerImpl.getInstance().isPluginEnabled(plugin) -+ )); -+ } - // Paper end - API to check if the server is sleeping - } -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index 7afc3d4244c096f78d48338da2eb65c4e834b6f1..ac8af406180bc680d46e8edc3da0fc2e5211345a 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -3264,5 +3264,10 @@ public final class CraftServer implements Server { - public boolean isPaused() { - return this.console.isTickPaused(); - } -+ -+ @Override -+ public void allowPausing(final Plugin plugin, final boolean value) { -+ this.console.addPluginAllowingSleep(plugin.getName(), value); -+ } - // Paper end - API to check if the server is sleeping - } diff --git a/patches/unapplied/server/1070-Configurable-Entity-Despawn-Time.patch b/patches/unapplied/server/1070-Configurable-Entity-Despawn-Time.patch deleted file mode 100644 index 3885f857a3..0000000000 --- a/patches/unapplied/server/1070-Configurable-Entity-Despawn-Time.patch +++ /dev/null @@ -1,39 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Kevin Raneri -Date: Mon, 30 Sep 2024 09:50:55 -0700 -Subject: [PATCH] Configurable Entity Despawn Time - - -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 8c62d1aa5c8a062685474dca7e91bf9f8b004ca5..a15546e433ebba6c0de01bdaaef201a3d99a87b5 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -388,6 +388,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - private UUID originWorld; - public boolean freezeLocked = false; // Paper - Freeze Tick Lock API - public boolean fixedPose = false; // Paper - Expand Pose API -+ private final int despawnTime; // Paper - entity despawn time limit - - public void setOrigin(@javax.annotation.Nonnull Location location) { - this.origin = location.toVector(); -@@ -570,6 +571,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - - public Entity(EntityType type, Level world) { - this.id = Entity.ENTITY_COUNTER.incrementAndGet(); -+ this.despawnTime = type == EntityType.PLAYER ? -1 : world.paperConfig().entities.spawning.despawnTime.getOrDefault(type, io.papermc.paper.configuration.type.number.IntOr.Disabled.DISABLED).or(-1); // Paper - entity despawn time limit - this.passengers = ImmutableList.of(); - this.deltaMovement = Vec3.ZERO; - this.bb = Entity.INITIAL_AABB; -@@ -868,6 +870,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - - public void tick() { -+ // Paper start - entity despawn time limit -+ if (this.despawnTime >= 0 && this.tickCount >= this.despawnTime) { -+ this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); -+ return; -+ } -+ // Paper end - entity despawn time limit - this.baseTick(); - } - diff --git a/patches/unapplied/server/1071-Expanded-Art-API.patch b/patches/unapplied/server/1071-Expanded-Art-API.patch deleted file mode 100644 index 56e4179720..0000000000 --- a/patches/unapplied/server/1071-Expanded-Art-API.patch +++ /dev/null @@ -1,33 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: kokiriglade <60290002+celerry@users.noreply.github.com> -Date: Sat, 23 Nov 2024 18:58:49 +0000 -Subject: [PATCH] Expanded Art API - - -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftArt.java b/src/main/java/org/bukkit/craftbukkit/CraftArt.java -index 40af940193d0df66bbcdcf5f46132e304016a4d7..9d73954282104a6e315c1840feb7d6034d27cfbe 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftArt.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftArt.java -@@ -75,6 +75,22 @@ public class CraftArt implements Art, Handleable { - return this.paintingVariant.height(); - } - -+ // Paper start - Expand Art API -+ @Override -+ public net.kyori.adventure.text.Component title() { -+ return this.paintingVariant.title().map(io.papermc.paper.adventure.PaperAdventure::asAdventure).orElse(null); -+ } -+ -+ @Override -+ public net.kyori.adventure.text.Component author() { -+ return this.paintingVariant.author().map(io.papermc.paper.adventure.PaperAdventure::asAdventure).orElse(null); -+ } -+ -+ public net.kyori.adventure.key.Key assetId() { -+ return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.paintingVariant.assetId()); -+ } -+ // Paper end - Expand Art API -+ - @Override - public int getId() { - return CraftRegistry.getMinecraftRegistry(Registries.PAINTING_VARIANT).getId(this.paintingVariant); diff --git a/patches/unapplied/server/1072-Only-attempt-to-find-spawn-position-if-there-isn-t-a.patch b/patches/unapplied/server/1072-Only-attempt-to-find-spawn-position-if-there-isn-t-a.patch deleted file mode 100644 index 43aea85654..0000000000 --- a/patches/unapplied/server/1072-Only-attempt-to-find-spawn-position-if-there-isn-t-a.patch +++ /dev/null @@ -1,28 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: MrPowerGamerBR -Date: Thu, 28 Nov 2024 15:20:25 -0300 -Subject: [PATCH] Only attempt to find spawn position if there isn't a fixed - spawn position set - - -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 780582ebaa8deb0c0b0c8de17de5abcebafa4bd3..4158473fd553a16fec23bcbcf9a278d413120600 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -816,7 +816,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop