From 07073cb4a3eeab47449e5fddadc92914252e7cfd Mon Sep 17 00:00:00 2001 From: xiaoxijun Date: Thu, 2 Jul 2026 18:11:12 +0800 Subject: [PATCH] #154 (#155) * Add PCA sync protocol support Introduce PCA sync protocol and integrate it into Carpet/Leaves. - Added org/leavesmc/leaves/protocol/PcaSyncProtocol.java: full implementation of the PCA sync protocol (handlers for sync requests, watcher maps, update payloads, player/block watch management, scheduling, and config-driven behavior). - Added fun/bm/lophine/config/modules/function/protocol/PcaSyncProtocolConfig.java and enum PcaPlayerEntityType.java to drive enabling and player-entity sync policy. - Integrated protocol into Carpet: register new carpet rules (pcaSyncProtocol, pcaSyncPlayerEntity) in CarpetCompatSync and added batching to avoid excessive updates when rules change. - Enhanced CarpetServerProtocol to track active players, batch rule updates, and send server rule data safely on the server thread. - Added a Minecraft patch (0046-PCA-sync-protocol.patch) to trigger syncs from setChanged() in containers, chest boats, minecarts and BlockEntity updates so watched clients receive entity/block-entity updates. This enables PCA clients to watch and receive synchronized block-entity and entity state on demand, while keeping updates gated by config and player permissions and improving rule-update performance via batching. * Add Vanilla-like experience config and updates Introduce a new VanillaLikeExperienceConfig module and a patch (0047) that gates multiple Paper/Luminol behavior changes behind this toggle to restore more vanilla-like technical behavior when enabled. The patch injects conditional checks across many server classes (entity merging, phantoms, pistons, hoppers, TNT handling, explosions, portal/piston protections, item merging, teleportation, spawn counting, etc.). Also adjust PCA sync protocol references in the existing 0046 patch to use the updated fun.bm.lophine package path. Includes the new fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.java file. * Restore vanilla ender pearl loading on Folia Add patch (features/0048) that restores vanilla ender-pearl loading behavior on Folia. Reintroduces ender-pearl tracking and persistence: ServerPlayer now uses a thread-safe enderPearls set and saves pearl tags, ThrownEnderpearl maintains a lastSavedEnderPearlTag (updated on construction, owner changes, spawn and restore) and updates/clears it on tick/removal, and EnderpearlItem updates the saved tag when spawning a pearl. PlayerList and BotList now schedule pearl removal via the Bukkit taskScheduler when players/bots quit. The changes preserve Paper's legacyEnderPearlBehavior checks where applicable and add logging and safer tag handling to avoid saving invalid snapshots. * Restore vanilla ender pearl loading Restore vanilla ender-pearl loading behavior on Folia by preserving the last saved NBT for thrown pearls. ThrownEnderpearl.java: add a volatile CompoundTag lastSavedEnderPearlTag, provide a defensive-copying getter getLastSavedEnderPearlTag(), adjust @Nullable annotation placement, and update logger/comment. Minor index/format updates in ServerPlayer.java, PlayerList.java and EnderpearlItem.java. * remove extra import * Add Precise mob cap and adjust ender pearl patch Introduce a Precise mob cap feature and integrate it into Paper/Folia world/region lifecycle. Adds new patch features/0049-Precise-mob-cap.patch which modifies RegionizedWorldData, ServerChunkCache and ServerLevel to support an incremental, event-driven mob counting system that can supersede the existing GlobalEntitiesCounter. Adds PreciseMobCapConfig (toggle and behavior flags) and PreciseMobCapCounter (unique id management, area map registration, per-level global counters, spawnable-chunk tracking, spawn state creation, and cleanup on unload). Also updates the existing 0048 ender-pearl loading patch file (minor import/index adjustments). The Precise mob cap counts only relevant mobs (configurable to match vanilla or Paper-optimized behavior), hooks into entity tracking start/end, and requires per-player-mob-spawns=false to be set in Paper world defaults when enabled. * remove extra import * pre merge config * transform config * merge config * rebuild patches * fix a bug in new counter * simplify code --------- Co-authored-by: Helvetica Volubi Co-authored-by: Helvetica Volubi <88063803+Suisuroru@users.noreply.github.com> --- .../0034-Global-Entities-Counter.patch | 67 ++- ...eaves-Catch-update-suppression-crash.patch | 2 +- .../features/0038-Instant-Block-Updater.patch | 4 +- .../features/0046-PCA-sync-protocol.patch | 76 +++ ...7-Add-Vanilla-like-experience-Config.patch | 256 ++++++++++ ...-Restore-vanilla-ender-pearl-loading.patch | 219 ++++++++ .../bm/lophine/carpet/CarpetCompatSync.java | 143 +++--- .../experiment/GlobalEntitiesCounter.java | 59 ++- .../fixes/VanillaLikeExperienceConfig.java | 13 + .../protocol/PcaSyncProtocolConfig.java | 43 ++ .../enums/GlobalEntitiesCounterType.java | 8 + .../bm/lophine/enums/PcaPlayerEntityType.java | 9 + .../bm/lophine/utils/EntitiesCounterUtil.java | 245 ++++++--- .../java/org/leavesmc/leaves/bot/BotList.java | 12 +- .../leaves/protocol/CarpetServerProtocol.java | 76 ++- .../leaves/protocol/PcaSyncProtocol.java | 483 ++++++++++++++++++ 16 files changed, 1542 insertions(+), 173 deletions(-) create mode 100644 lophine-server/minecraft-patches/features/0046-PCA-sync-protocol.patch create mode 100644 lophine-server/minecraft-patches/features/0047-Add-Vanilla-like-experience-Config.patch create mode 100644 lophine-server/minecraft-patches/features/0048-Restore-vanilla-ender-pearl-loading.patch create mode 100644 lophine-server/src/main/java/fun/bm/lophine/config/modules/fixes/VanillaLikeExperienceConfig.java create mode 100644 lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/PcaSyncProtocolConfig.java create mode 100644 lophine-server/src/main/java/fun/bm/lophine/enums/GlobalEntitiesCounterType.java create mode 100644 lophine-server/src/main/java/fun/bm/lophine/enums/PcaPlayerEntityType.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/PcaSyncProtocol.java diff --git a/lophine-server/minecraft-patches/features/0034-Global-Entities-Counter.patch b/lophine-server/minecraft-patches/features/0034-Global-Entities-Counter.patch index a29e91f..15da608 100644 --- a/lophine-server/minecraft-patches/features/0034-Global-Entities-Counter.patch +++ b/lophine-server/minecraft-patches/features/0034-Global-Entities-Counter.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Global Entities Counter diff --git a/io/papermc/paper/threadedregions/RegionizedWorldData.java b/io/papermc/paper/threadedregions/RegionizedWorldData.java -index eedbeb005d3c8090333c36b9857c837dfb75dbd4..5817f087187e9d3479c752129a1a9d853623a3f7 100644 +index eedbeb005d3c8090333c36b9857c837dfb75dbd4..7ea91dbfb988aff3215a91cf62c9beae94461484 100644 --- a/io/papermc/paper/threadedregions/RegionizedWorldData.java +++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java @@ -558,6 +558,9 @@ public final class RegionizedWorldData { @@ -23,10 +23,10 @@ index eedbeb005d3c8090333c36b9857c837dfb75dbd4..5817f087187e9d3479c752129a1a9d85 this.regionData = regionData; + // Lophine start - Global Entities Counter -+ if (fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.enabled) { ++ if (fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.isEnabled()) { + uniqueId = fun.bm.lophine.utils.EntitiesCounterUtil.generateUniqueId(); + fun.bm.lophine.utils.EntitiesCounterUtil.reportAreaMap(world, spawnChunkTracker, uniqueId); -+ fun.bm.lophine.utils.EntitiesCounterUtil.addDataToLoaded(world, loadedEntities, uniqueId); ++ if (!fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.isDefaultModule()) fun.bm.lophine.utils.EntitiesCounterUtil.addDataToLoaded(world, loadedEntities, uniqueId); + underGlobalEntitiesCounter = true; + } + // Lophine end - Global Entities Counter @@ -35,15 +35,15 @@ index eedbeb005d3c8090333c36b9857c837dfb75dbd4..5817f087187e9d3479c752129a1a9d85 this.updateTickData(); } diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java -index 27389ff98942e27c2a877ea5b671caa31353d18c..e9e5f929d638d3ddf279423d3e23b3f824ad7df2 100644 +index 27389ff98942e27c2a877ea5b671caa31353d18c..76fa17ce0afc7ff1c0b2b84aa2baddd2ba18c1a5 100644 --- a/net/minecraft/server/level/ServerChunkCache.java +++ b/net/minecraft/server/level/ServerChunkCache.java -@@ -513,6 +513,25 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -513,6 +513,26 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon this.chunkMap.tick(haveTime); profiler.pop(); this.clearCache(); + // Lophine start - Global Entities Counter -+ if (fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.enabled) { ++ if (fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.isEnabled()) { + io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); + if (regionizedWorldData != null) { + if (regionizedWorldData.underGlobalEntitiesCounter) { @@ -53,6 +53,7 @@ index 27389ff98942e27c2a877ea5b671caa31353d18c..e9e5f929d638d3ddf279423d3e23b3f8 + } + } else { + if (!regionizedWorldData.getLocalPlayers().isEmpty()) { ++ regionizedWorldData.uniqueId = fun.bm.lophine.utils.EntitiesCounterUtil.generateUniqueId(); + fun.bm.lophine.utils.EntitiesCounterUtil.reportAreaMap(level, regionizedWorldData.spawnChunkTracker, regionizedWorldData.uniqueId); + fun.bm.lophine.utils.EntitiesCounterUtil.addDataToLoaded(level, (ca.spottedleaf.moonrise.common.list.ReferenceList) regionizedWorldData.getLoadedEntities(), regionizedWorldData.uniqueId); + regionizedWorldData.underGlobalEntitiesCounter = true; @@ -64,14 +65,14 @@ index 27389ff98942e27c2a877ea5b671caa31353d18c..e9e5f929d638d3ddf279423d3e23b3f8 } private void tickChunks() { -@@ -577,7 +596,13 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -577,7 +597,13 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon } spawnCookie = NaturalSpawner.createState(chunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, null, true); // Folia - region threading - note: function only cares about loaded entities, doesn't need all } else { - spawnCookie = NaturalSpawner.createState(chunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); // Folia - region threading - note: function only cares about loaded entities, doesn't need all + // Lophine start - Global Entities Counter -+ if (fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.enabled) { -+ spawnCookie = fun.bm.lophine.utils.EntitiesCounterUtil.runRemainingTasks(level, regionizedWorldData.getLoadedEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); ++ if (fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.isEnabled()) { ++ spawnCookie = fun.bm.lophine.utils.EntitiesCounterUtil.createSpawnState(level, regionizedWorldData.getLoadedEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); + } else { + spawnCookie = NaturalSpawner.createState(chunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); // Folia - region threading - note: function only cares about loaded entities, doesn't need all + } @@ -79,7 +80,7 @@ index 27389ff98942e27c2a877ea5b671caa31353d18c..e9e5f929d638d3ddf279423d3e23b3f8 } } finally { foliaProfiler.stopTimer(ca.spottedleaf.leafprofiler.LProfilerRegistry.MOB_SPAWN_ENTITY_COUNT); } // Folia - profiler // Paper end - Optional per player mob spawns -@@ -585,7 +610,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -585,7 +611,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon boolean doMobSpawning = this.level.getGameRules().get(GameRules.SPAWN_MOBS) && !this.level.getLocalPlayers().isEmpty(); // CraftBukkit // Folia - region threading int tickSpeed = this.level.getGameRules().get(GameRules.RANDOM_TICK_SPEED); List spawningCategories; @@ -88,7 +89,7 @@ index 27389ff98942e27c2a877ea5b671caa31353d18c..e9e5f929d638d3ddf279423d3e23b3f8 // Paper start - PlayerNaturallySpawnCreaturesEvent for (ServerPlayer player : this.level.getLocalPlayers()) { // Folia - region threading int chunkRange = Math.min(level.spigotConfig.mobSpawnRange, player.getBukkitEntity().getViewDistance()); -@@ -645,7 +670,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -645,7 +671,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon this.level.tickThunder(chunk); } @@ -98,7 +99,7 @@ index 27389ff98942e27c2a877ea5b671caa31353d18c..e9e5f929d638d3ddf279423d3e23b3f8 NaturalSpawner.spawnForChunk(this.level, chunk, spawnCookie, spawningCategories); } diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index a7cebb797973cde399dc30c696b064efd40874f2..c0e62a69bb5c9e777265b5474d76ba8d06ccec4b 100644 +index a7cebb797973cde399dc30c696b064efd40874f2..1d60c20649b2a6469bfceb5b44c38bfd133e21eb 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -808,6 +808,11 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet @@ -106,15 +107,51 @@ index a7cebb797973cde399dc30c696b064efd40874f2..c0e62a69bb5c9e777265b5474d76ba8d public void tick(BooleanSupplier haveTime, final io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - regionised ticking + // Lophine start - Global Entities Counter -+ if (fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.enabled) { ++ if (fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.isEnabled()) { + if (fun.bm.lophine.utils.EntitiesCounterUtil.canRunNewTask(this)) fun.bm.lophine.utils.EntitiesCounterUtil.tick(this); + } + // Lophine end - Global Entities Counter final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking final ca.spottedleaf.leafprofiler.RegionizedProfiler.Handle foliaProfiler = io.papermc.paper.threadedregions.TickRegionScheduler.getProfiler(); // Folia - profiler ProfilerFiller profiler = Profiler.get(); +@@ -2902,6 +2907,11 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + public void close() throws IOException { + super.close(); + //this.entityManager.close(); // Paper - rewrite chunk system ++ // Lophine start - Precise mob cap ++ if (!fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.isDefaultModule()) { ++ fun.bm.lophine.utils.EntitiesCounterUtil.onWorldUnload(this); ++ } ++ // Lophine end - Precise mob cap + } + + @Override +@@ -3136,6 +3146,11 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + entity.inWorld = true; // CraftBukkit - Mark entity as in world + entity.valid = true; // CraftBukkit + ServerLevel.this.getChunkSource().addEntity(entity); // Paper - ignore and warn about illegal addEntity calls instead of crashing server ++ // Lophine start - Precise mob cap ++ if (fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.isEnabled() && !fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.isDefaultModule()) { ++ fun.bm.lophine.utils.EntitiesCounterUtil.onEntityTrackingStart(ServerLevel.this, entity); ++ } ++ // Lophine end - Precise mob cap + // Paper start - Entity origin API + if (entity.origin == null) { + entity.origin = entity.position(); +@@ -3151,6 +3166,11 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + @Override + public void onTrackingEnd(final Entity entity) { + org.spigotmc.AsyncCatcher.catchOp("entity unregister"); // Spigot ++ // Lophine start - Precise mob cap ++ if (fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.isEnabled() && !fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.isDefaultModule()) { ++ fun.bm.lophine.utils.EntitiesCounterUtil.onEntityTrackingEnd(ServerLevel.this, entity); ++ } ++ // Lophine end - Precise mob cap + ServerLevel.this.getCurrentWorldData().removeLoadedEntity(entity); // Folia - region threading + // Spigot start // TODO I don't think this is needed anymore + if (entity instanceof Player player) { diff --git a/net/minecraft/world/entity/Mob.java b/net/minecraft/world/entity/Mob.java -index fc29654dd7ee3329edf1ccf705f45631d0ecc079..9b8ee2df5a0b6d76d9bfc747187f6cd51bb4a520 100644 +index fc29654dd7ee3329edf1ccf705f45631d0ecc079..042f026f707e1a75b4ec7dc231e0eeb1b5afacc3 100644 --- a/net/minecraft/world/entity/Mob.java +++ b/net/minecraft/world/entity/Mob.java @@ -813,6 +813,44 @@ public abstract class Mob extends LivingEntity implements Targeting, EquipmentUs @@ -122,7 +159,7 @@ index fc29654dd7ee3329edf1ccf705f45631d0ecc079..9b8ee2df5a0b6d76d9bfc747187f6cd5 } } else { + // Lophine start - fix some bug if player is cross scheduler >> to remove monster if the region don't have any valid player -+ if (fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.enabled) { ++ if (fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter.isEnabled()) { + List playerList = this.level().getServer().getPlayerList().getPlayers(); + if (!playerList.isEmpty()) { + double d = -1.0; diff --git a/lophine-server/minecraft-patches/features/0035-Leaves-Catch-update-suppression-crash.patch b/lophine-server/minecraft-patches/features/0035-Leaves-Catch-update-suppression-crash.patch index ae542be..76aaa1c 100644 --- a/lophine-server/minecraft-patches/features/0035-Leaves-Catch-update-suppression-crash.patch +++ b/lophine-server/minecraft-patches/features/0035-Leaves-Catch-update-suppression-crash.patch @@ -74,7 +74,7 @@ index f99c02b4d88cea924e20ca8dced67873c83c093f..0c042c4af5d52c08e9fbc03a04c4b696 profiler.pop(); diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index c0e62a69bb5c9e777265b5474d76ba8d06ccec4b..3e7420cd98504732db17cb6b0fcfdfbb8deeb552 100644 +index 1d60c20649b2a6469bfceb5b44c38bfd133e21eb..b884e06a5e9d6a93c548820a99e5d05157e9d128 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -967,7 +967,18 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet diff --git a/lophine-server/minecraft-patches/features/0038-Instant-Block-Updater.patch b/lophine-server/minecraft-patches/features/0038-Instant-Block-Updater.patch index 41d1a95..ecaa6ee 100644 --- a/lophine-server/minecraft-patches/features/0038-Instant-Block-Updater.patch +++ b/lophine-server/minecraft-patches/features/0038-Instant-Block-Updater.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Instant Block Updater diff --git a/io/papermc/paper/threadedregions/RegionizedWorldData.java b/io/papermc/paper/threadedregions/RegionizedWorldData.java -index 5817f087187e9d3479c752129a1a9d853623a3f7..1d50a626c15fba4947c966e46f38e7684511e9fe 100644 +index 7ea91dbfb988aff3215a91cf62c9beae94461484..491d108cc8e440653746f20fc5b5bca3b791eb88 100644 --- a/io/papermc/paper/threadedregions/RegionizedWorldData.java +++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java @@ -506,7 +506,7 @@ public final class RegionizedWorldData { @@ -38,7 +38,7 @@ index 5817f087187e9d3479c752129a1a9d853623a3f7..1d50a626c15fba4947c966e46f38e768 \ No newline at end of file +} diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index 3e7420cd98504732db17cb6b0fcfdfbb8deeb552..257b440a0eea379433e9a249175c8eb16c37accb 100644 +index b884e06a5e9d6a93c548820a99e5d05157e9d128..cb449a5063f7944551c2e66b603c0f910d84e50f 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -1001,11 +1001,15 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet diff --git a/lophine-server/minecraft-patches/features/0046-PCA-sync-protocol.patch b/lophine-server/minecraft-patches/features/0046-PCA-sync-protocol.patch new file mode 100644 index 0000000..37963da --- /dev/null +++ b/lophine-server/minecraft-patches/features/0046-PCA-sync-protocol.patch @@ -0,0 +1,76 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Bacteriawa +Date: Tue, 23 Jun 2026 02:32:14 +0800 +Subject: [PATCH] PCA sync protocol + +Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com> +As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/252a16955f6e1e31aac85d222b093b0797d392f8/leaves-server/minecraft-patches/features/0027-PCA-sync-protocol.patch) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/net/minecraft/world/SimpleContainer.java b/net/minecraft/world/SimpleContainer.java +index 049c6ab6472415479435f2caab0444c3cbd33902..d9bb641c07e2e8d3780fadac44ff5e3ee0e35c8b 100644 +--- a/net/minecraft/world/SimpleContainer.java ++++ b/net/minecraft/world/SimpleContainer.java +@@ -199,6 +199,11 @@ public class SimpleContainer implements Container, StackedContentsCompatible { + + @Override + public void setChanged() { ++ // Leaves start - PCA sync protocol ++ if (fun.bm.lophine.config.modules.function.protocol.PcaSyncProtocolConfig.enabled && this.getOwner() instanceof org.bukkit.craftbukkit.entity.CraftEntity entity) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncEntityToClient(entity.getHandle()); ++ } ++ // Leaves end - PCA sync protocol + } + + @Override +diff --git a/net/minecraft/world/entity/vehicle/boat/AbstractChestBoat.java b/net/minecraft/world/entity/vehicle/boat/AbstractChestBoat.java +index 8da479d04c04366561549617c477b0811a9b3d7c..345f63ccac2579acf4cf4ee9507c231da12b03ba 100644 +--- a/net/minecraft/world/entity/vehicle/boat/AbstractChestBoat.java ++++ b/net/minecraft/world/entity/vehicle/boat/AbstractChestBoat.java +@@ -140,6 +140,11 @@ public abstract class AbstractChestBoat extends AbstractBoat implements HasCusto + + @Override + public void setChanged() { ++ // Leaves start - PCA sync protocol ++ if (fun.bm.lophine.config.modules.function.protocol.PcaSyncProtocolConfig.enabled) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncEntityToClient(this); ++ } ++ // Leaves end - PCA sync protocol + } + + @Override +diff --git a/net/minecraft/world/entity/vehicle/minecart/AbstractMinecartContainer.java b/net/minecraft/world/entity/vehicle/minecart/AbstractMinecartContainer.java +index d2c4b47b2f95c8e6bafc93e436041143e68ffa4f..12938642aa58b6b64d7536575712dc3eadfb6e13 100644 +--- a/net/minecraft/world/entity/vehicle/minecart/AbstractMinecartContainer.java ++++ b/net/minecraft/world/entity/vehicle/minecart/AbstractMinecartContainer.java +@@ -66,6 +66,11 @@ public abstract class AbstractMinecartContainer extends AbstractMinecart impleme + + @Override + public void setChanged() { ++ // Leaves start - PCA sync protocol ++ if (fun.bm.lophine.config.modules.function.protocol.PcaSyncProtocolConfig.enabled) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncEntityToClient(this); ++ } ++ // Leaves end - PCA sync protocol + } + + @Override +diff --git a/net/minecraft/world/level/block/entity/BlockEntity.java b/net/minecraft/world/level/block/entity/BlockEntity.java +index 5e7612ad30c218fcbb7c9cdd66ecb69d78d8875d..0fa8f720e3b386a5466b3a94d18d5dbbd0d48836 100644 +--- a/net/minecraft/world/level/block/entity/BlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BlockEntity.java +@@ -256,6 +256,14 @@ public abstract class BlockEntity implements DebugValueSource, TypedInstance +Date: Tue, 23 Jun 2026 03:06:41 +0800 +Subject: [PATCH] Add Vanilla-like experience Config + + +diff --git a/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java +index b97d65b81ec4b86e00b7c06341b40dc5c02b9044..d997e4f7ae6ee4e81f5fffe874f2895dda314df9 100644 +--- a/ca/spottedleaf/moonrise/paper/PaperHooks.java ++++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java +@@ -204,6 +204,11 @@ public class PaperHooks extends BaseChunkSystemHooks implements PlatformHooks { + + @Override + public int configMaxAutoSavePerTick(final ServerLevel world) { ++ // Lophine start - Vanilla-like experience ++ if (fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled) { ++ return 200; ++ } ++ // Lophine end - Vanilla-like experience + return world.paperConfig().chunks.maxAutoSaveChunksPerTick; + } + +@@ -308,4 +313,4 @@ public class PaperHooks extends BaseChunkSystemHooks implements PlatformHooks { + public boolean addTicketForEnderPearls(final ServerLevel world) { + return !world.paperConfig().misc.legacyEnderPearlBehavior; + } +-} +\ No newline at end of file ++} +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index 63300acdcb1cdab2bf080c4939abb256fe05efa1..99e61bbe15430a72096489777a4e417ec5125337 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -2390,7 +2390,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + public boolean isInvulnerableTo(final ServerLevel level, final DamageSource source) { + // Paper start - disable player cramming + return (super.isInvulnerableTo(level, source) || this.isChangingDimension() && !source.is(DamageTypes.ENDER_PEARL) || !this.connection.hasClientLoaded()) +- || (!this.level().paperConfig().collisions.allowPlayerCrammingDamage && source.is(DamageTypes.CRAMMING)); ++ || (!fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled && !this.level().paperConfig().collisions.allowPlayerCrammingDamage && source.is(DamageTypes.CRAMMING)); // Lophine - Vanilla-like experience + // Paper end - disable player cramming + } + +diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java +index 0bd4d786c8e957a31811f52514a992543a0cee92..31212878ac31a8d9849b471b103b95553b274501 100644 +--- a/net/minecraft/world/entity/LivingEntity.java ++++ b/net/minecraft/world/entity/LivingEntity.java +@@ -3993,7 +3993,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin + // Paper start - Cap entity collisions + this.numCollisions = Math.max(0, this.numCollisions - this.level().paperConfig().collisions.maxEntityCollisions); + for (Entity entityx : pushableEntities) { +- if (this.numCollisions >= this.level().paperConfig().collisions.maxEntityCollisions) { ++ if (this.numCollisions >= (fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled ? Integer.MAX_VALUE : this.level().paperConfig().collisions.maxEntityCollisions)) { // Lophine - Vanilla-like experience + break; + } + +diff --git a/net/minecraft/world/entity/item/FallingBlockEntity.java b/net/minecraft/world/entity/item/FallingBlockEntity.java +index c3f1f875b1adc75f9e9747e148f214e4623a0b4f..04c264611273872a9a33fe2a4d1e2a041e6e78d2 100644 +--- a/net/minecraft/world/entity/item/FallingBlockEntity.java ++++ b/net/minecraft/world/entity/item/FallingBlockEntity.java +@@ -383,7 +383,7 @@ public class FallingBlockEntity extends Entity { + ResourceKey oldDimension = this.level().dimension(); + boolean fromOrToEnd = (oldDimension == Level.END || newDimension == Level.END) && oldDimension != newDimension; + Entity newEntity = super.teleport(transition); +- this.forceTickAfterTeleportToDuplicate = newEntity != null && fromOrToEnd && (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowUnsafeEndPortalTeleportation || me.earthme.luminol.config.modules.fixes.UnsafeTeleportationConfig.enabled); // Paper // Luminol - Unsafe teleportation ++ this.forceTickAfterTeleportToDuplicate = newEntity != null && fromOrToEnd && (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowUnsafeEndPortalTeleportation || me.earthme.luminol.config.modules.fixes.UnsafeTeleportationConfig.enabled || fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled); // Paper // Luminol - Unsafe teleportation // Lophine - Vanilla-like experience + return newEntity; + } + } +diff --git a/net/minecraft/world/entity/item/ItemEntity.java b/net/minecraft/world/entity/item/ItemEntity.java +index 679a941a2af886697fbfcbc7f21e5f4e0ec77dc3..1d4856c477441dd3dc57432c7129fc7f54b528bd 100644 +--- a/net/minecraft/world/entity/item/ItemEntity.java ++++ b/net/minecraft/world/entity/item/ItemEntity.java +@@ -259,7 +259,7 @@ public class ItemEntity extends Entity implements TraceableEntity, ChangePublish + if (this.isMergable()) { + double radius = this.level().spigotConfig.itemMerge; // Spigot + for (ItemEntity entity : this.level() +- .getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate(radius, this.level().paperConfig().entities.behavior.onlyMergeItemsHorizontally ? 0 : radius - 0.5D, radius), other -> other != this && other.isMergable())) { // Spigot // Paper - configuration to only merge items horizontally ++ .getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate(radius, fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled ? radius : (this.level().paperConfig().entities.behavior.onlyMergeItemsHorizontally ? 0 : radius - 0.5D), radius), other -> other != this && other.isMergable())) { // Spigot // Paper - configuration to only merge items horizontally // Lophine - Vanilla-like experience + if (entity.isMergable()) { + // Paper start - Fix items merging through walls + if (this.level().paperConfig().fixes.fixItemsMergingThroughWalls) { +diff --git a/net/minecraft/world/entity/item/PrimedTnt.java b/net/minecraft/world/entity/item/PrimedTnt.java +index 5b5717a886dab57b2fe9ad926a8ae7b3d4f30155..05d8ba6e5a984660be2fba97f9022393bb84f62b 100644 +--- a/net/minecraft/world/entity/item/PrimedTnt.java ++++ b/net/minecraft/world/entity/item/PrimedTnt.java +@@ -107,7 +107,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { + + @Override + public void tick() { +- if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().getCurrentWorldData().currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot // Folia - region threading ++ if (!fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled && this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().getCurrentWorldData().currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot // Folia - region threading // Lophine - Vanilla-like experience + //this.handlePortal(); // Folia - region threading + this.applyGravity(); + this.move(MoverType.SELF, this.getDeltaMovement()); +diff --git a/net/minecraft/world/entity/monster/Phantom.java b/net/minecraft/world/entity/monster/Phantom.java +index 61ca77cceeacf0c4ebecf3bb66c4eb99c62033d2..fb3651bb6ebc4b75c17c2443dc9f95caeea98ef0 100644 +--- a/net/minecraft/world/entity/monster/Phantom.java ++++ b/net/minecraft/world/entity/monster/Phantom.java +@@ -259,7 +259,7 @@ public class Phantom extends Mob implements Enemy { + + for (Player player : players) { + if (Phantom.this.canAttack(level, player, TargetingConditions.DEFAULT)) { +- if (!level().paperConfig().entities.behavior.phantomsOnlyAttackInsomniacs || EntitySelector.IS_INSOMNIAC.test(player)) { // Paper - Add phantom creative and insomniac controls ++ if (!level().paperConfig().entities.behavior.phantomsOnlyAttackInsomniacs || fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled || EntitySelector.IS_INSOMNIAC.test(player)) { // Paper - Add phantom creative and insomniac controls // Lophine - Vanilla-like experience + Phantom.this.setTarget(player, org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_PLAYER); // CraftBukkit - reason + return true; + } // Paper - Add phantom creative and insomniac controls +diff --git a/net/minecraft/world/level/NaturalSpawner.java b/net/minecraft/world/level/NaturalSpawner.java +index c99c66563192629e399d1d904cb654c624fe9837..04e4e03399e5484bdbe8695fc2da5e5df152ba34 100644 +--- a/net/minecraft/world/level/NaturalSpawner.java ++++ b/net/minecraft/world/level/NaturalSpawner.java +@@ -90,7 +90,8 @@ public final class NaturalSpawner { + MobCategory category = entity.getType().getCategory(); + if (category != MobCategory.MISC) { + // Paper start - Only count natural spawns +- if (!entity.level().paperConfig().entities.spawning.countAllMobsForSpawning && ++ if (!fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled && // Lophine - Vanilla-like experience ++ !entity.level().paperConfig().entities.spawning.countAllMobsForSpawning && + !(entity.spawnReason == org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL || + entity.spawnReason == org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CHUNK_GEN)) { + continue; +diff --git a/net/minecraft/world/level/ServerExplosion.java b/net/minecraft/world/level/ServerExplosion.java +index 69d436268492183ae41fb9a60e61ed1f58a674d9..27e99cdd3a20c8c9177ccf8daa8fdf6984d51318 100644 +--- a/net/minecraft/world/level/ServerExplosion.java ++++ b/net/minecraft/world/level/ServerExplosion.java +@@ -451,7 +451,7 @@ public class ServerExplosion implements Explosion { + if (this.fire || !cachedBlock.blockState.isAir()) { + ret.add(cachedBlock.immutablePos); + // Paper start - prevent headless pistons from forming +- if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == net.minecraft.world.level.block.Blocks.MOVING_PISTON) { ++ if (!fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled && !io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == net.minecraft.world.level.block.Blocks.MOVING_PISTON) { // Lophine - Vanilla-like experience + net.minecraft.world.level.block.entity.BlockEntity extension = this.level.getBlockEntity(cachedBlock.immutablePos); // Paper - optimise collisions + if (extension instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity blockEntity && blockEntity.isSourcePiston()) { + net.minecraft.core.Direction direction = iblockdata.getValue(net.minecraft.world.level.block.piston.PistonHeadBlock.FACING); +diff --git a/net/minecraft/world/level/block/Block.java b/net/minecraft/world/level/block/Block.java +index 30f7e8f5e3e9440713e36ee25c90150b6b93c6ff..8527e46cab53ce6e67319cb87e9dcdda6b729de5 100644 +--- a/net/minecraft/world/level/block/Block.java ++++ b/net/minecraft/world/level/block/Block.java +@@ -116,7 +116,7 @@ public class Block extends BlockBehaviour implements ItemLike { + private BlockState defaultBlockState; + // Paper start - Protect Bedrock and End Portal/Frames from being destroyed + public final boolean isDestroyable() { +- return io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits || ++ return fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled || io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits || // Lophine - Vanilla-like experience + this != Blocks.BARRIER && + this != Blocks.BEDROCK && + this != Blocks.END_PORTAL_FRAME && +diff --git a/net/minecraft/world/level/block/TripWireHookBlock.java b/net/minecraft/world/level/block/TripWireHookBlock.java +index 836eae0c3e0891a3e3309b684f15d65f891215cf..a0fe40351ebb95fd5207bc63be82dbb887baebef 100644 +--- a/net/minecraft/world/level/block/TripWireHookBlock.java ++++ b/net/minecraft/world/level/block/TripWireHookBlock.java +@@ -188,7 +188,7 @@ public class TripWireHookBlock extends Block { + if (!cancelledEmitterHook) { // Paper - Call BlockRedstoneEvent + emitState(level, pos, attached, powered, wasAttached, wasPowered); + if (!isBeingDestroyed) { +- if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.skipTripwireHookPlacementValidation || level.getBlockState(pos).is(Blocks.TRIPWIRE_HOOK)) // Paper - Validate tripwire hook placement before update ++ if (fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled || io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.skipTripwireHookPlacementValidation || level.getBlockState(pos).is(Blocks.TRIPWIRE_HOOK)) // Paper - Validate tripwire hook placement before update // Lophine - Vanilla-like experience + level.setBlock(pos, newState.setValue(FACING, direction), Block.UPDATE_ALL); + if (canUpdate) { + notifyNeighbors(block, level, pos, direction); +diff --git a/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java b/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java +index 62d7a44f139c28a38b14833380e2ec2bf84f3a75..12af97b9ffcf7c0f9e815ccbd81f99a80c6a5819 100644 +--- a/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java +@@ -332,7 +332,7 @@ public class BeehiveBlockEntity extends BlockEntity { + iterator.remove(); + } + // Paper start - Fix bees aging inside; use exitTickCounter to keep actual bee life +- else if (level.paperConfig().entities.behavior.cooldownFailedBeehiveReleases) { ++ else if (!fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled && level.paperConfig().entities.behavior.cooldownFailedBeehiveReleases) { // Lophine - Vanilla-like experience + data.exitTickCounter = data.occupant.minTicksInHive / 2; + } + // Paper end - Fix bees aging inside; use exitTickCounter to keep actual bee life +diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index 8066cdcaba8d2fcfa2e9ba3b040a2867dbbc6978..c904e35d49a36e2722295490a5a6aea67f29826e 100644 +--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -346,7 +346,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + origItemStack.setCount(originalItemCount); + } + } +- if (foundItem && level.paperConfig().hopper.cooldownWhenFull) { // Inventory was full - cooldown ++ if (foundItem && !fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled && level.paperConfig().hopper.cooldownWhenFull) { // Inventory was full - cooldown // Lophine - Vanilla-like experience + hopper.setCooldown(level.spigotConfig.hopperTransfer); + } + return false; +diff --git a/net/minecraft/world/level/block/piston/PistonBaseBlock.java b/net/minecraft/world/level/block/piston/PistonBaseBlock.java +index a4d8ed890fc6bea1e961df814c00d22ae147734f..da037fbc38b4e605890711f4497ac8755a4f76f0 100644 +--- a/net/minecraft/world/level/block/piston/PistonBaseBlock.java ++++ b/net/minecraft/world/level/block/piston/PistonBaseBlock.java +@@ -152,7 +152,7 @@ public class PistonBaseBlock extends DirectionalBlock { + Direction direction = state.getValue(FACING); + // Paper start - Protect Bedrock and End Portal/Frames from being destroyed; prevent retracting when we're facing the wrong way (we were replaced before retraction could occur) + Direction directionQueuedAs = Direction.from3DDataValue(b1 & 7); // Paper - copied from below +- if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits && direction != directionQueuedAs) { ++ if (!fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled && !io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits && direction != directionQueuedAs) { // Lophine - Vanilla-like experience + return false; + } + // Paper end - Protect Bedrock and End Portal/Frames from being destroyed +@@ -237,7 +237,7 @@ public class PistonBaseBlock extends DirectionalBlock { + } else { + // Paper start - Protect Bedrock and End Portal/Frames from being destroyed; fix headless pistons breaking blocks + BlockPos headPos = pos.relative(direction); +- if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits || level.getBlockState(headPos) == Blocks.PISTON_HEAD.defaultBlockState().setValue(FACING, direction)) { // double check to make sure we're not a headless piston ++ if (fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled || io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits || level.getBlockState(headPos) == Blocks.PISTON_HEAD.defaultBlockState().setValue(FACING, direction)) { // double check to make sure we're not a headless piston // Lophine - Vanilla-like experience + level.removeBlock(headPos, false); + } else { + ((ServerLevel) level).getChunkSource().blockChanged(headPos); // ... fix client desync +@@ -379,7 +379,7 @@ public class PistonBaseBlock extends DirectionalBlock { + + for (int i = toPush.size() - 1; i >= 0; i--) { + // Paper start - fix a variety of piston desync dupes +- boolean allowDesync = !fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.tntDupingFix; ++ boolean allowDesync = fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled || !fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.tntDupingFix; // Lophine - Vanilla-like experience + BlockPos oldPos = toPush.get(i); + BlockPos pos = oldPos; + BlockState blockState = allowDesync ? level.getBlockState(oldPos) : null; +diff --git a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java +index 22663ad55f669081b43644ad2cf1bd4aac48b745..0d79157ac9b7d099c29a933c171fe87718f34f8e 100644 +--- a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java ++++ b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java +@@ -334,7 +334,7 @@ public class PistonMovingBlockEntity extends BlockEntity { + if (level.getBlockState(pos).is(Blocks.MOVING_PISTON)) { + BlockState newState = Block.updateFromNeighbourShapes(entity.movedState, level, pos); + if (newState.isAir()) { +- level.setBlock(pos, entity.movedState, Block.UPDATE_NONE | Block.UPDATE_MOVE_BY_PISTON | Block.UPDATE_KNOWN_SHAPE | (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPistonDuplication ? 0 : Block.UPDATE_CLIENTS)); // Paper - fix a variety of piston desync dupes; force notify (flag 2), it's possible the set type by the piston block (which doesn't notify) set this block to air ++ level.setBlock(pos, entity.movedState, Block.UPDATE_NONE | Block.UPDATE_MOVE_BY_PISTON | Block.UPDATE_KNOWN_SHAPE | ((fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled || io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPistonDuplication) ? 0 : Block.UPDATE_CLIENTS)); // Paper - fix a variety of piston desync dupes; force notify (flag 2), it's possible the set type by the piston block (which doesn't notify) set this block to air // Lophine - Vanilla-like experience + Block.updateOrDestroy(entity.movedState, newState, level, pos, Block.UPDATE_ALL); + } else { + if (newState.hasProperty(BlockStateProperties.WATERLOGGED) && newState.getValue(BlockStateProperties.WATERLOGGED)) { +diff --git a/net/minecraft/world/level/levelgen/PhantomSpawner.java b/net/minecraft/world/level/levelgen/PhantomSpawner.java +index 142ab133d3b68eb9b88651e307653f1aafdf1de3..6061ac704e504d53492f405e92c17b6a4b44716f 100644 +--- a/net/minecraft/world/level/levelgen/PhantomSpawner.java ++++ b/net/minecraft/world/level/levelgen/PhantomSpawner.java +@@ -41,7 +41,7 @@ public class PhantomSpawner implements CustomSpawner { + // Paper end - Ability to control player's insomnia and phantoms + if (level.getSkyDarken() >= 5 || !level.dimensionType().hasSkyLight()) { + for (ServerPlayer player : level.getLocalPlayers()) { // Folia - region threading +- if (!player.isSpectator() && (!level.paperConfig().entities.behavior.phantomsDoNotSpawnOnCreativePlayers || !player.isCreative())) { // Paper - Add phantom creative and insomniac controls ++ if (!player.isSpectator() && (fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled || !level.paperConfig().entities.behavior.phantomsDoNotSpawnOnCreativePlayers || !player.isCreative())) { // Paper - Add phantom creative and insomniac controls // Lophine - Vanilla-like experience + BlockPos playerPos = player.blockPosition(); + if (!level.dimensionType().hasSkyLight() || playerPos.getY() >= level.getSeaLevel() && level.canSeeSky(playerPos)) { + DifficultyInstance difficulty = level.getCurrentDifficultyAt(playerPos); +diff --git a/net/minecraft/world/level/portal/PortalForcer.java b/net/minecraft/world/level/portal/PortalForcer.java +index 4e4903d38d705dca6ff16418e54a7a367074aa08..b61259024c167f951d0fad292f40d2a814f6b8a5 100644 +--- a/net/minecraft/world/level/portal/PortalForcer.java ++++ b/net/minecraft/world/level/portal/PortalForcer.java +@@ -233,7 +233,7 @@ public class PortalForcer { + origin, direction.getStepX() * width + clockWise.getStepX() * offset, height, direction.getStepZ() * width + clockWise.getStepZ() * offset + ); + // Paper start - Protect Bedrock and End Portal/Frames from being destroyed +- if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits) { ++ if (!fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.enabled && !io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits) { // Lophine - Vanilla-like experience + if (!this.level.getBlockState(mutable).isDestroyable()) { + return false; + } diff --git a/lophine-server/minecraft-patches/features/0048-Restore-vanilla-ender-pearl-loading.patch b/lophine-server/minecraft-patches/features/0048-Restore-vanilla-ender-pearl-loading.patch new file mode 100644 index 0000000..dd1e1a4 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0048-Restore-vanilla-ender-pearl-loading.patch @@ -0,0 +1,219 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Bacteriawa +Date: Wed, 24 Jun 2026 00:00:00 +0800 +Subject: [PATCH] Restore vanilla ender pearl loading + + +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index 99e61bbe15430a72096489777a4e417ec5125337..3b76045aa0b4cd97f7b104921a7fa8e29234e1d3 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -272,7 +272,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + private @Nullable BlockPos raidOmenPosition; + private Vec3 lastKnownClientMovement = Vec3.ZERO; + private Input lastClientInput = Input.EMPTY; +- private final Set enderPearls = new HashSet<>(); ++ private final Set enderPearls = java.util.concurrent.ConcurrentHashMap.newKeySet(); // Lophine - restore vanilla ender pearl loading on Folia + private long timeEntitySatOnShoulder; + private CompoundTag shoulderEntityLeft = new CompoundTag(); + private CompoundTag shoulderEntityRight = new CompoundTag(); +@@ -796,18 +796,20 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + + private void saveEnderPearls(final ValueOutput playerOutput) { + if (!this.enderPearls.isEmpty()) { +- ValueOutput.ValueOutputList pearlsOutput = playerOutput.childrenList("ender_pearls"); +- ++ // Lophine start - restore vanilla ender pearl loading on Folia ++ ValueOutput.TypedOutputList pearlsOutput = null; + for (ThrownEnderpearl enderPearl : this.enderPearls) { +- if (enderPearl.level().paperConfig().misc.legacyEnderPearlBehavior) continue; // Paper - Allow using old ender pearl behavior +- if (enderPearl.isRemoved()) { +- LOGGER.warn("Trying to save removed ender pearl, skipping"); ++ CompoundTag pearlTag = enderPearl.getLastSavedEnderPearlTag(); ++ if (pearlTag == null) { ++ LOGGER.warn("Trying to save ender pearl without a region-owned snapshot, skipping"); + } else { +- ValueOutput pearlTag = pearlsOutput.addChild(); +- enderPearl.save(pearlTag); +- pearlTag.store("ender_pearl_dimension", Level.RESOURCE_KEY_CODEC, enderPearl.level().dimension()); ++ if (pearlsOutput == null) { ++ pearlsOutput = playerOutput.list(ServerPlayer.ENDER_PEARLS_TAG, CompoundTag.CODEC); ++ } ++ pearlsOutput.add(pearlTag); + } + } ++ // Lophine end - restore vanilla ender pearl loading on Folia + } + } + +@@ -3598,11 +3600,11 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + } + + public void registerEnderPearl(final ThrownEnderpearl enderPearl) { +- //this.enderPearls.add(enderPearl); // Folia - region threading - do not track ender pearls ++ this.enderPearls.add(enderPearl); // Lophine - restore vanilla ender pearl loading on Folia + } + + public void deregisterEnderPearl(final ThrownEnderpearl enderPearl) { +- //this.enderPearls.remove(enderPearl); // Folia - region threading - do not track ender pearls ++ this.enderPearls.remove(enderPearl); // Lophine - restore vanilla ender pearl loading on Folia + } + + public Set getEnderPearls() { +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index 64779d7b28e61c91897db5226158d65c32bfc835..f665ff0bc9068aaca6d6719ef6f7d87592f39816 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -752,11 +752,13 @@ public abstract class PlayerList { + player.unRide(); + + for (ThrownEnderpearl enderpearl : player.getEnderPearls()) { +- // Paper start - Allow using old ender pearl behavior +- if (!enderpearl.level().paperConfig().misc.legacyEnderPearlBehavior) { +- enderpearl.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER, org.bukkit.event.entity.EntityRemoveEvent.Cause.PLAYER_QUIT); // CraftBukkit - add Bukkit remove cause +- } +- // Paper end - Allow using old ender pearl behavior ++ // Lophine start - restore vanilla ender pearl loading on Folia ++ enderpearl.getBukkitEntity().taskScheduler.scheduleOrExecute((Entity pearl) -> { ++ if (!pearl.isRemoved()) { ++ pearl.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER, org.bukkit.event.entity.EntityRemoveEvent.Cause.PLAYER_QUIT); // CraftBukkit - add Bukkit remove cause ++ } ++ }); ++ // Lophine end - restore vanilla ender pearl loading on Folia + } + + level.removePlayerImmediately(player, Entity.RemovalReason.UNLOADED_WITH_PLAYER); +diff --git a/net/minecraft/world/entity/projectile/throwableitemprojectile/ThrownEnderpearl.java b/net/minecraft/world/entity/projectile/throwableitemprojectile/ThrownEnderpearl.java +index dc135f1a5d1ec33195e9ac897aa7acee4cbd13c6..bc9ca4680c18f28b97c4d1afe202cc2fff320daf 100644 +--- a/net/minecraft/world/entity/projectile/throwableitemprojectile/ThrownEnderpearl.java ++++ b/net/minecraft/world/entity/projectile/throwableitemprojectile/ThrownEnderpearl.java +@@ -29,7 +29,11 @@ import net.minecraft.world.phys.Vec3; + import org.jspecify.annotations.Nullable; + + public class ThrownEnderpearl extends ThrowableItemProjectile { ++ private static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger(); // Lophine - restore vanilla ender pearl loading on Folia + private long ticketTimer = 0L; ++ // Lophine start - restore vanilla ender pearl loading on Folia ++ private volatile net.minecraft.nbt.@Nullable CompoundTag lastSavedEnderPearlTag; ++ // Lophine end - restore vanilla ender pearl loading on Folia + + public ThrownEnderpearl(final EntityType type, final Level level) { + super(type, level); +@@ -37,6 +41,9 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + + public ThrownEnderpearl(final Level level, final LivingEntity mob, final ItemStack itemStack) { + super(EntityType.ENDER_PEARL, mob, level, itemStack); ++ // Lophine start - restore vanilla ender pearl loading on Folia ++ this.updateLastSavedEnderPearlTag(); ++ // Lophine end - restore vanilla ender pearl loading on Folia + } + + @Override +@@ -49,21 +56,73 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + this.deregisterFromCurrentOwner(); + super.setOwner(owner); + this.registerToCurrentOwner(); ++ // Lophine start - restore vanilla ender pearl loading on Folia ++ this.updateLastSavedEnderPearlTag(); ++ // Lophine end - restore vanilla ender pearl loading on Folia + } + + private void deregisterFromCurrentOwner() { +- // Folia - region threading - we remove the registration logic, we do not need to fetch the owner ++ // Lophine start - restore vanilla ender pearl loading on Folia ++ if (this.owner != null && this.level() instanceof ServerLevel serverLevel && serverLevel.getServer().getPlayerList().getPlayer(this.owner.getUUID()) instanceof ServerPlayer serverPlayer) { ++ serverPlayer.deregisterEnderPearl(this); ++ } ++ // Lophine end - restore vanilla ender pearl loading on Folia + } + + private void registerToCurrentOwner() { +- // Folia - region threading - we remove the registration logic, we do not need to fetch the owner ++ // Lophine start - restore vanilla ender pearl loading on Folia ++ if (this.owner != null ++ && this.level() instanceof ServerLevel serverLevel ++ && !serverLevel.paperConfig().misc.legacyEnderPearlBehavior ++ && serverLevel.getServer().getPlayerList().getPlayer(this.owner.getUUID()) instanceof ServerPlayer serverPlayer) { ++ serverPlayer.registerEnderPearl(this); ++ } ++ // Lophine end - restore vanilla ender pearl loading on Folia + } + ++ // Lophine start - restore vanilla ender pearl loading on Folia ++ public net.minecraft.nbt.@Nullable CompoundTag getLastSavedEnderPearlTag() { ++ net.minecraft.nbt.CompoundTag tag = this.lastSavedEnderPearlTag; ++ return tag == null ? null : tag.copy(); ++ } ++ ++ public void updateLastSavedEnderPearlTag() { ++ if (this.owner == null || this.isRemoved() || !(this.level() instanceof ServerLevel serverLevel) || serverLevel.paperConfig().misc.legacyEnderPearlBehavior) { ++ this.lastSavedEnderPearlTag = null; ++ return; ++ } ++ try (net.minecraft.util.ProblemReporter.ScopedCollector problemReporter = new net.minecraft.util.ProblemReporter.ScopedCollector(this.problemPath(), LOGGER)) { ++ net.minecraft.world.level.storage.TagValueOutput pearlOutput = net.minecraft.world.level.storage.TagValueOutput.createWithContext(problemReporter, this.registryAccess()); ++ if (this.save(pearlOutput)) { ++ pearlOutput.store(ServerPlayer.ENDER_PEARL_DIMENSION_TAG, Level.RESOURCE_KEY_CODEC, this.level().dimension()); ++ this.lastSavedEnderPearlTag = pearlOutput.buildResult(); ++ } else { ++ this.lastSavedEnderPearlTag = null; ++ } ++ } ++ } ++ // Lophine end - restore vanilla ender pearl loading on Folia ++ + @Override + public @Nullable Entity getOwner() { + return this.owner != null && this.level() instanceof ServerLevel serverLevel ? this.owner.getEntity(serverLevel, Entity.class) : super.getOwner(); + } + ++ // Lophine start - restore vanilla ender pearl loading on Folia ++ @Override ++ protected void readAdditionalSaveData(final net.minecraft.world.level.storage.ValueInput input) { ++ super.readAdditionalSaveData(input); ++ this.updateLastSavedEnderPearlTag(); ++ } ++ ++ @Override ++ public void restoreFrom(final Entity oldEntity) { ++ super.restoreFrom(oldEntity); ++ this.registerToCurrentOwner(); ++ this.updateLastSavedEnderPearlTag(); ++ } ++ // Lophine end - restore vanilla ender pearl loading on Folia ++ + private static @Nullable Entity findOwnerIncludingDeadPlayer(final ServerLevel serverLevel, final UUID uuid) { + Entity owner = serverLevel.getEntityInAnyDimension(uuid); + return (Entity)(owner != null ? owner : serverLevel.getServer().getPlayerList().getPlayer(uuid)); +@@ -281,6 +340,9 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + && owner instanceof ServerPlayer serverPlayer) { + this.ticketTimer = serverPlayer.registerAndUpdateEnderPearlTicket(this); + } ++ // Lophine start - restore vanilla ender pearl loading on Folia ++ this.updateLastSavedEnderPearlTag(); ++ // Lophine end - restore vanilla ender pearl loading on Folia + } + } else { + super.tick(); +@@ -328,6 +390,9 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + public void onRemoval(final Entity.RemovalReason reason) { + if (reason != Entity.RemovalReason.UNLOADED_WITH_PLAYER) { + this.deregisterFromCurrentOwner(); ++ // Lophine start - restore vanilla ender pearl loading on Folia ++ this.lastSavedEnderPearlTag = null; ++ // Lophine end - restore vanilla ender pearl loading on Folia + } + + super.onRemoval(reason); +diff --git a/net/minecraft/world/item/EnderpearlItem.java b/net/minecraft/world/item/EnderpearlItem.java +index 17a005321601b0fef71604e4468134f1e98cd974..a270b4ca7d65b91ffcf292ff80f3a23124cd595c 100644 +--- a/net/minecraft/world/item/EnderpearlItem.java ++++ b/net/minecraft/world/item/EnderpearlItem.java +@@ -27,6 +27,7 @@ public class EnderpearlItem extends Item { + final Projectile.Delayed thrownEnderpearl = Projectile.spawnProjectileFromRotationDelayed(ThrownEnderpearl::new, serverLevel, itemStack, player, 0.0F, EnderpearlItem.PROJECTILE_SHOOT_POWER, 1.0F); + com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) player.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack), (org.bukkit.entity.Projectile) thrownEnderpearl.projectile().getBukkitEntity()); + if (event.callEvent() && thrownEnderpearl.attemptSpawn()) { ++ thrownEnderpearl.projectile().updateLastSavedEnderPearlTag(); // Lophine - restore vanilla ender pearl loading on Folia + if (event.shouldConsume()) { + itemStack.consume(1, player); + } else { diff --git a/lophine-server/src/main/java/fun/bm/lophine/carpet/CarpetCompatSync.java b/lophine-server/src/main/java/fun/bm/lophine/carpet/CarpetCompatSync.java index f18970c..24d125b 100644 --- a/lophine-server/src/main/java/fun/bm/lophine/carpet/CarpetCompatSync.java +++ b/lophine-server/src/main/java/fun/bm/lophine/carpet/CarpetCompatSync.java @@ -4,6 +4,7 @@ import fun.bm.lophine.carpet.config.modules.FakePlayerCompatConfig; import fun.bm.lophine.carpet.config.modules.GeneralCompatConfig; import fun.bm.lophine.carpet.config.modules.WoolHopperCounterConfig; import fun.bm.lophine.config.modules.function.FakeplayerConfig; +import fun.bm.lophine.config.modules.function.protocol.PcaSyncProtocolConfig; import fun.bm.lophine.protocol.CarpetLoggerProtocol; import org.leavesmc.leaves.protocol.CarpetServerProtocol; @@ -24,75 +25,83 @@ public final class CarpetCompatSync { } private static void registerProtocolRules() { - CarpetServerProtocol.CarpetRules.clear(); + CarpetServerProtocol.CarpetRules.beginBatch(); + try { + CarpetServerProtocol.CarpetRules.clear(); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "language", GeneralCompatConfig.language)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "amsUpdateSuppressionCrashFix", GeneralCompatConfig.amsUpdateSuppressionCrashFix)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "yeetUpdateSuppressionCrash", GeneralCompatConfig.yeetUpdateSuppressionCrash)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "dustTrapdoorReintroduced", GeneralCompatConfig.dustTrapdoorReintroduced)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "shulkerBoxCCEReintroduced", GeneralCompatConfig.shulkerBoxCCEReintroduced)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "instantBlockUpdaterReintroduced", GeneralCompatConfig.instantBlockUpdaterReintroduced)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "commandTick", GeneralCompatConfig.commandTick)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "creativeNoClip", GeneralCompatConfig.creativeNoClip)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "optimizedDragonRespawn", GeneralCompatConfig.optimizedDragonRespawn)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "antiSpamDisabled", GeneralCompatConfig.antiSpamDisabled)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "blockPlacementIgnoreEntity", GeneralCompatConfig.blockPlacementIgnoreEntity)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "creativeOpenContainerForcibly", GeneralCompatConfig.creativeOpenContainerForcibly)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "creativeOneHitKill", GeneralCompatConfig.creativeOneHitKill)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "observerNoDetection", GeneralCompatConfig.observerNoDetection)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "bambooModelNoOffset", GeneralCompatConfig.bambooModelNoOffset)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "creativeNoItemCooldown", GeneralCompatConfig.creativeNoItemCooldown)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "ctrlQCraftingFix", GeneralCompatConfig.ctrlQCraftingFix)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "carpetAlwaysSetDefault", GeneralCompatConfig.carpetAlwaysSetDefault)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "placementRotationFix", GeneralCompatConfig.placementRotationFix)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "tntDoNotUpdate", GeneralCompatConfig.tntDoNotUpdate)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "totallyNoBlockUpdate", GeneralCompatConfig.totallyNoBlockUpdate)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tiscmNetworkProtocol", GeneralCompatConfig.tiscmNetworkProtocol)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetorgaddition", "hopperNoItemCost", GeneralCompatConfig.hopperNoItemCost)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "explosionNoBlockDamage", GeneralCompatConfig.explosionNoBlockDamage)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "optimizedTNTHighPriority", GeneralCompatConfig.optimizedTNTHighPriority)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "tntPrimerMomentumRemoved", GeneralCompatConfig.tntPrimerMomentumRemoved)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tntIgnoreRedstoneSignal", GeneralCompatConfig.tntIgnoreRedstoneSignal)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tntDupingFix", GeneralCompatConfig.tntDupingFix)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "interactionUpdates", GeneralCompatConfig.interactionUpdates)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "xpNoCooldown", GeneralCompatConfig.xpNoCooldown)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "powerfulExpMending", GeneralCompatConfig.powerfulExpMending)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "clientSettingsLostOnRespawnFix", GeneralCompatConfig.clientSettingsLostOnRespawnFix)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "sensibleEnderman", GeneralCompatConfig.sensibleEnderman)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "entityInstantDeathRemoval", GeneralCompatConfig.entityInstantDeathRemoval)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "farmlandTrampledDisabled", GeneralCompatConfig.farmlandTrampledDisabled)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "shulkerGolem", GeneralCompatConfig.shulkerGolem)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "preventEndSpikeRespawn", GeneralCompatConfig.preventEndSpikeRespawn)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "yeetOutOfOrderChatKick", GeneralCompatConfig.yeetOutOfOrderChatKick)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "betterCraftableBoneBlock", GeneralCompatConfig.betterCraftableBoneBlock)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "betterCraftableDispenser", GeneralCompatConfig.betterCraftableDispenser)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "viewDistance", GeneralCompatConfig.viewDistance)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tickCommandPermission", GeneralCompatConfig.normalizedTickCommandPermission())); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tickFreezeCommandToggleable", GeneralCompatConfig.tickFreezeCommandToggleable)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "syncServerMsptMetricsData", GeneralCompatConfig.syncServerMsptMetricsData)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetorgaddition", "simpleInGameCalculator", GeneralCompatConfig.simpleInGameCalculator)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "microTiming", GeneralCompatConfig.microTiming)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "fastRedstoneDust", GeneralCompatConfig.fastRedstoneDust)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "lagFreeSpawning", GeneralCompatConfig.lagFreeSpawning)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "optimizedFastEntityMovement", GeneralCompatConfig.optimizedFastEntityMovement)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "optimizedHardHitBoxEntityCollision", GeneralCompatConfig.optimizedHardHitBoxEntityCollision)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tntFuseDuration", GeneralCompatConfig.normalizedTntFuseDuration())); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "defaultLoggers", CarpetLoggerProtocol.serializeConfiguredDefaults(sanitizeDefaultLoggers(GeneralCompatConfig.defaultLoggers)))); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "language", GeneralCompatConfig.language)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "amsUpdateSuppressionCrashFix", GeneralCompatConfig.amsUpdateSuppressionCrashFix)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "yeetUpdateSuppressionCrash", GeneralCompatConfig.yeetUpdateSuppressionCrash)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "dustTrapdoorReintroduced", GeneralCompatConfig.dustTrapdoorReintroduced)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "shulkerBoxCCEReintroduced", GeneralCompatConfig.shulkerBoxCCEReintroduced)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "instantBlockUpdaterReintroduced", GeneralCompatConfig.instantBlockUpdaterReintroduced)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "commandTick", GeneralCompatConfig.commandTick)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "creativeNoClip", GeneralCompatConfig.creativeNoClip)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "optimizedDragonRespawn", GeneralCompatConfig.optimizedDragonRespawn)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "antiSpamDisabled", GeneralCompatConfig.antiSpamDisabled)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "blockPlacementIgnoreEntity", GeneralCompatConfig.blockPlacementIgnoreEntity)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "creativeOpenContainerForcibly", GeneralCompatConfig.creativeOpenContainerForcibly)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "creativeOneHitKill", GeneralCompatConfig.creativeOneHitKill)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "observerNoDetection", GeneralCompatConfig.observerNoDetection)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "bambooModelNoOffset", GeneralCompatConfig.bambooModelNoOffset)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "creativeNoItemCooldown", GeneralCompatConfig.creativeNoItemCooldown)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "ctrlQCraftingFix", GeneralCompatConfig.ctrlQCraftingFix)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "carpetAlwaysSetDefault", GeneralCompatConfig.carpetAlwaysSetDefault)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "placementRotationFix", GeneralCompatConfig.placementRotationFix)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "tntDoNotUpdate", GeneralCompatConfig.tntDoNotUpdate)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "totallyNoBlockUpdate", GeneralCompatConfig.totallyNoBlockUpdate)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tiscmNetworkProtocol", GeneralCompatConfig.tiscmNetworkProtocol)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetorgaddition", "hopperNoItemCost", GeneralCompatConfig.hopperNoItemCost)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "explosionNoBlockDamage", GeneralCompatConfig.explosionNoBlockDamage)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "optimizedTNTHighPriority", GeneralCompatConfig.optimizedTNTHighPriority)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "tntPrimerMomentumRemoved", GeneralCompatConfig.tntPrimerMomentumRemoved)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tntIgnoreRedstoneSignal", GeneralCompatConfig.tntIgnoreRedstoneSignal)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tntDupingFix", GeneralCompatConfig.tntDupingFix)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "interactionUpdates", GeneralCompatConfig.interactionUpdates)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "xpNoCooldown", GeneralCompatConfig.xpNoCooldown)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "powerfulExpMending", GeneralCompatConfig.powerfulExpMending)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "clientSettingsLostOnRespawnFix", GeneralCompatConfig.clientSettingsLostOnRespawnFix)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "sensibleEnderman", GeneralCompatConfig.sensibleEnderman)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "entityInstantDeathRemoval", GeneralCompatConfig.entityInstantDeathRemoval)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "farmlandTrampledDisabled", GeneralCompatConfig.farmlandTrampledDisabled)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "shulkerGolem", GeneralCompatConfig.shulkerGolem)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "preventEndSpikeRespawn", GeneralCompatConfig.preventEndSpikeRespawn)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "yeetOutOfOrderChatKick", GeneralCompatConfig.yeetOutOfOrderChatKick)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "betterCraftableBoneBlock", GeneralCompatConfig.betterCraftableBoneBlock)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "betterCraftableDispenser", GeneralCompatConfig.betterCraftableDispenser)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "viewDistance", GeneralCompatConfig.viewDistance)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tickCommandPermission", GeneralCompatConfig.normalizedTickCommandPermission())); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tickFreezeCommandToggleable", GeneralCompatConfig.tickFreezeCommandToggleable)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "syncServerMsptMetricsData", GeneralCompatConfig.syncServerMsptMetricsData)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetorgaddition", "simpleInGameCalculator", GeneralCompatConfig.simpleInGameCalculator)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "microTiming", GeneralCompatConfig.microTiming)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "fastRedstoneDust", GeneralCompatConfig.fastRedstoneDust)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "lagFreeSpawning", GeneralCompatConfig.lagFreeSpawning)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "optimizedFastEntityMovement", GeneralCompatConfig.optimizedFastEntityMovement)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "optimizedHardHitBoxEntityCollision", GeneralCompatConfig.optimizedHardHitBoxEntityCollision)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tntFuseDuration", GeneralCompatConfig.normalizedTntFuseDuration())); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "defaultLoggers", CarpetLoggerProtocol.serializeConfiguredDefaults(sanitizeDefaultLoggers(GeneralCompatConfig.defaultLoggers)))); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "commandBot", FakeplayerConfig.enable)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "commandPlayer", FakePlayerCompatConfig.commandPlayer)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "fakePlayerResident", FakePlayerCompatConfig.fakePlayerResident)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "openFakePlayerInventory", FakePlayerCompatConfig.openFakePlayerInventory)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "fakePlayerTicksLikeRealPlayer", FakePlayerCompatConfig.fakePlayerTicksLikeRealPlayer)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "fakePlayerDefaultSurvivalMode", FakePlayerCompatConfig.fakePlayerDefaultSurvivalMode)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "fakePlayerInteractLikeClient", FakePlayerCompatConfig.fakePlayerInteractLikeClient)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "fakePlayerAutoReplaceTool", FakePlayerCompatConfig.fakePlayerAutoReplaceTool)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "fakePlayerAutoReplenishment", FakePlayerCompatConfig.fakePlayerAutoReplenishment)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "fakePlayerAutoReplenishmentFormShulkerBox", FakePlayerCompatConfig.fakePlayerAutoReplenishmentFormShulkerBox)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "fakePlayerAutoFish", FakePlayerCompatConfig.fakePlayerAutoFish)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "fakePlayerReloadAction", FakePlayerCompatConfig.fakePlayerReloadAction)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "commandBot", FakeplayerConfig.enable)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "commandPlayer", FakePlayerCompatConfig.commandPlayer)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "fakePlayerResident", FakePlayerCompatConfig.fakePlayerResident)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "openFakePlayerInventory", FakePlayerCompatConfig.openFakePlayerInventory)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "fakePlayerTicksLikeRealPlayer", FakePlayerCompatConfig.fakePlayerTicksLikeRealPlayer)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "fakePlayerDefaultSurvivalMode", FakePlayerCompatConfig.fakePlayerDefaultSurvivalMode)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "fakePlayerInteractLikeClient", FakePlayerCompatConfig.fakePlayerInteractLikeClient)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "fakePlayerAutoReplaceTool", FakePlayerCompatConfig.fakePlayerAutoReplaceTool)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "fakePlayerAutoReplenishment", FakePlayerCompatConfig.fakePlayerAutoReplenishment)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "fakePlayerAutoReplenishmentFormShulkerBox", FakePlayerCompatConfig.fakePlayerAutoReplenishmentFormShulkerBox)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "fakePlayerAutoFish", FakePlayerCompatConfig.fakePlayerAutoFish)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "fakePlayerReloadAction", FakePlayerCompatConfig.fakePlayerReloadAction)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "hopperCounters", WoolHopperCounterConfig.hopperCounters)); - CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "hopperCountersUnlimitedSpeed", WoolHopperCounterConfig.hopperCountersUnlimitedSpeed)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "hopperCounters", WoolHopperCounterConfig.hopperCounters)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "hopperCountersUnlimitedSpeed", WoolHopperCounterConfig.hopperCountersUnlimitedSpeed)); + + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("pca", "pcaSyncProtocol", PcaSyncProtocolConfig.enabled)); + CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("pca", "pcaSyncPlayerEntity", PcaSyncProtocolConfig.syncPlayerEntity)); + } finally { + CarpetServerProtocol.CarpetRules.endBatch(); + } } } diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/experiment/GlobalEntitiesCounter.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/experiment/GlobalEntitiesCounter.java index a2c028a..e7f42b3 100644 --- a/lophine-server/src/main/java/fun/bm/lophine/config/modules/experiment/GlobalEntitiesCounter.java +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/experiment/GlobalEntitiesCounter.java @@ -1,20 +1,67 @@ package fun.bm.lophine.config.modules.experiment; +import fun.bm.lophine.enums.GlobalEntitiesCounterType; +import me.earthme.luminol.config.ConfigManager; +import me.earthme.luminol.config.ConfigsInstance; import me.earthme.luminol.config.IConfigModule; import me.earthme.luminol.config.flags.ConfigClassInfo; import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.config.flags.DoNotLoad; import me.earthme.luminol.config.flags.HotReloadUnsupported; import me.earthme.luminol.enums.EnumConfigCategory; @ConfigClassInfo(name = "global_entities_counter", category = EnumConfigCategory.EXPERIMENT) public class GlobalEntitiesCounter implements IConfigModule { @HotReloadUnsupported - @ConfigInfo(name = "enabled", comments = """ - Enable global entities counter. + @ConfigInfo(name = "version", comments = """ + DISABLED + DEFAULT_SYNC: Enable global entities counter origin version with sync counter module. + DEFAULT_ASYNC: Enable global entities counter origin version with async counter module. + PRECISE: Enable precise mob cap calculation with incremental counting. Replaces the periodic full-scan with event-driven real-time updates. + You need to set per-player-mob-spawns to false on paper-world-defaults.yml or paper-world.yml""") - public static boolean enabled = false; + public static GlobalEntitiesCounterType type = GlobalEntitiesCounterType.DISABLED; - @HotReloadUnsupported - @ConfigInfo(name = "async", comments = "Enable Asynchronous(maybe cause bugs)") - public static boolean async = false; + @DoNotLoad + private static boolean enabled; + @DoNotLoad + private static boolean async; + @DoNotLoad + private static boolean defaultModule; + + public static boolean isEnabled() { + return enabled; + } + + public static boolean isAsync() { + return async; + } + + public static boolean isDefaultModule() { + return defaultModule; + } + + @Override + public void beforeFinalLoad() { + ConfigsInstance configsInstance = ConfigManager.getConfigs("lophine"); + try { + Boolean enabled0 = configsInstance.getConfigOrigin("experiment.global_entities_counter.enabled"); + Boolean async0 = configsInstance.getConfigOrigin("experiment.global_entities_counter.async"); + if (enabled0 != null) { + if (enabled0) { + if (async0 != null && async0) { + type = GlobalEntitiesCounterType.DEFAULT_ASYNC; + } else { + type = GlobalEntitiesCounterType.DEFAULT_SYNC; + } + } + } + configsInstance.removeConfig("experiment.global_entities_counter.enabled"); + configsInstance.removeConfig("experiment.global_entities_counter.async"); + } catch (Exception ignored) { + } + enabled = type != GlobalEntitiesCounterType.DISABLED; + async = type == GlobalEntitiesCounterType.DEFAULT_ASYNC; + defaultModule = type == GlobalEntitiesCounterType.DEFAULT_SYNC || type == GlobalEntitiesCounterType.DEFAULT_ASYNC; + } } diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/fixes/VanillaLikeExperienceConfig.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/fixes/VanillaLikeExperienceConfig.java new file mode 100644 index 0000000..dcaa68b --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/fixes/VanillaLikeExperienceConfig.java @@ -0,0 +1,13 @@ +package fun.bm.lophine.config.modules.fixes; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "vanilla-like-experience") +public class VanillaLikeExperienceConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = """ + Restore a more vanilla-like technical gameplay experience by bypassing some Paper safety and behavior changes.""") + public static boolean enabled = false; +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/PcaSyncProtocolConfig.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/PcaSyncProtocolConfig.java new file mode 100644 index 0000000..b47aef6 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/PcaSyncProtocolConfig.java @@ -0,0 +1,43 @@ +package fun.bm.lophine.config.modules.function.protocol; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import fun.bm.lophine.carpet.CarpetCompatSync; +import fun.bm.lophine.enums.PcaPlayerEntityType; +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.config.flags.TransformedConfig; +import me.earthme.luminol.enums.EnumConfigCategory; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.PcaSyncProtocol; + +import java.util.Set; + +@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "pca", directory = {"protocol"}) +public class PcaSyncProtocolConfig implements IConfigModule { + private static boolean lastEnabled = false; + + @TransformedConfig(name = "pca-sync-protocol", directory = {"protocol"}) + @ConfigInfo(name = "enabled", comments = """ + Enable PCA sync protocol support""") + public static boolean enabled = false; + + @TransformedConfig(name = "pca-sync-player-entity", directory = {"protocol"}) + @ConfigInfo(name = "sync-player-entity", comments = """ + Controls which player entities can be watched through the PCA sync protocol. + NOBODY: never sync player entities + BOT: only sync Lophine fake players + OPS: sync fake players and allow operators to sync real players + OPS_AND_SELF: sync fake players, operators, and a player's own entity + EVERYONE: allow all player entities""") + public static PcaPlayerEntityType syncPlayerEntity = PcaPlayerEntityType.OPS; + + @Override + public void onLoaded(CommentedFileConfig configInstance, @Nullable Set e) { + if (lastEnabled != enabled) { + PcaSyncProtocol.onConfigModify(enabled); + lastEnabled = enabled; + } + CarpetCompatSync.apply(); + } +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/enums/GlobalEntitiesCounterType.java b/lophine-server/src/main/java/fun/bm/lophine/enums/GlobalEntitiesCounterType.java new file mode 100644 index 0000000..f0bc54f --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/enums/GlobalEntitiesCounterType.java @@ -0,0 +1,8 @@ +package fun.bm.lophine.enums; + +public enum GlobalEntitiesCounterType { + DISABLED, + DEFAULT_SYNC, + DEFAULT_ASYNC, + PRECISE +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/enums/PcaPlayerEntityType.java b/lophine-server/src/main/java/fun/bm/lophine/enums/PcaPlayerEntityType.java new file mode 100644 index 0000000..ec24b8b --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/enums/PcaPlayerEntityType.java @@ -0,0 +1,9 @@ +package fun.bm.lophine.enums; + +public enum PcaPlayerEntityType { + NOBODY, + BOT, + OPS, + OPS_AND_SELF, + EVERYONE +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/utils/EntitiesCounterUtil.java b/lophine-server/src/main/java/fun/bm/lophine/utils/EntitiesCounterUtil.java index fad87d5..0d874a7 100644 --- a/lophine-server/src/main/java/fun/bm/lophine/utils/EntitiesCounterUtil.java +++ b/lophine-server/src/main/java/fun/bm/lophine/utils/EntitiesCounterUtil.java @@ -6,6 +6,7 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.mojang.logging.LogUtils; import fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter; +import fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; @@ -24,10 +25,10 @@ import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; - -import static net.minecraft.world.level.NaturalSpawner.getRoughBiome; +import java.util.concurrent.atomic.AtomicInteger; public class EntitiesCounterUtil { + // global entities counter private static final Map>> globalLoadedEntities = new ConcurrentHashMap<>(); private static final Map> mobsMap = Collections.synchronizedMap(new WeakHashMap<>()); private static final Map>> mobsAreaMap = new ConcurrentHashMap<>(); @@ -35,6 +36,9 @@ public class EntitiesCounterUtil { private static final Map> tasks = new ConcurrentHashMap<>(); private static final Set UniqueIds = ConcurrentHashMap.newKeySet(); + // precise mob counter extra + private static final Map> globalMobCounts = new ConcurrentHashMap<>(); + private static int lastUsedId = 0; private static final int CLEANUP_INTERVAL = 200; // each 200 ids used @@ -104,94 +108,193 @@ public class EntitiesCounterUtil { } public static void tick(ServerLevel level) { - Runnable task = () -> { - try { - Cache> data0 = globalLoadedEntities.get(level); - if (data0 == null) return; + if (GlobalEntitiesCounter.isDefaultModule()) { + Runnable task = () -> { + try { + Cache> data0 = globalLoadedEntities.get(level); + if (data0 == null) return; - Object2IntOpenHashMap map = new Object2IntOpenHashMap<>(); - Collection> snapshot = data0.asMap().values(); + Object2IntOpenHashMap map = new Object2IntOpenHashMap<>(); + Collection> snapshot = data0.asMap().values(); - for (ReferenceList data : snapshot) { - if (data == null) continue; - for (Entity entity : GlobalEntitiesCounter.async ? data.copy() : data) { - if (entity == null || entity.isRemoved() || !entity.isAlive()) continue; - // Lophine start - Copy from net/minecraft/world/level/NaturalSpawner - MobCategory category = entity.getType().getCategory(); - if (category != MobCategory.MISC) { - // Paper start - Only count natural spawns - if (!entity.level().paperConfig().entities.spawning.countAllMobsForSpawning && - !(entity.spawnReason == CreatureSpawnEvent.SpawnReason.NATURAL || - entity.spawnReason == CreatureSpawnEvent.SpawnReason.CHUNK_GEN)) { - continue; + for (ReferenceList data : snapshot) { + if (data == null) continue; + for (Entity entity : GlobalEntitiesCounter.isAsync() ? data.copy() : data) { + if (entity == null || entity.isRemoved() || !entity.isAlive()) continue; + // Lophine start - Copy from net/minecraft/world/level/NaturalSpawner + MobCategory category = entity.getType().getCategory(); + if (category != MobCategory.MISC) { + // Paper start - Only count natural spawns + if (!entity.level().paperConfig().entities.spawning.countAllMobsForSpawning && + !(entity.spawnReason == CreatureSpawnEvent.SpawnReason.NATURAL || + entity.spawnReason == CreatureSpawnEvent.SpawnReason.CHUNK_GEN)) { + continue; + } + // Paper end - Only count natural spawns + map.addTo(category, 1); } - // Paper end - Only count natural spawns - map.addTo(category, 1); } + // Lophine end - Copy from net/minecraft/world/level/NaturalSpawner } - // Lophine end - Copy from net/minecraft/world/level/NaturalSpawner - } - mobsMap.put(level, map); + mobsMap.put(level, map); - Cache> collection = mobsAreaMap.get(level); - if (collection != null) { - int count = 0; - for (PositionCountingAreaMap areaMap : collection.asMap().values()) { - if (areaMap != null) { - count += areaMap.getTotalPositions(); + Cache> collection = mobsAreaMap.get(level); + if (collection != null) { + int count = 0; + for (PositionCountingAreaMap areaMap : collection.asMap().values()) { + if (areaMap != null) { + count += areaMap.getTotalPositions(); + } } + spawnableChunkCount.put(level, count); } - spawnableChunkCount.put(level, count); + } catch (Exception e) { + LogUtils.getClassLogger().error("Failed to run task", e); } - } catch (Exception e) { - LogUtils.getClassLogger().error("Failed to run task", e); + }; + if (GlobalEntitiesCounter.isAsync()) { + tasks.put(level, CompletableFuture.runAsync(task).exceptionally(ex -> { + LogUtils.getClassLogger().error("Failed to run task", ex); + return null; + })); + } else { + task.run(); } - }; - if (GlobalEntitiesCounter.async) { - tasks.put(level, CompletableFuture.runAsync(task).exceptionally(ex -> { - LogUtils.getClassLogger().error("Failed to run task", ex); - return null; - })); } else { - task.run(); + Cache> cache = mobsAreaMap.get(level); + if (cache == null) return; + + int total = 0; + for (PositionCountingAreaMap map : cache.asMap().values()) { + if (map != null) { + total += map.getTotalPositions(); + } + } + + spawnableChunkCount.put(level, total); } } - public static NaturalSpawner.SpawnState runRemainingTasks( + public static NaturalSpawner.SpawnState createSpawnState( ServerLevel level, Iterable entities, NaturalSpawner.ChunkGetter chunkGetter, LocalMobCapCalculator calculator, final boolean countMobs ) { - Object2IntOpenHashMap map = getMobsMap(level); - if (map == null) return null; // skip if no data - // Lophine start - Copy from net/minecraft/world/level/NaturalSpawner - PotentialCalculator potentialCalculator = new PotentialCalculator(); - for (Entity entity : entities) { - if (entity == null || entity.isRemoved() || !entity.isAlive()) continue; - // Paper start - Only count natural spawns - if (!entity.level().paperConfig().entities.spawning.countAllMobsForSpawning && - !(entity.spawnReason == CreatureSpawnEvent.SpawnReason.NATURAL || - entity.spawnReason == CreatureSpawnEvent.SpawnReason.CHUNK_GEN)) { - continue; + if (GlobalEntitiesCounter.isDefaultModule()) { + Object2IntOpenHashMap map = getMobsMap(level); + if (map == null) return null; // skip if no data + // Lophine start - Copy from net/minecraft/world/level/NaturalSpawner + PotentialCalculator potentialCalculator = new PotentialCalculator(); + for (Entity entity : entities) { + if (entity == null || entity.isRemoved() || !entity.isAlive()) continue; + // Paper start - Only count natural spawns + if (!entity.level().paperConfig().entities.spawning.countAllMobsForSpawning && + !(entity.spawnReason == CreatureSpawnEvent.SpawnReason.NATURAL || + entity.spawnReason == CreatureSpawnEvent.SpawnReason.CHUNK_GEN)) { + continue; + } + // Paper end - Only count natural spawns + BlockPos blockPos = entity.blockPosition(); + chunkGetter.query(ChunkPos.pack(blockPos), chunk -> { + MobSpawnSettings.MobSpawnCost mobSpawnCost = NaturalSpawner.getRoughBiome(blockPos, chunk).getMobSettings().getMobSpawnCost(entity.getType()); + if (mobSpawnCost != null) { + potentialCalculator.addCharge(entity.blockPosition(), mobSpawnCost.charge()); + } + + if (calculator != null && entity instanceof Mob) { // Paper - Optional per player mob spawns + calculator.addMob(chunk.getPos(), entity.getType().getCategory()); + } + + // Paper start - Optional per player mob spawns + if (countMobs) { + chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity); + } + // Paper end - Optional per player mob spawns + }); } - // Paper end - Only count natural spawns - BlockPos blockPos = entity.blockPosition(); - chunkGetter.query(ChunkPos.pack(blockPos), chunk -> { - MobSpawnSettings.MobSpawnCost mobSpawnCost = getRoughBiome(blockPos, chunk).getMobSettings().getMobSpawnCost(entity.getType()); - if (mobSpawnCost != null) { - potentialCalculator.addCharge(entity.blockPosition(), mobSpawnCost.charge()); - } + return new NaturalSpawner.SpawnState(getTotalChunkCount(level), map, potentialCalculator, calculator); + // Lophine end - Copy from net/minecraft/world/level/NaturalSpawner + } else { + int chunkCount = getSpawnableChunkCount(level); + if (chunkCount == 0) return null; - if (calculator != null && entity instanceof Mob) { // Paper - Optional per player mob spawns - calculator.addMob(chunk.getPos(), entity.getType().getCategory()); - } + Object2IntOpenHashMap globalCounts = getGlobalCounts(level); - // Paper start - Optional per player mob spawns - if (countMobs) { - chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity); - } - // Paper end - Optional per player mob spawns - }); + PotentialCalculator potentialCalculator = new PotentialCalculator(); + for (Entity entity : entities) { + if (entity == null || entity.isRemoved() || !entity.isAlive()) continue; + if (!shouldCount(entity)) continue; + + BlockPos pos = entity.blockPosition(); + chunkGetter.query(ChunkPos.pack(pos), chunk -> { + MobSpawnSettings.MobSpawnCost mobSpawnCost = NaturalSpawner.getRoughBiome(pos, chunk).getMobSettings().getMobSpawnCost(entity.getType()); + if (mobSpawnCost != null) { + potentialCalculator.addCharge(entity.blockPosition(), mobSpawnCost.charge()); + } + + if (calculator != null && entity instanceof Mob) { + calculator.addMob(chunk.getPos(), entity.getType().getCategory()); + } + }); + } + + return new NaturalSpawner.SpawnState(chunkCount, globalCounts, potentialCalculator, calculator); + } + } + + public static Object2IntOpenHashMap getGlobalCounts(ServerLevel level) { + EnumMap counts = globalMobCounts.get(level); + Object2IntOpenHashMap result = new Object2IntOpenHashMap<>(); + if (counts != null) { + for (Map.Entry entry : counts.entrySet()) { + result.put(entry.getKey(), entry.getValue().get()); + } + } + return result; + } + + public static int getSpawnableChunkCount(ServerLevel level) { + Integer value = spawnableChunkCount.get(level); + return value != null ? value : 0; + } + + public static void onWorldUnload(ServerLevel level) { + globalMobCounts.remove(level); + mobsAreaMap.remove(level); + spawnableChunkCount.remove(level); + } + + public static void onEntityTrackingStart(ServerLevel level, Entity entity) { + if (!shouldCount(entity)) return; + MobCategory category = entity.getType().getCategory(); + getOrCreateCounts(level).computeIfAbsent(category, k -> new AtomicInteger(0)).incrementAndGet(); + } + + public static boolean shouldCount(Entity entity) { + MobCategory category = entity.getType().getCategory(); + if (category == MobCategory.MISC) return false; + + if (entity instanceof Mob mob && (mob.isPersistenceRequired() || mob.requiresCustomPersistence())) { + return false; + } + + if (VanillaLikeExperienceConfig.enabled || entity.level().paperConfig().entities.spawning.countAllMobsForSpawning) { + return true; + } + + return entity.spawnReason == CreatureSpawnEvent.SpawnReason.NATURAL + || entity.spawnReason == CreatureSpawnEvent.SpawnReason.CHUNK_GEN; + } + + private static EnumMap getOrCreateCounts(ServerLevel level) { + return globalMobCounts.computeIfAbsent(level, _ -> new EnumMap<>(MobCategory.class)); + } + + public static void onEntityTrackingEnd(ServerLevel level, Entity entity) { + if (!shouldCount(entity)) return; + MobCategory category = entity.getType().getCategory(); + EnumMap counts = getOrCreateCounts(level); + AtomicInteger counter = counts.get(category); + if (counter != null) { + counter.updateAndGet(v -> Math.max(0, v - 1)); } - return new NaturalSpawner.SpawnState(getTotalChunkCount(level), map, potentialCalculator, calculator); - // Lophine end - Copy from net/minecraft/world/level/NaturalSpawner } } diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/bot/BotList.java b/lophine-server/src/main/java/org/leavesmc/leaves/bot/BotList.java index 6a11bf2..6adfc97 100644 --- a/lophine-server/src/main/java/org/leavesmc/leaves/bot/BotList.java +++ b/lophine-server/src/main/java/org/leavesmc/leaves/bot/BotList.java @@ -315,11 +315,13 @@ public class BotList { bot.unRide(); for (ThrownEnderpearl thrownEnderpearl : bot.getEnderPearls()) { - if (!thrownEnderpearl.level().paperConfig().misc.legacyEnderPearlBehavior) { - thrownEnderpearl.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER, EntityRemoveEvent.Cause.PLAYER_QUIT); - } else { - thrownEnderpearl.setOwner(null); - } + // Lophine start - restore vanilla ender pearl loading on Folia + thrownEnderpearl.getBukkitEntity().taskScheduler.scheduleOrExecute((Entity pearl) -> { + if (!pearl.isRemoved()) { + pearl.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER, EntityRemoveEvent.Cause.PLAYER_QUIT); + } + }); + // Lophine end - restore vanilla ender pearl loading on Folia } bot.level().getCurrentWorldData().connections.remove(bot.connection.connection); diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/CarpetServerProtocol.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/CarpetServerProtocol.java index 43c1f50..151d1a2 100644 --- a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/CarpetServerProtocol.java +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/CarpetServerProtocol.java @@ -6,18 +6,22 @@ import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.plugin.MinecraftInternalPlugin; import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; import org.leavesmc.leaves.protocol.core.LeavesProtocol; import org.leavesmc.leaves.protocol.core.ProtocolHandler; import org.leavesmc.leaves.protocol.core.ProtocolUtils; import org.slf4j.Logger; -import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; @LeavesProtocol.Register(namespace = "carpet") public class CarpetServerProtocol implements LeavesProtocol { @@ -28,6 +32,9 @@ public class CarpetServerProtocol implements LeavesProtocol { private static final String HI = "69"; private static final String HELLO = "420"; + private static final Set activePlayers = ConcurrentHashMap.newKeySet(); + private static boolean batchingRules = false; + private static boolean rulesDirty = false; @Contract("_ -> new") public static Identifier id(String path) { @@ -44,21 +51,68 @@ public class CarpetServerProtocol implements LeavesProtocol { @ProtocolHandler.PayloadReceiver(payload = CarpetPayload.class) private static void handleHello(@NotNull ServerPlayer player, @NotNull CarpetServerProtocol.CarpetPayload payload) { if (payload.nbt.contains(HELLO)) { - LOGGER.info("Player {} joined with carpet {}", player.getScoreboardName(), payload.nbt.getString(HELLO).orElse("Unknown")); - CompoundTag data = new CompoundTag(); - CarpetRules.write(data); - ProtocolUtils.sendPayloadPacket(player, new CarpetPayload(data)); + UUID playerId = player.getUUID(); + String carpetVersion = payload.nbt.getString(HELLO).orElse("Unknown"); + player.getBukkitEntity().getScheduler().execute(MinecraftInternalPlugin.INSTANCE, () -> { + ServerPlayer onlinePlayer = MinecraftServer.getServer().getPlayerList().getPlayer(playerId); + if (onlinePlayer == null) { + return; + } + + LOGGER.info("Player {} joined with carpet {}", onlinePlayer.getScoreboardName(), carpetVersion); + sendServerData(onlinePlayer); + activePlayers.add(playerId); + }, null, 1L); } } + @ProtocolHandler.PlayerLeave + public static void onPlayerLeave(ServerPlayer player) { + activePlayers.remove(player.getUUID()); + } + @Override public boolean isActive() { return CarpetRules.hasRules(); } + private static void sendServerData(ServerPlayer player) { + sendServerData(player.getUUID()); + } + + private static void sendServerData(UUID playerId) { + ServerPlayer player = MinecraftServer.getServer().getPlayerList().getPlayer(playerId); + if (player == null) { + activePlayers.remove(playerId); + return; + } + + CompoundTag data = new CompoundTag(); + CarpetRules.write(data); + player.getBukkitEntity().getScheduler().execute(MinecraftInternalPlugin.INSTANCE, () -> { + ServerPlayer onlinePlayer = MinecraftServer.getServer().getPlayerList().getPlayer(playerId); + if (onlinePlayer != null) { + ProtocolUtils.sendPayloadPacket(onlinePlayer, new CarpetPayload(data)); + } + }, null, 1L); + } + public static class CarpetRules { - private static final Map rules = new HashMap<>(); + private static final Map rules = new ConcurrentHashMap<>(); + + public static void beginBatch() { + batchingRules = true; + rulesDirty = false; + } + + public static void endBatch() { + batchingRules = false; + if (rulesDirty) { + activePlayers.forEach(CarpetServerProtocol::sendServerData); + rulesDirty = false; + } + } public static void write(@NotNull CompoundTag tag) { CompoundTag rulesNbt = new CompoundTag(); @@ -69,15 +123,25 @@ public class CarpetServerProtocol implements LeavesProtocol { public static void register(CarpetRule rule) { rules.put(rule.name, rule); + markDirty(); } public static void clear() { rules.clear(); + markDirty(); } public static boolean hasRules() { return !rules.isEmpty(); } + + private static void markDirty() { + if (batchingRules) { + rulesDirty = true; + } else { + activePlayers.forEach(CarpetServerProtocol::sendServerData); + } + } } public record CarpetRule(String identifier, String name, String value) { diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/PcaSyncProtocol.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/PcaSyncProtocol.java new file mode 100644 index 0000000..305b1a3 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/PcaSyncProtocol.java @@ -0,0 +1,483 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol; + +import ca.spottedleaf.moonrise.common.util.TickThread; +import fun.bm.lophine.config.modules.function.protocol.PcaSyncProtocolConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.ChestBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.ChestType; +import net.minecraft.world.level.chunk.LevelChunk; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.bukkit.Bukkit; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.plugin.MinecraftInternalPlugin; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.util.TagUtil; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@LeavesProtocol.Register(namespace = "pca") +public class PcaSyncProtocol implements LeavesProtocol { + public static final String PROTOCOL_ID = "pca"; + + private static final Identifier ENABLE_PCA_SYNC_PROTOCOL = id("enable_pca_sync_protocol"); + private static final Identifier DISABLE_PCA_SYNC_PROTOCOL = id("disable_pca_sync_protocol"); + + private static final ConcurrentMap> playerWatchBlockPos = new ConcurrentHashMap<>(); + private static final ConcurrentMap> playerWatchEntity = new ConcurrentHashMap<>(); + private static final ConcurrentMap, Set> blockPosWatchPlayerSet = new ConcurrentHashMap<>(); + private static final ConcurrentMap, Set> entityWatchPlayerSet = new ConcurrentHashMap<>(); + + @Contract("_ -> new") + public static Identifier id(String path) { + return Identifier.fromNamespaceAndPath(PROTOCOL_ID, path); + } + + @ProtocolHandler.PlayerJoin + private static void onJoin(ServerPlayer player) { + if (PcaSyncProtocolConfig.enabled) { + enablePcaSyncProtocol(player); + } + } + + @ProtocolHandler.PlayerLeave + private static void onLeave(ServerPlayer player) { + clearPlayerWatchData(player); + } + + @ProtocolHandler.BytebufReceiver(key = "cancel_sync_block_entity") + private static void cancelSyncBlockEntityHandler(ServerPlayer player, FriendlyByteBuf buf) { + clearPlayerWatchBlock(player); + } + + @ProtocolHandler.BytebufReceiver(key = "cancel_sync_entity") + private static void cancelSyncEntityHandler(ServerPlayer player, FriendlyByteBuf buf) { + clearPlayerWatchEntity(player); + } + + @ProtocolHandler.PayloadReceiver(payload = SyncBlockEntityPayload.class) + private static void syncBlockEntityHandler(ServerPlayer player, SyncBlockEntityPayload payload) { + if (!PcaSyncProtocolConfig.enabled) { + return; + } + + UUID playerId = player.getUUID(); + player.getBukkitEntity().getScheduler().execute(MinecraftInternalPlugin.INSTANCE, () -> { + ServerPlayer onlinePlayer = MinecraftServer.getServer().getPlayerList().getPlayer(playerId); + if (!PcaSyncProtocolConfig.enabled || onlinePlayer == null || onlinePlayer.hasDisconnected() || !TickThread.isTickThreadFor(onlinePlayer)) { + return; + } + + ServerLevel world = onlinePlayer.level(); + BlockPos pos = payload.pos; + Bukkit.getRegionScheduler().execute(MinecraftInternalPlugin.INSTANCE, world.getWorld(), pos.getX() >> 4, pos.getZ() >> 4, () -> { + if (!PcaSyncProtocolConfig.enabled || MinecraftServer.getServer().getPlayerList().getPlayer(playerId) == null || !TickThread.isTickThreadFor(world, pos)) { + return; + } + + BlockState blockState = world.getBlockState(pos); + clearPlayerWatchData(playerId); + + BlockEntity blockEntityAdj = null; + if (blockState.getBlock() instanceof ChestBlock && blockState.getValue(ChestBlock.TYPE) != ChestType.SINGLE) { + BlockPos posAdj = pos.relative(ChestBlock.getConnectedDirection(blockState)); + if (TickThread.isTickThreadFor(world, posAdj)) { + blockEntityAdj = getLoadedBlockEntity(world, posAdj); + } else { + scheduleBlockEntityUpdate(playerId, world, posAdj); + } + } + + if (blockEntityAdj != null) { + updateBlockEntity(playerId, blockEntityAdj); + } + + BlockEntity blockEntity = getLoadedBlockEntity(world, pos); + if (blockEntity != null) { + updateBlockEntity(playerId, blockEntity); + } + + Pair pair = new ImmutablePair<>(world.dimension().identifier(), pos); + playerWatchBlockPos.put(playerId, pair); + blockPosWatchPlayerSet.computeIfAbsent(pair, ignored -> ConcurrentHashMap.newKeySet()).add(playerId); + }); + }, null, 1L); + } + + @ProtocolHandler.PayloadReceiver(payload = SyncEntityPayload.class) + private static void syncEntityHandler(ServerPlayer player, SyncEntityPayload payload) { + if (!PcaSyncProtocolConfig.enabled) { + return; + } + + UUID playerId = player.getUUID(); + player.getBukkitEntity().getScheduler().execute(MinecraftInternalPlugin.INSTANCE, () -> { + ServerPlayer onlinePlayer = MinecraftServer.getServer().getPlayerList().getPlayer(playerId); + if (!PcaSyncProtocolConfig.enabled || onlinePlayer == null || onlinePlayer.hasDisconnected() || !TickThread.isTickThreadFor(onlinePlayer)) { + return; + } + + ServerLevel world = onlinePlayer.level(); + Entity entity = world.getEntity(payload.entityId); + if (entity == null) { + return; + } + + boolean viewerIsOp = MinecraftServer.getServer().getPlayerList().isOp(onlinePlayer.nameAndId()); + if (TickThread.isTickThreadFor(entity)) { + syncEntityForWatcher(playerId, viewerIsOp, entity); + } else { + entity.getBukkitEntity().getScheduler().execute(MinecraftInternalPlugin.INSTANCE, () -> syncEntityForWatcher(playerId, viewerIsOp, entity), null, 1L); + } + }, null, 1L); + } + + public static void onConfigModify(boolean enable) { + if (enable) { + enablePcaSyncProtocolGlobal(); + } else { + disablePcaSyncProtocolGlobal(); + } + } + + public static void enablePcaSyncProtocol(@NotNull ServerPlayer player) { + sendEmptyPacket(player.getUUID(), ENABLE_PCA_SYNC_PROTOCOL); + } + + public static void disablePcaSyncProtocol(@NotNull ServerPlayer player) { + sendEmptyPacket(player.getUUID(), DISABLE_PCA_SYNC_PROTOCOL); + } + + public static void updateEntity(@NotNull ServerPlayer player, @NotNull Entity entity) { + updateEntity(player.getUUID(), entity); + } + + private static void updateEntity(@NotNull UUID playerId, @NotNull Entity entity) { + if (!TickThread.isTickThreadFor(entity) || entity.isRemoved()) { + return; + } + CompoundTag nbt = TagUtil.saveEntity(entity); + sendPayloadPacket(playerId, new UpdateEntityPayload(entity.level().dimension().identifier(), entity.getId(), nbt)); + } + + public static void updateBlockEntity(@NotNull ServerPlayer player, @NotNull BlockEntity blockEntity) { + updateBlockEntity(player.getUUID(), blockEntity); + } + + private static void updateBlockEntity(@NotNull UUID playerId, @NotNull BlockEntity blockEntity) { + Level world = blockEntity.getLevel(); + if (world == null || world.isClientSide() || !TickThread.isTickThreadFor(world, blockEntity.getBlockPos())) { + return; + } + + sendPayloadPacket(playerId, new UpdateBlockEntityPayload(world.dimension().identifier(), blockEntity.getBlockPos(), blockEntity.saveWithoutMetadata(world.registryAccess()))); + } + + public static boolean syncEntityToClient(@NotNull Entity entity) { + if (!PcaSyncProtocolConfig.enabled || entity.level().isClientSide() || !TickThread.isTickThreadFor(entity) || entity.isRemoved()) { + return false; + } + + Pair pair = new ImmutablePair<>(entity.level().dimension().identifier(), entity.getUUID()); + Set playerIds = entityWatchPlayerSet.get(pair); + if (playerIds == null || playerIds.isEmpty()) { + return false; + } + + ArrayList stalePlayers = new ArrayList<>(); + boolean ret = false; + for (UUID playerId : playerIds) { + if (MinecraftServer.getServer().getPlayerList().getPlayer(playerId) == null) { + stalePlayers.add(playerId); + continue; + } + updateEntity(playerId, entity); + ret = true; + } + stalePlayers.forEach(playerIds::remove); + if (playerIds.isEmpty()) { + entityWatchPlayerSet.remove(pair, playerIds); + } + return ret; + } + + public static boolean syncBlockEntityToClient(@NotNull BlockEntity blockEntity) { + Level world = blockEntity.getLevel(); + if (!PcaSyncProtocolConfig.enabled || world == null || world.isClientSide() || !TickThread.isTickThreadFor(world, blockEntity.getBlockPos())) { + return false; + } + + BlockPos pos = blockEntity.getBlockPos(); + BlockState blockState = world.getBlockState(pos); + Set playerIds = collectWatchers(world, pos); + + if (blockState.getBlock() instanceof ChestBlock && blockState.getValue(ChestBlock.TYPE) != ChestType.SINGLE) { + BlockPos posAdj = pos.relative(ChestBlock.getConnectedDirection(blockState)); + playerIds.addAll(collectWatchers(world, posAdj)); + } + + if (playerIds.isEmpty()) { + return false; + } + + boolean ret = false; + for (UUID playerId : playerIds) { + if (MinecraftServer.getServer().getPlayerList().getPlayer(playerId) == null) { + removePlayerFromWatchers(playerId); + continue; + } + updateBlockEntity(playerId, blockEntity); + ret = true; + } + return ret; + } + + public static void disablePcaSyncProtocolGlobal() { + playerWatchBlockPos.clear(); + playerWatchEntity.clear(); + blockPosWatchPlayerSet.clear(); + entityWatchPlayerSet.clear(); + for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) { + disablePcaSyncProtocol(player); + } + } + + public static void enablePcaSyncProtocolGlobal() { + for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) { + enablePcaSyncProtocol(player); + } + } + + public static void clearPlayerWatchData(ServerPlayer player) { + clearPlayerWatchData(player.getUUID()); + } + + private static void clearPlayerWatchData(UUID playerId) { + clearPlayerWatchBlock(playerId); + clearPlayerWatchEntity(playerId); + } + + private static void clearPlayerWatchEntity(ServerPlayer player) { + clearPlayerWatchEntity(player.getUUID()); + } + + private static void clearPlayerWatchEntity(UUID playerId) { + Pair pair = playerWatchEntity.remove(playerId); + if (pair != null) { + removeFromSet(entityWatchPlayerSet, pair, playerId); + } + } + + private static void clearPlayerWatchBlock(ServerPlayer player) { + clearPlayerWatchBlock(player.getUUID()); + } + + private static void clearPlayerWatchBlock(UUID playerId) { + Pair pair = playerWatchBlockPos.remove(playerId); + if (pair != null) { + removeFromSet(blockPosWatchPlayerSet, pair, playerId); + } + } + + private static void removePlayerFromWatchers(UUID playerId) { + Pair blockPair = playerWatchBlockPos.remove(playerId); + if (blockPair != null) { + removeFromSet(blockPosWatchPlayerSet, blockPair, playerId); + } + + Pair entityPair = playerWatchEntity.remove(playerId); + if (entityPair != null) { + removeFromSet(entityWatchPlayerSet, entityPair, playerId); + } + } + + private static void removeFromSet(ConcurrentMap> map, K key, UUID playerId) { + Set playerSet = map.get(key); + if (playerSet != null) { + playerSet.remove(playerId); + if (playerSet.isEmpty()) { + map.remove(key, playerSet); + } + } + } + + private static Set collectWatchers(@NotNull Level world, @NotNull BlockPos blockPos) { + Set playerIds = blockPosWatchPlayerSet.get(new ImmutablePair<>(world.dimension().identifier(), blockPos)); + return playerIds == null ? new HashSet<>() : new HashSet<>(playerIds); + } + + private static @org.jetbrains.annotations.Nullable BlockEntity getLoadedBlockEntity(ServerLevel world, BlockPos pos) { + LevelChunk chunk = world.getChunkIfLoaded(pos); + return chunk == null ? null : chunk.getBlockEntity(pos); + } + + private static void scheduleBlockEntityUpdate(UUID playerId, ServerLevel world, BlockPos pos) { + Bukkit.getRegionScheduler().execute(MinecraftInternalPlugin.INSTANCE, world.getWorld(), pos.getX() >> 4, pos.getZ() >> 4, () -> { + if (!PcaSyncProtocolConfig.enabled || MinecraftServer.getServer().getPlayerList().getPlayer(playerId) == null || !TickThread.isTickThreadFor(world, pos)) { + return; + } + + BlockEntity blockEntity = getLoadedBlockEntity(world, pos); + if (blockEntity != null) { + updateBlockEntity(playerId, blockEntity); + } + }); + } + + private static void syncEntityForWatcher(UUID playerId, boolean viewerIsOp, Entity entity) { + if (!PcaSyncProtocolConfig.enabled || MinecraftServer.getServer().getPlayerList().getPlayer(playerId) == null || !TickThread.isTickThreadFor(entity) || entity.isRemoved()) { + return; + } + + if (entity instanceof Player && !maySyncPlayerEntity(playerId, viewerIsOp, entity)) { + return; + } + + clearPlayerWatchData(playerId); + updateEntity(playerId, entity); + + Pair pair = new ImmutablePair<>(entity.level().dimension().identifier(), entity.getUUID()); + playerWatchEntity.put(playerId, pair); + entityWatchPlayerSet.computeIfAbsent(pair, ignored -> ConcurrentHashMap.newKeySet()).add(playerId); + } + + private static boolean maySyncPlayerEntity(UUID viewerId, boolean viewerIsOp, Entity entity) { + return switch (PcaSyncProtocolConfig.syncPlayerEntity) { + case NOBODY -> false; + case BOT -> entity instanceof ServerBot; + case OPS -> entity instanceof ServerBot || viewerIsOp; + case OPS_AND_SELF -> entity instanceof ServerBot || viewerIsOp || entity.getUUID().equals(viewerId); + case EVERYONE -> true; + }; + } + + private static void sendEmptyPacket(UUID playerId, Identifier id) { + ServerPlayer player = MinecraftServer.getServer().getPlayerList().getPlayer(playerId); + if (player == null) { + removePlayerFromWatchers(playerId); + return; + } + + player.getBukkitEntity().getScheduler().execute(MinecraftInternalPlugin.INSTANCE, () -> { + ServerPlayer onlinePlayer = MinecraftServer.getServer().getPlayerList().getPlayer(playerId); + if (onlinePlayer != null) { + ProtocolUtils.sendEmptyPacket(onlinePlayer, id); + } + }, null, 1L); + } + + private static void sendPayloadPacket(UUID playerId, CustomPacketPayload payload) { + ServerPlayer player = MinecraftServer.getServer().getPlayerList().getPlayer(playerId); + if (player == null) { + removePlayerFromWatchers(playerId); + return; + } + + player.getBukkitEntity().getScheduler().execute(MinecraftInternalPlugin.INSTANCE, () -> { + ServerPlayer onlinePlayer = MinecraftServer.getServer().getPlayerList().getPlayer(playerId); + if (onlinePlayer != null) { + ProtocolUtils.sendPayloadPacket(onlinePlayer, payload); + } + }, null, 1L); + } + + @Override + public boolean isActive() { + return PcaSyncProtocolConfig.enabled; + } + + public record UpdateEntityPayload(Identifier dimension, int entityId, + CompoundTag tag) implements LeavesCustomPayload { + @ID + public static final Identifier UPDATE_ENTITY = PcaSyncProtocol.id("update_entity"); + + @Codec + public static final StreamCodec CODEC = StreamCodec.composite( + Identifier.STREAM_CODEC, + UpdateEntityPayload::dimension, + ByteBufCodecs.INT, + UpdateEntityPayload::entityId, + ByteBufCodecs.COMPOUND_TAG, + UpdateEntityPayload::tag, + UpdateEntityPayload::new + ); + } + + public record UpdateBlockEntityPayload(Identifier dimension, BlockPos blockPos, + CompoundTag tag) implements LeavesCustomPayload { + @ID + private static final Identifier UPDATE_BLOCK_ENTITY = PcaSyncProtocol.id("update_block_entity"); + + @Codec + private static final StreamCodec CODEC = StreamCodec.composite( + Identifier.STREAM_CODEC, + UpdateBlockEntityPayload::dimension, + BlockPos.STREAM_CODEC, + UpdateBlockEntityPayload::blockPos, + ByteBufCodecs.COMPOUND_TAG, + UpdateBlockEntityPayload::tag, + UpdateBlockEntityPayload::new + ); + } + + public record SyncBlockEntityPayload(BlockPos pos) implements LeavesCustomPayload { + @ID + public static final Identifier SYNC_BLOCK_ENTITY = PcaSyncProtocol.id("sync_block_entity"); + + @Codec + private static final StreamCodec CODEC = StreamCodec.composite( + BlockPos.STREAM_CODEC, SyncBlockEntityPayload::pos, SyncBlockEntityPayload::new + ); + } + + public record SyncEntityPayload(int entityId) implements LeavesCustomPayload { + @ID + public static final Identifier SYNC_ENTITY = PcaSyncProtocol.id("sync_entity"); + + @Codec + private static final StreamCodec CODEC = StreamCodec.composite( + ByteBufCodecs.INT, SyncEntityPayload::entityId, SyncEntityPayload::new + ); + } +}